mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-13 00:56:25 -04:00
Merge branch 'master' into master
This commit is contained in:
commit
2381d0f554
25
.github/workflows/ci.yml
vendored
25
.github/workflows/ci.yml
vendored
|
|
@ -53,8 +53,16 @@ jobs:
|
|||
- 2.38.2 # first version that supports the rebase.updateRefs config
|
||||
- 2.44.0
|
||||
- latest # We rely on github to have the latest version installed on their VMs
|
||||
race:
|
||||
- false
|
||||
# Additionally run the whole suite once under the race detector. Data
|
||||
# races live in lazygit's own Go code rather than in git, so a single
|
||||
# git version is enough; use the latest to skip the git-build steps.
|
||||
include:
|
||||
- git-version: latest
|
||||
race: true
|
||||
runs-on: ubuntu-latest
|
||||
name: "Integration Tests - git ${{matrix.git-version}}"
|
||||
name: "Integration Tests - git ${{matrix.git-version}}${{ matrix.race && ' (race)' || '' }}"
|
||||
env:
|
||||
GOFLAGS: -mod=vendor
|
||||
steps:
|
||||
|
|
@ -92,12 +100,23 @@ jobs:
|
|||
run: git --version
|
||||
- name: Test code
|
||||
env:
|
||||
# See https://go.dev/blog/integration-test-coverage
|
||||
LAZYGIT_GOCOVERDIR: /tmp/code_coverage
|
||||
# See https://go.dev/blog/integration-test-coverage. The race variant
|
||||
# skips coverage: it's redundant with the non-race latest job and
|
||||
# would only slow the -race build down further. Leaving the dir unset
|
||||
# makes run_integration_tests.sh take its non-coverage path.
|
||||
LAZYGIT_GOCOVERDIR: ${{ !matrix.race && '/tmp/code_coverage' || '' }}
|
||||
# Only set for the race variant. The race detector needs cgo; it's on
|
||||
# by default on the Linux runner, but we set it explicitly to be safe.
|
||||
LAZYGIT_RACE_DETECTOR: ${{ matrix.race && '1' || '' }}
|
||||
CGO_ENABLED: ${{ matrix.race && '1' || '' }}
|
||||
# Append each test's duration to this file; run_integration_tests.sh
|
||||
# prints the slowest at the end, to spot slow/anomalous tests.
|
||||
LAZYGIT_TEST_TIMING: /tmp/test_timings.txt
|
||||
run: |
|
||||
mkdir -p /tmp/code_coverage
|
||||
./scripts/run_integration_tests.sh
|
||||
- name: Upload code coverage artifacts
|
||||
if: ${{ !matrix.race }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: coverage-integration-${{ matrix.git-version }}-${{ github.run_id }}
|
||||
|
|
|
|||
18
.github/workflows/release.yml
vendored
18
.github/workflows/release.yml
vendored
|
|
@ -13,10 +13,15 @@ on:
|
|||
description: 'Version bump type'
|
||||
type: choice
|
||||
required: true
|
||||
default: 'patch'
|
||||
default: 'minor (normal)'
|
||||
options:
|
||||
- minor
|
||||
- patch
|
||||
- minor (normal)
|
||||
- patch (hotfix)
|
||||
branch:
|
||||
description: 'Branch to release from'
|
||||
type: string
|
||||
required: true
|
||||
default: 'master'
|
||||
ignore_blocks:
|
||||
description: 'Ignore blocking PRs/issues'
|
||||
type: boolean
|
||||
|
|
@ -49,12 +54,13 @@ jobs:
|
|||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: jesseduffield/lazygit
|
||||
ref: ${{ inputs.branch }}
|
||||
token: ${{ secrets.LAZYGIT_RELEASE_PAT }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get Latest Tag
|
||||
run: |
|
||||
latest_tag=$(git describe --tags $(git rev-list --tags --max-count=1) || echo "v0.0.0")
|
||||
latest_tag=$(git describe --tags --abbrev=0 || echo "v0.0.0")
|
||||
|
||||
if ! [[ $latest_tag =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Error: Tag format is invalid. Expected format: vX.X.X"
|
||||
|
|
@ -121,7 +127,7 @@ jobs:
|
|||
IFS='.' read -r major minor patch <<< "$LATEST_TAG"
|
||||
|
||||
if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then
|
||||
if [[ "$VERSION_BUMP" == "patch" ]]; then
|
||||
if [[ "$VERSION_BUMP" == "patch (hotfix)" ]]; then
|
||||
patch=$((patch + 1))
|
||||
else
|
||||
minor=$((minor + 1))
|
||||
|
|
@ -151,7 +157,7 @@ jobs:
|
|||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git tag "$NEW_TAG" -a -m "Release $NEW_TAG"
|
||||
git push origin "$NEW_TAG"
|
||||
git push origin "refs/tags/$NEW_TAG"
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
version: "2"
|
||||
run:
|
||||
go: "1.25"
|
||||
issues:
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
uniq-by-line: false
|
||||
linters:
|
||||
enable:
|
||||
- copyloopvar
|
||||
|
|
|
|||
91
AGENTS.md
91
AGENTS.md
|
|
@ -26,6 +26,24 @@ Windows box has only `just`).
|
|||
(most useful with `--sandbox` or `--slow`).
|
||||
- `just lint` — run golangci-lint.
|
||||
|
||||
## Prefer gopls MCP tools for Go symbol questions
|
||||
|
||||
When the gopls MCP tools are available in the session, prefer them over grep
|
||||
for type-aware questions about Go code: who calls a function or method
|
||||
(`go_symbol_references`), finding a symbol by fuzzy name (`go_search`), or
|
||||
inspecting a package's API (`go_package_api`). Method names in this codebase
|
||||
collide a lot (`draw`, `Show`, `Refresh` exist on several types), and grep
|
||||
needs manual filtering that gopls doesn't. This includes code under
|
||||
`vendor/`, which gopls resolves as part of the module build.
|
||||
|
||||
Grep remains the right tool for strings, comments, config keys, non-Go
|
||||
files, and anything textual. Don't adopt the full workflow from
|
||||
`gopls mcp -instructions` (vulncheck on session start, `go_file_context`
|
||||
after every file read); that overhead isn't worth it here.
|
||||
|
||||
If the tools aren't available in a session, fall back to grep silently —
|
||||
don't try to install, register, or start the server.
|
||||
|
||||
## When to commit
|
||||
|
||||
Do not leave completed work uncommitted. Once a logical unit of work is done
|
||||
|
|
@ -48,6 +66,9 @@ while still being meaningful and self-contained.
|
|||
commits that leave the tree broken and rely on a follow-up to fix it.
|
||||
- **Every commit must be `gofumpt`-formatted.** Run `just format` before
|
||||
committing.
|
||||
- **Every commit must be lint-clean.** Run `just lint` before committing —
|
||||
don't introduce a lint warning in one commit and rely on a later commit
|
||||
(or the user) to clean it up.
|
||||
- **Commit messages explain _why_, not _what_.** The diff already shows what
|
||||
changed; the message should capture the motivation, the constraint, or the
|
||||
bug being fixed. If the reason is obvious from a one-line subject, no body
|
||||
|
|
@ -157,6 +178,16 @@ genuine forks — the ones where a reasonable person might pick differently, or
|
|||
where you'd be trading away something the plan assumed (scope, UX, performance,
|
||||
reload behavior, …). When in doubt, surface it.
|
||||
|
||||
This applies with equal force to unforeseen _discoveries_, not just to
|
||||
decisions you set out to make. If you find something the plan didn't account
|
||||
for — a latent bug, a race, a wrong assumption, a case that turns out
|
||||
unhandled — stop and raise it before designing or writing a fix, even when the
|
||||
fix seems obvious and even when it's "just correctness." Finding the problem is
|
||||
itself the fork: whether to fix it here or in a separate change, how generally
|
||||
to solve it, and whether it reshapes the current work are all calls for me to
|
||||
make with you. Don't quietly fold a self-directed fix for a newly-found problem
|
||||
into the branch and let me discover it in the diff.
|
||||
|
||||
## Prefer the cleaner design over the smaller diff
|
||||
|
||||
When a task could be implemented either by tacking onto existing code or by
|
||||
|
|
@ -243,6 +274,34 @@ Follow this even when the need for the refactor is only discovered in the middle
|
|||
of working on the branch; suggest to the user to rewrite the history to move the
|
||||
refactor to an earlier commit (but don't do it without asking first).
|
||||
|
||||
## Don't read model state right after a `Refresh`
|
||||
|
||||
A `Refresh` (or `RefreshFromWorker`) does its git work on a worker and then
|
||||
*enqueues* the model update onto the UI thread. So when `Refresh` returns, the
|
||||
model is **not** updated yet — the write is still queued. Reading a field
|
||||
synchronously right after refreshing its scope reads the stale, pre-refresh
|
||||
value (and this is true even for SYNC refreshes):
|
||||
|
||||
```go
|
||||
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}})
|
||||
files := self.c.Model().Files // BUG: still the pre-refresh value
|
||||
```
|
||||
|
||||
Put the read in `RefreshOptions.Then` instead — it's queued after the scope's
|
||||
model writes, so it sees the fresh value:
|
||||
|
||||
```go
|
||||
self.c.Refresh(types.RefreshOptions{
|
||||
Scope: []types.RefreshableView{types.FILES},
|
||||
Then: func() error {
|
||||
files := self.c.Model().Files // fresh
|
||||
return nil
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
`Then` is a `func() error` and works with any non-`ASYNC` mode.
|
||||
|
||||
## Integration test conventions
|
||||
|
||||
Don't bind views to local variables. Always chain method calls directly from
|
||||
|
|
@ -279,6 +338,29 @@ languages), and the map form extends cleanly when a string later needs more
|
|||
than one placeholder. This holds for every user-facing string, including short
|
||||
ones like disabled-action reasons and toasts.
|
||||
|
||||
## Only edit the English translations
|
||||
|
||||
`pkg/i18n/english.go` is the one translation file you edit; add, change, and
|
||||
remove strings there. The other languages under `pkg/i18n/translations/` are
|
||||
maintained by Crowdin and synced automatically — never edit them by hand, not
|
||||
even to add a key you just introduced or to delete one you just removed. A
|
||||
removed English string simply leaves an orphan key in those files, which
|
||||
Crowdin cleans up on its own; an unknown key in a translation file is ignored
|
||||
at load time, so it does no harm in the meantime.
|
||||
|
||||
## Try to keep new english.go strings within the existing column alignment
|
||||
|
||||
`gofumpt` aligns the `TranslationSet` struct fields and the `EnglishTranslationSet`
|
||||
literal into columns, so a new field whose name is longer than the widest one in
|
||||
its alignment block re-indents every line in that block. When there are several
|
||||
feature branches in flight that all add strings, that reformatting churn turns
|
||||
english.go into a rebase-conflict magnet. So when it's cheap to do so, make an
|
||||
effort to keep a new field name within the current widest name in the block
|
||||
(measure it; it's around 40 characters today), shortening the Go field name to
|
||||
fit. This is a soft preference, not a rule: the usual "best name wins" still
|
||||
applies, so don't mangle a name past the point of readability just to save a
|
||||
column. Applies only to `pkg/i18n/english.go`.
|
||||
|
||||
## Code comments are for future readers, not development history
|
||||
|
||||
Comments in source code explain *why this code is shaped the way it is*. They
|
||||
|
|
@ -347,3 +429,12 @@ Never run `find` (or similar) from `/` or other paths outside the project. All
|
|||
third-party code we use is vendored under `vendor/`, so dependency sources are
|
||||
reachable from inside the working tree — search there instead of the host
|
||||
filesystem.
|
||||
|
||||
## gocui is in-tree, not a dependency
|
||||
|
||||
The `gocui` TUI library is a fork maintained directly in this repo under
|
||||
`pkg/gocui` — it's an ordinary package, not a Go module dependency. Don't look
|
||||
for it in `go.mod`/`go.sum` or the module cache (`$GOMODCACHE`); it isn't
|
||||
there. When you need to read or change gocui internals (the task manager, the
|
||||
event loop, worker/UI-thread dispatch, view rendering), edit `pkg/gocui`
|
||||
directly.
|
||||
|
|
|
|||
277
CONTRIBUTING.md
277
CONTRIBUTING.md
|
|
@ -1,273 +1,34 @@
|
|||
# Contributing
|
||||
|
||||
This project does not accept pull requests.
|
||||
## The short version
|
||||
|
||||
In todays world of agentic coding I have decided that it no longer makes sense for me to look at incoming pull requests. As far as I can tell, the vast majority of these is AI-generated these days, which in itself is not necessarily a bad thing; however, there's no way for me to tell whether the person posting the PR actually understands anything about the code that is being contributed or not, and I don't feel like spending time and energy on finding out whether they do.
|
||||
This project does not accept pull requests. Don't bother making one, it won't be merged.
|
||||
|
||||
Now you might ask why this even matters; coding agents are capable of producing amazingly high-quality code, so why is it important that the person opening the PR understands it, as long as the code works and tests are green? It does actually matter very much to me. AI generated code needs to be carefully reviewed and iterated on, and it is the contributor's job to do that, not mine. And I have no idea to what extent the contributor has done this, or whether they are even capable of it.
|
||||
However, there are other forms of contributions that are very welcome and encouraged; see below for what those are.
|
||||
|
||||
Every PR needs work and iterations until it is mergeable, whether manually coded or AI generated (even very good ones do), and if I don't know whether the person posting the PR will act on my review feedback themselves or just pass it on to their coding agent (which I guess is the much more likely case today), then it doesn't make sense for me to work with them.
|
||||
## Why no PRs?
|
||||
|
||||
For this reason I will close incoming pull requests by default from now on, without comment. Sorry if this sounds hostile, but honestly I don't feel I have much of a choice if I want maintaining this project to still be enjoyable for me.
|
||||
There are two main reasons for this, and I want to be very honest about them:
|
||||
|
||||
With that said, if you are indeed serious about contributing a high-quality PR to lazygit, and you are familiar with go, and you have learned enough about lazygit's code base to tell whether your changes are good, then do raise an issue and explain what you are planning to do, and somehow make it plausible that your PR will be worth my time reviewing it. In such a case I might make an exception from the default rule.
|
||||
- I am maintaining lazygit for fun, as a hobby in my free time (which is quite limited). I'd like to spend my free time on things that I enjoy doing. I enjoy working on lazygit's code and improving it myself; I don't enjoy reviewing PRs. It's that simple, really. Reviewing PRs takes a lot of time; time that I would rather spend on developing lazygit myself.
|
||||
- Even if I had the time and inclination to review PRs, this has become quite difficult today: most PRs nowadays are AI-generated to some extent (often completely), which in itself is not necessarily a bad thing; I heavily use AI myself these days, and I get great results from it. However, agentic coding needs to be guided by humans so that the results are good, and for contributed PRs I can't tell to what extent the human contributor did this, or is even capable of it; and I don't want to do the work of guiding a contributor's coding agent. If I post PR review feedback and have to suspect that the contributor simply passes it on to their coding agent, then that is a work mode that doesn't make sense to me, and I would rather just drive my own agent to do the work.
|
||||
|
||||
In the future I might also consider adopting a vouch system similar to [Ghostty's](https://github.com/ghostty-org/ghostty/blob/main/CONTRIBUTING.md#first-time-contributors), but right now I feel the effort needed to set this up and maintain is not justified given the rather low number of high-quality contributions I have seen in recent times.
|
||||
### Why it might still make sense to post a PR
|
||||
|
||||
Even though we no longer accept pull requests, I find it important to emphasize that Lazygit is still a community project, and non-PR contributions are still very welcome. Do file issues for bug reports or feature requests, and help shape the future of lazygit by actively participating in discussing UX designs. Also, the localization system very much depends on everybody's help with translating texts (see https://crowdin.com/project/lazygit).
|
||||
I can think of two such reasons:
|
||||
|
||||
---
|
||||
- You implemented a lazygit improvement that you want to use yourself; in this case it could make sense to let others merge this change into their forks if they find it useful too. And if enough people say they want the feature, this can persuade me to add it, so putting it out there to give it visibility can be helpful.
|
||||
- You posted an issue for a feature request, and have a prototype that implements it; it could be useful to publish the branch as a draft PR to better illustrate how the feature works.
|
||||
|
||||
The remainder of this document is the old version from a time when contributing pull requests was still encouraged. Keeping it here in case I reconsider my policy in the future.
|
||||
For this reason I usually don't close pull requests to give them more visibility. Just don't expect your PR to be merged.
|
||||
|
||||
## PR walkthrough
|
||||
## So how can I contribute then?
|
||||
|
||||
[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.
|
||||
There are other forms of contributions to a project besides source code that are very welcome and encouraged; for instance:
|
||||
|
||||
## Design principles
|
||||
- File issues for bugs that you find, and I'll do my best to take care of fixing them (if they are important enough).
|
||||
- File feature requests for new functionality that you want to see in lazygit. I have a lot of ideas for future improvement myself, but I have also implemented a lot of feature ideas that weren't mine, and I'm grateful for those ideas. (Of course, there are also lots of feature requests that I don't implement, so don't be disappointed if I don't jump on yours.)
|
||||
- Help make other people's bug reports reproducible. Sometimes people report bugs that they have only seen once, and in such a case it can be helpful to come up with reproducible scenarios.
|
||||
- Help complete or improve the translation into other languages; join https://crowdin.com/project/lazygit for that.
|
||||
|
||||
See [here](./VISION.md) for a set of design principles that we want to consider when building a feature or making a change.
|
||||
|
||||
## Codebase guide
|
||||
|
||||
[This doc](./docs/dev/Codebase_Guide.md) explains:
|
||||
|
||||
- what the different packages in the codebase are for
|
||||
- where important files live
|
||||
- important concepts in the code
|
||||
- how the event loop works
|
||||
- other useful information
|
||||
|
||||
## All code changes happen through Pull Requests
|
||||
|
||||
Pull requests are the best way to propose changes to the codebase. We actively
|
||||
welcome your pull requests:
|
||||
|
||||
1. Fork the repo and create your branch from `master`.
|
||||
2. If you've added code that should be tested, add tests.
|
||||
3. If you've added code that needs documentation, update the documentation.
|
||||
4. Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
|
||||
5. Issue that pull request!
|
||||
|
||||
Please do not raise pull request from your fork's master branch: make a feature branch instead. Lazygit maintainers will sometimes push changes to your branch when reviewing a PR and we often can't do this if you use your master branch.
|
||||
|
||||
If you've never written Go in your life, then join the club! Lazygit was the maintainer's first Go program, and most contributors have never used Go before. Go is widely considered an easy-to-learn language, so if you're looking for an open source project to gain dev experience, you've come to the right place.
|
||||
|
||||
## Commit history
|
||||
|
||||
We value a clean and useful commit history, so please take some time to organize your commits so that they make sense. Don't assume that they will be squashed on merge anyway; we don't do that here.
|
||||
|
||||
In particular:
|
||||
|
||||
- Refactorings and behavior changes should be in separate commits. There are very few exceptions where this is not possible, but in my experience they are very rare.
|
||||
- Strive for minimal commits; every change that is independent from other changes should be in a commit of its own (with a good commit message that explains why the change is made).
|
||||
- When you need to iterate over your implementation during review (e.g. because you discovered a bug, or a maintainer requested changes), don't just pile new commits on top. Use fixup commits to make your changes transparent while still maintaining a good commit history. If you don't know what that means, [here's a brief introduction](docs/Fixup_Commits.md).
|
||||
|
||||
## A note about AI
|
||||
|
||||
It has become common recently to throw an issue at a coding agent and submit whatever comes out of it as a PR. This is not appreciated here, and I will close PRs where I can tell this was the case, or where I even suspect it was the case.
|
||||
|
||||
Some of these PRs may actually be good and useful, but many are not, and it's not a good use of my time as a maintainer to look at generated PRs to decide. This is the job of the PR's contributor, and if you don't speak enough go or can't be bothered to get familiar enough with lazygit's codebase to tell, then don't contribute the PR.
|
||||
|
||||
## Running in a VSCode dev container
|
||||
|
||||
If you want to spare yourself the hassle of setting up your dev environment yourself (i.e. installing Go, extensions, and extra tools), you can run the Lazygit code in a VSCode dev container like so:
|
||||
|
||||

|
||||
|
||||
This requires that:
|
||||
|
||||
- you have docker installed
|
||||
- you have the dev containers extension installed in VSCode
|
||||
|
||||
See [here](https://code.visualstudio.com/docs/devcontainers/containers) for more info about dev containers.
|
||||
|
||||
## Running in a Github Codespace
|
||||
|
||||
If you want to start contributing to Lazygit with the click of a button, you can open the lazygit codebase in a Codespace. First fork the repo, then click to create a codespace:
|
||||
|
||||

|
||||
|
||||
To run lazygit from within the integrated terminal just go `go run main.go`
|
||||
|
||||
This allows you to contribute to Lazygit without needing to install anything on your local machine. The Codespace has all the necessary tools and extensions pre-installed.
|
||||
|
||||
## Using Nix for development
|
||||
|
||||
If you use Nix, you can leverage the included flake to set up a complete development environment with all necessary dependencies:
|
||||
|
||||
```sh
|
||||
nix develop
|
||||
```
|
||||
|
||||
This will drop you into a development shell that includes:
|
||||
|
||||
- Latest Go toolchain
|
||||
- golangci-lint for code linting
|
||||
- git and make
|
||||
|
||||
You can also build and run lazygit using nix:
|
||||
|
||||
```sh
|
||||
# Build lazygit
|
||||
nix build
|
||||
|
||||
# Run lazygit directly
|
||||
nix run
|
||||
```
|
||||
|
||||
The nix flake supports multiple architectures (x86_64-linux, aarch64-linux, x86_64-darwin, aarch64-darwin) and provides a consistent development environment across different systems.
|
||||
|
||||
## Code of conduct
|
||||
|
||||
Please note by participating in this project, you agree to abide by the [code of conduct].
|
||||
|
||||
[code of conduct]: https://github.com/jesseduffield/lazygit/blob/master/CODE-OF-CONDUCT.md
|
||||
|
||||
## Any contributions you make will be under the MIT Software License
|
||||
|
||||
In short, when you submit code changes, your submissions are understood to be
|
||||
under the same [MIT License](http://choosealicense.com/licenses/mit/) that
|
||||
covers the project. Feel free to contact the maintainers if that's a concern.
|
||||
|
||||
## Report bugs using Github's [issues](https://github.com/jesseduffield/lazygit/issues)
|
||||
|
||||
We use GitHub issues to track public bugs. Report a bug by [opening a new
|
||||
issue](https://github.com/jesseduffield/lazygit/issues/new); it's that easy!
|
||||
|
||||
## Go
|
||||
|
||||
This project is written in Go. Go is an opinionated language with strict idioms, but some of those idioms are a little extreme. Some things we do differently:
|
||||
|
||||
1. There is no shame in using `self` as a receiver name in a struct method. In fact we encourage it
|
||||
2. There is no shame in prefixing an interface with 'I' instead of suffixing with 'er' when there are several methods on the interface.
|
||||
3. If a struct implements an interface, we make it explicit with something like:
|
||||
|
||||
```go
|
||||
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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To run gofumpt from your terminal go:
|
||||
|
||||
```
|
||||
go install mvdan.cc/gofumpt@latest && gofumpt -l -w .
|
||||
```
|
||||
|
||||
## Programming Font
|
||||
|
||||
Lazygit supports [Nerd Fonts](https://www.nerdfonts.com) to render certain icons. Sometimes we use some of these icons verbatim in string literals in the code (mainly in tests), so you need to set your development environment to use a nerd font to see these.
|
||||
|
||||
## Internationalisation
|
||||
|
||||
Boy that's a hard word to spell. Anyway, lazygit is translated into several languages within the pkg/i18n package.
|
||||
|
||||
### For developers adding new text
|
||||
|
||||
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. Then you can access via `gui.Tr.YourNewText` (or `self.c.Tr.YourNewText`, etc).
|
||||
|
||||
Note, we use 'Sentence case' for everything (so no 'Title Case' or 'whatever-it's-called-when-there's-no-capital-letters-case')
|
||||
|
||||
### For translators
|
||||
|
||||
Lazygit translations are managed through [Crowdin](https://crowdin.com/project/lazygit/). If you'd like to contribute translations:
|
||||
|
||||
1. Join the Crowdin project at https://crowdin.com/project/lazygit/
|
||||
2. Select your target language and help translate missing strings
|
||||
3. The translation files in `pkg/i18n/translations/` are managed by the maintainers - please don't edit them directly
|
||||
|
||||
For detailed information about the translation process, including how maintainers sync translations, see `pkg/i18n/translations/README.md`.
|
||||
|
||||
## Debugging
|
||||
|
||||
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")` or `self.c.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 setting the `LAZYGIT_LOG_PATH` environment variable and using `logs.Global.Warn("blah")`. This is a global logger that's only intended for development purposes.
|
||||
|
||||
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!
|
||||
|
||||
### 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
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "debug lazygit",
|
||||
"type": "go",
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Profiling
|
||||
|
||||
If you want to investigate what's contributing to CPU or memory usage, see [this separate document](docs/dev/Profiling.md).
|
||||
|
||||
## 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. For integration tests, see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
|
||||
|
||||
## Updating Gocui
|
||||
|
||||
Sometimes you will need to make a change in the gocui fork (https://github.com/jesseduffield/gocui). Gocui is the package responsible for rendering windows and handling user input. Here's the typical process to follow:
|
||||
|
||||
1. Make the changes in gocui inside lazygit's vendor directory so it's easy to test against lazygit
|
||||
2. Copy the changes over to the actual gocui repo (clone it if you haven't already, and use the `awesome` branch, not `master`)
|
||||
3. Raise a PR on the gocui repo with your changes
|
||||
4. After that PR is merged, make a PR in lazygit bumping the gocui version. You can bump the version by running the following at the lazygit repo root:
|
||||
|
||||
```sh
|
||||
./scripts/bump_gocui.sh
|
||||
```
|
||||
|
||||
5. Raise a PR in lazygit with those changes
|
||||
|
||||
## Updating Lazycore
|
||||
|
||||
[Lazycore](https://github.com/jesseduffield/lazycore) is a repo containing shared functionality between lazygit and lazydocker. Sometimes you will need to make a change to that repo and import the changes into lazygit. Similar to updating Gocui, here's what you do:
|
||||
|
||||
1. Make the changes in lazycore inside lazygit's vendor directory so it's easy to test against lazygit
|
||||
2. Copy the changes over to the actual lazycore repo (clone it if you haven't already, and use the `master` branch)
|
||||
3. Raise a PR on the lazycore repo with your changes
|
||||
4. After that PR is merged, make a PR in lazygit bumping the lazycore version. You can bump the version by running the following at the lazygit repo root:
|
||||
|
||||
```sh
|
||||
./scripts/bump_lazycore.sh
|
||||
```
|
||||
|
||||
Or if you're using VSCode, there is a bump lazycore task you can find by going `cmd+shift+p` and typing 'Run task'
|
||||
|
||||
5. Raise a PR in lazygit with those changes
|
||||
|
||||
## Improvements
|
||||
|
||||
If you can think of any way to improve these docs let us know.
|
||||
Importantly, if you file issues (whether bug reports or feature requests), stay around to answer questions and discuss your issue. There are few things that I find more annoying than spending time on responding to someone's issue (sometimes even making a PR that addresses it), and to then never hear from the OP again. So please set up your Github notifications so that you see when there's activity on your issue, and continue to participate.
|
||||
|
|
|
|||
|
|
@ -110,6 +110,11 @@ gui:
|
|||
# is true.
|
||||
expandedSidePanelWeight: 2
|
||||
|
||||
# If true, don't give a side panel more height than it needs to show its
|
||||
# content; when all panels fit, the leftover height is shared among them so that
|
||||
# they still fill the screen.
|
||||
shrinkSidePanelsToContent: false
|
||||
|
||||
# The side panels, in the order they appear from top to bottom.
|
||||
# Each entry is a list of one or more names that share a single panel as tabs
|
||||
# (cycle through them with the next-tab/previous-tab keys).
|
||||
|
|
@ -527,6 +532,15 @@ git:
|
|||
# to 40 to disable truncation.
|
||||
truncateCopiedCommitHashesTo: 12
|
||||
|
||||
# Config relating to git worktrees
|
||||
worktree:
|
||||
# Default parent directory for new worktrees. It is offered as a candidate
|
||||
# location alongside the parent directories of any worktrees you already have.
|
||||
# A relative path is resolved against the repository's root directory, so
|
||||
# "../worktrees" sits beside the repo and ".worktrees" sits inside it.
|
||||
# A leading "~" is expanded to your home directory, so "~/worktrees" works.
|
||||
defaultPath: ""
|
||||
|
||||
# Periodic update checks
|
||||
update:
|
||||
# One of: 'prompt' (default) | 'background' | 'never'
|
||||
|
|
@ -681,6 +695,7 @@ keybinding:
|
|||
confirmInEditor: [<ctrl+enter>, <ctrl+s>]
|
||||
remove: d
|
||||
new: "n"
|
||||
newWorktree: w
|
||||
edit: e
|
||||
openFile: o
|
||||
scrollUpMain: [<pgup>, K, <ctrl+u>]
|
||||
|
|
@ -761,8 +776,6 @@ keybinding:
|
|||
fetchRemote: f
|
||||
addForkRemote: F
|
||||
sortOrder: s
|
||||
worktrees:
|
||||
viewWorktreeOptions: w
|
||||
commits:
|
||||
squashDown: s
|
||||
renameCommit: r
|
||||
|
|
@ -1094,6 +1107,12 @@ keybinding:
|
|||
edit: <disabled> # disable 'edit file'
|
||||
```
|
||||
|
||||
### Overriding the platform for default keybindings
|
||||
|
||||
A few keybindings have different defaults on macOS than on Linux and Windows (e.g. word-wise cursor movement in text inputs uses `alt` on macOS but `ctrl` elsewhere). Lazygit picks these based on the OS it's running on, but you can override that with the `LAZYGIT_KEYBINDING_PLATFORM` environment variable. Set it to `darwin`, `linux`, or `windows`; any other value is ignored and the actual OS is used.
|
||||
|
||||
This is useful when running lazygit in a Linux container that you access over ssh from a Mac, where you'd rather use the macOS keybindings.
|
||||
|
||||
### Example Keybindings For Colemak Users
|
||||
|
||||
```yaml
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
Lazygit supports custom pagers, [configured](/docs/Config.md) in the config.yml file (which can be opened by pressing `e` in the Status panel).
|
||||
|
||||
Support does not extend to Windows users, because we're making use of a package which doesn't have Windows support. However, see [below](#emulating-custom-pagers-on-windows) for a workaround.
|
||||
|
||||
Multiple pagers are supported; you can cycle through them with the `|` key. This can be useful if you usually prefer a particular pager, but want to use a different one for certain kinds of diffs.
|
||||
|
||||
Pagers are configured with the `pagers` array in the git section; here's an example for a multi-pager setup (use an empty object `{}` for the default builtin diff display that doesn't use a pager):
|
||||
|
|
@ -81,6 +79,22 @@ git:
|
|||
- externalDiffCommand: difft --color=always --display=inline --syntax-highlight=off
|
||||
```
|
||||
|
||||
This can also be used for normal git diffs with custom parameters, such as `--color-words` or `--word-diff` which some people find useful. To do that, save a script like this to, say, `~/bin/color-words.sh`:
|
||||
|
||||
```sh
|
||||
#!/bin/sh
|
||||
|
||||
git diff --color-words --no-index --color=always --no-ext-diff "$2" "$5"
|
||||
```
|
||||
|
||||
And then use it in your git config like so:
|
||||
|
||||
```yaml
|
||||
git:
|
||||
pagers:
|
||||
- externalDiffCommand: ~/bin/color-words.sh
|
||||
```
|
||||
|
||||
Instead of setting this command in lazygit's `externalDiffCommand` config, you can also tell lazygit to use the external diff command that is configured in git itself (`diff.external`), by using
|
||||
|
||||
```yaml
|
||||
|
|
@ -92,30 +106,3 @@ git:
|
|||
This can be useful if you also want to use it for diffs on the command line, and it also has the advantage that you can configure it per file type in `.gitattributes`; see https://git-scm.com/docs/gitattributes#_defining_an_external_diff_driver.
|
||||
|
||||
`pager`, `externalDiffCommand`, and `useExternalDiffGitConfig` are alternative ways of producing the diff, so a pager entry may use at most one of them.
|
||||
|
||||
## Emulating custom pagers on Windows
|
||||
|
||||
There is a trick to emulate custom pagers on Windows using a Powershell script configured as an external diff command. It's not perfect, but certainly better than nothing. To do this, save the following script as `lazygit-pager.ps1` at a convenient place on your disk:
|
||||
|
||||
```pwsh
|
||||
#!/usr/bin/env pwsh
|
||||
|
||||
$old = $args[1].Replace('\', '/')
|
||||
$new = $args[4].Replace('\', '/')
|
||||
$path = $args[0]
|
||||
git diff --no-index --no-ext-diff $old $new
|
||||
| %{ $_.Replace($old, $path).Replace($new, $path) }
|
||||
| delta --width=$env:LAZYGIT_COLUMNS
|
||||
```
|
||||
|
||||
Use the pager of your choice with the arguments you like in the last line of the script. Personally I wouldn't want to use lazygit anymore without delta's `--hyperlinks --hyperlinks-file-link-format="lazygit-edit://{path}:{line}"` args, see [above](#delta).
|
||||
|
||||
In your lazygit config, use
|
||||
|
||||
```yml
|
||||
git:
|
||||
pagers:
|
||||
- externalDiffCommand: "C:/wherever/lazygit-pager.ps1"
|
||||
```
|
||||
|
||||
The main limitation of this approach compared to a "real" pager is that renames are not displayed correctly; they are shown as if they were modifications of the old file. (This affects only the hunk headers; the diff itself is always correct.)
|
||||
|
|
|
|||
|
|
@ -112,13 +112,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | Open commit in browser | |
|
||||
| `` n `` | Create new branch off of commit | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | Copy (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View files | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Search the current view by text | |
|
||||
|
||||
## Confirmation panel
|
||||
|
|
@ -178,6 +178,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <space> `` | Checkout | Checkout selected item. |
|
||||
| `` n `` | New branch | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` o `` | Create pull request | |
|
||||
| `` O `` | View create pull request options | |
|
||||
| `` G `` | Open pull request in browser | |
|
||||
|
|
@ -197,7 +198,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View commits | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Main panel (merging)
|
||||
|
|
@ -282,6 +282,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | Open commit in browser | |
|
||||
| `` n `` | Create new branch off of commit | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | Copy (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <ctrl+r> `` | Reset copied (cherry-picked) commits selection | |
|
||||
|
|
@ -289,7 +290,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View commits | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Remote branches
|
||||
|
|
@ -299,6 +299,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | Copy branch name to clipboard | |
|
||||
| `` <space> `` | Checkout | Checkout a new local branch based on the selected remote branch, or the remote branch as a detached head. |
|
||||
| `` n `` | New branch | |
|
||||
| `` w `` | New worktree | |
|
||||
| `` M `` | Merge | View options for merging the selected item into the current branch (regular merge, squash merge) |
|
||||
| `` r `` | Rebase | Rebase the checked-out branch onto the selected branch. |
|
||||
| `` d `` | Delete | Delete the remote branch from the remote. |
|
||||
|
|
@ -308,7 +309,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View commits | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Remotes
|
||||
|
|
@ -339,10 +339,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` g `` | Pop | Apply the stash entry to your working directory and remove the stash entry. |
|
||||
| `` d `` | Drop | Remove the stash entry from the stash list. |
|
||||
| `` n `` | New branch | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
|
||||
| `` w `` | New worktree | |
|
||||
| `` r `` | Rename stash | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View files | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Status
|
||||
|
|
@ -366,6 +366,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | Open commit in browser | |
|
||||
| `` n `` | Create new branch off of commit | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | Copy (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <ctrl+r> `` | Reset copied (cherry-picked) commits selection | |
|
||||
|
|
@ -373,7 +374,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View files | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Search the current view by text | |
|
||||
|
||||
## Submodules
|
||||
|
|
@ -397,13 +397,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | Copy tag to clipboard | |
|
||||
| `` <space> `` | Checkout | Checkout the selected tag as a detached HEAD. |
|
||||
| `` n `` | New tag | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. |
|
||||
| `` w `` | New worktree | |
|
||||
| `` d `` | Delete | View delete options for local/remote tag. |
|
||||
| `` P `` | Push tag | Push the selected tag to a remote. You'll be prompted to select a remote. |
|
||||
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View commits | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Worktrees
|
||||
|
|
|
|||
|
|
@ -92,13 +92,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | ブラウザでコミットを開く | |
|
||||
| `` n `` | コミットから新しいブランチを作成 | |
|
||||
| `` N `` | コミットを新しいブランチに移動 | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | 新しいワークツリー | |
|
||||
| `` g `` | リセット | 選択した項目へのリセットオプション(ソフト/ミックス/ハード)を表示します。各リセットタイプの詳細は次の通りです:<br>- ソフトリセット:変更を保持し、ステージされた状態にします<br>- ミックスリセット:変更を保持し、ステージされていない状態にします<br>- ハードリセット:すべての変更を破棄します |
|
||||
| `` C `` | コピー(チェリーピック) | コミットをコピーとしてマークします。ローカルコミットビューで `V` を押すと、コピーしたコミットをチェックアウトしたブランチにペースト(チェリーピック)できます。いつでも `<esc>` を押して選択をキャンセルできます。 |
|
||||
| `` <ctrl+t> `` | 外部差分ツールを開く(git difftool) | |
|
||||
| `` * `` | 現在のブランチのコミットを選択 | |
|
||||
| `` 0 `` | メインビューにフォーカス | |
|
||||
| `` <enter> `` | ファイルを表示 | |
|
||||
| `` w `` | ワークツリーオプションを表示 | |
|
||||
| `` / `` | 現在のビューをテキストで検索 | |
|
||||
|
||||
## コミットファイル
|
||||
|
|
@ -138,6 +138,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | ブラウザでコミットを開く | |
|
||||
| `` n `` | コミットから新しいブランチを作成 | |
|
||||
| `` N `` | コミットを新しいブランチに移動 | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | 新しいワークツリー | |
|
||||
| `` g `` | リセット | 選択した項目へのリセットオプション(ソフト/ミックス/ハード)を表示します。各リセットタイプの詳細は次の通りです:<br>- ソフトリセット:変更を保持し、ステージされた状態にします<br>- ミックスリセット:変更を保持し、ステージされていない状態にします<br>- ハードリセット:すべての変更を破棄します |
|
||||
| `` C `` | コピー(チェリーピック) | コミットをコピーとしてマークします。ローカルコミットビューで `V` を押すと、コピーしたコミットをチェックアウトしたブランチにペースト(チェリーピック)できます。いつでも `<esc>` を押して選択をキャンセルできます。 |
|
||||
| `` <ctrl+r> `` | コピーされた(チェリーピックされた)コミットの選択をリセット | |
|
||||
|
|
@ -145,7 +146,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | 現在のブランチのコミットを選択 | |
|
||||
| `` 0 `` | メインビューにフォーカス | |
|
||||
| `` <enter> `` | ファイルを表示 | |
|
||||
| `` w `` | ワークツリーオプションを表示 | |
|
||||
| `` / `` | 現在のビューをテキストで検索 | |
|
||||
|
||||
## サブモジュール
|
||||
|
|
@ -170,10 +170,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` g `` | ポップ | スタッシュエントリをワーキングディレクトリに適用し、スタッシュエントリを削除します。 |
|
||||
| `` d `` | 削除 | スタッシュリストからスタッシュエントリを削除します。 |
|
||||
| `` n `` | 新しいブランチ | 選択したスタッシュエントリから新しいブランチを作成します。これは、スタッシュエントリが作成されたコミットをgitがチェックアウトし、そのコミットから新しいブランチを作成した後、スタッシュエントリを追加のコミットとして新しいブランチに適用することで機能します。 |
|
||||
| `` w `` | 新しいワークツリー | |
|
||||
| `` r `` | スタッシュの名前を変更 | |
|
||||
| `` 0 `` | メインビューにフォーカス | |
|
||||
| `` <enter> `` | ファイルを表示 | |
|
||||
| `` w `` | ワークツリーオプションを表示 | |
|
||||
| `` / `` | 現在のビューをテキストでフィルタリング | |
|
||||
|
||||
## ステータス
|
||||
|
|
@ -202,13 +202,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | タグをクリップボードにコピー | |
|
||||
| `` <space> `` | チェックアウト(ブランチの切り替え) | 選択したタグをデタッチドHEADとしてチェックアウトします。 |
|
||||
| `` n `` | 新しいタグを作成 | 現在のコミットから新しいタグを作成します。タグ名とオプションの説明を入力するよう促されます。 |
|
||||
| `` w `` | 新しいワークツリー | |
|
||||
| `` d `` | 削除 | ローカル/リモートタグの削除オプションを表示します。 |
|
||||
| `` P `` | タグをプッシュ | 選択したタグをリモートにプッシュします。リモートを選択するよう促されます。 |
|
||||
| `` g `` | リセット | 選択した項目へのリセットオプション(ソフト/ミックス/ハード)を表示します。各リセットタイプの詳細は次の通りです:<br>- ソフトリセット:変更を保持し、ステージされた状態にします<br>- ミックスリセット:変更を保持し、ステージされていない状態にします<br>- ハードリセット:すべての変更を破棄します |
|
||||
| `` <ctrl+t> `` | 外部差分ツールを開く(git difftool) | |
|
||||
| `` 0 `` | メインビューにフォーカス | |
|
||||
| `` <enter> `` | コミットを表示 | |
|
||||
| `` w `` | ワークツリーオプションを表示 | |
|
||||
| `` / `` | 現在のビューをテキストでフィルタリング | |
|
||||
|
||||
## ファイル
|
||||
|
|
@ -326,6 +326,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | ブラウザでコミットを開く | |
|
||||
| `` n `` | コミットから新しいブランチを作成 | |
|
||||
| `` N `` | コミットを新しいブランチに移動 | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | 新しいワークツリー | |
|
||||
| `` g `` | リセット | 選択した項目へのリセットオプション(ソフト/ミックス/ハード)を表示します。各リセットタイプの詳細は次の通りです:<br>- ソフトリセット:変更を保持し、ステージされた状態にします<br>- ミックスリセット:変更を保持し、ステージされていない状態にします<br>- ハードリセット:すべての変更を破棄します |
|
||||
| `` C `` | コピー(チェリーピック) | コミットをコピーとしてマークします。ローカルコミットビューで `V` を押すと、コピーしたコミットをチェックアウトしたブランチにペースト(チェリーピック)できます。いつでも `<esc>` を押して選択をキャンセルできます。 |
|
||||
| `` <ctrl+r> `` | コピーされた(チェリーピックされた)コミットの選択をリセット | |
|
||||
|
|
@ -333,7 +334,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | 現在のブランチのコミットを選択 | |
|
||||
| `` 0 `` | メインビューにフォーカス | |
|
||||
| `` <enter> `` | コミットを表示 | |
|
||||
| `` w `` | ワークツリーオプションを表示 | |
|
||||
| `` / `` | 現在のビューをテキストでフィルタリング | |
|
||||
|
||||
## リモート
|
||||
|
|
@ -355,6 +355,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | ブランチ名をクリップボードにコピー | |
|
||||
| `` <space> `` | チェックアウト(ブランチの切り替え) | 選択したリモートブランチに基づいて新しいローカルブランチをチェックアウトするか、リモートブランチをデタッチドヘッドとしてチェックアウトします。 |
|
||||
| `` n `` | 新しいブランチ | |
|
||||
| `` w `` | 新しいワークツリー | |
|
||||
| `` M `` | マージ | 選択した項目を現在のブランチにマージするためのオプションを表示します(通常のマージ、スカッシュマージ) |
|
||||
| `` r `` | リベース | チェックアウトしたブランチを選択したブランチ上にリベースします。 |
|
||||
| `` d `` | 削除 | リモートからリモートブランチを削除します。 |
|
||||
|
|
@ -364,7 +365,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | 外部差分ツールを開く(git difftool) | |
|
||||
| `` 0 `` | メインビューにフォーカス | |
|
||||
| `` <enter> `` | コミットを表示 | |
|
||||
| `` w `` | ワークツリーオプションを表示 | |
|
||||
| `` / `` | 現在のビューをテキストでフィルタリング | |
|
||||
|
||||
## ローカルブランチ
|
||||
|
|
@ -376,6 +376,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <space> `` | チェックアウト(ブランチの切り替え) | 選択した項目をチェックアウトします。 |
|
||||
| `` n `` | 新しいブランチ | |
|
||||
| `` N `` | コミットを新しいブランチに移動 | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | 新しいワークツリー | |
|
||||
| `` o `` | プルリクエストを作成 | |
|
||||
| `` O `` | プルリクエスト作成オプションを表示 | |
|
||||
| `` G `` | Open pull request in browser | |
|
||||
|
|
@ -395,7 +396,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | 外部差分ツールを開く(git difftool) | |
|
||||
| `` 0 `` | メインビューにフォーカス | |
|
||||
| `` <enter> `` | コミットを表示 | |
|
||||
| `` w `` | ワークツリーオプションを表示 | |
|
||||
| `` / `` | 現在のビューをテキストでフィルタリング | |
|
||||
|
||||
## ワークツリー
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | 브라우저에서 커밋 열기 | |
|
||||
| `` n `` | 커밋에서 새 브랜치를 만듭니다. | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` g `` | View reset options | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | 커밋을 복사 (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <ctrl+r> `` | Reset cherry-picked (copied) commits selection | |
|
||||
|
|
@ -76,7 +77,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 커밋 보기 | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Secondary
|
||||
|
|
@ -95,10 +95,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` g `` | Pop | Apply the stash entry to your working directory and remove the stash entry. |
|
||||
| `` d `` | Drop | Remove the stash entry from the stash list. |
|
||||
| `` n `` | 새 브랜치 생성 | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
|
||||
| `` w `` | New worktree | |
|
||||
| `` r `` | Rename stash | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View selected item's files | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Sub-commits
|
||||
|
|
@ -111,6 +111,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | 브라우저에서 커밋 열기 | |
|
||||
| `` n `` | 커밋에서 새 브랜치를 만듭니다. | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` g `` | View reset options | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | 커밋을 복사 (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <ctrl+r> `` | Reset cherry-picked (copied) commits selection | |
|
||||
|
|
@ -118,7 +119,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View selected item's files | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | 검색 시작 | |
|
||||
|
||||
## Worktrees
|
||||
|
|
@ -212,6 +212,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <space> `` | 체크아웃 | Checkout selected item. |
|
||||
| `` n `` | 새 브랜치 생성 | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` o `` | 풀 리퀘스트 생성 | |
|
||||
| `` O `` | 풀 리퀘스트 생성 옵션 | |
|
||||
| `` G `` | Open pull request in browser | |
|
||||
|
|
@ -231,7 +232,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 커밋 보기 | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## 상태
|
||||
|
|
@ -278,6 +278,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | 브랜치명을 클립보드에 복사 | |
|
||||
| `` <space> `` | 체크아웃 | Checkout a new local branch based on the selected remote branch, or the remote branch as a detached head. |
|
||||
| `` n `` | 새 브랜치 생성 | |
|
||||
| `` w `` | New worktree | |
|
||||
| `` M `` | 현재 브랜치에 병합 | View options for merging the selected item into the current branch (regular merge, squash merge) |
|
||||
| `` r `` | 체크아웃된 브랜치를 이 브랜치에 리베이스 | Rebase the checked-out branch onto the selected branch. |
|
||||
| `` d `` | 삭제 | Delete the remote branch from the remote. |
|
||||
|
|
@ -287,7 +288,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 커밋 보기 | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## 커밋
|
||||
|
|
@ -323,13 +323,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | 브라우저에서 커밋 열기 | |
|
||||
| `` n `` | 커밋에서 새 브랜치를 만듭니다. | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` g `` | View reset options | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | 커밋을 복사 (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View selected item's files | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | 검색 시작 | |
|
||||
|
||||
## 커밋 파일
|
||||
|
|
@ -366,13 +366,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | Copy tag to clipboard | |
|
||||
| `` <space> `` | 체크아웃 | Checkout the selected tag as a detached HEAD. |
|
||||
| `` n `` | 태그를 생성 | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. |
|
||||
| `` w `` | New worktree | |
|
||||
| `` d `` | 삭제 | View delete options for local/remote tag. |
|
||||
| `` P `` | 태그를 push | Push the selected tag to a remote. You'll be prompted to select a remote. |
|
||||
| `` g `` | 초기화 | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 커밋 보기 | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## 파일
|
||||
|
|
|
|||
|
|
@ -10,15 +10,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <pgup>, K, <ctrl+u> (fn+up/shift+k) `` | Scroll naar beneden vanaf hoofdpaneel | |
|
||||
| `` <pgdown>, J, <ctrl+d> (fn+down/shift+j) `` | Scroll naar beneden vanaf hoofdpaneel | |
|
||||
| `` @ `` | View command log options | View options for the command log e.g. show/hide the command log and focus the command log. |
|
||||
| `` P `` | Push | Push the current branch to its upstream branch. If no upstream is configured, you will be prompted to configure an upstream branch. |
|
||||
| `` p `` | Pull | Pull changes from the remote for the current branch. If no upstream is configured, you will be prompted to configure an upstream branch. |
|
||||
| `` P `` | Push | Push de huidige branch naar de bijbehorende upstream-branch. Als er geen upstream is geconfigureerd wordt er gevraagd om een upstream-branch te configureren. |
|
||||
| `` p `` | Pull | Pull wijzigingen van de remote voor de huidige branch. Als er geen upstream is geconfigureerd wordt er gevraagd om een upstream-branch te configureren. |
|
||||
| `` ) `` | Increase rename similarity threshold | Increase the similarity threshold for a deletion and addition pair to be treated as a rename.<br><br>The default can be changed in the config file with the key 'git.renameSimilarityThreshold'. |
|
||||
| `` ( `` | Decrease rename similarity threshold | Decrease the similarity threshold for a deletion and addition pair to be treated as a rename.<br><br>The default can be changed in the config file with the key 'git.renameSimilarityThreshold'. |
|
||||
| `` } `` | Increase diff context size | Increase the amount of the context shown around changes in the diff view.<br><br>The default can be changed in the config file with the key 'git.diffContextSize'. |
|
||||
| `` { `` | Decrease diff context size | Decrease the amount of the context shown around changes in the diff view.<br><br>The default can be changed in the config file with the key 'git.diffContextSize'. |
|
||||
| `` : `` | Execute shell command | Bring up a prompt where you can enter a shell command to execute. |
|
||||
| `` <ctrl+p> `` | Bekijk aangepaste patch opties | |
|
||||
| `` m `` | Bekijk merge/rebase opties | View options to abort/continue/skip the current merge/rebase. |
|
||||
| `` m `` | Bekijk merge/rebase opties | Toon abort/continue/skip opties voor huidige merge/rebase. |
|
||||
| `` R `` | Verversen | Refresh the git state (i.e. run `git status`, `git branch`, etc in background to update the contents of panels). This does not run `git fetch`. |
|
||||
| `` + `` | Volgende scherm modus (normaal/half/groot) | |
|
||||
| `` _ `` | Vorige scherm modus | |
|
||||
|
|
@ -28,10 +28,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` ? `` | Open menu | |
|
||||
| `` <ctrl+s> `` | Bekijk scoping opties | View options for filtering the commit log, so that only commits matching the filter are shown. |
|
||||
| `` W, <ctrl+e> `` | Open diff menu | View options relating to diffing two refs e.g. diffing against selected ref, entering ref to diff against, and reversing the diff direction. |
|
||||
| `` q, <ctrl+c> `` | Quit | |
|
||||
| `` <ctrl+z> `` | Suspend the application | |
|
||||
| `` q, <ctrl+c> `` | Afsluiten | |
|
||||
| `` <ctrl+z> `` | Pauzeer de applicatie | |
|
||||
| `` <ctrl+w> `` | Toggle whitespace | Toggle whether or not whitespace changes are shown in the diff view.<br><br>The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. |
|
||||
| `` <alt+shift+c> `` | Verander config bestand | Open file in external editor. |
|
||||
| `` <alt+shift+c> `` | Verander config bestand | Open bestand in externe editor. |
|
||||
| `` z `` | Ongedaan maken (via reflog) (experimenteel) | The reflog will be used to determine what git command to run to undo the last git command. This does not include changes to the working tree; only commits are taken into consideration. |
|
||||
| `` Z `` | Redo (via reflog) (experimenteel) | The reflog will be used to determine what git command to run to redo the last git command. This does not include changes to the working tree; only commits are taken into consideration. |
|
||||
|
||||
|
|
@ -47,8 +47,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <shift+down> `` | Range select down | |
|
||||
| `` <shift+up> `` | Range select up | |
|
||||
| `` / `` | Start met zoeken | |
|
||||
| `` H `` | Scroll left | |
|
||||
| `` L `` | Scroll right | |
|
||||
| `` H `` | Scroll naar links | |
|
||||
| `` L `` | Scroll naar rechts | |
|
||||
| `` ] `` | Volgende tabblad | |
|
||||
| `` [ `` | Vorige tabblad | |
|
||||
|
||||
|
|
@ -58,15 +58,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
|-----|--------|-------------|
|
||||
| `` <ctrl+o> `` | Kopieer de bestandsnaam naar het klembord | |
|
||||
| `` <space> `` | Toggle staged | Toggle staged for selected file. |
|
||||
| `` <ctrl+b> `` | Filter files by status | |
|
||||
| `` y `` | Copy to clipboard | |
|
||||
| `` c `` | Commit veranderingen | Commit staged changes. |
|
||||
| `` <ctrl+b> `` | Filter bestanden op status | |
|
||||
| `` y `` | Kopieer naar klembord | |
|
||||
| `` c `` | Commit veranderingen | Commit gestagede wijzigingen. |
|
||||
| `` w `` | Commit veranderingen zonder pre-commit hook | |
|
||||
| `` A `` | Wijzig laatste commit | |
|
||||
| `` C `` | Commit veranderingen met de git editor | |
|
||||
| `` <ctrl+f> `` | Find base commit for fixup | Find the commit that your current changes are building upon, for the sake of amending/fixing up the commit. This spares you from having to look through your branch's commits one-by-one to see which commit should be amended/fixed up. See docs: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` e `` | Edit | Open file in external editor. |
|
||||
| `` o `` | Open bestand | Open file in default application. |
|
||||
| `` <ctrl+f> `` | Find base commit for fixup | Vind de commit waar je huidige wijzigingen bovenop zijn gebouwd met als doel die commit te amenden/fixen. Hierdoor hoef je dit niet met de hand te doen. Zie: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` e `` | Edit | Open bestand in externe editor. |
|
||||
| `` o `` | Open bestand | Open bestand in standaardapplicatie. |
|
||||
| `` i `` | Ignore or exclude file | |
|
||||
| `` r `` | Refresh bestanden | |
|
||||
| `` s `` | Stash | Stash all changes. For other variations of stashing, use the view stash options keybinding. |
|
||||
|
|
@ -75,13 +75,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <enter> `` | Stage individuele hunks/lijnen | If the selected item is a file, focus the staging view so you can stage individual hunks/lines. If the selected item is a directory, collapse/expand it. |
|
||||
| `` d `` | Bekijk 'veranderingen ongedaan maken' opties | View options for discarding changes to the selected file. |
|
||||
| `` g `` | Bekijk upstream reset opties | |
|
||||
| `` D `` | Reset | View reset options for working tree (e.g. nuking the working tree). |
|
||||
| `` D `` | Resetten | View reset options for working tree (e.g. nuking the working tree). |
|
||||
| `` ` `` | Toggle bestandsboom weergave | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.<br><br>The default can be changed in the config file with the key 'gui.showFileTree'. |
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
|
||||
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
|
||||
| `` M `` | Bekijk merge conflict opties | Bekijk opties voor het oplossen van mergeconflicten. |
|
||||
| `` f `` | Fetch | Fetch changes from remote. |
|
||||
| `` - `` | Collapse all files | Collapse all directories in the files tree |
|
||||
| `` = `` | Expand all files | Expand all directories in the file tree |
|
||||
| `` = `` | Vouw alle bestanden uit | Vouw alle mappen in de bestandsstructuur uit |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
|
|
@ -91,7 +91,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
|-----|--------|-------------|
|
||||
| `` <enter> `` | Bevestig | |
|
||||
| `` <esc> `` | Sluiten | |
|
||||
| `` <ctrl+o> `` | Copy to clipboard | |
|
||||
| `` <ctrl+o> `` | Kopieer naar klembord | |
|
||||
|
||||
## Branches
|
||||
|
||||
|
|
@ -99,18 +99,19 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
|-----|--------|-------------|
|
||||
| `` <ctrl+o> `` | Kopieer branch name naar klembord | |
|
||||
| `` i `` | Laat git-flow opties zien | |
|
||||
| `` <space> `` | Uitchecken | Checkout selected item. |
|
||||
| `` <space> `` | Uitchecken | Geselecteerd item uitchecken. |
|
||||
| `` n `` | Nieuwe branch | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` N `` | Verplaats commits naar nieuwe branch | Maak een nieuwe branch en verplaats niet-gepushte commits van de huidige branch hier naar toe. Gebruik dit in het geval dat je deze commits eigenlijk op een nieuwe branch had willen maken.<br><br>Let op dat de selectie genegeerd wordt. De nieuwe branch komt ofwel bovenop de main branch, of bovenop de huidige branch (je kan kiezen). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` o `` | Maak een pull-request | |
|
||||
| `` O `` | Bekijk opties voor pull-aanvraag | |
|
||||
| `` G `` | Open pull request in browser | |
|
||||
| `` <ctrl+y> `` | Kopieer de URL van het pull-verzoek naar het klembord | |
|
||||
| `` c `` | Uitchecken bij naam | Checkout by name. In the input box you can enter '-' to switch to the previous branch. |
|
||||
| `` - `` | Checkout previous branch | |
|
||||
| `` - `` | Vorige branch uitchecken | |
|
||||
| `` F `` | Forceer checkout | Force checkout selected branch. This will discard all local changes in your working directory before checking out the selected branch. |
|
||||
| `` d `` | Delete | View delete options for local/remote branch. |
|
||||
| `` r `` | Rebase branch | Rebase the checked-out branch onto the selected branch. |
|
||||
| `` d `` | Verwijderen | View delete options for local/remote branch. |
|
||||
| `` r `` | Rebase branch | Rebase de uitgecheckte branch bovenop de geselecteerde branch. |
|
||||
| `` M `` | Merge in met huidige checked out branch | View options for merging the selected item into the current branch (regular merge, squash merge) |
|
||||
| `` f `` | Fast-forward deze branch vanaf zijn upstream | Fast-forward selected branch from its upstream. |
|
||||
| `` T `` | Creëer tag | |
|
||||
|
|
@ -118,10 +119,9 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` g `` | Bekijk reset opties | |
|
||||
| `` R `` | Hernoem branch | |
|
||||
| `` u `` | View upstream options | View options relating to the branch's upstream e.g. setting/unsetting the upstream and resetting to the upstream. |
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Bekijk commits | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Commit bericht
|
||||
|
|
@ -136,18 +136,18 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <ctrl+o> `` | Kopieer de bestandsnaam naar het klembord | |
|
||||
| `` y `` | Copy to clipboard | |
|
||||
| `` y `` | Kopieer naar klembord | |
|
||||
| `` c `` | Uitchecken | Bestand uitchecken |
|
||||
| `` d `` | Bekijk 'veranderingen ongedaan maken' opties | Uitsluit deze commit zijn veranderingen aan dit bestand |
|
||||
| `` o `` | Open bestand | Open file in default application. |
|
||||
| `` e `` | Edit | Open file in external editor. |
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` o `` | Open bestand | Open bestand in standaardapplicatie. |
|
||||
| `` e `` | Edit | Open bestand in externe editor. |
|
||||
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
|
||||
| `` <space> `` | Toggle bestand inbegrepen in patch | Toggle whether the file is included in the custom patch. See https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
|
||||
| `` a `` | Toggle all files | Add/remove all commit's files to custom patch. See https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
|
||||
| `` <enter> `` | Enter bestand om geselecteerde regels toe te voegen aan de patch | If a file is selected, enter the file so that you can add/remove individual lines to the custom patch. If a directory is selected, toggle the directory. |
|
||||
| `` ` `` | Toggle bestandsboom weergave | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.<br><br>The default can be changed in the config file with the key 'gui.showFileTree'. |
|
||||
| `` - `` | Collapse all files | Collapse all directories in the files tree |
|
||||
| `` = `` | Expand all files | Expand all directories in the file tree |
|
||||
| `` = `` | Vouw alle bestanden uit | Vouw alle mappen in de bestandsstructuur uit |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
|
|
@ -183,14 +183,14 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | Open commit in browser | |
|
||||
| `` n `` | Creëer nieuwe branch van commit | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` N `` | Verplaats commits naar nieuwe branch | Maak een nieuwe branch en verplaats niet-gepushte commits van de huidige branch hier naar toe. Gebruik dit in het geval dat je deze commits eigenlijk op een nieuwe branch had willen maken.<br><br>Let op dat de selectie genegeerd wordt. De nieuwe branch komt ofwel bovenop de main branch, of bovenop de huidige branch (je kan kiezen). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` g `` | Bekijk reset opties | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | Kopieer commit (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Bekijk gecommite bestanden | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Start met zoeken | |
|
||||
|
||||
## Input prompt
|
||||
|
|
@ -219,9 +219,9 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <left>, h `` | Selecteer voorgaand conflict | |
|
||||
| `` <right>, l `` | Selecteer volgende conflict | |
|
||||
| `` z `` | Ongedaan maken | Undo last merge conflict resolution. |
|
||||
| `` e `` | Verander bestand | Open file in external editor. |
|
||||
| `` o `` | Open bestand | Open file in default application. |
|
||||
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
|
||||
| `` e `` | Verander bestand | Open bestand in externe editor. |
|
||||
| `` o `` | Open bestand | Open bestand in standaardapplicatie. |
|
||||
| `` M `` | Bekijk merge conflict opties | Bekijk opties voor het oplossen van mergeconflicten. |
|
||||
| `` <esc> `` | Ga terug naar het bestanden paneel | |
|
||||
|
||||
## Normaal
|
||||
|
|
@ -243,8 +243,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` v `` | Toggle drag selecteer | |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` <ctrl+o> `` | Copy selected text to clipboard | |
|
||||
| `` o `` | Open bestand | Open file in default application. |
|
||||
| `` e `` | Verander bestand | Open file in external editor. |
|
||||
| `` o `` | Open bestand | Open bestand in standaardapplicatie. |
|
||||
| `` e `` | Verander bestand | Open bestand in externe editor. |
|
||||
| `` <space> `` | Voeg toe/verwijder lijn(en) in patch | |
|
||||
| `` d `` | Remove lines from commit | Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines. |
|
||||
| `` <esc> `` | Sluit lijn-bij-lijn modus | |
|
||||
|
|
@ -259,15 +259,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | Open commit in browser | |
|
||||
| `` n `` | Creëer nieuwe branch van commit | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` N `` | Verplaats commits naar nieuwe branch | Maak een nieuwe branch en verplaats niet-gepushte commits van de huidige branch hier naar toe. Gebruik dit in het geval dat je deze commits eigenlijk op een nieuwe branch had willen maken.<br><br>Let op dat de selectie genegeerd wordt. De nieuwe branch komt ofwel bovenop de main branch, of bovenop de huidige branch (je kan kiezen). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` g `` | Bekijk reset opties | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | Kopieer commit (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <ctrl+r> `` | Reset cherry-picked (gekopieerde) commits selectie | |
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Bekijk commits | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Remote branches
|
||||
|
|
@ -275,27 +275,27 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <ctrl+o> `` | Kopieer branch name naar klembord | |
|
||||
| `` <space> `` | Uitchecken | Checkout a new local branch based on the selected remote branch, or the remote branch as a detached head. |
|
||||
| `` <space> `` | Uitchecken | Geselecteerde remote branch uitchecken als nieuwe locale branch of als detached head. |
|
||||
| `` n `` | Nieuwe branch | |
|
||||
| `` w `` | New worktree | |
|
||||
| `` M `` | Merge in met huidige checked out branch | View options for merging the selected item into the current branch (regular merge, squash merge) |
|
||||
| `` r `` | Rebase branch | Rebase the checked-out branch onto the selected branch. |
|
||||
| `` d `` | Delete | Delete the remote branch from the remote. |
|
||||
| `` u `` | Set as upstream | Stel in als upstream van uitgecheckte branch |
|
||||
| `` r `` | Rebase branch | Rebase de uitgecheckte branch bovenop de geselecteerde branch. |
|
||||
| `` d `` | Verwijderen | Delete the remote branch from the remote. |
|
||||
| `` u `` | Instellen als upstream | Stel in als upstream van uitgecheckte branch |
|
||||
| `` s `` | Sort order | |
|
||||
| `` g `` | Bekijk reset opties | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Bekijk commits | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Remotes
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <enter> `` | View branches | |
|
||||
| `` <enter> `` | Bekijk branches | |
|
||||
| `` n `` | Voeg een nieuwe remote toe | |
|
||||
| `` d `` | Remove | Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected. |
|
||||
| `` d `` | Verwijderen | Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected. |
|
||||
| `` e `` | Edit | Wijzig remote |
|
||||
| `` f `` | Fetch | Fetch remote |
|
||||
| `` F `` | Add fork remote | Quickly add a fork remote by replacing the owner in the origin URL and optionally check out a branch from new remote. |
|
||||
|
|
@ -320,15 +320,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | Copy selected text to clipboard | |
|
||||
| `` <space> `` | Toggle staged | Toggle lijnen staged / unstaged |
|
||||
| `` d `` | Verwijdert change (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
|
||||
| `` o `` | Open bestand | Open file in default application. |
|
||||
| `` e `` | Verander bestand | Open file in external editor. |
|
||||
| `` o `` | Open bestand | Open bestand in standaardapplicatie. |
|
||||
| `` e `` | Verander bestand | Open bestand in externe editor. |
|
||||
| `` <esc> `` | Ga terug naar het bestanden paneel | |
|
||||
| `` <tab> `` | Ga naar een ander paneel | Switch to other view (staged/unstaged changes). |
|
||||
| `` E `` | Edit hunk | Edit selected hunk in external editor. |
|
||||
| `` c `` | Commit veranderingen | Commit staged changes. |
|
||||
| `` c `` | Commit veranderingen | Commit gestagede wijzigingen. |
|
||||
| `` w `` | Commit veranderingen zonder pre-commit hook | |
|
||||
| `` C `` | Commit veranderingen met de git editor | |
|
||||
| `` <ctrl+f> `` | Find base commit for fixup | Find the commit that your current changes are building upon, for the sake of amending/fixing up the commit. This spares you from having to look through your branch's commits one-by-one to see which commit should be amended/fixed up. See docs: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` <ctrl+f> `` | Find base commit for fixup | Vind de commit waar je huidige wijzigingen bovenop zijn gebouwd met als doel die commit te amenden/fixen. Hierdoor hoef je dit niet met de hand te doen. Zie: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` / `` | Start met zoeken | |
|
||||
|
||||
## Stash
|
||||
|
|
@ -339,17 +339,17 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` g `` | Pop | Apply the stash entry to your working directory and remove the stash entry. |
|
||||
| `` d `` | Laten vallen | Remove the stash entry from the stash list. |
|
||||
| `` n `` | Nieuwe branch | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
|
||||
| `` r `` | Rename stash | |
|
||||
| `` w `` | New worktree | |
|
||||
| `` r `` | Hernoem stash | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Bekijk gecommite bestanden | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Status
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` e `` | Verander config bestand | Open file in external editor. |
|
||||
| `` e `` | Verander config bestand | Open bestand in externe editor. |
|
||||
| `` u `` | Check voor updates | |
|
||||
| `` <enter> `` | Wissel naar een recente repo | |
|
||||
| `` a `` | Show/cycle all branch logs | |
|
||||
|
|
@ -365,15 +365,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | Open commit in browser | |
|
||||
| `` n `` | Creëer nieuwe branch van commit | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` N `` | Verplaats commits naar nieuwe branch | Maak een nieuwe branch en verplaats niet-gepushte commits van de huidige branch hier naar toe. Gebruik dit in het geval dat je deze commits eigenlijk op een nieuwe branch had willen maken.<br><br>Let op dat de selectie genegeerd wordt. De nieuwe branch komt ofwel bovenop de main branch, of bovenop de huidige branch (je kan kiezen). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` g `` | Bekijk reset opties | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | Kopieer commit (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <ctrl+r> `` | Reset cherry-picked (gekopieerde) commits selectie | |
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Bekijk gecommite bestanden | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Start met zoeken | |
|
||||
|
||||
## Submodules
|
||||
|
|
@ -382,7 +382,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
|-----|--------|-------------|
|
||||
| `` <ctrl+o> `` | Kopieer submodule naam naar klembord | |
|
||||
| `` <enter> `` | Enter | Enter submodule |
|
||||
| `` d `` | Remove | Remove the selected submodule and its corresponding directory. |
|
||||
| `` d `` | Verwijderen | Remove the selected submodule and its corresponding directory. |
|
||||
| `` u `` | Update | Update selected submodule. |
|
||||
| `` n `` | Voeg nieuwe submodule toe | |
|
||||
| `` e `` | Update submodule URL | |
|
||||
|
|
@ -395,15 +395,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <ctrl+o> `` | Copy tag to clipboard | |
|
||||
| `` <space> `` | Uitchecken | Checkout the selected tag as a detached HEAD. |
|
||||
| `` <space> `` | Uitchecken | Geselecteerde tag uitchecken als detached HEAD. |
|
||||
| `` n `` | Creëer tag | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. |
|
||||
| `` d `` | Delete | View delete options for local/remote tag. |
|
||||
| `` w `` | New worktree | |
|
||||
| `` d `` | Verwijderen | View delete options for local/remote tag. |
|
||||
| `` P `` | Push tag | Push the selected tag to a remote. You'll be prompted to select a remote. |
|
||||
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` g `` | Resetten | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Bekijk commits | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Worktrees
|
||||
|
|
@ -412,6 +412,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
|-----|--------|-------------|
|
||||
| `` n `` | New worktree | |
|
||||
| `` <space> `` | Switch | Switch to the selected worktree. |
|
||||
| `` o `` | Open in editor | |
|
||||
| `` d `` | Remove | Remove the selected worktree. This will both delete the worktree's directory, as well as metadata about the worktree in the .git directory. |
|
||||
| `` o `` | Openen in editor | |
|
||||
| `` d `` | Verwijderen | Remove the selected worktree. This will both delete the worktree's directory, as well as metadata about the worktree in the .git directory. |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
|
|
|||
|
|
@ -85,13 +85,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | Otwórz commit w przeglądarce | |
|
||||
| `` n `` | Utwórz nową gałąź z commita | |
|
||||
| `` N `` | Przenieś commity do nowej gałęzi | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | Nowe drzewo pracy | |
|
||||
| `` g `` | Reset | Wyświetl opcje resetu (miękki/mieszany/twardy) do wybranego elementu. |
|
||||
| `` C `` | Kopiuj (cherry-pick) | Oznacz commit jako skopiowany. Następnie, w widoku lokalnych commitów, możesz nacisnąć `V`, aby wkleić (cherry-pick) skopiowane commity do sprawdzonej gałęzi. W dowolnym momencie możesz nacisnąć `<esc>`, aby anulować zaznaczenie. |
|
||||
| `` <ctrl+t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Wyświetl pliki | |
|
||||
| `` w `` | Zobacz opcje drzewa pracy | |
|
||||
| `` / `` | Szukaj w bieżącym widoku po tekście | |
|
||||
|
||||
## Dodatkowy
|
||||
|
|
@ -122,6 +122,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | Otwórz commit w przeglądarce | |
|
||||
| `` n `` | Utwórz nową gałąź z commita | |
|
||||
| `` N `` | Przenieś commity do nowej gałęzi | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | Nowe drzewo pracy | |
|
||||
| `` g `` | Reset | Wyświetl opcje resetu (miękki/mieszany/twardy) do wybranego elementu. |
|
||||
| `` C `` | Kopiuj (cherry-pick) | Oznacz commit jako skopiowany. Następnie, w widoku lokalnych commitów, możesz nacisnąć `V`, aby wkleić (cherry-pick) skopiowane commity do sprawdzonej gałęzi. W dowolnym momencie możesz nacisnąć `<esc>`, aby anulować zaznaczenie. |
|
||||
| `` <ctrl+r> `` | Resetuj wybrane (cherry-picked) commity | |
|
||||
|
|
@ -129,7 +130,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Pokaż commity | |
|
||||
| `` w `` | Zobacz opcje drzewa pracy | |
|
||||
| `` / `` | Filtruj bieżący widok po tekście | |
|
||||
|
||||
## Główny panel (budowanie łatki)
|
||||
|
|
@ -164,6 +164,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <space> `` | Przełącz | Przełącz wybrany element. |
|
||||
| `` n `` | Nowa gałąź | |
|
||||
| `` N `` | Przenieś commity do nowej gałęzi | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | Nowe drzewo pracy | |
|
||||
| `` o `` | Utwórz żądanie ściągnięcia | |
|
||||
| `` O `` | Zobacz opcje tworzenia pull requesta | |
|
||||
| `` G `` | Otwórz żądanie ściągnięcia w przeglądarce | |
|
||||
|
|
@ -183,7 +184,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Pokaż commity | |
|
||||
| `` w `` | Zobacz opcje drzewa pracy | |
|
||||
| `` / `` | Filtruj bieżący widok po tekście | |
|
||||
|
||||
## Menu
|
||||
|
|
@ -318,10 +318,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` g `` | Wyciągnij | Zastosuj wpis schowka do katalogu roboczego i usuń wpis schowka. |
|
||||
| `` d `` | Usuń | Usuń wpis schowka z listy schowka. |
|
||||
| `` n `` | Nowa gałąź | Utwórz nową gałąź z wybranego wpisu schowka. Działa poprzez przełączenie git na commit, na którym wpis schowka został utworzony, tworzenie nowej gałęzi z tego commita, a następnie zastosowanie wpisu schowka do nowej gałęzi jako dodatkowego commita. |
|
||||
| `` w `` | Nowe drzewo pracy | |
|
||||
| `` r `` | Zmień nazwę schowka | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Wyświetl pliki | |
|
||||
| `` w `` | Zobacz opcje drzewa pracy | |
|
||||
| `` / `` | Filtruj bieżący widok po tekście | |
|
||||
|
||||
## Status
|
||||
|
|
@ -345,6 +345,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | Otwórz commit w przeglądarce | |
|
||||
| `` n `` | Utwórz nową gałąź z commita | |
|
||||
| `` N `` | Przenieś commity do nowej gałęzi | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | Nowe drzewo pracy | |
|
||||
| `` g `` | Reset | Wyświetl opcje resetu (miękki/mieszany/twardy) do wybranego elementu. |
|
||||
| `` C `` | Kopiuj (cherry-pick) | Oznacz commit jako skopiowany. Następnie, w widoku lokalnych commitów, możesz nacisnąć `V`, aby wkleić (cherry-pick) skopiowane commity do sprawdzonej gałęzi. W dowolnym momencie możesz nacisnąć `<esc>`, aby anulować zaznaczenie. |
|
||||
| `` <ctrl+r> `` | Resetuj wybrane (cherry-picked) commity | |
|
||||
|
|
@ -352,7 +353,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Wyświetl pliki | |
|
||||
| `` w `` | Zobacz opcje drzewa pracy | |
|
||||
| `` / `` | Szukaj w bieżącym widoku po tekście | |
|
||||
|
||||
## Submoduły
|
||||
|
|
@ -376,13 +376,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | Skopiuj tag do schowka | |
|
||||
| `` <space> `` | Przełącz | Przełącz wybrany tag jako odłączoną głowę (detached HEAD). |
|
||||
| `` n `` | Nowy tag | Utwórz nowy tag z bieżącego commita. Zostaniesz poproszony o wprowadzenie nazwy tagu i opcjonalnego opisu. |
|
||||
| `` w `` | Nowe drzewo pracy | |
|
||||
| `` d `` | Usuń | Wyświetl opcje usuwania lokalnego/odległego tagu. |
|
||||
| `` P `` | Wyślij tag | Wyślij wybrany tag do zdalnego. Zostaniesz poproszony o wybranie zdalnego. |
|
||||
| `` g `` | Reset | Wyświetl opcje resetu (miękki/mieszany/twardy) do wybranego elementu. |
|
||||
| `` <ctrl+t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Pokaż commity | |
|
||||
| `` w `` | Zobacz opcje drzewa pracy | |
|
||||
| `` / `` | Filtruj bieżący widok po tekście | |
|
||||
|
||||
## Zdalne
|
||||
|
|
@ -404,6 +404,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | Kopiuj nazwę gałęzi do schowka | |
|
||||
| `` <space> `` | Przełącz | Przełącz na nową lokalną gałąź na podstawie wybranej gałęzi zdalnej. Nowa gałąź będzie śledzić gałąź zdalną. |
|
||||
| `` n `` | Nowa gałąź | |
|
||||
| `` w `` | Nowe drzewo pracy | |
|
||||
| `` M `` | Scal | Scal wybraną gałąź z aktualnie sprawdzoną gałęzią. |
|
||||
| `` r `` | Przebazuj | Przebazuj przełączoną gałąź na wybraną gałąź. |
|
||||
| `` d `` | Usuń | Usuń gałąź zdalną ze zdalnego. |
|
||||
|
|
@ -413,5 +414,4 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Pokaż commity | |
|
||||
| `` w `` | Zobacz opcje drzewa pracy | |
|
||||
| `` / `` | Filtruj bieżący widok po tekście | |
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <space> `` | Verificar | Checar item selecionado |
|
||||
| `` n `` | Nova branch | |
|
||||
| `` N `` | Mover commits para uma nova branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | Nova árvore de trabalho | |
|
||||
| `` o `` | Criar solicitação de pull | |
|
||||
| `` O `` | View create pull request options | |
|
||||
| `` G `` | Open pull request in browser | |
|
||||
|
|
@ -113,7 +114,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | Abrir ferramenta de diff externa (git difftool) | |
|
||||
| `` 0 `` | Focar visualização principal | |
|
||||
| `` <enter> `` | Ver commits | |
|
||||
| `` w `` | Ver opções da árvore de trabalho | |
|
||||
| `` / `` | Filtrar a visualização atual por texto | |
|
||||
|
||||
## Branches remotos
|
||||
|
|
@ -123,6 +123,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | Copiar nome da branch para área de transferência | |
|
||||
| `` <space> `` | Verificar | Checar a nova branch baseada na brach remota selecionada, ou a branch remota como HEAD, desanexado |
|
||||
| `` n `` | Nova branch | |
|
||||
| `` w `` | Nova árvore de trabalho | |
|
||||
| `` M `` | Mesclar | Ver opções para mesclar o item selecionado no branch atual (mesclar regularmente, mesclar squash) |
|
||||
| `` r `` | Refazer | Refazer a branch checada na branch selecionada |
|
||||
| `` d `` | Apagar | Excluir o branch remoto do controle remoto. |
|
||||
|
|
@ -132,7 +133,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | Abrir ferramenta de diff externa (git difftool) | |
|
||||
| `` 0 `` | Focar visualização principal | |
|
||||
| `` <enter> `` | Ver commits | |
|
||||
| `` w `` | Ver opções da árvore de trabalho | |
|
||||
| `` / `` | Filtrar a visualização atual por texto | |
|
||||
|
||||
## Commit arquivos
|
||||
|
|
@ -188,13 +188,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | Abrir commit no navegador | |
|
||||
| `` n `` | Create new branch off of commit | |
|
||||
| `` N `` | Mover commits para uma nova branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | Nova árvore de trabalho | |
|
||||
| `` g `` | Restaurar | Ver opções de redefinição (soft/mixed/hard) para redefinir para o item selecionado. |
|
||||
| `` C `` | Copiar (cherry-pick) | Marcar commit como copiado. Então, dentro da visualização local de commits, você pode pressionar `V` para colar (cherry-pick) o(s) commit(s) copiado(s) em seu branch de check-out. A qualquer momento você pode pressionar `<esc>` para cancelar a seleção. |
|
||||
| `` <ctrl+t> `` | Abrir ferramenta de diff externa (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focar visualização principal | |
|
||||
| `` <enter> `` | Ver arquivos | |
|
||||
| `` w `` | Ver opções da árvore de trabalho | |
|
||||
| `` / `` | Pesquisar na visualização atual por texto | |
|
||||
|
||||
## Etiquetas
|
||||
|
|
@ -204,13 +204,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | Copiar etiqueta para área de transferência | |
|
||||
| `` <space> `` | Verificar | Checar a tag selecionada como um HEAD, desanexado |
|
||||
| `` n `` | Nova etiqueta | Crie uma nova etiqueta a partir do commit atual. Você será solicitado a digitar um nome e uma descrição opcional. |
|
||||
| `` w `` | Nova árvore de trabalho | |
|
||||
| `` d `` | Apagar | Ver opções de exclusão para tag local/remoto. |
|
||||
| `` P `` | Empurrar etiqueta | Push the selected tag to a remote. You'll be prompted to select a remote. |
|
||||
| `` g `` | Restaurar | Ver opções de redefinição (soft/mixed/hard) para redefinir para o item selecionado. |
|
||||
| `` <ctrl+t> `` | Abrir ferramenta de diff externa (git difftool) | |
|
||||
| `` 0 `` | Focar visualização principal | |
|
||||
| `` <enter> `` | Ver commits | |
|
||||
| `` w `` | Ver opções da árvore de trabalho | |
|
||||
| `` / `` | Filtrar a visualização atual por texto | |
|
||||
|
||||
## Input prompt
|
||||
|
|
@ -310,6 +310,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | Abrir commit no navegador | |
|
||||
| `` n `` | Create new branch off of commit | |
|
||||
| `` N `` | Mover commits para uma nova branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | Nova árvore de trabalho | |
|
||||
| `` g `` | Restaurar | Ver opções de redefinição (soft/mixed/hard) para redefinir para o item selecionado. |
|
||||
| `` C `` | Copiar (cherry-pick) | Marcar commit como copiado. Então, dentro da visualização local de commits, você pode pressionar `V` para colar (cherry-pick) o(s) commit(s) copiado(s) em seu branch de check-out. A qualquer momento você pode pressionar `<esc>` para cancelar a seleção. |
|
||||
| `` <ctrl+r> `` | Reset copied (cherry-picked) commits selection | |
|
||||
|
|
@ -317,7 +318,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focar visualização principal | |
|
||||
| `` <enter> `` | Ver commits | |
|
||||
| `` w `` | Ver opções da árvore de trabalho | |
|
||||
| `` / `` | Filtrar a visualização atual por texto | |
|
||||
|
||||
## Remotes
|
||||
|
|
@ -348,10 +348,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` g `` | Pop | Aplique a entrada de stash no seu diretório de trabalho e remova a entrada de stash. |
|
||||
| `` d `` | Descartar | Remova a entrada do stash da lista de armazenamento. |
|
||||
| `` n `` | Nova branch | Criar um novo ramo a partir da entrada de lixo selecionada. Isso funciona verificando o commit do qual a entrada de lixo foi criada, criar um novo branch a partir desse commit e, em seguida, aplicar a entrada de lixo ao novo branch como um commit adicional. |
|
||||
| `` w `` | Nova árvore de trabalho | |
|
||||
| `` r `` | Renomear o stash | |
|
||||
| `` 0 `` | Focar visualização principal | |
|
||||
| `` <enter> `` | Ver arquivos | |
|
||||
| `` w `` | Ver opções da árvore de trabalho | |
|
||||
| `` / `` | Filtrar a visualização atual por texto | |
|
||||
|
||||
## Status
|
||||
|
|
@ -375,6 +375,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | Abrir commit no navegador | |
|
||||
| `` n `` | Create new branch off of commit | |
|
||||
| `` N `` | Mover commits para uma nova branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | Nova árvore de trabalho | |
|
||||
| `` g `` | Restaurar | Ver opções de redefinição (soft/mixed/hard) para redefinir para o item selecionado. |
|
||||
| `` C `` | Copiar (cherry-pick) | Marcar commit como copiado. Então, dentro da visualização local de commits, você pode pressionar `V` para colar (cherry-pick) o(s) commit(s) copiado(s) em seu branch de check-out. A qualquer momento você pode pressionar `<esc>` para cancelar a seleção. |
|
||||
| `` <ctrl+r> `` | Reset copied (cherry-picked) commits selection | |
|
||||
|
|
@ -382,7 +383,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focar visualização principal | |
|
||||
| `` <enter> `` | Ver arquivos | |
|
||||
| `` w `` | Ver opções da árvore de trabalho | |
|
||||
| `` / `` | Pesquisar na visualização atual por texto | |
|
||||
|
||||
## Submódulos
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | Открыть коммит в браузере | |
|
||||
| `` n `` | Создать новую ветку с этого коммита | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` g `` | Просмотреть параметры сброса | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | Скопировать отобранные коммит (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <ctrl+r> `` | Сбросить отобранную (скопированную \| cherry-picked) выборку коммитов | |
|
||||
|
|
@ -158,7 +159,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Просмотреть коммиты | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Коммиты
|
||||
|
|
@ -194,13 +194,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | Открыть коммит в браузере | |
|
||||
| `` n `` | Создать новую ветку с этого коммита | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` g `` | Просмотреть параметры сброса | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | Скопировать отобранные коммит (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Просмотреть файлы выбранного элемента | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Найти | |
|
||||
|
||||
## Локальные Ветки
|
||||
|
|
@ -212,6 +212,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <space> `` | Переключить | Checkout selected item. |
|
||||
| `` n `` | Новая ветка | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` o `` | Создать запрос на принятие изменений | |
|
||||
| `` O `` | Создать параметры запроса принятие изменений | |
|
||||
| `` G `` | Open pull request in browser | |
|
||||
|
|
@ -231,7 +232,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Просмотреть коммиты | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Меню
|
||||
|
|
@ -260,6 +260,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | Открыть коммит в браузере | |
|
||||
| `` n `` | Создать новую ветку с этого коммита | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` g `` | Просмотреть параметры сброса | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | Скопировать отобранные коммит (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <ctrl+r> `` | Сбросить отобранную (скопированную \| cherry-picked) выборку коммитов | |
|
||||
|
|
@ -267,7 +268,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Просмотреть файлы выбранного элемента | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Найти | |
|
||||
|
||||
## Подмодули
|
||||
|
|
@ -329,13 +329,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | Copy tag to clipboard | |
|
||||
| `` <space> `` | Переключить | Checkout the selected tag as a detached HEAD. |
|
||||
| `` n `` | Создать тег | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. |
|
||||
| `` w `` | New worktree | |
|
||||
| `` d `` | Delete | View delete options for local/remote tag. |
|
||||
| `` P `` | Отправить тег | Push the selected tag to a remote. You'll be prompted to select a remote. |
|
||||
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Просмотреть коммиты | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Удалённые ветки
|
||||
|
|
@ -345,6 +345,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | Скопировать название ветки в буфер обмена | |
|
||||
| `` <space> `` | Переключить | Checkout a new local branch based on the selected remote branch, or the remote branch as a detached head. |
|
||||
| `` n `` | Новая ветка | |
|
||||
| `` w `` | New worktree | |
|
||||
| `` M `` | Слияние с текущей переключённой веткой | View options for merging the selected item into the current branch (regular merge, squash merge) |
|
||||
| `` r `` | Перебазировать переключённую ветку на эту ветку | Rebase the checked-out branch onto the selected branch. |
|
||||
| `` d `` | Delete | Delete the remote branch from the remote. |
|
||||
|
|
@ -354,7 +355,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Просмотреть коммиты | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Удалённые репозитории
|
||||
|
|
@ -410,8 +410,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` g `` | Применить припрятанные изменения и тут же удалить их из хранилища | Apply the stash entry to your working directory and remove the stash entry. |
|
||||
| `` d `` | Удалить припрятанные изменения из хранилища | Remove the stash entry from the stash list. |
|
||||
| `` n `` | Новая ветка | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
|
||||
| `` w `` | New worktree | |
|
||||
| `` r `` | Переименовать хранилище | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Просмотреть файлы выбранного элемента | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | 在浏览器中打开提交 | |
|
||||
| `` n `` | 从提交创建新分支 | |
|
||||
| `` N `` | 移动提交至新分支 | 创建一个新分支,并将当前分支未推送的提交移动到该分支。如果您打算开始新工作但忘记先创建新分支,这会很有用。<br><br>请注意,此操作忽略选择,新分支总是从主分支创建或堆叠在当前分支之上(您可以选择哪种方式)。 |
|
||||
| `` w `` | 新建工作树 | |
|
||||
| `` g `` | 查看重置选项 | 查看重置选项 (soft/mixed/hard) 用于重置到选择项 |
|
||||
| `` C `` | 复制提交(拣选) | 标记提交为已复制。然后,在本地提交视图中,您可以按 `V` (Cherry-Pick) 将已复制的提交粘贴到已检出的分支中。任何时候都可以按 `<esc>` 来取消选择。 |
|
||||
| `` <ctrl+r> `` | 重置已拣选(复制)的提交 | |
|
||||
|
|
@ -69,7 +70,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | 选择当前分支的提交 | |
|
||||
| `` 0 `` | 聚焦主视图 | |
|
||||
| `` <enter> `` | 查看提交的文件 | |
|
||||
| `` w `` | 查看工作区选项 | |
|
||||
| `` / `` | 开始搜索 | |
|
||||
|
||||
## 子模块
|
||||
|
|
@ -106,6 +106,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | 在浏览器中打开提交 | |
|
||||
| `` n `` | 从提交创建新分支 | |
|
||||
| `` N `` | 移动提交至新分支 | 创建一个新分支,并将当前分支未推送的提交移动到该分支。如果您打算开始新工作但忘记先创建新分支,这会很有用。<br><br>请注意,此操作忽略选择,新分支总是从主分支创建或堆叠在当前分支之上(您可以选择哪种方式)。 |
|
||||
| `` w `` | 新建工作树 | |
|
||||
| `` g `` | 查看重置选项 | 查看重置选项 (soft/mixed/hard) 用于重置到选择项 |
|
||||
| `` C `` | 复制提交(拣选) | 标记提交为已复制。然后,在本地提交视图中,您可以按 `V` (Cherry-Pick) 将已复制的提交粘贴到已检出的分支中。任何时候都可以按 `<esc>` 来取消选择。 |
|
||||
| `` <ctrl+r> `` | 重置已拣选(复制)的提交 | |
|
||||
|
|
@ -113,7 +114,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | 选择当前分支的提交 | |
|
||||
| `` 0 `` | 聚焦主视图 | |
|
||||
| `` <enter> `` | 查看提交 | |
|
||||
| `` w `` | 查看工作区选项 | |
|
||||
| `` / `` | 通过文本过滤当前视图 | |
|
||||
|
||||
## 提交
|
||||
|
|
@ -149,13 +149,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | 在浏览器中打开提交 | |
|
||||
| `` n `` | 从提交创建新分支 | |
|
||||
| `` N `` | 移动提交至新分支 | 创建一个新分支,并将当前分支未推送的提交移动到该分支。如果您打算开始新工作但忘记先创建新分支,这会很有用。<br><br>请注意,此操作忽略选择,新分支总是从主分支创建或堆叠在当前分支之上(您可以选择哪种方式)。 |
|
||||
| `` w `` | 新建工作树 | |
|
||||
| `` g `` | 查看重置选项 | 查看重置选项 (soft/mixed/hard) 用于重置到选择项 |
|
||||
| `` C `` | 复制提交(拣选) | 标记提交为已复制。然后,在本地提交视图中,您可以按 `V` (Cherry-Pick) 将已复制的提交粘贴到已检出的分支中。任何时候都可以按 `<esc>` 来取消选择。 |
|
||||
| `` <ctrl+t> `` | 使用外部差异比较工具(git difftool) | |
|
||||
| `` * `` | 选择当前分支的提交 | |
|
||||
| `` 0 `` | 聚焦主视图 | |
|
||||
| `` <enter> `` | 查看提交的文件 | |
|
||||
| `` w `` | 查看工作区选项 | |
|
||||
| `` / `` | 开始搜索 | |
|
||||
|
||||
## 提交信息
|
||||
|
|
@ -227,6 +227,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <space> `` | 检出 | 检出选中的项目 |
|
||||
| `` n `` | 新分支 | |
|
||||
| `` N `` | 移动提交至新分支 | 创建一个新分支,并将当前分支未推送的提交移动到该分支。如果您打算开始新工作但忘记先创建新分支,这会很有用。<br><br>请注意,此操作忽略选择,新分支总是从主分支创建或堆叠在当前分支之上(您可以选择哪种方式)。 |
|
||||
| `` w `` | 新建工作树 | |
|
||||
| `` o `` | 创建拉取请求 | |
|
||||
| `` O `` | 创建拉取请求选项 | |
|
||||
| `` G `` | 在浏览器中打开拉取请求 | |
|
||||
|
|
@ -246,7 +247,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | 使用外部差异比较工具(git difftool) | |
|
||||
| `` 0 `` | 聚焦主视图 | |
|
||||
| `` <enter> `` | 查看提交 | |
|
||||
| `` w `` | 查看工作区选项 | |
|
||||
| `` / `` | 通过文本过滤当前视图 | |
|
||||
|
||||
## 构建补丁中
|
||||
|
|
@ -272,13 +272,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | 复制标签到剪贴板 | |
|
||||
| `` <space> `` | 检出 | 检出选择的标签作为分离的HEAD |
|
||||
| `` n `` | 创建标签 | 基于当前提交创建一个新标签。您将在弹窗中输入标签名称和描述(可选)。 |
|
||||
| `` w `` | 新建工作树 | |
|
||||
| `` d `` | 删除 | 查看本地/远程标签的删除选项 |
|
||||
| `` P `` | 推送标签 | 推送选择的标签到远端。您将在弹窗中选择一个远端。 |
|
||||
| `` g `` | 重置 | 查看重置选项 (soft/mixed/hard) 用于重置到选择项 |
|
||||
| `` <ctrl+t> `` | 使用外部差异比较工具(git difftool) | |
|
||||
| `` 0 `` | 聚焦主视图 | |
|
||||
| `` <enter> `` | 查看提交 | |
|
||||
| `` w `` | 查看工作区选项 | |
|
||||
| `` / `` | 通过文本过滤当前视图 | |
|
||||
|
||||
## 次要
|
||||
|
|
@ -372,10 +372,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` g `` | 应用并删除 | 将存储项应用到工作目录并删除存储项。 |
|
||||
| `` d `` | 删除 | 从贮藏列表中删除该贮藏项 |
|
||||
| `` n `` | 新分支 | 从选定的贮藏项创建一个新分支。这是通过 git 检查创建贮藏项的提交,从该提交创建一个新分支,然后将贮藏项作为附加提交应用到新分支来实现的。 |
|
||||
| `` w `` | 新建工作树 | |
|
||||
| `` r `` | 重命名贮藏 | |
|
||||
| `` 0 `` | 聚焦主视图 | |
|
||||
| `` <enter> `` | 查看提交的文件 | |
|
||||
| `` w `` | 查看工作区选项 | |
|
||||
| `` / `` | 通过文本过滤当前视图 | |
|
||||
|
||||
## 输入提示
|
||||
|
|
@ -404,6 +404,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | 复制分支名称到剪贴板 | |
|
||||
| `` <space> `` | 检出 | 基于当前选中的远程分支检出一个新的本地分支,或者将远程分支作分离的HEAD。 |
|
||||
| `` n `` | 新分支 | |
|
||||
| `` w `` | 新建工作树 | |
|
||||
| `` M `` | 合并到当前检出的分支 | 查看将选中项合并到当前分支的选项(正常合并,压缩合并) |
|
||||
| `` r `` | 变基 | 将检出的分支变基到所选的分支上。 |
|
||||
| `` d `` | 删除 | 从远程删除远程分支。 |
|
||||
|
|
@ -413,5 +414,4 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | 使用外部差异比较工具(git difftool) | |
|
||||
| `` 0 `` | 聚焦主视图 | |
|
||||
| `` <enter> `` | 查看提交 | |
|
||||
| `` w `` | 查看工作区选项 | |
|
||||
| `` / `` | 通过文本过滤当前视图 | |
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | 在瀏覽器中開啟提交 | |
|
||||
| `` n `` | 從提交建立新分支 | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` g `` | 檢視重設選項 | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | 複製提交 (揀選) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <ctrl+r> `` | 重設選定的揀選 (複製) 提交 | |
|
||||
|
|
@ -148,7 +149,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 檢視所選項目的檔案 | |
|
||||
| `` w `` | 檢視工作目錄選項 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 子模組
|
||||
|
|
@ -208,13 +208,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | 在瀏覽器中開啟提交 | |
|
||||
| `` n `` | 從提交建立新分支 | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` g `` | 檢視重設選項 | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | 複製提交 (揀選) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 檢視所選項目的檔案 | |
|
||||
| `` w `` | 檢視工作目錄選項 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 提交摘要
|
||||
|
|
@ -252,10 +252,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` g `` | 還原 | Apply the stash entry to your working directory and remove the stash entry. |
|
||||
| `` d `` | 捨棄 | Remove the stash entry from the stash list. |
|
||||
| `` n `` | 新分支 | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
|
||||
| `` w `` | New worktree | |
|
||||
| `` r `` | 重新命名收藏 | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 檢視所選項目的檔案 | |
|
||||
| `` w `` | 檢視工作目錄選項 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 日誌
|
||||
|
|
@ -268,6 +268,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | 在瀏覽器中開啟提交 | |
|
||||
| `` n `` | 從提交建立新分支 | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` g `` | 檢視重設選項 | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | 複製提交 (揀選) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <ctrl+r> `` | 重設選定的揀選 (複製) 提交 | |
|
||||
|
|
@ -275,7 +276,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 檢視提交 | |
|
||||
| `` w `` | 檢視工作目錄選項 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 本地分支
|
||||
|
|
@ -287,6 +287,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <space> `` | 檢出 | 檢出選定的項目。 |
|
||||
| `` n `` | 新分支 | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` o `` | 建立拉取請求 | |
|
||||
| `` O `` | 建立拉取請求選項 | |
|
||||
| `` G `` | Open pull request in browser | |
|
||||
|
|
@ -306,7 +307,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 檢視提交 | |
|
||||
| `` w `` | 檢視工作目錄選項 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 標籤
|
||||
|
|
@ -316,13 +316,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | Copy tag to clipboard | |
|
||||
| `` <space> `` | 檢出 | Checkout the selected tag as a detached HEAD. |
|
||||
| `` n `` | 建立標籤 | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. |
|
||||
| `` w `` | New worktree | |
|
||||
| `` d `` | 刪除 | View delete options for local/remote tag. |
|
||||
| `` P `` | 推送標籤 | Push the selected tag to a remote. You'll be prompted to select a remote. |
|
||||
| `` g `` | 重設 | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 檢視提交 | |
|
||||
| `` w `` | 檢視工作目錄選項 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 檔案
|
||||
|
|
@ -404,6 +404,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | 複製分支名稱到剪貼簿 | |
|
||||
| `` <space> `` | 檢出 | Checkout a new local branch based on the selected remote branch, or the remote branch as a detached head. |
|
||||
| `` n `` | 新分支 | |
|
||||
| `` w `` | New worktree | |
|
||||
| `` M `` | 合併到當前檢出的分支 | View options for merging the selected item into the current branch (regular merge, squash merge) |
|
||||
| `` r `` | 將已檢出的分支變基至此分支 | Rebase the checked-out branch onto the selected branch. |
|
||||
| `` d `` | 刪除 | Delete the remote branch from the remote. |
|
||||
|
|
@ -413,5 +414,4 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 檢視提交 | |
|
||||
| `` w `` | 檢視工作目錄選項 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
|
|
|||
|
|
@ -110,6 +110,26 @@ gui:
|
|||
# is true.
|
||||
expandedSidePanelWeight: 2
|
||||
|
||||
# If true, don't give a side panel more height than it needs to show its
|
||||
# content; when all panels fit, the leftover height is shared among them so that
|
||||
# they still fill the screen.
|
||||
shrinkSidePanelsToContent: false
|
||||
|
||||
# The side panels, in the order they appear from top to bottom.
|
||||
# Each entry is a list of one or more names that share a single panel as tabs
|
||||
# (cycle through them with the next-tab/previous-tab keys).
|
||||
# Omit a name to hide it; give a name its own one-element list to promote a tab
|
||||
# to a top-level panel.
|
||||
# Valid names are: 'status', 'files', 'worktrees', 'submodules', 'branches',
|
||||
# 'remotes', 'tags', 'commits', 'reflog', 'stash'. 'files', 'branches', and
|
||||
# 'commits' must always be included; they can't be hidden.
|
||||
sidePanels:
|
||||
- [status]
|
||||
- [files, worktrees, submodules]
|
||||
- [branches, remotes, tags]
|
||||
- [commits, reflog]
|
||||
- [stash]
|
||||
|
||||
# Sometimes the main window is split in two (e.g. when the selected file has
|
||||
# both staged and unstaged changes). This setting controls how the two sections
|
||||
# are split.
|
||||
|
|
@ -342,6 +362,11 @@ gui:
|
|||
git:
|
||||
# Array of pagers. Each entry has the following format:
|
||||
#
|
||||
# # A name for the pager, shown in the notification when cycling pagers.
|
||||
# # If not set, the name is derived from the first word of the pager
|
||||
# # command (or of the external diff command).
|
||||
# name: ""
|
||||
#
|
||||
# # Value of the --color arg in the git diff command. Some pagers want
|
||||
# # this to be set to 'always' and some want it set to 'never'
|
||||
# colorArg: "always"
|
||||
|
|
@ -361,6 +386,9 @@ git:
|
|||
# # https://git-scm.com/docs/gitattributes#_defining_an_external_diff_driver.
|
||||
# useExternalDiffGitConfig: false
|
||||
#
|
||||
# 'pager', 'externalDiffCommand', and 'useExternalDiffGitConfig' are mutually
|
||||
# exclusive; set at most one per entry.
|
||||
#
|
||||
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Custom_Pagers.md
|
||||
# for more information.
|
||||
pagers: []
|
||||
|
|
@ -406,6 +434,11 @@ git:
|
|||
# If true, periodically refresh files and submodules
|
||||
autoRefresh: true
|
||||
|
||||
# If true, poll the repo periodically for external ref changes (commits, branch
|
||||
# updates, checkouts made outside lazygit) and refresh when one is detected.
|
||||
# Independent of autoRefresh, which only governs the files panel.
|
||||
autoDetectExternalChanges: true
|
||||
|
||||
# If not "none", lazygit will automatically fast-forward local branches to match
|
||||
# their upstream after fetching. Applies to branches that are not the currently
|
||||
# checked out branch, and only to those that are strictly behind their upstream
|
||||
|
|
@ -499,6 +532,15 @@ git:
|
|||
# to 40 to disable truncation.
|
||||
truncateCopiedCommitHashesTo: 12
|
||||
|
||||
# Config relating to git worktrees
|
||||
worktree:
|
||||
# Default parent directory for new worktrees. It is offered as a candidate
|
||||
# location alongside the parent directories of any worktrees you already have.
|
||||
# A relative path is resolved against the repository's root directory, so
|
||||
# "../worktrees" sits beside the repo and ".worktrees" sits inside it.
|
||||
# A leading "~" is expanded to your home directory, so "~/worktrees" works.
|
||||
defaultPath: ""
|
||||
|
||||
# Periodic update checks
|
||||
update:
|
||||
# One of: 'prompt' (default) | 'background' | 'never'
|
||||
|
|
@ -517,6 +559,11 @@ refresher:
|
|||
# Auto-fetch can be disabled via option 'git.autoFetch'.
|
||||
fetchInterval: 60
|
||||
|
||||
# Interval in seconds at which lazygit polls for external ref changes (commits,
|
||||
# branch updates, checkouts made outside lazygit).
|
||||
# Detection can be disabled via option 'git.autoDetectExternalChanges'.
|
||||
externalChangeCheckInterval: 2
|
||||
|
||||
# If true, show a confirmation popup before quitting Lazygit
|
||||
confirmOnQuit: false
|
||||
|
||||
|
|
@ -648,6 +695,7 @@ keybinding:
|
|||
confirmInEditor: [<ctrl+enter>, <ctrl+s>]
|
||||
remove: d
|
||||
new: "n"
|
||||
newWorktree: w
|
||||
edit: e
|
||||
openFile: o
|
||||
scrollUpMain: [<pgup>, K, <ctrl+u>]
|
||||
|
|
@ -667,6 +715,7 @@ keybinding:
|
|||
nextScreenMode: +
|
||||
prevScreenMode: _
|
||||
cyclePagers: '|'
|
||||
cyclePagersReverse: \
|
||||
undo: z
|
||||
redo: Z
|
||||
filteringMenu: <ctrl+s>
|
||||
|
|
@ -681,6 +730,7 @@ keybinding:
|
|||
increaseRenameSimilarityThreshold: )
|
||||
decreaseRenameSimilarityThreshold: (
|
||||
openDiffTool: <ctrl+t>
|
||||
editConfig: <alt+shift+c>
|
||||
status:
|
||||
checkForUpdate: u
|
||||
recentRepos: <enter>
|
||||
|
|
@ -726,8 +776,6 @@ keybinding:
|
|||
fetchRemote: f
|
||||
addForkRemote: F
|
||||
sortOrder: s
|
||||
worktrees:
|
||||
viewWorktreeOptions: w
|
||||
commits:
|
||||
squashDown: s
|
||||
renameCommit: r
|
||||
|
|
@ -1059,6 +1107,12 @@ keybinding:
|
|||
edit: <disabled> # disable 'edit file'
|
||||
```
|
||||
|
||||
### Overriding the platform for default keybindings
|
||||
|
||||
A few keybindings have different defaults on macOS than on Linux and Windows (e.g. word-wise cursor movement in text inputs uses `alt` on macOS but `ctrl` elsewhere). Lazygit picks these based on the OS it's running on, but you can override that with the `LAZYGIT_KEYBINDING_PLATFORM` environment variable. Set it to `darwin`, `linux`, or `windows`; any other value is ignored and the actual OS is used.
|
||||
|
||||
This is useful when running lazygit in a Linux container that you access over ssh from a Mac, where you'd rather use the macOS keybindings.
|
||||
|
||||
### Example Keybindings For Colemak Users
|
||||
|
||||
```yaml
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
Lazygit supports custom pagers, [configured](/docs/Config.md) in the config.yml file (which can be opened by pressing `e` in the Status panel).
|
||||
|
||||
Support does not extend to Windows users, because we're making use of a package which doesn't have Windows support. However, see [below](#emulating-custom-pagers-on-windows) for a workaround.
|
||||
|
||||
Multiple pagers are supported; you can cycle through them with the `|` key. This can be useful if you usually prefer a particular pager, but want to use a different one for certain kinds of diffs.
|
||||
|
||||
Pagers are configured with the `pagers` array in the git section; here's an example for a multi-pager setup (use an empty object `{}` for the default builtin diff display that doesn't use a pager):
|
||||
|
|
@ -71,7 +69,7 @@ git:
|
|||
- externalDiffCommand: difft --color=always
|
||||
```
|
||||
|
||||
The `colorArg` and `pager` options are not used in this case.
|
||||
The `colorArg` option is not used in this case.
|
||||
|
||||
You can add whatever extra arguments you prefer for your difftool; for instance
|
||||
|
||||
|
|
@ -81,6 +79,22 @@ git:
|
|||
- externalDiffCommand: difft --color=always --display=inline --syntax-highlight=off
|
||||
```
|
||||
|
||||
This can also be used for normal git diffs with custom parameters, such as `--color-words` or `--word-diff` which some people find useful. To do that, save a script like this to, say, `~/bin/color-words.sh`:
|
||||
|
||||
```sh
|
||||
#!/bin/sh
|
||||
|
||||
git diff --color-words --no-index --color=always --no-ext-diff "$2" "$5"
|
||||
```
|
||||
|
||||
And then use it in your git config like so:
|
||||
|
||||
```yaml
|
||||
git:
|
||||
pagers:
|
||||
- externalDiffCommand: ~/bin/color-words.sh
|
||||
```
|
||||
|
||||
Instead of setting this command in lazygit's `externalDiffCommand` config, you can also tell lazygit to use the external diff command that is configured in git itself (`diff.external`), by using
|
||||
|
||||
```yaml
|
||||
|
|
@ -91,29 +105,4 @@ git:
|
|||
|
||||
This can be useful if you also want to use it for diffs on the command line, and it also has the advantage that you can configure it per file type in `.gitattributes`; see https://git-scm.com/docs/gitattributes#_defining_an_external_diff_driver.
|
||||
|
||||
## Emulating custom pagers on Windows
|
||||
|
||||
There is a trick to emulate custom pagers on Windows using a Powershell script configured as an external diff command. It's not perfect, but certainly better than nothing. To do this, save the following script as `lazygit-pager.ps1` at a convenient place on your disk:
|
||||
|
||||
```pwsh
|
||||
#!/usr/bin/env pwsh
|
||||
|
||||
$old = $args[1].Replace('\', '/')
|
||||
$new = $args[4].Replace('\', '/')
|
||||
$path = $args[0]
|
||||
git diff --no-index --no-ext-diff $old $new
|
||||
| %{ $_.Replace($old, $path).Replace($new, $path) }
|
||||
| delta --width=$env:LAZYGIT_COLUMNS
|
||||
```
|
||||
|
||||
Use the pager of your choice with the arguments you like in the last line of the script. Personally I wouldn't want to use lazygit anymore without delta's `--hyperlinks --hyperlinks-file-link-format="lazygit-edit://{path}:{line}"` args, see [above](#delta).
|
||||
|
||||
In your lazygit config, use
|
||||
|
||||
```yml
|
||||
git:
|
||||
pagers:
|
||||
- externalDiffCommand: "C:/wherever/lazygit-pager.ps1"
|
||||
```
|
||||
|
||||
The main limitation of this approach compared to a "real" pager is that renames are not displayed correctly; they are shown as if they were modifications of the old file. (This affects only the hunk headers; the diff itself is always correct.)
|
||||
`pager`, `externalDiffCommand`, and `useExternalDiffGitConfig` are alternative ways of producing the diff, so a pager entry may use at most one of them.
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` R `` | Refresh | Refresh the git state (i.e. run `git status`, `git branch`, etc in background to update the contents of panels). This does not run `git fetch`. |
|
||||
| `` + `` | Next screen mode (normal/half/fullscreen) | |
|
||||
| `` _ `` | Prev screen mode | |
|
||||
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers |
|
||||
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
|
||||
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
|
||||
| `` <esc> `` | Cancel | |
|
||||
| `` ? `` | Open keybindings menu | |
|
||||
| `` <ctrl+s> `` | View filter options | View options for filtering the commit log, so that only commits matching the filter are shown. |
|
||||
|
|
@ -30,6 +31,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` q, <ctrl+c> `` | Quit | |
|
||||
| `` <ctrl+z> `` | Suspend the application | |
|
||||
| `` <ctrl+w> `` | Toggle whitespace | Toggle whether or not whitespace changes are shown in the diff view.<br><br>The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. |
|
||||
| `` <alt+shift+c> `` | Edit config file | Open file in external editor. |
|
||||
| `` z `` | Undo | The reflog will be used to determine what git command to run to undo the last git command. This does not include changes to the working tree; only commits are taken into consideration. |
|
||||
| `` Z `` | Redo | The reflog will be used to determine what git command to run to redo the last git command. This does not include changes to the working tree; only commits are taken into consideration. |
|
||||
|
||||
|
|
@ -110,13 +112,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | Open commit in browser | |
|
||||
| `` n `` | Create new branch off of commit | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | Copy (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View files | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Search the current view by text | |
|
||||
|
||||
## Confirmation panel
|
||||
|
|
@ -176,6 +178,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <space> `` | Checkout | Checkout selected item. |
|
||||
| `` n `` | New branch | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` o `` | Create pull request | |
|
||||
| `` O `` | View create pull request options | |
|
||||
| `` G `` | Open pull request in browser | |
|
||||
|
|
@ -195,7 +198,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View commits | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Main panel (merging)
|
||||
|
|
@ -203,7 +205,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <space> `` | Pick hunk | |
|
||||
| `` b `` | Pick all hunks | |
|
||||
| `` b `` | Pick both hunks | |
|
||||
| `` <up>, k `` | Previous hunk | |
|
||||
| `` <down>, j `` | Next hunk | |
|
||||
| `` <left>, h `` | Previous conflict | |
|
||||
|
|
@ -280,6 +282,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | Open commit in browser | |
|
||||
| `` n `` | Create new branch off of commit | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | Copy (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <ctrl+r> `` | Reset copied (cherry-picked) commits selection | |
|
||||
|
|
@ -287,7 +290,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View commits | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Remote branches
|
||||
|
|
@ -297,6 +299,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | Copy branch name to clipboard | |
|
||||
| `` <space> `` | Checkout | Checkout a new local branch based on the selected remote branch, or the remote branch as a detached head. |
|
||||
| `` n `` | New branch | |
|
||||
| `` w `` | New worktree | |
|
||||
| `` M `` | Merge | View options for merging the selected item into the current branch (regular merge, squash merge) |
|
||||
| `` r `` | Rebase | Rebase the checked-out branch onto the selected branch. |
|
||||
| `` d `` | Delete | Delete the remote branch from the remote. |
|
||||
|
|
@ -306,7 +309,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View commits | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Remotes
|
||||
|
|
@ -337,17 +339,16 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` g `` | Pop | Apply the stash entry to your working directory and remove the stash entry. |
|
||||
| `` d `` | Drop | Remove the stash entry from the stash list. |
|
||||
| `` n `` | New branch | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
|
||||
| `` w `` | New worktree | |
|
||||
| `` r `` | Rename stash | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View files | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Status
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` o `` | Open config file | Open file in default application. |
|
||||
| `` e `` | Edit config file | Open file in external editor. |
|
||||
| `` u `` | Check for update | |
|
||||
| `` <enter> `` | Switch to a recent repo | |
|
||||
|
|
@ -365,6 +366,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | Open commit in browser | |
|
||||
| `` n `` | Create new branch off of commit | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | Copy (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <ctrl+r> `` | Reset copied (cherry-picked) commits selection | |
|
||||
|
|
@ -372,7 +374,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View files | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Search the current view by text | |
|
||||
|
||||
## Submodules
|
||||
|
|
@ -396,13 +397,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | Copy tag to clipboard | |
|
||||
| `` <space> `` | Checkout | Checkout the selected tag as a detached HEAD. |
|
||||
| `` n `` | New tag | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. |
|
||||
| `` w `` | New worktree | |
|
||||
| `` d `` | Delete | View delete options for local/remote tag. |
|
||||
| `` P `` | Push tag | Push the selected tag to a remote. You'll be prompted to select a remote. |
|
||||
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View commits | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Worktrees
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` R `` | 更新 | Gitの状態を更新します(`git status`、`git branch`などをバックグラウンドで実行してパネルの内容を更新します)。これは`git fetch`を実行しません。 |
|
||||
| `` + `` | 次の画面モード(通常/半分/全画面) | |
|
||||
| `` _ `` | 前の画面モード | |
|
||||
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers |
|
||||
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
|
||||
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
|
||||
| `` <esc> `` | キャンセル | |
|
||||
| `` ? `` | キーバインディングメニューを開く | |
|
||||
| `` <ctrl+s> `` | フィルターオプションを表示 | コミットログのフィルタリングオプションを表示し、フィルタに一致するコミットのみを表示します。 |
|
||||
|
|
@ -30,6 +31,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` q, <ctrl+c> `` | 終了 | |
|
||||
| `` <ctrl+z> `` | Suspend the application | |
|
||||
| `` <ctrl+w> `` | 空白表示の切り替え | Toggle whether or not whitespace changes are shown in the diff view.<br><br>The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. |
|
||||
| `` <alt+shift+c> `` | 設定ファイルを編集 | 外部エディタでファイルを開きます。 |
|
||||
| `` z `` | 元に戻す | 最後のgitコマンドを元に戻すために実行するgitコマンドを決定するためにreflogが使用されます。これにはワーキングツリーへの変更は含まれません。コミットのみが考慮されます。 |
|
||||
| `` Z `` | やり直す | 最後のgitコマンドをやり直すために実行するgitコマンドを決定するためにreflogが使用されます。これにはワーキングツリーへの変更は含まれません。コミットのみが考慮されます。 |
|
||||
|
||||
|
|
@ -90,13 +92,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | ブラウザでコミットを開く | |
|
||||
| `` n `` | コミットから新しいブランチを作成 | |
|
||||
| `` N `` | コミットを新しいブランチに移動 | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | 新しいワークツリー | |
|
||||
| `` g `` | リセット | 選択した項目へのリセットオプション(ソフト/ミックス/ハード)を表示します。各リセットタイプの詳細は次の通りです:<br>- ソフトリセット:変更を保持し、ステージされた状態にします<br>- ミックスリセット:変更を保持し、ステージされていない状態にします<br>- ハードリセット:すべての変更を破棄します |
|
||||
| `` C `` | コピー(チェリーピック) | コミットをコピーとしてマークします。ローカルコミットビューで `V` を押すと、コピーしたコミットをチェックアウトしたブランチにペースト(チェリーピック)できます。いつでも `<esc>` を押して選択をキャンセルできます。 |
|
||||
| `` <ctrl+t> `` | 外部差分ツールを開く(git difftool) | |
|
||||
| `` * `` | 現在のブランチのコミットを選択 | |
|
||||
| `` 0 `` | メインビューにフォーカス | |
|
||||
| `` <enter> `` | ファイルを表示 | |
|
||||
| `` w `` | ワークツリーオプションを表示 | |
|
||||
| `` / `` | 現在のビューをテキストで検索 | |
|
||||
|
||||
## コミットファイル
|
||||
|
|
@ -136,6 +138,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | ブラウザでコミットを開く | |
|
||||
| `` n `` | コミットから新しいブランチを作成 | |
|
||||
| `` N `` | コミットを新しいブランチに移動 | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | 新しいワークツリー | |
|
||||
| `` g `` | リセット | 選択した項目へのリセットオプション(ソフト/ミックス/ハード)を表示します。各リセットタイプの詳細は次の通りです:<br>- ソフトリセット:変更を保持し、ステージされた状態にします<br>- ミックスリセット:変更を保持し、ステージされていない状態にします<br>- ハードリセット:すべての変更を破棄します |
|
||||
| `` C `` | コピー(チェリーピック) | コミットをコピーとしてマークします。ローカルコミットビューで `V` を押すと、コピーしたコミットをチェックアウトしたブランチにペースト(チェリーピック)できます。いつでも `<esc>` を押して選択をキャンセルできます。 |
|
||||
| `` <ctrl+r> `` | コピーされた(チェリーピックされた)コミットの選択をリセット | |
|
||||
|
|
@ -143,7 +146,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | 現在のブランチのコミットを選択 | |
|
||||
| `` 0 `` | メインビューにフォーカス | |
|
||||
| `` <enter> `` | ファイルを表示 | |
|
||||
| `` w `` | ワークツリーオプションを表示 | |
|
||||
| `` / `` | 現在のビューをテキストで検索 | |
|
||||
|
||||
## サブモジュール
|
||||
|
|
@ -168,17 +170,16 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` g `` | ポップ | スタッシュエントリをワーキングディレクトリに適用し、スタッシュエントリを削除します。 |
|
||||
| `` d `` | 削除 | スタッシュリストからスタッシュエントリを削除します。 |
|
||||
| `` n `` | 新しいブランチ | 選択したスタッシュエントリから新しいブランチを作成します。これは、スタッシュエントリが作成されたコミットをgitがチェックアウトし、そのコミットから新しいブランチを作成した後、スタッシュエントリを追加のコミットとして新しいブランチに適用することで機能します。 |
|
||||
| `` w `` | 新しいワークツリー | |
|
||||
| `` r `` | スタッシュの名前を変更 | |
|
||||
| `` 0 `` | メインビューにフォーカス | |
|
||||
| `` <enter> `` | ファイルを表示 | |
|
||||
| `` w `` | ワークツリーオプションを表示 | |
|
||||
| `` / `` | 現在のビューをテキストでフィルタリング | |
|
||||
|
||||
## ステータス
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` o `` | 設定ファイルを開く | デフォルトのアプリケーションでファイルを開きます。 |
|
||||
| `` e `` | 設定ファイルを編集 | 外部エディタでファイルを開きます。 |
|
||||
| `` u `` | 更新を確認 | |
|
||||
| `` <enter> `` | 最近のリポジトリをチェックアウト | |
|
||||
|
|
@ -201,13 +202,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | タグをクリップボードにコピー | |
|
||||
| `` <space> `` | チェックアウト(ブランチの切り替え) | 選択したタグをデタッチドHEADとしてチェックアウトします。 |
|
||||
| `` n `` | 新しいタグを作成 | 現在のコミットから新しいタグを作成します。タグ名とオプションの説明を入力するよう促されます。 |
|
||||
| `` w `` | 新しいワークツリー | |
|
||||
| `` d `` | 削除 | ローカル/リモートタグの削除オプションを表示します。 |
|
||||
| `` P `` | タグをプッシュ | 選択したタグをリモートにプッシュします。リモートを選択するよう促されます。 |
|
||||
| `` g `` | リセット | 選択した項目へのリセットオプション(ソフト/ミックス/ハード)を表示します。各リセットタイプの詳細は次の通りです:<br>- ソフトリセット:変更を保持し、ステージされた状態にします<br>- ミックスリセット:変更を保持し、ステージされていない状態にします<br>- ハードリセット:すべての変更を破棄します |
|
||||
| `` <ctrl+t> `` | 外部差分ツールを開く(git difftool) | |
|
||||
| `` 0 `` | メインビューにフォーカス | |
|
||||
| `` <enter> `` | コミットを表示 | |
|
||||
| `` w `` | ワークツリーオプションを表示 | |
|
||||
| `` / `` | 現在のビューをテキストでフィルタリング | |
|
||||
|
||||
## ファイル
|
||||
|
|
@ -286,7 +287,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <space> `` | ハンクを選択 | |
|
||||
| `` b `` | すべてのハンクを選択 | |
|
||||
| `` b `` | Pick both hunks | |
|
||||
| `` <up>, k `` | 前のハンク | |
|
||||
| `` <down>, j `` | 次のハンク | |
|
||||
| `` <left>, h `` | 前のコンフリクト | |
|
||||
|
|
@ -325,6 +326,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | ブラウザでコミットを開く | |
|
||||
| `` n `` | コミットから新しいブランチを作成 | |
|
||||
| `` N `` | コミットを新しいブランチに移動 | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | 新しいワークツリー | |
|
||||
| `` g `` | リセット | 選択した項目へのリセットオプション(ソフト/ミックス/ハード)を表示します。各リセットタイプの詳細は次の通りです:<br>- ソフトリセット:変更を保持し、ステージされた状態にします<br>- ミックスリセット:変更を保持し、ステージされていない状態にします<br>- ハードリセット:すべての変更を破棄します |
|
||||
| `` C `` | コピー(チェリーピック) | コミットをコピーとしてマークします。ローカルコミットビューで `V` を押すと、コピーしたコミットをチェックアウトしたブランチにペースト(チェリーピック)できます。いつでも `<esc>` を押して選択をキャンセルできます。 |
|
||||
| `` <ctrl+r> `` | コピーされた(チェリーピックされた)コミットの選択をリセット | |
|
||||
|
|
@ -332,7 +334,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | 現在のブランチのコミットを選択 | |
|
||||
| `` 0 `` | メインビューにフォーカス | |
|
||||
| `` <enter> `` | コミットを表示 | |
|
||||
| `` w `` | ワークツリーオプションを表示 | |
|
||||
| `` / `` | 現在のビューをテキストでフィルタリング | |
|
||||
|
||||
## リモート
|
||||
|
|
@ -354,6 +355,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | ブランチ名をクリップボードにコピー | |
|
||||
| `` <space> `` | チェックアウト(ブランチの切り替え) | 選択したリモートブランチに基づいて新しいローカルブランチをチェックアウトするか、リモートブランチをデタッチドヘッドとしてチェックアウトします。 |
|
||||
| `` n `` | 新しいブランチ | |
|
||||
| `` w `` | 新しいワークツリー | |
|
||||
| `` M `` | マージ | 選択した項目を現在のブランチにマージするためのオプションを表示します(通常のマージ、スカッシュマージ) |
|
||||
| `` r `` | リベース | チェックアウトしたブランチを選択したブランチ上にリベースします。 |
|
||||
| `` d `` | 削除 | リモートからリモートブランチを削除します。 |
|
||||
|
|
@ -363,7 +365,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | 外部差分ツールを開く(git difftool) | |
|
||||
| `` 0 `` | メインビューにフォーカス | |
|
||||
| `` <enter> `` | コミットを表示 | |
|
||||
| `` w `` | ワークツリーオプションを表示 | |
|
||||
| `` / `` | 現在のビューをテキストでフィルタリング | |
|
||||
|
||||
## ローカルブランチ
|
||||
|
|
@ -375,6 +376,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <space> `` | チェックアウト(ブランチの切り替え) | 選択した項目をチェックアウトします。 |
|
||||
| `` n `` | 新しいブランチ | |
|
||||
| `` N `` | コミットを新しいブランチに移動 | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | 新しいワークツリー | |
|
||||
| `` o `` | プルリクエストを作成 | |
|
||||
| `` O `` | プルリクエスト作成オプションを表示 | |
|
||||
| `` G `` | Open pull request in browser | |
|
||||
|
|
@ -394,7 +396,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | 外部差分ツールを開く(git difftool) | |
|
||||
| `` 0 `` | メインビューにフォーカス | |
|
||||
| `` <enter> `` | コミットを表示 | |
|
||||
| `` w `` | ワークツリーオプションを表示 | |
|
||||
| `` / `` | 現在のビューをテキストでフィルタリング | |
|
||||
|
||||
## ワークツリー
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` R `` | 새로고침 | Refresh the git state (i.e. run `git status`, `git branch`, etc in background to update the contents of panels). This does not run `git fetch`. |
|
||||
| `` + `` | 다음 스크린 모드 (normal/half/fullscreen) | |
|
||||
| `` _ `` | 이전 스크린 모드 | |
|
||||
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers |
|
||||
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
|
||||
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
|
||||
| `` <esc> `` | 취소 | |
|
||||
| `` ? `` | 매뉴 열기 | |
|
||||
| `` <ctrl+s> `` | View filter-by-path options | View options for filtering the commit log, so that only commits matching the filter are shown. |
|
||||
|
|
@ -30,6 +31,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` q, <ctrl+c> `` | 종료 | |
|
||||
| `` <ctrl+z> `` | Suspend the application | |
|
||||
| `` <ctrl+w> `` | 공백문자를 Diff 뷰에서 표시 여부 전환 | Toggle whether or not whitespace changes are shown in the diff view.<br><br>The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. |
|
||||
| `` <alt+shift+c> `` | 설정 파일 수정 | Open file in external editor. |
|
||||
| `` z `` | 되돌리기 (reflog) (실험적) | The reflog will be used to determine what git command to run to undo the last git command. This does not include changes to the working tree; only commits are taken into consideration. |
|
||||
| `` Z `` | 다시 실행 (reflog) (실험적) | The reflog will be used to determine what git command to run to redo the last git command. This does not include changes to the working tree; only commits are taken into consideration. |
|
||||
|
||||
|
|
@ -67,6 +69,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | 브라우저에서 커밋 열기 | |
|
||||
| `` n `` | 커밋에서 새 브랜치를 만듭니다. | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` g `` | View reset options | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | 커밋을 복사 (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <ctrl+r> `` | Reset cherry-picked (copied) commits selection | |
|
||||
|
|
@ -74,7 +77,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 커밋 보기 | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Secondary
|
||||
|
|
@ -93,10 +95,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` g `` | Pop | Apply the stash entry to your working directory and remove the stash entry. |
|
||||
| `` d `` | Drop | Remove the stash entry from the stash list. |
|
||||
| `` n `` | 새 브랜치 생성 | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
|
||||
| `` w `` | New worktree | |
|
||||
| `` r `` | Rename stash | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View selected item's files | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Sub-commits
|
||||
|
|
@ -109,6 +111,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | 브라우저에서 커밋 열기 | |
|
||||
| `` n `` | 커밋에서 새 브랜치를 만듭니다. | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` g `` | View reset options | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | 커밋을 복사 (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <ctrl+r> `` | Reset cherry-picked (copied) commits selection | |
|
||||
|
|
@ -116,7 +119,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View selected item's files | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | 검색 시작 | |
|
||||
|
||||
## Worktrees
|
||||
|
|
@ -142,7 +144,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <space> `` | Pick hunk | |
|
||||
| `` b `` | Pick all hunks | |
|
||||
| `` b `` | Pick both hunks | |
|
||||
| `` <up>, k `` | 이전 hunk를 선택 | |
|
||||
| `` <down>, j `` | 다음 hunk를 선택 | |
|
||||
| `` <left>, h `` | 이전 충돌을 선택 | |
|
||||
|
|
@ -210,6 +212,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <space> `` | 체크아웃 | Checkout selected item. |
|
||||
| `` n `` | 새 브랜치 생성 | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` o `` | 풀 리퀘스트 생성 | |
|
||||
| `` O `` | 풀 리퀘스트 생성 옵션 | |
|
||||
| `` G `` | Open pull request in browser | |
|
||||
|
|
@ -229,14 +232,12 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 커밋 보기 | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## 상태
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` o `` | 설정 파일 열기 | Open file in default application. |
|
||||
| `` e `` | 설정 파일 수정 | Open file in external editor. |
|
||||
| `` u `` | 업데이트 확인 | |
|
||||
| `` <enter> `` | 최근에 사용한 저장소로 전환 | |
|
||||
|
|
@ -277,6 +278,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | 브랜치명을 클립보드에 복사 | |
|
||||
| `` <space> `` | 체크아웃 | Checkout a new local branch based on the selected remote branch, or the remote branch as a detached head. |
|
||||
| `` n `` | 새 브랜치 생성 | |
|
||||
| `` w `` | New worktree | |
|
||||
| `` M `` | 현재 브랜치에 병합 | View options for merging the selected item into the current branch (regular merge, squash merge) |
|
||||
| `` r `` | 체크아웃된 브랜치를 이 브랜치에 리베이스 | Rebase the checked-out branch onto the selected branch. |
|
||||
| `` d `` | 삭제 | Delete the remote branch from the remote. |
|
||||
|
|
@ -286,7 +288,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 커밋 보기 | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## 커밋
|
||||
|
|
@ -322,13 +323,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | 브라우저에서 커밋 열기 | |
|
||||
| `` n `` | 커밋에서 새 브랜치를 만듭니다. | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` g `` | View reset options | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | 커밋을 복사 (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | View selected item's files | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | 검색 시작 | |
|
||||
|
||||
## 커밋 파일
|
||||
|
|
@ -365,13 +366,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | Copy tag to clipboard | |
|
||||
| `` <space> `` | 체크아웃 | Checkout the selected tag as a detached HEAD. |
|
||||
| `` n `` | 태그를 생성 | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. |
|
||||
| `` w `` | New worktree | |
|
||||
| `` d `` | 삭제 | View delete options for local/remote tag. |
|
||||
| `` P `` | 태그를 push | Push the selected tag to a remote. You'll be prompted to select a remote. |
|
||||
| `` g `` | 초기화 | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 커밋 보기 | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## 파일
|
||||
|
|
|
|||
|
|
@ -10,26 +10,28 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <pgup>, K, <ctrl+u> (fn+up/shift+k) `` | Scroll naar beneden vanaf hoofdpaneel | |
|
||||
| `` <pgdown>, J, <ctrl+d> (fn+down/shift+j) `` | Scroll naar beneden vanaf hoofdpaneel | |
|
||||
| `` @ `` | View command log options | View options for the command log e.g. show/hide the command log and focus the command log. |
|
||||
| `` P `` | Push | Push the current branch to its upstream branch. If no upstream is configured, you will be prompted to configure an upstream branch. |
|
||||
| `` p `` | Pull | Pull changes from the remote for the current branch. If no upstream is configured, you will be prompted to configure an upstream branch. |
|
||||
| `` P `` | Push | Push de huidige branch naar de bijbehorende upstream-branch. Als er geen upstream is geconfigureerd wordt er gevraagd om een upstream-branch te configureren. |
|
||||
| `` p `` | Pull | Pull wijzigingen van de remote voor de huidige branch. Als er geen upstream is geconfigureerd wordt er gevraagd om een upstream-branch te configureren. |
|
||||
| `` ) `` | Increase rename similarity threshold | Increase the similarity threshold for a deletion and addition pair to be treated as a rename.<br><br>The default can be changed in the config file with the key 'git.renameSimilarityThreshold'. |
|
||||
| `` ( `` | Decrease rename similarity threshold | Decrease the similarity threshold for a deletion and addition pair to be treated as a rename.<br><br>The default can be changed in the config file with the key 'git.renameSimilarityThreshold'. |
|
||||
| `` } `` | Increase diff context size | Increase the amount of the context shown around changes in the diff view.<br><br>The default can be changed in the config file with the key 'git.diffContextSize'. |
|
||||
| `` { `` | Decrease diff context size | Decrease the amount of the context shown around changes in the diff view.<br><br>The default can be changed in the config file with the key 'git.diffContextSize'. |
|
||||
| `` : `` | Execute shell command | Bring up a prompt where you can enter a shell command to execute. |
|
||||
| `` <ctrl+p> `` | Bekijk aangepaste patch opties | |
|
||||
| `` m `` | Bekijk merge/rebase opties | View options to abort/continue/skip the current merge/rebase. |
|
||||
| `` m `` | Bekijk merge/rebase opties | Toon abort/continue/skip opties voor huidige merge/rebase. |
|
||||
| `` R `` | Verversen | Refresh the git state (i.e. run `git status`, `git branch`, etc in background to update the contents of panels). This does not run `git fetch`. |
|
||||
| `` + `` | Volgende scherm modus (normaal/half/groot) | |
|
||||
| `` _ `` | Vorige scherm modus | |
|
||||
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers |
|
||||
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
|
||||
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
|
||||
| `` <esc> `` | Annuleren | |
|
||||
| `` ? `` | Open menu | |
|
||||
| `` <ctrl+s> `` | Bekijk scoping opties | View options for filtering the commit log, so that only commits matching the filter are shown. |
|
||||
| `` W, <ctrl+e> `` | Open diff menu | View options relating to diffing two refs e.g. diffing against selected ref, entering ref to diff against, and reversing the diff direction. |
|
||||
| `` q, <ctrl+c> `` | Quit | |
|
||||
| `` <ctrl+z> `` | Suspend the application | |
|
||||
| `` q, <ctrl+c> `` | Afsluiten | |
|
||||
| `` <ctrl+z> `` | Pauzeer de applicatie | |
|
||||
| `` <ctrl+w> `` | Toggle whitespace | Toggle whether or not whitespace changes are shown in the diff view.<br><br>The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. |
|
||||
| `` <alt+shift+c> `` | Verander config bestand | Open bestand in externe editor. |
|
||||
| `` z `` | Ongedaan maken (via reflog) (experimenteel) | The reflog will be used to determine what git command to run to undo the last git command. This does not include changes to the working tree; only commits are taken into consideration. |
|
||||
| `` Z `` | Redo (via reflog) (experimenteel) | The reflog will be used to determine what git command to run to redo the last git command. This does not include changes to the working tree; only commits are taken into consideration. |
|
||||
|
||||
|
|
@ -45,8 +47,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <shift+down> `` | Range select down | |
|
||||
| `` <shift+up> `` | Range select up | |
|
||||
| `` / `` | Start met zoeken | |
|
||||
| `` H `` | Scroll left | |
|
||||
| `` L `` | Scroll right | |
|
||||
| `` H `` | Scroll naar links | |
|
||||
| `` L `` | Scroll naar rechts | |
|
||||
| `` ] `` | Volgende tabblad | |
|
||||
| `` [ `` | Vorige tabblad | |
|
||||
|
||||
|
|
@ -56,15 +58,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
|-----|--------|-------------|
|
||||
| `` <ctrl+o> `` | Kopieer de bestandsnaam naar het klembord | |
|
||||
| `` <space> `` | Toggle staged | Toggle staged for selected file. |
|
||||
| `` <ctrl+b> `` | Filter files by status | |
|
||||
| `` y `` | Copy to clipboard | |
|
||||
| `` c `` | Commit veranderingen | Commit staged changes. |
|
||||
| `` <ctrl+b> `` | Filter bestanden op status | |
|
||||
| `` y `` | Kopieer naar klembord | |
|
||||
| `` c `` | Commit veranderingen | Commit gestagede wijzigingen. |
|
||||
| `` w `` | Commit veranderingen zonder pre-commit hook | |
|
||||
| `` A `` | Wijzig laatste commit | |
|
||||
| `` C `` | Commit veranderingen met de git editor | |
|
||||
| `` <ctrl+f> `` | Find base commit for fixup | Find the commit that your current changes are building upon, for the sake of amending/fixing up the commit. This spares you from having to look through your branch's commits one-by-one to see which commit should be amended/fixed up. See docs: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` e `` | Edit | Open file in external editor. |
|
||||
| `` o `` | Open bestand | Open file in default application. |
|
||||
| `` <ctrl+f> `` | Find base commit for fixup | Vind de commit waar je huidige wijzigingen bovenop zijn gebouwd met als doel die commit te amenden/fixen. Hierdoor hoef je dit niet met de hand te doen. Zie: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` e `` | Edit | Open bestand in externe editor. |
|
||||
| `` o `` | Open bestand | Open bestand in standaardapplicatie. |
|
||||
| `` i `` | Ignore or exclude file | |
|
||||
| `` r `` | Refresh bestanden | |
|
||||
| `` s `` | Stash | Stash all changes. For other variations of stashing, use the view stash options keybinding. |
|
||||
|
|
@ -73,13 +75,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <enter> `` | Stage individuele hunks/lijnen | If the selected item is a file, focus the staging view so you can stage individual hunks/lines. If the selected item is a directory, collapse/expand it. |
|
||||
| `` d `` | Bekijk 'veranderingen ongedaan maken' opties | View options for discarding changes to the selected file. |
|
||||
| `` g `` | Bekijk upstream reset opties | |
|
||||
| `` D `` | Reset | View reset options for working tree (e.g. nuking the working tree). |
|
||||
| `` D `` | Resetten | View reset options for working tree (e.g. nuking the working tree). |
|
||||
| `` ` `` | Toggle bestandsboom weergave | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.<br><br>The default can be changed in the config file with the key 'gui.showFileTree'. |
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
|
||||
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
|
||||
| `` M `` | Bekijk merge conflict opties | Bekijk opties voor het oplossen van mergeconflicten. |
|
||||
| `` f `` | Fetch | Fetch changes from remote. |
|
||||
| `` - `` | Collapse all files | Collapse all directories in the files tree |
|
||||
| `` = `` | Expand all files | Expand all directories in the file tree |
|
||||
| `` = `` | Vouw alle bestanden uit | Vouw alle mappen in de bestandsstructuur uit |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
|
|
@ -89,7 +91,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
|-----|--------|-------------|
|
||||
| `` <enter> `` | Bevestig | |
|
||||
| `` <esc> `` | Sluiten | |
|
||||
| `` <ctrl+o> `` | Copy to clipboard | |
|
||||
| `` <ctrl+o> `` | Kopieer naar klembord | |
|
||||
|
||||
## Branches
|
||||
|
||||
|
|
@ -97,18 +99,19 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
|-----|--------|-------------|
|
||||
| `` <ctrl+o> `` | Kopieer branch name naar klembord | |
|
||||
| `` i `` | Laat git-flow opties zien | |
|
||||
| `` <space> `` | Uitchecken | Checkout selected item. |
|
||||
| `` <space> `` | Uitchecken | Geselecteerd item uitchecken. |
|
||||
| `` n `` | Nieuwe branch | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` N `` | Verplaats commits naar nieuwe branch | Maak een nieuwe branch en verplaats niet-gepushte commits van de huidige branch hier naar toe. Gebruik dit in het geval dat je deze commits eigenlijk op een nieuwe branch had willen maken.<br><br>Let op dat de selectie genegeerd wordt. De nieuwe branch komt ofwel bovenop de main branch, of bovenop de huidige branch (je kan kiezen). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` o `` | Maak een pull-request | |
|
||||
| `` O `` | Bekijk opties voor pull-aanvraag | |
|
||||
| `` G `` | Open pull request in browser | |
|
||||
| `` <ctrl+y> `` | Kopieer de URL van het pull-verzoek naar het klembord | |
|
||||
| `` c `` | Uitchecken bij naam | Checkout by name. In the input box you can enter '-' to switch to the previous branch. |
|
||||
| `` - `` | Checkout previous branch | |
|
||||
| `` - `` | Vorige branch uitchecken | |
|
||||
| `` F `` | Forceer checkout | Force checkout selected branch. This will discard all local changes in your working directory before checking out the selected branch. |
|
||||
| `` d `` | Delete | View delete options for local/remote branch. |
|
||||
| `` r `` | Rebase branch | Rebase the checked-out branch onto the selected branch. |
|
||||
| `` d `` | Verwijderen | View delete options for local/remote branch. |
|
||||
| `` r `` | Rebase branch | Rebase de uitgecheckte branch bovenop de geselecteerde branch. |
|
||||
| `` M `` | Merge in met huidige checked out branch | View options for merging the selected item into the current branch (regular merge, squash merge) |
|
||||
| `` f `` | Fast-forward deze branch vanaf zijn upstream | Fast-forward selected branch from its upstream. |
|
||||
| `` T `` | Creëer tag | |
|
||||
|
|
@ -116,10 +119,9 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` g `` | Bekijk reset opties | |
|
||||
| `` R `` | Hernoem branch | |
|
||||
| `` u `` | View upstream options | View options relating to the branch's upstream e.g. setting/unsetting the upstream and resetting to the upstream. |
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Bekijk commits | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Commit bericht
|
||||
|
|
@ -134,18 +136,18 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <ctrl+o> `` | Kopieer de bestandsnaam naar het klembord | |
|
||||
| `` y `` | Copy to clipboard | |
|
||||
| `` y `` | Kopieer naar klembord | |
|
||||
| `` c `` | Uitchecken | Bestand uitchecken |
|
||||
| `` d `` | Bekijk 'veranderingen ongedaan maken' opties | Uitsluit deze commit zijn veranderingen aan dit bestand |
|
||||
| `` o `` | Open bestand | Open file in default application. |
|
||||
| `` e `` | Edit | Open file in external editor. |
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` o `` | Open bestand | Open bestand in standaardapplicatie. |
|
||||
| `` e `` | Edit | Open bestand in externe editor. |
|
||||
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
|
||||
| `` <space> `` | Toggle bestand inbegrepen in patch | Toggle whether the file is included in the custom patch. See https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
|
||||
| `` a `` | Toggle all files | Add/remove all commit's files to custom patch. See https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
|
||||
| `` <enter> `` | Enter bestand om geselecteerde regels toe te voegen aan de patch | If a file is selected, enter the file so that you can add/remove individual lines to the custom patch. If a directory is selected, toggle the directory. |
|
||||
| `` ` `` | Toggle bestandsboom weergave | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.<br><br>The default can be changed in the config file with the key 'gui.showFileTree'. |
|
||||
| `` - `` | Collapse all files | Collapse all directories in the files tree |
|
||||
| `` = `` | Expand all files | Expand all directories in the file tree |
|
||||
| `` = `` | Vouw alle bestanden uit | Vouw alle mappen in de bestandsstructuur uit |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
|
|
@ -181,14 +183,14 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | Open commit in browser | |
|
||||
| `` n `` | Creëer nieuwe branch van commit | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` N `` | Verplaats commits naar nieuwe branch | Maak een nieuwe branch en verplaats niet-gepushte commits van de huidige branch hier naar toe. Gebruik dit in het geval dat je deze commits eigenlijk op een nieuwe branch had willen maken.<br><br>Let op dat de selectie genegeerd wordt. De nieuwe branch komt ofwel bovenop de main branch, of bovenop de huidige branch (je kan kiezen). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` g `` | Bekijk reset opties | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | Kopieer commit (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Bekijk gecommite bestanden | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Start met zoeken | |
|
||||
|
||||
## Input prompt
|
||||
|
|
@ -211,15 +213,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <space> `` | Kies stuk | |
|
||||
| `` b `` | Kies beide stukken | |
|
||||
| `` b `` | Pick both hunks | |
|
||||
| `` <up>, k `` | Selecteer bovenste hunk | |
|
||||
| `` <down>, j `` | Selecteer onderste hunk | |
|
||||
| `` <left>, h `` | Selecteer voorgaand conflict | |
|
||||
| `` <right>, l `` | Selecteer volgende conflict | |
|
||||
| `` z `` | Ongedaan maken | Undo last merge conflict resolution. |
|
||||
| `` e `` | Verander bestand | Open file in external editor. |
|
||||
| `` o `` | Open bestand | Open file in default application. |
|
||||
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
|
||||
| `` e `` | Verander bestand | Open bestand in externe editor. |
|
||||
| `` o `` | Open bestand | Open bestand in standaardapplicatie. |
|
||||
| `` M `` | Bekijk merge conflict opties | Bekijk opties voor het oplossen van mergeconflicten. |
|
||||
| `` <esc> `` | Ga terug naar het bestanden paneel | |
|
||||
|
||||
## Normaal
|
||||
|
|
@ -241,8 +243,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` v `` | Toggle drag selecteer | |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` <ctrl+o> `` | Copy selected text to clipboard | |
|
||||
| `` o `` | Open bestand | Open file in default application. |
|
||||
| `` e `` | Verander bestand | Open file in external editor. |
|
||||
| `` o `` | Open bestand | Open bestand in standaardapplicatie. |
|
||||
| `` e `` | Verander bestand | Open bestand in externe editor. |
|
||||
| `` <space> `` | Voeg toe/verwijder lijn(en) in patch | |
|
||||
| `` d `` | Remove lines from commit | Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines. |
|
||||
| `` <esc> `` | Sluit lijn-bij-lijn modus | |
|
||||
|
|
@ -257,15 +259,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | Open commit in browser | |
|
||||
| `` n `` | Creëer nieuwe branch van commit | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` N `` | Verplaats commits naar nieuwe branch | Maak een nieuwe branch en verplaats niet-gepushte commits van de huidige branch hier naar toe. Gebruik dit in het geval dat je deze commits eigenlijk op een nieuwe branch had willen maken.<br><br>Let op dat de selectie genegeerd wordt. De nieuwe branch komt ofwel bovenop de main branch, of bovenop de huidige branch (je kan kiezen). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` g `` | Bekijk reset opties | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | Kopieer commit (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <ctrl+r> `` | Reset cherry-picked (gekopieerde) commits selectie | |
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Bekijk commits | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Remote branches
|
||||
|
|
@ -273,27 +275,27 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <ctrl+o> `` | Kopieer branch name naar klembord | |
|
||||
| `` <space> `` | Uitchecken | Checkout a new local branch based on the selected remote branch, or the remote branch as a detached head. |
|
||||
| `` <space> `` | Uitchecken | Geselecteerde remote branch uitchecken als nieuwe locale branch of als detached head. |
|
||||
| `` n `` | Nieuwe branch | |
|
||||
| `` w `` | New worktree | |
|
||||
| `` M `` | Merge in met huidige checked out branch | View options for merging the selected item into the current branch (regular merge, squash merge) |
|
||||
| `` r `` | Rebase branch | Rebase the checked-out branch onto the selected branch. |
|
||||
| `` d `` | Delete | Delete the remote branch from the remote. |
|
||||
| `` u `` | Set as upstream | Stel in als upstream van uitgecheckte branch |
|
||||
| `` r `` | Rebase branch | Rebase de uitgecheckte branch bovenop de geselecteerde branch. |
|
||||
| `` d `` | Verwijderen | Delete the remote branch from the remote. |
|
||||
| `` u `` | Instellen als upstream | Stel in als upstream van uitgecheckte branch |
|
||||
| `` s `` | Sort order | |
|
||||
| `` g `` | Bekijk reset opties | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Bekijk commits | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Remotes
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <enter> `` | View branches | |
|
||||
| `` <enter> `` | Bekijk branches | |
|
||||
| `` n `` | Voeg een nieuwe remote toe | |
|
||||
| `` d `` | Remove | Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected. |
|
||||
| `` d `` | Verwijderen | Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected. |
|
||||
| `` e `` | Edit | Wijzig remote |
|
||||
| `` f `` | Fetch | Fetch remote |
|
||||
| `` F `` | Add fork remote | Quickly add a fork remote by replacing the owner in the origin URL and optionally check out a branch from new remote. |
|
||||
|
|
@ -318,15 +320,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | Copy selected text to clipboard | |
|
||||
| `` <space> `` | Toggle staged | Toggle lijnen staged / unstaged |
|
||||
| `` d `` | Verwijdert change (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
|
||||
| `` o `` | Open bestand | Open file in default application. |
|
||||
| `` e `` | Verander bestand | Open file in external editor. |
|
||||
| `` o `` | Open bestand | Open bestand in standaardapplicatie. |
|
||||
| `` e `` | Verander bestand | Open bestand in externe editor. |
|
||||
| `` <esc> `` | Ga terug naar het bestanden paneel | |
|
||||
| `` <tab> `` | Ga naar een ander paneel | Switch to other view (staged/unstaged changes). |
|
||||
| `` E `` | Edit hunk | Edit selected hunk in external editor. |
|
||||
| `` c `` | Commit veranderingen | Commit staged changes. |
|
||||
| `` c `` | Commit veranderingen | Commit gestagede wijzigingen. |
|
||||
| `` w `` | Commit veranderingen zonder pre-commit hook | |
|
||||
| `` C `` | Commit veranderingen met de git editor | |
|
||||
| `` <ctrl+f> `` | Find base commit for fixup | Find the commit that your current changes are building upon, for the sake of amending/fixing up the commit. This spares you from having to look through your branch's commits one-by-one to see which commit should be amended/fixed up. See docs: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` <ctrl+f> `` | Find base commit for fixup | Vind de commit waar je huidige wijzigingen bovenop zijn gebouwd met als doel die commit te amenden/fixen. Hierdoor hoef je dit niet met de hand te doen. Zie: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` / `` | Start met zoeken | |
|
||||
|
||||
## Stash
|
||||
|
|
@ -337,18 +339,17 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` g `` | Pop | Apply the stash entry to your working directory and remove the stash entry. |
|
||||
| `` d `` | Laten vallen | Remove the stash entry from the stash list. |
|
||||
| `` n `` | Nieuwe branch | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
|
||||
| `` r `` | Rename stash | |
|
||||
| `` w `` | New worktree | |
|
||||
| `` r `` | Hernoem stash | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Bekijk gecommite bestanden | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Status
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` o `` | Open config bestand | Open file in default application. |
|
||||
| `` e `` | Verander config bestand | Open file in external editor. |
|
||||
| `` e `` | Verander config bestand | Open bestand in externe editor. |
|
||||
| `` u `` | Check voor updates | |
|
||||
| `` <enter> `` | Wissel naar een recente repo | |
|
||||
| `` a `` | Show/cycle all branch logs | |
|
||||
|
|
@ -364,15 +365,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` o `` | Open commit in browser | |
|
||||
| `` n `` | Creëer nieuwe branch van commit | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` N `` | Verplaats commits naar nieuwe branch | Maak een nieuwe branch en verplaats niet-gepushte commits van de huidige branch hier naar toe. Gebruik dit in het geval dat je deze commits eigenlijk op een nieuwe branch had willen maken.<br><br>Let op dat de selectie genegeerd wordt. De nieuwe branch komt ofwel bovenop de main branch, of bovenop de huidige branch (je kan kiezen). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` g `` | Bekijk reset opties | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | Kopieer commit (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <ctrl+r> `` | Reset cherry-picked (gekopieerde) commits selectie | |
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Bekijk gecommite bestanden | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Start met zoeken | |
|
||||
|
||||
## Submodules
|
||||
|
|
@ -381,7 +382,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
|-----|--------|-------------|
|
||||
| `` <ctrl+o> `` | Kopieer submodule naam naar klembord | |
|
||||
| `` <enter> `` | Enter | Enter submodule |
|
||||
| `` d `` | Remove | Remove the selected submodule and its corresponding directory. |
|
||||
| `` d `` | Verwijderen | Remove the selected submodule and its corresponding directory. |
|
||||
| `` u `` | Update | Update selected submodule. |
|
||||
| `` n `` | Voeg nieuwe submodule toe | |
|
||||
| `` e `` | Update submodule URL | |
|
||||
|
|
@ -394,15 +395,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <ctrl+o> `` | Copy tag to clipboard | |
|
||||
| `` <space> `` | Uitchecken | Checkout the selected tag as a detached HEAD. |
|
||||
| `` <space> `` | Uitchecken | Geselecteerde tag uitchecken als detached HEAD. |
|
||||
| `` n `` | Creëer tag | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. |
|
||||
| `` d `` | Delete | View delete options for local/remote tag. |
|
||||
| `` w `` | New worktree | |
|
||||
| `` d `` | Verwijderen | View delete options for local/remote tag. |
|
||||
| `` P `` | Push tag | Push the selected tag to a remote. You'll be prompted to select a remote. |
|
||||
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` g `` | Resetten | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Bekijk commits | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Worktrees
|
||||
|
|
@ -411,6 +412,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
|-----|--------|-------------|
|
||||
| `` n `` | New worktree | |
|
||||
| `` <space> `` | Switch | Switch to the selected worktree. |
|
||||
| `` o `` | Open in editor | |
|
||||
| `` d `` | Remove | Remove the selected worktree. This will both delete the worktree's directory, as well as metadata about the worktree in the .git directory. |
|
||||
| `` o `` | Openen in editor | |
|
||||
| `` d `` | Verwijderen | Remove the selected worktree. This will both delete the worktree's directory, as well as metadata about the worktree in the .git directory. |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` R `` | Odśwież | Odśwież stan git (tj. uruchom `git status`, `git branch`, itp. w tle, aby zaktualizować zawartość paneli). To nie uruchamia `git fetch`. |
|
||||
| `` + `` | Następny tryb ekranu (normalny/półpełny/pełnoekranowy) | |
|
||||
| `` _ `` | Poprzedni tryb ekranu | |
|
||||
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers |
|
||||
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
|
||||
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
|
||||
| `` <esc> `` | Anuluj | |
|
||||
| `` ? `` | Otwórz menu przypisań klawiszy | |
|
||||
| `` <ctrl+s> `` | Pokaż opcje filtrowania | Pokaż opcje filtrowania dziennika commitów, tak aby pokazywane były tylko commity pasujące do filtra. |
|
||||
|
|
@ -30,6 +31,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` q, <ctrl+c> `` | Wyjdź | |
|
||||
| `` <ctrl+z> `` | Suspend the application | |
|
||||
| `` <ctrl+w> `` | Przełącz białe znaki | Toggle whether or not whitespace changes are shown in the diff view.<br><br>The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. |
|
||||
| `` <alt+shift+c> `` | Edytuj plik konfiguracyjny | Otwórz plik w zewnętrznym edytorze. |
|
||||
| `` z `` | Cofnij | Dziennik reflog zostanie użyty do określenia, jakie polecenie git należy uruchomić, aby cofnąć ostatnie polecenie git. Nie obejmuje to zmian w drzewie roboczym; brane są pod uwagę tylko commity. |
|
||||
| `` Z `` | Ponów | Dziennik reflog zostanie użyty do określenia, jakie polecenie git należy uruchomić, aby ponowić ostatnie polecenie git. Nie obejmuje to zmian w drzewie roboczym; brane są pod uwagę tylko commity. |
|
||||
|
||||
|
|
@ -83,13 +85,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | Otwórz commit w przeglądarce | |
|
||||
| `` n `` | Utwórz nową gałąź z commita | |
|
||||
| `` N `` | Przenieś commity do nowej gałęzi | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | Nowe drzewo pracy | |
|
||||
| `` g `` | Reset | Wyświetl opcje resetu (miękki/mieszany/twardy) do wybranego elementu. |
|
||||
| `` C `` | Kopiuj (cherry-pick) | Oznacz commit jako skopiowany. Następnie, w widoku lokalnych commitów, możesz nacisnąć `V`, aby wkleić (cherry-pick) skopiowane commity do sprawdzonej gałęzi. W dowolnym momencie możesz nacisnąć `<esc>`, aby anulować zaznaczenie. |
|
||||
| `` <ctrl+t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Wyświetl pliki | |
|
||||
| `` w `` | Zobacz opcje drzewa pracy | |
|
||||
| `` / `` | Szukaj w bieżącym widoku po tekście | |
|
||||
|
||||
## Dodatkowy
|
||||
|
|
@ -120,6 +122,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | Otwórz commit w przeglądarce | |
|
||||
| `` n `` | Utwórz nową gałąź z commita | |
|
||||
| `` N `` | Przenieś commity do nowej gałęzi | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | Nowe drzewo pracy | |
|
||||
| `` g `` | Reset | Wyświetl opcje resetu (miękki/mieszany/twardy) do wybranego elementu. |
|
||||
| `` C `` | Kopiuj (cherry-pick) | Oznacz commit jako skopiowany. Następnie, w widoku lokalnych commitów, możesz nacisnąć `V`, aby wkleić (cherry-pick) skopiowane commity do sprawdzonej gałęzi. W dowolnym momencie możesz nacisnąć `<esc>`, aby anulować zaznaczenie. |
|
||||
| `` <ctrl+r> `` | Resetuj wybrane (cherry-picked) commity | |
|
||||
|
|
@ -127,7 +130,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Pokaż commity | |
|
||||
| `` w `` | Zobacz opcje drzewa pracy | |
|
||||
| `` / `` | Filtruj bieżący widok po tekście | |
|
||||
|
||||
## Główny panel (budowanie łatki)
|
||||
|
|
@ -162,6 +164,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <space> `` | Przełącz | Przełącz wybrany element. |
|
||||
| `` n `` | Nowa gałąź | |
|
||||
| `` N `` | Przenieś commity do nowej gałęzi | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | Nowe drzewo pracy | |
|
||||
| `` o `` | Utwórz żądanie ściągnięcia | |
|
||||
| `` O `` | Zobacz opcje tworzenia pull requesta | |
|
||||
| `` G `` | Otwórz żądanie ściągnięcia w przeglądarce | |
|
||||
|
|
@ -181,7 +184,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Pokaż commity | |
|
||||
| `` w `` | Zobacz opcje drzewa pracy | |
|
||||
| `` / `` | Filtruj bieżący widok po tekście | |
|
||||
|
||||
## Menu
|
||||
|
|
@ -207,7 +209,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <space> `` | Wybierz fragment | |
|
||||
| `` b `` | Wybierz wszystkie fragmenty | |
|
||||
| `` b `` | Pick both hunks | |
|
||||
| `` <up>, k `` | Poprzedni fragment | |
|
||||
| `` <down>, j `` | Następny fragment | |
|
||||
| `` <left>, h `` | Poprzedni konflikt | |
|
||||
|
|
@ -316,17 +318,16 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` g `` | Wyciągnij | Zastosuj wpis schowka do katalogu roboczego i usuń wpis schowka. |
|
||||
| `` d `` | Usuń | Usuń wpis schowka z listy schowka. |
|
||||
| `` n `` | Nowa gałąź | Utwórz nową gałąź z wybranego wpisu schowka. Działa poprzez przełączenie git na commit, na którym wpis schowka został utworzony, tworzenie nowej gałęzi z tego commita, a następnie zastosowanie wpisu schowka do nowej gałęzi jako dodatkowego commita. |
|
||||
| `` w `` | Nowe drzewo pracy | |
|
||||
| `` r `` | Zmień nazwę schowka | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Wyświetl pliki | |
|
||||
| `` w `` | Zobacz opcje drzewa pracy | |
|
||||
| `` / `` | Filtruj bieżący widok po tekście | |
|
||||
|
||||
## Status
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` o `` | Otwórz plik konfiguracyjny | Otwórz plik w domyślnej aplikacji. |
|
||||
| `` e `` | Edytuj plik konfiguracyjny | Otwórz plik w zewnętrznym edytorze. |
|
||||
| `` u `` | Sprawdź aktualizacje | |
|
||||
| `` <enter> `` | Przełącz na ostatnie repozytorium | |
|
||||
|
|
@ -344,6 +345,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | Otwórz commit w przeglądarce | |
|
||||
| `` n `` | Utwórz nową gałąź z commita | |
|
||||
| `` N `` | Przenieś commity do nowej gałęzi | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | Nowe drzewo pracy | |
|
||||
| `` g `` | Reset | Wyświetl opcje resetu (miękki/mieszany/twardy) do wybranego elementu. |
|
||||
| `` C `` | Kopiuj (cherry-pick) | Oznacz commit jako skopiowany. Następnie, w widoku lokalnych commitów, możesz nacisnąć `V`, aby wkleić (cherry-pick) skopiowane commity do sprawdzonej gałęzi. W dowolnym momencie możesz nacisnąć `<esc>`, aby anulować zaznaczenie. |
|
||||
| `` <ctrl+r> `` | Resetuj wybrane (cherry-picked) commity | |
|
||||
|
|
@ -351,7 +353,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Wyświetl pliki | |
|
||||
| `` w `` | Zobacz opcje drzewa pracy | |
|
||||
| `` / `` | Szukaj w bieżącym widoku po tekście | |
|
||||
|
||||
## Submoduły
|
||||
|
|
@ -375,13 +376,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | Skopiuj tag do schowka | |
|
||||
| `` <space> `` | Przełącz | Przełącz wybrany tag jako odłączoną głowę (detached HEAD). |
|
||||
| `` n `` | Nowy tag | Utwórz nowy tag z bieżącego commita. Zostaniesz poproszony o wprowadzenie nazwy tagu i opcjonalnego opisu. |
|
||||
| `` w `` | Nowe drzewo pracy | |
|
||||
| `` d `` | Usuń | Wyświetl opcje usuwania lokalnego/odległego tagu. |
|
||||
| `` P `` | Wyślij tag | Wyślij wybrany tag do zdalnego. Zostaniesz poproszony o wybranie zdalnego. |
|
||||
| `` g `` | Reset | Wyświetl opcje resetu (miękki/mieszany/twardy) do wybranego elementu. |
|
||||
| `` <ctrl+t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Pokaż commity | |
|
||||
| `` w `` | Zobacz opcje drzewa pracy | |
|
||||
| `` / `` | Filtruj bieżący widok po tekście | |
|
||||
|
||||
## Zdalne
|
||||
|
|
@ -403,6 +404,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | Kopiuj nazwę gałęzi do schowka | |
|
||||
| `` <space> `` | Przełącz | Przełącz na nową lokalną gałąź na podstawie wybranej gałęzi zdalnej. Nowa gałąź będzie śledzić gałąź zdalną. |
|
||||
| `` n `` | Nowa gałąź | |
|
||||
| `` w `` | Nowe drzewo pracy | |
|
||||
| `` M `` | Scal | Scal wybraną gałąź z aktualnie sprawdzoną gałęzią. |
|
||||
| `` r `` | Przebazuj | Przebazuj przełączoną gałąź na wybraną gałąź. |
|
||||
| `` d `` | Usuń | Usuń gałąź zdalną ze zdalnego. |
|
||||
|
|
@ -412,5 +414,4 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Pokaż commity | |
|
||||
| `` w `` | Zobacz opcje drzewa pracy | |
|
||||
| `` / `` | Filtruj bieżący widok po tekście | |
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` R `` | Atualizar | Atualize o estado do git (ou seja, execute `git status`, `git branch`, etc em segundo plano para atualizar o conteúdo de painéis). Isso não executa `git fetch`. |
|
||||
| `` + `` | Modo de tela seguinte (normal/metade/tela cheia) | |
|
||||
| `` _ `` | Modo de tela anterior | |
|
||||
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers |
|
||||
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
|
||||
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
|
||||
| `` <esc> `` | Cancelar | |
|
||||
| `` ? `` | Abrir o menu de atalhos do teclado | |
|
||||
| `` <ctrl+s> `` | Ver opções de filtro | View options for filtering the commit log, so that only commits matching the filter are shown. |
|
||||
|
|
@ -30,6 +31,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` q, <ctrl+c> `` | Sair | |
|
||||
| `` <ctrl+z> `` | Suspender a aplicação | |
|
||||
| `` <ctrl+w> `` | Toggle whitespace | Toggle whether or not whitespace changes are shown in the diff view.<br><br>The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. |
|
||||
| `` <alt+shift+c> `` | Editar arquivo de configuração | Abrir arquivo no editor externo. |
|
||||
| `` z `` | Desfazer | O reflog será usado para determinar qual comando git para executar para desfazer o último comando git. Isto não inclui mudanças na árvore de trabalho; apenas compromissos são tidos em consideração. |
|
||||
| `` Z `` | Refazer | O reflog será usado para determinar qual comando git para executar para refazer o último comando git. Isto não inclui mudanças na árvore de trabalho; apenas compromissos são tidos em consideração. |
|
||||
|
||||
|
|
@ -92,6 +94,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <space> `` | Verificar | Checar item selecionado |
|
||||
| `` n `` | Nova branch | |
|
||||
| `` N `` | Mover commits para uma nova branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | Nova árvore de trabalho | |
|
||||
| `` o `` | Criar solicitação de pull | |
|
||||
| `` O `` | View create pull request options | |
|
||||
| `` G `` | Open pull request in browser | |
|
||||
|
|
@ -111,7 +114,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | Abrir ferramenta de diff externa (git difftool) | |
|
||||
| `` 0 `` | Focar visualização principal | |
|
||||
| `` <enter> `` | Ver commits | |
|
||||
| `` w `` | Ver opções da árvore de trabalho | |
|
||||
| `` / `` | Filtrar a visualização atual por texto | |
|
||||
|
||||
## Branches remotos
|
||||
|
|
@ -121,6 +123,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | Copiar nome da branch para área de transferência | |
|
||||
| `` <space> `` | Verificar | Checar a nova branch baseada na brach remota selecionada, ou a branch remota como HEAD, desanexado |
|
||||
| `` n `` | Nova branch | |
|
||||
| `` w `` | Nova árvore de trabalho | |
|
||||
| `` M `` | Mesclar | Ver opções para mesclar o item selecionado no branch atual (mesclar regularmente, mesclar squash) |
|
||||
| `` r `` | Refazer | Refazer a branch checada na branch selecionada |
|
||||
| `` d `` | Apagar | Excluir o branch remoto do controle remoto. |
|
||||
|
|
@ -130,7 +133,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | Abrir ferramenta de diff externa (git difftool) | |
|
||||
| `` 0 `` | Focar visualização principal | |
|
||||
| `` <enter> `` | Ver commits | |
|
||||
| `` w `` | Ver opções da árvore de trabalho | |
|
||||
| `` / `` | Filtrar a visualização atual por texto | |
|
||||
|
||||
## Commit arquivos
|
||||
|
|
@ -186,13 +188,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | Abrir commit no navegador | |
|
||||
| `` n `` | Create new branch off of commit | |
|
||||
| `` N `` | Mover commits para uma nova branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | Nova árvore de trabalho | |
|
||||
| `` g `` | Restaurar | Ver opções de redefinição (soft/mixed/hard) para redefinir para o item selecionado. |
|
||||
| `` C `` | Copiar (cherry-pick) | Marcar commit como copiado. Então, dentro da visualização local de commits, você pode pressionar `V` para colar (cherry-pick) o(s) commit(s) copiado(s) em seu branch de check-out. A qualquer momento você pode pressionar `<esc>` para cancelar a seleção. |
|
||||
| `` <ctrl+t> `` | Abrir ferramenta de diff externa (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focar visualização principal | |
|
||||
| `` <enter> `` | Ver arquivos | |
|
||||
| `` w `` | Ver opções da árvore de trabalho | |
|
||||
| `` / `` | Pesquisar na visualização atual por texto | |
|
||||
|
||||
## Etiquetas
|
||||
|
|
@ -202,13 +204,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | Copiar etiqueta para área de transferência | |
|
||||
| `` <space> `` | Verificar | Checar a tag selecionada como um HEAD, desanexado |
|
||||
| `` n `` | Nova etiqueta | Crie uma nova etiqueta a partir do commit atual. Você será solicitado a digitar um nome e uma descrição opcional. |
|
||||
| `` w `` | Nova árvore de trabalho | |
|
||||
| `` d `` | Apagar | Ver opções de exclusão para tag local/remoto. |
|
||||
| `` P `` | Empurrar etiqueta | Push the selected tag to a remote. You'll be prompted to select a remote. |
|
||||
| `` g `` | Restaurar | Ver opções de redefinição (soft/mixed/hard) para redefinir para o item selecionado. |
|
||||
| `` <ctrl+t> `` | Abrir ferramenta de diff externa (git difftool) | |
|
||||
| `` 0 `` | Focar visualização principal | |
|
||||
| `` <enter> `` | Ver commits | |
|
||||
| `` w `` | Ver opções da árvore de trabalho | |
|
||||
| `` / `` | Filtrar a visualização atual por texto | |
|
||||
|
||||
## Input prompt
|
||||
|
|
@ -271,7 +273,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <space> `` | Escolha o local | |
|
||||
| `` b `` | Pegar todos os pedaços | |
|
||||
| `` b `` | Pick both hunks | |
|
||||
| `` <up>, k `` | Trecho anterior | |
|
||||
| `` <down>, j `` | Próximo trecho | |
|
||||
| `` <left>, h `` | Conflito anterior | |
|
||||
|
|
@ -308,6 +310,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | Abrir commit no navegador | |
|
||||
| `` n `` | Create new branch off of commit | |
|
||||
| `` N `` | Mover commits para uma nova branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | Nova árvore de trabalho | |
|
||||
| `` g `` | Restaurar | Ver opções de redefinição (soft/mixed/hard) para redefinir para o item selecionado. |
|
||||
| `` C `` | Copiar (cherry-pick) | Marcar commit como copiado. Então, dentro da visualização local de commits, você pode pressionar `V` para colar (cherry-pick) o(s) commit(s) copiado(s) em seu branch de check-out. A qualquer momento você pode pressionar `<esc>` para cancelar a seleção. |
|
||||
| `` <ctrl+r> `` | Reset copied (cherry-picked) commits selection | |
|
||||
|
|
@ -315,7 +318,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focar visualização principal | |
|
||||
| `` <enter> `` | Ver commits | |
|
||||
| `` w `` | Ver opções da árvore de trabalho | |
|
||||
| `` / `` | Filtrar a visualização atual por texto | |
|
||||
|
||||
## Remotes
|
||||
|
|
@ -346,17 +348,16 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` g `` | Pop | Aplique a entrada de stash no seu diretório de trabalho e remova a entrada de stash. |
|
||||
| `` d `` | Descartar | Remova a entrada do stash da lista de armazenamento. |
|
||||
| `` n `` | Nova branch | Criar um novo ramo a partir da entrada de lixo selecionada. Isso funciona verificando o commit do qual a entrada de lixo foi criada, criar um novo branch a partir desse commit e, em seguida, aplicar a entrada de lixo ao novo branch como um commit adicional. |
|
||||
| `` w `` | Nova árvore de trabalho | |
|
||||
| `` r `` | Renomear o stash | |
|
||||
| `` 0 `` | Focar visualização principal | |
|
||||
| `` <enter> `` | Ver arquivos | |
|
||||
| `` w `` | Ver opções da árvore de trabalho | |
|
||||
| `` / `` | Filtrar a visualização atual por texto | |
|
||||
|
||||
## Status
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` o `` | Abrir o ficheiro de config | Abrir arquivo no aplicativo padrão. |
|
||||
| `` e `` | Editar arquivo de configuração | Abrir arquivo no editor externo. |
|
||||
| `` u `` | Verificar atualização | |
|
||||
| `` <enter> `` | Mudar para um repositório recente | |
|
||||
|
|
@ -374,6 +375,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | Abrir commit no navegador | |
|
||||
| `` n `` | Create new branch off of commit | |
|
||||
| `` N `` | Mover commits para uma nova branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | Nova árvore de trabalho | |
|
||||
| `` g `` | Restaurar | Ver opções de redefinição (soft/mixed/hard) para redefinir para o item selecionado. |
|
||||
| `` C `` | Copiar (cherry-pick) | Marcar commit como copiado. Então, dentro da visualização local de commits, você pode pressionar `V` para colar (cherry-pick) o(s) commit(s) copiado(s) em seu branch de check-out. A qualquer momento você pode pressionar `<esc>` para cancelar a seleção. |
|
||||
| `` <ctrl+r> `` | Reset copied (cherry-picked) commits selection | |
|
||||
|
|
@ -381,7 +383,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focar visualização principal | |
|
||||
| `` <enter> `` | Ver arquivos | |
|
||||
| `` w `` | Ver opções da árvore de trabalho | |
|
||||
| `` / `` | Pesquisar na visualização atual por texto | |
|
||||
|
||||
## Submódulos
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` R `` | Обновить | Refresh the git state (i.e. run `git status`, `git branch`, etc in background to update the contents of panels). This does not run `git fetch`. |
|
||||
| `` + `` | Следующий режим экрана (нормальный/полуэкранный/полноэкранный) | |
|
||||
| `` _ `` | Предыдущий режим экрана | |
|
||||
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers |
|
||||
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
|
||||
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
|
||||
| `` <esc> `` | Отменить | |
|
||||
| `` ? `` | Открыть меню | |
|
||||
| `` <ctrl+s> `` | Просмотреть параметры фильтрации по пути | View options for filtering the commit log, so that only commits matching the filter are shown. |
|
||||
|
|
@ -30,6 +31,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` q, <ctrl+c> `` | Выйти | |
|
||||
| `` <ctrl+z> `` | Suspend the application | |
|
||||
| `` <ctrl+w> `` | Переключить отображение изменении пробелов в просмотрщике сравнении | Toggle whether or not whitespace changes are shown in the diff view.<br><br>The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. |
|
||||
| `` <alt+shift+c> `` | Редактировать файл конфигурации | Open file in external editor. |
|
||||
| `` z `` | Отменить (через reflog) (экспериментальный) | Журнал ссылок (reflog) будет использоваться для определения того, какую команду git запустить, чтобы отменить последнюю команду git. Сюда не входят изменения в рабочем дереве; учитываются только коммиты. |
|
||||
| `` Z `` | Повторить (через reflog) (экспериментальный) | Журнал ссылок (reflog) будет использоваться для определения того, какую команду git нужно запустить, чтобы повторить последнюю команду git. Сюда не входят изменения в рабочем дереве; учитываются только коммиты. |
|
||||
|
||||
|
|
@ -112,7 +114,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <space> `` | Выбрать эту часть | |
|
||||
| `` b `` | Выбрать все части | |
|
||||
| `` b `` | Pick both hunks | |
|
||||
| `` <up>, k `` | Выбрать предыдущую часть | |
|
||||
| `` <down>, j `` | Выбрать следующую часть | |
|
||||
| `` <left>, h `` | Выбрать предыдущий конфликт | |
|
||||
|
|
@ -149,6 +151,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | Открыть коммит в браузере | |
|
||||
| `` n `` | Создать новую ветку с этого коммита | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` g `` | Просмотреть параметры сброса | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | Скопировать отобранные коммит (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <ctrl+r> `` | Сбросить отобранную (скопированную \| cherry-picked) выборку коммитов | |
|
||||
|
|
@ -156,7 +159,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Просмотреть коммиты | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Коммиты
|
||||
|
|
@ -192,13 +194,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | Открыть коммит в браузере | |
|
||||
| `` n `` | Создать новую ветку с этого коммита | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` g `` | Просмотреть параметры сброса | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | Скопировать отобранные коммит (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Просмотреть файлы выбранного элемента | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Найти | |
|
||||
|
||||
## Локальные Ветки
|
||||
|
|
@ -210,6 +212,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <space> `` | Переключить | Checkout selected item. |
|
||||
| `` n `` | Новая ветка | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` o `` | Создать запрос на принятие изменений | |
|
||||
| `` O `` | Создать параметры запроса принятие изменений | |
|
||||
| `` G `` | Open pull request in browser | |
|
||||
|
|
@ -229,7 +232,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Просмотреть коммиты | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Меню
|
||||
|
|
@ -258,6 +260,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | Открыть коммит в браузере | |
|
||||
| `` n `` | Создать новую ветку с этого коммита | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` g `` | Просмотреть параметры сброса | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | Скопировать отобранные коммит (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <ctrl+r> `` | Сбросить отобранную (скопированную \| cherry-picked) выборку коммитов | |
|
||||
|
|
@ -265,7 +268,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Просмотреть файлы выбранного элемента | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Найти | |
|
||||
|
||||
## Подмодули
|
||||
|
|
@ -313,7 +315,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` o `` | Открыть файл конфигурации | Open file in default application. |
|
||||
| `` e `` | Редактировать файл конфигурации | Open file in external editor. |
|
||||
| `` u `` | Проверить обновления | |
|
||||
| `` <enter> `` | Переключиться на последний репозиторий | |
|
||||
|
|
@ -328,13 +329,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | Copy tag to clipboard | |
|
||||
| `` <space> `` | Переключить | Checkout the selected tag as a detached HEAD. |
|
||||
| `` n `` | Создать тег | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. |
|
||||
| `` w `` | New worktree | |
|
||||
| `` d `` | Delete | View delete options for local/remote tag. |
|
||||
| `` P `` | Отправить тег | Push the selected tag to a remote. You'll be prompted to select a remote. |
|
||||
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Просмотреть коммиты | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Удалённые ветки
|
||||
|
|
@ -344,6 +345,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | Скопировать название ветки в буфер обмена | |
|
||||
| `` <space> `` | Переключить | Checkout a new local branch based on the selected remote branch, or the remote branch as a detached head. |
|
||||
| `` n `` | Новая ветка | |
|
||||
| `` w `` | New worktree | |
|
||||
| `` M `` | Слияние с текущей переключённой веткой | View options for merging the selected item into the current branch (regular merge, squash merge) |
|
||||
| `` r `` | Перебазировать переключённую ветку на эту ветку | Rebase the checked-out branch onto the selected branch. |
|
||||
| `` d `` | Delete | Delete the remote branch from the remote. |
|
||||
|
|
@ -353,7 +355,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Просмотреть коммиты | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
||||
## Удалённые репозитории
|
||||
|
|
@ -409,8 +410,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` g `` | Применить припрятанные изменения и тут же удалить их из хранилища | Apply the stash entry to your working directory and remove the stash entry. |
|
||||
| `` d `` | Удалить припрятанные изменения из хранилища | Remove the stash entry from the stash list. |
|
||||
| `` n `` | Новая ветка | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
|
||||
| `` w `` | New worktree | |
|
||||
| `` r `` | Переименовать хранилище | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | Просмотреть файлы выбранного элемента | |
|
||||
| `` w `` | View worktree options | |
|
||||
| `` / `` | Filter the current view by text | |
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` + `` | 下一屏模式(正常/半屏/全屏) | |
|
||||
| `` _ `` | 上一屏模式 | |
|
||||
| `` \| `` | 切换分页器 | 从已配置的分页器列表中选择下一个分页器 |
|
||||
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
|
||||
| `` <esc> `` | 取消 | |
|
||||
| `` ? `` | 打开菜单 | |
|
||||
| `` <ctrl+s> `` | 查看按路径过滤选项 | 查看用于过滤提交日志的选项,以便仅显示与过滤器匹配的提交。 |
|
||||
|
|
@ -30,6 +31,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` q, <ctrl+c> `` | 退出 | |
|
||||
| `` <ctrl+z> `` | 挂起应用程序 | |
|
||||
| `` <ctrl+w> `` | 切换是否在差异视图中显示空白字符差异 | 切换是否在差异视图中显示空白字符更改。<br><br>默认值可在配置文件中通过键 'git.ignoreWhitespaceInDiffView' 更改。 |
|
||||
| `` <alt+shift+c> `` | 编辑配置文件 | 使用外部编辑器打开文件 |
|
||||
| `` z `` | 撤销 | Reflog将用于确定运行哪个git命令来撤消最后一个git命令。这并不包括对工作树的更改,只考虑提交。 |
|
||||
| `` Z `` | 重做 | Reflog将用于确定运行哪个git命令来重做上一个git命令。这并不包括对工作树的更改,只考虑提交。 |
|
||||
|
||||
|
|
@ -60,6 +62,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | 在浏览器中打开提交 | |
|
||||
| `` n `` | 从提交创建新分支 | |
|
||||
| `` N `` | 移动提交至新分支 | 创建一个新分支,并将当前分支未推送的提交移动到该分支。如果您打算开始新工作但忘记先创建新分支,这会很有用。<br><br>请注意,此操作忽略选择,新分支总是从主分支创建或堆叠在当前分支之上(您可以选择哪种方式)。 |
|
||||
| `` w `` | 新建工作树 | |
|
||||
| `` g `` | 查看重置选项 | 查看重置选项 (soft/mixed/hard) 用于重置到选择项 |
|
||||
| `` C `` | 复制提交(拣选) | 标记提交为已复制。然后,在本地提交视图中,您可以按 `V` (Cherry-Pick) 将已复制的提交粘贴到已检出的分支中。任何时候都可以按 `<esc>` 来取消选择。 |
|
||||
| `` <ctrl+r> `` | 重置已拣选(复制)的提交 | |
|
||||
|
|
@ -67,7 +70,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | 选择当前分支的提交 | |
|
||||
| `` 0 `` | 聚焦主视图 | |
|
||||
| `` <enter> `` | 查看提交的文件 | |
|
||||
| `` w `` | 查看工作区选项 | |
|
||||
| `` / `` | 开始搜索 | |
|
||||
|
||||
## 子模块
|
||||
|
|
@ -104,6 +106,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | 在浏览器中打开提交 | |
|
||||
| `` n `` | 从提交创建新分支 | |
|
||||
| `` N `` | 移动提交至新分支 | 创建一个新分支,并将当前分支未推送的提交移动到该分支。如果您打算开始新工作但忘记先创建新分支,这会很有用。<br><br>请注意,此操作忽略选择,新分支总是从主分支创建或堆叠在当前分支之上(您可以选择哪种方式)。 |
|
||||
| `` w `` | 新建工作树 | |
|
||||
| `` g `` | 查看重置选项 | 查看重置选项 (soft/mixed/hard) 用于重置到选择项 |
|
||||
| `` C `` | 复制提交(拣选) | 标记提交为已复制。然后,在本地提交视图中,您可以按 `V` (Cherry-Pick) 将已复制的提交粘贴到已检出的分支中。任何时候都可以按 `<esc>` 来取消选择。 |
|
||||
| `` <ctrl+r> `` | 重置已拣选(复制)的提交 | |
|
||||
|
|
@ -111,7 +114,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | 选择当前分支的提交 | |
|
||||
| `` 0 `` | 聚焦主视图 | |
|
||||
| `` <enter> `` | 查看提交 | |
|
||||
| `` w `` | 查看工作区选项 | |
|
||||
| `` / `` | 通过文本过滤当前视图 | |
|
||||
|
||||
## 提交
|
||||
|
|
@ -147,13 +149,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | 在浏览器中打开提交 | |
|
||||
| `` n `` | 从提交创建新分支 | |
|
||||
| `` N `` | 移动提交至新分支 | 创建一个新分支,并将当前分支未推送的提交移动到该分支。如果您打算开始新工作但忘记先创建新分支,这会很有用。<br><br>请注意,此操作忽略选择,新分支总是从主分支创建或堆叠在当前分支之上(您可以选择哪种方式)。 |
|
||||
| `` w `` | 新建工作树 | |
|
||||
| `` g `` | 查看重置选项 | 查看重置选项 (soft/mixed/hard) 用于重置到选择项 |
|
||||
| `` C `` | 复制提交(拣选) | 标记提交为已复制。然后,在本地提交视图中,您可以按 `V` (Cherry-Pick) 将已复制的提交粘贴到已检出的分支中。任何时候都可以按 `<esc>` 来取消选择。 |
|
||||
| `` <ctrl+t> `` | 使用外部差异比较工具(git difftool) | |
|
||||
| `` * `` | 选择当前分支的提交 | |
|
||||
| `` 0 `` | 聚焦主视图 | |
|
||||
| `` <enter> `` | 查看提交的文件 | |
|
||||
| `` w `` | 查看工作区选项 | |
|
||||
| `` / `` | 开始搜索 | |
|
||||
|
||||
## 提交信息
|
||||
|
|
@ -225,6 +227,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <space> `` | 检出 | 检出选中的项目 |
|
||||
| `` n `` | 新分支 | |
|
||||
| `` N `` | 移动提交至新分支 | 创建一个新分支,并将当前分支未推送的提交移动到该分支。如果您打算开始新工作但忘记先创建新分支,这会很有用。<br><br>请注意,此操作忽略选择,新分支总是从主分支创建或堆叠在当前分支之上(您可以选择哪种方式)。 |
|
||||
| `` w `` | 新建工作树 | |
|
||||
| `` o `` | 创建拉取请求 | |
|
||||
| `` O `` | 创建拉取请求选项 | |
|
||||
| `` G `` | 在浏览器中打开拉取请求 | |
|
||||
|
|
@ -244,7 +247,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | 使用外部差异比较工具(git difftool) | |
|
||||
| `` 0 `` | 聚焦主视图 | |
|
||||
| `` <enter> `` | 查看提交 | |
|
||||
| `` w `` | 查看工作区选项 | |
|
||||
| `` / `` | 通过文本过滤当前视图 | |
|
||||
|
||||
## 构建补丁中
|
||||
|
|
@ -270,13 +272,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | 复制标签到剪贴板 | |
|
||||
| `` <space> `` | 检出 | 检出选择的标签作为分离的HEAD |
|
||||
| `` n `` | 创建标签 | 基于当前提交创建一个新标签。您将在弹窗中输入标签名称和描述(可选)。 |
|
||||
| `` w `` | 新建工作树 | |
|
||||
| `` d `` | 删除 | 查看本地/远程标签的删除选项 |
|
||||
| `` P `` | 推送标签 | 推送选择的标签到远端。您将在弹窗中选择一个远端。 |
|
||||
| `` g `` | 重置 | 查看重置选项 (soft/mixed/hard) 用于重置到选择项 |
|
||||
| `` <ctrl+t> `` | 使用外部差异比较工具(git difftool) | |
|
||||
| `` 0 `` | 聚焦主视图 | |
|
||||
| `` <enter> `` | 查看提交 | |
|
||||
| `` w `` | 查看工作区选项 | |
|
||||
| `` / `` | 通过文本过滤当前视图 | |
|
||||
|
||||
## 次要
|
||||
|
|
@ -292,7 +294,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <space> `` | 选中区块 | |
|
||||
| `` b `` | 选中所有区块 | |
|
||||
| `` b `` | Pick both hunks | |
|
||||
| `` <up>, k `` | 选择顶部块 | |
|
||||
| `` <down>, j `` | 选择底部块 | |
|
||||
| `` <left>, h `` | 选择上一个冲突 | |
|
||||
|
|
@ -339,7 +341,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` o `` | 打开配置文件 | 使用默认程序打开该文件 |
|
||||
| `` e `` | 编辑配置文件 | 使用外部编辑器打开文件 |
|
||||
| `` u `` | 检查更新 | |
|
||||
| `` <enter> `` | 切换到最近的仓库 | |
|
||||
|
|
@ -371,10 +372,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` g `` | 应用并删除 | 将存储项应用到工作目录并删除存储项。 |
|
||||
| `` d `` | 删除 | 从贮藏列表中删除该贮藏项 |
|
||||
| `` n `` | 新分支 | 从选定的贮藏项创建一个新分支。这是通过 git 检查创建贮藏项的提交,从该提交创建一个新分支,然后将贮藏项作为附加提交应用到新分支来实现的。 |
|
||||
| `` w `` | 新建工作树 | |
|
||||
| `` r `` | 重命名贮藏 | |
|
||||
| `` 0 `` | 聚焦主视图 | |
|
||||
| `` <enter> `` | 查看提交的文件 | |
|
||||
| `` w `` | 查看工作区选项 | |
|
||||
| `` / `` | 通过文本过滤当前视图 | |
|
||||
|
||||
## 输入提示
|
||||
|
|
@ -403,6 +404,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | 复制分支名称到剪贴板 | |
|
||||
| `` <space> `` | 检出 | 基于当前选中的远程分支检出一个新的本地分支,或者将远程分支作分离的HEAD。 |
|
||||
| `` n `` | 新分支 | |
|
||||
| `` w `` | 新建工作树 | |
|
||||
| `` M `` | 合并到当前检出的分支 | 查看将选中项合并到当前分支的选项(正常合并,压缩合并) |
|
||||
| `` r `` | 变基 | 将检出的分支变基到所选的分支上。 |
|
||||
| `` d `` | 删除 | 从远程删除远程分支。 |
|
||||
|
|
@ -412,5 +414,4 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | 使用外部差异比较工具(git difftool) | |
|
||||
| `` 0 `` | 聚焦主视图 | |
|
||||
| `` <enter> `` | 查看提交 | |
|
||||
| `` w `` | 查看工作区选项 | |
|
||||
| `` / `` | 通过文本过滤当前视图 | |
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` R `` | 重新整理 | Refresh the git state (i.e. run `git status`, `git branch`, etc in background to update the contents of panels). This does not run `git fetch`. |
|
||||
| `` + `` | 下一個螢幕模式(常規/半螢幕/全螢幕) | |
|
||||
| `` _ `` | 上一個螢幕模式 | |
|
||||
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers |
|
||||
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
|
||||
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
|
||||
| `` <esc> `` | 取消 | |
|
||||
| `` ? `` | 開啟選單 | |
|
||||
| `` <ctrl+s> `` | 檢視篩選路徑選項 | View options for filtering the commit log, so that only commits matching the filter are shown. |
|
||||
|
|
@ -30,6 +31,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` q, <ctrl+c> `` | 結束 | |
|
||||
| `` <ctrl+z> `` | Suspend the application | |
|
||||
| `` <ctrl+w> `` | 切換是否在差異檢視中顯示空格變更 | Toggle whether or not whitespace changes are shown in the diff view.<br><br>The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. |
|
||||
| `` <alt+shift+c> `` | 編輯設定檔案 | 使用外部編輯器開啟 |
|
||||
| `` z `` | 復原 | 將使用 reflog 確任 git 指令以復原。這不包括工作區更改;只考慮提交。 |
|
||||
| `` Z `` | 取消復原 | 將使用 reflog 確任 git 指令以重作。這不包括工作區更改;只考慮提交。 |
|
||||
|
||||
|
|
@ -88,7 +90,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <space> `` | 挑選程式碼片段 | |
|
||||
| `` b `` | 挑選所有程式碼片段 | |
|
||||
| `` b `` | Pick both hunks | |
|
||||
| `` <up>, k `` | 選擇上一段 | |
|
||||
| `` <down>, j `` | 選擇下一段 | |
|
||||
| `` <left>, h `` | 選擇上一個衝突 | |
|
||||
|
|
@ -139,6 +141,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | 在瀏覽器中開啟提交 | |
|
||||
| `` n `` | 從提交建立新分支 | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` g `` | 檢視重設選項 | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | 複製提交 (揀選) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <ctrl+r> `` | 重設選定的揀選 (複製) 提交 | |
|
||||
|
|
@ -146,7 +149,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 檢視所選項目的檔案 | |
|
||||
| `` w `` | 檢視工作目錄選項 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 子模組
|
||||
|
|
@ -206,13 +208,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | 在瀏覽器中開啟提交 | |
|
||||
| `` n `` | 從提交建立新分支 | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` g `` | 檢視重設選項 | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | 複製提交 (揀選) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 檢視所選項目的檔案 | |
|
||||
| `` w `` | 檢視工作目錄選項 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 提交摘要
|
||||
|
|
@ -250,10 +252,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` g `` | 還原 | Apply the stash entry to your working directory and remove the stash entry. |
|
||||
| `` d `` | 捨棄 | Remove the stash entry from the stash list. |
|
||||
| `` n `` | 新分支 | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
|
||||
| `` w `` | New worktree | |
|
||||
| `` r `` | 重新命名收藏 | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 檢視所選項目的檔案 | |
|
||||
| `` w `` | 檢視工作目錄選項 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 日誌
|
||||
|
|
@ -266,6 +268,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` o `` | 在瀏覽器中開啟提交 | |
|
||||
| `` n `` | 從提交建立新分支 | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` g `` | 檢視重設選項 | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` C `` | 複製提交 (揀選) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
|
||||
| `` <ctrl+r> `` | 重設選定的揀選 (複製) 提交 | |
|
||||
|
|
@ -273,7 +276,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 檢視提交 | |
|
||||
| `` w `` | 檢視工作目錄選項 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 本地分支
|
||||
|
|
@ -285,6 +287,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <space> `` | 檢出 | 檢出選定的項目。 |
|
||||
| `` n `` | 新分支 | |
|
||||
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
|
||||
| `` w `` | New worktree | |
|
||||
| `` o `` | 建立拉取請求 | |
|
||||
| `` O `` | 建立拉取請求選項 | |
|
||||
| `` G `` | Open pull request in browser | |
|
||||
|
|
@ -304,7 +307,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 檢視提交 | |
|
||||
| `` w `` | 檢視工作目錄選項 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 標籤
|
||||
|
|
@ -314,13 +316,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | Copy tag to clipboard | |
|
||||
| `` <space> `` | 檢出 | Checkout the selected tag as a detached HEAD. |
|
||||
| `` n `` | 建立標籤 | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. |
|
||||
| `` w `` | New worktree | |
|
||||
| `` d `` | 刪除 | View delete options for local/remote tag. |
|
||||
| `` P `` | 推送標籤 | Push the selected tag to a remote. You'll be prompted to select a remote. |
|
||||
| `` g `` | 重設 | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 檢視提交 | |
|
||||
| `` w `` | 檢視工作目錄選項 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 檔案
|
||||
|
|
@ -368,7 +370,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` o `` | 開啟設定檔案 | 使用預設軟體開啟 |
|
||||
| `` e `` | 編輯設定檔案 | 使用外部編輯器開啟 |
|
||||
| `` u `` | 檢查更新 | |
|
||||
| `` <enter> `` | 切換到最近使用的版本庫 | |
|
||||
|
|
@ -403,6 +404,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+o> `` | 複製分支名稱到剪貼簿 | |
|
||||
| `` <space> `` | 檢出 | Checkout a new local branch based on the selected remote branch, or the remote branch as a detached head. |
|
||||
| `` n `` | 新分支 | |
|
||||
| `` w `` | New worktree | |
|
||||
| `` M `` | 合併到當前檢出的分支 | View options for merging the selected item into the current branch (regular merge, squash merge) |
|
||||
| `` r `` | 將已檢出的分支變基至此分支 | Rebase the checked-out branch onto the selected branch. |
|
||||
| `` d `` | 刪除 | Delete the remote branch from the remote. |
|
||||
|
|
@ -412,5 +414,4 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <enter> `` | 檢視提交 | |
|
||||
| `` w `` | 檢視工作目錄選項 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
|
|
|||
17
go.mod
17
go.mod
|
|
@ -13,7 +13,7 @@ require (
|
|||
github.com/cli/go-gh/v2 v2.13.0
|
||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/gdamore/tcell/v3 v3.4.0
|
||||
github.com/gdamore/tcell/v3 v3.4.1
|
||||
github.com/go-errors/errors v1.5.1
|
||||
github.com/gookit/color v1.6.1
|
||||
github.com/integrii/flaggy v1.8.0
|
||||
|
|
@ -25,6 +25,7 @@ require (
|
|||
github.com/lucasb-eyer/go-colorful v1.4.0
|
||||
github.com/mgutz/str v1.2.0
|
||||
github.com/mitchellh/go-ps v1.0.0
|
||||
github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe
|
||||
github.com/rivo/uniseg v0.4.7
|
||||
github.com/sahilm/fuzzy v0.1.3
|
||||
github.com/samber/lo v1.53.0
|
||||
|
|
@ -37,8 +38,8 @@ require (
|
|||
github.com/stretchr/testify v1.11.1
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
|
||||
golang.org/x/sync v0.21.0
|
||||
golang.org/x/sys v0.46.0
|
||||
golang.org/x/sync v0.22.0
|
||||
golang.org/x/sys v0.47.0
|
||||
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
|
@ -62,14 +63,12 @@ require (
|
|||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/onsi/ginkgo v1.10.3 // indirect
|
||||
github.com/onsi/gomega v1.34.1 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
golang.org/x/mod v0.35.0 // indirect
|
||||
golang.org/x/net v0.55.0 // indirect
|
||||
golang.org/x/term v0.43.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
golang.org/x/tools v0.44.0 // indirect
|
||||
golang.org/x/mod v0.37.0 // indirect
|
||||
golang.org/x/term v0.45.0 // indirect
|
||||
golang.org/x/text v0.40.0 // indirect
|
||||
golang.org/x/tools v0.47.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/fsnotify.v1 v1.4.7 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
|
|
|
|||
32
go.sum
32
go.sum
|
|
@ -32,8 +32,8 @@ 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/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
|
||||
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
|
||||
github.com/gdamore/tcell/v3 v3.4.0 h1:VUym1HQZiYodA5PGQrqLxF7QwqQndcAUwQD7G7XUy5E=
|
||||
github.com/gdamore/tcell/v3 v3.4.0/go.mod h1:fjKxNiIFwbzTxDU+i+AAMz+xPOgXVaZq5tbShsKseHc=
|
||||
github.com/gdamore/tcell/v3 v3.4.1 h1:22227t1EUwqxTlmCX9vw0RUE2IEPGw6oYcNan+bPe4w=
|
||||
github.com/gdamore/tcell/v3 v3.4.1/go.mod h1:YWwuxZNi14VGQC5g2VGNEDRXpBraTwvVjMovRH6G6hw=
|
||||
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
|
||||
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
|
|
@ -139,19 +139,19 @@ golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0
|
|||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20170407050850-f3918c30c5c2/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
|
@ -163,26 +163,26 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
|
|
|
|||
2
justfile
2
justfile
|
|
@ -40,7 +40,7 @@ lint:
|
|||
./scripts/gofumpt-check.sh
|
||||
./scripts/golangci-lint-shim.sh run
|
||||
|
||||
e2e-test-command := "go test pkg/integration/clients/*.go"
|
||||
e2e-test-command := "go test -timeout 30m pkg/integration/clients/*.go"
|
||||
|
||||
# Run integration tests headlessly: no args runs all tests, a test name (or path) runs just that one. Use e2e-cli for a visible UI.
|
||||
e2e *args:
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ func Start(buildInfo *BuildInfo, integrationTest integrationTypes.IntegrationTes
|
|||
if cliArgs.PrintDefaultConfig {
|
||||
var buf bytes.Buffer
|
||||
encoder := yaml.NewEncoder(&buf)
|
||||
err := encoder.Encode(config.GetDefaultConfigForPlatform(runtime.GOOS))
|
||||
err := encoder.Encode(config.GetDefaultConfigForPlatform(config.KeybindingPlatform()))
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,10 @@ func NewGitCommand(
|
|||
return nil, utils.WrapError(err)
|
||||
}
|
||||
|
||||
// Pin the config reads to the repo directory like all other git commands
|
||||
// (see NewGitCmdObjBuilder); the config commands run outside that builder.
|
||||
gitConfig.SetDir(repoPaths.WorktreePath())
|
||||
|
||||
return NewGitCommandAux(
|
||||
cmn,
|
||||
version,
|
||||
|
|
@ -90,7 +94,7 @@ func NewGitCommandAux(
|
|||
repoPaths *git_commands.RepoPaths,
|
||||
pagerConfig *config.PagerConfig,
|
||||
) *GitCommand {
|
||||
cmd := NewGitCmdObjBuilder(cmn.Log, osCommand.Cmd)
|
||||
cmd := NewGitCmdObjBuilder(cmn.Log, osCommand.Cmd, repoPaths.WorktreePath())
|
||||
|
||||
// here we're doing a bunch of dependency injection for each of our commands structs.
|
||||
// This is admittedly messy, but allows us to test each command struct in isolation,
|
||||
|
|
@ -117,8 +121,8 @@ func NewGitCommandAux(
|
|||
rebaseCommands := git_commands.NewRebaseCommands(gitCommon, commitCommands, workingTreeCommands)
|
||||
stashCommands := git_commands.NewStashCommands(gitCommon, fileLoader, workingTreeCommands)
|
||||
patchBuilder := patch.NewPatchBuilder(cmn.Log,
|
||||
func(from string, to string, reverse bool, filename string, plain bool) (string, error) {
|
||||
return workingTreeCommands.ShowFileDiff(from, to, reverse, filename, plain)
|
||||
func(from string, to string, reverse bool, filename string, previousPath string, plain bool) (string, error) {
|
||||
return workingTreeCommands.ShowFileDiff(from, to, reverse, filename, previousPath, plain)
|
||||
})
|
||||
patchCommands := git_commands.NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchBuilder)
|
||||
bisectCommands := git_commands.NewBisectCommands(gitCommon)
|
||||
|
|
|
|||
|
|
@ -1,20 +1,59 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// NewGitCmdObjBuilder returns a command object builder whose runner is wrapped
|
||||
// with our git-specific runner (logging, credential handling, etc.).
|
||||
func NewGitCmdObjBuilder(log *logrus.Entry, innerBuilder *oscommands.CmdObjBuilder) *oscommands.CmdObjBuilder {
|
||||
// We decorate the runner rather than exposing the builder's runner field:
|
||||
// that field stays unexported so there's a single API for running commands
|
||||
// across the codebase.
|
||||
return innerBuilder.CloneWithNewRunner(func(runner oscommands.ICmdObjRunner) oscommands.ICmdObjRunner {
|
||||
// all we're doing here is wrapping the default command object builder with
|
||||
// some git-specific stuff: e.g. adding a git-specific env var
|
||||
|
||||
type gitCmdObjBuilder struct {
|
||||
innerBuilder *oscommands.CmdObjBuilder
|
||||
|
||||
// The directory of the repo (or worktree) this builder was created for;
|
||||
// every command we produce runs there, regardless of the process's current
|
||||
// working directory. The two are the same until the user switches to
|
||||
// another repo: lazygit chdirs on a switch, but work still in flight for
|
||||
// the previous repo (e.g. a background refresh spawning commands through
|
||||
// the old builder) must keep running its commands against the repo it
|
||||
// started in, not whichever one the process has since moved to.
|
||||
repoDir string
|
||||
}
|
||||
|
||||
var _ oscommands.ICmdObjBuilder = &gitCmdObjBuilder{}
|
||||
|
||||
// We disable git's optional locks on every command by default so that our git
|
||||
// invocations never contend for index.lock. See git_commands.OptionalLocksEnvVar
|
||||
// for the full rationale. Individual commands that do want the lock (currently
|
||||
// only the foreground files refresh) opt back in via CmdObj.RemoveEnvVar.
|
||||
var defaultEnvVar = git_commands.OptionalLocksEnvVar + "=0"
|
||||
|
||||
func NewGitCmdObjBuilder(log *logrus.Entry, innerBuilder *oscommands.CmdObjBuilder, repoDir string) *gitCmdObjBuilder {
|
||||
// the price of having a convenient interface where we can say .New(...).Run() is that our builder now depends on our runner, so when we want to wrap the default builder/runner in new functionality we need to jump through some hoops. We could avoid the use of a decorator function here by just exporting the runner field on the default builder but that would be misleading because we don't want anybody using that to run commands (i.e. we want there to be a single API used across the codebase)
|
||||
updatedBuilder := innerBuilder.CloneWithNewRunner(func(runner oscommands.ICmdObjRunner) oscommands.ICmdObjRunner {
|
||||
return &gitCmdObjRunner{
|
||||
log: log,
|
||||
innerRunner: runner,
|
||||
log: log,
|
||||
innerRunner: runner,
|
||||
initialRetryDelay: defaultInitialRetryDelay,
|
||||
}
|
||||
})
|
||||
|
||||
return &gitCmdObjBuilder{
|
||||
innerBuilder: updatedBuilder,
|
||||
repoDir: repoDir,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *gitCmdObjBuilder) New(args []string) *oscommands.CmdObj {
|
||||
return self.innerBuilder.New(args).AddEnvVars(defaultEnvVar).SetWd(self.repoDir)
|
||||
}
|
||||
|
||||
func (self *gitCmdObjBuilder) NewShell(cmdStr string, shellFunctionsFile string) *oscommands.CmdObj {
|
||||
return self.innerBuilder.NewShell(cmdStr, shellFunctionsFile).AddEnvVars(defaultEnvVar).SetWd(self.repoDir)
|
||||
}
|
||||
|
||||
func (self *gitCmdObjBuilder) Quote(str string) string {
|
||||
return self.innerBuilder.Quote(str)
|
||||
}
|
||||
|
|
|
|||
41
pkg/commands/git_cmd_obj_builder_test.go
Normal file
41
pkg/commands/git_cmd_obj_builder_test.go
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Every git command we build disables optional locks by default, so that our
|
||||
// invocations never contend for index.lock (see git_commands.OptionalLocksEnvVar
|
||||
// for the rationale). Commands that want the lock opt back in with
|
||||
// CmdObj.RemoveEnvVar.
|
||||
func TestGitCmdObjBuilderDisablesOptionalLocksByDefault(t *testing.T) {
|
||||
builder := NewGitCmdObjBuilder(
|
||||
utils.NewDummyLog(),
|
||||
oscommands.NewDummyCmdObjBuilder(oscommands.NewFakeRunner(t)),
|
||||
"/path/to/repo",
|
||||
)
|
||||
|
||||
assert.Contains(t, builder.New([]string{"git", "status"}).GetEnvVars(), git_commands.OptionalLocksEnvVar+"=0")
|
||||
assert.Contains(t, builder.NewShell("git status", "").GetEnvVars(), git_commands.OptionalLocksEnvVar+"=0")
|
||||
}
|
||||
|
||||
// Every command the builder produces runs in the directory of the repo the
|
||||
// builder was created for, not in the process's current directory: lazygit
|
||||
// chdirs when switching repos, and commands built for the previous repo after
|
||||
// that (e.g. by a background refresh still in flight) must keep addressing the
|
||||
// repo they were built for.
|
||||
func TestGitCmdObjBuilderPinsCommandsToRepoDir(t *testing.T) {
|
||||
builder := NewGitCmdObjBuilder(
|
||||
utils.NewDummyLog(),
|
||||
oscommands.NewDummyCmdObjBuilder(oscommands.NewFakeRunner(t)),
|
||||
"/path/to/repo",
|
||||
)
|
||||
|
||||
assert.Equal(t, "/path/to/repo", builder.New([]string{"git", "status"}).GetCmd().Dir)
|
||||
assert.Equal(t, "/path/to/repo", builder.NewShell("git status", "").GetCmd().Dir)
|
||||
}
|
||||
|
|
@ -11,20 +11,42 @@ import (
|
|||
// here we're wrapping the default command runner in some git-specific stuff e.g. retry logic if we get an error due to the presence of .git/index.lock
|
||||
|
||||
const (
|
||||
WaitTime = 50 * time.Millisecond
|
||||
RetryCount = 5
|
||||
// defaultInitialRetryDelay is how long we wait before the first retry of a
|
||||
// command that failed with a transient lock error. We double it before each
|
||||
// subsequent retry (see retryOnLockError), so across maxRetries attempts we
|
||||
// wait for a bit over a second in total. That's long enough to outlast the
|
||||
// brief window during which another git process holds a lock we need —
|
||||
// typically our own foreground `git status` refresh, which takes index.lock
|
||||
// to persist its refreshed stat-cache.
|
||||
defaultInitialRetryDelay = 20 * time.Millisecond
|
||||
maxRetries = 7
|
||||
)
|
||||
|
||||
type gitCmdObjRunner struct {
|
||||
log *logrus.Entry
|
||||
innerRunner oscommands.ICmdObjRunner
|
||||
// initialRetryDelay is the wait before the first lock-error retry. It's a
|
||||
// field rather than the constant directly so tests can set it to zero and
|
||||
// not actually sleep.
|
||||
initialRetryDelay time.Duration
|
||||
}
|
||||
|
||||
// isRetryableError returns true if the error output indicates a transient
|
||||
// lock-related error that may succeed on retry
|
||||
func isRetryableError(output string) bool {
|
||||
return strings.Contains(output, ".git/index.lock") ||
|
||||
strings.Contains(output, "cannot lock ref")
|
||||
// isRetryableError returns true if a failed command hit a transient
|
||||
// lock-related condition that may succeed on retry. The lock message can reach
|
||||
// us either in the command's captured output or, for streamed commands whose
|
||||
// output we don't capture, only in the returned error, so we check both.
|
||||
//
|
||||
// We match the bare "index.lock" fragment rather than a fuller path or message
|
||||
// so we catch the lock wherever git puts it: the main .git dir, a linked
|
||||
// worktree's git dir (.git/worktrees/<name>/index.lock), or a submodule's git
|
||||
// dir.
|
||||
func isRetryableError(output string, err error) bool {
|
||||
text := output
|
||||
if err != nil {
|
||||
text += "\n" + err.Error()
|
||||
}
|
||||
return strings.Contains(text, "index.lock") ||
|
||||
strings.Contains(text, "cannot lock ref")
|
||||
}
|
||||
|
||||
func (self *gitCmdObjRunner) Run(cmdObj *oscommands.CmdObj) error {
|
||||
|
|
@ -33,41 +55,44 @@ func (self *gitCmdObjRunner) Run(cmdObj *oscommands.CmdObj) error {
|
|||
}
|
||||
|
||||
func (self *gitCmdObjRunner) RunWithOutput(cmdObj *oscommands.CmdObj) (string, error) {
|
||||
var output string
|
||||
var err error
|
||||
for range RetryCount {
|
||||
newCmdObj := cmdObj.Clone()
|
||||
output, err = self.innerRunner.RunWithOutput(newCmdObj)
|
||||
|
||||
if err == nil || !isRetryableError(output) {
|
||||
return output, err
|
||||
}
|
||||
|
||||
// if we have an error based on a lock, we should wait a bit and then retry
|
||||
self.log.Warn("lock error prevented command from running. Retrying command after a small wait")
|
||||
time.Sleep(WaitTime)
|
||||
}
|
||||
|
||||
return output, err
|
||||
return self.retryOnLockError(func() (string, error) {
|
||||
return self.innerRunner.RunWithOutput(cmdObj.Clone())
|
||||
})
|
||||
}
|
||||
|
||||
func (self *gitCmdObjRunner) RunWithOutputs(cmdObj *oscommands.CmdObj) (string, string, error) {
|
||||
var stdout, stderr string
|
||||
var err error
|
||||
for range RetryCount {
|
||||
newCmdObj := cmdObj.Clone()
|
||||
stdout, stderr, err = self.innerRunner.RunWithOutputs(newCmdObj)
|
||||
_, err := self.retryOnLockError(func() (string, error) {
|
||||
var runErr error
|
||||
stdout, stderr, runErr = self.innerRunner.RunWithOutputs(cmdObj.Clone())
|
||||
return stdout + stderr, runErr
|
||||
})
|
||||
return stdout, stderr, err
|
||||
}
|
||||
|
||||
if err == nil || !isRetryableError(stdout+stderr) {
|
||||
return stdout, stderr, err
|
||||
// retryOnLockError runs the given function, retrying if it fails with a
|
||||
// transient lock error (see isRetryableError). The string returned by run is
|
||||
// the command output we inspect to classify the failure. We clone the command
|
||||
// for each attempt (inside run) because an *exec.Cmd can only be run once.
|
||||
func (self *gitCmdObjRunner) retryOnLockError(run func() (string, error)) (string, error) {
|
||||
delay := self.initialRetryDelay
|
||||
var output string
|
||||
var err error
|
||||
for attempt := range maxRetries {
|
||||
output, err = run()
|
||||
|
||||
if err == nil || !isRetryableError(output, err) {
|
||||
break
|
||||
}
|
||||
|
||||
// if we have an error based on a lock, we should wait a bit and then retry
|
||||
self.log.Warn("lock error prevented command from running. Retrying command after a small wait")
|
||||
time.Sleep(WaitTime)
|
||||
if attempt < maxRetries-1 {
|
||||
self.log.Warnf("lock error prevented command from running; retrying in %s", delay)
|
||||
time.Sleep(delay)
|
||||
delay *= 2
|
||||
}
|
||||
}
|
||||
|
||||
return stdout, stderr, err
|
||||
return output, err
|
||||
}
|
||||
|
||||
// Retry logic not implemented here, but these commands typically don't need to obtain a lock.
|
||||
|
|
|
|||
137
pkg/commands/git_cmd_obj_runner_test.go
Normal file
137
pkg/commands/git_cmd_obj_runner_test.go
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type runnerResult struct {
|
||||
output string
|
||||
err error
|
||||
}
|
||||
|
||||
// scriptedRunner is an ICmdObjRunner stub that returns a preconfigured result
|
||||
// for each successive call, letting us drive the retry loop deterministically.
|
||||
// It counts calls so tests can assert whether a command was retried.
|
||||
type scriptedRunner struct {
|
||||
results []runnerResult
|
||||
calls int
|
||||
}
|
||||
|
||||
func (self *scriptedRunner) next() (string, error) {
|
||||
result := self.results[self.calls]
|
||||
self.calls++
|
||||
return result.output, result.err
|
||||
}
|
||||
|
||||
func (self *scriptedRunner) Run(*oscommands.CmdObj) error {
|
||||
_, err := self.next()
|
||||
return err
|
||||
}
|
||||
|
||||
func (self *scriptedRunner) RunWithOutput(*oscommands.CmdObj) (string, error) {
|
||||
return self.next()
|
||||
}
|
||||
|
||||
func (self *scriptedRunner) RunWithOutputs(*oscommands.CmdObj) (string, string, error) {
|
||||
output, err := self.next()
|
||||
return output, "", err
|
||||
}
|
||||
|
||||
func (self *scriptedRunner) RunAndProcessLines(*oscommands.CmdObj, func(string) (bool, error)) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func newTestRunner(inner *scriptedRunner) *gitCmdObjRunner {
|
||||
return &gitCmdObjRunner{
|
||||
log: utils.NewDummyLog(),
|
||||
innerRunner: inner,
|
||||
// don't actually sleep between retries
|
||||
initialRetryDelay: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// dummyCmdObj returns a throwaway command; only its clonability matters, since
|
||||
// the scriptedRunner ignores it and returns preconfigured results.
|
||||
func dummyCmdObj() *oscommands.CmdObj {
|
||||
return oscommands.NewDummyCmdObjBuilder(nil).New([]string{"git", "status"})
|
||||
}
|
||||
|
||||
func TestRunWithOutputReturnsSuccessWithoutRetrying(t *testing.T) {
|
||||
inner := &scriptedRunner{results: []runnerResult{{output: "done", err: nil}}}
|
||||
|
||||
output, err := newTestRunner(inner).RunWithOutput(dummyCmdObj())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "done", output)
|
||||
assert.Equal(t, 1, inner.calls)
|
||||
}
|
||||
|
||||
func TestRunWithOutputDoesNotRetryNonLockError(t *testing.T) {
|
||||
inner := &scriptedRunner{results: []runnerResult{{output: "boom", err: errors.New("boom")}}}
|
||||
|
||||
_, err := newTestRunner(inner).RunWithOutput(dummyCmdObj())
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 1, inner.calls)
|
||||
}
|
||||
|
||||
func TestRunWithOutputRetriesWhenLockErrorIsInOutput(t *testing.T) {
|
||||
inner := &scriptedRunner{results: []runnerResult{
|
||||
{output: "fatal: Unable to create '/repo/.git/index.lock': File exists.", err: errors.New("exit status 128")},
|
||||
{output: "done", err: nil},
|
||||
}}
|
||||
|
||||
output, err := newTestRunner(inner).RunWithOutput(dummyCmdObj())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "done", output)
|
||||
assert.Equal(t, 2, inner.calls)
|
||||
}
|
||||
|
||||
func TestRunWithOutputRetriesWhenLockErrorIsOnlyInError(t *testing.T) {
|
||||
// A streamed command (e.g. an amend run through the gpg helper) doesn't
|
||||
// capture its output, so a lock failure surfaces only in the returned error
|
||||
// with an empty output string. The retry logic must still recognize it.
|
||||
inner := &scriptedRunner{results: []runnerResult{
|
||||
{output: "", err: errors.New("fatal: Unable to create '/repo/.git/index.lock': File exists.")},
|
||||
{output: "", err: nil},
|
||||
}}
|
||||
|
||||
_, err := newTestRunner(inner).RunWithOutput(dummyCmdObj())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, inner.calls)
|
||||
}
|
||||
|
||||
func TestRunWithOutputGivesUpAfterMaxRetries(t *testing.T) {
|
||||
results := make([]runnerResult, maxRetries)
|
||||
for i := range results {
|
||||
results[i] = runnerResult{err: errors.New("fatal: Unable to create '/repo/.git/index.lock': File exists.")}
|
||||
}
|
||||
inner := &scriptedRunner{results: results}
|
||||
|
||||
_, err := newTestRunner(inner).RunWithOutput(dummyCmdObj())
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, maxRetries, inner.calls)
|
||||
}
|
||||
|
||||
func TestRunWithOutputRetriesLockErrorInLinkedWorktree(t *testing.T) {
|
||||
// In a linked worktree the lock lives at .git/worktrees/<name>/index.lock
|
||||
// rather than .git/index.lock, so only matching the bare "index.lock"
|
||||
// fragment lets the retry fire there too.
|
||||
inner := &scriptedRunner{results: []runnerResult{
|
||||
{output: "", err: errors.New("fatal: Unable to create '/repo/.git/worktrees/feature/index.lock': File exists.")},
|
||||
{output: "", err: nil},
|
||||
}}
|
||||
|
||||
_, err := newTestRunner(inner).RunWithOutput(dummyCmdObj())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, inner.calls)
|
||||
}
|
||||
|
|
@ -29,5 +29,5 @@ func (self *BlameCommands) BlameLineRange(filename string, commit string, firstL
|
|||
Arg("--").
|
||||
Arg(filename)
|
||||
|
||||
return self.cmd.New(cmdArgs.ToArgv()).RunWithOutput()
|
||||
return self.cmd.New(cmdArgs.ToArgv()).DontLog().RunWithOutput()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
package git_commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"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 {
|
||||
|
|
@ -29,7 +29,7 @@ func (self *CommitFileLoader) GetFilesInDiff(from string, to string, reverse boo
|
|||
Arg("--no-ext-diff").
|
||||
Arg("--name-status").
|
||||
Arg("-z").
|
||||
Arg("--no-renames").
|
||||
Arg(fmt.Sprintf("--find-renames=%d%%", self.UserConfig().Git.RenameSimilarityThreshold)).
|
||||
ArgIf(reverse, "-R").
|
||||
Arg(from).
|
||||
Arg(to).
|
||||
|
|
@ -44,18 +44,37 @@ func (self *CommitFileLoader) GetFilesInDiff(from string, to string, reverse boo
|
|||
}
|
||||
|
||||
// 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
|
||||
// so we need to split it by the null character and then map each status-name pair
|
||||
// to a commit file. Renames (and copies) are special: their status is followed by
|
||||
// two paths (the old one and the new one) rather than one, e.g.
|
||||
// "R100\x00old\x00new\x00".
|
||||
func getCommitFilesFromFilenames(filenames string) []*models.CommitFile {
|
||||
lines := strings.Split(strings.TrimRight(filenames, "\x00"), "\x00")
|
||||
if len(lines) == 1 {
|
||||
fields := strings.Split(strings.TrimRight(filenames, "\x00"), "\x00")
|
||||
if len(fields) == 1 {
|
||||
return []*models.CommitFile{}
|
||||
}
|
||||
|
||||
// typical result looks like 'A my_file' meaning my_file was added
|
||||
return lo.Map(lo.Chunk(lines, 2), func(chunk []string, _ int) *models.CommitFile {
|
||||
return &models.CommitFile{
|
||||
ChangeStatus: chunk[0],
|
||||
Path: chunk[1],
|
||||
commitFiles := make([]*models.CommitFile, 0, len(fields)/2)
|
||||
for i := 0; i < len(fields)-1; {
|
||||
changeStatus := fields[i]
|
||||
if changeStatus[0] == 'R' || changeStatus[0] == 'C' {
|
||||
// The status has a similarity score appended (e.g. "R100"); drop it
|
||||
// so the rest of the code only has to deal with a plain "R" or "C".
|
||||
commitFiles = append(commitFiles, &models.CommitFile{
|
||||
ChangeStatus: changeStatus[:1],
|
||||
PreviousPath: fields[i+1],
|
||||
Path: fields[i+2],
|
||||
})
|
||||
i += 3
|
||||
} else {
|
||||
// typical result looks like 'A my_file' meaning my_file was added
|
||||
commitFiles = append(commitFiles, &models.CommitFile{
|
||||
ChangeStatus: changeStatus,
|
||||
Path: fields[i+1],
|
||||
})
|
||||
i += 2
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return commitFiles
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,25 @@ func TestGetCommitFilesFromFilenames(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "a rename among regular files",
|
||||
input: "M\x00Myfile\x00R100\x00before\x00after\x00A\x00Added\x00",
|
||||
output: []*models.CommitFile{
|
||||
{
|
||||
Path: "Myfile",
|
||||
ChangeStatus: "M",
|
||||
},
|
||||
{
|
||||
Path: "after",
|
||||
PreviousPath: "before",
|
||||
ChangeStatus: "R",
|
||||
},
|
||||
{
|
||||
Path: "Added",
|
||||
ChangeStatus: "A",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
|
|
|||
|
|
@ -36,10 +36,11 @@ type GetStatusFileOptions struct {
|
|||
// This is useful for users with bare repos for dotfiles who default to hiding untracked files,
|
||||
// but want to occasionally see them to `git add` a new file.
|
||||
ForceShowUntracked bool
|
||||
// When true, this status is part of an unattended background refresh, so we
|
||||
// pass --no-optional-locks to avoid index.lock contention with git commands
|
||||
// the user runs in a terminal (at the cost of not persisting git's refreshed
|
||||
// stat-cache).
|
||||
// When true, this status is part of an unattended background refresh, so it
|
||||
// keeps the default suppression of optional locks (avoiding index.lock
|
||||
// contention with git commands the user runs in a terminal, at the cost of
|
||||
// not persisting git's refreshed stat-cache). A foreground status opts back
|
||||
// in; see gitStatus.
|
||||
Background bool
|
||||
}
|
||||
|
||||
|
|
@ -175,7 +176,6 @@ func (self *FileLoader) gitDiffNumStat() (string, error) {
|
|||
|
||||
func (self *FileLoader) gitStatus(opts GitStatusOptions) ([]FileStatus, error) {
|
||||
cmdArgs := NewGitCmd("status").
|
||||
GlobalArgIf(opts.Background, "--no-optional-locks").
|
||||
Arg(opts.UntrackedFilesArg).
|
||||
Arg("--porcelain").
|
||||
Arg("-z").
|
||||
|
|
@ -186,7 +186,17 @@ func (self *FileLoader) gitStatus(opts GitStatusOptions) ([]FileStatus, error) {
|
|||
).
|
||||
ToArgv()
|
||||
|
||||
statusLines, _, err := self.cmd.New(cmdArgs).DontLog().RunWithOutputs()
|
||||
cmdObj := self.cmd.New(cmdArgs).DontLog()
|
||||
if !opts.Background {
|
||||
// Every git command suppresses optional locks by default (see
|
||||
// OptionalLocksEnvVar). A foreground refresh is the one exception: we let
|
||||
// it take the lock so it persists git's refreshed stat-cache, which keeps
|
||||
// subsequent status calls fast. Background refreshes leave it suppressed so
|
||||
// they can't contend for index.lock.
|
||||
cmdObj.RemoveEnvVar(OptionalLocksEnvVar)
|
||||
}
|
||||
|
||||
statusLines, _, err := cmdObj.RunWithOutputs()
|
||||
if err != nil {
|
||||
return []FileStatus{}, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ func TestFileGetStatusFiles(t *testing.T) {
|
|||
type scenario struct {
|
||||
testName string
|
||||
similarityThreshold int
|
||||
background bool
|
||||
runner oscommands.ICmdObjRunner
|
||||
showNumstatInFilesView bool
|
||||
expectedFiles []*models.File
|
||||
|
|
@ -27,14 +26,6 @@ func TestFileGetStatusFiles(t *testing.T) {
|
|||
ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z", "--find-renames=50%"}, "", nil),
|
||||
expectedFiles: []*models.File{},
|
||||
},
|
||||
{
|
||||
testName: "Background refresh passes --no-optional-locks",
|
||||
similarityThreshold: 50,
|
||||
background: true,
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"--no-optional-locks", "status", "--untracked-files=yes", "--porcelain", "-z", "--find-renames=50%"}, "", nil),
|
||||
expectedFiles: []*models.File{},
|
||||
},
|
||||
{
|
||||
testName: "Several files found",
|
||||
similarityThreshold: 50,
|
||||
|
|
@ -255,7 +246,7 @@ func TestFileGetStatusFiles(t *testing.T) {
|
|||
getFileType: func(string) string { return "file" },
|
||||
}
|
||||
|
||||
assert.EqualValues(t, s.expectedFiles, loader.GetStatusFiles(GetStatusFileOptions{Background: s.background}))
|
||||
assert.EqualValues(t, s.expectedFiles, loader.GetStatusFiles(GetStatusFileOptions{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,16 @@ import (
|
|||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
)
|
||||
|
||||
// OptionalLocksEnvVar is the name of the environment variable that tells git
|
||||
// whether it may take "optional" locks — chiefly the index.lock that `git
|
||||
// status` grabs to write back a refreshed stat-cache. We set it to 0 on every
|
||||
// git command by default (see NewGitCmdObjBuilder) so our invocations never
|
||||
// contend for index.lock, neither with each other (e.g. a main-view `git diff
|
||||
// --submodule`, which runs `git status` inside submodules, racing a submodule
|
||||
// action) nor with git commands the user runs in a terminal. The one command
|
||||
// that opts back in is the foreground files refresh; see FileLoader.gitStatus.
|
||||
const OptionalLocksEnvVar = "GIT_OPTIONAL_LOCKS"
|
||||
|
||||
// convenience struct for building git commands. Especially useful when
|
||||
// including conditional args
|
||||
type GitCommandBuilder struct {
|
||||
|
|
@ -38,22 +48,6 @@ func (self *GitCommandBuilder) ArgIfElse(condition bool, ifTrue string, ifFalse
|
|||
return self.Arg(ifFalse)
|
||||
}
|
||||
|
||||
// GlobalArg adds top-level options for git itself (e.g. --no-optional-locks).
|
||||
// Unlike Arg, these are prepended before the command, where git expects them.
|
||||
func (self *GitCommandBuilder) GlobalArg(args ...string) *GitCommandBuilder {
|
||||
self.args = append(append([]string{}, args...), self.args...)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *GitCommandBuilder) GlobalArgIf(condition bool, args ...string) *GitCommandBuilder {
|
||||
if condition {
|
||||
self.GlobalArg(args...)
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *GitCommandBuilder) Config(value string) *GitCommandBuilder {
|
||||
// config settings come before the command
|
||||
self.args = append([]string{"-c", value}, self.args...)
|
||||
|
|
|
|||
|
|
@ -210,7 +210,10 @@ func (self *GitHubCommands) fetchRecentPRsAux(endpoint string, repoOwner string,
|
|||
req.Header.Set("Authorization", "token "+token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
// Bound the request so that a dead or extremely slow network can't leave
|
||||
// the pull-request refresh in flight for minutes. The data is auxiliary,
|
||||
// so giving up and retrying on the next refresh beats waiting.
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -28,10 +28,15 @@ func NewSubmoduleCommands(gitCommon *GitCommon) *SubmoduleCommands {
|
|||
}
|
||||
|
||||
func (self *SubmoduleCommands) GetConfigs(parentModule *models.SubmoduleConfig) ([]*models.SubmoduleConfig, error) {
|
||||
gitModulesPath := ".gitmodules"
|
||||
// Resolve the path against the repo this commands object was created for
|
||||
// rather than the process working directory, so that a read from a
|
||||
// still-running refresh keeps addressing that repo after the user
|
||||
// switched to another one.
|
||||
dir := self.repoPaths.WorktreePath()
|
||||
if parentModule != nil {
|
||||
gitModulesPath = filepath.Join(parentModule.FullPath(), gitModulesPath)
|
||||
dir = filepath.Join(dir, parentModule.FullPath())
|
||||
}
|
||||
gitModulesPath := filepath.Join(dir, ".gitmodules")
|
||||
file, err := os.Open(gitModulesPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
|
|
@ -180,7 +185,7 @@ func (self *SubmoduleCommands) ConflictSideLog(path string, side string, otherSi
|
|||
func (self *SubmoduleCommands) Stash(submodule *models.SubmoduleConfig) error {
|
||||
// if the path does not exist then it hasn't yet been initialized so we'll swallow the error
|
||||
// because the intention here is to have no dirty worktree state
|
||||
if _, err := os.Stat(submodule.Path); os.IsNotExist(err) {
|
||||
if _, err := os.Stat(filepath.Join(self.repoPaths.WorktreePath(), submodule.FullPath())); os.IsNotExist(err) {
|
||||
self.Log.Infof("submodule path %s does not exist, returning", submodule.FullPath())
|
||||
return nil
|
||||
}
|
||||
|
|
@ -213,51 +218,51 @@ func (self *SubmoduleCommands) UpdateAll() error {
|
|||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
// runInParentModule runs the given command in the submodule's parent module's
|
||||
// directory when the submodule is nested: its path arguments (and the
|
||||
// .gitmodules file the config commands touch) are relative to the parent
|
||||
// module. The directory is set on the command itself rather than by
|
||||
// temporarily chdir-ing the process there, which would leak the parent
|
||||
// module's directory into whatever other commands run concurrently (e.g. a
|
||||
// background refresh's).
|
||||
func (self *SubmoduleCommands) runInParentModule(submodule *models.SubmoduleConfig, cmdObj *oscommands.CmdObj) error {
|
||||
if submodule.ParentModule != nil {
|
||||
cmdObj.SetWd(submodule.ParentModule.FullPath())
|
||||
}
|
||||
return cmdObj.Run()
|
||||
}
|
||||
|
||||
func (self *SubmoduleCommands) Delete(submodule *models.SubmoduleConfig) error {
|
||||
// based on https://gist.github.com/myusuf3/7f645819ded92bda6677
|
||||
|
||||
if submodule.ParentModule != nil {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.Chdir(submodule.ParentModule.FullPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() { _ = os.Chdir(wd) }()
|
||||
}
|
||||
|
||||
if err := self.cmd.New(
|
||||
if err := self.runInParentModule(submodule, self.cmd.New(
|
||||
NewGitCmd("submodule").
|
||||
Arg("deinit", "--force", "--", submodule.Path).ToArgv(),
|
||||
).Run(); err != nil {
|
||||
)); err != nil {
|
||||
if !strings.Contains(err.Error(), "did not match any file(s) known to git") {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := self.cmd.New(
|
||||
if err := self.runInParentModule(submodule, self.cmd.New(
|
||||
NewGitCmd("config").
|
||||
Arg("--file", ".gitmodules", "--remove-section", "submodule."+submodule.Path).
|
||||
ToArgv(),
|
||||
).Run(); err != nil {
|
||||
)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := self.cmd.New(
|
||||
if err := self.runInParentModule(submodule, self.cmd.New(
|
||||
NewGitCmd("config").
|
||||
Arg("--remove-section", "submodule."+submodule.Path).
|
||||
ToArgv(),
|
||||
).Run(); err != nil {
|
||||
)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := self.cmd.New(
|
||||
if err := self.runInParentModule(submodule, self.cmd.New(
|
||||
NewGitCmd("rm").Arg("--force", "-r", submodule.Path).ToArgv(),
|
||||
).Run(); err != nil {
|
||||
)); err != nil {
|
||||
// if the directory isn't there then that's fine
|
||||
self.Log.Error(err)
|
||||
}
|
||||
|
|
@ -282,20 +287,6 @@ func (self *SubmoduleCommands) Add(name string, path string, url string) error {
|
|||
}
|
||||
|
||||
func (self *SubmoduleCommands) UpdateUrl(submodule *models.SubmoduleConfig, newUrl string) error {
|
||||
if submodule.ParentModule != nil {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.Chdir(submodule.ParentModule.FullPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() { _ = os.Chdir(wd) }()
|
||||
}
|
||||
|
||||
setUrlCmdStr := NewGitCmd("config").
|
||||
Arg(
|
||||
"--file", ".gitmodules", "submodule."+submodule.Name+".url", newUrl,
|
||||
|
|
@ -303,14 +294,14 @@ func (self *SubmoduleCommands) UpdateUrl(submodule *models.SubmoduleConfig, newU
|
|||
ToArgv()
|
||||
|
||||
// the set-url command is only for later git versions so we're doing it manually here
|
||||
if err := self.cmd.New(setUrlCmdStr).Run(); err != nil {
|
||||
if err := self.runInParentModule(submodule, self.cmd.New(setUrlCmdStr)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
syncCmdStr := NewGitCmd("submodule").Arg("sync", "--", submodule.Path).
|
||||
ToArgv()
|
||||
|
||||
if err := self.cmd.New(syncCmdStr).Run(); err != nil {
|
||||
if err := self.runInParentModule(submodule, self.cmd.New(syncCmdStr)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ func (self *TagCommands) HasTag(tagName string) bool {
|
|||
Arg("refs/tags/" + tagName).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run() == nil
|
||||
return self.cmd.New(cmdArgs).DontLog().Run() == nil
|
||||
}
|
||||
|
||||
func (self *TagCommands) LocalDelete(tagName string) error {
|
||||
|
|
@ -74,7 +74,7 @@ func (self *TagCommands) ShowAnnotationInfo(tagName string) (string, error) {
|
|||
Arg("refs/tags/" + tagName).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).RunWithOutput()
|
||||
return self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
||||
}
|
||||
|
||||
func (self *TagCommands) IsTagAnnotated(tagName string) (bool, error) {
|
||||
|
|
@ -83,6 +83,6 @@ func (self *TagCommands) IsTagAnnotated(tagName string) (bool, error) {
|
|||
Arg("refs/tags/" + tagName).
|
||||
ToArgv()
|
||||
|
||||
output, err := self.cmd.New(cmdArgs).RunWithOutput()
|
||||
output, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
||||
return strings.TrimSpace(output) == "tag", err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -432,8 +432,14 @@ func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain
|
|||
|
||||
// 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) {
|
||||
return self.ShowFileDiffCmdObj(from, to, reverse, []string{fileName}, plain).RunWithOutput()
|
||||
// For a renamed file, previousPath is the path it was renamed from (empty otherwise);
|
||||
// both paths must be passed to git for the rename to be detected.
|
||||
func (self *WorkingTreeCommands) ShowFileDiff(from string, to string, reverse bool, fileName string, previousPath string, plain bool) (string, error) {
|
||||
fileNames := []string{fileName}
|
||||
if previousPath != "" {
|
||||
fileNames = append(fileNames, previousPath)
|
||||
}
|
||||
return self.ShowFileDiffCmdObj(from, to, reverse, fileNames, plain).RunWithOutput()
|
||||
}
|
||||
|
||||
func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reverse bool, fileNames []string, plain bool) *oscommands.CmdObj {
|
||||
|
|
@ -454,7 +460,7 @@ func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reve
|
|||
ArgIfElse(useExtDiff || useExtDiffGitConfig, "--ext-diff", "--no-ext-diff").
|
||||
Arg("--submodule").
|
||||
Arg(fmt.Sprintf("--unified=%d", contextSize)).
|
||||
Arg("--no-renames").
|
||||
Arg(fmt.Sprintf("--find-renames=%d%%", self.UserConfig().Git.RenameSimilarityThreshold)).
|
||||
Arg(fmt.Sprintf("--color=%s", colorArg)).
|
||||
Arg(from).
|
||||
Arg(to).
|
||||
|
|
|
|||
|
|
@ -339,6 +339,8 @@ func TestWorkingTreeShowFileDiff(t *testing.T) {
|
|||
from string
|
||||
to string
|
||||
reverse bool
|
||||
fileName string
|
||||
previousPath string
|
||||
plain bool
|
||||
ignoreWhitespace bool
|
||||
contextSize uint64
|
||||
|
|
@ -353,33 +355,49 @@ func TestWorkingTreeShowFileDiff(t *testing.T) {
|
|||
from: "1234567890",
|
||||
to: "0987654321",
|
||||
reverse: false,
|
||||
fileName: "test.txt",
|
||||
plain: false,
|
||||
ignoreWhitespace: false,
|
||||
contextSize: 3,
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "diff", "--no-ext-diff", "--submodule", "--unified=3", "--no-renames", "--color=always", "1234567890", "0987654321", "--", "test.txt"}, expectedResult, nil),
|
||||
ExpectGitArgs([]string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "diff", "--no-ext-diff", "--submodule", "--unified=3", "--find-renames=50%", "--color=always", "1234567890", "0987654321", "--", "test.txt"}, expectedResult, nil),
|
||||
},
|
||||
{
|
||||
testName: "Show diff with custom context size",
|
||||
from: "1234567890",
|
||||
to: "0987654321",
|
||||
reverse: false,
|
||||
fileName: "test.txt",
|
||||
plain: false,
|
||||
ignoreWhitespace: false,
|
||||
contextSize: 123,
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "diff", "--no-ext-diff", "--submodule", "--unified=123", "--no-renames", "--color=always", "1234567890", "0987654321", "--", "test.txt"}, expectedResult, nil),
|
||||
ExpectGitArgs([]string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "diff", "--no-ext-diff", "--submodule", "--unified=123", "--find-renames=50%", "--color=always", "1234567890", "0987654321", "--", "test.txt"}, expectedResult, nil),
|
||||
},
|
||||
{
|
||||
testName: "Default case (ignore whitespace)",
|
||||
from: "1234567890",
|
||||
to: "0987654321",
|
||||
reverse: false,
|
||||
fileName: "test.txt",
|
||||
plain: false,
|
||||
ignoreWhitespace: true,
|
||||
contextSize: 3,
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "diff", "--no-ext-diff", "--submodule", "--unified=3", "--no-renames", "--color=always", "1234567890", "0987654321", "--ignore-all-space", "--", "test.txt"}, expectedResult, nil),
|
||||
ExpectGitArgs([]string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "diff", "--no-ext-diff", "--submodule", "--unified=3", "--find-renames=50%", "--color=always", "1234567890", "0987654321", "--ignore-all-space", "--", "test.txt"}, expectedResult, nil),
|
||||
},
|
||||
{
|
||||
testName: "Renamed file passes both paths so the rename is detected",
|
||||
from: "1234567890",
|
||||
to: "0987654321",
|
||||
reverse: false,
|
||||
fileName: "new.txt",
|
||||
previousPath: "old.txt",
|
||||
plain: false,
|
||||
ignoreWhitespace: false,
|
||||
contextSize: 3,
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "diff", "--no-ext-diff", "--submodule", "--unified=3", "--find-renames=50%", "--color=always", "1234567890", "0987654321", "--", "new.txt", "old.txt"}, expectedResult, nil),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -394,7 +412,7 @@ func TestWorkingTreeShowFileDiff(t *testing.T) {
|
|||
|
||||
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner, userConfig: userConfig, appState: &config.AppState{}, repoPaths: &repoPaths})
|
||||
|
||||
result, err := instance.ShowFileDiff(s.from, s.to, s.reverse, "test.txt", s.plain)
|
||||
result, err := instance.ShowFileDiff(s.from, s.to, s.reverse, s.fileName, s.previousPath, s.plain)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedResult, result)
|
||||
s.runner.CheckForMissingCalls()
|
||||
|
|
|
|||
|
|
@ -16,11 +16,19 @@ type IGitConfig interface {
|
|||
// this is for when you want to pass 'mykey' and check if the result is truthy
|
||||
GetBool(string) bool
|
||||
|
||||
// SetDir pins the config commands to the given repo directory, so that
|
||||
// they keep reading that repo's local config even if the process working
|
||||
// directory changes later (i.e. the user switches repos while this
|
||||
// instance is still in use by in-flight work). Called once, before the
|
||||
// first read.
|
||||
SetDir(string)
|
||||
|
||||
DropCache()
|
||||
}
|
||||
|
||||
type CachedGitConfig struct {
|
||||
cache map[string]string
|
||||
dir string
|
||||
runGitConfigCmd func(*exec.Cmd) (string, error)
|
||||
log *logrus.Entry
|
||||
mutex sync.Mutex
|
||||
|
|
@ -39,6 +47,13 @@ func NewCachedGitConfig(runGitConfigCmd func(*exec.Cmd) (string, error), log *lo
|
|||
}
|
||||
}
|
||||
|
||||
func (self *CachedGitConfig) SetDir(dir string) {
|
||||
self.mutex.Lock()
|
||||
defer self.mutex.Unlock()
|
||||
|
||||
self.dir = dir
|
||||
}
|
||||
|
||||
func (self *CachedGitConfig) Get(key string) string {
|
||||
self.mutex.Lock()
|
||||
defer self.mutex.Unlock()
|
||||
|
|
@ -69,6 +84,7 @@ func (self *CachedGitConfig) GetGeneral(args string) string {
|
|||
|
||||
func (self *CachedGitConfig) getGeneralAux(args string) string {
|
||||
cmd := getGitConfigGeneralCmd(args)
|
||||
cmd.Dir = self.dir
|
||||
value, err := self.runGitConfigCmd(cmd)
|
||||
if err != nil {
|
||||
self.log.Debugf("Error getting git config value for args: %s. Error: %v", args, err.Error())
|
||||
|
|
@ -79,6 +95,7 @@ func (self *CachedGitConfig) getGeneralAux(args string) string {
|
|||
|
||||
func (self *CachedGitConfig) getAux(key string) string {
|
||||
cmd := getGitConfigCmd(key)
|
||||
cmd.Dir = self.dir
|
||||
value, err := self.runGitConfigCmd(cmd)
|
||||
if err != nil {
|
||||
self.log.Debugf("Error getting git config value for key: %s. Error: %v", key, err.Error())
|
||||
|
|
|
|||
|
|
@ -116,3 +116,20 @@ func TestGet(t *testing.T) {
|
|||
assert.Equal(t, "blah", result)
|
||||
assert.Equal(t, 1, count)
|
||||
}
|
||||
|
||||
// The config commands run in the directory set by SetDir rather than in the
|
||||
// process's current directory: lazygit chdirs when switching repos, and config
|
||||
// reads issued for the previous repo after that must keep addressing the repo
|
||||
// they were created for.
|
||||
func TestSetDirPinsCommandsToDirectory(t *testing.T) {
|
||||
real := NewCachedGitConfig(
|
||||
func(cmd *exec.Cmd) (string, error) {
|
||||
assert.Equal(t, "/path/to/repo", cmd.Dir)
|
||||
return "blah", nil
|
||||
},
|
||||
utils.NewDummyLog(),
|
||||
)
|
||||
real.SetDir("/path/to/repo")
|
||||
real.Get("commit.gpgsign")
|
||||
real.GetGeneral("--local --get-regexp foo")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,5 +28,8 @@ func (self *FakeGitConfig) GetBool(key string) bool {
|
|||
return isTruthy(self.Get(key))
|
||||
}
|
||||
|
||||
func (self *FakeGitConfig) SetDir(dir string) {
|
||||
}
|
||||
|
||||
func (self *FakeGitConfig) DropCache() {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ package models
|
|||
type CommitFile struct {
|
||||
Path string
|
||||
|
||||
// For a renamed file, the path it was renamed from; empty otherwise.
|
||||
PreviousPath string
|
||||
|
||||
ChangeStatus string // e.g. 'A' for added or 'M' for modified. This is based on the result from git diff --name-status
|
||||
}
|
||||
|
||||
|
|
@ -23,6 +26,24 @@ func (f *CommitFile) Deleted() bool {
|
|||
return f.ChangeStatus == "D"
|
||||
}
|
||||
|
||||
func (f *CommitFile) IsRename() bool {
|
||||
return f.PreviousPath != ""
|
||||
}
|
||||
|
||||
// Names returns an array containing just the path, or in the case of a rename,
|
||||
// the after path and the before path.
|
||||
func (f *CommitFile) Names() []string {
|
||||
result := []string{f.Path}
|
||||
if f.PreviousPath != "" {
|
||||
result = append(result, f.PreviousPath)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (f *CommitFile) GetPath() string {
|
||||
return f.Path
|
||||
}
|
||||
|
||||
func (f *CommitFile) GetPreviousPath() string {
|
||||
return f.PreviousPath
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,6 +91,19 @@ func (self *CmdObj) AddEnvVars(vars ...string) *CmdObj {
|
|||
return self
|
||||
}
|
||||
|
||||
// RemoveEnvVar removes every occurrence of the named environment variable from
|
||||
// the command's environment. It's the counterpart to AddEnvVars, used to opt a
|
||||
// single command out of a variable that the builder sets on every command by
|
||||
// default.
|
||||
func (self *CmdObj) RemoveEnvVar(name string) *CmdObj {
|
||||
prefix := name + "="
|
||||
self.cmd.Env = lo.Filter(self.cmd.Env, func(envVar string, _ int) bool {
|
||||
return !strings.HasPrefix(envVar, prefix)
|
||||
})
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *CmdObj) GetEnvVars() []string {
|
||||
return self.cmd.Env
|
||||
}
|
||||
|
|
@ -144,7 +157,7 @@ func (self *CmdObj) ShouldStreamOutput() bool {
|
|||
}
|
||||
|
||||
// when you call this, then call Run(), we'll use a PTY to run the command. Only
|
||||
// has an effect if StreamOutput() was also called. Ignored on Windows.
|
||||
// has an effect if StreamOutput() was also called.
|
||||
func (self *CmdObj) UsePty() *CmdObj {
|
||||
self.usePty = true
|
||||
|
||||
|
|
|
|||
|
|
@ -219,13 +219,15 @@ type cmdHandler struct {
|
|||
stdoutPipe io.Reader
|
||||
stdinPipe io.Writer
|
||||
close func() error
|
||||
// wait blocks until the child process exits. Needed as a separate
|
||||
// field because the pty path on Windows spawns via CreateProcess and
|
||||
// never runs *exec.Cmd.Start — so cmd.Wait wouldn't work there.
|
||||
wait func() error
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) runAndStream(cmdObj *CmdObj) error {
|
||||
return self.runAndStreamAux(cmdObj, func(handler *cmdHandler, cmdWriter io.Writer) {
|
||||
go func() {
|
||||
_, _ = io.Copy(cmdWriter, handler.stdoutPipe)
|
||||
}()
|
||||
_, _ = io.Copy(cmdWriter, handler.stdoutPipe)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -240,6 +242,10 @@ func (self *cmdObjRunner) runAndStreamAux(
|
|||
} else {
|
||||
cmdWriter = self.guiIO.newCmdWriterFn()
|
||||
}
|
||||
// The command's stdout and stderr are streamed to cmdWriter concurrently
|
||||
// from separate goroutines (stderr via the MultiWriter below, stdout via
|
||||
// onRun), so it must be safe for concurrent writes.
|
||||
cmdWriter = &synchronizedWriter{writer: cmdWriter}
|
||||
|
||||
if cmdObj.ShouldLog() {
|
||||
self.logCmdObj(cmdObj)
|
||||
|
|
@ -272,9 +278,28 @@ func (self *cmdObjRunner) runAndStreamAux(
|
|||
|
||||
t := time.Now()
|
||||
|
||||
onRun(handler, cmdWriter)
|
||||
// Stream the command's output on a goroutine while it runs, but keep a
|
||||
// handle on it: the buffers it fills (stdout, and combinedOutput when
|
||||
// output is suppressed) must not be read below until it has finished.
|
||||
streamingDone := make(chan struct{})
|
||||
go utils.Safe(func() {
|
||||
defer close(streamingDone)
|
||||
onRun(handler, cmdWriter)
|
||||
})
|
||||
|
||||
err = cmd.Wait()
|
||||
err = handler.wait()
|
||||
|
||||
// The command has exited; wait for the streaming goroutine to drain the
|
||||
// last of its output before reading those buffers. A pty reader reaches
|
||||
// EOF on its own now the process is gone, but the non-pty pipe never does,
|
||||
// so close it to unblock the reader — the pipe is synchronous, so all
|
||||
// output has already been read by now and nothing is lost.
|
||||
if !cmdObj.ShouldUsePty() {
|
||||
if closeErr := handler.close(); closeErr != nil {
|
||||
self.log.Error(closeErr)
|
||||
}
|
||||
}
|
||||
<-streamingDone
|
||||
|
||||
self.log.Infof("%s (%s)", cmdObj.ToString(), time.Since(t))
|
||||
|
||||
|
|
@ -350,10 +375,7 @@ func (self *cmdObjRunner) runAndDetectCredentialRequest(
|
|||
|
||||
return self.runAndStreamAux(cmdObj, func(handler *cmdHandler, cmdWriter io.Writer) {
|
||||
tr := io.TeeReader(handler.stdoutPipe, cmdWriter)
|
||||
|
||||
go utils.Safe(func() {
|
||||
self.processOutput(tr, handler.stdinPipe, promptUserForCredential, handler.close, cmdObj)
|
||||
})
|
||||
self.processOutput(tr, handler.stdinPipe, promptUserForCredential, handler.close, cmdObj)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -376,9 +398,7 @@ func (self *cmdObjRunner) processOutput(
|
|||
responseChan := promptUserForCredential(askFor)
|
||||
if responseChan == nil {
|
||||
// Returning a nil channel means we should terminate the process.
|
||||
// We achieve this by closing the pty that it's running in. Note that this won't
|
||||
// work for the case where we're not running in a pty (i.e. on Windows), but
|
||||
// in that case we'll never be prompted for credentials, so it's not a concern.
|
||||
// We achieve this by closing the pty that it's running in.
|
||||
if err := closeFunc(); err != nil {
|
||||
self.log.Error(err)
|
||||
}
|
||||
|
|
@ -449,6 +469,20 @@ func (self *cmdObjRunner) getCheckForCredentialRequestFunc() func([]byte) (Crede
|
|||
}
|
||||
}
|
||||
|
||||
// synchronizedWriter serializes writes to its underlying writer so that it can
|
||||
// be written from multiple goroutines at once (see runAndStreamAux, which
|
||||
// streams a command's stdout and stderr to one writer from two goroutines).
|
||||
type synchronizedWriter struct {
|
||||
mutex deadlock.Mutex
|
||||
writer io.Writer
|
||||
}
|
||||
|
||||
func (self *synchronizedWriter) Write(p []byte) (int, error) {
|
||||
self.mutex.Lock()
|
||||
defer self.mutex.Unlock()
|
||||
return self.writer.Write(p)
|
||||
}
|
||||
|
||||
type Buffer struct {
|
||||
b bytes.Buffer
|
||||
m deadlock.Mutex
|
||||
|
|
@ -480,6 +514,25 @@ func (self *cmdObjRunner) getCmdHandlerNonPty(cmd *exec.Cmd) (*cmdHandler, error
|
|||
return &cmdHandler{
|
||||
stdoutPipe: stdoutReader,
|
||||
stdinPipe: buf,
|
||||
close: func() error { return nil },
|
||||
// Closing the read end makes a blocked read on it return, which is how
|
||||
// runAndStreamAux unblocks and joins the streaming goroutine once the
|
||||
// command has finished (the pipe delivers no EOF of its own).
|
||||
close: func() error { return stdoutReader.Close() },
|
||||
wait: cmd.Wait,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) getCmdHandlerPty(cmd *exec.Cmd) (*cmdHandler, error) {
|
||||
// Size will be adjusted by the caller if it cares; this just avoids a
|
||||
// zero-size pty.
|
||||
sp, err := StartPty(cmd, 80, 24)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cmdHandler{
|
||||
stdoutPipe: sp.Pty,
|
||||
stdinPipe: sp.Pty,
|
||||
close: sp.Pty.Close,
|
||||
wait: sp.Wait,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
//go:build !windows
|
||||
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
|
||||
"github.com/creack/pty"
|
||||
)
|
||||
|
||||
// we define this separately for windows and non-windows given that windows does
|
||||
// not have great PTY support and we need a PTY to handle a credential request
|
||||
func (self *cmdObjRunner) getCmdHandlerPty(cmd *exec.Cmd) (*cmdHandler, error) {
|
||||
ptmx, err := pty.Start(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cmdHandler{
|
||||
stdoutPipe: ptmx,
|
||||
stdinPipe: ptmx,
|
||||
close: ptmx.Close,
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package oscommands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func (self *cmdObjRunner) getCmdHandlerPty(cmd *exec.Cmd) (*cmdHandler, error) {
|
||||
// We don't have PTY support on Windows yet, so we just return a non-PTY handler.
|
||||
return self.getCmdHandlerNonPty(cmd)
|
||||
}
|
||||
|
|
@ -5,8 +5,29 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gocui"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRemoveEnvVar(t *testing.T) {
|
||||
cmd := exec.Command("git", "status")
|
||||
cmd.Env = []string{
|
||||
"PATH=/usr/bin",
|
||||
"GIT_OPTIONAL_LOCKS=0",
|
||||
"GIT_OPTIONAL_LOCKS_OTHER=1", // name is a prefix of ours but not the same var
|
||||
"GIT_OPTIONAL_LOCKS=0", // duplicates must all be removed
|
||||
"HOME=/home/me",
|
||||
}
|
||||
cmdObj := &CmdObj{cmd: cmd}
|
||||
|
||||
cmdObj.RemoveEnvVar("GIT_OPTIONAL_LOCKS")
|
||||
|
||||
assert.Equal(t, []string{
|
||||
"PATH=/usr/bin",
|
||||
"GIT_OPTIONAL_LOCKS_OTHER=1",
|
||||
"HOME=/home/me",
|
||||
}, cmdObj.GetEnvVars())
|
||||
}
|
||||
|
||||
func TestCmdObjToString(t *testing.T) {
|
||||
quote := func(s string) string {
|
||||
return "\"" + s + "\""
|
||||
|
|
|
|||
|
|
@ -45,10 +45,10 @@ func (c *OSCommand) UpdateWindowTitle() error {
|
|||
// this call is reached on every host; only the Windows build does anything.
|
||||
func setRawCmdLine(cmd *exec.Cmd, cmdLine string) {}
|
||||
|
||||
func TerminateProcessGracefully(cmd *exec.Cmd) error {
|
||||
if cmd.Process == nil {
|
||||
func TerminateProcessGracefully(proc *os.Process) error {
|
||||
if proc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return cmd.Process.Signal(syscall.SIGTERM)
|
||||
return proc.Signal(syscall.SIGTERM)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ func (c *OSCommand) UpdateWindowTitle() error {
|
|||
return c.Cmd.NewShell(argString, c.UserConfig().OS.ShellFunctionsFile).Run()
|
||||
}
|
||||
|
||||
func TerminateProcessGracefully(cmd *exec.Cmd) error {
|
||||
func TerminateProcessGracefully(proc *os.Process) error {
|
||||
// Signals other than SIGKILL are not supported on Windows
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
34
pkg/commands/oscommands/pty.go
Normal file
34
pkg/commands/oscommands/pty.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
package oscommands
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Pty is the master side of a pseudo-terminal running a subprocess. The
|
||||
// concrete implementation is platform-specific: creack/pty on Unix and
|
||||
// ConPTY on Windows.
|
||||
type Pty interface {
|
||||
io.ReadWriteCloser
|
||||
Resize(cols, rows uint16) error
|
||||
}
|
||||
|
||||
// StartedPty is the result of StartPty.
|
||||
type StartedPty struct {
|
||||
// Pty is the master side of the pseudo-terminal; read from it to get
|
||||
// the child's combined stdout/stderr and write to it to feed stdin.
|
||||
Pty Pty
|
||||
// Process is the spawned child. Useful for signalling; on Windows the
|
||||
// original *exec.Cmd was not Start()ed (ConPTY spawns via
|
||||
// CreateProcess, not os/exec) so cmd.Process is nil and this is the
|
||||
// only handle.
|
||||
Process *os.Process
|
||||
// Wait blocks until the child exits and returns a non-nil error on a
|
||||
// nonzero exit status, matching *exec.Cmd.Wait semantics.
|
||||
Wait func() error
|
||||
}
|
||||
|
||||
// StartPty runs cmd in a pseudo-terminal with the given initial dimensions.
|
||||
// Implemented per-platform in pty_unix.go / pty_windows.go.
|
||||
//
|
||||
// func StartPty(cmd *exec.Cmd, cols, rows uint16) (StartedPty, error)
|
||||
34
pkg/commands/oscommands/pty_unix.go
Normal file
34
pkg/commands/oscommands/pty_unix.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
//go:build !windows
|
||||
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
creackpty "github.com/creack/pty"
|
||||
)
|
||||
|
||||
type unixPty struct {
|
||||
master *os.File
|
||||
}
|
||||
|
||||
func (u *unixPty) Read(p []byte) (int, error) { return u.master.Read(p) }
|
||||
func (u *unixPty) Write(p []byte) (int, error) { return u.master.Write(p) }
|
||||
func (u *unixPty) Close() error { return u.master.Close() }
|
||||
|
||||
func (u *unixPty) Resize(cols, rows uint16) error {
|
||||
return creackpty.Setsize(u.master, &creackpty.Winsize{Cols: cols, Rows: rows})
|
||||
}
|
||||
|
||||
func StartPty(cmd *exec.Cmd, cols, rows uint16) (StartedPty, error) {
|
||||
f, err := creackpty.StartWithSize(cmd, &creackpty.Winsize{Cols: cols, Rows: rows})
|
||||
if err != nil {
|
||||
return StartedPty{}, err
|
||||
}
|
||||
return StartedPty{
|
||||
Pty: &unixPty{master: f},
|
||||
Process: cmd.Process,
|
||||
Wait: cmd.Wait,
|
||||
}, nil
|
||||
}
|
||||
266
pkg/commands/oscommands/pty_windows.go
Normal file
266
pkg/commands/oscommands/pty_windows.go
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
package oscommands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
type winPty struct {
|
||||
hpc windows.Handle
|
||||
inWrite *os.File
|
||||
outRead *os.File
|
||||
|
||||
// mu guards hpcClosed, which gates ClosePseudoConsole (it must run
|
||||
// exactly once) and also keeps Resize from touching the HPCON once it's
|
||||
// been freed: the background waiter in StartPty closes the pseudoconsole
|
||||
// on child exit, which would otherwise race a concurrent onResize and
|
||||
// hand ResizePseudoConsole a freed handle.
|
||||
mu sync.Mutex
|
||||
hpcClosed bool
|
||||
}
|
||||
|
||||
func (p *winPty) Read(buf []byte) (int, error) { return p.outRead.Read(buf) }
|
||||
func (p *winPty) Write(buf []byte) (int, error) { return p.inWrite.Write(buf) }
|
||||
|
||||
func (p *winPty) Resize(cols, rows uint16) error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if p.hpcClosed {
|
||||
// The child already exited and the pseudoconsole was torn down, so
|
||||
// there is nothing left to resize.
|
||||
return nil
|
||||
}
|
||||
return windows.ResizePseudoConsole(p.hpc, windows.Coord{X: int16(cols), Y: int16(rows)})
|
||||
}
|
||||
|
||||
// closeHpc closes the pseudoconsole exactly once. Safe to call from multiple
|
||||
// goroutines and at any time. We need this separately from Close because the
|
||||
// background waiter in StartPty closes the pseudoconsole as soon as the child
|
||||
// exits — that's what makes outRead return EOF, matching the Unix behavior
|
||||
// where the master fd EOFs when the slave closes — while the pipe fds stay
|
||||
// open until somebody explicitly tears the pty down.
|
||||
func (p *winPty) closeHpc() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if p.hpcClosed {
|
||||
return
|
||||
}
|
||||
p.hpcClosed = true
|
||||
windows.ClosePseudoConsole(p.hpc)
|
||||
}
|
||||
|
||||
// Close tears the pty down without waiting for it: the teardown runs on a
|
||||
// background goroutine and Close returns immediately.
|
||||
//
|
||||
// It has to, because ClosePseudoConsole can block for a long time: before
|
||||
// Windows 11 24H2 it waits for the console host to exit, and since closing
|
||||
// only delivers CTRL_CLOSE_EVENT to the attached client without terminating
|
||||
// it, a client that keeps running (git still computing an expensive diff, a
|
||||
// pager waiting for input) keeps the host — and with it ClosePseudoConsole —
|
||||
// alive arbitrarily long. Close is called while holding the global PtyMutex
|
||||
// and while the task's onDone once is executing, where blocking wedges every
|
||||
// subsequent task for the view (and with it the UI), so none of this may
|
||||
// happen on the caller's thread.
|
||||
//
|
||||
// Within the teardown, the pipe ends must be closed before the
|
||||
// pseudoconsole, and without holding p.mu: closing the pseudoconsole flushes
|
||||
// the client's pending output into the out pipe, and with the task stopped
|
||||
// nobody is reading anymore, so that flush can only complete once the pipe
|
||||
// is broken. The background waiter's closeHpc may already be wedged in such
|
||||
// a flush while holding p.mu; closing the pipes is what unblocks it.
|
||||
func (p *winPty) Close() error {
|
||||
go utils.Safe(func() {
|
||||
p.inWrite.Close()
|
||||
p.outRead.Close()
|
||||
p.closeHpc()
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// startWaiter runs proc.Wait in a goroutine and, as soon as the child exits,
|
||||
// closes the pseudoconsole so that any pending Read on outRead returns EOF
|
||||
// after buffered output drains. Returns a Wait func that blocks until the
|
||||
// child has exited and reports its exit status with *exec.Cmd.Wait semantics.
|
||||
//
|
||||
// This shape exists because on Unix the master fd EOFs naturally when the
|
||||
// slave closes on child exit, but ConPTY keeps the pipe alive until we call
|
||||
// ClosePseudoConsole explicitly. Without doing that on child exit, the
|
||||
// scanner in pkg/tasks.NewCmdTask would block forever on the next read and
|
||||
// the post-content view never gets cleared (FlushStaleCells never fires).
|
||||
func startWaiter(proc *os.Process, p *winPty) func() error {
|
||||
done := make(chan struct{})
|
||||
var waitErr error
|
||||
go func() {
|
||||
defer close(done)
|
||||
state, err := proc.Wait()
|
||||
p.closeHpc()
|
||||
if err != nil {
|
||||
waitErr = err
|
||||
return
|
||||
}
|
||||
if !state.Success() {
|
||||
waitErr = fmt.Errorf("exit status %d", state.ExitCode())
|
||||
}
|
||||
}()
|
||||
return func() error {
|
||||
<-done
|
||||
return waitErr
|
||||
}
|
||||
}
|
||||
|
||||
func StartPty(cmd *exec.Cmd, cols, rows uint16) (sp StartedPty, err error) {
|
||||
// Two pipes: one for the child's stdin (we never write to it, but ConPTY
|
||||
// needs a handle), one for the child's stdout/stderr multiplexed through
|
||||
// the pseudoconsole.
|
||||
var inRead, inWrite, outRead, outWrite windows.Handle
|
||||
if err = windows.CreatePipe(&inRead, &inWrite, nil, 0); err != nil {
|
||||
return StartedPty{}, fmt.Errorf("CreatePipe (in): %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
_ = windows.CloseHandle(inWrite)
|
||||
}
|
||||
}()
|
||||
if err = windows.CreatePipe(&outRead, &outWrite, nil, 0); err != nil {
|
||||
_ = windows.CloseHandle(inRead)
|
||||
return StartedPty{}, fmt.Errorf("CreatePipe (out): %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
_ = windows.CloseHandle(outRead)
|
||||
}
|
||||
}()
|
||||
|
||||
// CreatePseudoConsole dupes the handles it needs internally; we release
|
||||
// our references to the child-side ends immediately after.
|
||||
var hpc windows.Handle
|
||||
size := windows.Coord{X: int16(cols), Y: int16(rows)}
|
||||
if err = windows.CreatePseudoConsole(size, inRead, outWrite, 0, &hpc); err != nil {
|
||||
_ = windows.CloseHandle(inRead)
|
||||
_ = windows.CloseHandle(outWrite)
|
||||
return StartedPty{}, fmt.Errorf("CreatePseudoConsole: %w", err)
|
||||
}
|
||||
_ = windows.CloseHandle(inRead)
|
||||
_ = windows.CloseHandle(outWrite)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
windows.ClosePseudoConsole(hpc)
|
||||
}
|
||||
}()
|
||||
|
||||
// Attach the pseudoconsole to the child via a process attribute list.
|
||||
attrList, err := windows.NewProcThreadAttributeList(1)
|
||||
if err != nil {
|
||||
return StartedPty{}, fmt.Errorf("NewProcThreadAttributeList: %w", err)
|
||||
}
|
||||
defer attrList.Delete()
|
||||
// PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE wants the HPCON value itself as
|
||||
// the attribute value, not a pointer to it — an HPCON is already a
|
||||
// pointer-sized handle, per Microsoft's ConPTY sample. Spelling that as
|
||||
// unsafe.Pointer(hpc) trips go vet's unsafeptr check (a uintptr-based
|
||||
// type converted straight to unsafe.Pointer), which gopls surfaces in
|
||||
// the editor. Reinterpret the handle's bits through its address instead:
|
||||
// &hpc is a real pointer, so none of these conversions is the flagged
|
||||
// uintptr→unsafe.Pointer cast, while the resulting value is identical.
|
||||
if err = attrList.Update(
|
||||
windows.PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
|
||||
*(*unsafe.Pointer)(unsafe.Pointer(&hpc)),
|
||||
unsafe.Sizeof(hpc),
|
||||
); err != nil {
|
||||
return StartedPty{}, fmt.Errorf("UpdateProcThreadAttribute: %w", err)
|
||||
}
|
||||
|
||||
var si windows.StartupInfoEx
|
||||
si.Cb = uint32(unsafe.Sizeof(si))
|
||||
si.ProcThreadAttributeList = attrList.List()
|
||||
|
||||
var appNamePtr *uint16
|
||||
if cmd.Path != "" {
|
||||
if appNamePtr, err = windows.UTF16PtrFromString(cmd.Path); err != nil {
|
||||
return StartedPty{}, err
|
||||
}
|
||||
}
|
||||
cmdLinePtr, err := windows.UTF16PtrFromString(windows.ComposeCommandLine(cmd.Args))
|
||||
if err != nil {
|
||||
return StartedPty{}, err
|
||||
}
|
||||
var dirPtr *uint16
|
||||
if cmd.Dir != "" {
|
||||
if dirPtr, err = windows.UTF16PtrFromString(cmd.Dir); err != nil {
|
||||
return StartedPty{}, err
|
||||
}
|
||||
}
|
||||
envBlock, err := createEnvBlock(cmd.Env)
|
||||
if err != nil {
|
||||
return StartedPty{}, err
|
||||
}
|
||||
var envPtr *uint16
|
||||
if envBlock != nil {
|
||||
envPtr = &envBlock[0]
|
||||
}
|
||||
|
||||
var pi windows.ProcessInformation
|
||||
err = windows.CreateProcess(
|
||||
appNamePtr,
|
||||
cmdLinePtr,
|
||||
nil, // process security
|
||||
nil, // thread security
|
||||
false,
|
||||
windows.EXTENDED_STARTUPINFO_PRESENT|windows.CREATE_UNICODE_ENVIRONMENT,
|
||||
envPtr,
|
||||
dirPtr,
|
||||
&si.StartupInfo,
|
||||
&pi,
|
||||
)
|
||||
if err != nil {
|
||||
return StartedPty{}, fmt.Errorf("CreateProcess: %w", err)
|
||||
}
|
||||
_ = windows.CloseHandle(pi.Thread)
|
||||
|
||||
// Re-open the process by PID to get an *os.Process to wait on. Do this
|
||||
// while pi.Process is still open: Windows won't recycle a PID while any
|
||||
// handle to the process remains, so FindProcess can't latch onto a
|
||||
// different process that has since reused the PID. Release the original
|
||||
// handle once we have our own.
|
||||
proc, err := os.FindProcess(int(pi.ProcessId))
|
||||
_ = windows.CloseHandle(pi.Process)
|
||||
if err != nil {
|
||||
return StartedPty{}, err
|
||||
}
|
||||
|
||||
wp := &winPty{
|
||||
hpc: hpc,
|
||||
inWrite: os.NewFile(uintptr(inWrite), "conpty-in"),
|
||||
outRead: os.NewFile(uintptr(outRead), "conpty-out"),
|
||||
}
|
||||
return StartedPty{
|
||||
Pty: wp,
|
||||
Process: proc,
|
||||
Wait: startWaiter(proc, wp),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// createEnvBlock packs env vars into the UTF-16 double-null-terminated block
|
||||
// that CreateProcess expects. Returns nil if env is empty, which tells
|
||||
// CreateProcess to inherit the parent's environment.
|
||||
func createEnvBlock(env []string) ([]uint16, error) {
|
||||
if len(env) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var block []uint16
|
||||
for _, s := range env {
|
||||
utf16s, err := windows.UTF16FromString(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
block = append(block, utf16s...)
|
||||
}
|
||||
block = append(block, 0)
|
||||
return block, nil
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
"github.com/jesseduffield/generics/maps"
|
||||
"github.com/samber/lo"
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
|
|
@ -25,10 +26,14 @@ type fileInfo struct {
|
|||
mode PatchStatus
|
||||
includedLineIndices []int
|
||||
diff string
|
||||
// For a renamed file, the path it was renamed from; empty otherwise. We
|
||||
// need to keep hold of it so we can re-render the file's patch (which is
|
||||
// keyed by the new path) without the caller having to supply it again.
|
||||
previousPath string
|
||||
}
|
||||
|
||||
type (
|
||||
loadFileDiffFunc func(from string, to string, reverse bool, filename string, plain bool) (string, error)
|
||||
loadFileDiffFunc func(from string, to string, reverse bool, filename string, previousPath string, plain bool) (string, error)
|
||||
)
|
||||
|
||||
// PatchBuilder 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
|
||||
|
|
@ -46,6 +51,13 @@ type PatchBuilder struct {
|
|||
fileInfoMap map[string]*fileInfo
|
||||
Log *logrus.Entry
|
||||
|
||||
// mutex guards the fields that a git worker can mutate (via Reset, at the
|
||||
// end of a patch-consuming operation) while the UI thread reads them to
|
||||
// render — chiefly To and the fileInfoMap pointer. The map's *entries* are
|
||||
// only ever touched on the UI thread, so we only hold the lock long enough
|
||||
// to read or swap the fields, never across the git I/O in getFileInfo.
|
||||
mutex deadlock.Mutex
|
||||
|
||||
// loadFileDiff loads the diff of a file, for a given to (typically a commit hash)
|
||||
loadFileDiff loadFileDiffFunc
|
||||
}
|
||||
|
|
@ -58,6 +70,9 @@ func NewPatchBuilder(log *logrus.Entry, loadFileDiff loadFileDiffFunc) *PatchBui
|
|||
}
|
||||
|
||||
func (p *PatchBuilder) Start(from, to string, reverse bool, canRebase bool) {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
p.To = to
|
||||
p.From = from
|
||||
p.reverse = reverse
|
||||
|
|
@ -65,16 +80,28 @@ func (p *PatchBuilder) Start(from, to string, reverse bool, canRebase bool) {
|
|||
p.fileInfoMap = map[string]*fileInfo{}
|
||||
}
|
||||
|
||||
// snapshotFileInfoMap returns the current fileInfoMap under the lock. The map's
|
||||
// entries are only mutated on the UI thread, so callers can read the returned
|
||||
// map without holding the lock; the lock only serializes the pointer swap that
|
||||
// Reset/Start do (potentially from a git worker) against these reads.
|
||||
func (p *PatchBuilder) snapshotFileInfoMap() map[string]*fileInfo {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
return p.fileInfoMap
|
||||
}
|
||||
|
||||
func (p *PatchBuilder) PatchToApply(reverse bool, turnAddedFilesIntoDiffAgainstEmptyFile bool) string {
|
||||
var patch strings.Builder
|
||||
|
||||
for filename, info := range p.fileInfoMap {
|
||||
for filename, info := range p.snapshotFileInfoMap() {
|
||||
if info.mode == UNSELECTED {
|
||||
continue
|
||||
}
|
||||
|
||||
patch.WriteString(p.RenderPatchForFile(RenderPatchForFileOpts{
|
||||
Filename: filename,
|
||||
PreviousPath: info.previousPath,
|
||||
Plain: true,
|
||||
Reverse: reverse,
|
||||
TurnAddedFilesIntoDiffAgainstEmptyFile: turnAddedFilesIntoDiffAgainstEmptyFile,
|
||||
|
|
@ -102,8 +129,8 @@ func (p *PatchBuilder) removeFile(info *fileInfo) {
|
|||
info.includedLineIndices = nil
|
||||
}
|
||||
|
||||
func (p *PatchBuilder) AddFileWhole(filename string) error {
|
||||
info, err := p.getFileInfo(filename)
|
||||
func (p *PatchBuilder) AddFileWhole(filename string, previousPath string) error {
|
||||
info, err := p.getFileInfo(filename, previousPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -113,8 +140,8 @@ func (p *PatchBuilder) AddFileWhole(filename string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *PatchBuilder) RemoveFile(filename string) error {
|
||||
info, err := p.getFileInfo(filename)
|
||||
func (p *PatchBuilder) RemoveFile(filename string, previousPath string) error {
|
||||
info, err := p.getFileInfo(filename, previousPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -124,28 +151,34 @@ func (p *PatchBuilder) RemoveFile(filename string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *PatchBuilder) getFileInfo(filename string) (*fileInfo, error) {
|
||||
info, ok := p.fileInfoMap[filename]
|
||||
func (p *PatchBuilder) getFileInfo(filename string, previousPath string) (*fileInfo, error) {
|
||||
p.mutex.Lock()
|
||||
fileInfoMap := p.fileInfoMap
|
||||
from, to, reverse := p.From, p.To, p.reverse
|
||||
p.mutex.Unlock()
|
||||
|
||||
info, ok := fileInfoMap[filename]
|
||||
if ok {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
diff, err := p.loadFileDiff(p.From, p.To, p.reverse, filename, true)
|
||||
diff, err := p.loadFileDiff(from, to, reverse, filename, previousPath, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info = &fileInfo{
|
||||
mode: UNSELECTED,
|
||||
diff: diff,
|
||||
mode: UNSELECTED,
|
||||
diff: diff,
|
||||
previousPath: previousPath,
|
||||
}
|
||||
|
||||
p.fileInfoMap[filename] = info
|
||||
fileInfoMap[filename] = info
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (p *PatchBuilder) AddFileLineRange(filename string, lineIndices []int) error {
|
||||
info, err := p.getFileInfo(filename)
|
||||
func (p *PatchBuilder) AddFileLineRange(filename string, previousPath string, lineIndices []int) error {
|
||||
info, err := p.getFileInfo(filename, previousPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -155,8 +188,8 @@ func (p *PatchBuilder) AddFileLineRange(filename string, lineIndices []int) erro
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *PatchBuilder) RemoveFileLineRange(filename string, lineIndices []int) error {
|
||||
info, err := p.getFileInfo(filename)
|
||||
func (p *PatchBuilder) RemoveFileLineRange(filename string, previousPath string, lineIndices []int) error {
|
||||
info, err := p.getFileInfo(filename, previousPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -171,13 +204,14 @@ func (p *PatchBuilder) RemoveFileLineRange(filename string, lineIndices []int) e
|
|||
|
||||
type RenderPatchForFileOpts struct {
|
||||
Filename string
|
||||
PreviousPath string
|
||||
Plain bool
|
||||
Reverse bool
|
||||
TurnAddedFilesIntoDiffAgainstEmptyFile bool
|
||||
}
|
||||
|
||||
func (p *PatchBuilder) RenderPatchForFile(opts RenderPatchForFileOpts) string {
|
||||
info, err := p.getFileInfo(opts.Filename)
|
||||
info, err := p.getFileInfo(opts.Filename, opts.PreviousPath)
|
||||
if err != nil {
|
||||
p.Log.Error(err)
|
||||
return ""
|
||||
|
|
@ -198,7 +232,12 @@ func (p *PatchBuilder) RenderPatchForFile(opts RenderPatchForFileOpts) string {
|
|||
Transform(TransformOpts{
|
||||
Reverse: opts.Reverse,
|
||||
TurnAddedFilesIntoDiffAgainstEmptyFile: opts.TurnAddedFilesIntoDiffAgainstEmptyFile,
|
||||
IncludedLineIndices: info.includedLineIndices,
|
||||
// For a partial selection of a renamed file we keep only the
|
||||
// content change and drop the rename, so that the rename stays in
|
||||
// the commit. A whole-file selection keeps the rename (and short-
|
||||
// circuits before this for plain output).
|
||||
StripRename: info.mode == PART && info.previousPath != "",
|
||||
IncludedLineIndices: info.includedLineIndices,
|
||||
})
|
||||
|
||||
if opts.Plain {
|
||||
|
|
@ -208,13 +247,16 @@ func (p *PatchBuilder) RenderPatchForFile(opts RenderPatchForFileOpts) string {
|
|||
}
|
||||
|
||||
func (p *PatchBuilder) renderEachFilePatch(plain bool) []string {
|
||||
fileInfoMap := p.snapshotFileInfoMap()
|
||||
|
||||
// sort files by name then iterate through and render each patch
|
||||
filenames := maps.Keys(p.fileInfoMap)
|
||||
filenames := maps.Keys(fileInfoMap)
|
||||
|
||||
sort.Strings(filenames)
|
||||
patches := lo.Map(filenames, func(filename string, _ int) string {
|
||||
return p.RenderPatchForFile(RenderPatchForFileOpts{
|
||||
Filename: filename,
|
||||
PreviousPath: fileInfoMap[filename].previousPath,
|
||||
Plain: plain,
|
||||
Reverse: false,
|
||||
TurnAddedFilesIntoDiffAgainstEmptyFile: true,
|
||||
|
|
@ -232,11 +274,16 @@ func (p *PatchBuilder) RenderAggregatedPatch(plain bool) string {
|
|||
}
|
||||
|
||||
func (p *PatchBuilder) GetFileStatus(filename string, parent string) PatchStatus {
|
||||
if parent != p.To {
|
||||
p.mutex.Lock()
|
||||
to := p.To
|
||||
fileInfoMap := p.fileInfoMap
|
||||
p.mutex.Unlock()
|
||||
|
||||
if parent != to {
|
||||
return UNSELECTED
|
||||
}
|
||||
|
||||
info, ok := p.fileInfoMap[filename]
|
||||
info, ok := fileInfoMap[filename]
|
||||
if !ok {
|
||||
return UNSELECTED
|
||||
}
|
||||
|
|
@ -244,8 +291,8 @@ func (p *PatchBuilder) GetFileStatus(filename string, parent string) PatchStatus
|
|||
return info.mode
|
||||
}
|
||||
|
||||
func (p *PatchBuilder) GetFileIncLineIndices(filename string) ([]int, error) {
|
||||
info, err := p.getFileInfo(filename)
|
||||
func (p *PatchBuilder) GetFileIncLineIndices(filename string, previousPath string) ([]int, error) {
|
||||
info, err := p.getFileInfo(filename, previousPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -254,16 +301,22 @@ func (p *PatchBuilder) GetFileIncLineIndices(filename string) ([]int, error) {
|
|||
|
||||
// clears the patch
|
||||
func (p *PatchBuilder) Reset() {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
p.To = ""
|
||||
p.fileInfoMap = map[string]*fileInfo{}
|
||||
}
|
||||
|
||||
func (p *PatchBuilder) Active() bool {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
return p.To != ""
|
||||
}
|
||||
|
||||
func (p *PatchBuilder) IsEmpty() bool {
|
||||
for _, fileInfo := range p.fileInfoMap {
|
||||
for _, fileInfo := range p.snapshotFileInfoMap() {
|
||||
if fileInfo.mode == WHOLE || (fileInfo.mode == PART && len(fileInfo.includedLineIndices) > 0) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -274,9 +327,12 @@ func (p *PatchBuilder) IsEmpty() bool {
|
|||
|
||||
// if any of these things change we'll need to reset and start a new patch
|
||||
func (p *PatchBuilder) NewPatchRequired(from string, to string, reverse bool) bool {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
return from != p.From || to != p.To || reverse != p.reverse
|
||||
}
|
||||
|
||||
func (p *PatchBuilder) AllFilesInPatch() []string {
|
||||
return lo.Keys(p.fileInfoMap)
|
||||
return lo.Keys(p.snapshotFileInfoMap())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,22 @@ index dcd3485..1ba5540 100644
|
|||
...
|
||||
`
|
||||
|
||||
const renameWithModificationDiff = `diff --git a/oldname b/newname
|
||||
similarity index 62%
|
||||
rename from oldname
|
||||
rename to newname
|
||||
index dcd3485..1ba5540 100644
|
||||
--- a/oldname
|
||||
+++ b/newname
|
||||
@@ -1,5 +1,5 @@
|
||||
apple
|
||||
-orange
|
||||
+grape
|
||||
...
|
||||
...
|
||||
...
|
||||
`
|
||||
|
||||
const addNewlineToEndOfFile = `diff --git a/filename b/filename
|
||||
index 80a73f1..e48a11c 100644
|
||||
--- a/filename
|
||||
|
|
@ -152,6 +168,7 @@ func TestTransform(t *testing.T) {
|
|||
firstLineIndex int
|
||||
lastLineIndex int
|
||||
reverse bool
|
||||
stripRename bool
|
||||
expected string
|
||||
}
|
||||
|
||||
|
|
@ -515,6 +532,43 @@ func TestTransform(t *testing.T) {
|
|||
orange
|
||||
banana
|
||||
lemon
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "renamed file, whole change selected, strips the rename so only the content change is applied",
|
||||
firstLineIndex: 9,
|
||||
lastLineIndex: 10,
|
||||
stripRename: true,
|
||||
diffText: renameWithModificationDiff,
|
||||
expected: `diff --git a/newname b/newname
|
||||
index dcd3485..1ba5540 100644
|
||||
--- a/newname
|
||||
+++ b/newname
|
||||
@@ -1,5 +1,5 @@
|
||||
apple
|
||||
-orange
|
||||
+grape
|
||||
...
|
||||
...
|
||||
...
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "renamed file, only removal selected, strips the rename",
|
||||
firstLineIndex: 9,
|
||||
lastLineIndex: 9,
|
||||
stripRename: true,
|
||||
diffText: renameWithModificationDiff,
|
||||
expected: `diff --git a/newname b/newname
|
||||
index dcd3485..1ba5540 100644
|
||||
--- a/newname
|
||||
+++ b/newname
|
||||
@@ -1,5 +1,4 @@
|
||||
apple
|
||||
-orange
|
||||
...
|
||||
...
|
||||
...
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
|
@ -527,6 +581,7 @@ func TestTransform(t *testing.T) {
|
|||
Transform(TransformOpts{
|
||||
Reverse: s.reverse,
|
||||
FileNameOverride: s.filename,
|
||||
StripRename: s.stripRename,
|
||||
IncludedLineIndices: lineIndices,
|
||||
}).
|
||||
FormatPlain()
|
||||
|
|
|
|||
|
|
@ -33,6 +33,13 @@ type TransformOpts struct {
|
|||
// treat it as a diff against an empty file.
|
||||
TurnAddedFilesIntoDiffAgainstEmptyFile bool
|
||||
|
||||
// When building a partial patch for a renamed file, strip the rename
|
||||
// metadata from the header and point it at the new path. Applying the
|
||||
// resulting patch then only changes the file's contents and leaves the
|
||||
// rename itself in place. (For a whole-file selection we keep the rename
|
||||
// so that it moves or is discarded together with the contents.)
|
||||
StripRename bool
|
||||
|
||||
// The indices of lines that should be included in the patch.
|
||||
IncludedLineIndices []int
|
||||
}
|
||||
|
|
@ -72,21 +79,61 @@ func (self *patchTransformer) transformHeader() []string {
|
|||
"--- a/" + self.opts.FileNameOverride,
|
||||
"+++ b/" + self.opts.FileNameOverride,
|
||||
}
|
||||
} else if self.opts.TurnAddedFilesIntoDiffAgainstEmptyFile {
|
||||
result := make([]string, 0, len(self.patch.header))
|
||||
for idx, line := range self.patch.header {
|
||||
}
|
||||
|
||||
header := self.patch.header
|
||||
if self.opts.StripRename {
|
||||
header = stripRenameFromHeader(header)
|
||||
}
|
||||
|
||||
if self.opts.TurnAddedFilesIntoDiffAgainstEmptyFile {
|
||||
result := make([]string, 0, len(header))
|
||||
for idx, line := range header {
|
||||
if strings.HasPrefix(line, "new file mode") {
|
||||
continue
|
||||
}
|
||||
if line == "--- /dev/null" && strings.HasPrefix(self.patch.header[idx+1], "+++ b/") {
|
||||
line = "--- a/" + self.patch.header[idx+1][6:]
|
||||
if line == "--- /dev/null" && strings.HasPrefix(header[idx+1], "+++ b/") {
|
||||
line = "--- a/" + header[idx+1][6:]
|
||||
}
|
||||
result = append(result, line)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
return self.patch.header
|
||||
return header
|
||||
}
|
||||
|
||||
// stripRenameFromHeader rewrites a rename diff header so that it looks like a
|
||||
// plain modification of the new path: it drops the rename metadata and points
|
||||
// the diff at the new path on both sides, while keeping the blob index line so
|
||||
// that `git apply --3way` can still fall back to a blob merge. See the
|
||||
// StripRename option for why we do this.
|
||||
func stripRenameFromHeader(header []string) []string {
|
||||
newPath := ""
|
||||
for _, line := range header {
|
||||
if path, ok := strings.CutPrefix(line, "+++ b/"); ok {
|
||||
newPath = path
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]string, 0, len(header))
|
||||
for _, line := range header {
|
||||
switch {
|
||||
case strings.HasPrefix(line, "similarity index "),
|
||||
strings.HasPrefix(line, "dissimilarity index "),
|
||||
strings.HasPrefix(line, "rename from "),
|
||||
strings.HasPrefix(line, "rename to "):
|
||||
// drop the rename metadata
|
||||
case strings.HasPrefix(line, "diff --git "):
|
||||
result = append(result, "diff --git a/"+newPath+" b/"+newPath)
|
||||
case strings.HasPrefix(line, "--- "):
|
||||
result = append(result, "--- a/"+newPath)
|
||||
default:
|
||||
result = append(result, line)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (self *patchTransformer) transformHunks() []*Hunk {
|
||||
|
|
|
|||
|
|
@ -136,8 +136,23 @@ func findOrCreateConfigDir() (string, error) {
|
|||
return folder, os.MkdirAll(folder, 0o755)
|
||||
}
|
||||
|
||||
// KeybindingPlatform returns the platform whose default keybindings should be
|
||||
// used. Normally this is the OS we're running on, but it can be overridden with
|
||||
// the LAZYGIT_KEYBINDING_PLATFORM environment variable; this is useful e.g. when
|
||||
// running lazygit in a Linux container that you access over ssh from a Mac, and
|
||||
// you'd rather use the Mac keybindings. An unrecognized value falls back to the
|
||||
// real OS, which gives meaningful bindings, rather than to the (arbitrary)
|
||||
// non-darwin defaults.
|
||||
func KeybindingPlatform() string {
|
||||
platform := os.Getenv("LAZYGIT_KEYBINDING_PLATFORM")
|
||||
if lo.Contains([]string{"darwin", "linux", "windows"}, platform) {
|
||||
return platform
|
||||
}
|
||||
return runtime.GOOS
|
||||
}
|
||||
|
||||
func loadUserConfigWithDefaults(configFiles []*ConfigFile, isGuiInitialized bool) (*UserConfig, error) {
|
||||
return loadUserConfig(configFiles, GetDefaultConfigForPlatform(runtime.GOOS), isGuiInitialized)
|
||||
return loadUserConfig(configFiles, GetDefaultConfigForPlatform(KeybindingPlatform()), isGuiInitialized)
|
||||
}
|
||||
|
||||
func loadUserConfig(configFiles []*ConfigFile, base *UserConfig, isGuiInitialized bool) (*UserConfig, error) {
|
||||
|
|
@ -287,6 +302,26 @@ func computeMigratedConfig(path string, content []byte, changes *ChangesSet) ([]
|
|||
}
|
||||
}
|
||||
|
||||
pathsToMove := []struct {
|
||||
oldPath []string
|
||||
newPath []string
|
||||
}{
|
||||
{
|
||||
[]string{"keybinding", "worktrees", "viewWorktreeOptions"},
|
||||
[]string{"keybinding", "universal", "newWorktree"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, pathToMove := range pathsToMove {
|
||||
err, didMove := yaml_utils.MoveYamlKey(&rootNode, pathToMove.oldPath, pathToMove.newPath)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("Couldn't migrate config file at `%s` for key %s: %w", path, strings.Join(pathToMove.oldPath, "."), err)
|
||||
}
|
||||
if didMove {
|
||||
changes.Add(fmt.Sprintf("Moved '%s' to '%s'", strings.Join(pathToMove.oldPath, "."), strings.Join(pathToMove.newPath, ".")))
|
||||
}
|
||||
}
|
||||
|
||||
err = changeNullKeybindingsToDisabled(&rootNode, changes)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("Couldn't migrate config file at `%s`: %w", path, err)
|
||||
|
|
@ -449,7 +484,8 @@ func migrateAllBranchesLogCmd(rootNode *yaml.Node, changes *ChangesSet) error {
|
|||
// We will later populate it with the individual allBranchesLogCmd record
|
||||
cmdsKeyNode = &yaml.Node{Kind: yaml.ScalarNode, Value: "allBranchesLogCmds"}
|
||||
cmdsValueNode = &yaml.Node{Kind: yaml.SequenceNode, Content: []*yaml.Node{}}
|
||||
gitNode.Content = append(gitNode.Content,
|
||||
gitNode.Content = append(
|
||||
gitNode.Content,
|
||||
cmdsKeyNode,
|
||||
cmdsValueNode,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,53 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestKeybindingPlatform(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
envValue string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Not set falls back to the host OS",
|
||||
envValue: "",
|
||||
expected: runtime.GOOS,
|
||||
},
|
||||
{
|
||||
name: "darwin is honored",
|
||||
envValue: "darwin",
|
||||
expected: "darwin",
|
||||
},
|
||||
{
|
||||
name: "linux is honored",
|
||||
envValue: "linux",
|
||||
expected: "linux",
|
||||
},
|
||||
{
|
||||
name: "windows is honored",
|
||||
envValue: "windows",
|
||||
expected: "windows",
|
||||
},
|
||||
{
|
||||
name: "An unrecognized value falls back to the host OS",
|
||||
envValue: "mac",
|
||||
expected: runtime.GOOS,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
t.Setenv("LAZYGIT_KEYBINDING_PLATFORM", s.envValue)
|
||||
assert.Equal(t, s.expected, KeybindingPlatform())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrationOfRenamedKeys(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
|
|
@ -78,6 +120,73 @@ keybinding:
|
|||
}
|
||||
}
|
||||
|
||||
func TestMigrationOfMovedKeys(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
expectedDidChange bool
|
||||
expectedChanges []string
|
||||
}{
|
||||
{
|
||||
name: "Empty String",
|
||||
input: "",
|
||||
expectedDidChange: false,
|
||||
expectedChanges: []string{},
|
||||
},
|
||||
{
|
||||
name: "No move needed",
|
||||
input: `foo:
|
||||
bar: 5
|
||||
`,
|
||||
expectedDidChange: false,
|
||||
expectedChanges: []string{},
|
||||
},
|
||||
{
|
||||
name: "Move worktree keybinding into the universal section",
|
||||
input: `keybinding:
|
||||
universal:
|
||||
quit: q
|
||||
worktrees:
|
||||
viewWorktreeOptions: w
|
||||
`,
|
||||
expected: `keybinding:
|
||||
universal:
|
||||
quit: q
|
||||
newWorktree: w
|
||||
`,
|
||||
expectedDidChange: true,
|
||||
expectedChanges: []string{"Moved 'keybinding.worktrees.viewWorktreeOptions' to 'keybinding.universal.newWorktree'"},
|
||||
},
|
||||
{
|
||||
name: "Create the universal section if it doesn't exist",
|
||||
input: `keybinding:
|
||||
worktrees:
|
||||
viewWorktreeOptions: w
|
||||
`,
|
||||
expected: `keybinding:
|
||||
universal:
|
||||
newWorktree: w
|
||||
`,
|
||||
expectedDidChange: true,
|
||||
expectedChanges: []string{"Moved 'keybinding.worktrees.viewWorktreeOptions' to 'keybinding.universal.newWorktree'"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
changes := NewChangesSet()
|
||||
actual, didChange, err := computeMigratedConfig("path doesn't matter", []byte(s.input), changes)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, s.expectedDidChange, didChange)
|
||||
if didChange {
|
||||
assert.Equal(t, s.expected, string(actual))
|
||||
}
|
||||
assert.Equal(t, s.expectedChanges, changes.ToSliceFromOldest())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateNullKeybindingsToDisabled(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ type UserConfig struct {
|
|||
Gui GuiConfig `yaml:"gui"`
|
||||
// Config relating to git
|
||||
Git GitConfig `yaml:"git"`
|
||||
// Config relating to git worktrees
|
||||
Worktree WorktreeConfig `yaml:"worktree"`
|
||||
// Periodic update checks
|
||||
Update UpdateConfig `yaml:"update"`
|
||||
// Background refreshes
|
||||
|
|
@ -109,6 +111,8 @@ type GuiConfig struct {
|
|||
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
|
||||
// The weight of the expanded side panel, relative to the other panels. 2 means twice as tall as the other panels. Only relevant if `expandFocusedSidePanel` is true.
|
||||
ExpandedSidePanelWeight int `yaml:"expandedSidePanelWeight"`
|
||||
// If true, don't give a side panel more height than it needs to show its content; when all panels fit, the leftover height is shared among them so that they still fill the screen.
|
||||
ShrinkSidePanelsToContent bool `yaml:"shrinkSidePanelsToContent"`
|
||||
// The side panels, in the order they appear from top to bottom.
|
||||
// Each entry is a list of one or more names that share a single panel as tabs (cycle through them with the next-tab/previous-tab keys).
|
||||
// Omit a name to hide it; give a name its own one-element list to promote a tab to a top-level panel.
|
||||
|
|
@ -422,6 +426,13 @@ type CommitPrefixConfig struct {
|
|||
Replace string `yaml:"replace" jsonschema:"example=[$1]"`
|
||||
}
|
||||
|
||||
type WorktreeConfig struct {
|
||||
// Default parent directory for new worktrees. It is offered as a candidate location alongside the parent directories of any worktrees you already have.
|
||||
// A relative path is resolved against the repository's root directory, so "../worktrees" sits beside the repo and ".worktrees" sits inside it.
|
||||
// A leading "~" is expanded to your home directory, so "~/worktrees" works.
|
||||
DefaultPath string `yaml:"defaultPath"`
|
||||
}
|
||||
|
||||
type UpdateConfig struct {
|
||||
// One of: 'prompt' (default) | 'background' | 'never'
|
||||
Method string `yaml:"method" jsonschema:"enum=prompt,enum=background,enum=never"`
|
||||
|
|
@ -434,7 +445,6 @@ type KeybindingConfig struct {
|
|||
Status KeybindingStatusConfig `yaml:"status"`
|
||||
Files KeybindingFilesConfig `yaml:"files"`
|
||||
Branches KeybindingBranchesConfig `yaml:"branches"`
|
||||
Worktrees KeybindingWorktreesConfig `yaml:"worktrees"`
|
||||
Commits KeybindingCommitsConfig `yaml:"commits"`
|
||||
AmendAttribute KeybindingAmendAttributeConfig `yaml:"amendAttribute"`
|
||||
Stash KeybindingStashConfig `yaml:"stash"`
|
||||
|
|
@ -502,6 +512,7 @@ type KeybindingUniversalConfig struct {
|
|||
ConfirmInEditorAlt Keybinding `yaml:"confirmInEditor-alt"`
|
||||
Remove Keybinding `yaml:"remove"`
|
||||
New Keybinding `yaml:"new"`
|
||||
NewWorktree Keybinding `yaml:"newWorktree"`
|
||||
Edit Keybinding `yaml:"edit"`
|
||||
OpenFile Keybinding `yaml:"openFile"`
|
||||
ScrollUpMain Keybinding `yaml:"scrollUpMain"`
|
||||
|
|
@ -596,10 +607,6 @@ type KeybindingBranchesConfig struct {
|
|||
SortOrder Keybinding `yaml:"sortOrder"`
|
||||
}
|
||||
|
||||
type KeybindingWorktreesConfig struct {
|
||||
ViewWorktreeOptions Keybinding `yaml:"viewWorktreeOptions"`
|
||||
}
|
||||
|
||||
type KeybindingCommitsConfig struct {
|
||||
SquashDown Keybinding `yaml:"squashDown"`
|
||||
RenameCommit Keybinding `yaml:"renameCommit"`
|
||||
|
|
@ -842,18 +849,19 @@ func GetDefaultConfig() *UserConfig {
|
|||
func GetDefaultConfigForPlatform(platform string) *UserConfig {
|
||||
return &UserConfig{
|
||||
Gui: GuiConfig{
|
||||
ScrollHeight: 2,
|
||||
ScrollPastBottom: true,
|
||||
ScrollOffMargin: 2,
|
||||
ScrollOffBehavior: "margin",
|
||||
TabWidth: 4,
|
||||
MouseEvents: true,
|
||||
SkipAmendWarning: false,
|
||||
SkipDiscardChangeWarning: false,
|
||||
SkipStashWarning: false,
|
||||
SidePanelWidth: 0.3333,
|
||||
ExpandFocusedSidePanel: false,
|
||||
ExpandedSidePanelWeight: 2,
|
||||
ScrollHeight: 2,
|
||||
ScrollPastBottom: true,
|
||||
ScrollOffMargin: 2,
|
||||
ScrollOffBehavior: "margin",
|
||||
TabWidth: 4,
|
||||
MouseEvents: true,
|
||||
SkipAmendWarning: false,
|
||||
SkipDiscardChangeWarning: false,
|
||||
SkipStashWarning: false,
|
||||
SidePanelWidth: 0.3333,
|
||||
ExpandFocusedSidePanel: false,
|
||||
ExpandedSidePanelWeight: 2,
|
||||
ShrinkSidePanelsToContent: false,
|
||||
SidePanels: []SidePanel{
|
||||
{"status"},
|
||||
{"files", "worktrees", "submodules"},
|
||||
|
|
@ -959,6 +967,9 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig {
|
|||
ParseEmoji: false,
|
||||
TruncateCopiedCommitHashesTo: 12,
|
||||
},
|
||||
Worktree: WorktreeConfig{
|
||||
DefaultPath: "",
|
||||
},
|
||||
Refresher: RefresherConfig{
|
||||
RefreshInterval: 10,
|
||||
FetchInterval: 60,
|
||||
|
|
@ -1024,6 +1035,7 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig {
|
|||
ConfirmInEditorAlt: Keybinding{"<ctrl+s>"},
|
||||
Remove: Keybinding{"d"},
|
||||
New: Keybinding{"n"},
|
||||
NewWorktree: Keybinding{"w"},
|
||||
Edit: Keybinding{"e"},
|
||||
OpenFile: Keybinding{"o"},
|
||||
OpenRecentRepos: Keybinding{"<ctrl+r>"},
|
||||
|
|
@ -1109,9 +1121,6 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig {
|
|||
AddForkRemote: Keybinding{"F"},
|
||||
SortOrder: Keybinding{"s"},
|
||||
},
|
||||
Worktrees: KeybindingWorktreesConfig{
|
||||
ViewWorktreeOptions: Keybinding{"w"},
|
||||
},
|
||||
Commits: KeybindingCommitsConfig{
|
||||
SquashDown: Keybinding{"s"},
|
||||
RenameCommit: Keybinding{"r"},
|
||||
|
|
|
|||
98
pkg/gocui/block_events_test.go
Normal file
98
pkg/gocui/block_events_test.go
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
package gocui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEventWithheldWhileBlocking(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
event GocuiEvent
|
||||
withheld bool
|
||||
}{
|
||||
{"key", GocuiEvent{Type: eventKey, Key: NewKeyRune('x')}, true},
|
||||
{"mouse click", GocuiEvent{Type: eventMouse, Key: NewKeyName(MouseLeft)}, true},
|
||||
{"mouse scroll", GocuiEvent{Type: eventMouse, Key: NewKeyName(MouseWheelDown)}, false},
|
||||
{"mouse move", GocuiEvent{Type: eventMouseMove}, true},
|
||||
{"resize", GocuiEvent{Type: eventResize}, false},
|
||||
{"focus", GocuiEvent{Type: eventFocus}, false},
|
||||
{"paste", GocuiEvent{Type: eventPaste}, false},
|
||||
{"error", GocuiEvent{Type: eventError}, false},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
assert.Equal(t, s.withheld, eventWithheldWhileBlocking(&s.event))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// setupKeyRecorder wires a keybinding on a focused view that records each time
|
||||
// it fires, and returns the key event that triggers it plus the record slice.
|
||||
func setupKeyRecorder(t *testing.T, g *Gui) (GocuiEvent, *[]int) {
|
||||
t.Helper()
|
||||
|
||||
_, _ = g.SetView("main", 0, 0, 80, 22, 0)
|
||||
_, err := g.SetCurrentView("main")
|
||||
assert.NoError(t, err)
|
||||
|
||||
fired := []int{}
|
||||
callCount := 0
|
||||
key := NewKeyRune('x')
|
||||
g.SetKeybinding("main", key, func(*Gui, *View) error {
|
||||
callCount++
|
||||
fired = append(fired, callCount)
|
||||
return nil
|
||||
})
|
||||
|
||||
return GocuiEvent{Type: eventKey, Key: key}, &fired
|
||||
}
|
||||
|
||||
func TestBlockingEvents_KeysBufferedAndReplayed(t *testing.T) {
|
||||
g := newTestGui(t)
|
||||
keyEvent, fired := setupKeyRecorder(t, g)
|
||||
|
||||
// Not blocking: the key dispatches immediately.
|
||||
assert.NoError(t, g.handleEvent(&keyEvent))
|
||||
assert.Len(t, *fired, 1)
|
||||
|
||||
// While blocking: the key is buffered, not dispatched.
|
||||
g.BeginBlockingEvents()
|
||||
assert.NoError(t, g.handleEvent(&keyEvent))
|
||||
assert.NoError(t, g.handleEvent(&keyEvent))
|
||||
assert.Len(t, *fired, 1, "buffered keys must not dispatch while blocking")
|
||||
|
||||
// Unblocking replays the buffered keys.
|
||||
assert.NoError(t, g.EndBlockingEvents())
|
||||
assert.Len(t, *fired, 3, "both buffered keys should replay on unblock")
|
||||
assert.Empty(t, g.bufferedKeyEvents)
|
||||
}
|
||||
|
||||
func TestBlockingEvents_NestsWithCounter(t *testing.T) {
|
||||
g := newTestGui(t)
|
||||
keyEvent, fired := setupKeyRecorder(t, g)
|
||||
|
||||
g.BeginBlockingEvents()
|
||||
g.BeginBlockingEvents()
|
||||
assert.NoError(t, g.handleEvent(&keyEvent))
|
||||
|
||||
// The inner block ending still leaves us blocked: no replay yet.
|
||||
assert.NoError(t, g.EndBlockingEvents())
|
||||
assert.Empty(t, *fired)
|
||||
|
||||
// Only the outermost block ending replays.
|
||||
assert.NoError(t, g.EndBlockingEvents())
|
||||
assert.Len(t, *fired, 1)
|
||||
}
|
||||
|
||||
func TestBlockingEvents_MouseClicksDroppedNotBuffered(t *testing.T) {
|
||||
g := newTestGui(t)
|
||||
|
||||
g.BeginBlockingEvents()
|
||||
click := GocuiEvent{Type: eventMouse, Key: NewKeyName(MouseLeft)}
|
||||
assert.NoError(t, g.handleEvent(&click))
|
||||
assert.Empty(t, g.bufferedKeyEvents, "mouse clicks must be dropped, not buffered")
|
||||
assert.NoError(t, g.EndBlockingEvents())
|
||||
}
|
||||
|
|
@ -19,6 +19,29 @@ type escapeInterpreter struct {
|
|||
mode OutputMode
|
||||
instruction instruction
|
||||
hyperlink strings.Builder
|
||||
|
||||
// ConPTY emits cursor-positioning escapes (CUP) to skip over blank
|
||||
// rows rather than emitting LFs for them. To convert those into row
|
||||
// advances the view can act on, we track where in the pseudo-terminal
|
||||
// screen the cursor currently is. 1-based to match the escape
|
||||
// sequences.
|
||||
//
|
||||
// We also have to track the column, but only well enough to count
|
||||
// soft-wraps when written content runs past the right edge: ConPTY's
|
||||
// CUPs are addressed against its post-wrap screen, so a logical line
|
||||
// long enough to wrap in ConPTY's screen counts for two rows from the
|
||||
// next CUP's perspective. Column accuracy past wrap-counting isn't
|
||||
// modelled — we don't track the col argument of CUPs, and most
|
||||
// pager-style emitters use col 1 anyway.
|
||||
screenRow, screenCol int
|
||||
|
||||
// The screen width that soft-wraps are counted against (see
|
||||
// notifyCellsWritten). It's a snapshot of the view's InnerWidth taken on
|
||||
// the UI thread (in NewView, and refreshed per render via
|
||||
// View.SetContentWidth), rather than read live from the view's dimensions:
|
||||
// a view's output is written from a task goroutine, and reading the live
|
||||
// dimensions there would race the UI thread updating them during layout.
|
||||
screenColMax int
|
||||
}
|
||||
|
||||
type (
|
||||
|
|
@ -32,6 +55,21 @@ type eraseInLineFromCursor struct{}
|
|||
|
||||
func (self eraseInLineFromCursor) isInstruction() {}
|
||||
|
||||
// cursorDown asks the view to advance N rows. Emitted when CUP / CUD /
|
||||
// CNL / VPA targets a row past the current one; backward moves are
|
||||
// ignored because the view's buffer is line-based and can't undo.
|
||||
type cursorDown struct{ n int }
|
||||
|
||||
func (self cursorDown) isInstruction() {}
|
||||
|
||||
// cursorForward asks the view to materialize N space cells. Emitted
|
||||
// when CUF advances the cursor right — ConPTY uses CUF (often paired
|
||||
// with ECH) to encode runs of default-colored spaces compactly, so we
|
||||
// have to render the gap, not just bump a counter.
|
||||
type cursorForward struct{ n int }
|
||||
|
||||
func (self cursorForward) isInstruction() {}
|
||||
|
||||
type noInstruction struct{}
|
||||
|
||||
func (self noInstruction) isInstruction() {}
|
||||
|
|
@ -99,11 +137,15 @@ func newEscapeInterpreter(mode OutputMode) *escapeInterpreter {
|
|||
curBgColor: ColorDefault,
|
||||
mode: mode,
|
||||
instruction: noInstruction{},
|
||||
screenRow: 1,
|
||||
screenCol: 1,
|
||||
}
|
||||
return ei
|
||||
}
|
||||
|
||||
// reset sets the escapeInterpreter in initial state.
|
||||
// reset sets the escapeInterpreter in initial state. Note: this only resets
|
||||
// escape-parsing state. Screen cursor state survives so that mid-stream
|
||||
// malformed escapes don't desync the row tracking from the view.
|
||||
func (ei *escapeInterpreter) reset() {
|
||||
ei.state = stateNone
|
||||
ei.curFgColor = ColorDefault
|
||||
|
|
@ -111,6 +153,80 @@ func (ei *escapeInterpreter) reset() {
|
|||
ei.csiParam = nil
|
||||
}
|
||||
|
||||
// resetScreenCursor returns the screen-cursor tracking to the top of the
|
||||
// pseudo-terminal screen. Called when the view is rewound before a fresh pty
|
||||
// render, and on cursor-home (which ConPTY emits at the start of each screen)
|
||||
// for views that aren't rewound in lockstep — see the CUP handling in parseOne.
|
||||
func (ei *escapeInterpreter) resetScreenCursor() {
|
||||
ei.screenRow = 1
|
||||
ei.screenCol = 1
|
||||
}
|
||||
|
||||
// notifyRowAdvance must be called by the view whenever it advances to the
|
||||
// next row in response to an LF / CRLF outside of an escape sequence
|
||||
// (i.e. the row transitions the parser doesn't see directly). Keeps the
|
||||
// parser's notion of the current screen row in sync with the view.
|
||||
func (ei *escapeInterpreter) notifyRowAdvance() {
|
||||
ei.screenRow++
|
||||
ei.screenCol = 1
|
||||
}
|
||||
|
||||
// notifyColumnReset must be called when the view processes a bare CR
|
||||
// (column reset without row advance). Keeps screenCol in sync so wrap
|
||||
// counting starts over from col 1.
|
||||
func (ei *escapeInterpreter) notifyColumnReset() {
|
||||
ei.screenCol = 1
|
||||
}
|
||||
|
||||
// notifyCellsWritten must be called after the view writes visible cells
|
||||
// to its buffer. Advances the parser's idea of the cursor by `width`
|
||||
// columns; if that crosses the right edge of a `screenColMax`-wide pty
|
||||
// screen, the corresponding number of soft-wraps are added to screenRow
|
||||
// so subsequent CUPs land on the right line.
|
||||
func (ei *escapeInterpreter) notifyCellsWritten(width int) {
|
||||
if ei.screenColMax <= 0 {
|
||||
return
|
||||
}
|
||||
// One column at a time: matches ConPTY's "pending wrap" semantics
|
||||
// where the cursor stays at col max+1 after writing the rightmost
|
||||
// cell and only wraps on the next cell. Loops over individual
|
||||
// columns rather than doing the math in one shot so wide cells on a
|
||||
// row boundary still wrap cleanly.
|
||||
for range width {
|
||||
if ei.screenCol > ei.screenColMax {
|
||||
ei.screenRow++
|
||||
ei.screenCol = 1
|
||||
}
|
||||
ei.screenCol++
|
||||
}
|
||||
}
|
||||
|
||||
// emitCursorAdvance schedules a cursorDown instruction for the next time
|
||||
// the view checks ei.instruction, advancing the parser's screen row by
|
||||
// the same amount. n <= 0 is a no-op (backward / same-row CUPs are
|
||||
// ignored — the view's buffer is line-based and can't undo).
|
||||
func (ei *escapeInterpreter) emitCursorAdvance(n int) {
|
||||
if n <= 0 {
|
||||
return
|
||||
}
|
||||
ei.instruction = cursorDown{n: n}
|
||||
ei.screenRow += n
|
||||
ei.screenCol = 1
|
||||
}
|
||||
|
||||
// firstParamOrDefault returns the first CSI parameter parsed as an int,
|
||||
// or dflt if it's absent / empty / unparseable.
|
||||
func (ei *escapeInterpreter) firstParamOrDefault(dflt int) int {
|
||||
if len(ei.csiParam) == 0 || ei.csiParam[0] == "" {
|
||||
return dflt
|
||||
}
|
||||
n, err := strconv.Atoi(ei.csiParam[0])
|
||||
if err != nil {
|
||||
return dflt
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (ei *escapeInterpreter) instructionRead() {
|
||||
ei.instruction = noInstruction{}
|
||||
}
|
||||
|
|
@ -170,8 +286,13 @@ func (ei *escapeInterpreter) parseOne(ch []byte) (isEscape bool, err error) {
|
|||
ei.csiParam = append(ei.csiParam, "")
|
||||
case characterEquals(ch, 'm'):
|
||||
ei.csiParam = append(ei.csiParam, "0")
|
||||
case characterEquals(ch, 'K'):
|
||||
// fall through
|
||||
case characterEquals(ch, 'K'),
|
||||
characterEquals(ch, 'H'), characterEquals(ch, 'f'), characterEquals(ch, 'd'),
|
||||
characterEquals(ch, 'B'), characterEquals(ch, 'E'),
|
||||
characterEquals(ch, 'C'):
|
||||
// fall through — let stateParams handle these with default
|
||||
// params (CUP/VPA default to row 1, CUD/CNL/CUF default to
|
||||
// advance by 1).
|
||||
case characterEquals(ch, ';'):
|
||||
// Empty first param ([;Xm ≡ [0;Xm). Seed a slot for the
|
||||
// empty param; stateParams will append the next one when it
|
||||
|
|
@ -240,6 +361,43 @@ func (ei *escapeInterpreter) parseOne(ch []byte) (isEscape bool, err error) {
|
|||
ei.instruction = noInstruction{}
|
||||
}
|
||||
|
||||
ei.state = stateNone
|
||||
ei.csiParam = nil
|
||||
return true, nil
|
||||
case characterEquals(ch, 'H'), characterEquals(ch, 'f'),
|
||||
characterEquals(ch, 'd'):
|
||||
// CUP / HVP (absolute (row, col), col ignored) or VPA (absolute row).
|
||||
targetRow := ei.firstParamOrDefault(1)
|
||||
if targetRow <= 1 {
|
||||
// Cursor home. ConPTY emits this (after [2J) at the start of
|
||||
// every screen, so it marks where ConPTY's coordinate origin
|
||||
// now sits. Re-anchor our row tracking to the current write
|
||||
// position rather than treating it as a backward move: a view
|
||||
// that isn't rewound in lockstep with ConPTY's screen (the
|
||||
// command log) would otherwise carry stale drift, making every
|
||||
// later absolute CUP compute a negative, dropped advance and
|
||||
// collapsing the blank rows ConPTY positioned with.
|
||||
ei.resetScreenCursor()
|
||||
} else {
|
||||
// Skip forward to the target row; ignore backward moves.
|
||||
ei.emitCursorAdvance(targetRow - ei.screenRow)
|
||||
}
|
||||
ei.state = stateNone
|
||||
ei.csiParam = nil
|
||||
return true, nil
|
||||
case characterEquals(ch, 'B'), characterEquals(ch, 'E'):
|
||||
// CUD / CNL — relative row advance by N. CNL also resets
|
||||
// the column, which we don't track, so the two are
|
||||
// equivalent for our purposes.
|
||||
ei.emitCursorAdvance(ei.firstParamOrDefault(1))
|
||||
ei.state = stateNone
|
||||
ei.csiParam = nil
|
||||
return true, nil
|
||||
case characterEquals(ch, 'C'):
|
||||
// CUF — cursor forward N. Emit space cells so the gap
|
||||
// renders. (screenCol is updated by the view via
|
||||
// notifyCellsWritten as those spaces are emitted.)
|
||||
ei.instruction = cursorForward{n: ei.firstParamOrDefault(1)}
|
||||
ei.state = stateNone
|
||||
ei.csiParam = nil
|
||||
return true, nil
|
||||
|
|
|
|||
|
|
@ -153,17 +153,16 @@ func TestParseOneColours(t *testing.T) {
|
|||
|
||||
func TestParseOneIgnoresUnknownSequences(t *testing.T) {
|
||||
// Escape sequences the interpreter doesn't implement -- whether well-formed-but-unsupported
|
||||
// (cursor movement, private modes, DECSCUSR, …) or outright malformed -- must be silently
|
||||
// (private modes, DECSCUSR, …) or outright malformed -- must be silently
|
||||
// consumed rather than leaked into the view as literal text.
|
||||
scenarios := []string{
|
||||
"\x1b[?9001h", // DEC private-mode set (?-prefix)
|
||||
"\x1b[?25l", // hide cursor
|
||||
"\x1b[?25h", // show cursor
|
||||
"\x1b[2;J", // erase display (unusual 2;J variant)
|
||||
"\x1b[H", // cursor home — final byte immediately after [
|
||||
"\x1b[5;1;H", // cursor position with multiple params
|
||||
"\x1b[H", // cursor home — re-anchors to row 1 (no-op when already there)
|
||||
"\x1bc", // RIS — single-char ESC sequence
|
||||
"\x1b[;5H", // empty first param (';' immediately after '[')
|
||||
"\x1b[;5H", // empty first param — defaults to row 1, no-op
|
||||
"\x1b[ q", // intermediate byte with no params (DECSCUSR family)
|
||||
"\x1b[0 q", // intermediate byte after a param
|
||||
"\x1b[1;;m", // malformed SGR: empty middle param
|
||||
|
|
@ -184,6 +183,89 @@ func TestParseOneIgnoresUnknownSequences(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestParseOneCursorPositioning(t *testing.T) {
|
||||
// Cursor-positioning escapes that advance the row forward emit a
|
||||
// cursorDown instruction; backward / same-row moves are ignored
|
||||
// because the view's buffer is line-based.
|
||||
scenarios := []struct {
|
||||
input string
|
||||
startRow int // parser's screenRow before parsing
|
||||
wantAdvance int // 0 means "no instruction emitted"
|
||||
}{
|
||||
{"\x1b[5;1H", 1, 4}, // CUP — absolute row 5 from row 1
|
||||
{"\x1b[5H", 1, 4}, // CUP with only the row param
|
||||
{"\x1b[5;1H", 5, 0}, // CUP to the same row we're on — no-op
|
||||
{"\x1b[2;1H", 5, 0}, // CUP backward — ignored
|
||||
{"\x1b[5;1f", 1, 4}, // HVP alias for CUP
|
||||
{"\x1b[5d", 1, 4}, // VPA — absolute row
|
||||
{"\x1b[2d", 5, 0}, // VPA backward — ignored
|
||||
{"\x1b[3B", 1, 3}, // CUD — relative
|
||||
{"\x1b[B", 1, 1}, // CUD with default param of 1
|
||||
{"\x1b[2E", 1, 2}, // CNL — relative
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
ei := newEscapeInterpreter(OutputNormal)
|
||||
ei.screenRow = s.startRow
|
||||
parseEscRunes(t, ei, s.input)
|
||||
if s.wantAdvance == 0 {
|
||||
_, noop := ei.instruction.(noInstruction)
|
||||
assert.True(t, noop, "input %q at row %d should be a no-op", s.input, s.startRow)
|
||||
} else {
|
||||
cd, ok := ei.instruction.(cursorDown)
|
||||
if assert.True(t, ok, "input %q at row %d should emit cursorDown", s.input, s.startRow) {
|
||||
assert.Equal(t, s.wantAdvance, cd.n, "input %q at row %d", s.input, s.startRow)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseOneCursorHomeReanchors(t *testing.T) {
|
||||
// ConPTY emits cursor-home ([H) after [2J at the start of every screen.
|
||||
// In a view that isn't rewound in lockstep with ConPTY (the command log)
|
||||
// screenRow has drifted, so home must re-anchor it to the current write
|
||||
// position rather than be dropped as a backward move — otherwise the
|
||||
// absolute CUPs that follow compute negative, dropped advances and the
|
||||
// rows ConPTY positioned with collapse together.
|
||||
ei := newEscapeInterpreter(OutputNormal)
|
||||
ei.screenRow = 12 // accumulated drift from earlier command-log output
|
||||
|
||||
parseEscRunes(t, ei, "\x1b[H")
|
||||
assert.Equal(t, 1, ei.screenRow, "home should re-anchor screenRow")
|
||||
_, noop := ei.instruction.(noInstruction)
|
||||
assert.True(t, noop, "home should not emit an instruction")
|
||||
|
||||
// A subsequent CUP now advances relative to the re-anchored origin.
|
||||
parseEscRunes(t, ei, "\x1b[3;1H")
|
||||
cd, ok := ei.instruction.(cursorDown)
|
||||
if assert.True(t, ok, "CUP after home should emit cursorDown") {
|
||||
assert.Equal(t, 2, cd.n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseOneCursorForward(t *testing.T) {
|
||||
// CUF (\x1b[NC) emits a cursorForward instruction so the view can
|
||||
// materialize the N-cell gap as spaces. ConPTY uses this (often
|
||||
// paired with ECH) to encode runs of default-colored spaces.
|
||||
scenarios := []struct {
|
||||
input string
|
||||
wantN int
|
||||
}{
|
||||
{"\x1b[5C", 5},
|
||||
{"\x1b[1C", 1},
|
||||
{"\x1b[C", 1}, // no param defaults to 1
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
ei := newEscapeInterpreter(OutputNormal)
|
||||
parseEscRunes(t, ei, s.input)
|
||||
cf, ok := ei.instruction.(cursorForward)
|
||||
if assert.True(t, ok, "input %q should emit cursorForward", s.input) {
|
||||
assert.Equal(t, s.wantN, cf.n, "input %q", s.input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseEscRunes(t *testing.T, ei *escapeInterpreter, runes string) {
|
||||
t.Helper()
|
||||
for _, b := range []byte(runes) {
|
||||
|
|
|
|||
|
|
@ -39,15 +39,15 @@ func setupViews(t *testing.T, g *Gui) (*View, *View) {
|
|||
return status, main
|
||||
}
|
||||
|
||||
// pushContentOnly pushes a content-only event directly to the channel
|
||||
// (synchronous, deterministic — unlike Update which spawns a goroutine).
|
||||
// pushContentOnly enqueues a content-only event directly, letting the test
|
||||
// control the contentOnly flag (which Update/UpdateContentOnly hard-code).
|
||||
func pushContentOnly(g *Gui, f func(*Gui) error) {
|
||||
g.userEvents <- userEvent{f: f, task: g.NewTask(), contentOnly: true}
|
||||
g.userEvents.enqueue(userEvent{f: f, task: g.NewTask(), contentOnly: true})
|
||||
}
|
||||
|
||||
// pushRegular pushes a regular event directly to the channel.
|
||||
// pushRegular enqueues a regular (non-content-only) event directly.
|
||||
func pushRegular(g *Gui, f func(*Gui) error) {
|
||||
g.userEvents <- userEvent{f: f, task: g.NewTask(), contentOnly: false}
|
||||
g.userEvents.enqueue(userEvent{f: f, task: g.NewTask(), contentOnly: false})
|
||||
}
|
||||
|
||||
func TestFlushContentOnly_SkipsUntaintedViews(t *testing.T) {
|
||||
|
|
|
|||
458
pkg/gocui/gui.go
458
pkg/gocui/gui.go
|
|
@ -9,11 +9,13 @@ import (
|
|||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell/v3"
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/generics/set"
|
||||
"github.com/petermattis/goid"
|
||||
"github.com/rivo/uniseg"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
|
@ -123,15 +125,18 @@ type clickInfo struct {
|
|||
// and keybindings.
|
||||
type Gui struct {
|
||||
RecordingConfig
|
||||
// ReplayedEvents is for passing pre-recorded input events, for the purposes of testing
|
||||
ReplayedEvents replayedEvents
|
||||
// replayedEvents is for passing simulated input events, for the purposes
|
||||
// of testing. Events must be submitted through the Replay* methods, which
|
||||
// attach a task to each event; pushing into the channels directly would
|
||||
// bypass the busy-tracking that integration tests rely on.
|
||||
replayedEvents replayedEvents
|
||||
playRecording bool
|
||||
|
||||
tabClickBindings []*tabClickBinding
|
||||
viewMouseBindings []*ViewMouseBinding
|
||||
lastClick *clickInfo
|
||||
gEvents chan GocuiEvent
|
||||
userEvents chan userEvent
|
||||
userEvents *userEventQueue
|
||||
views []*View
|
||||
currentView *View
|
||||
managers []Manager
|
||||
|
|
@ -143,6 +148,10 @@ type Gui struct {
|
|||
maxX, maxY int
|
||||
outputMode OutputMode
|
||||
stop chan struct{}
|
||||
// loopExited is closed when MainLoop returns, so callers (e.g. the
|
||||
// integration-test harness) can wait for the event loop to actually finish
|
||||
// rather than polling or sleeping a fixed interval.
|
||||
loopExited chan struct{}
|
||||
|
||||
// BgColor and FgColor allow to configure the background and foreground
|
||||
// colors of the GUI.
|
||||
|
|
@ -193,7 +202,26 @@ type Gui struct {
|
|||
|
||||
taskManager *TaskManager
|
||||
|
||||
// The task of the event currently being processed on the main goroutine, if
|
||||
// any. Only touched from the main goroutine (in processEvent). It's excluded
|
||||
// from the Busy() check so that an event handler asking "is anything else
|
||||
// busy?" doesn't count itself.
|
||||
currentTask Task
|
||||
|
||||
lastHoverView *View
|
||||
|
||||
// uiThreadID is the goroutine id of the main event loop, recorded when
|
||||
// MainLoop starts. IsUIThread compares against it. Written once, read from
|
||||
// worker goroutines, so it's atomic.
|
||||
uiThreadID atomic.Int64
|
||||
|
||||
// blockInputCount, when greater than zero, withholds keyboard input from
|
||||
// the handlers: key events are buffered into bufferedKeyEvents and replayed
|
||||
// once the count drops back to zero, while mouse clicks and hover are
|
||||
// dropped outright. It's a counter so blocking can nest. Both fields are
|
||||
// only touched on the UI thread. See BeginBlockingEvents.
|
||||
blockInputCount int
|
||||
bufferedKeyEvents []GocuiEvent
|
||||
}
|
||||
|
||||
type NewGuiOpts struct {
|
||||
|
|
@ -236,13 +264,14 @@ func NewGui(opts NewGuiOpts) (*Gui, error) {
|
|||
g.outputMode = opts.OutputMode
|
||||
|
||||
g.stop = make(chan struct{})
|
||||
g.loopExited = make(chan struct{})
|
||||
|
||||
g.gEvents = make(chan GocuiEvent, 20)
|
||||
g.userEvents = make(chan userEvent, 20)
|
||||
g.userEvents = newUserEventQueue()
|
||||
g.taskManager = newTaskManager()
|
||||
|
||||
if opts.PlayRecording {
|
||||
g.ReplayedEvents = replayedEvents{
|
||||
g.replayedEvents = replayedEvents{
|
||||
Keys: make(chan *TcellKeyEventWrapper),
|
||||
Resizes: make(chan *TcellResizeEventWrapper),
|
||||
MouseEvents: make(chan *TcellMouseEventWrapper),
|
||||
|
|
@ -264,18 +293,63 @@ func NewGui(opts NewGuiOpts) (*Gui, error) {
|
|||
|
||||
g.playRecording = opts.PlayRecording
|
||||
|
||||
// Record the UI thread here, at construction. This assumes NewGui is called
|
||||
// on the same goroutine that will run MainLoop, which holds for all our
|
||||
// callers -- and it means IsUIThread is already correct for the UI work that
|
||||
// runs during startup, before we reach MainLoop.
|
||||
g.uiThreadID.Store(goid.Get())
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func (g *Gui) NewTask() *TaskImpl {
|
||||
return g.taskManager.NewTask()
|
||||
return g.taskManager.NewTask(false)
|
||||
}
|
||||
|
||||
// An idle listener listens for when the program is idle. This is useful for
|
||||
// integration tests which can wait for the program to be idle before taking
|
||||
// the next step in the test.
|
||||
func (g *Gui) AddIdleListener(c chan struct{}) {
|
||||
g.taskManager.addIdleListener(c)
|
||||
// NewBackgroundTask creates a task that is tracked for idle detection but does
|
||||
// not count towards the program being busy for repo-switch safety. See
|
||||
// TaskImpl.background.
|
||||
func (g *Gui) NewBackgroundTask() *TaskImpl {
|
||||
return g.taskManager.NewTask(true)
|
||||
}
|
||||
|
||||
// ReplayKeyEvent simulates a key press, as if the user had typed it. It's used
|
||||
// by integration tests. The event carries a task, so that the program counts
|
||||
// as busy from before the event is submitted until the main loop has fully
|
||||
// processed it; the test driver relies on this when it waits for the program
|
||||
// to go idle after submitting an event. (If the task were only created once
|
||||
// the main loop picks the event up, there would be a window in which the event
|
||||
// is still in flight but nothing counts as busy.)
|
||||
func (g *Gui) ReplayKeyEvent(ev *TcellKeyEventWrapper) {
|
||||
ev.task = g.NewTask()
|
||||
g.replayedEvents.Keys <- ev
|
||||
}
|
||||
|
||||
// ReplayMouseEvent is like ReplayKeyEvent, but for mouse events.
|
||||
func (g *Gui) ReplayMouseEvent(ev *TcellMouseEventWrapper) {
|
||||
ev.task = g.NewTask()
|
||||
g.replayedEvents.MouseEvents <- ev
|
||||
}
|
||||
|
||||
// ReplayFocusEvent is like ReplayKeyEvent, but for focus events.
|
||||
func (g *Gui) ReplayFocusEvent(ev *TcellFocusEventWrapper) {
|
||||
ev.task = g.NewTask()
|
||||
g.replayedEvents.FocusEvents <- ev
|
||||
}
|
||||
|
||||
// Busy reports whether any foreground work is in flight, ignoring the event
|
||||
// currently being processed on the main goroutine (see currentTask). Background
|
||||
// routines (auto-fetch etc.) don't count. It's used to decide whether it's safe
|
||||
// to switch repos. Must be called on the main goroutine.
|
||||
func (g *Gui) Busy() bool {
|
||||
return g.taskManager.hasBusyForegroundTaskExcept(g.currentTask)
|
||||
}
|
||||
|
||||
// WaitUntilIdle blocks until the program is idle (no busy tasks). This is
|
||||
// useful for integration tests which want to wait for the program to finish
|
||||
// processing before taking the next step in the test.
|
||||
func (g *Gui) WaitUntilIdle() {
|
||||
g.taskManager.WaitUntilIdle()
|
||||
}
|
||||
|
||||
// Close finalizes the library. It should be called after a successful
|
||||
|
|
@ -285,6 +359,11 @@ func (g *Gui) Close() {
|
|||
Screen.Fini()
|
||||
}
|
||||
|
||||
// LoopExited returns a channel that is closed once MainLoop has returned.
|
||||
func (g *Gui) LoopExited() <-chan struct{} {
|
||||
return g.loopExited
|
||||
}
|
||||
|
||||
// Size returns the terminal's size.
|
||||
func (g *Gui) Size() (x, y int) {
|
||||
return g.maxX, g.maxY
|
||||
|
|
@ -322,7 +401,7 @@ func (g *Gui) SetView(name string, x0, y0, x1, y1 int, overlaps byte) (*View, er
|
|||
v.y1 = y1
|
||||
|
||||
if sizeChanged {
|
||||
v.clearViewLines()
|
||||
v.ClearViewLines()
|
||||
|
||||
if v.Editable {
|
||||
cursorX, cursorY := v.TextArea.GetCursorXY()
|
||||
|
|
@ -603,6 +682,13 @@ func (g *Gui) SetRenderSearchStatusFunc(renderSearchStatusFunc func(*View, int,
|
|||
g.renderSearchStatusFunc = renderSearchStatusFunc
|
||||
}
|
||||
|
||||
// SetUpdateQueueHighWaterMarkHandler registers a diagnostic callback invoked
|
||||
// with the new depth whenever the queue of pending Update callbacks reaches a
|
||||
// new maximum. It may be called from any goroutine.
|
||||
func (g *Gui) SetUpdateQueueHighWaterMarkHandler(f func(depth int)) {
|
||||
g.userEvents.setHighWaterMarkHandler(f)
|
||||
}
|
||||
|
||||
// userEvent represents an event triggered by the user.
|
||||
type userEvent struct {
|
||||
f func(*Gui) error
|
||||
|
|
@ -613,34 +699,204 @@ type userEvent struct {
|
|||
contentOnly bool
|
||||
}
|
||||
|
||||
// Update executes the passed function. This method can be called safely from a
|
||||
// goroutine in order to update the GUI. It is important to note that the
|
||||
// passed function won't be executed immediately, instead it will be added to
|
||||
// the user events queue. Given that Update spawns a goroutine, the order in
|
||||
// which the user events will be handled is not guaranteed.
|
||||
// userEventQueue is an unbounded, order-preserving FIFO of work enqueued by
|
||||
// Update and friends for the main loop to run.
|
||||
//
|
||||
// It's unbounded (rather than a fixed-size channel) because producers must
|
||||
// never block or lose work. Update can be called from the UI goroutine itself,
|
||||
// where a blocking send would deadlock against the loop that drains the queue;
|
||||
// and it can be called from arbitrary worker goroutines that may enqueue faster
|
||||
// than the loop drains. That happens while the loop is stalled — suspended for
|
||||
// a subprocess (the editor runs on the UI thread), or hung in a long handler —
|
||||
// and also when a long-running worker operation emits a steady stream of
|
||||
// updates that outpaces the loop (e.g. the waiting-status spinner ticks while a
|
||||
// large directory is toggled into a custom patch). A fixed channel forces a
|
||||
// choice between blocking (deadlock), dropping or reordering, and panicking on
|
||||
// overflow; an unbounded queue avoids all three while preserving FIFO order.
|
||||
//
|
||||
// enqueue appends under the mutex and rings the doorbell; the main loop selects
|
||||
// on the doorbell to wake, then drains the slice to empty. The doorbell is
|
||||
// buffered(1) and rung with a non-blocking send, so it's a coalescing "work
|
||||
// pending" flag rather than a per-event signal: a burst of appends leaves at
|
||||
// most one token, and the loop drains everything the token represents on a
|
||||
// single wake. A token left over after a drain (because the drain happened to
|
||||
// empty the slice after the ring) just causes one harmless empty wake.
|
||||
type userEventQueue struct {
|
||||
mutex sync.Mutex
|
||||
events []userEvent
|
||||
doorbell chan struct{}
|
||||
|
||||
// highWaterMark is the deepest the queue has ever been, and
|
||||
// onHighWaterMark (if set) is called with the new depth each time that
|
||||
// record is broken. Purely diagnostic: it lets us see how deep the queue
|
||||
// gets in practice (see SetUpdateQueueHighWaterMarkHandler).
|
||||
highWaterMark int
|
||||
onHighWaterMark func(int)
|
||||
}
|
||||
|
||||
func newUserEventQueue() *userEventQueue {
|
||||
return &userEventQueue{doorbell: make(chan struct{}, 1)}
|
||||
}
|
||||
|
||||
// enqueue appends an event and wakes the main loop. It never blocks.
|
||||
func (q *userEventQueue) enqueue(ev userEvent) {
|
||||
q.mutex.Lock()
|
||||
q.events = append(q.events, ev)
|
||||
newHighWaterMark := 0
|
||||
if len(q.events) > q.highWaterMark {
|
||||
q.highWaterMark = len(q.events)
|
||||
newHighWaterMark = q.highWaterMark
|
||||
}
|
||||
onHighWaterMark := q.onHighWaterMark
|
||||
q.mutex.Unlock()
|
||||
|
||||
// Report outside the lock: the handler does I/O (logging) and must not
|
||||
// stall other producers or the draining loop.
|
||||
if newHighWaterMark > 0 && onHighWaterMark != nil {
|
||||
onHighWaterMark(newHighWaterMark)
|
||||
}
|
||||
|
||||
select {
|
||||
case q.doorbell <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (q *userEventQueue) setHighWaterMarkHandler(f func(int)) {
|
||||
q.mutex.Lock()
|
||||
q.onHighWaterMark = f
|
||||
q.mutex.Unlock()
|
||||
}
|
||||
|
||||
// dequeue pops the oldest event, reporting false when the queue is empty.
|
||||
func (q *userEventQueue) dequeue() (userEvent, bool) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
if len(q.events) == 0 {
|
||||
return userEvent{}, false
|
||||
}
|
||||
ev := q.events[0]
|
||||
if len(q.events) == 1 {
|
||||
// Release the backing array whenever the queue drains, so a one-off
|
||||
// burst doesn't pin its peak size for the rest of the session.
|
||||
q.events = nil
|
||||
} else {
|
||||
q.events[0] = userEvent{}
|
||||
q.events = q.events[1:]
|
||||
}
|
||||
return ev, true
|
||||
}
|
||||
|
||||
// Update enqueues f for the UI loop to run on its next iteration. Multiple
|
||||
// Update calls from the same goroutine arrive in source order (the queue is
|
||||
// FIFO). The enqueue never blocks and never drops work; see userEventQueue for
|
||||
// why the queue is unbounded.
|
||||
func (g *Gui) Update(f func(*Gui) error) {
|
||||
task := g.NewTask()
|
||||
|
||||
go g.updateAsyncAux(f, task)
|
||||
g.update(f, false)
|
||||
}
|
||||
|
||||
// UpdateAsync is a version of Update that does not spawn a go routine, it can
|
||||
// be a bit more efficient in cases where Update is called many times like when
|
||||
// tailing a file. In general you should use Update()
|
||||
func (g *Gui) UpdateAsync(f func(*Gui) error) {
|
||||
task := g.NewTask()
|
||||
|
||||
g.updateAsyncAux(f, task)
|
||||
// Like Update, but the enqueued work is a background routine (or triggered by
|
||||
// one), so it doesn't count towards the program being busy for repo-switch
|
||||
// safety. See TaskImpl.background.
|
||||
func (g *Gui) UpdateBackground(f func(*Gui) error) {
|
||||
g.update(f, true)
|
||||
}
|
||||
|
||||
func (g *Gui) updateAsyncAux(f func(*Gui) error, task Task) {
|
||||
g.userEvents <- userEvent{f: f, task: task}
|
||||
func (g *Gui) update(f func(*Gui) error, background bool) {
|
||||
task := g.taskManager.NewTask(background)
|
||||
g.userEvents.enqueue(userEvent{f: f, task: task})
|
||||
}
|
||||
|
||||
// Like Update, but signals that the callback only modifies content.
|
||||
func (g *Gui) UpdateContentOnly(f func(*Gui) error) {
|
||||
task := g.NewTask()
|
||||
g.userEvents <- userEvent{f: f, task: task, contentOnly: true}
|
||||
g.updateContentOnly(f, false)
|
||||
}
|
||||
|
||||
// Like UpdateContentOnly, but for background work (see UpdateBackground).
|
||||
func (g *Gui) UpdateContentOnlyBackground(f func(*Gui) error) {
|
||||
g.updateContentOnly(f, true)
|
||||
}
|
||||
|
||||
func (g *Gui) updateContentOnly(f func(*Gui) error, background bool) {
|
||||
task := g.taskManager.NewTask(background)
|
||||
g.userEvents.enqueue(userEvent{f: f, task: task, contentOnly: true})
|
||||
}
|
||||
|
||||
// IsUIThread reports whether the caller is running on the main event-loop
|
||||
// goroutine (the one running MainLoop). It calls goid.Get, so use it only for
|
||||
// debug assertions, not to drive production control flow.
|
||||
func (g *Gui) IsUIThread() bool {
|
||||
return goid.Get() == g.uiThreadID.Load()
|
||||
}
|
||||
|
||||
// BeginBlockingEvents starts withholding keyboard input from the handlers, so a
|
||||
// long-running operation can't be disrupted by keys the user presses while it
|
||||
// runs. Keys are buffered and replayed once EndBlockingEvents balances this
|
||||
// call; mouse clicks and hover are dropped for the duration. Scrolling,
|
||||
// resizing, focus changes and all rendering keep working throughout. It's a
|
||||
// counter, so blocking nests; every call must be paired with EndBlockingEvents.
|
||||
//
|
||||
// Must be called on the UI thread. Callers arrange this by beginning the block
|
||||
// synchronously from the keybinding handler, before dispatching the operation
|
||||
// to a worker — beginning it from the worker would race the next queued
|
||||
// keypress, which is exactly the input we mean to withhold.
|
||||
func (g *Gui) BeginBlockingEvents() {
|
||||
g.blockInputCount++
|
||||
}
|
||||
|
||||
// EndBlockingEvents balances a BeginBlockingEvents call. When the last nested
|
||||
// block ends, the keys buffered while blocked are replayed in order through the
|
||||
// normal dispatch path, so they act on the now-current context (a key whose
|
||||
// binding no longer exists is simply ignored, just as if it had been pressed
|
||||
// now). Must be called on the UI thread.
|
||||
func (g *Gui) EndBlockingEvents() error {
|
||||
g.blockInputCount--
|
||||
if g.blockInputCount > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
buffered := g.bufferedKeyEvents
|
||||
g.bufferedKeyEvents = nil
|
||||
for i := range buffered {
|
||||
if err := g.handleEvent(&buffered[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnUIThreadAndWait runs f on the main event-loop goroutine and blocks the
|
||||
// caller until f has run, returning f's error. Use it to read UI-thread-owned
|
||||
// state (the model, contexts) from a worker without racing the UI thread.
|
||||
//
|
||||
// It must be called from a worker goroutine, never from the UI thread itself:
|
||||
// the UI thread would block waiting for a callback only it can run, which
|
||||
// deadlocks. Callers arrange this by construction (see the refresh helper's
|
||||
// RefreshFromWorker); a debug-only assertion there guards against getting it
|
||||
// wrong.
|
||||
func (g *Gui) OnUIThreadAndWait(f func() error) error {
|
||||
return g.onUIThreadAndWait(f, false)
|
||||
}
|
||||
|
||||
// Like OnUIThreadAndWait, but the enqueued work belongs to a background routine,
|
||||
// so it doesn't count towards the program being busy (see UpdateBackground).
|
||||
func (g *Gui) OnUIThreadAndWaitBackground(f func() error) error {
|
||||
return g.onUIThreadAndWait(f, true)
|
||||
}
|
||||
|
||||
func (g *Gui) onUIThreadAndWait(f func() error, background bool) error {
|
||||
enqueue := g.Update
|
||||
if background {
|
||||
enqueue = g.UpdateBackground
|
||||
}
|
||||
|
||||
result := make(chan error, 1)
|
||||
enqueue(func(*Gui) error {
|
||||
result <- f()
|
||||
return nil
|
||||
})
|
||||
return <-result
|
||||
}
|
||||
|
||||
// Calls a function in a goroutine. Handles panics gracefully and tracks
|
||||
|
|
@ -650,7 +906,18 @@ func (g *Gui) UpdateContentOnly(f func(*Gui) error) {
|
|||
// background goroutines where you wouldn't want lazygit to be considered busy
|
||||
// (i.e. when you wouldn't want a loader to be shown to the user)
|
||||
func (g *Gui) OnWorker(f func(Task) error) {
|
||||
task := g.NewTask()
|
||||
g.onWorker(f, false)
|
||||
}
|
||||
|
||||
// Like OnWorker, but for a background routine (or work triggered by one), so it
|
||||
// doesn't count towards the program being busy for repo-switch safety. See
|
||||
// TaskImpl.background.
|
||||
func (g *Gui) OnWorkerBackground(f func(Task) error) {
|
||||
g.onWorker(f, true)
|
||||
}
|
||||
|
||||
func (g *Gui) onWorker(f func(Task) error, background bool) {
|
||||
task := g.taskManager.NewTask(background)
|
||||
go func() {
|
||||
g.onWorkerAux(f, task)
|
||||
task.Done()
|
||||
|
|
@ -714,6 +981,8 @@ func (g *Gui) SetManagerFunc(manager func(*Gui) error) {
|
|||
// MainLoop runs the main loop until an error is returned. A successful
|
||||
// finish should return ErrQuit.
|
||||
func (g *Gui) MainLoop() error {
|
||||
defer close(g.loopExited)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
|
|
@ -758,17 +1027,37 @@ func (g *Gui) handleError(err error) error {
|
|||
func (g *Gui) processEvent() error {
|
||||
contentOnly := false
|
||||
|
||||
// currentTask is the task of the event we're about to handle; recording it
|
||||
// lets Busy() ignore it, so a handler asking "is anything else busy?" (the
|
||||
// repo-switch guard does) doesn't count itself. Handlers of the remaining
|
||||
// events drained below run with currentTask still set to this primary event;
|
||||
// that's fine because the only Busy() callers are keybinding handlers, which
|
||||
// are always the primary event here.
|
||||
select {
|
||||
case ev := <-g.gEvents:
|
||||
task := g.NewTask()
|
||||
defer func() { task.Done() }()
|
||||
// Replayed test events already carry their task (see ReplayKeyEvent);
|
||||
// organic events get theirs here.
|
||||
task := ev.task
|
||||
if task == nil {
|
||||
task = g.NewTask()
|
||||
}
|
||||
g.currentTask = task
|
||||
defer func() { g.currentTask = nil; task.Done() }()
|
||||
|
||||
if err := g.handleError(g.handleEvent(&ev)); err != nil {
|
||||
return err
|
||||
}
|
||||
case ev := <-g.userEvents:
|
||||
case <-g.userEvents.doorbell:
|
||||
ev, ok := g.userEvents.dequeue()
|
||||
if !ok {
|
||||
// A leftover doorbell token whose events were already drained by a
|
||||
// previous iteration's processRemainingEvents: nothing to run and
|
||||
// nothing new to render.
|
||||
return nil
|
||||
}
|
||||
contentOnly = ev.contentOnly
|
||||
defer func() { ev.task.Done() }()
|
||||
g.currentTask = ev.task
|
||||
defer func() { g.currentTask = nil; ev.task.Done() }()
|
||||
|
||||
if err := g.handleError(ev.f(g)); err != nil {
|
||||
return err
|
||||
|
|
@ -795,18 +1084,27 @@ func (g *Gui) processRemainingEvents() (bool, error) {
|
|||
select {
|
||||
case ev := <-g.gEvents:
|
||||
contentOnly = false
|
||||
if err := g.handleError(g.handleEvent(&ev)); err != nil {
|
||||
err := g.handleError(g.handleEvent(&ev))
|
||||
if ev.task != nil {
|
||||
ev.task.Done()
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
case ev := <-g.userEvents:
|
||||
default:
|
||||
// No gui event is pending; drain a queued user event instead.
|
||||
// gui events take priority so input stays responsive, but they're
|
||||
// bounded (buffer of 20), so this can't starve the user-event queue.
|
||||
ev, ok := g.userEvents.dequeue()
|
||||
if !ok {
|
||||
return contentOnly, nil
|
||||
}
|
||||
contentOnly = ev.contentOnly && contentOnly
|
||||
err := g.handleError(ev.f(g))
|
||||
ev.task.Done()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
default:
|
||||
return contentOnly, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -814,6 +1112,17 @@ func (g *Gui) processRemainingEvents() (bool, error) {
|
|||
// handleEvent handles an event, based on its type (key-press, error,
|
||||
// etc.)
|
||||
func (g *Gui) handleEvent(ev *GocuiEvent) error {
|
||||
if g.blockInputCount > 0 && eventWithheldWhileBlocking(ev) {
|
||||
if ev.Type == eventKey {
|
||||
// Buffer keys so they replay against fresh state on unblock.
|
||||
g.bufferedKeyEvents = append(g.bufferedKeyEvents, *ev)
|
||||
}
|
||||
// Mouse clicks and hover fall through to here without being buffered:
|
||||
// replaying them once the operation has changed the layout underneath
|
||||
// them would target the wrong thing, so we drop them outright.
|
||||
return nil
|
||||
}
|
||||
|
||||
switch ev.Type {
|
||||
case eventKey, eventMouse, eventMouseMove:
|
||||
return g.onKey(ev)
|
||||
|
|
@ -832,6 +1141,24 @@ func (g *Gui) handleEvent(ev *GocuiEvent) error {
|
|||
}
|
||||
}
|
||||
|
||||
// eventWithheldWhileBlocking reports whether an event must not reach the
|
||||
// handlers while input is blocked (see BeginBlockingEvents). Key events are
|
||||
// withheld (buffered for replay); mouse clicks and hover are withheld (dropped).
|
||||
// Everything else — mouse scrolling, resize, focus, paste, errors — flows
|
||||
// through as usual.
|
||||
func eventWithheldWhileBlocking(ev *GocuiEvent) bool {
|
||||
switch ev.Type {
|
||||
case eventKey:
|
||||
return true
|
||||
case eventMouse:
|
||||
return !IsMouseScrollKey(ev.Key.KeyName())
|
||||
case eventMouseMove:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Gui) onResize() {
|
||||
// not sure if we actually need this
|
||||
// g.screen.Sync()
|
||||
|
|
@ -1143,6 +1470,11 @@ func (g *Gui) drawListFooter(v *View, fgColor, bgColor Attribute) error {
|
|||
|
||||
// flush updates the gui, re-drawing frames and buffers.
|
||||
func (g *Gui) flush() error {
|
||||
// The screen must not be touched while suspended (see Suspend).
|
||||
if g.isSuspended() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// pretty sure we don't need this, but keeping it here in case we get weird visual artifacts
|
||||
// g.clear(g.FgColor, g.BgColor)
|
||||
|
||||
|
|
@ -1150,7 +1482,7 @@ func (g *Gui) flush() error {
|
|||
// if GUI's size has changed, we need to redraw all views
|
||||
if maxX != g.maxX || maxY != g.maxY {
|
||||
for _, v := range g.views {
|
||||
v.clearViewLines()
|
||||
v.ClearViewLines()
|
||||
}
|
||||
}
|
||||
g.maxX, g.maxY = maxX, maxY
|
||||
|
|
@ -1175,6 +1507,11 @@ func (g *Gui) flush() error {
|
|||
// actually-changed cells are emitted to the terminal.
|
||||
// Will also redraw any views that overlap tainted views
|
||||
func (g *Gui) flushContentOnly(views []*View) error {
|
||||
// The screen must not be touched while suspended (see Suspend).
|
||||
if g.isSuspended() {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, v := range viewsToRedrawContentOnly(views) {
|
||||
if err := g.draw(v); err != nil {
|
||||
return err
|
||||
|
|
@ -1189,7 +1526,7 @@ func viewsToRedrawContentOnly(views []*View) []*View {
|
|||
redrawIndexes := set.New[int]()
|
||||
|
||||
for i, v := range views {
|
||||
if !v.tainted && !redrawIndexes.Includes(i) {
|
||||
if !v.IsTainted() && !redrawIndexes.Includes(i) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -1228,10 +1565,6 @@ func (g *Gui) ForceFlushViewsContentOnly(views []*View) error {
|
|||
|
||||
// draw manages the cursor and calls the draw function of a view.
|
||||
func (g *Gui) draw(v *View) error {
|
||||
if g.suspended {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !v.Visible || v.y1 < v.y0 || v.x1 < v.x0 {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1603,6 +1936,14 @@ func (g *Gui) onFocus(ev *GocuiEvent) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// While g.suspended is true, nothing must be drawn to the screen: tcell
|
||||
// releases the screen's cell buffer when disengaging, and drawing to a
|
||||
// disengaged screen spins forever inside tcell while holding the screen lock,
|
||||
// which then blocks Resume (and with it all further input) forever. For the
|
||||
// flag to guarantee that, it must only ever be false while the screen is
|
||||
// engaged: Suspend sets it before disengaging, and Resume clears it only
|
||||
// after re-engaging.
|
||||
|
||||
func (g *Gui) Suspend() error {
|
||||
g.suspendedMutex.Lock()
|
||||
defer g.suspendedMutex.Unlock()
|
||||
|
|
@ -1613,7 +1954,12 @@ func (g *Gui) Suspend() error {
|
|||
|
||||
g.suspended = true
|
||||
|
||||
return g.screen.Suspend()
|
||||
if err := g.screen.Suspend(); err != nil {
|
||||
g.suspended = false
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Gui) Resume() error {
|
||||
|
|
@ -1624,9 +1970,25 @@ func (g *Gui) Resume() error {
|
|||
return errors.New("Cannot resume because we are not suspended")
|
||||
}
|
||||
|
||||
if err := g.screen.Resume(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
g.suspended = false
|
||||
|
||||
return g.screen.Resume()
|
||||
// Schedule a redraw of the whole screen. Nothing else guarantees one:
|
||||
// flushes are skipped while suspended, and after re-engaging the screen
|
||||
// the terminal shows nothing until we draw again.
|
||||
go func() { g.gEvents <- GocuiEvent{Type: eventResize} }()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Gui) isSuspended() bool {
|
||||
g.suspendedMutex.Lock()
|
||||
defer g.suspendedMutex.Unlock()
|
||||
|
||||
return g.suspended
|
||||
}
|
||||
|
||||
// matchView returns if the keybinding matches the current view (and the view's context)
|
||||
|
|
|
|||
69
pkg/gocui/suspend_test.go
Normal file
69
pkg/gocui/suspend_test.go
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
package gocui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// A flush while suspended must return without touching the screen: tcell
|
||||
// releases the screen's cell buffer when disengaging, and drawing to a
|
||||
// disengaged screen spins forever inside tcell while holding the screen lock,
|
||||
// blocking the resume triggered by fg (#5309). The flush runs in a goroutine
|
||||
// so that a regression fails the test instead of hanging the suite.
|
||||
func TestFlushIsNoOpWhileSuspended(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
flush func(g *Gui) error
|
||||
}{
|
||||
{"flush", func(g *Gui) error { return g.flush() }},
|
||||
{"flushContentOnly", func(g *Gui) error { return g.flushContentOnly(g.views) }},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Deliberately not newTestGui: its cleanup closes the screen,
|
||||
// which would deadlock on the screen lock if a regression makes
|
||||
// the flush below spin.
|
||||
g, err := NewGui(NewGuiOpts{
|
||||
OutputMode: OutputNormal,
|
||||
Headless: true,
|
||||
Width: 80,
|
||||
Height: 24,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, g.Suspend())
|
||||
|
||||
flushReturned := make(chan error, 1)
|
||||
go func() { flushReturned <- tc.flush(g) }()
|
||||
|
||||
select {
|
||||
case err := <-flushReturned:
|
||||
assert.NoError(t, err)
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("flush touched the suspended screen and got stuck")
|
||||
}
|
||||
|
||||
assert.NoError(t, g.Resume())
|
||||
g.Close()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResumeSchedulesRedraw(t *testing.T) {
|
||||
g := newTestGui(t)
|
||||
|
||||
assert.NoError(t, g.Suspend())
|
||||
assert.NoError(t, g.Resume())
|
||||
|
||||
ev := GocuiEvent{Type: eventNone}
|
||||
select {
|
||||
case ev = <-g.gEvents:
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
|
||||
assert.Equal(t, eventResize, ev.Type,
|
||||
"resuming must schedule a redraw; without one the screen stays blank until the next event arrives")
|
||||
}
|
||||
|
|
@ -8,8 +8,9 @@ type Task interface {
|
|||
Done()
|
||||
Pause()
|
||||
Continue()
|
||||
// not exporting because we don't need to
|
||||
// not exporting these because we don't need to
|
||||
isBusy() bool
|
||||
isBackground() bool
|
||||
}
|
||||
|
||||
type TaskImpl struct {
|
||||
|
|
@ -17,6 +18,17 @@ type TaskImpl struct {
|
|||
busy bool
|
||||
onDone func()
|
||||
withMutex func(func())
|
||||
// Background tasks don't count towards the program being "busy" for the
|
||||
// purpose of deciding whether a repo switch is safe (see
|
||||
// TaskManager.hasBusyForegroundTaskExcept). Two kinds of work are tagged
|
||||
// this way: the ongoing background routines (auto-fetch, files refresh,
|
||||
// external-change detection) and the refreshes they trigger, whose model
|
||||
// writes are already guarded against a concurrent repo switch by the repo
|
||||
// generation; and view-buffer content rendering, which only paints a view
|
||||
// and so is harmless to leave running across a switch. What stays
|
||||
// foreground is lazygit driving a git operation and applying its results
|
||||
// to the model — exactly the work a repo switch must not run underneath.
|
||||
background bool
|
||||
}
|
||||
|
||||
func (self *TaskImpl) Done() {
|
||||
|
|
@ -39,6 +51,10 @@ func (self *TaskImpl) isBusy() bool {
|
|||
return self.busy
|
||||
}
|
||||
|
||||
func (self *TaskImpl) isBackground() bool {
|
||||
return self.background
|
||||
}
|
||||
|
||||
type TaskStatus int
|
||||
|
||||
const (
|
||||
|
|
@ -73,6 +89,10 @@ func (self *FakeTask) isBusy() bool {
|
|||
return self.status == TaskStatusBusy
|
||||
}
|
||||
|
||||
func (self *FakeTask) isBackground() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (self *FakeTask) Status() TaskStatus {
|
||||
return self.status
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,23 +6,26 @@ import "sync"
|
|||
// the main goroutine or a worker goroutine). Used by integration tests
|
||||
// to wait until the program is idle before progressing.
|
||||
type TaskManager struct {
|
||||
// each of these listeners will be notified when the program goes from busy to idle
|
||||
idleListeners []chan struct{}
|
||||
tasks map[int]Task
|
||||
tasks map[int]Task
|
||||
// auto-incrementing id for new tasks
|
||||
nextId int
|
||||
|
||||
mutex sync.Mutex
|
||||
// signalled whenever the program transitions from busy to idle; used by
|
||||
// WaitUntilIdle
|
||||
idleCond *sync.Cond
|
||||
}
|
||||
|
||||
func newTaskManager() *TaskManager {
|
||||
return &TaskManager{
|
||||
tasks: make(map[int]Task),
|
||||
idleListeners: []chan struct{}{},
|
||||
self := &TaskManager{
|
||||
tasks: make(map[int]Task),
|
||||
}
|
||||
self.idleCond = sync.NewCond(&self.mutex)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *TaskManager) NewTask() *TaskImpl {
|
||||
func (self *TaskManager) NewTask(background bool) *TaskImpl {
|
||||
self.mutex.Lock()
|
||||
defer self.mutex.Unlock()
|
||||
|
||||
|
|
@ -30,14 +33,54 @@ func (self *TaskManager) NewTask() *TaskImpl {
|
|||
taskId := self.nextId
|
||||
|
||||
onDone := func() { self.delete(taskId) }
|
||||
task := &TaskImpl{id: taskId, busy: true, onDone: onDone, withMutex: self.withMutex}
|
||||
task := &TaskImpl{id: taskId, busy: true, background: background, onDone: onDone, withMutex: self.withMutex}
|
||||
self.tasks[taskId] = task
|
||||
|
||||
return task
|
||||
}
|
||||
|
||||
func (self *TaskManager) addIdleListener(c chan struct{}) {
|
||||
self.idleListeners = append(self.idleListeners, c)
|
||||
// hasBusyForegroundTaskExcept reports whether any task other than `ignore` is
|
||||
// currently busy and not a background task. It's used to decide whether a repo
|
||||
// switch is safe: a foreground operation (or the refresh it triggers, or that
|
||||
// refresh's follow-up callbacks) still in flight means the switch must wait, so
|
||||
// it doesn't run against a repo that's about to be swapped out.
|
||||
//
|
||||
// `ignore` is the event currently being processed on the UI thread — the switch
|
||||
// attempt itself — which is always busy and so must not count as a reason to
|
||||
// refuse itself.
|
||||
func (self *TaskManager) hasBusyForegroundTaskExcept(ignore Task) bool {
|
||||
self.mutex.Lock()
|
||||
defer self.mutex.Unlock()
|
||||
|
||||
for _, task := range self.tasks {
|
||||
if task != ignore && task.isBusy() && !task.isBackground() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// WaitUntilIdle blocks until no task is busy. Integration tests use it to wait
|
||||
// for the program to finish processing before taking the next step.
|
||||
func (self *TaskManager) WaitUntilIdle() {
|
||||
self.mutex.Lock()
|
||||
defer self.mutex.Unlock()
|
||||
|
||||
for self.hasBusyTask() {
|
||||
self.idleCond.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
// caller must hold self.mutex
|
||||
func (self *TaskManager) hasBusyTask() bool {
|
||||
for _, task := range self.tasks {
|
||||
if task.isBusy() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (self *TaskManager) withMutex(f func()) {
|
||||
|
|
@ -46,17 +89,12 @@ func (self *TaskManager) withMutex(f func()) {
|
|||
|
||||
f()
|
||||
|
||||
// Check if all tasks are done
|
||||
for _, task := range self.tasks {
|
||||
if task.isBusy() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, all tasks are done, so
|
||||
// notify listeners that the program is idle
|
||||
for _, listener := range self.idleListeners {
|
||||
listener <- struct{}{}
|
||||
// Wake up any goroutine blocked in WaitUntilIdle. This must not block on
|
||||
// the waiter (we hold the mutex, and the waiter may itself be trying to
|
||||
// acquire it, e.g. by creating a task, before it next waits) — which is
|
||||
// exactly what Broadcast guarantees.
|
||||
if !self.hasBusyTask() {
|
||||
self.idleCond.Broadcast()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
129
pkg/gocui/task_manager_test.go
Normal file
129
pkg/gocui/task_manager_test.go
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
package gocui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTaskManagerHasBusyForegroundTaskExcept(t *testing.T) {
|
||||
t.Run("no tasks", func(t *testing.T) {
|
||||
tm := newTaskManager()
|
||||
assert.False(t, tm.hasBusyForegroundTaskExcept(nil))
|
||||
})
|
||||
|
||||
t.Run("a busy foreground task counts", func(t *testing.T) {
|
||||
tm := newTaskManager()
|
||||
tm.NewTask(false)
|
||||
assert.True(t, tm.hasBusyForegroundTaskExcept(nil))
|
||||
})
|
||||
|
||||
t.Run("a busy background task does not count", func(t *testing.T) {
|
||||
tm := newTaskManager()
|
||||
tm.NewTask(true)
|
||||
assert.False(t, tm.hasBusyForegroundTaskExcept(nil))
|
||||
})
|
||||
|
||||
t.Run("a done foreground task does not count", func(t *testing.T) {
|
||||
tm := newTaskManager()
|
||||
task := tm.NewTask(false)
|
||||
task.Done()
|
||||
assert.False(t, tm.hasBusyForegroundTaskExcept(nil))
|
||||
})
|
||||
|
||||
t.Run("a paused foreground task does not count", func(t *testing.T) {
|
||||
tm := newTaskManager()
|
||||
task := tm.NewTask(false)
|
||||
task.Pause()
|
||||
assert.False(t, tm.hasBusyForegroundTaskExcept(nil))
|
||||
})
|
||||
|
||||
t.Run("the ignored task does not count", func(t *testing.T) {
|
||||
tm := newTaskManager()
|
||||
task := tm.NewTask(false)
|
||||
assert.False(t, tm.hasBusyForegroundTaskExcept(task))
|
||||
})
|
||||
|
||||
t.Run("another foreground task counts even when one is ignored", func(t *testing.T) {
|
||||
tm := newTaskManager()
|
||||
ignored := tm.NewTask(false)
|
||||
tm.NewTask(false)
|
||||
assert.True(t, tm.hasBusyForegroundTaskExcept(ignored))
|
||||
})
|
||||
|
||||
t.Run("only a background task alongside the ignored current event", func(t *testing.T) {
|
||||
// This is the repo-switch case: the switch is handled as the current
|
||||
// event (ignored) while a background refresh is in flight; it must not
|
||||
// be considered busy.
|
||||
tm := newTaskManager()
|
||||
current := tm.NewTask(false)
|
||||
tm.NewTask(true)
|
||||
assert.False(t, tm.hasBusyForegroundTaskExcept(current))
|
||||
})
|
||||
}
|
||||
|
||||
func TestTaskManagerWaitUntilIdle(t *testing.T) {
|
||||
// returnsWithin reports whether f returns within the given duration.
|
||||
returnsWithin := func(d time.Duration, f func()) bool {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
f()
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
return true
|
||||
case <-time.After(d):
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("returns immediately when no task was ever created", func(t *testing.T) {
|
||||
tm := newTaskManager()
|
||||
assert.True(t, returnsWithin(time.Second, tm.WaitUntilIdle))
|
||||
})
|
||||
|
||||
t.Run("blocks while a task is busy", func(t *testing.T) {
|
||||
tm := newTaskManager()
|
||||
tm.NewTask(false)
|
||||
assert.False(t, returnsWithin(50*time.Millisecond, tm.WaitUntilIdle))
|
||||
})
|
||||
|
||||
t.Run("wakes up when the last busy task completes", func(t *testing.T) {
|
||||
tm := newTaskManager()
|
||||
task := tm.NewTask(false)
|
||||
go func() {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
task.Done()
|
||||
}()
|
||||
assert.True(t, returnsWithin(time.Second, tm.WaitUntilIdle))
|
||||
})
|
||||
|
||||
t.Run("a paused task counts as idle", func(t *testing.T) {
|
||||
tm := newTaskManager()
|
||||
task := tm.NewTask(false)
|
||||
task.Pause()
|
||||
assert.True(t, returnsWithin(time.Second, tm.WaitUntilIdle))
|
||||
})
|
||||
|
||||
t.Run("a task completing while nobody waits must not block", func(t *testing.T) {
|
||||
// This is the deadlock case: the waiter (the integration-test runner)
|
||||
// is between waits, and itself needs the task manager's mutex (it
|
||||
// creates a task whenever it enqueues work) before it waits again. The
|
||||
// idle notification must neither block the completing task while it
|
||||
// holds the mutex, nor get lost.
|
||||
tm := newTaskManager()
|
||||
assert.True(t, returnsWithin(time.Second, func() {
|
||||
// the program goes idle with nobody waiting...
|
||||
tm.NewTask(true).Done()
|
||||
|
||||
// ...and creating and completing more tasks afterwards must still
|
||||
// be possible
|
||||
task := tm.NewTask(false)
|
||||
tm.NewTask(false).Done()
|
||||
task.Done()
|
||||
}))
|
||||
assert.True(t, returnsWithin(time.Second, tm.WaitUntilIdle))
|
||||
})
|
||||
}
|
||||
|
|
@ -172,6 +172,12 @@ type GocuiEvent struct {
|
|||
Focused bool
|
||||
Start bool
|
||||
N int
|
||||
|
||||
// task tracks the processing of this event for idle detection. Events
|
||||
// replayed by integration tests carry a task from the moment they are
|
||||
// submitted (see Gui.ReplayKeyEvent); for organic events it is nil, and
|
||||
// the main loop creates a task when it picks the event up.
|
||||
task Task
|
||||
}
|
||||
|
||||
// Event types.
|
||||
|
|
@ -208,6 +214,8 @@ type TcellKeyEventWrapper struct {
|
|||
Mod tcell.ModMask
|
||||
Key tcell.Key
|
||||
Ch string
|
||||
|
||||
task Task // see GocuiEvent.task
|
||||
}
|
||||
|
||||
func NewTcellKeyEventWrapper(event *tcell.EventKey, timestamp int64) *TcellKeyEventWrapper {
|
||||
|
|
@ -229,6 +237,8 @@ type TcellMouseEventWrapper struct {
|
|||
Y int
|
||||
ButtonMask tcell.ButtonMask
|
||||
ModMask tcell.ModMask
|
||||
|
||||
task Task // see GocuiEvent.task
|
||||
}
|
||||
|
||||
func NewTcellMouseEventWrapper(event *tcell.EventMouse, timestamp int64) *TcellMouseEventWrapper {
|
||||
|
|
@ -269,6 +279,8 @@ func (wrapper TcellResizeEventWrapper) toTcellEvent() tcell.Event {
|
|||
type TcellFocusEventWrapper struct {
|
||||
Timestamp int64
|
||||
Focused bool
|
||||
|
||||
task Task // see GocuiEvent.task
|
||||
}
|
||||
|
||||
func NewTcellFocusEventWrapper(event *tcell.EventFocus, timestamp int64) *TcellFocusEventWrapper {
|
||||
|
|
@ -285,21 +297,31 @@ func (wrapper TcellFocusEventWrapper) toTcellEvent() tcell.Event {
|
|||
// pollEvent get tcell.Event and transform it into gocuiEvent
|
||||
func (g *Gui) pollEvent() GocuiEvent {
|
||||
var tev tcell.Event
|
||||
var task Task
|
||||
if g.playRecording {
|
||||
select {
|
||||
case ev := <-g.ReplayedEvents.Keys:
|
||||
case ev := <-g.replayedEvents.Keys:
|
||||
tev = (ev).toTcellEvent()
|
||||
case ev := <-g.ReplayedEvents.Resizes:
|
||||
task = ev.task
|
||||
case ev := <-g.replayedEvents.Resizes:
|
||||
tev = (ev).toTcellEvent()
|
||||
case ev := <-g.ReplayedEvents.MouseEvents:
|
||||
case ev := <-g.replayedEvents.MouseEvents:
|
||||
tev = (ev).toTcellEvent()
|
||||
case ev := <-g.ReplayedEvents.FocusEvents:
|
||||
task = ev.task
|
||||
case ev := <-g.replayedEvents.FocusEvents:
|
||||
tev = (ev).toTcellEvent()
|
||||
task = ev.task
|
||||
}
|
||||
} else {
|
||||
tev = <-Screen.EventQ()
|
||||
}
|
||||
|
||||
event := gocuiEventFromTcellEvent(tev)
|
||||
event.task = task
|
||||
return event
|
||||
}
|
||||
|
||||
func gocuiEventFromTcellEvent(tev tcell.Event) GocuiEvent {
|
||||
switch tev := tev.(type) {
|
||||
case *tcell.EventInterrupt:
|
||||
return GocuiEvent{Type: eventInterrupt}
|
||||
|
|
|
|||
111
pkg/gocui/user_event_queue_test.go
Normal file
111
pkg/gocui/user_event_queue_test.go
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
package gocui
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Enqueuing far more events than the old fixed 256-slot buffer, without the
|
||||
// main loop draining them, used to panic ("userEvents channel full"). It must
|
||||
// not: producers can legitimately burst faster than a stalled UI loop drains
|
||||
// (e.g. one command-log entry per git command when adding a large directory to
|
||||
// a custom patch, or any producer while the loop is blocked in a subprocess).
|
||||
// The events must also stay in FIFO order.
|
||||
func TestUpdateIsUnboundedAndPreservesOrder(t *testing.T) {
|
||||
g := newTestGui(t)
|
||||
|
||||
const n = 1000
|
||||
var got []int
|
||||
for i := range n {
|
||||
g.Update(func(*Gui) error {
|
||||
got = append(got, i)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Drain the whole queue the way the main loop's inner drain does.
|
||||
_, err := g.processRemainingEvents()
|
||||
assert.NoError(t, err)
|
||||
|
||||
want := make([]int, n)
|
||||
for i := range want {
|
||||
want[i] = i
|
||||
}
|
||||
assert.Equal(t, want, got)
|
||||
}
|
||||
|
||||
// The high-water-mark handler fires only when the queue reaches a new maximum
|
||||
// depth, reporting that depth. It does not reset when the queue drains.
|
||||
func TestUpdateQueueHighWaterMark(t *testing.T) {
|
||||
g := newTestGui(t)
|
||||
|
||||
var marks []int
|
||||
g.SetUpdateQueueHighWaterMarkHandler(func(depth int) { marks = append(marks, depth) })
|
||||
|
||||
noop := func(*Gui) error { return nil }
|
||||
|
||||
// Three enqueues with no drain: new highs 1, 2, 3.
|
||||
g.Update(noop)
|
||||
g.Update(noop)
|
||||
g.Update(noop)
|
||||
_, err := g.processRemainingEvents()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Two enqueues stay below the previous high of 3: no new marks.
|
||||
g.Update(noop)
|
||||
g.Update(noop)
|
||||
_, err = g.processRemainingEvents()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Four enqueues with no drain: only depth 4 beats the previous high.
|
||||
for range 4 {
|
||||
g.Update(noop)
|
||||
}
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3, 4}, marks)
|
||||
}
|
||||
|
||||
// Concurrent producers must be able to enqueue safely (run under -race). Only
|
||||
// same-goroutine order is guaranteed, so we check that every event is delivered
|
||||
// exactly once and that each producer's own events stay in order.
|
||||
func TestUpdateConcurrentProducers(t *testing.T) {
|
||||
g := newTestGui(t)
|
||||
|
||||
const producers = 8
|
||||
const perProducer = 500
|
||||
|
||||
type item struct{ producer, seq int }
|
||||
var got []item
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for p := range producers {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for seq := range perProducer {
|
||||
g.Update(func(*Gui) error {
|
||||
got = append(got, item{p, seq})
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}()
|
||||
}
|
||||
// Update is a synchronous, non-blocking enqueue, so once every producer has
|
||||
// returned, every event is in the queue and a single drain sees them all.
|
||||
wg.Wait()
|
||||
|
||||
_, err := g.processRemainingEvents()
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, got, producers*perProducer)
|
||||
lastSeq := make([]int, producers)
|
||||
for p := range lastSeq {
|
||||
lastSeq[p] = -1
|
||||
}
|
||||
for _, it := range got {
|
||||
assert.Equal(t, lastSeq[it.producer]+1, it.seq, "producer %d events out of order", it.producer)
|
||||
lastSeq[it.producer] = it.seq
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ package gocui
|
|||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode"
|
||||
|
|
@ -50,6 +51,14 @@ type View struct {
|
|||
// tained is true if the viewLines must be updated
|
||||
tainted bool
|
||||
|
||||
// firstDirtyLine is the index of the lowest line in `lines` that has been
|
||||
// written to or highlighted since viewLines was last refreshed, and whose
|
||||
// cached wrapping (lineType.wrappedCells) may therefore be stale. Lines
|
||||
// below it are unchanged and can reuse their cached wrapping instead of
|
||||
// being re-wrapped, which keeps refreshViewLinesIfNeeded cheap while
|
||||
// scrolling appends new lines to a long buffer.
|
||||
firstDirtyLine int
|
||||
|
||||
// the last position that the mouse was hovering over; nil if the mouse is outside of
|
||||
// this view, or not hovering over a cell
|
||||
lastHoverPosition *pos
|
||||
|
|
@ -206,6 +215,16 @@ func (v *View) clearViewLines() {
|
|||
v.clearHover()
|
||||
}
|
||||
|
||||
// ClearViewLines is clearViewLines guarded by writeMutex. It's for callers on
|
||||
// the UI thread (the layout pass) that touch a view whose content a task
|
||||
// goroutine may be writing concurrently: viewLines/tainted/hover are all
|
||||
// buffer state that writeMutex protects.
|
||||
func (v *View) ClearViewLines() {
|
||||
v.writeMutex.Lock()
|
||||
defer v.writeMutex.Unlock()
|
||||
v.clearViewLines()
|
||||
}
|
||||
|
||||
type searcher struct {
|
||||
searchString string
|
||||
searchPositions []SearchPosition
|
||||
|
|
@ -457,6 +476,16 @@ type viewLine struct {
|
|||
type lineType struct {
|
||||
cells cells
|
||||
trailingFillAttributes *trailingFillAttributes
|
||||
|
||||
// wrappedCells caches the result of wrapping `cells` to `wrappedColumns`
|
||||
// columns, so that unchanged lines don't have to be re-wrapped on every
|
||||
// refreshViewLinesIfNeeded (which runs on every scroll event, via
|
||||
// ViewLinesHeight). Wrapping measures every cell's width and allocates, so
|
||||
// for a long buffer that dominates the cost of scrolling. The cache is used
|
||||
// only for lines below View.firstDirtyLine whose wrappedColumns still
|
||||
// matches the current width; nil means nothing is cached yet.
|
||||
wrappedCells [][]cell
|
||||
wrappedColumns int
|
||||
}
|
||||
|
||||
// trailingFillAttributes describes the fg/bg colors that draw() should
|
||||
|
|
@ -518,9 +547,20 @@ func NewView(name string, x0, y0, x1, y1 int, mode OutputMode) *View {
|
|||
v.SelFgColor, v.SelBgColor = ColorDefault, ColorDefault
|
||||
v.InactiveViewSelBgColor = ColorDefault
|
||||
v.TitleColor, v.FrameColor = ColorDefault, ColorDefault
|
||||
v.ei.screenColMax = v.InnerWidth()
|
||||
return v
|
||||
}
|
||||
|
||||
// SetContentWidth tells the view the screen width that content written to it
|
||||
// should count soft-wraps against (see escapeInterpreter.notifyCellsWritten).
|
||||
// Callers pass the view's InnerWidth; it's a separate call, made on the UI
|
||||
// thread when a render starts, so that the task goroutine that streams the
|
||||
// content can consult this snapshot instead of reading the view's live
|
||||
// dimensions (which the UI thread mutates during layout).
|
||||
func (v *View) SetContentWidth(width int) {
|
||||
v.ei.screenColMax = width
|
||||
}
|
||||
|
||||
// Dimensions returns the dimensions of the View
|
||||
func (v *View) Dimensions() (int, int, int, int) {
|
||||
return v.x0, v.y0, v.x1, v.y1
|
||||
|
|
@ -815,6 +855,9 @@ func (v *View) Write(p []byte) (n int, err error) {
|
|||
|
||||
func (v *View) write(p []byte) {
|
||||
v.tainted = true
|
||||
// write only ever touches lines from v.wy onwards, so any cached wrapping
|
||||
// below that stays valid.
|
||||
v.firstDirtyLine = min(v.firstDirtyLine, v.wy)
|
||||
v.clearHover()
|
||||
|
||||
// Fill with empty cells, if writing outside current view buffer
|
||||
|
|
@ -834,6 +877,7 @@ func (v *View) write(p []byte) {
|
|||
|
||||
if v.pendingNewline {
|
||||
advanceToNextLine()
|
||||
v.ei.notifyRowAdvance()
|
||||
v.pendingNewline = false
|
||||
}
|
||||
|
||||
|
|
@ -855,11 +899,20 @@ func (v *View) write(p []byte) {
|
|||
case characterEquals(chr, '\n') || isCRLF(chr):
|
||||
finishLine()
|
||||
advanceToNextLine()
|
||||
v.ei.notifyRowAdvance()
|
||||
case characterEquals(chr, '\r'):
|
||||
finishLine()
|
||||
v.wx = 0
|
||||
v.ei.notifyColumnReset()
|
||||
default:
|
||||
truncateLine, cells := v.parseInput(chr, width, v.wx, v.wy)
|
||||
if cd, ok := v.ei.instruction.(cursorDown); ok {
|
||||
v.ei.instructionRead()
|
||||
for range cd.n {
|
||||
v.autoRenderHyperlinksInCurrentLine()
|
||||
advanceToNextLine()
|
||||
}
|
||||
}
|
||||
if cells == nil {
|
||||
continue
|
||||
}
|
||||
|
|
@ -867,6 +920,17 @@ func (v *View) write(p []byte) {
|
|||
if truncateLine {
|
||||
v.lines[v.wy].cells = v.lines[v.wy].cells[:v.wx]
|
||||
}
|
||||
// Soft-wrap tracking. truncateLine is true exactly when the
|
||||
// cells are from \x1b[K filling to end of line — ConPTY
|
||||
// doesn't advance the cursor for that, so we shouldn't count
|
||||
// it toward wraps either.
|
||||
if !truncateLine {
|
||||
totalWidth := 0
|
||||
for _, c := range cells {
|
||||
totalWidth += c.width
|
||||
}
|
||||
v.ei.notifyCellsWritten(totalWidth)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -982,6 +1046,14 @@ func (v *View) parseInput(ch []byte, width int, x int, _ int) (bool, []cell) {
|
|||
bg: v.ei.curBgColor,
|
||||
}
|
||||
return truncateLine, []cell{}
|
||||
} else if cf, ok := v.ei.instruction.(cursorForward); ok {
|
||||
// emit `n` space cells under the parser-tracked SGR — used
|
||||
// to materialize ConPTY's compressed runs of spaces (which
|
||||
// it emits as ECH+CUF instead of literal whitespace).
|
||||
v.ei.instructionRead()
|
||||
repeatCount = cf.n
|
||||
ch = []byte{' '}
|
||||
width = 1
|
||||
} else if isEscape {
|
||||
// do not output anything
|
||||
return truncateLine, nil
|
||||
|
|
@ -1075,10 +1147,25 @@ func (v *View) CopyContent(from *View) {
|
|||
v.writeMutex.Lock()
|
||||
defer v.writeMutex.Unlock()
|
||||
|
||||
// A background task may be streaming output into the source view's buffer
|
||||
// via Write, so read it under its own lock. The source is always a
|
||||
// different view than the destination (see the sole caller,
|
||||
// moveMainContextToTop), and no other code holds two view write locks at
|
||||
// once, so this can't deadlock.
|
||||
from.writeMutex.Lock()
|
||||
defer from.writeMutex.Unlock()
|
||||
|
||||
v.clear()
|
||||
|
||||
v.lines = from.lines
|
||||
v.viewLines = from.viewLines
|
||||
// Clone the row slices rather than sharing them: the source view stays
|
||||
// live (its streaming task keeps appending rows, and refreshViewLinesIfNeeded
|
||||
// fills each row's wrapping cache in place via &lines[i]), so sharing the
|
||||
// backing arrays would race those writes against this view's own rendering.
|
||||
// This is a shallow clone -- the per-row cell data is immutable once written
|
||||
// and stays shared, so the cost is proportional to the number of rows, not
|
||||
// their contents.
|
||||
v.lines = slices.Clone(from.lines)
|
||||
v.viewLines = slices.Clone(from.viewLines)
|
||||
v.ox = from.ox
|
||||
v.oy = from.oy
|
||||
v.cx = from.cx
|
||||
|
|
@ -1116,6 +1203,7 @@ func (v *View) FlushStaleCells() {
|
|||
|
||||
func (v *View) rewind() {
|
||||
v.ei.reset()
|
||||
v.ei.resetScreenCursor()
|
||||
|
||||
v.SetReadPos(0, 0)
|
||||
v.SetWritePos(0, 0)
|
||||
|
|
@ -1225,6 +1313,8 @@ func (v *View) updateSearchPositions() {
|
|||
|
||||
// IsTainted tells us if the view is tainted
|
||||
func (v *View) IsTainted() bool {
|
||||
v.writeMutex.Lock()
|
||||
defer v.writeMutex.Unlock()
|
||||
return v.tainted
|
||||
}
|
||||
|
||||
|
|
@ -1328,48 +1418,64 @@ func (v *View) draw() {
|
|||
}
|
||||
|
||||
func (v *View) refreshViewLinesIfNeeded() {
|
||||
if v.tainted {
|
||||
maxX := v.InnerWidth()
|
||||
lineIdx := 0
|
||||
lines := v.lines
|
||||
for i, line := range lines {
|
||||
wrap := 0
|
||||
if v.Wrap {
|
||||
wrap = maxX
|
||||
}
|
||||
|
||||
ls := lineWrap(line.cells, wrap)
|
||||
for j := range ls {
|
||||
// Per-segment trailing fill. When the source line opted in
|
||||
// via '\x1b[K', the LAST wrapped segment uses those colors
|
||||
// directly; earlier segments use the colors of their own
|
||||
// last cell, so the trailing area matches the bg active
|
||||
// where that segment ended rather than bleeding the
|
||||
// '\x1b[K' bg back across color changes in the line.
|
||||
var attrs *trailingFillAttributes
|
||||
if line.trailingFillAttributes != nil {
|
||||
if j == len(ls)-1 {
|
||||
attrs = line.trailingFillAttributes
|
||||
} else if len(ls[j]) > 0 {
|
||||
last := ls[j][len(ls[j])-1]
|
||||
attrs = &trailingFillAttributes{fg: last.fgColor, bg: last.bgColor}
|
||||
}
|
||||
}
|
||||
vline := viewLine{
|
||||
linesX: j, linesY: i, line: ls[j],
|
||||
trailingFillAttributes: attrs,
|
||||
}
|
||||
|
||||
if lineIdx > len(v.viewLines)-1 {
|
||||
v.viewLines = append(v.viewLines, vline)
|
||||
} else {
|
||||
v.viewLines[lineIdx] = vline
|
||||
}
|
||||
lineIdx++
|
||||
}
|
||||
}
|
||||
v.tainted = false
|
||||
if !v.tainted {
|
||||
return
|
||||
}
|
||||
|
||||
maxX := v.InnerWidth()
|
||||
wrap := 0
|
||||
if v.Wrap {
|
||||
wrap = maxX
|
||||
}
|
||||
|
||||
lineIdx := 0
|
||||
lines := v.lines
|
||||
for i := range lines {
|
||||
line := &lines[i]
|
||||
|
||||
// Reuse the previously wrapped result for lines that haven't changed
|
||||
// since the last refresh (i.e. below firstDirtyLine) and were wrapped at
|
||||
// the current width. Wrapping is expensive and this loop runs on every
|
||||
// scroll event, so only the lines that were actually just read (or
|
||||
// re-highlighted) should be wrapped afresh.
|
||||
if line.wrappedCells == nil || line.wrappedColumns != wrap || i >= v.firstDirtyLine {
|
||||
line.wrappedCells = lineWrap(line.cells, wrap)
|
||||
line.wrappedColumns = wrap
|
||||
}
|
||||
ls := line.wrappedCells
|
||||
|
||||
for j := range ls {
|
||||
// Per-segment trailing fill. When the source line opted in
|
||||
// via '\x1b[K', the LAST wrapped segment uses those colors
|
||||
// directly; earlier segments use the colors of their own
|
||||
// last cell, so the trailing area matches the bg active
|
||||
// where that segment ended rather than bleeding the
|
||||
// '\x1b[K' bg back across color changes in the line.
|
||||
var attrs *trailingFillAttributes
|
||||
if line.trailingFillAttributes != nil {
|
||||
if j == len(ls)-1 {
|
||||
attrs = line.trailingFillAttributes
|
||||
} else if len(ls[j]) > 0 {
|
||||
last := ls[j][len(ls[j])-1]
|
||||
attrs = &trailingFillAttributes{fg: last.fgColor, bg: last.bgColor}
|
||||
}
|
||||
}
|
||||
vline := viewLine{
|
||||
linesX: j, linesY: i, line: ls[j],
|
||||
trailingFillAttributes: attrs,
|
||||
}
|
||||
|
||||
if lineIdx > len(v.viewLines)-1 {
|
||||
v.viewLines = append(v.viewLines, vline)
|
||||
} else {
|
||||
v.viewLines[lineIdx] = vline
|
||||
}
|
||||
lineIdx++
|
||||
}
|
||||
}
|
||||
|
||||
v.firstDirtyLine = len(lines)
|
||||
v.tainted = false
|
||||
}
|
||||
|
||||
// if autoscroll is enabled but we only have a single row of cells shown to the
|
||||
|
|
@ -1457,6 +1563,9 @@ func (v *View) BufferLines() []string {
|
|||
// Buffer returns a string with the contents of the view's internal
|
||||
// buffer.
|
||||
func (v *View) Buffer() string {
|
||||
v.writeMutex.Lock()
|
||||
defer v.writeMutex.Unlock()
|
||||
|
||||
return linesToString(v.lines)
|
||||
}
|
||||
|
||||
|
|
@ -1569,6 +1678,7 @@ func (v *View) SetHighlight(y int, on bool) {
|
|||
cells = append(cells, c)
|
||||
}
|
||||
v.tainted = true
|
||||
v.firstDirtyLine = min(v.firstDirtyLine, y)
|
||||
v.lines[y].cells = cells
|
||||
v.clearHover()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -238,6 +238,91 @@ func TestContainsColoredText(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestWriteCursorPositionEscape(t *testing.T) {
|
||||
// ConPTY presents its child's output as a screen buffer and uses cursor
|
||||
// positioning escapes (CUP, `\x1b[<row>;<col>H`) to skip over blank rows
|
||||
// rather than emitting empty LFs for them. The escape interpreter must
|
||||
// synthesize the row advances those CUPs imply; otherwise non-blank rows
|
||||
// that the child separated with blank lines end up adjacent in the view.
|
||||
v := NewView("name", 0, 0, 20, 10, OutputNormal)
|
||||
// "a", then "skip to row 3" (i.e. one blank row), then "b".
|
||||
v.writeString("a\r\n\x1b[3;1Hb\r\n")
|
||||
|
||||
got := make([][]string, 0, len(v.lines))
|
||||
for _, l := range v.lines {
|
||||
got = append(got, cellsToStrings(l.cells))
|
||||
}
|
||||
|
||||
assert.Equal(t, [][]string{{"a"}, {}, {"b"}}, got)
|
||||
}
|
||||
|
||||
func TestWriteCursorPositionEscapeAcrossWrites(t *testing.T) {
|
||||
// Mirrors the production flow: bufio.Scanner splits the pty output on
|
||||
// LF and feeds the view one Write per line (each with a trailing \n
|
||||
// appended). The parser's screen-row counter must keep ticking across
|
||||
// writes; otherwise CUPs are evaluated against a stale row and
|
||||
// overshoot, producing too many blank lines instead of the right
|
||||
// number.
|
||||
v := NewView("name", 0, 0, 30, 30, OutputNormal)
|
||||
v.writeString("a\n")
|
||||
v.writeString("b\n")
|
||||
// ConPTY is on row 3 here; CUP to row 5 should skip exactly one row.
|
||||
v.writeString("c\x1b[5;1Hd\n")
|
||||
|
||||
got := make([][]string, 0, len(v.lines))
|
||||
for _, l := range v.lines {
|
||||
got = append(got, cellsToStrings(l.cells))
|
||||
}
|
||||
assert.Equal(t, [][]string{
|
||||
{"a"},
|
||||
{"b"},
|
||||
{"c"},
|
||||
{},
|
||||
{"d"},
|
||||
}, got)
|
||||
}
|
||||
|
||||
func TestWriteCursorForwardEscape(t *testing.T) {
|
||||
// ConPTY compresses runs of default-colored spaces into ECH (\x1b[NX,
|
||||
// "clear N cells, cursor stationary") + CUF (\x1b[NC, "cursor forward
|
||||
// N") rather than emitting them literally. The interpreter has to
|
||||
// materialize CUF as N visible spaces; otherwise the gap collapses
|
||||
// and content that followed the indentation slides left.
|
||||
v := NewView("name", 0, 0, 20, 10, OutputNormal)
|
||||
// "a" + ECH 5 + CUF 5 + "b" — visually "a b".
|
||||
v.writeString("a\x1b[5X\x1b[5Cb\n")
|
||||
|
||||
got := make([][]string, 0, len(v.lines))
|
||||
for _, l := range v.lines {
|
||||
got = append(got, cellsToStrings(l.cells))
|
||||
}
|
||||
|
||||
assert.Equal(t, [][]string{{"a", " ", " ", " ", " ", " ", "b"}}, got)
|
||||
}
|
||||
|
||||
func TestWriteCursorPositionEscapeWithSoftWraps(t *testing.T) {
|
||||
// If a logical line is longer than ConPTY's terminal width, ConPTY
|
||||
// soft-wraps it onto multiple physical rows in its screen, and any
|
||||
// subsequent CUP is addressed against the post-wrap row count. To
|
||||
// keep our screen-row counter accurate we have to count those wraps
|
||||
// as we write the cells. InnerWidth here is 5; "abcdefghij" (10
|
||||
// cells) wraps onto 2 rows, so ConPTY is on row 3 after the LF and a
|
||||
// CUP to row 4 should skip exactly one row.
|
||||
v := NewView("name", 0, 0, 6, 30, OutputNormal) // Width=7, InnerWidth=5
|
||||
v.writeString("abcdefghij\n")
|
||||
v.writeString("\x1b[4;1Hxyz\n")
|
||||
|
||||
got := make([][]string, 0, len(v.lines))
|
||||
for _, l := range v.lines {
|
||||
got = append(got, cellsToStrings(l.cells))
|
||||
}
|
||||
assert.Equal(t, [][]string{
|
||||
{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"},
|
||||
{},
|
||||
{"x", "y", "z"},
|
||||
}, got)
|
||||
}
|
||||
|
||||
func stringToCells(s string) []cell {
|
||||
var cells []cell
|
||||
state := -1
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ import (
|
|||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
|
@ -43,6 +45,11 @@ func (self *BackgroundRoutineMgr) startBackgroundRoutines() {
|
|||
if userConfig.Git.AutoFetch {
|
||||
fetchInterval := userConfig.Refresher.FetchInterval
|
||||
if fetchInterval > 0 {
|
||||
// The channel must be created here, on the UI thread and before
|
||||
// the fetch goroutine spawns, so that triggerImmediateFetch (also
|
||||
// running on the UI thread) can read the field without racing the
|
||||
// write. See triggerImmediateFetch for why it is buffered.
|
||||
self.triggerFetch = make(chan struct{}, 1)
|
||||
go utils.Safe(self.startBackgroundFetch)
|
||||
} else {
|
||||
self.gui.c.Log.Errorf(
|
||||
|
|
@ -74,7 +81,7 @@ func (self *BackgroundRoutineMgr) startBackgroundRoutines() {
|
|||
}
|
||||
|
||||
if self.gui.Config.GetDebug() {
|
||||
self.goEvery(time.Second*time.Duration(10), self.gui.stopChan, func(_ bool) error {
|
||||
self.goEvery(time.Second*time.Duration(10), self.gui.stopChan, nil, func(_ bool) error {
|
||||
formatBytes := func(b uint64) string {
|
||||
const unit = 1000
|
||||
if b < unit {
|
||||
|
|
@ -101,23 +108,35 @@ func (self *BackgroundRoutineMgr) startBackgroundFetch() {
|
|||
self.gui.waitForIntro.Wait()
|
||||
|
||||
fetch := func(firstTimeOrRetriggered bool) error {
|
||||
// Do this on the UI thread so that we don't have to deal with synchronization around the
|
||||
// access of the repo state.
|
||||
self.gui.onUIThread(func() error {
|
||||
// There's a race here, where we might be recording the time stamp for a different repo
|
||||
// than where the fetch actually ran. It's not very likely though, and not harmful if it
|
||||
// does happen; guarding against it would be more effort than it's worth.
|
||||
// Capture what the fetch needs from the gui's per-repo state in a
|
||||
// single UI-thread hop: gui.git, gui.helpers and gui.State are all
|
||||
// replaced on a repo switch (which runs on the UI thread), so reading
|
||||
// them from this background goroutine would race the reassignment.
|
||||
// Capturing them together also ties the fetch, the post-fetch
|
||||
// refresh's generation baseline, and the recorded fetch time to the
|
||||
// same repo.
|
||||
var git *commands.GitCommand
|
||||
var appStatusHelper *helpers.AppStatusHelper
|
||||
var branchesHelper *helpers.BranchesHelper
|
||||
var fetchGeneration int
|
||||
if err := self.gui.g.OnUIThreadAndWaitBackground(func() error {
|
||||
git = self.gui.git
|
||||
appStatusHelper = self.gui.helpers.AppStatus
|
||||
branchesHelper = self.gui.helpers.BranchesHelper
|
||||
fetchGeneration = self.gui.c.State().GetRepoGeneration()
|
||||
self.gui.State.LastBackgroundFetchTime = time.Now()
|
||||
return nil
|
||||
})
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if self.gui.UserConfig().Gui.ShowBottomLine || firstTimeOrRetriggered {
|
||||
return self.gui.helpers.AppStatus.WithWaitingStatusImpl(self.gui.Tr.FetchingStatus, func(gocui.Task) error {
|
||||
return self.backgroundFetch()
|
||||
return appStatusHelper.WithWaitingStatusImpl(self.gui.Tr.FetchingStatus, func(gocui.Task) error {
|
||||
return self.backgroundFetch(git, branchesHelper, fetchGeneration)
|
||||
}, nil)
|
||||
}
|
||||
|
||||
return self.backgroundFetch()
|
||||
return self.backgroundFetch(git, branchesHelper, fetchGeneration)
|
||||
}
|
||||
|
||||
// We want an immediate fetch at startup, and since goEvery starts by
|
||||
|
|
@ -125,15 +144,15 @@ func (self *BackgroundRoutineMgr) startBackgroundFetch() {
|
|||
_ = fetch(true)
|
||||
|
||||
userConfig := self.gui.UserConfig()
|
||||
self.triggerFetch = self.goEvery(userConfig.Refresher.FetchIntervalDuration(), self.gui.stopChan, fetch)
|
||||
self.goEvery(userConfig.Refresher.FetchIntervalDuration(), self.gui.stopChan, self.triggerFetch, fetch)
|
||||
}
|
||||
|
||||
func (self *BackgroundRoutineMgr) startBackgroundFilesRefresh() {
|
||||
self.gui.waitForIntro.Wait()
|
||||
|
||||
userConfig := self.gui.UserConfig()
|
||||
self.goEvery(userConfig.Refresher.RefreshIntervalDuration(), self.gui.stopChan, func(_ bool) error {
|
||||
self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}, Background: true})
|
||||
self.goEvery(userConfig.Refresher.RefreshIntervalDuration(), self.gui.stopChan, nil, func(_ bool) error {
|
||||
self.gui.c.RefreshFromWorker(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}, Background: true})
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
@ -151,6 +170,7 @@ func (self *BackgroundRoutineMgr) startBackgroundExternalChangeDetection() {
|
|||
self.goEvery(
|
||||
userConfig.Refresher.ExternalChangeCheckIntervalDuration(),
|
||||
self.gui.stopChan,
|
||||
nil,
|
||||
func(_ bool) error {
|
||||
self.checkForExternalChanges()
|
||||
return nil
|
||||
|
|
@ -159,7 +179,20 @@ func (self *BackgroundRoutineMgr) startBackgroundExternalChangeDetection() {
|
|||
}
|
||||
|
||||
func (self *BackgroundRoutineMgr) checkForExternalChanges() {
|
||||
current, err := self.gui.git.Status.RefsSnapshot()
|
||||
// Capture the per-repo objects in a UI-thread hop, like the background
|
||||
// fetch does: gui.git and gui.helpers are replaced on a repo switch, so
|
||||
// reading them from this background goroutine would race the reassignment.
|
||||
var git *commands.GitCommand
|
||||
var refreshHelper *helpers.RefreshHelper
|
||||
if err := self.gui.g.OnUIThreadAndWaitBackground(func() error {
|
||||
git = self.gui.git
|
||||
refreshHelper = self.gui.helpers.Refresh
|
||||
return nil
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
current, err := git.Status.RefsSnapshot()
|
||||
if err != nil {
|
||||
// Transient error (e.g. git process couldn't start). Don't update the
|
||||
// stored snapshot; we'll retry next tick.
|
||||
|
|
@ -167,7 +200,7 @@ func (self *BackgroundRoutineMgr) checkForExternalChanges() {
|
|||
return
|
||||
}
|
||||
|
||||
if !self.gui.helpers.Refresh.RefsSnapshotChangedSince(current) {
|
||||
if !refreshHelper.RefsSnapshotChangedSince(current) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -184,13 +217,13 @@ func (self *BackgroundRoutineMgr) checkForExternalChanges() {
|
|||
|
||||
// No need to update the stored snapshot here; Refresh does that.
|
||||
self.gui.c.Log.Info("External ref change detected — refreshing")
|
||||
self.gui.c.Refresh(types.RefreshOptions{Background: true})
|
||||
self.gui.c.RefreshFromWorker(types.RefreshOptions{Background: true})
|
||||
}
|
||||
|
||||
// returns a channel that can be used to trigger the callback immediately
|
||||
func (self *BackgroundRoutineMgr) goEvery(interval time.Duration, stop chan struct{}, function func(bool) error) chan struct{} {
|
||||
// Runs function every interval until stop is closed. A send on retrigger (if
|
||||
// non-nil) runs the callback immediately and restarts the interval.
|
||||
func (self *BackgroundRoutineMgr) goEvery(interval time.Duration, stop, retrigger chan struct{}, function func(bool) error) {
|
||||
done := make(chan struct{})
|
||||
retrigger := make(chan struct{})
|
||||
go utils.Safe(func() {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
|
@ -198,7 +231,10 @@ func (self *BackgroundRoutineMgr) goEvery(interval time.Duration, stop chan stru
|
|||
if self.backgroundRefreshesPaused() {
|
||||
return
|
||||
}
|
||||
self.gui.c.OnWorker(func(gocui.Task) error {
|
||||
// OnWorkerBackground, not OnWorker: these routines and the refreshes
|
||||
// they trigger must not count towards lazygit being busy, or they'd
|
||||
// spuriously block a repo switch every time one happens to be running.
|
||||
self.gui.c.OnWorkerBackground(func(gocui.Task) error {
|
||||
_ = function(retriggered)
|
||||
done <- struct{}{}
|
||||
return nil
|
||||
|
|
@ -220,17 +256,30 @@ func (self *BackgroundRoutineMgr) goEvery(interval time.Duration, stop chan stru
|
|||
}
|
||||
}
|
||||
})
|
||||
return retrigger
|
||||
}
|
||||
|
||||
func (self *BackgroundRoutineMgr) backgroundFetch() (err error) {
|
||||
err = self.gui.git.Sync.FetchBackground()
|
||||
// The parameters are captured by the caller before the fetch starts, not read
|
||||
// here after it: the fetch is a network call during which the user may switch
|
||||
// repos, and the post-fetch refresh needs to be able to tell (see
|
||||
// PostFetchRefresh).
|
||||
func (self *BackgroundRoutineMgr) backgroundFetch(git *commands.GitCommand, branchesHelper *helpers.BranchesHelper, fetchGeneration int) error {
|
||||
err := git.Sync.FetchBackground()
|
||||
|
||||
return self.gui.helpers.BranchesHelper.PostFetchRefresh(err, true)
|
||||
return branchesHelper.PostFetchRefresh(err, true, fetchGeneration)
|
||||
}
|
||||
|
||||
func (self *BackgroundRoutineMgr) triggerImmediateFetch() {
|
||||
if self.triggerFetch != nil {
|
||||
self.triggerFetch <- struct{}{}
|
||||
// This runs on the UI thread, which must never block waiting for a
|
||||
// background routine; in particular, the goEvery loop only receives
|
||||
// between callbacks, and an in-flight fetch can itself be waiting for
|
||||
// the UI thread to perform its post-fetch refresh, so a blocking send
|
||||
// here would deadlock. The channel has a buffer of one, so the trigger
|
||||
// is latched even when the loop isn't currently receiving; if one is
|
||||
// already pending, the two coalesce.
|
||||
select {
|
||||
case self.triggerFetch <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,10 +27,20 @@ func (gui *Gui) LogAction(action string) {
|
|||
return
|
||||
}
|
||||
|
||||
gui.Views.Extras.Autoscroll = true
|
||||
// LogAction and LogCommand are called both from the UI thread and from git
|
||||
// worker goroutines, so bounce the writes onto the UI thread: they touch the
|
||||
// view's autoscroll flag and the GuiLog slice, which the layout/draw code
|
||||
// reads. Ordering between successive log calls is preserved by the FIFO the
|
||||
// bounce enqueues onto. It's a background bounce because writing the command
|
||||
// log is incidental display work that must not count towards lazygit being
|
||||
// busy (otherwise it could block a repo switch).
|
||||
gui.onUIThreadBackground(func() error {
|
||||
gui.Views.Extras.Autoscroll = true
|
||||
|
||||
gui.GuiLog = append(gui.GuiLog, action)
|
||||
fmt.Fprint(gui.Views.Extras, "\n"+style.FgYellow.Sprint(action))
|
||||
gui.GuiLog = append(gui.GuiLog, action)
|
||||
fmt.Fprint(gui.Views.Extras, "\n"+style.FgYellow.Sprint(action))
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) LogCommand(cmdStr string, commandLine bool) {
|
||||
|
|
@ -38,17 +48,23 @@ func (gui *Gui) LogCommand(cmdStr string, commandLine bool) {
|
|||
return
|
||||
}
|
||||
|
||||
gui.Views.Extras.Autoscroll = true
|
||||
|
||||
textStyle := theme.DefaultTextColor
|
||||
if !commandLine {
|
||||
// if we're not dealing with a direct command that could be run on the command line,
|
||||
// we style it differently to communicate that
|
||||
textStyle = style.FgMagenta
|
||||
}
|
||||
gui.GuiLog = append(gui.GuiLog, cmdStr)
|
||||
indentedCmdStr := " " + strings.ReplaceAll(cmdStr, "\n", "\n ")
|
||||
fmt.Fprint(gui.Views.Extras, "\n"+textStyle.Sprint(indentedCmdStr))
|
||||
|
||||
// See the comment in LogAction: bounce onto the UI thread since we may be
|
||||
// called from a git worker, in the background so it can't block a repo switch.
|
||||
gui.onUIThreadBackground(func() error {
|
||||
gui.Views.Extras.Autoscroll = true
|
||||
|
||||
gui.GuiLog = append(gui.GuiLog, cmdStr)
|
||||
fmt.Fprint(gui.Views.Extras, "\n"+textStyle.Sprint(indentedCmdStr))
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) printCommandLogHeader() {
|
||||
|
|
|
|||
|
|
@ -32,34 +32,76 @@ type ListRenderer struct {
|
|||
getNonModelItems func() []*NonModelItem
|
||||
|
||||
// The remaining fields are private and shouldn't be initialized by clients
|
||||
numNonModelItems int
|
||||
viewIndicesByModelIndex []int
|
||||
modelIndicesByViewIndex []int
|
||||
columnPositions []int
|
||||
columnPositions []int
|
||||
}
|
||||
|
||||
func (self *ListRenderer) GetList() types.IList {
|
||||
return self.list
|
||||
}
|
||||
|
||||
func (self *ListRenderer) ModelIndexToViewIndex(modelIndex int) int {
|
||||
modelIndex = lo.Clamp(modelIndex, 0, self.list.Len())
|
||||
if self.viewIndicesByModelIndex != nil {
|
||||
return self.viewIndicesByModelIndex[modelIndex]
|
||||
func (self *ListRenderer) getNonModelItemList() []*NonModelItem {
|
||||
if self.getNonModelItems == nil {
|
||||
return nil
|
||||
}
|
||||
return self.getNonModelItems()
|
||||
}
|
||||
|
||||
return modelIndex
|
||||
func (self *ListRenderer) ModelIndexToViewIndex(modelIndex int) int {
|
||||
return modelIndexToViewIndex(self.list.Len(), self.getNonModelItemList(), modelIndex)
|
||||
}
|
||||
|
||||
func (self *ListRenderer) ViewIndexToModelIndex(viewIndex int) int {
|
||||
viewIndex = lo.Clamp(viewIndex, 0, self.list.Len()+self.numNonModelItems)
|
||||
if self.modelIndicesByViewIndex != nil {
|
||||
return self.modelIndicesByViewIndex[viewIndex]
|
||||
}
|
||||
return viewIndexToModelIndex(self.list.Len(), self.getNonModelItemList(), viewIndex)
|
||||
}
|
||||
|
||||
// modelToViewIndexConverter returns a model-to-view index conversion that
|
||||
// reuses a single snapshot of the non-model items. Callers that convert many
|
||||
// indices in a row (e.g. search, which converts every commit) should use this
|
||||
// rather than calling ModelIndexToViewIndex per index, which would rebuild the
|
||||
// non-model items each time.
|
||||
func (self *ListRenderer) modelToViewIndexConverter() func(modelIndex int) int {
|
||||
listLength := self.list.Len()
|
||||
nonModelItems := self.getNonModelItemList()
|
||||
return func(modelIndex int) int {
|
||||
return modelIndexToViewIndex(listLength, nonModelItems, modelIndex)
|
||||
}
|
||||
}
|
||||
|
||||
// The view shows the model items with the non-model items (e.g. section
|
||||
// headers) inserted at their model indices. The two conversions below are
|
||||
// computed directly from the current list length and non-model items, so they
|
||||
// don't depend on the list having been rendered, and they can never be stale
|
||||
// with respect to a model that changed since the last render (which used to
|
||||
// cause both wrong results and index-out-of-range panics).
|
||||
//
|
||||
// The non-model items are assumed to be ordered by their Index, which is how
|
||||
// all producers build them; the i-th one therefore ends up at view index
|
||||
// Index+i.
|
||||
func modelIndexToViewIndex(listLength int, nonModelItems []*NonModelItem, modelIndex int) int {
|
||||
modelIndex = lo.Clamp(modelIndex, 0, listLength)
|
||||
// Each non-model item inserted at or before this model item pushes it down
|
||||
// by one row in the view.
|
||||
viewIndex := modelIndex
|
||||
for _, item := range nonModelItems {
|
||||
if item.Index <= modelIndex {
|
||||
viewIndex++
|
||||
}
|
||||
}
|
||||
return viewIndex
|
||||
}
|
||||
|
||||
func viewIndexToModelIndex(listLength int, nonModelItems []*NonModelItem, viewIndex int) int {
|
||||
viewIndex = lo.Clamp(viewIndex, 0, listLength+len(nonModelItems))
|
||||
// Subtract the non-model items that appear before this view index.
|
||||
modelIndex := viewIndex
|
||||
for i, item := range nonModelItems {
|
||||
if item.Index+i < viewIndex {
|
||||
modelIndex--
|
||||
}
|
||||
}
|
||||
return modelIndex
|
||||
}
|
||||
|
||||
func (self *ListRenderer) ColumnPositions() []int {
|
||||
return self.columnPositions
|
||||
}
|
||||
|
|
@ -71,23 +113,18 @@ func (self *ListRenderer) renderLines(startIdx int, endIdx int) string {
|
|||
if self.getColumnAlignments != nil {
|
||||
columnAlignments = self.getColumnAlignments()
|
||||
}
|
||||
nonModelItems := []*NonModelItem{}
|
||||
self.numNonModelItems = 0
|
||||
if self.getNonModelItems != nil {
|
||||
nonModelItems = self.getNonModelItems()
|
||||
self.prepareConversionArrays(nonModelItems)
|
||||
}
|
||||
nonModelItems := self.getNonModelItemList()
|
||||
startModelIdx := 0
|
||||
if startIdx == -1 {
|
||||
startIdx = 0
|
||||
} else {
|
||||
startModelIdx = self.ViewIndexToModelIndex(startIdx)
|
||||
startModelIdx = viewIndexToModelIndex(self.list.Len(), nonModelItems, startIdx)
|
||||
}
|
||||
endModelIdx := self.list.Len()
|
||||
if endIdx == -1 {
|
||||
endIdx = endModelIdx + len(nonModelItems)
|
||||
} else {
|
||||
endModelIdx = self.ViewIndexToModelIndex(endIdx)
|
||||
endModelIdx = viewIndexToModelIndex(self.list.Len(), nonModelItems, endIdx)
|
||||
}
|
||||
lines, columnPositions := utils.RenderDisplayStrings(
|
||||
self.getDisplayStrings(startModelIdx, endModelIdx),
|
||||
|
|
@ -97,23 +134,6 @@ func (self *ListRenderer) renderLines(startIdx int, endIdx int) string {
|
|||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func (self *ListRenderer) prepareConversionArrays(nonModelItems []*NonModelItem) {
|
||||
self.numNonModelItems = len(nonModelItems)
|
||||
viewIndicesByModelIndex := lo.Range(self.list.Len() + 1)
|
||||
modelIndicesByViewIndex := lo.Range(self.list.Len() + 1)
|
||||
offset := 0
|
||||
for _, item := range nonModelItems {
|
||||
for i := item.Index; i <= self.list.Len(); i++ {
|
||||
viewIndicesByModelIndex[i]++
|
||||
}
|
||||
modelIndicesByViewIndex = slices.Insert(
|
||||
modelIndicesByViewIndex, item.Index+offset, modelIndicesByViewIndex[item.Index+offset])
|
||||
offset++
|
||||
}
|
||||
self.viewIndicesByModelIndex = viewIndicesByModelIndex
|
||||
self.modelIndicesByViewIndex = modelIndicesByViewIndex
|
||||
}
|
||||
|
||||
func (self *ListRenderer) insertNonModelItems(
|
||||
nonModelItems []*NonModelItem, endIdx int, startIdx int, lines []string, columnPositions []int,
|
||||
) []string {
|
||||
|
|
|
|||
|
|
@ -254,9 +254,6 @@ func TestListRenderer_ModelIndexToViewIndex_and_back(t *testing.T) {
|
|||
getNonModelItems: getNonModelItems,
|
||||
}
|
||||
|
||||
// Need to render first so that it knows the non-model items
|
||||
self.renderLines(-1, -1)
|
||||
|
||||
for i := range len(s.modelIndices) {
|
||||
assert.Equal(t, s.expectedViewIndices[i], self.ModelIndexToViewIndex(s.modelIndices[i]))
|
||||
}
|
||||
|
|
@ -267,3 +264,27 @@ func TestListRenderer_ModelIndexToViewIndex_and_back(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The index conversions must not depend on the list having been rendered
|
||||
// first. It used to be renderLines that populated the conversion arrays, so
|
||||
// converting an index before the first render silently ignored the non-model
|
||||
// items (and converting after the model changed used a stale snapshot).
|
||||
func TestListRenderer_IndexConversionsAreRenderIndependent(t *testing.T) {
|
||||
modelInts := lo.Map(lo.Range(3), func(i int, _ int) myint { return myint(i) })
|
||||
self := &ListRenderer{
|
||||
list: NewListViewModel(func() []myint { return modelInts }),
|
||||
getDisplayStrings: func(startIdx int, endIdx int) [][]string {
|
||||
return lo.Map(modelInts[startIdx:endIdx],
|
||||
func(i myint, _ int) []string { return []string{fmt.Sprint(i)} })
|
||||
},
|
||||
// A section header sits at model index 1, so model item 1 is pushed down
|
||||
// to view index 2, and view index 2 maps back to model item 1.
|
||||
getNonModelItems: func() []*NonModelItem {
|
||||
return []*NonModelItem{{Index: 1, Content: "--- header ---"}}
|
||||
},
|
||||
}
|
||||
|
||||
// Deliberately convert without rendering first.
|
||||
assert.Equal(t, 2, self.ModelIndexToViewIndex(1))
|
||||
assert.Equal(t, 1, self.ViewIndexToModelIndex(2))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
|
|
@ -142,7 +143,9 @@ type LocalCommitsViewModel struct {
|
|||
|
||||
// If this is true we limit the amount of commits we load, for the sake of keeping things fast.
|
||||
// If the user attempts to scroll past the end of the list, we will load more commits.
|
||||
limitCommits bool
|
||||
// Atomic because a checkout or reset sets it from a worker goroutine while the
|
||||
// commits refresh reads it on the UI thread to decide how many commits to load.
|
||||
limitCommits atomic.Bool
|
||||
|
||||
// If this is true we'll use git log --all when fetching the commits.
|
||||
showWholeGitGraph bool
|
||||
|
|
@ -151,9 +154,9 @@ type LocalCommitsViewModel struct {
|
|||
func NewLocalCommitsViewModel(getModel func() []*models.Commit, c *ContextCommon) *LocalCommitsViewModel {
|
||||
self := &LocalCommitsViewModel{
|
||||
ListViewModel: NewListViewModel(getModel),
|
||||
limitCommits: true,
|
||||
showWholeGitGraph: c.UserConfig().Git.Log.ShowWholeGraph,
|
||||
}
|
||||
self.limitCommits.Store(true)
|
||||
|
||||
return self
|
||||
}
|
||||
|
|
@ -221,15 +224,15 @@ func (self *LocalCommitsContext) RefForAdjustingLineNumberInDiff() string {
|
|||
}
|
||||
|
||||
func (self *LocalCommitsContext) ModelSearchResults(searchStr string, caseSensitive bool) []gocui.SearchPosition {
|
||||
return searchModelCommits(caseSensitive, self.GetCommits(), self.ColumnPositions(), self.ModelIndexToViewIndex, searchStr)
|
||||
return searchModelCommits(caseSensitive, self.GetCommits(), self.ColumnPositions(), self.modelToViewIndexConverter(), searchStr)
|
||||
}
|
||||
|
||||
func (self *LocalCommitsViewModel) SetLimitCommits(value bool) {
|
||||
self.limitCommits = value
|
||||
self.limitCommits.Store(value)
|
||||
}
|
||||
|
||||
func (self *LocalCommitsViewModel) GetLimitCommits() bool {
|
||||
return self.limitCommits
|
||||
return self.limitCommits.Load()
|
||||
}
|
||||
|
||||
func (self *LocalCommitsViewModel) SetShowWholeGitGraph(value bool) {
|
||||
|
|
|
|||
|
|
@ -60,8 +60,11 @@ func NewContextTree(c *ContextCommon) *ContextTree {
|
|||
"main",
|
||||
PATCH_BUILDING_MAIN_CONTEXT_KEY,
|
||||
func() []int {
|
||||
filename := commitFilesContext.GetSelectedPath()
|
||||
includedLineIndices, err := c.Git().Patch.PatchBuilder.GetFileIncLineIndices(filename)
|
||||
file := commitFilesContext.GetSelectedFile()
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
includedLineIndices, err := c.Git().Patch.PatchBuilder.GetFileIncLineIndices(file.Path, file.PreviousPath)
|
||||
if err != nil {
|
||||
c.Log.Error(err)
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ func (self *SubCommitsContext) RefForAdjustingLineNumberInDiff() string {
|
|||
}
|
||||
|
||||
func (self *SubCommitsContext) ModelSearchResults(searchStr string, caseSensitive bool) []gocui.SearchPosition {
|
||||
return searchModelCommits(caseSensitive, self.GetCommits(), self.ColumnPositions(), self.ModelIndexToViewIndex, searchStr)
|
||||
return searchModelCommits(caseSensitive, self.GetCommits(), self.ColumnPositions(), self.modelToViewIndexConverter(), searchStr)
|
||||
}
|
||||
|
||||
func (self *SubCommitsContext) IndexForGotoBottom() int {
|
||||
|
|
|
|||
|
|
@ -81,10 +81,17 @@ func (self *SuggestionsContext) SetSuggestions(suggestions []*types.Suggestion)
|
|||
}
|
||||
|
||||
func (self *SuggestionsContext) RefreshSuggestions() {
|
||||
// Capture the suggestions function and the prompt input here, on the UI
|
||||
// thread, rather than inside the worker below: the main thread rewrites both
|
||||
// (State.FindSuggestions and the prompt's TextArea) when it (re)creates a
|
||||
// prompt panel, so reading them from the worker races those writes. It's
|
||||
// also more correct -- we search for the input as it was when dispatched,
|
||||
// which is what this request's AsyncHandler id corresponds to.
|
||||
findSuggestionsFn := self.State.FindSuggestions
|
||||
promptInput := self.c.GetPromptInput()
|
||||
self.State.AsyncHandler.Do(func() func() {
|
||||
findSuggestionsFn := self.State.FindSuggestions
|
||||
if findSuggestionsFn != nil {
|
||||
suggestions := findSuggestionsFn(self.c.GetPromptInput())
|
||||
suggestions := findSuggestionsFn(promptInput)
|
||||
return func() { self.SetSuggestions(suggestions) }
|
||||
}
|
||||
return func() {}
|
||||
|
|
|
|||
|
|
@ -207,18 +207,6 @@ func (gui *Gui) resetHelpersAndControllers() {
|
|||
controllers.AttachControllers(context, searchControllerFactory.Create(context))
|
||||
}
|
||||
|
||||
for _, context := range []controllers.CanViewWorktreeOptions{
|
||||
gui.State.Contexts.LocalCommits,
|
||||
gui.State.Contexts.ReflogCommits,
|
||||
gui.State.Contexts.SubCommits,
|
||||
gui.State.Contexts.Stash,
|
||||
gui.State.Contexts.Branches,
|
||||
gui.State.Contexts.RemoteBranches,
|
||||
gui.State.Contexts.Tags,
|
||||
} {
|
||||
controllers.AttachControllers(context, controllers.NewWorktreeOptionsController(common, context))
|
||||
}
|
||||
|
||||
// allow for navigating between side window contexts
|
||||
for _, context := range []types.Context{
|
||||
gui.State.Contexts.Status,
|
||||
|
|
|
|||
|
|
@ -89,6 +89,12 @@ func (self *BasicCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
|
|||
Description: self.c.Tr.MoveCommitsToNewBranch,
|
||||
Tooltip: self.c.Tr.MoveCommitsToNewBranchTooltip,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Universal.NewWorktree),
|
||||
Handler: self.withItem(self.c.Helpers().Worktree.NewWorktreeMenuForCommit),
|
||||
Description: self.c.Tr.NewWorktree,
|
||||
OpensMenu: true,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Commits.ViewResetOptions),
|
||||
Handler: self.withItem(self.createResetMenu),
|
||||
|
|
|
|||
|
|
@ -274,18 +274,21 @@ func (self *BisectController) afterMark(selectCurrent bool, waitToReselect bool)
|
|||
}
|
||||
|
||||
func (self *BisectController) afterBisectMarkRefresh(selectCurrent bool, waitToReselect bool) error {
|
||||
selectFn := func() {
|
||||
selectFn := func() error {
|
||||
if selectCurrent {
|
||||
self.selectCurrentBisectCommit()
|
||||
}
|
||||
}
|
||||
|
||||
if waitToReselect {
|
||||
self.c.Refresh(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{}, Then: selectFn})
|
||||
return nil
|
||||
}
|
||||
|
||||
selectFn()
|
||||
if waitToReselect {
|
||||
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{}, Then: selectFn})
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := selectFn(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
self.c.Helpers().Bisect.PostBisectCommandRefresh()
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -69,6 +69,12 @@ func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*ty
|
|||
Description: self.c.Tr.MoveCommitsToNewBranch,
|
||||
Tooltip: self.c.Tr.MoveCommitsToNewBranchTooltip,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Universal.NewWorktree),
|
||||
Handler: self.withItem(self.c.Helpers().Worktree.NewWorktreeMenuForBranch),
|
||||
Description: self.c.Tr.NewWorktree,
|
||||
OpensMenu: true,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Branches.CreatePullRequest),
|
||||
Handler: self.withItem(self.handleCreatePullRequest),
|
||||
|
|
@ -325,7 +331,6 @@ func (self *BranchesController) viewUpstreamOptions(selectedBranch *models.Branc
|
|||
return err
|
||||
}
|
||||
self.c.Refresh(types.RefreshOptions{
|
||||
Mode: types.SYNC,
|
||||
Scope: []types.RefreshableView{
|
||||
types.BRANCHES,
|
||||
types.COMMITS,
|
||||
|
|
@ -349,7 +354,6 @@ func (self *BranchesController) viewUpstreamOptions(selectedBranch *models.Branc
|
|||
return err
|
||||
}
|
||||
self.c.Refresh(types.RefreshOptions{
|
||||
Mode: types.SYNC,
|
||||
Scope: []types.RefreshableView{
|
||||
types.BRANCHES,
|
||||
types.COMMITS,
|
||||
|
|
@ -540,7 +544,7 @@ func (self *BranchesController) forceCheckout() error {
|
|||
if err := self.c.Git().Branch.Checkout(branch.Name, git_commands.CheckoutOptions{Force: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
|
||||
self.c.Refresh(types.RefreshOptions{})
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
|
@ -593,11 +597,10 @@ func (self *BranchesController) createNewBranchWithName(newBranchName string) er
|
|||
return err
|
||||
}
|
||||
|
||||
self.c.Helpers().Refs.SelectFirstBranchAndFirstCommit()
|
||||
self.c.Refresh(types.RefreshOptions{
|
||||
Mode: types.ASYNC,
|
||||
KeepBranchSelectionIndex: true,
|
||||
CommitSelection: types.KeepCommitSelectionIndex,
|
||||
BranchSelection: types.SelectCheckedOutBranch,
|
||||
CommitSelection: types.SelectHeadCommit,
|
||||
SelectTopReflogCommit: true,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
|
@ -704,9 +707,9 @@ func (self *BranchesController) fastForward(branch *models.Branch) error {
|
|||
}
|
||||
|
||||
action := self.c.Tr.Actions.FastForwardBranch
|
||||
worktree, ok := self.worktreeForBranch(branch)
|
||||
|
||||
return self.c.WithInlineStatus(branch, types.ItemOperationFastForwarding, context.LOCAL_BRANCHES_CONTEXT_KEY, func(task gocui.Task) error {
|
||||
worktree, ok := self.worktreeForBranch(branch)
|
||||
if ok {
|
||||
self.c.LogAction(action)
|
||||
|
||||
|
|
@ -728,7 +731,7 @@ func (self *BranchesController) fastForward(branch *models.Branch) error {
|
|||
WorktreePath: worktreePath,
|
||||
},
|
||||
)
|
||||
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
|
||||
self.c.RefreshFromWorker(types.RefreshOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -737,7 +740,7 @@ func (self *BranchesController) fastForward(branch *models.Branch) error {
|
|||
err := self.c.Git().Sync.FastForward(
|
||||
task, branch.Name, branch.UpstreamRemote, branch.UpstreamBranch,
|
||||
)
|
||||
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES}})
|
||||
self.c.RefreshFromWorker(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES}})
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
|
@ -754,7 +757,7 @@ func (self *BranchesController) createSortMenu() error {
|
|||
if self.c.UserConfig().Git.LocalBranchSortOrder != sortOrder {
|
||||
self.c.UserConfig().Git.LocalBranchSortOrder = sortOrder
|
||||
self.c.Contexts().Branches.SetSelection(0)
|
||||
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES}})
|
||||
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES}})
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
|
|
@ -777,20 +780,24 @@ func (self *BranchesController) rename(branch *models.Branch) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// need to find where the branch is now so that we can re-select it. That means we need to refetch the branches synchronously and then find our branch
|
||||
// need to find where the branch is now so that we can re-select it. That means we need to
|
||||
// refetch the branches and then find our branch. The branches model update is bounced
|
||||
// onto the UI thread, so the re-selection (which reads Model.Branches) has to run in
|
||||
// Then; reading it inline here would see the previous model.
|
||||
self.c.Refresh(types.RefreshOptions{
|
||||
Mode: types.SYNC,
|
||||
Scope: []types.RefreshableView{types.BRANCHES, types.WORKTREES},
|
||||
Then: func() error {
|
||||
// now that we've got our stuff again we need to find that branch and reselect it.
|
||||
for i, newBranch := range self.c.Model().Branches {
|
||||
if newBranch.Name == newBranchName {
|
||||
self.context().SetSelection(i)
|
||||
self.context().HandleRender()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
// now that we've got our stuff again we need to find that branch and reselect it.
|
||||
for i, newBranch := range self.c.Model().Branches {
|
||||
if newBranch.Name == newBranchName {
|
||||
self.context().SetSelection(i)
|
||||
self.context().HandleRender()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -191,11 +191,11 @@ func (self *CommitFilesController) GetOnRenderToMain() func() {
|
|||
}
|
||||
}
|
||||
|
||||
func (self *CommitFilesController) copyDiffToClipboard(path string, toastMessage string) error {
|
||||
func (self *CommitFilesController) copyDiffToClipboard(paths []string, toastMessage string) error {
|
||||
from, to := self.context().GetFromAndToForDiff()
|
||||
from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(from)
|
||||
|
||||
cmdObj := self.c.Git().WorkingTree.ShowFileDiffCmdObj(from, to, reverse, []string{path}, true)
|
||||
cmdObj := self.c.Git().WorkingTree.ShowFileDiffCmdObj(from, to, reverse, paths, true)
|
||||
diff, err := cmdObj.RunWithOutput()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -263,7 +263,7 @@ func (self *CommitFilesController) openCopyMenu() error {
|
|||
copyFileDiffItem := &types.MenuItem{
|
||||
Label: self.c.Tr.CopySelectedDiff,
|
||||
OnPress: func() error {
|
||||
return self.copyDiffToClipboard(node.GetPath(), self.c.Tr.FileDiffCopiedToast)
|
||||
return self.copyDiffToClipboard(self.pathsForDiff(node), self.c.Tr.FileDiffCopiedToast)
|
||||
},
|
||||
DisabledReason: self.require(self.singleItemSelected())(),
|
||||
Keys: menuKey('s'),
|
||||
|
|
@ -271,7 +271,7 @@ func (self *CommitFilesController) openCopyMenu() error {
|
|||
copyAllDiff := &types.MenuItem{
|
||||
Label: self.c.Tr.CopyAllFilesDiff,
|
||||
OnPress: func() error {
|
||||
return self.copyDiffToClipboard(".", self.c.Tr.AllFilesDiffCopiedToast)
|
||||
return self.copyDiffToClipboard([]string{"."}, self.c.Tr.AllFilesDiffCopiedToast)
|
||||
},
|
||||
DisabledReason: self.require(self.itemsSelected())(),
|
||||
Keys: menuKey('a'),
|
||||
|
|
@ -324,7 +324,7 @@ func (self *CommitFilesController) checkout(node *filetree.CommitFileNode) error
|
|||
return err
|
||||
}
|
||||
|
||||
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
|
||||
self.c.Refresh(types.RefreshOptions{})
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -337,7 +337,9 @@ func (self *CommitFilesController) discard(selectedNodes []*filetree.CommitFileN
|
|||
Title: self.c.Tr.DiscardFileChangesTitle,
|
||||
Prompt: prompt,
|
||||
HandleConfirm: func() error {
|
||||
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(gocui.Task) error {
|
||||
commits := self.c.Model().Commits
|
||||
selectedLineIdx := self.c.Contexts().LocalCommits.GetSelectedLineIdx()
|
||||
return self.c.WithWaitingStatusBlockingInput(self.c.Tr.RebasingStatus, func(gocui.Task) error {
|
||||
var filePaths []string
|
||||
selectedNodes = normalisedSelectedCommitFileNodes(selectedNodes)
|
||||
|
||||
|
|
@ -348,19 +350,25 @@ func (self *CommitFilesController) discard(selectedNodes []*filetree.CommitFileN
|
|||
|
||||
for _, node := range selectedNodes {
|
||||
_ = node.ForEachFile(func(file *models.CommitFile) error {
|
||||
filePaths = append(filePaths, file.GetPath())
|
||||
// For a rename we discard both the new and the old path,
|
||||
// so that the new file is removed and the old one is
|
||||
// restored.
|
||||
filePaths = append(filePaths, file.Names()...)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
err := self.c.Git().Rebase.DiscardOldFileChanges(self.c.Model().Commits, self.c.Contexts().LocalCommits.GetSelectedLineIdx(), filePaths)
|
||||
err := self.c.Git().Rebase.DiscardOldFileChanges(commits, selectedLineIdx, filePaths)
|
||||
if err := self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if self.context().RangeSelectEnabled() {
|
||||
self.context().GetList().CancelRangeSelect()
|
||||
}
|
||||
self.c.OnUIThread(func() error {
|
||||
if self.context().RangeSelectEnabled() {
|
||||
self.context().GetList().CancelRangeSelect()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
})
|
||||
|
|
@ -439,20 +447,16 @@ func (self *CommitFilesController) toggleForPatch(selectedNodes []*filetree.Comm
|
|||
self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView)
|
||||
}
|
||||
|
||||
refName := self.context().GetRef().RefName()
|
||||
|
||||
toggle := func() error {
|
||||
return self.c.WithWaitingStatus(self.c.Tr.UpdatingPatch, func(gocui.Task) error {
|
||||
if !self.c.Git().Patch.PatchBuilder.Active() {
|
||||
if err := self.startPatchBuilder(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
selectedNodes = normalisedSelectedCommitFileNodes(selectedNodes)
|
||||
|
||||
// Find if any file in the selection is unselected or partially added
|
||||
adding := lo.SomeBy(selectedNodes, func(node *filetree.CommitFileNode) bool {
|
||||
return node.SomeFile(func(file *models.CommitFile) bool {
|
||||
fileStatus := self.c.Git().Patch.PatchBuilder.GetFileStatus(file.Path, self.context().GetRef().RefName())
|
||||
fileStatus := self.c.Git().Patch.PatchBuilder.GetFileStatus(file.Path, refName)
|
||||
return fileStatus == patch.PART || fileStatus == patch.UNSELECTED
|
||||
})
|
||||
})
|
||||
|
|
@ -465,7 +469,7 @@ func (self *CommitFilesController) toggleForPatch(selectedNodes []*filetree.Comm
|
|||
|
||||
for _, node := range selectedNodes {
|
||||
err := node.ForEachFile(func(file *models.CommitFile) error {
|
||||
return patchOperationFunction(file.Path)
|
||||
return patchOperationFunction(file.Path, file.PreviousPath)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -495,6 +499,12 @@ func (self *CommitFilesController) toggleForPatch(selectedNodes []*filetree.Comm
|
|||
self.c.Git().Patch.PatchBuilder.Reset()
|
||||
}
|
||||
|
||||
if !self.c.Git().Patch.PatchBuilder.Active() {
|
||||
if err := self.startPatchBuilder(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return toggle()
|
||||
},
|
||||
})
|
||||
|
|
@ -610,11 +620,16 @@ func (self *CommitFilesController) pathsForDiff(node *filetree.CommitFileNode) [
|
|||
if !node.IsFile() && self.context().IsFiltering() {
|
||||
var paths []string
|
||||
_ = node.ForEachFile(func(file *models.CommitFile) error {
|
||||
paths = append(paths, file.Path)
|
||||
// For a rename we need to pass both paths so that git detects it as
|
||||
// a rename rather than an unrelated delete and add.
|
||||
paths = append(paths, file.Names()...)
|
||||
return nil
|
||||
})
|
||||
return paths
|
||||
}
|
||||
if file := node.GetFile(); file != nil {
|
||||
return file.Names()
|
||||
}
|
||||
return []string{node.GetPath()}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -132,10 +132,11 @@ func (self *CustomPatchOptionsMenuAction) returnFocusFromPatchExplorerIfNecessar
|
|||
func (self *CustomPatchOptionsMenuAction) handleDeletePatchFromCommit() error {
|
||||
self.returnFocusFromPatchExplorerIfNecessary()
|
||||
|
||||
commits := self.c.Model().Commits
|
||||
commitIndex := self.getPatchCommitIndex()
|
||||
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(gocui.Task) error {
|
||||
commitIndex := self.getPatchCommitIndex()
|
||||
self.c.LogAction(self.c.Tr.Actions.RemovePatchFromCommit)
|
||||
err := self.c.Git().Patch.DeletePatchesFromCommit(self.c.Model().Commits, commitIndex)
|
||||
err := self.c.Git().Patch.DeletePatchesFromCommit(commits, commitIndex)
|
||||
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err)
|
||||
})
|
||||
}
|
||||
|
|
@ -143,10 +144,12 @@ func (self *CustomPatchOptionsMenuAction) handleDeletePatchFromCommit() error {
|
|||
func (self *CustomPatchOptionsMenuAction) handleMovePatchToSelectedCommit() error {
|
||||
self.returnFocusFromPatchExplorerIfNecessary()
|
||||
|
||||
commits := self.c.Model().Commits
|
||||
commitIndex := self.getPatchCommitIndex()
|
||||
toCommitIndex := self.c.Contexts().LocalCommits.GetSelectedLineIdx()
|
||||
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(gocui.Task) error {
|
||||
commitIndex := self.getPatchCommitIndex()
|
||||
self.c.LogAction(self.c.Tr.Actions.MovePatchToSelectedCommit)
|
||||
err := self.c.Git().Patch.MovePatchToSelectedCommit(self.c.Model().Commits, commitIndex, self.c.Contexts().LocalCommits.GetSelectedLineIdx())
|
||||
err := self.c.Git().Patch.MovePatchToSelectedCommit(commits, commitIndex, toCommitIndex)
|
||||
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err)
|
||||
})
|
||||
}
|
||||
|
|
@ -159,10 +162,11 @@ func (self *CustomPatchOptionsMenuAction) handleMovePatchIntoWorkingTree() error
|
|||
Title: self.c.Tr.MustStashTitle,
|
||||
Prompt: self.c.Tr.MustStashWarning,
|
||||
HandleConfirm: func() error {
|
||||
commits := self.c.Model().Commits
|
||||
commitIndex := self.getPatchCommitIndex()
|
||||
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(gocui.Task) error {
|
||||
commitIndex := self.getPatchCommitIndex()
|
||||
self.c.LogAction(self.c.Tr.Actions.MovePatchIntoIndex)
|
||||
err := self.c.Git().Patch.MovePatchIntoIndex(self.c.Model().Commits, commitIndex, mustStash)
|
||||
err := self.c.Git().Patch.MovePatchIntoIndex(commits, commitIndex, mustStash)
|
||||
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err)
|
||||
})
|
||||
},
|
||||
|
|
@ -183,14 +187,18 @@ func (self *CustomPatchOptionsMenuAction) handlePullPatchIntoNewCommit() error {
|
|||
DescriptionTitle: self.c.Tr.CommitDescriptionTitle,
|
||||
PreserveMessage: false,
|
||||
OnConfirm: func(summary string, description string) error {
|
||||
commits := self.c.Model().Commits
|
||||
self.c.Helpers().Commits.CloseCommitMessagePanel()
|
||||
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(gocui.Task) error {
|
||||
self.c.Helpers().Commits.CloseCommitMessagePanel()
|
||||
self.c.LogAction(self.c.Tr.Actions.MovePatchIntoNewCommit)
|
||||
err := self.c.Git().Patch.PullPatchIntoNewCommit(self.c.Model().Commits, commitIndex, summary, description)
|
||||
err := self.c.Git().Patch.PullPatchIntoNewCommit(commits, commitIndex, summary, description)
|
||||
if err := self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err); err != nil {
|
||||
return err
|
||||
}
|
||||
self.c.Context().Push(self.c.Contexts().LocalCommits, types.OnFocusOpts{})
|
||||
self.c.OnUIThread(func() error {
|
||||
self.c.Context().Push(self.c.Contexts().LocalCommits, types.OnFocusOpts{})
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
})
|
||||
},
|
||||
|
|
@ -214,14 +222,18 @@ func (self *CustomPatchOptionsMenuAction) handlePullPatchIntoNewCommitBefore() e
|
|||
DescriptionTitle: self.c.Tr.CommitDescriptionTitle,
|
||||
PreserveMessage: false,
|
||||
OnConfirm: func(summary string, description string) error {
|
||||
commits := self.c.Model().Commits
|
||||
self.c.Helpers().Commits.CloseCommitMessagePanel()
|
||||
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(gocui.Task) error {
|
||||
self.c.Helpers().Commits.CloseCommitMessagePanel()
|
||||
self.c.LogAction(self.c.Tr.Actions.MovePatchIntoNewCommit)
|
||||
err := self.c.Git().Patch.PullPatchIntoNewCommitBefore(self.c.Model().Commits, commitIndex, summary, description)
|
||||
err := self.c.Git().Patch.PullPatchIntoNewCommitBefore(commits, commitIndex, summary, description)
|
||||
if err := self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err); err != nil {
|
||||
return err
|
||||
}
|
||||
self.c.Context().Push(self.c.Contexts().LocalCommits, types.OnFocusOpts{})
|
||||
self.c.OnUIThread(func() error {
|
||||
self.c.Context().Push(self.c.Contexts().LocalCommits, types.OnFocusOpts{})
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
})
|
||||
},
|
||||
|
|
@ -257,7 +269,7 @@ func (self *CustomPatchOptionsMenuAction) handleApplyPatch(reverse bool) error {
|
|||
return err
|
||||
}
|
||||
|
||||
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
|
||||
self.c.Refresh(types.RefreshOptions{})
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ func (self *DiffingMenuAction) Call() error {
|
|||
OnPress: func() error {
|
||||
self.c.Modes().Diffing.Ref = name
|
||||
// can scope this down based on current view but too lazy right now
|
||||
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
|
||||
self.c.Refresh(types.RefreshOptions{})
|
||||
return nil
|
||||
},
|
||||
},
|
||||
|
|
@ -38,7 +38,7 @@ func (self *DiffingMenuAction) Call() error {
|
|||
FindSuggestionsFunc: self.c.Helpers().Suggestions.GetRefsSuggestionsFunc(),
|
||||
HandleConfirm: func(response string) error {
|
||||
self.c.Modes().Diffing.Ref = response
|
||||
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
|
||||
self.c.Refresh(types.RefreshOptions{})
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
|
@ -54,7 +54,7 @@ func (self *DiffingMenuAction) Call() error {
|
|||
Label: self.c.Tr.SwapDiff,
|
||||
OnPress: func() error {
|
||||
self.c.Modes().Diffing.Reverse = !self.c.Modes().Diffing.Reverse
|
||||
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
|
||||
self.c.Refresh(types.RefreshOptions{})
|
||||
return nil
|
||||
},
|
||||
},
|
||||
|
|
@ -62,7 +62,7 @@ func (self *DiffingMenuAction) Call() error {
|
|||
Label: self.c.Tr.ExitDiffMode,
|
||||
OnPress: func() error {
|
||||
self.c.Modes().Diffing = diffing.New()
|
||||
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
|
||||
self.c.Refresh(types.RefreshOptions{})
|
||||
return nil
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types
|
|||
{
|
||||
Keys: opts.GetKeys(opts.Config.Universal.Select),
|
||||
Handler: self.withItems(self.press),
|
||||
GetDisabledReason: self.require(self.withFileTreeViewModelMutex(self.itemsSelected(self.canStageSelection))),
|
||||
GetDisabledReason: self.require(self.itemsSelected(self.canStageSelection)),
|
||||
Description: self.c.Tr.Stage,
|
||||
Tooltip: self.c.Tr.StageTooltip,
|
||||
DisplayOnScreen: true,
|
||||
|
|
@ -91,7 +91,7 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types
|
|||
{
|
||||
Keys: opts.GetKeys(opts.Config.Universal.Edit),
|
||||
Handler: self.withItems(self.edit),
|
||||
GetDisabledReason: self.require(self.withFileTreeViewModelMutex(self.itemsSelected(self.canEditFiles))),
|
||||
GetDisabledReason: self.require(self.itemsSelected(self.canEditFiles)),
|
||||
Description: self.c.Tr.Edit,
|
||||
Tooltip: self.c.Tr.EditFileTooltip,
|
||||
DisplayOnScreen: true,
|
||||
|
|
@ -145,7 +145,7 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types
|
|||
{
|
||||
Keys: opts.GetKeys(opts.Config.Universal.Remove),
|
||||
Handler: self.withItems(self.remove),
|
||||
GetDisabledReason: self.withFileTreeViewModelMutex(self.require(self.itemsSelected(self.canRemove))),
|
||||
GetDisabledReason: self.require(self.itemsSelected(self.canRemove)),
|
||||
Description: self.c.Tr.Discard,
|
||||
Tooltip: self.c.Tr.DiscardFileChangesTooltip,
|
||||
OpensMenu: true,
|
||||
|
|
@ -182,7 +182,7 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types
|
|||
Handler: self.withItems(self.openMergeConflictMenu),
|
||||
Description: self.c.Tr.ViewMergeConflictOptions,
|
||||
Tooltip: self.c.Tr.ViewMergeConflictOptionsTooltip,
|
||||
GetDisabledReason: self.require(self.withFileTreeViewModelMutex(self.itemsSelected(self.canOpenMergeConflictMenu))),
|
||||
GetDisabledReason: self.require(self.itemsSelected(self.canOpenMergeConflictMenu)),
|
||||
OpensMenu: true,
|
||||
DisplayOnScreen: true,
|
||||
},
|
||||
|
|
@ -209,15 +209,6 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types
|
|||
}
|
||||
}
|
||||
|
||||
func (self *FilesController) withFileTreeViewModelMutex(callback func() *types.DisabledReason) func() *types.DisabledReason {
|
||||
return func() *types.DisabledReason {
|
||||
self.c.Contexts().Files.FileTreeViewModel.RWMutex.RLock()
|
||||
defer self.c.Contexts().Files.FileTreeViewModel.RWMutex.RUnlock()
|
||||
|
||||
return callback()
|
||||
}
|
||||
}
|
||||
|
||||
func (self *FilesController) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding {
|
||||
return []*gocui.ViewMouseBinding{
|
||||
{
|
||||
|
|
@ -574,11 +565,6 @@ func (self *FilesController) toggleStaged(
|
|||
}
|
||||
|
||||
func (self *FilesController) pressWithLock(selectedNodes []*filetree.FileNode) error {
|
||||
// Obtaining this lock because optimistic rendering requires us to mutate
|
||||
// the files in our model.
|
||||
self.c.Mutexes().RefreshingFilesMutex.Lock()
|
||||
defer self.c.Mutexes().RefreshingFilesMutex.Unlock()
|
||||
|
||||
// When filtering, expand directory nodes to individual visible file paths
|
||||
// so that only filtered files are staged/unstaged.
|
||||
toPaths := func(nodes []*filetree.FileNode) []string {
|
||||
|
|
@ -650,7 +636,7 @@ func (self *FilesController) press(nodes []*filetree.FileNode) error {
|
|||
return err
|
||||
}
|
||||
|
||||
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}, Mode: types.ASYNC})
|
||||
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}})
|
||||
|
||||
self.context().HandleFocus(types.OnFocusOpts{})
|
||||
return nil
|
||||
|
|
@ -935,16 +921,13 @@ func (self *FilesController) toggleStagedAll() error {
|
|||
return err
|
||||
}
|
||||
|
||||
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}, Mode: types.ASYNC})
|
||||
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}})
|
||||
|
||||
self.context().HandleFocus(types.OnFocusOpts{})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *FilesController) toggleStagedAllWithLock() error {
|
||||
self.c.Mutexes().RefreshingFilesMutex.Lock()
|
||||
defer self.c.Mutexes().RefreshingFilesMutex.Unlock()
|
||||
|
||||
root := self.context().FileTreeViewModel.GetRoot()
|
||||
|
||||
stage := func(unstagedNodes []*filetree.FileNode) error {
|
||||
|
|
@ -1221,7 +1204,7 @@ func (self *FilesController) setStatusFiltering(filter filetree.FileTreeDisplayF
|
|||
// Whenever we switch between untracked and other filters, we need to refresh the files view
|
||||
// because the untracked files filter applies when running `git status`.
|
||||
if previousFilter != filter && (previousFilter == filetree.DisplayUntracked || filter == filetree.DisplayUntracked) {
|
||||
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}, Mode: types.ASYNC})
|
||||
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}})
|
||||
} else {
|
||||
self.c.PostRefreshUpdate(self.context())
|
||||
}
|
||||
|
|
@ -1544,6 +1527,7 @@ func (self *FilesController) onClickMain(opts gocui.ViewMouseBindingOpts) error
|
|||
}
|
||||
|
||||
func (self *FilesController) fetch() error {
|
||||
fetchGeneration := self.c.State().GetRepoGeneration()
|
||||
return self.c.WithWaitingStatus(self.c.Tr.FetchingStatus, func(task gocui.Task) error {
|
||||
self.c.LogAction("Fetch")
|
||||
err := self.c.Git().Sync.Fetch(task)
|
||||
|
|
@ -1552,7 +1536,7 @@ func (self *FilesController) fetch() error {
|
|||
return errors.New(self.c.Tr.PassUnameWrong)
|
||||
}
|
||||
|
||||
return self.c.Helpers().BranchesHelper.PostFetchRefresh(err, false)
|
||||
return self.c.Helpers().BranchesHelper.PostFetchRefresh(err, false, fetchGeneration)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -1757,7 +1741,7 @@ func (self *FilesController) remove(selectedNodes []*filetree.FileNode) error {
|
|||
return err
|
||||
}
|
||||
|
||||
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES, types.WORKTREES}})
|
||||
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES, types.WORKTREES}})
|
||||
return nil
|
||||
},
|
||||
Keys: self.c.KeybindingsOpts().GetKeys(self.c.UserConfig().Keybinding.Files.ConfirmDiscard),
|
||||
|
|
@ -1783,7 +1767,7 @@ func (self *FilesController) remove(selectedNodes []*filetree.FileNode) error {
|
|||
return err
|
||||
}
|
||||
|
||||
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES, types.WORKTREES}})
|
||||
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES, types.WORKTREES}})
|
||||
return nil
|
||||
},
|
||||
Keys: menuKey('u'),
|
||||
|
|
@ -1808,10 +1792,10 @@ func (self *FilesController) remove(selectedNodes []*filetree.FileNode) error {
|
|||
}
|
||||
|
||||
func (self *FilesController) ResetSubmodule(submodule *models.SubmoduleConfig) error {
|
||||
file := self.c.Helpers().WorkingTree.FileForSubmodule(submodule)
|
||||
return self.c.WithWaitingStatus(self.c.Tr.ResettingSubmoduleStatus, func(gocui.Task) error {
|
||||
self.c.LogAction(self.c.Tr.Actions.ResetSubmodule)
|
||||
|
||||
file := self.c.Helpers().WorkingTree.FileForSubmodule(submodule)
|
||||
if file != nil {
|
||||
if err := self.c.Git().WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil {
|
||||
return err
|
||||
|
|
@ -1825,7 +1809,7 @@ func (self *FilesController) ResetSubmodule(submodule *models.SubmoduleConfig) e
|
|||
return err
|
||||
}
|
||||
|
||||
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES, types.SUBMODULES}})
|
||||
self.c.RefreshFromWorker(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES, types.SUBMODULES}})
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,9 +122,10 @@ func (self *FilteringMenuAction) setFiltering() error {
|
|||
|
||||
self.c.Context().Push(self.c.Contexts().LocalCommits, types.OnFocusOpts{})
|
||||
|
||||
self.c.Refresh(types.RefreshOptions{Scope: helpers.ScopesToRefreshWhenFilteringModeChanges(), Then: func() {
|
||||
self.c.Refresh(types.RefreshOptions{Scope: helpers.ScopesToRefreshWhenFilteringModeChanges(), Then: func() error {
|
||||
self.c.Contexts().LocalCommits.SetSelection(0)
|
||||
self.c.Contexts().LocalCommits.HandleFocus(types.OnFocusOpts{})
|
||||
return nil
|
||||
}})
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ func (self *GlobalController) createCustomPatchOptionsMenu() error {
|
|||
}
|
||||
|
||||
func (self *GlobalController) refresh() error {
|
||||
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
|
||||
self.c.Refresh(types.RefreshOptions{})
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -65,6 +65,10 @@ func (self *AppStatusHelper) WithWaitingStatus(message string, f func(gocui.Task
|
|||
})
|
||||
}
|
||||
|
||||
// WithWaitingStatusImpl is WithWaitingStatus for callers that already run on a
|
||||
// goroutine of their own (e.g. the auto-fetch poller) rather than wanting the
|
||||
// work dispatched to a worker. task is used to hide the status while the task
|
||||
// is paused; it may be nil for callers whose f ignores its task.
|
||||
func (self *AppStatusHelper) WithWaitingStatusImpl(message string, f func(gocui.Task) error, task gocui.Task) error {
|
||||
// A waiting status means lazygit is driving a git operation itself (often
|
||||
// one that internally runs a rebase and continues it). Pause the background
|
||||
|
|
@ -78,16 +82,33 @@ func (self *AppStatusHelper) WithWaitingStatusImpl(message string, f func(gocui.
|
|||
})
|
||||
}
|
||||
|
||||
func (self *AppStatusHelper) WithWaitingStatusSync(message string, f func() error) error {
|
||||
self.c.PauseBackgroundRefreshes(true)
|
||||
defer self.c.PauseBackgroundRefreshes(false)
|
||||
|
||||
return self.statusMgr().WithWaitingStatus(message, func() {}, func(*status.WaitingStatusHandle) error {
|
||||
stop := make(chan struct{})
|
||||
defer func() { close(stop) }()
|
||||
self.renderAppStatusSync(stop)
|
||||
|
||||
return f()
|
||||
// WithWaitingStatusBlockingInput is like WithWaitingStatus, but it also blocks
|
||||
// keyboard input for the whole duration of the operation: keys the user presses
|
||||
// while it runs are buffered and replayed against the post-operation state (see
|
||||
// gocui.BeginBlockingEvents). Use it for operations that manipulate an
|
||||
// in-progress rebase or otherwise rewrite commits, where a racing keypress
|
||||
// would target the wrong commit or todo.
|
||||
//
|
||||
// Must be called on the UI thread: the block is begun synchronously here, before
|
||||
// the operation is dispatched to a worker, so no keypress can slip through in
|
||||
// between.
|
||||
func (self *AppStatusHelper) WithWaitingStatusBlockingInput(message string, f func(gocui.Task) error) {
|
||||
self.c.GocuiGui().BeginBlockingEvents()
|
||||
// Hide the rebasing-mode indicator (and its reset button) while we drive the
|
||||
// rebase ourselves; it reflects the transient on-disk state and would
|
||||
// otherwise flash on for the duration of the operation.
|
||||
self.modeHelper.SetSuppressRebasingMode(true)
|
||||
self.c.OnWorker(func(task gocui.Task) error {
|
||||
// End the block and restore the mode indicator once the operation and its
|
||||
// refresh have applied their UI updates: OnUIThread queues this after the
|
||||
// refresh's model bounces and Then (which RefreshFromWorker has already
|
||||
// enqueued by the time f returns), so the replayed keys act on the
|
||||
// refreshed state and any resulting rebase state shows correctly.
|
||||
defer self.c.OnUIThread(func() error {
|
||||
self.modeHelper.SetSuppressRebasingMode(false)
|
||||
return self.c.GocuiGui().EndBlockingEvents()
|
||||
})
|
||||
return self.WithWaitingStatusImpl(message, f, task)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -100,21 +121,36 @@ func (self *AppStatusHelper) GetStatusString() string {
|
|||
return appStatus
|
||||
}
|
||||
|
||||
// renderAppStatus ensures the render loop that keeps the app-status view up to
|
||||
// date is running. There is one loop for the whole status stack, no matter how
|
||||
// many statuses are showing: it draws whatever the top status currently is,
|
||||
// and exits after drawing a final empty frame once the last status is removed.
|
||||
//
|
||||
// The loop always runs as a background task, regardless of what kind of
|
||||
// operation owns a status: rendering runs no git commands, so it must never
|
||||
// count towards lazygit being busy — otherwise it would block repo switching
|
||||
// for as long as anything is showing (e.g. for the whole duration of a hung
|
||||
// background fetch, or of a toast fading). A foreground operation's busy-ness
|
||||
// is carried by its own worker task, not by the renderer.
|
||||
func (self *AppStatusHelper) renderAppStatus() {
|
||||
self.c.OnWorker(func(_ gocui.Task) error {
|
||||
if !self.statusMgr().ClaimRenderLoop() {
|
||||
return
|
||||
}
|
||||
|
||||
self.c.OnWorkerBackground(func(_ gocui.Task) error {
|
||||
ticker := time.NewTicker(time.Millisecond * time.Duration(self.c.UserConfig().Gui.Spinner.Rate))
|
||||
defer ticker.Stop()
|
||||
prevAppStatus := ""
|
||||
for range ticker.C {
|
||||
appStatus, color := self.statusMgr().GetStatusString(self.c.UserConfig())
|
||||
|
||||
update := self.c.OnUIThreadContentOnly
|
||||
update := self.c.OnUIThreadContentOnlyBackground
|
||||
if utils.StringWidth(appStatus) != utils.StringWidth(prevAppStatus) {
|
||||
// Need a full layout whenever the width of the status string changes. This can't
|
||||
// happen during normal spinning because we validate that all spinner frames have
|
||||
// the same width, so typically this will only be triggered at the beginning and end
|
||||
// of a status, or if the status string changes midway for some reason.
|
||||
update = self.c.OnUIThread
|
||||
update = self.c.OnUIThreadBackground
|
||||
}
|
||||
update(func() error {
|
||||
self.c.Views().AppStatus.FgColor = color
|
||||
|
|
@ -123,64 +159,12 @@ func (self *AppStatusHelper) renderAppStatus() {
|
|||
})
|
||||
prevAppStatus = appStatus
|
||||
|
||||
if appStatus == "" {
|
||||
// Checked after rendering, so that the frame which clears the view
|
||||
// has already been drawn when we exit.
|
||||
if self.statusMgr().ReleaseRenderLoopIfEmpty() {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (self *AppStatusHelper) renderAppStatusSync(stop chan struct{}) {
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Millisecond * time.Duration(self.c.UserConfig().Gui.Spinner.Rate))
|
||||
defer ticker.Stop()
|
||||
|
||||
// Write the status into the view before the first layout below, so that
|
||||
// layout (which sizes the bottom line based on the actual content of the
|
||||
// AppStatus view) leaves room for it and it shows right away. The ticker
|
||||
// only updates the spinner frame using ForceFlushViewsContentOnly, so this
|
||||
// doesn't re-layout.
|
||||
self.setAppStatusContent()
|
||||
|
||||
// Forcing a re-layout and redraw after we added the waiting status;
|
||||
// this is needed in case the gui.showBottomLine config is set to false,
|
||||
// to make sure the bottom line appears. It's also useful for redrawing
|
||||
// once after each of several consecutive keypresses, e.g. pressing
|
||||
// ctrl-j to move a commit down several steps.
|
||||
_ = self.c.GocuiGui().ForceLayoutAndRedraw()
|
||||
|
||||
self.modeHelper.SetSuppressRebasingMode(true)
|
||||
defer func() { self.modeHelper.SetSuppressRebasingMode(false) }()
|
||||
|
||||
outer:
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
self.setAppStatusContent()
|
||||
// Redraw all views of the bottom line:
|
||||
bottomLineViews := []*gocui.View{
|
||||
self.c.Views().AppStatus, self.c.Views().Options, self.c.Views().Information,
|
||||
self.c.Views().StatusSpacer1, self.c.Views().StatusSpacer2,
|
||||
}
|
||||
_ = self.c.GocuiGui().ForceFlushViewsContentOnly(bottomLineViews)
|
||||
case <-stop:
|
||||
// Clear the status from the view and re-layout, otherwise the
|
||||
// stale content would keep layout reserving room for it forever.
|
||||
// The UI thread is free again at this point, so we go through
|
||||
// OnUIThread like the async renderAppStatus does.
|
||||
self.c.OnUIThread(func() error {
|
||||
self.c.SetViewContent(self.c.Views().AppStatus, "")
|
||||
return nil
|
||||
})
|
||||
break outer
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (self *AppStatusHelper) setAppStatusContent() {
|
||||
appStatus, color := self.statusMgr().GetStatusString(self.c.UserConfig())
|
||||
self.c.Views().AppStatus.FgColor = color
|
||||
self.c.SetViewContent(self.c.Views().AppStatus, appStatus)
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue