mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-29 10:36:45 -04:00
Compare commits
299
Commits
@@ -1,7 +0,0 @@
|
||||
[codespell]
|
||||
# Ref: https://github.com/codespell-project/codespell#using-a-config-file
|
||||
skip = .git*,go.sum,*.lock,.codespellrc,vendor,translations,Keybindings_*.md,./pkg/gocui
|
||||
check-hidden = true
|
||||
# camel-cased
|
||||
ignore-regex = (\b[A-Za-z][a-z]*[A-Z]\S+\b|\.edn\b|\S+…|\\nd\b)
|
||||
ignore-words-list = fomrat,inbetween
|
||||
@@ -8,7 +8,7 @@ jobs:
|
||||
check-required-label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: mheap/github-action-required-labels@0ac283b4e65c1fb28ce6079dea5546ceca98ccbe # v5
|
||||
- uses: mheap/github-action-required-labels@23e10fde7e062233401931a0eece796cd9bf3177 # v5
|
||||
with:
|
||||
mode: exactly
|
||||
count: 1
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
- name: Test code
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
path: ~/git-${{matrix.git-version}}
|
||||
key: ${{runner.os}}-git-${{matrix.git-version}}
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
- name: Print git version
|
||||
@@ -130,7 +130,7 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
- name: Build linux binary
|
||||
@@ -157,7 +157,7 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
- name: Check Vendor Directory
|
||||
@@ -183,7 +183,7 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
- name: Check formatting
|
||||
@@ -195,7 +195,7 @@ jobs:
|
||||
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9
|
||||
with:
|
||||
# If you change this, make sure to also update scripts/golangci-lint-shim.sh
|
||||
version: v2.4.0
|
||||
version: v2.12.2
|
||||
upload-coverage:
|
||||
# List all jobs that produce coverage files
|
||||
needs: [unit-tests, integration-tests]
|
||||
@@ -206,7 +206,7 @@ jobs:
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# Codespell configuration is within .codespellrc
|
||||
---
|
||||
name: Codespell
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
codespell:
|
||||
name: Check for spelling errors
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
- name: Annotate locations with typos
|
||||
uses: codespell-project/codespell-problem-matcher@9ba2c57125d4908eade4308f32c4ff814c184633 # v1.2.0
|
||||
- name: Codespell
|
||||
uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # v2.2
|
||||
@@ -160,7 +160,7 @@ jobs:
|
||||
git push origin "refs/tags/$NEW_TAG"
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Generate Sponsors 💖
|
||||
uses: JamesIves/github-sponsors-readme-action@2fd9142e765f755780202122261dc85e78459405 # v1.6.0
|
||||
uses: JamesIves/github-sponsors-readme-action@02650b8cd445fc16dfef73195f9c406dce041623 # v1.6.1
|
||||
with:
|
||||
token: ${{ secrets.SPONSORS_TOKEN }}
|
||||
file: "README.md"
|
||||
|
||||
@@ -99,8 +99,6 @@ linters:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
paths:
|
||||
- vendor/
|
||||
|
||||
@@ -63,7 +63,9 @@ Prefer a fine-grained commit history. Commits should be as small as possible
|
||||
while still being meaningful and self-contained.
|
||||
|
||||
- **Every commit must compile and pass all tests.** No "WIP" commits, no
|
||||
commits that leave the tree broken and rely on a follow-up to fix it.
|
||||
commits that leave the tree broken and rely on a follow-up to fix it. A
|
||||
`fixup!` is not such a follow-up; see "Iterate with `fixup!` commits" for
|
||||
what one may leave broken until it is folded in.
|
||||
- **Every commit must be `gofumpt`-formatted.** Run `just format` before
|
||||
committing.
|
||||
- **Every commit must be lint-clean.** Run `just lint` before committing —
|
||||
@@ -82,11 +84,26 @@ while still being meaningful and self-contained.
|
||||
excuse bundling it in. Before committing, review your diff and split out any
|
||||
hunk that is behavior-preserving (an extraction, a rename, a move) into a
|
||||
preceding commit, by staging hunks or resetting and recommitting in order.
|
||||
- **A preparatory refactor is a new commit only when it prepares something
|
||||
new.** Before adding one, find the commit that introduced the code you are
|
||||
about to restructure. If that commit is on this branch, the refactor is a
|
||||
`fixup!` for it rather than a commit of its own: a branch must never contain
|
||||
a commit whose code a later commit on the same branch tidies up. A prep
|
||||
refactor earns a commit of its own only when the shape it corrects came from
|
||||
before the branch. This holds across a branch stack too — if the commit that
|
||||
introduced the code is in an earlier branch of the stack, the fixup belongs
|
||||
there, and the branches above it get replayed. The one exception is when
|
||||
fixing it there turns out to be unreasonably difficult; ask me what to do
|
||||
rather than deciding to leave the repair at the tip.
|
||||
- **Do not use conventional commits** (no `feat:`/`fix:`/`chore:` prefixes).
|
||||
Match the plain English imperative style of the existing history.
|
||||
- **Wrap message body to 72 characters**. The subject is allowed to go up to 80
|
||||
characters, or even a little more if needed to convey a good single-line
|
||||
summary; the body should be wrapped at 72 exactly, no more, no less.
|
||||
- **End every commit message with the `Co-authored-by:` trailer** naming the
|
||||
model that wrote it, exactly as your harness instructions spell it. Nothing
|
||||
in `just check` catches a missing one, so it has to be part of writing the
|
||||
message rather than something to notice afterwards.
|
||||
|
||||
## Iterate with `fixup!` commits
|
||||
|
||||
@@ -105,6 +122,46 @@ separate, reviewable commit that the user decides when to fold in. A bare
|
||||
`--amend` rewrites the commit on the spot and skips that checkpoint. Don't
|
||||
treat "I'm only touching the tip commit" as an exception.
|
||||
|
||||
Always use `fixup!` or `amend!` commits, never amend changes directly, even if
|
||||
you naturally would because "the branch isn't pushed yet". The user always wants
|
||||
to review what you changed, so make this transparent; no exceptions.
|
||||
|
||||
**When the tip is the wrong place for a fixup, insert it mid-branch.**
|
||||
Committing a fixup at the tip of the branch only works while the code it
|
||||
touches still looks the same there; once later commits have rewritten that
|
||||
code — or the target has since been split — the fixup won't apply, and
|
||||
rewriting the later commits to accommodate it defeats the point. Check out the
|
||||
target, make the change, `git commit --fixup=<target>`, then
|
||||
`git rebase --onto <the fixup> <target> <branch>` to replay the rest of the
|
||||
branch. The fixup stays a separate, reviewable commit; only its position
|
||||
changes.
|
||||
|
||||
**A fixup may leave commits before it broken until it is folded in.** If a
|
||||
`fixup!` on an early commit deletes something that a later commit still uses,
|
||||
the later commit doesn't build until its own `fixup!`, right behind it, catches
|
||||
up; the same goes for lint. That is expected. The rules above about every
|
||||
commit compiling, testing and linting clean describe the history _after_
|
||||
autosquash, and I fold fixups in soon after reviewing them. Never amend a
|
||||
commit directly, or edit the commits between two fixups, to keep every commit
|
||||
of the un-squashed history green. The reviewable fixup is worth more than a
|
||||
green intermediate state. Verify at each fixup instead, since the tree there
|
||||
is what the folded-in history will have at that point, and say in the handoff
|
||||
which commits stay broken until which fixup.
|
||||
|
||||
**After a mid-stack fixup, check every branch tip above it, not just the stack
|
||||
tip.** A fixup that deletes or renames something rewrites every commit replayed
|
||||
above it, and a commit further up can hide the damage at the tip. A helper
|
||||
whose last caller the fixup deleted is flagged as unused by `just lint` at the
|
||||
tip of its own PR, but a later PR that calls it again makes the stack tip lint
|
||||
clean. Each PR is reviewed and merged on its own, so each PR branch tip has to
|
||||
be green on its own. After the replay, run `just build`, `just unit-test` and
|
||||
`just lint` at each branch tip from the insertion point up. If the fixup deleted
|
||||
or renamed a symbol, also build every replayed commit, for example with
|
||||
`git -c rebase.autosquash=false rebase -x 'go build ./...' <insertion point>`;
|
||||
unchanged commits are fast-forwarded, so their hashes stay, and the commits a
|
||||
fixup is expected to leave broken stop it, so `git rebase --continue` past
|
||||
those.
|
||||
|
||||
If the changes don't map cleanly onto existing commits — say they cut
|
||||
across several of them, or restructure something at a different layer
|
||||
than any existing commit naturally owns — stop and ask the user how to
|
||||
@@ -151,7 +208,7 @@ looks messy. The whole point of a fixup is that the iteration stays
|
||||
**visible and reviewable**; squashing it away yourself destroys exactly the
|
||||
artifact it exists to create. Collapsing fixups into their targets is the
|
||||
user's action, taken once they've reviewed the iterations. Every mention of
|
||||
`--autosquash` in this section describes what the *user* will eventually
|
||||
`--autosquash` in this section describes what the _user_ will eventually
|
||||
run, never a step for you to perform. If you think the history is ready to
|
||||
collapse, say so and leave it to them.
|
||||
|
||||
@@ -291,7 +348,7 @@ 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
|
||||
_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):
|
||||
@@ -377,7 +434,7 @@ 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
|
||||
Comments in source code explain _why this code is shaped the way it is_. They
|
||||
are not the place to narrate the path we took during development — what was
|
||||
tried first, what didn't work, what's "more reliable" or "cleaner" than some
|
||||
alternative. That framing is interesting in the moment, but it's noise to
|
||||
@@ -393,7 +450,7 @@ Avoid phrasings like:
|
||||
- "X rather than Y", where Y is what the code did before the change
|
||||
|
||||
The iteration story is sometimes worth preserving — but it belongs in the
|
||||
commit message, which is the durable record of *why this change was made*. The
|
||||
commit message, which is the durable record of _why this change was made_. The
|
||||
code comment should make sense to someone who has never seen any prior version
|
||||
and is just trying to understand the file as it currently exists.
|
||||
|
||||
@@ -446,7 +503,7 @@ So:
|
||||
struct, run `just generate` and include the regenerated
|
||||
`docs-master/Config.md` (and `schema-master/config.json`) in your commit.
|
||||
- Don't hard-wrap the doc comments on `userConfig` fields. This applies
|
||||
*only* to `userConfig`, because those comments are fed through the doc
|
||||
_only_ to `userConfig`, because those comments are fed through the doc
|
||||
generator; comments on every other struct follow the normal Go wrapping
|
||||
conventions. For `userConfig` fields, write each sentence (or paragraph)
|
||||
as a single unwrapped line, however long — the generator re-wraps them for
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
// author_colors_repo creates a git repository for checking that the colors
|
||||
// lazygit gives to authors are readable.
|
||||
//
|
||||
// If gui.authorColors names no color for an author, lazygit derives one from a
|
||||
// hash of their name. The authors of an ordinary repository rarely land near the
|
||||
// edges of the range that this color is picked from. Every commit in the
|
||||
// repository created here is by an author at one of those edges: the lowest or
|
||||
// highest lightness, combined with the lowest or highest saturation, at twelve
|
||||
// hues around the color wheel. The commit subject says which edge it is.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// go run ./cmd/author_colors_repo <path>
|
||||
//
|
||||
// Then open the repository with lazygit in each terminal theme you want to check.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation/authors"
|
||||
)
|
||||
|
||||
type extreme int
|
||||
|
||||
const (
|
||||
lowest extreme = iota
|
||||
highest
|
||||
)
|
||||
|
||||
func (self extreme) String() string {
|
||||
if self == lowest {
|
||||
return "min"
|
||||
}
|
||||
return "max"
|
||||
}
|
||||
|
||||
func (self extreme) matches(fraction float64) bool {
|
||||
if self == lowest {
|
||||
return fraction < 0.01
|
||||
}
|
||||
return fraction >= 0.99
|
||||
}
|
||||
|
||||
const (
|
||||
numHues = 12
|
||||
|
||||
// Small enough that the windows of neighbouring hues don't overlap
|
||||
hueTolerance = 0.02
|
||||
)
|
||||
|
||||
type commit struct {
|
||||
author string
|
||||
subject string
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
log.Fatalf("usage: %s <path>", os.Args[0])
|
||||
}
|
||||
path := os.Args[1]
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
log.Fatalf("%s already exists", path)
|
||||
}
|
||||
|
||||
extremes := []extreme{lowest, highest}
|
||||
commits := make([]commit, 0, len(extremes)*len(extremes)*numHues)
|
||||
for _, lightness := range extremes {
|
||||
for _, saturation := range extremes {
|
||||
for i := range numHues {
|
||||
hue := float64(i) / numHues
|
||||
author, actualHue := findAuthor(lightness, saturation, hue)
|
||||
subject := fmt.Sprintf("lightness %s, saturation %s, hue %.0f°", lightness, saturation, actualHue*360)
|
||||
commits = append(commits, commit{author: author, subject: subject})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := runGit("", nil, "init", "-q", path); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Commit in reverse, so that the commits panel lists them in the order above
|
||||
startTime := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
for i := range commits {
|
||||
c := commits[len(commits)-1-i]
|
||||
date := fmt.Sprintf("%d +0000", startTime.Add(time.Duration(i)*time.Hour).Unix())
|
||||
env := []string{
|
||||
"GIT_AUTHOR_NAME=" + c.author,
|
||||
"GIT_AUTHOR_EMAIL=author@example.com",
|
||||
"GIT_AUTHOR_DATE=" + date,
|
||||
"GIT_COMMITTER_NAME=" + c.author,
|
||||
"GIT_COMMITTER_EMAIL=author@example.com",
|
||||
"GIT_COMMITTER_DATE=" + date,
|
||||
}
|
||||
if err := runGit(path, env, "commit", "-q", "--allow-empty", "-m", c.subject); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("Created %s with %d commits\n", path, len(commits))
|
||||
}
|
||||
|
||||
// findAuthor returns the first name of the form "L<lightness> S<saturation> <n>"
|
||||
// whose color lies at the given extremes of lightness and saturation, and close
|
||||
// to the given hue. It also returns the hue that the name lands on.
|
||||
//
|
||||
// Every name of this form has the same initials, so the color is the only
|
||||
// thing that differs between authors in the commits panel.
|
||||
func findAuthor(lightness extreme, saturation extreme, hue float64) (string, float64) {
|
||||
for n := 1; ; n++ {
|
||||
name := fmt.Sprintf("L%s S%s %d", lightness, saturation, n)
|
||||
h, s, l := authors.ColorPosition(name)
|
||||
if lightness.matches(l) && saturation.matches(s) && hueDistance(h, hue) <= hueTolerance {
|
||||
return name, h
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hueDistance is the distance between two hues on the color wheel, where each
|
||||
// hue is a fraction of a full turn.
|
||||
func hueDistance(a float64, b float64) float64 {
|
||||
d := math.Abs(a - b)
|
||||
return math.Min(d, 1-d)
|
||||
}
|
||||
|
||||
func runGit(dir string, env []string, args ...string) error {
|
||||
cmd := exec.Command("git", args...)
|
||||
cmd.Dir = dir
|
||||
// Keep the user's git config out of it, so that hooks, commit signing and
|
||||
// the like don't apply, and every run creates the same commits
|
||||
cmd.Env = append(os.Environ(), "GIT_CONFIG_GLOBAL="+os.DevNull, "GIT_CONFIG_NOSYSTEM=1")
|
||||
cmd.Env = append(cmd.Env, env...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
+101
-32
@@ -37,12 +37,6 @@ This is only meant as a reference for what config options exist, and what their
|
||||
```yaml
|
||||
# Config relating to the Lazygit UI
|
||||
gui:
|
||||
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-author-color
|
||||
authorColors: {}
|
||||
|
||||
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-branch-color
|
||||
branchColorPatterns: {}
|
||||
|
||||
# Custom icons for filenames and file extensions
|
||||
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-files-icon--color
|
||||
customIcons:
|
||||
@@ -78,7 +72,7 @@ gui:
|
||||
# If true, do not show a warning when amending a commit.
|
||||
skipAmendWarning: false
|
||||
|
||||
# If true, do not show a warning when discarding changes in the staging view.
|
||||
# If true, do not show a warning when discarding changes from a focused diff.
|
||||
skipDiscardChangeWarning: false
|
||||
|
||||
# If true, do not show warning when applying/popping the stash
|
||||
@@ -148,14 +142,13 @@ gui:
|
||||
# - 'top': split the window vertically (side panel on top, main view below)
|
||||
enlargedSideViewLocation: left
|
||||
|
||||
# If true, wrap lines in the staging view to the width of the view. This makes
|
||||
# it much easier to work with diffs that have long lines, e.g. paragraphs of
|
||||
# If true, wrap lines in focused diffs to the width of the view. This makes it
|
||||
# much easier to work with diffs that have long lines, e.g. paragraphs of
|
||||
# markdown text.
|
||||
wrapLinesInStagingView: true
|
||||
wrapLinesInDiffView: true
|
||||
|
||||
# If true, hunk selection mode will be enabled by default when entering the
|
||||
# staging view.
|
||||
useHunkModeInStagingView: true
|
||||
# If true, hunk selection mode will be enabled by default when focusing a diff.
|
||||
useHunkModeInDiffView: true
|
||||
|
||||
# One of 'auto' (default) | 'en' | 'zh-CN' | 'zh-TW' | 'pl' | 'nl' | 'ja' | 'ko'
|
||||
# | 'ru' | 'pt'
|
||||
@@ -169,6 +162,14 @@ gui:
|
||||
# Uses Go's time format syntax: https://pkg.go.dev/time#Time.Format
|
||||
shortTimeFormat: 3:04PM
|
||||
|
||||
# Whether the terminal has a dark or a light background. This decides whether
|
||||
# 'darkTheme' or 'lightTheme' applies, and the colors of authors are picked to
|
||||
# stand out against it.
|
||||
# One of: 'auto' (default) | 'dark' | 'light'
|
||||
# With 'auto', lazygit asks the terminal, and assumes a dark background if the
|
||||
# terminal doesn't tell.
|
||||
colorScheme: auto
|
||||
|
||||
# Config relating to colors and styles.
|
||||
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#color-attributes
|
||||
theme:
|
||||
@@ -179,7 +180,7 @@ gui:
|
||||
|
||||
# Border color of non-focused windows
|
||||
inactiveBorderColor:
|
||||
- default
|
||||
- dim
|
||||
|
||||
# Border color of focused window when searching in that window
|
||||
searchingActiveBorderColor:
|
||||
@@ -190,14 +191,22 @@ gui:
|
||||
optionsTextColor:
|
||||
- blue
|
||||
|
||||
# Color and attributes of the text of the selected line. The attributes are
|
||||
# added to those of the text, and a color replaces the colors of the text.
|
||||
# Set it to 'default' to leave the text as it is, e.g. if you don't want the
|
||||
# selected line in bold.
|
||||
selectedLineFgColor:
|
||||
- bold
|
||||
|
||||
# Background color of selected line.
|
||||
# Default: 'blue' if the terminal has a dark background, or a suitable RGB blue
|
||||
# computed from the background color if it is light.
|
||||
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#highlighting-the-selected-line
|
||||
selectedLineBgColor:
|
||||
- blue
|
||||
selectedLineBgColor: []
|
||||
|
||||
# Background color of selected line when view doesn't have focus.
|
||||
inactiveViewSelectedLineBgColor:
|
||||
- bold
|
||||
# Default: a suitable RGB grey computed from the terminal's background color.
|
||||
inactiveViewSelectedLineBgColor: []
|
||||
|
||||
# Foreground color of copied commit
|
||||
cherryPickedCommitFgColor:
|
||||
@@ -223,6 +232,22 @@ gui:
|
||||
defaultFgColor:
|
||||
- default
|
||||
|
||||
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-author-color
|
||||
authorColors: {}
|
||||
|
||||
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-branch-color
|
||||
branchColorPatterns: {}
|
||||
|
||||
# Colors and styles that override those in 'theme' when the terminal has a dark
|
||||
# background. It has the same fields as 'theme'.
|
||||
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#themes-for-dark-and-light-backgrounds
|
||||
darkTheme: {}
|
||||
|
||||
# Colors and styles that override those in 'theme' when the terminal has a light
|
||||
# background. It has the same fields as 'theme'.
|
||||
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#themes-for-dark-and-light-backgrounds
|
||||
lightTheme: {}
|
||||
|
||||
# Config relating to the commit length indicator
|
||||
commitLength:
|
||||
# If true, show an indicator of commit message length
|
||||
@@ -443,7 +468,9 @@ git:
|
||||
# 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
|
||||
# (as opposed to diverged).
|
||||
# (as opposed to diverged). A branch that is checked out in another worktree is
|
||||
# fast-forwarded there, unless that worktree has changes to tracked files or is
|
||||
# in the middle of a rebase or bisect.
|
||||
# Possible values: 'none' | 'onlyMainBranches' | 'allBranches'
|
||||
autoForwardBranches: onlyMainBranches
|
||||
|
||||
@@ -670,6 +697,7 @@ keybinding:
|
||||
- "4"
|
||||
- "5"
|
||||
focusMainView: "0"
|
||||
jumpToFile: <ctrl+g>
|
||||
nextMatch: "n"
|
||||
prevMatch: "N"
|
||||
startSearch: /
|
||||
@@ -817,6 +845,8 @@ keybinding:
|
||||
main:
|
||||
prevHunk: [<left>, h]
|
||||
nextHunk: [<right>, l]
|
||||
prevFile: "N"
|
||||
nextFile: "n"
|
||||
toggleSelectHunk: a
|
||||
pickBothHunks: b
|
||||
editSelectHunk: E
|
||||
@@ -907,7 +937,9 @@ It is used, for example, when pasting a commit message into the commit message p
|
||||
|
||||
## Configuring File Editing
|
||||
|
||||
There are two commands for opening files, `o` for "open" and `e` for "edit". `o` acts as if the file was double-clicked in the Finder/Explorer, so it also works for non-text files, whereas `e` opens the file in an editor. `e` can also jump to the right line in the file if you invoke it from the staging panel, for example.
|
||||
There are two commands for opening files, `o` for "open" and `e` for "edit". `o` acts as if the file was double-clicked in the Finder/Explorer, so it also works for non-text files, whereas `e` opens the file in an editor. `e` can also jump to the right line in the file when you invoke it from a focused diff.
|
||||
|
||||
You can also open a line in your editor with the mouse: alt-click or shift-click it. Both modifiers do the same thing, because some terminals only support one or the other. The click leaves the focus and the selection where they are, so it works while you are reading a diff from another panel, or while a popup is open.
|
||||
|
||||
To tell lazygit which editor to use for the `e` command, the easiest way to do that is to provide an editPreset config, e.g.
|
||||
|
||||
@@ -970,7 +1002,7 @@ When the selected line gets close to the bottom of the window and you hit down-a
|
||||
|
||||
That's the behavior when `gui.scrollOffBehavior` is set to "margin" (the default). If you set `gui.scrollOffBehavior` to "jump", then upon reaching the last line of a view and hitting down-arrow the view will scroll by half a page so that the selection ends up in the middle of the view. This may feel a little jarring because the cursor jumps around when continuously moving down, but it has the advantage that the view doesn't scroll as often.
|
||||
|
||||
This setting applies both to all list views (e.g. commits and branches etc), and to the staging view.
|
||||
This setting applies both to all list views (e.g. commits and branches etc), and to focused diffs.
|
||||
|
||||
## Filtering
|
||||
|
||||
@@ -999,6 +1031,7 @@ The available attributes are:
|
||||
|
||||
- bold
|
||||
- default
|
||||
- dim # faint text; not supported by every terminal
|
||||
- reverse # useful for high-contrast
|
||||
- underline
|
||||
- strikethrough
|
||||
@@ -1023,28 +1056,61 @@ gui:
|
||||
- reverse
|
||||
```
|
||||
|
||||
The text of the selected line is bold by default. If you don't want that, set `selectedLineFgColor` to `default`:
|
||||
|
||||
```yaml
|
||||
gui:
|
||||
theme:
|
||||
selectedLineFgColor:
|
||||
- default
|
||||
```
|
||||
|
||||
## Themes for dark and light backgrounds
|
||||
|
||||
The colors in `gui.theme` apply whether your terminal has a dark or a light background. If you want different colors for the two, set them in `gui.darkTheme` or `gui.lightTheme`. These have the same fields as `gui.theme`, and a field that you set in them overrides the one in `gui.theme`:
|
||||
|
||||
```yaml
|
||||
gui:
|
||||
theme:
|
||||
activeBorderColor:
|
||||
- green
|
||||
- bold
|
||||
lightTheme:
|
||||
activeBorderColor:
|
||||
- blue
|
||||
- bold
|
||||
```
|
||||
|
||||
For `authorColors` and `branchColorPatterns`, each entry overrides the one with the same key in `gui.theme`, and the other entries of `gui.theme` still apply. Branch color patterns of `gui.darkTheme` or `gui.lightTheme` come before those of `gui.theme`.
|
||||
|
||||
Lazygit asks the terminal whether its background is dark or light. If your terminal doesn't tell, lazygit assumes a dark background; set `gui.colorScheme` to `light` if yours is light.
|
||||
|
||||
## Custom Author Color
|
||||
|
||||
Lazygit will assign a random color for every commit author in the commits pane by default.
|
||||
|
||||
These colors are picked to be readable against the background of your terminal, and lazygit asks the terminal whether its background is dark or light. If your terminal doesn't tell, lazygit assumes a dark background; set `gui.colorScheme` to `light` if yours is light.
|
||||
|
||||
You can customize the color in case you're not happy with the randomly assigned one:
|
||||
|
||||
```yaml
|
||||
gui:
|
||||
authorColors:
|
||||
'John Smith': 'red' # use red for John Smith
|
||||
'Alan Smithee': '#00ff00' # use green for Alan Smithee
|
||||
theme:
|
||||
authorColors:
|
||||
'John Smith': 'red' # use red for John Smith
|
||||
'Alan Smithee': '#00ff00' # use green for Alan Smithee
|
||||
```
|
||||
|
||||
You can use wildcard to set a unified color in case your are lazy to customize the color for every author or you just want a single color for all/other authors:
|
||||
|
||||
```yaml
|
||||
gui:
|
||||
authorColors:
|
||||
# use red for John Smith
|
||||
'John Smith': 'red'
|
||||
# use blue for other authors
|
||||
'*': '#0000ff'
|
||||
theme:
|
||||
authorColors:
|
||||
# use red for John Smith
|
||||
'John Smith': 'red'
|
||||
# use blue for other authors
|
||||
'*': '#0000ff'
|
||||
```
|
||||
|
||||
## Custom Branch Color
|
||||
@@ -1053,13 +1119,16 @@ You can customize the color of branches based on branch patterns (regular expres
|
||||
|
||||
```yaml
|
||||
gui:
|
||||
branchColorPatterns:
|
||||
'^docs/': '#11aaff' # use a light blue for branches beginning with 'docs/'
|
||||
'ISSUE-\d+': '#ff5733' # use a bright orange for branches containing 'ISSUE-<some-number>'
|
||||
theme:
|
||||
branchColorPatterns:
|
||||
'^docs/': '#11aaff' # use a light blue for branches beginning with 'docs/'
|
||||
'ISSUE-\d+': '#ff5733' # use a bright orange for branches containing 'ISSUE-<some-number>'
|
||||
```
|
||||
|
||||
Note that the regular expressions are not implicitly anchored to the beginning/end of the branch name. If you want to do that, add leading `^` and/or trailing `$` as needed.
|
||||
|
||||
If several patterns match a branch, the first one wins.
|
||||
|
||||
## Custom Files Icon & Color
|
||||
|
||||
You can customize the icon and color of files based on filenames or extensions:
|
||||
|
||||
@@ -23,24 +23,31 @@ Fields only for `extDiff`:
|
||||
|
||||
- **command** The command line to use for the `diff.external` git config. If left empty, it uses the global value of git's `diff.external` config; 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.
|
||||
|
||||
You can include the `{{diffContext}}` template variable to pass lazygit's current diff context size (the value controlled by the `{`/`}` keybindings) to the diff tool.
|
||||
|
||||
Fields only for `rawGit`:
|
||||
|
||||
- **args** The additional arguments to use in the `git diff` or `git show` call (e.g. `--color-words`)
|
||||
- **args** The additional arguments to use in the `git diff` or `git show` call (e.g. `--color-words`), as an array of strings.
|
||||
|
||||
The `command` of a `stdinFilter` or `extDiff` renderer is a [Go template](https://pkg.go.dev/text/template) with these variables:
|
||||
|
||||
- `{{width}}`: the width of the view that the diff is rendered into.
|
||||
- `{{colorScheme}}`: `dark` or `light`, depending on whether the terminal has a dark or a light background. Lazygit asks the terminal about this; if yours doesn't tell, set `gui.colorScheme`.
|
||||
- `{{columnWidth}}` (only for `stdinFilter`): the width of one side of a side-by-side rendering, e.g. for `ydiff -p cat -s -w {{columnWidth}}`.
|
||||
- `{{diffContext}}` (only for `extDiff`): lazygit's current diff context size, the value controlled by the `{`/`}` keybindings.
|
||||
|
||||
A variable can also be written with a leading dot, as in `{{.width}}`. The command can use template expressions too; for example, `delta --paging=never {{if gt .width 160}}--side-by-side{{end}}` shows the diff side by side only when there is room for it, or `delta --syntax-theme={{if eq .colorScheme "light"}}Github{{else}}Dracula{{end}}` picks a different syntax theme based on the background.
|
||||
|
||||
Here's an example for a multi-renderer setup:
|
||||
|
||||
```yaml
|
||||
git:
|
||||
diffRenderers:
|
||||
- command: delta --dark --paging=never
|
||||
- command: delta --{{colorScheme}} --paging=never
|
||||
- command: ydiff -p cat
|
||||
colorArg: never
|
||||
- type: extDiff
|
||||
command: difft --color=always --context={{diffContext}}
|
||||
command: difft --color=always --background={{colorScheme}} --context={{diffContext}}
|
||||
- type: rawGit
|
||||
args: --color-words
|
||||
args: [--color-words]
|
||||
name: color-words
|
||||
- type: rawGit # git's default diff
|
||||
name: default
|
||||
@@ -51,12 +58,14 @@ git:
|
||||
```yaml
|
||||
git:
|
||||
diffRenderers:
|
||||
- command: delta --dark --paging=never
|
||||
- command: delta --{{colorScheme}} --paging=never
|
||||
```
|
||||
|
||||

|
||||
|
||||
A cool feature of delta is --hyperlinks, which renders clickable links for the line numbers in the left margin, and lazygit supports these. To use them, set the `command:` field to `delta --dark --paging=never --line-numbers --hyperlinks --hyperlinks-file-link-format="lazygit-edit://{path}:{line}"`; this allows you to click on an underlined line number in the diff to jump right to that same line in your editor.
|
||||
`--{{colorScheme}}` passes `--dark` or `--light` to delta, so that it matches the background of your terminal.
|
||||
|
||||
A cool feature of delta is --hyperlinks, which renders clickable links for the line numbers in the left margin, and lazygit supports these. To use them, set the `command:` field to `delta --{{colorScheme}} --paging=never --line-numbers --hyperlinks --hyperlinks-file-link-format="lazygit-edit://{path}:{line}"`; this allows you to click on an underlined line number in the diff to jump right to that same line in your editor.
|
||||
|
||||
Note that delta's `--navigate` option doesn't work in lazygit, for technical reasons.
|
||||
|
||||
|
||||
@@ -4,10 +4,14 @@
|
||||
|
||||
Depending on the currently focused view, hitting '/' will bring up a filter or search prompt. When filtering, the contents of the view will be filtered down to only those lines which match the query string. When searching, the contents of the view are not filtered, but matching lines are highlighted and you can iterate through matches with `n`/`N`.
|
||||
|
||||
We intend to support filtering for the files view soon, but at the moment it uses searching. We intend to continue using search for the commits view because you typically care about the commits that come before/after a matching commit.
|
||||
In the commits view we don't filter, but search; this is deliberate because you typically care about the commits that come before/after a matching commit.
|
||||
|
||||
If you would like both filtering and searching to be enabled on a given view, please raise an issue for this.
|
||||
|
||||
## Menu filtering
|
||||
|
||||
The keybindings (`?`) and recent repositories menus can be filtered simply by typing. The filter field appears at the bottom of the menu while you type; there is no need to press `/` or confirm the filter before navigating the results.
|
||||
|
||||
## Filtering files by status
|
||||
|
||||
You can filter the files view to only show staged/unstaged files by pressing `<c-b>` in the files view.
|
||||
|
||||
@@ -16,3 +16,36 @@ branches properly stacked onto it.
|
||||
Lazygit visualizes the individual branch heads in the stack by marking them with a
|
||||
cyan asterisk (or a cyan branch symbol if you are using [nerd
|
||||
fonts](Config.md#display-nerd-fonts-icons)).
|
||||
|
||||
When you push the topmost branch of the stack with `P`, and the branches below
|
||||
it have commits that haven't been pushed yet, lazygit offers to push them along
|
||||
with it. After rebasing the stack this saves you from checking out and
|
||||
force-pushing every branch one by one; you are asked to confirm the force push
|
||||
once for all of them. Only branches that already have an upstream are included.
|
||||
Each of them is pushed to where `git push` would push it if it were checked out,
|
||||
so your push configuration applies to them as usual.
|
||||
|
||||
When somebody else rebases the stack and force-pushes it, all your branches
|
||||
show up as diverged, for example `↓5↑3`, even though the commits they are ahead
|
||||
by are only the old versions of the ones that are now on the remote. Lazygit
|
||||
tells this apart from a branch that carries work of your own, and shows the
|
||||
divergence dimmed for such a branch. Pressing `f` on it resets it to its
|
||||
upstream instead of refusing, so you don't have to check the branch out and pull
|
||||
it. Lazygit only does this when every commit of the branch was on its remote
|
||||
branch at some point. It finds that out from the reflog of the remote-tracking
|
||||
branch. Reflogs are enabled by default, except in a bare repository; if you work
|
||||
in one with linked worktrees, set `core.logAllRefUpdates` to true there to make
|
||||
this work.
|
||||
|
||||
`f` works on a [range selection](Range_Select.md) too, so you can select the
|
||||
whole stack and bring all of it back in sync at once. If any of the selected
|
||||
branches can't be updated, none of them is, so that you don't end up with half
|
||||
of the stack updated.
|
||||
|
||||
Alternatively, check out the topmost branch of the stack and pull it with `p`.
|
||||
If branches below it can be updated this way, or are simply behind their
|
||||
upstream, lazygit offers to update them along with it. Lazygit decides this
|
||||
from the last fetch, so a branch whose changes on the remote haven't been
|
||||
fetched yet isn't offered; with auto-fetch turned off, pull a second time after
|
||||
the first pull has fetched them. Branches that are checked out in another
|
||||
worktree are left alone. The topmost branch itself is pulled as usual.
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
* `pkg/gui/keybindings`: Contains code for mapping between keybindings and their labels
|
||||
* `pkg/gui/mergeconflicts`: Contains code relating to the handling of merge conflicts
|
||||
* `pkg/gui/modes`: Contains code relating to the state of different modes e.g. cherry picking mode, rebase mode.
|
||||
* `pkg/gui/patch_exploring`: Contains code relating to the state of patch-oriented views like the staging view.
|
||||
* `pkg/gui/popup`: Contains code that lets you easily raise popups
|
||||
* `pkg/gui/presentation`: Contains presentation code i.e. code concerned with rendering content inside views
|
||||
* `pkg/gui/services/custom_commands`: Contains code related to user-defined custom commands.
|
||||
|
||||
@@ -10,8 +10,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <pgup>, K, <ctrl+u> (fn+up/shift+k) `` | Scroll up main window | |
|
||||
| `` <pgdown>, J, <ctrl+d> (fn+down/shift+j) `` | Scroll down main window | |
|
||||
| `` @ `` | 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 the current branch to its upstream branch. If no upstream is configured, you will be prompted to configure an upstream branch. If other branches are stacked below the current one and have commits to push, you are offered to push those too. |
|
||||
| `` 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. If other branches are stacked below the current one and have changed on the remote, you are offered to update those too. |
|
||||
| `` ) `` | 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'. |
|
||||
@@ -24,6 +24,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` _ `` | Prev screen mode | |
|
||||
| `` \| `` | Cycle diff renderers | Choose the next renderer in the list of configured diff renderers. |
|
||||
| `` \ `` | Cycle diff renderers (reverse) | Choose the previous renderer in the list of configured diff renderers. |
|
||||
| `` <ctrl+g> `` | Jump to file in diff | Pick one of the files of the diff shown in the main view, and scroll the main view to it. The focus stays in this panel. |
|
||||
| `` <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. |
|
||||
@@ -65,7 +66,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` <space> `` | Toggle file included 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 file / Toggle directory collapsed | 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. |
|
||||
| `` <enter> `` | Focus file diff / Toggle directory | If a file is selected, focus its diff so you can act on individual lines. If it is a directory, collapse or expand it. |
|
||||
| `` ` `` | Toggle file tree view | 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 |
|
||||
@@ -149,7 +150,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` s `` | Stash | Stash all changes. For other variations of stashing, use the view stash options keybinding. |
|
||||
| `` S `` | View stash options | View stash options (e.g. stash all, stash staged, stash unstaged). |
|
||||
| `` a `` | Stage all | Toggle staged/unstaged for all files in working tree. |
|
||||
| `` <enter> `` | Stage lines / Collapse directory | 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. |
|
||||
| `` <enter> `` | Focus file diff / Collapse directory | If the selected item is a file, focus its diff so you can act on individual hunks or lines. If it is a directory, collapse or expand it. |
|
||||
| `` d `` | Discard | View options for discarding changes to the selected file. |
|
||||
| `` g `` | View upstream reset options | |
|
||||
| `` D `` | Reset | View reset options for working tree (e.g. nuking the working tree). |
|
||||
@@ -189,7 +190,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` d `` | Delete | View delete options for local/remote branch. |
|
||||
| `` r `` | Rebase | Rebase the checked-out branch onto the selected branch. |
|
||||
| `` M `` | Merge | View options for merging the selected item into the current branch (regular merge, squash merge) |
|
||||
| `` f `` | Fast-forward | Fast-forward selected branch from its upstream. |
|
||||
| `` f `` | Fast-forward | Fast-forward selected branch from its upstream. If the branch has diverged from its upstream because the upstream branch was rewritten, and it has no commits of its own, it is reset to its upstream instead. This needs reflogs to be enabled; a bare repository doesn't keep them by default (core.logAllRefUpdates). |
|
||||
| `` T `` | New tag | |
|
||||
| `` s `` | Sort order | |
|
||||
| `` g `` | Reset | |
|
||||
@@ -222,42 +223,21 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|-----|--------|-------------|
|
||||
| `` <mouse wheel down> (fn+up) `` | Scroll down | |
|
||||
| `` <mouse wheel up> (fn+down) `` | Scroll up | |
|
||||
| `` <tab> `` | Switch view | Switch to other view (staged/unstaged changes). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` / `` | Search the current view by text | |
|
||||
|
||||
## Main panel (patch building)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | Go to previous hunk | |
|
||||
| `` <right>, l `` | Go to next hunk | |
|
||||
| `` v `` | Toggle range select | |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` <ctrl+o> `` | Copy selected text to clipboard | |
|
||||
| `` o `` | Open file | Open file in default application. |
|
||||
| `` v `` | Toggle range select | |
|
||||
| `` e `` | Edit file | Open file in external editor. |
|
||||
| `` <space> `` | Toggle lines 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> `` | Exit custom patch builder | |
|
||||
| `` / `` | Search the current view by text | |
|
||||
|
||||
## Main panel (staging)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | Go to previous hunk | |
|
||||
| `` <right>, l `` | Go to next hunk | |
|
||||
| `` v `` | Toggle range select | |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` <ctrl+o> `` | Copy selected text to clipboard | |
|
||||
| `` <space> `` | Stage | Toggle selection staged / unstaged. |
|
||||
| `` d `` | Discard | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
|
||||
| `` o `` | Open file | Open file in default application. |
|
||||
| `` e `` | Edit file | Open file in external editor. |
|
||||
| `` <esc> `` | Return to files panel | |
|
||||
| `` <tab> `` | Switch view | Switch to other view (staged/unstaged changes). |
|
||||
| `` E `` | Edit hunk | Edit selected hunk in external editor. |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | Go to previous hunk | |
|
||||
| `` <right>, l `` | Go to next hunk | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | Jump to file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` c `` | Commit | Commit staged changes. |
|
||||
| `` w `` | Commit changes without pre-commit hook | |
|
||||
| `` C `` | Commit changes using git editor | |
|
||||
@@ -327,8 +307,25 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <tab> `` | Switch view | Switch to other view (staged/unstaged changes). |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | Toggle range select | |
|
||||
| `` e `` | Edit file | Open file in external editor. |
|
||||
| `` <space> `` | Stage | Toggle selection staged / unstaged. |
|
||||
| `` d `` | Discard | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
|
||||
| `` E `` | Edit hunk | Edit selected hunk in external editor. |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | Go to previous hunk | |
|
||||
| `` <right>, l `` | Go to next hunk | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | Jump to file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` c `` | Commit | Commit staged changes. |
|
||||
| `` w `` | Commit changes without pre-commit hook | |
|
||||
| `` C `` | Commit changes using 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> |
|
||||
| `` / `` | Search the current view by text | |
|
||||
|
||||
## Stash
|
||||
|
||||
@@ -24,6 +24,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` _ `` | 前の画面モード | |
|
||||
| `` \| `` | Cycle diff renderers | Choose the next renderer in the list of configured diff renderers. |
|
||||
| `` \ `` | Cycle diff renderers (reverse) | Choose the previous renderer in the list of configured diff renderers. |
|
||||
| `` <ctrl+g> `` | Jump to file in diff | Pick one of the files of the diff shown in the main view, and scroll the main view to it. The focus stays in this panel. |
|
||||
| `` <esc> `` | キャンセル | |
|
||||
| `` ? `` | キーバインディングメニューを開く | |
|
||||
| `` <ctrl+s> `` | フィルターオプションを表示 | コミットログのフィルタリングオプションを表示し、フィルタに一致するコミットのみを表示します。 |
|
||||
@@ -114,7 +115,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <ctrl+t> `` | 外部差分ツールを開く(git difftool) | |
|
||||
| `` <space> `` | パッチに含めるファイルを切り替え | ファイルがカスタムパッチに含まれるかどうかを切り替えます。https://github.com/jesseduffield/lazygit#rebase-magic-custom-patchesを参照してください。 |
|
||||
| `` a `` | すべてのファイルを切り替え | コミットのすべてのファイルをカスタムパッチに追加/削除します。https://github.com/jesseduffield/lazygit#rebase-magic-custom-patchesを参照してください。 |
|
||||
| `` <enter> `` | ファイルに入る / ディレクトリの折りたたみを切り替える | ファイルが選択されている場合、そのファイルに入ってカスタムパッチに個々の行を追加/削除できます。ディレクトリが選択されている場合、ディレクトリを切り替えます。 |
|
||||
| `` <enter> `` | Focus file diff / Toggle directory | If a file is selected, focus its diff so you can act on individual lines. If it is a directory, collapse or expand it. |
|
||||
| `` ` `` | ファイルツリービューを切り替え | ファイル表示をフラット表示とツリー表示で切り替えます。フラット表示はすべてのファイルパスを一覧で表示し、ツリー表示はディレクトリごとにファイルをグループ化します。<br><br>デフォルトは設定ファイル内の 'gui.showFileTree' キーで変更できます。 |
|
||||
| `` - `` | すべてのファイルを折りたたむ | ファイルツリー内のすべてのディレクトリを折りたたみます |
|
||||
| `` = `` | すべてのファイルを展開 | ファイルツリー内のすべてのディレクトリを展開します |
|
||||
@@ -191,8 +192,25 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <tab> `` | ビューを切り替え | 他のビュー(ステージされた変更/ステージされていない変更)に切り替えます。 |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | ハンクの選択を切り替える | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | 範囲選択を切り替え | |
|
||||
| `` e `` | ファイルを編集 | 外部エディタでファイルを開きます。 |
|
||||
| `` <space> `` | ステージ | 選択された部分のステージ / アンステージを切り替えます。 |
|
||||
| `` d `` | 破棄 | ステージされていない変更が選択されている場合、`git reset`を使用して変更を破棄します。ステージされた変更が選択されている場合、変更をアンステージします。 |
|
||||
| `` E `` | ハンクを編集 | 選択したハンクを外部エディタで編集します。 |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | 前のハンクに移動 | |
|
||||
| `` <right>, l `` | 次のハンクに移動 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | Jump to file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | サイドパネルに戻る | |
|
||||
| `` c `` | コミット | ステージされた変更をコミットします。 |
|
||||
| `` w `` | pre-commitフックなしで変更をコミット | |
|
||||
| `` C `` | Gitエディタを使用して変更をコミット | |
|
||||
| `` <ctrl+f> `` | フィックスアップのベースコミットを検索 | 現在の変更が基づいているコミットを見つけて、コミットの修正/フィックスアップを行います。これにより、ブランチのコミットを一つずつ確認して、どのコミットを修正/フィックスアップすべきかを調べる手間が省けます。詳細はドキュメントを参照: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` / `` | 現在のビューをテキストで検索 | |
|
||||
|
||||
## タグ
|
||||
@@ -244,44 +262,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` 0 `` | メインビューにフォーカス | |
|
||||
| `` / `` | 現在のビューをテキストでフィルタリング | |
|
||||
|
||||
## メインパネル(ステージング)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | 前のハンクに移動 | |
|
||||
| `` <right>, l `` | 次のハンクに移動 | |
|
||||
| `` v `` | 範囲選択を切り替え | |
|
||||
| `` a `` | ハンクの選択を切り替える | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` <ctrl+o> `` | 選択したテキストをクリップボードにコピー | |
|
||||
| `` <space> `` | ステージ | 選択された部分のステージ / アンステージを切り替えます。 |
|
||||
| `` d `` | 破棄 | ステージされていない変更が選択されている場合、`git reset`を使用して変更を破棄します。ステージされた変更が選択されている場合、変更をアンステージします。 |
|
||||
| `` o `` | ファイルを開く | デフォルトのアプリケーションでファイルを開きます。 |
|
||||
| `` e `` | ファイルを編集 | 外部エディタでファイルを開きます。 |
|
||||
| `` <esc> `` | ファイルパネルに戻る | |
|
||||
| `` <tab> `` | ビューを切り替え | 他のビュー(ステージされた変更/ステージされていない変更)に切り替えます。 |
|
||||
| `` E `` | ハンクを編集 | 選択したハンクを外部エディタで編集します。 |
|
||||
| `` c `` | コミット | ステージされた変更をコミットします。 |
|
||||
| `` w `` | pre-commitフックなしで変更をコミット | |
|
||||
| `` C `` | Gitエディタを使用して変更をコミット | |
|
||||
| `` <ctrl+f> `` | フィックスアップのベースコミットを検索 | 現在の変更が基づいているコミットを見つけて、コミットの修正/フィックスアップを行います。これにより、ブランチのコミットを一つずつ確認して、どのコミットを修正/フィックスアップすべきかを調べる手間が省けます。詳細はドキュメントを参照: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` / `` | 現在のビューをテキストで検索 | |
|
||||
|
||||
## メインパネル(パッチ作成)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | 前のハンクに移動 | |
|
||||
| `` <right>, l `` | 次のハンクに移動 | |
|
||||
| `` v `` | 範囲選択を切り替え | |
|
||||
| `` a `` | ハンクの選択を切り替える | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` <ctrl+o> `` | 選択したテキストをクリップボードにコピー | |
|
||||
| `` o `` | ファイルを開く | デフォルトのアプリケーションでファイルを開きます。 |
|
||||
| `` e `` | ファイルを編集 | 外部エディタでファイルを開きます。 |
|
||||
| `` <space> `` | パッチ内の行を切り替え | |
|
||||
| `` 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> `` | カスタムパッチビルダーを終了 | |
|
||||
| `` / `` | 現在のビューをテキストで検索 | |
|
||||
|
||||
## メインパネル(マージ中)
|
||||
|
||||
| Key | Action | Info |
|
||||
@@ -304,8 +284,25 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|-----|--------|-------------|
|
||||
| `` <mouse wheel down> (fn+up) `` | 下にスクロール | |
|
||||
| `` <mouse wheel up> (fn+down) `` | 上にスクロール | |
|
||||
| `` <tab> `` | ビューを切り替え | 他のビュー(ステージされた変更/ステージされていない変更)に切り替えます。 |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | ハンクの選択を切り替える | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | 範囲選択を切り替え | |
|
||||
| `` e `` | ファイルを編集 | 外部エディタでファイルを開きます。 |
|
||||
| `` <space> `` | ステージ | 選択された部分のステージ / アンステージを切り替えます。 |
|
||||
| `` d `` | 破棄 | ステージされていない変更が選択されている場合、`git reset`を使用して変更を破棄します。ステージされた変更が選択されている場合、変更をアンステージします。 |
|
||||
| `` E `` | ハンクを編集 | 選択したハンクを外部エディタで編集します。 |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | 前のハンクに移動 | |
|
||||
| `` <right>, l `` | 次のハンクに移動 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | Jump to file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | サイドパネルに戻る | |
|
||||
| `` c `` | コミット | ステージされた変更をコミットします。 |
|
||||
| `` w `` | pre-commitフックなしで変更をコミット | |
|
||||
| `` C `` | Gitエディタを使用して変更をコミット | |
|
||||
| `` <ctrl+f> `` | フィックスアップのベースコミットを検索 | 現在の変更が基づいているコミットを見つけて、コミットの修正/フィックスアップを行います。これにより、ブランチのコミットを一つずつ確認して、どのコミットを修正/フィックスアップすべきかを調べる手間が省けます。詳細はドキュメントを参照: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` / `` | 現在のビューをテキストで検索 | |
|
||||
|
||||
## メニュー
|
||||
|
||||
@@ -10,8 +10,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <pgup>, K, <ctrl+u> (fn+up/shift+k) `` | 메인 패널을 위로 스크롤 | |
|
||||
| `` <pgdown>, J, <ctrl+d> (fn+down/shift+j) `` | 메인 패널을 아래로로 스크롤 | |
|
||||
| `` @ `` | 명령어 로그 메뉴 열기 | View options for the command log e.g. show/hide the command log and focus the command log. |
|
||||
| `` P `` | 푸시 | Push the current branch to its upstream branch. If no upstream is configured, you will be prompted to configure an upstream branch. |
|
||||
| `` p `` | 업데이트 | 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 the current branch to its upstream branch. If no upstream is configured, you will be prompted to configure an upstream branch. If other branches are stacked below the current one and have commits to push, you are offered to push those too. |
|
||||
| `` p `` | 업데이트 | Pull changes from the remote for the current branch. If no upstream is configured, you will be prompted to configure an upstream branch. If other branches are stacked below the current one and have changed on the remote, you are offered to update those too. |
|
||||
| `` ) `` | 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'. |
|
||||
| `` } `` | Diff 보기의 변경 사항 주위에 표시되는 컨텍스트의 크기를 늘리기 | 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'. |
|
||||
@@ -24,6 +24,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` _ `` | 이전 스크린 모드 | |
|
||||
| `` \| `` | Cycle diff renderers | Choose the next renderer in the list of configured diff renderers. |
|
||||
| `` \ `` | Cycle diff renderers (reverse) | Choose the previous renderer in the list of configured diff renderers. |
|
||||
| `` <ctrl+g> `` | Jump to file in diff | Pick one of the files of the diff shown in the main view, and scroll the main view to it. The focus stays in this panel. |
|
||||
| `` <esc> `` | 취소 | |
|
||||
| `` ? `` | 매뉴 열기 | |
|
||||
| `` <ctrl+s> `` | View filter-by-path options | View options for filtering the commit log, so that only commits matching the filter are shown. |
|
||||
@@ -83,8 +84,25 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <tab> `` | 패널 전환 | Switch to other view (staged/unstaged changes). |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | 드래그 선택 전환 | |
|
||||
| `` e `` | 파일 편집 | Open file in external editor. |
|
||||
| `` <space> `` | Staged 전환 | 선택한 행을 staged / unstaged |
|
||||
| `` d `` | 변경을 삭제 (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
|
||||
| `` E `` | Edit hunk | Edit selected hunk in external editor. |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | 이전 hunk를 선택 | |
|
||||
| `` <right>, l `` | 다음 hunk를 선택 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | Jump to file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` c `` | 커밋 변경내용 | 스테이징된 변경 사항 커밋. |
|
||||
| `` w `` | Commit changes without pre-commit hook | |
|
||||
| `` C `` | Git 편집기를 사용하여 변경 내용을 커밋합니다. | |
|
||||
| `` <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> |
|
||||
| `` / `` | 검색 시작 | |
|
||||
|
||||
## Stash
|
||||
@@ -161,42 +179,21 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|-----|--------|-------------|
|
||||
| `` <mouse wheel down> (fn+up) `` | 아래로 스크롤 | |
|
||||
| `` <mouse wheel up> (fn+down) `` | 위로 스크롤 | |
|
||||
| `` <tab> `` | 패널 전환 | Switch to other view (staged/unstaged changes). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` / `` | 검색 시작 | |
|
||||
|
||||
## 메인 패널 (Patch Building)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | 이전 hunk를 선택 | |
|
||||
| `` <right>, l `` | 다음 hunk를 선택 | |
|
||||
| `` v `` | 드래그 선택 전환 | |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` <ctrl+o> `` | 선택한 텍스트를 클립보드에 복사 | |
|
||||
| `` o `` | 파일 닫기 | Open file in default application. |
|
||||
| `` v `` | 드래그 선택 전환 | |
|
||||
| `` e `` | 파일 편집 | Open file in external editor. |
|
||||
| `` <space> `` | Line(s)을 패치에 추가/삭제 | |
|
||||
| `` 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> `` | Exit custom patch builder | |
|
||||
| `` / `` | 검색 시작 | |
|
||||
|
||||
## 메인 패널 (Staging)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | 이전 hunk를 선택 | |
|
||||
| `` <right>, l `` | 다음 hunk를 선택 | |
|
||||
| `` v `` | 드래그 선택 전환 | |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` <ctrl+o> `` | 선택한 텍스트를 클립보드에 복사 | |
|
||||
| `` <space> `` | Staged 전환 | 선택한 행을 staged / unstaged |
|
||||
| `` d `` | 변경을 삭제 (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
|
||||
| `` o `` | 파일 닫기 | Open file in default application. |
|
||||
| `` e `` | 파일 편집 | Open file in external editor. |
|
||||
| `` <esc> `` | 파일 목록으로 돌아가기 | |
|
||||
| `` <tab> `` | 패널 전환 | Switch to other view (staged/unstaged changes). |
|
||||
| `` E `` | Edit hunk | Edit selected hunk in external editor. |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | 이전 hunk를 선택 | |
|
||||
| `` <right>, l `` | 다음 hunk를 선택 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | Jump to file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` c `` | 커밋 변경내용 | 스테이징된 변경 사항 커밋. |
|
||||
| `` w `` | Commit changes without pre-commit hook | |
|
||||
| `` C `` | Git 편집기를 사용하여 변경 내용을 커밋합니다. | |
|
||||
@@ -223,7 +220,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` d `` | 삭제 | View delete options for local/remote branch. |
|
||||
| `` r `` | 체크아웃된 브랜치를 이 브랜치에 리베이스 | Rebase the checked-out branch onto the selected branch. |
|
||||
| `` M `` | 현재 브랜치에 병합 | View options for merging the selected item into the current branch (regular merge, squash merge) |
|
||||
| `` f `` | Fast-forward this branch from its upstream | Fast-forward selected branch from its upstream. |
|
||||
| `` f `` | Fast-forward this branch from its upstream | Fast-forward selected branch from its upstream. If the branch has diverged from its upstream because the upstream branch was rewritten, and it has no commits of its own, it is reset to its upstream instead. This needs reflogs to be enabled; a bare repository doesn't keep them by default (core.logAllRefUpdates). |
|
||||
| `` T `` | 태그를 생성 | |
|
||||
| `` s `` | Sort order | |
|
||||
| `` g `` | View reset options | |
|
||||
@@ -345,7 +342,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` <space> `` | Toggle file included 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 included in patch | Add/remove all commit's files to custom patch. See https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
|
||||
| `` <enter> `` | Enter file to add selected lines to the patch (or toggle directory collapsed) | 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. |
|
||||
| `` <enter> `` | Focus file diff / Toggle directory | If a file is selected, focus its diff so you can act on individual lines. If it is a directory, collapse or expand it. |
|
||||
| `` ` `` | 파일 트리뷰로 전환 | 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 |
|
||||
@@ -395,7 +392,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` s `` | Stash | Stash all changes. For other variations of stashing, use the view stash options keybinding. |
|
||||
| `` S `` | Stash 옵션 보기 | View stash options (e.g. stash all, stash staged, stash unstaged). |
|
||||
| `` a `` | 모든 변경을 Staged/unstaged으로 전환 | Toggle staged/unstaged for all files in working tree. |
|
||||
| `` <enter> `` | Stage individual hunks/lines for file, or collapse/expand for directory | 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. |
|
||||
| `` <enter> `` | Stage individual hunks/lines for file, or collapse/expand for directory | If the selected item is a file, focus its diff so you can act on individual hunks or lines. If it is a directory, collapse or expand it. |
|
||||
| `` d `` | View 'discard changes' options | View options for discarding changes to the selected file. |
|
||||
| `` g `` | View upstream reset options | |
|
||||
| `` D `` | 초기화 | View reset options for working tree (e.g. nuking the working tree). |
|
||||
|
||||
@@ -24,6 +24,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` _ `` | Vorige scherm modus | |
|
||||
| `` \| `` | Cycle diff renderers | Choose the next renderer in the list of configured diff renderers. |
|
||||
| `` \ `` | Cycle diff renderers (reverse) | Choose the previous renderer in the list of configured diff renderers. |
|
||||
| `` <ctrl+g> `` | Jump to file in diff | Pick one of the files of the diff shown in the main view, and scroll the main view to it. The focus stays in this panel. |
|
||||
| `` <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. |
|
||||
@@ -72,7 +73,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` s `` | Stash | Stash all changes. For other variations of stashing, use the view stash options keybinding. |
|
||||
| `` S `` | Bekijk stash opties | View stash options (e.g. stash all, stash staged, stash unstaged). |
|
||||
| `` a `` | Toggle staged alle | Toggle staged/unstaged for all files in working tree. |
|
||||
| `` <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. |
|
||||
| `` <enter> `` | Stage individuele hunks/lijnen | If the selected item is a file, focus its diff so you can act on individual hunks or lines. If it is a directory, collapse or expand it. |
|
||||
| `` d `` | Bekijk 'veranderingen ongedaan maken' opties | View options for discarding changes to the selected file. |
|
||||
| `` g `` | Bekijk upstream reset opties | |
|
||||
| `` D `` | Resetten | View reset options for working tree (e.g. nuking the working tree). |
|
||||
@@ -113,7 +114,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` 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. |
|
||||
| `` f `` | Fast-forward deze branch vanaf zijn upstream | Fast-forward selected branch from its upstream. If the branch has diverged from its upstream because the upstream branch was rewritten, and it has no commits of its own, it is reset to its upstream instead. This needs reflogs to be enabled; a bare repository doesn't keep them by default (core.logAllRefUpdates). |
|
||||
| `` T `` | Creëer tag | |
|
||||
| `` s `` | Sort order | |
|
||||
| `` g `` | Bekijk reset opties | |
|
||||
@@ -144,7 +145,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <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. |
|
||||
| `` <enter> `` | Focus file diff / Toggle directory | If a file is selected, focus its diff so you can act on individual lines. If it is a directory, collapse or expand it. |
|
||||
| `` ` `` | 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 |
|
||||
| `` = `` | Vouw alle bestanden uit | Vouw alle mappen in de bestandsstructuur uit |
|
||||
@@ -230,24 +231,25 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|-----|--------|-------------|
|
||||
| `` <mouse wheel down> (fn+up) `` | Scroll omlaag | |
|
||||
| `` <mouse wheel up> (fn+down) `` | Scroll omhoog | |
|
||||
| `` <tab> `` | Ga naar een ander paneel | Switch to other view (staged/unstaged changes). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` / `` | Start met zoeken | |
|
||||
|
||||
## Patch bouwen
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | Wissel tussen hunk selectie aan of uit | Wissel tussen regel-voor-regel of hunk selectie modus. |
|
||||
| `` v `` | Toggle drag selecteer | |
|
||||
| `` e `` | Verander bestand | Open bestand in externe editor. |
|
||||
| `` <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. |
|
||||
| `` E `` | Edit hunk | Edit selected hunk in external editor. |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | Selecteer de vorige hunk | |
|
||||
| `` <right>, l `` | Selecteer de volgende hunk | |
|
||||
| `` v `` | Toggle drag selecteer | |
|
||||
| `` a `` | Wissel tussen hunk selectie aan of uit | Wissel tussen regel-voor-regel of hunk selectie modus. |
|
||||
| `` <ctrl+o> `` | Copy selected text to clipboard | |
|
||||
| `` 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 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | Jump to file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` 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 | 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 | |
|
||||
|
||||
## Reflog
|
||||
@@ -280,7 +282,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` 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 de uitgecheckte branch bovenop de geselecteerde branch. |
|
||||
| `` d `` | Verwijderen | Delete the remote branch from the remote. |
|
||||
| `` d `` | Verwijderen | Verwijder de remote branch van de 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. |
|
||||
@@ -295,7 +297,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|-----|--------|-------------|
|
||||
| `` <enter> `` | Bekijk branches | |
|
||||
| `` n `` | Voeg een nieuwe remote toe | |
|
||||
| `` d `` | Verwijderen | Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected. |
|
||||
| `` d `` | Verwijderen | Verwijder de geselecteerde remote. Locale branches die een branch tracken van de remote worden niet aangepast. |
|
||||
| `` 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. |
|
||||
@@ -305,26 +307,21 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <tab> `` | Ga naar een ander paneel | Switch to other view (staged/unstaged changes). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` / `` | Start met zoeken | |
|
||||
|
||||
## Staging
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | Selecteer de vorige hunk | |
|
||||
| `` <right>, l `` | Selecteer de volgende hunk | |
|
||||
| `` v `` | Toggle drag selecteer | |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | Wissel tussen hunk selectie aan of uit | Wissel tussen regel-voor-regel of hunk selectie modus. |
|
||||
| `` <ctrl+o> `` | Copy selected text to clipboard | |
|
||||
| `` v `` | Toggle drag selecteer | |
|
||||
| `` e `` | Verander bestand | Open bestand in externe editor. |
|
||||
| `` <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 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. |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | Selecteer de vorige hunk | |
|
||||
| `` <right>, l `` | Selecteer de volgende hunk | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | Jump to file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` c `` | Commit veranderingen | Commit gestagede wijzigingen. |
|
||||
| `` w `` | Commit veranderingen zonder pre-commit hook | |
|
||||
| `` C `` | Commit veranderingen met de git editor | |
|
||||
|
||||
@@ -24,6 +24,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` _ `` | Poprzedni tryb ekranu | |
|
||||
| `` \| `` | Cycle diff renderers | Choose the next renderer in the list of configured diff renderers. |
|
||||
| `` \ `` | Cycle diff renderers (reverse) | Choose the previous renderer in the list of configured diff renderers. |
|
||||
| `` <ctrl+g> `` | Jump to file in diff | Pick one of the files of the diff shown in the main view, and scroll the main view to it. The focus stays in this panel. |
|
||||
| `` <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. |
|
||||
@@ -98,8 +99,25 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <tab> `` | Przełącz widok | Przełącz na inny widok (zatwierdzone/niezatwierdzone zmiany). |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | Przełącz zaznaczenie zakresu | |
|
||||
| `` e `` | Edytuj plik | Otwórz plik w zewnętrznym edytorze. |
|
||||
| `` <space> `` | Zatwierdź | Przełącz zaznaczenie zatwierdzone/niezatwierdzone. |
|
||||
| `` d `` | Odrzuć | Gdy zaznaczona jest niezatwierdzona zmiana, odrzuć ją używając `git reset`. Gdy zaznaczona jest zatwierdzona zmiana, cofnij zatwierdzenie. |
|
||||
| `` E `` | Edytuj fragment | Edytuj wybrany fragment w zewnętrznym edytorze. |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | Idź do poprzedniego fragmentu | |
|
||||
| `` <right>, l `` | Idź do następnego fragmentu | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | Jump to file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` c `` | Commit | Zatwierdź zmiany zatwierdzone. |
|
||||
| `` w `` | Zatwierdź zmiany bez hooka pre-commit | |
|
||||
| `` C `` | Zatwierdź zmiany używając edytora git | |
|
||||
| `` <ctrl+f> `` | Znajdź bazowy commit do poprawki | Znajdź commit, na którym opierają się Twoje obecne zmiany, w celu poprawienia/zmiany commita. To pozwala Ci uniknąć przeglądania commitów w Twojej gałęzi jeden po drugim, aby zobaczyć, który commit powinien być poprawiony/zmieniony. Zobacz dokumentację: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` / `` | Szukaj w bieżącym widoku po tekście | |
|
||||
|
||||
## Drzewa pracy
|
||||
@@ -132,22 +150,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <enter> `` | Pokaż commity | |
|
||||
| `` / `` | Filtruj bieżący widok po tekście | |
|
||||
|
||||
## Główny panel (budowanie łatki)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | Idź do poprzedniego fragmentu | |
|
||||
| `` <right>, l `` | Idź do następnego fragmentu | |
|
||||
| `` v `` | Przełącz zaznaczenie zakresu | |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` <ctrl+o> `` | Kopiuj zaznaczony tekst do schowka | |
|
||||
| `` o `` | Otwórz plik | Otwórz plik w domyślnej aplikacji. |
|
||||
| `` e `` | Edytuj plik | Otwórz plik w zewnętrznym edytorze. |
|
||||
| `` <space> `` | Przełącz linie w łatce | |
|
||||
| `` 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> `` | Wyjdź z budowniczego niestandardowej łatki | |
|
||||
| `` / `` | Szukaj w bieżącym widoku po tekście | |
|
||||
|
||||
## Input prompt
|
||||
|
||||
| Key | Action | Info |
|
||||
@@ -200,8 +202,25 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|-----|--------|-------------|
|
||||
| `` <mouse wheel down> (fn+up) `` | Przewiń w dół | |
|
||||
| `` <mouse wheel up> (fn+down) `` | Przewiń w górę | |
|
||||
| `` <tab> `` | Przełącz widok | Przełącz na inny widok (zatwierdzone/niezatwierdzone zmiany). |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | Przełącz zaznaczenie zakresu | |
|
||||
| `` e `` | Edytuj plik | Otwórz plik w zewnętrznym edytorze. |
|
||||
| `` <space> `` | Zatwierdź | Przełącz zaznaczenie zatwierdzone/niezatwierdzone. |
|
||||
| `` d `` | Odrzuć | Gdy zaznaczona jest niezatwierdzona zmiana, odrzuć ją używając `git reset`. Gdy zaznaczona jest zatwierdzona zmiana, cofnij zatwierdzenie. |
|
||||
| `` E `` | Edytuj fragment | Edytuj wybrany fragment w zewnętrznym edytorze. |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | Idź do poprzedniego fragmentu | |
|
||||
| `` <right>, l `` | Idź do następnego fragmentu | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | Jump to file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` c `` | Commit | Zatwierdź zmiany zatwierdzone. |
|
||||
| `` w `` | Zatwierdź zmiany bez hooka pre-commit | |
|
||||
| `` C `` | Zatwierdź zmiany używając edytora git | |
|
||||
| `` <ctrl+f> `` | Znajdź bazowy commit do poprawki | Znajdź commit, na którym opierają się Twoje obecne zmiany, w celu poprawienia/zmiany commita. To pozwala Ci uniknąć przeglądania commitów w Twojej gałęzi jeden po drugim, aby zobaczyć, który commit powinien być poprawiony/zmieniony. Zobacz dokumentację: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` / `` | Szukaj w bieżącym widoku po tekście | |
|
||||
|
||||
## Panel główny (scalanie)
|
||||
@@ -220,28 +239,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
|
||||
| `` <esc> `` | Wróć do panelu plików | |
|
||||
|
||||
## Panel główny (zatwierdzanie)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | Idź do poprzedniego fragmentu | |
|
||||
| `` <right>, l `` | Idź do następnego fragmentu | |
|
||||
| `` v `` | Przełącz zaznaczenie zakresu | |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` <ctrl+o> `` | Kopiuj zaznaczony tekst do schowka | |
|
||||
| `` <space> `` | Zatwierdź | Przełącz zaznaczenie zatwierdzone/niezatwierdzone. |
|
||||
| `` d `` | Odrzuć | Gdy zaznaczona jest niezatwierdzona zmiana, odrzuć ją używając `git reset`. Gdy zaznaczona jest zatwierdzona zmiana, cofnij zatwierdzenie. |
|
||||
| `` o `` | Otwórz plik | Otwórz plik w domyślnej aplikacji. |
|
||||
| `` e `` | Edytuj plik | Otwórz plik w zewnętrznym edytorze. |
|
||||
| `` <esc> `` | Wróć do panelu plików | |
|
||||
| `` <tab> `` | Przełącz widok | Przełącz na inny widok (zatwierdzone/niezatwierdzone zmiany). |
|
||||
| `` E `` | Edytuj fragment | Edytuj wybrany fragment w zewnętrznym edytorze. |
|
||||
| `` c `` | Commit | Zatwierdź zmiany zatwierdzone. |
|
||||
| `` w `` | Zatwierdź zmiany bez hooka pre-commit | |
|
||||
| `` C `` | Zatwierdź zmiany używając edytora git | |
|
||||
| `` <ctrl+f> `` | Znajdź bazowy commit do poprawki | Znajdź commit, na którym opierają się Twoje obecne zmiany, w celu poprawienia/zmiany commita. To pozwala Ci uniknąć przeglądania commitów w Twojej gałęzi jeden po drugim, aby zobaczyć, który commit powinien być poprawiony/zmieniony. Zobacz dokumentację: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` / `` | Szukaj w bieżącym widoku po tekście | |
|
||||
|
||||
## Panel potwierdzenia
|
||||
|
||||
| Key | Action | Info |
|
||||
@@ -296,7 +293,7 @@ _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) | |
|
||||
| `` <space> `` | Przełącz plik włączony w łatkę | Przełącz, czy plik jest włączony w niestandardową łatkę. Zobacz https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
|
||||
| `` a `` | Przełącz wszystkie pliki | Dodaj/usuń wszystkie pliki commita do niestandardowej łatki. Zobacz https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
|
||||
| `` <enter> `` | Wejdź do pliku / Przełącz zwiń katalog | Jeśli plik jest wybrany, wejdź do pliku, aby móc dodawać/usuwać poszczególne linie do niestandardowej łatki. Jeśli wybrany jest katalog, przełącz katalog. |
|
||||
| `` <enter> `` | Focus file diff / Toggle directory | If a file is selected, focus its diff so you can act on individual lines. If it is a directory, collapse or expand it. |
|
||||
| `` ` `` | Przełącz widok drzewa plików | 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 |
|
||||
|
||||
@@ -24,6 +24,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` _ `` | Modo de tela anterior | |
|
||||
| `` \| `` | Cycle diff renderers | Choose the next renderer in the list of configured diff renderers. |
|
||||
| `` \ `` | Cycle diff renderers (reverse) | Choose the previous renderer in the list of configured diff renderers. |
|
||||
| `` <ctrl+g> `` | Jump to file in diff | Pick one of the files of the diff shown in the main view, and scroll the main view to it. The focus stays in this panel. |
|
||||
| `` <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. |
|
||||
@@ -148,7 +149,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <ctrl+t> `` | Abrir ferramenta de diff externa (git difftool) | |
|
||||
| `` <space> `` | Alternar entre o arquivo incluído no patch | Alternar se o arquivo está incluído no patch personalizado. Veja https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
|
||||
| `` a `` | Alternar todos os arquivos | Adicionar/remover todos os arquivos de commit para atualização personalizada. Consulte https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
|
||||
| `` <enter> `` | Insira o arquivo / Alternar diretório recolhido | Se um arquivo estiver selecionado, insira o arquivo para que você possa adicionar/remover linhas individuais no patch personalizado. Se um diretório for selecionado, ative o diretório. |
|
||||
| `` <enter> `` | Focus file diff / Toggle directory | If a file is selected, focus its diff so you can act on individual lines. If it is a directory, collapse or expand it. |
|
||||
| `` ` `` | Alternar exibição de árvore de arquivo | 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'. |
|
||||
| `` - `` | Recolher todos os arquivos | Recolher todos os diretórios na árvore de arquivos |
|
||||
| `` = `` | Expandir todos os arquivos | Expandir todos os diretórios na árvore do arquivo |
|
||||
@@ -234,26 +235,21 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|-----|--------|-------------|
|
||||
| `` <mouse wheel down> (fn+up) `` | Rolar para baixo | |
|
||||
| `` <mouse wheel up> (fn+down) `` | Rolar para cima | |
|
||||
| `` <tab> `` | Mudar de visão | Alternar para outra visão (staged/não processadas alterações). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` / `` | Pesquisar na visualização atual por texto | |
|
||||
|
||||
## Painel Principal (preparação)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | Ir para o local anterior | |
|
||||
| `` <right>, l `` | Ir para o próximo trecho | |
|
||||
| `` v `` | Toggle range select | |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | Toggle hunk selection | Ativa/desativa modo linha por linha vs. modo de seleção por partes. |
|
||||
| `` <ctrl+o> `` | Copiar texto selecionado para área de transferência | |
|
||||
| `` v `` | Toggle range select | |
|
||||
| `` e `` | Editar arquivo | Abrir arquivo no editor externo. |
|
||||
| `` <space> `` | Etapa | Ativar/desativar seleção em staged/unstaged |
|
||||
| `` d `` | Descartar | Quando a mudança não desejada for selecionada, descarte a mudança usando `git reset`. Quando a mudança em fase é selecionada, despare a mudança. |
|
||||
| `` o `` | Abrir arquivo | Abrir arquivo no aplicativo padrão. |
|
||||
| `` e `` | Editar arquivo | Abrir arquivo no editor externo. |
|
||||
| `` <esc> `` | Retornar ao painel de arquivos | |
|
||||
| `` <tab> `` | Mudar de visão | Alternar para outra visão (staged/não processadas alterações). |
|
||||
| `` E `` | Editar hunk | Editar o local selecionado no editor externo. |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | Ir para o local anterior | |
|
||||
| `` <right>, l `` | Ir para o próximo trecho | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | Jump to file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` c `` | Commit | Submeter mudanças em staging |
|
||||
| `` w `` | Fazer commit de alterações sem pré-commit | |
|
||||
| `` C `` | Enviar alteração usando um editor Git | |
|
||||
@@ -284,22 +280,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
|
||||
| `` <esc> `` | Retornar ao painel de arquivos | |
|
||||
|
||||
## Painel principal (patch build)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | Ir para o local anterior | |
|
||||
| `` <right>, l `` | Ir para o próximo trecho | |
|
||||
| `` v `` | Toggle range select | |
|
||||
| `` a `` | Toggle hunk selection | Ativa/desativa modo linha por linha vs. modo de seleção por partes. |
|
||||
| `` <ctrl+o> `` | Copiar texto selecionado para área de transferência | |
|
||||
| `` o `` | Abrir arquivo | Abrir arquivo no aplicativo padrão. |
|
||||
| `` e `` | Editar arquivo | Abrir arquivo no editor externo. |
|
||||
| `` <space> `` | Alternar linhas no caminho | |
|
||||
| `` d `` | Remover linhas do 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> `` | Sair do construtor de patch personalizado | |
|
||||
| `` / `` | Pesquisar na visualização atual por texto | |
|
||||
|
||||
## Reflog
|
||||
|
||||
| Key | Action | Info |
|
||||
@@ -336,8 +316,25 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <tab> `` | Mudar de visão | Alternar para outra visão (staged/não processadas alterações). |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | Toggle hunk selection | Ativa/desativa modo linha por linha vs. modo de seleção por partes. |
|
||||
| `` v `` | Toggle range select | |
|
||||
| `` e `` | Editar arquivo | Abrir arquivo no editor externo. |
|
||||
| `` <space> `` | Etapa | Ativar/desativar seleção em staged/unstaged |
|
||||
| `` d `` | Descartar | Quando a mudança não desejada for selecionada, descarte a mudança usando `git reset`. Quando a mudança em fase é selecionada, despare a mudança. |
|
||||
| `` E `` | Editar hunk | Editar o local selecionado no editor externo. |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | Ir para o local anterior | |
|
||||
| `` <right>, l `` | Ir para o próximo trecho | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | Jump to file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` c `` | Commit | Submeter mudanças em staging |
|
||||
| `` w `` | Fazer commit de alterações sem pré-commit | |
|
||||
| `` C `` | Enviar alteração usando um editor Git | |
|
||||
| `` <ctrl+f> `` | Encontrar commit da base para corrigir | Encontre o commit em que as suas mudanças atuais estão se baseando, para alterar/consertar o commit. Isso poupa-te você de ter que olhar pelos commits da sua branch um por um para ver qual commit deve ser alterado/consertado<br>Veja a documentação:<br><https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` / `` | Pesquisar na visualização atual por texto | |
|
||||
|
||||
## Stash
|
||||
|
||||
@@ -10,8 +10,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <pgup>, K, <ctrl+u> (fn+up/shift+k) `` | Прокрутить вверх главную панель | |
|
||||
| `` <pgdown>, J, <ctrl+d> (fn+down/shift+j) `` | Прокрутить вниз главную панель | |
|
||||
| `` @ `` | Открыть меню журнала команд | View options for the command log e.g. show/hide the command log and focus the command log. |
|
||||
| `` P `` | Отправить изменения | Push the current branch to its upstream branch. If no upstream is configured, you will be prompted to configure an upstream branch. |
|
||||
| `` p `` | Получить и слить изменения | 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 the current branch to its upstream branch. If no upstream is configured, you will be prompted to configure an upstream branch. If other branches are stacked below the current one and have commits to push, you are offered to push those too. |
|
||||
| `` p `` | Получить и слить изменения | Pull changes from the remote for the current branch. If no upstream is configured, you will be prompted to configure an upstream branch. If other branches are stacked below the current one and have changed on the remote, you are offered to update those too. |
|
||||
| `` ) `` | 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 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'. |
|
||||
@@ -24,6 +24,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` _ `` | Предыдущий режим экрана | |
|
||||
| `` \| `` | Cycle diff renderers | Choose the next renderer in the list of configured diff renderers. |
|
||||
| `` \ `` | Cycle diff renderers (reverse) | Choose the previous renderer in the list of configured diff renderers. |
|
||||
| `` <ctrl+g> `` | Jump to file in diff | Pick one of the files of the diff shown in the main view, and scroll the main view to it. The focus stays in this panel. |
|
||||
| `` <esc> `` | Отменить | |
|
||||
| `` ? `` | Открыть меню | |
|
||||
| `` <ctrl+s> `` | Просмотреть параметры фильтрации по пути | View options for filtering the commit log, so that only commits matching the filter are shown. |
|
||||
@@ -73,26 +74,21 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <tab> `` | Переключиться на другую панель (проиндексированные/непроиндексированные изменения) | Switch to other view (staged/unstaged changes). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` / `` | Найти | |
|
||||
|
||||
## Главная панель (Индексирование)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | Выбрать предыдущую часть | |
|
||||
| `` <right>, l `` | Выбрать следующую часть | |
|
||||
| `` v `` | Переключить выборку перетаскивания | |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` <ctrl+o> `` | Скопировать выделенный текст в буфер обмена | |
|
||||
| `` v `` | Переключить выборку перетаскивания | |
|
||||
| `` e `` | Редактировать файл | Open file in external editor. |
|
||||
| `` <space> `` | Переключить индекс | Переключить строку в проиндексированные / непроиндексированные |
|
||||
| `` d `` | Отменить изменение (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
|
||||
| `` o `` | Открыть файл | Open file in default application. |
|
||||
| `` e `` | Редактировать файл | Open file in external editor. |
|
||||
| `` <esc> `` | Вернуться к панели файлов | |
|
||||
| `` <tab> `` | Переключиться на другую панель (проиндексированные/непроиндексированные изменения) | Switch to other view (staged/unstaged changes). |
|
||||
| `` E `` | Изменить эту часть | Edit selected hunk in external editor. |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | Выбрать предыдущую часть | |
|
||||
| `` <right>, l `` | Выбрать следующую часть | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | Jump to file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` c `` | Сохранить изменения | Commit staged changes. |
|
||||
| `` w `` | Закоммитить изменения без предварительного хука коммита | |
|
||||
| `` C `` | Сохранить изменения с помощью редактора git | |
|
||||
@@ -105,8 +101,25 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|-----|--------|-------------|
|
||||
| `` <mouse wheel down> (fn+up) `` | Прокрутить вниз | |
|
||||
| `` <mouse wheel up> (fn+down) `` | Прокрутить вверх | |
|
||||
| `` <tab> `` | Переключиться на другую панель (проиндексированные/непроиндексированные изменения) | Switch to other view (staged/unstaged changes). |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | Переключить выборку перетаскивания | |
|
||||
| `` e `` | Редактировать файл | Open file in external editor. |
|
||||
| `` <space> `` | Переключить индекс | Переключить строку в проиндексированные / непроиндексированные |
|
||||
| `` d `` | Отменить изменение (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
|
||||
| `` E `` | Изменить эту часть | Edit selected hunk in external editor. |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | Выбрать предыдущую часть | |
|
||||
| `` <right>, l `` | Выбрать следующую часть | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | Jump to file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` c `` | Сохранить изменения | Commit staged changes. |
|
||||
| `` w `` | Закоммитить изменения без предварительного хука коммита | |
|
||||
| `` C `` | Сохранить изменения с помощью редактора git | |
|
||||
| `` <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> |
|
||||
| `` / `` | Найти | |
|
||||
|
||||
## Главная панель (Слияние)
|
||||
@@ -125,22 +138,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
|
||||
| `` <esc> `` | Вернуться к панели файлов | |
|
||||
|
||||
## Главная панель (сборка патчей)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | Выбрать предыдущую часть | |
|
||||
| `` <right>, l `` | Выбрать следующую часть | |
|
||||
| `` v `` | Переключить выборку перетаскивания | |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` <ctrl+o> `` | Скопировать выделенный текст в буфер обмена | |
|
||||
| `` o `` | Открыть файл | Open file in default application. |
|
||||
| `` e `` | Редактировать файл | Open file in external editor. |
|
||||
| `` <space> `` | Добавить/удалить строку(и) для патча | |
|
||||
| `` 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> `` | Выйти из сборщика пользовательских патчей | |
|
||||
| `` / `` | Найти | |
|
||||
|
||||
## Журнал ссылок (Reflog)
|
||||
|
||||
| Key | Action | Info |
|
||||
@@ -223,7 +220,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` d `` | Delete | View delete options for local/remote branch. |
|
||||
| `` r `` | Перебазировать переключённую ветку на эту ветку | Rebase the checked-out branch onto the selected branch. |
|
||||
| `` M `` | Слияние с текущей переключённой веткой | View options for merging the selected item into the current branch (regular merge, squash merge) |
|
||||
| `` f `` | Перемотать эту ветку вперёд из её upstream-ветки | Fast-forward selected branch from its upstream. |
|
||||
| `` f `` | Перемотать эту ветку вперёд из её upstream-ветки | Fast-forward selected branch from its upstream. If the branch has diverged from its upstream because the upstream branch was rewritten, and it has no commits of its own, it is reset to its upstream instead. This needs reflogs to be enabled; a bare repository doesn't keep them by default (core.logAllRefUpdates). |
|
||||
| `` T `` | Создать тег | |
|
||||
| `` s `` | Порядок сортировки | |
|
||||
| `` g `` | Просмотреть параметры сброса | |
|
||||
@@ -304,7 +301,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` <space> `` | Переключить файлы включённые в патч | Toggle whether the file is included in the custom patch. See https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
|
||||
| `` a `` | Переключить все файлы, включённые в патч | Add/remove all commit's files to custom patch. See https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
|
||||
| `` <enter> `` | Введите файл, чтобы добавить выбранные строки в патч (или свернуть каталог переключения) | 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. |
|
||||
| `` <enter> `` | Focus file diff / Toggle directory | If a file is selected, focus its diff so you can act on individual lines. If it is a directory, collapse or expand it. |
|
||||
| `` ` `` | Переключить вид дерева файлов | 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 |
|
||||
@@ -389,7 +386,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` s `` | Stash | Stash all changes. For other variations of stashing, use the view stash options keybinding. |
|
||||
| `` S `` | Просмотреть параметры хранилища | View stash options (e.g. stash all, stash staged, stash unstaged). |
|
||||
| `` a `` | Все проиндексированные/непроиндексированные | Toggle staged/unstaged for all files in working tree. |
|
||||
| `` <enter> `` | Проиндексировать отдельные части/строки для файла или свернуть/развернуть для каталога | 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. |
|
||||
| `` <enter> `` | Проиндексировать отдельные части/строки для файла или свернуть/развернуть для каталога | If the selected item is a file, focus its diff so you can act on individual hunks or lines. If it is a directory, collapse or expand it. |
|
||||
| `` d `` | Просмотреть параметры «отмены изменении» | View options for discarding changes to the selected file. |
|
||||
| `` g `` | Просмотреть параметры сброса upstream-ветки | |
|
||||
| `` D `` | Reset | View reset options for working tree (e.g. nuking the working tree). |
|
||||
|
||||
@@ -24,6 +24,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` _ `` | 上一屏模式 | |
|
||||
| `` \| `` | Cycle diff renderers | Choose the next renderer in the list of configured diff renderers. |
|
||||
| `` \ `` | Cycle diff renderers (reverse) | Choose the previous renderer in the list of configured diff renderers. |
|
||||
| `` <ctrl+g> `` | Jump to file in diff | Pick one of the files of the diff shown in the main view, and scroll the main view to it. The focus stays in this panel. |
|
||||
| `` <esc> `` | 取消 | |
|
||||
| `` ? `` | 打开菜单 | |
|
||||
| `` <ctrl+s> `` | 查看按路径过滤选项 | 查看用于过滤提交日志的选项,以便仅显示与过滤器匹配的提交。 |
|
||||
@@ -178,7 +179,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <ctrl+t> `` | 使用外部差异比较工具(git difftool) | |
|
||||
| `` <space> `` | 补丁中包含的切换文件 | 切换文件是否包含在自定义补丁中。请参阅 https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches。 |
|
||||
| `` a `` | 操作所有文件 | 添加或删除所有提交中的文件到自定义的补丁中。请参阅 https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches。 |
|
||||
| `` <enter> `` | 输入文件以将所选行添加到补丁中(或切换目录折叠) | 如果已选择一个文件,则Enter进入该文件,以便您可以向自定义补丁添加/删除单独的行。如果选择了目录,则切换目录。 |
|
||||
| `` <enter> `` | Focus file diff / Toggle directory | If a file is selected, focus its diff so you can act on individual lines. If it is a directory, collapse or expand it. |
|
||||
| `` ` `` | 切换文件树视图 | 在平面布局和树布局之间切换文件视图。平面布局在单个列表中显示所有文件路径,树布局按目录分组文件。<br><br>可以在配置文件中使用 'gui.showFileTree' 键更改默认设置。 |
|
||||
| `` - `` | 折叠全部文件 | 折叠文件树中的全部目录 |
|
||||
| `` = `` | 展开全部文件 | 展开文件树中的全部目录 |
|
||||
@@ -249,22 +250,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <enter> `` | 查看提交 | |
|
||||
| `` / `` | 通过文本过滤当前视图 | |
|
||||
|
||||
## 构建补丁中
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | 选择上一个区块 | |
|
||||
| `` <right>, l `` | 选择下一个区块 | |
|
||||
| `` v `` | 切换拖动选择 | |
|
||||
| `` a `` | 切换代码块选择 | 切换逐行选择与代码块选择模式。 |
|
||||
| `` <ctrl+o> `` | 复制选中文本到剪贴板 | |
|
||||
| `` o `` | 打开文件 | 使用默认程序打开该文件 |
|
||||
| `` e `` | 编辑文件 | 使用外部编辑器打开文件 |
|
||||
| `` <space> `` | 添加/移除 行到补丁 | |
|
||||
| `` d `` | 从提交中移除行 | 从本次提交中移除所选行。此操作会在后台运行交互式变基,因此如果后续提交也修改了这些行,您可能会遇到合并冲突。 |
|
||||
| `` <esc> `` | 退出逐行模式 | |
|
||||
| `` / `` | 开始搜索 | |
|
||||
|
||||
## 标签
|
||||
|
||||
| Key | Action | Info |
|
||||
@@ -285,8 +270,25 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <tab> `` | 切换到其他面板 | 切换到其他视图(已暂存/未暂存的变更) |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | 切换代码块选择 | 切换逐行选择与代码块选择模式。 |
|
||||
| `` v `` | 切换拖动选择 | |
|
||||
| `` e `` | 编辑文件 | 使用外部编辑器打开文件 |
|
||||
| `` <space> `` | 切换暂存状态 | 切换行暂存状态 |
|
||||
| `` d `` | 取消变更(git reset) | 当选择未暂存的变更时,使用git reset丢弃该变更。当选择已暂存的变更时,取消暂存该变更 |
|
||||
| `` E `` | 编辑代码块 | 在外部编辑器中编辑选中的代码块 |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | 选择上一个区块 | |
|
||||
| `` <right>, l `` | 选择下一个区块 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | Jump to file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | 退出回到侧边面板 | |
|
||||
| `` c `` | 提交变更 | 提交暂存文件 |
|
||||
| `` w `` | 提交变更而无需预先提交钩子 | |
|
||||
| `` C `` | 使用 Git 编辑器提交变更 | |
|
||||
| `` <ctrl+f> `` | 找到用于修复的基准提交 | 找到您当前变更所基于的提交,以便于修正/改进该提交。这样做可以省去您逐一查看分支提交来确定应该修正/改进哪个提交的麻烦。请参阅文档: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` / `` | 开始搜索 | |
|
||||
|
||||
## 正在合并
|
||||
@@ -305,36 +307,31 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` M `` | 查看合并冲突选项 | 查看用于解决合并冲突的选项。 |
|
||||
| `` <esc> `` | 返回文件面板 | |
|
||||
|
||||
## 正在暂存
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | 选择上一个区块 | |
|
||||
| `` <right>, l `` | 选择下一个区块 | |
|
||||
| `` v `` | 切换拖动选择 | |
|
||||
| `` a `` | 切换代码块选择 | 切换逐行选择与代码块选择模式。 |
|
||||
| `` <ctrl+o> `` | 复制选中文本到剪贴板 | |
|
||||
| `` <space> `` | 切换暂存状态 | 切换行暂存状态 |
|
||||
| `` d `` | 取消变更(git reset) | 当选择未暂存的变更时,使用git reset丢弃该变更。当选择已暂存的变更时,取消暂存该变更 |
|
||||
| `` o `` | 打开文件 | 使用默认程序打开该文件 |
|
||||
| `` e `` | 编辑文件 | 使用外部编辑器打开文件 |
|
||||
| `` <esc> `` | 返回文件面板 | |
|
||||
| `` <tab> `` | 切换到其他面板 | 切换到其他视图(已暂存/未暂存的变更) |
|
||||
| `` E `` | 编辑代码块 | 在外部编辑器中编辑选中的代码块 |
|
||||
| `` c `` | 提交变更 | 提交暂存文件 |
|
||||
| `` w `` | 提交变更而无需预先提交钩子 | |
|
||||
| `` C `` | 使用 Git 编辑器提交变更 | |
|
||||
| `` <ctrl+f> `` | 找到用于修复的基准提交 | 找到您当前变更所基于的提交,以便于修正/改进该提交。这样做可以省去您逐一查看分支提交来确定应该修正/改进哪个提交的麻烦。请参阅文档: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` / `` | 开始搜索 | |
|
||||
|
||||
## 正常
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <mouse wheel down> (fn+up) `` | 向下滚动 | |
|
||||
| `` <mouse wheel up> (fn+down) `` | 向上滚动 | |
|
||||
| `` <tab> `` | 切换到其他面板 | 切换到其他视图(已暂存/未暂存的变更) |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | 切换代码块选择 | 切换逐行选择与代码块选择模式。 |
|
||||
| `` v `` | 切换拖动选择 | |
|
||||
| `` e `` | 编辑文件 | 使用外部编辑器打开文件 |
|
||||
| `` <space> `` | 切换暂存状态 | 切换行暂存状态 |
|
||||
| `` d `` | 取消变更(git reset) | 当选择未暂存的变更时,使用git reset丢弃该变更。当选择已暂存的变更时,取消暂存该变更 |
|
||||
| `` E `` | 编辑代码块 | 在外部编辑器中编辑选中的代码块 |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | 选择上一个区块 | |
|
||||
| `` <right>, l `` | 选择下一个区块 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | Jump to file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | 退出回到侧边面板 | |
|
||||
| `` c `` | 提交变更 | 提交暂存文件 |
|
||||
| `` w `` | 提交变更而无需预先提交钩子 | |
|
||||
| `` C `` | 使用 Git 编辑器提交变更 | |
|
||||
| `` <ctrl+f> `` | 找到用于修复的基准提交 | 找到您当前变更所基于的提交,以便于修正/改进该提交。这样做可以省去您逐一查看分支提交来确定应该修正/改进哪个提交的麻烦。请参阅文档: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` / `` | 开始搜索 | |
|
||||
|
||||
## 状态
|
||||
|
||||
@@ -9,28 +9,29 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <ctrl+r> `` | 切換到最近使用的版本庫 | |
|
||||
| `` <pgup>, K, <ctrl+u> (fn+up/shift+k) `` | 向上捲動主面板 | |
|
||||
| `` <pgdown>, J, <ctrl+d> (fn+down/shift+j) `` | 向下捲動主面板 | |
|
||||
| `` @ `` | 開啟命令記錄選單 | View options for the command log e.g. show/hide the command log and focus the command log. |
|
||||
| `` @ `` | 開啟命令記錄選單 | 檢視命令日誌的選項,例如顯示/隱藏命令日誌以及聚焦命令日誌。 |
|
||||
| `` P `` | 推送 | 推送到遠端。如果沒有設定遠端,會開啟設定視窗。 |
|
||||
| `` p `` | 拉取 | 從遠端同步當前分支。如果沒有設定遠端,會開啟設定視窗。 |
|
||||
| `` ) `` | 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 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 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. |
|
||||
| `` ) `` | 提高重新命名相似度閾值 | 提高將刪除和新增對視為重新命名所需的相似度閾值。<br><br>預設值可在設定檔中透過鍵 'git.renameSimilarityThreshold' 更改。 |
|
||||
| `` ( `` | 降低重新命名相似度閾值 | 降低將刪除和新增對視為重新命名所需的相似度閾值。<br><br>預設值可在設定檔中透過鍵 'git.renameSimilarityThreshold' 更改。 |
|
||||
| `` } `` | 增加差異檢視中顯示變更周圍上下文的大小 | 增加差異檢視中變更周圍顯示的上下文量。<br><br>預設值可在設定檔中透過鍵 'git.diffContextSize' 更改。 |
|
||||
| `` { `` | 減小差異檢視中顯示變更周圍上下文的大小 | 減少差異檢視中變更周圍顯示的上下文量。<br><br>預設值可在設定檔中透過鍵 'git.diffContextSize' 更改。 |
|
||||
| `` : `` | 執行 Shell 命令 | 調出可輸入shell命令執行的提示符。 |
|
||||
| `` <ctrl+p> `` | 檢視自訂補丁選項 | |
|
||||
| `` m `` | 查看合併/變基選項 | View options to abort/continue/skip the current merge/rebase. |
|
||||
| `` 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`. |
|
||||
| `` m `` | 查看合併/變基選項 | 檢視目前合併或變基的中止、繼續、跳過選項。 |
|
||||
| `` R `` | 重新整理 | 重新整理Git狀態(即在背景執行`git status`、`git branch`等命令以更新面板內容)。此操作不會執行`git fetch`。 |
|
||||
| `` + `` | 下一個螢幕模式(常規/半螢幕/全螢幕) | |
|
||||
| `` _ `` | 上一個螢幕模式 | |
|
||||
| `` \| `` | Cycle diff renderers | Choose the next renderer in the list of configured diff renderers. |
|
||||
| `` \ `` | Cycle diff renderers (reverse) | Choose the previous renderer in the list of configured diff renderers. |
|
||||
| `` \| `` | 切換差異渲染器 | 選擇已設定的差異渲染器清單中的下一個渲染器。 |
|
||||
| `` \ `` | 切換差異渲染器(反向) | 選擇已設定的差異渲染器清單中的上一個渲染器。 |
|
||||
| `` <ctrl+g> `` | Jump to file in diff | Pick one of the files of the diff shown in the main view, and scroll the main view to it. The focus stays in this panel. |
|
||||
| `` <esc> `` | 取消 | |
|
||||
| `` ? `` | 開啟選單 | |
|
||||
| `` <ctrl+s> `` | 檢視篩選路徑選項 | View options for filtering the commit log, so that only commits matching the filter are shown. |
|
||||
| `` W, <ctrl+e> `` | 開啟差異比較選單 | View options relating to diffing two refs e.g. diffing against selected ref, entering ref to diff against, and reversing the diff direction. |
|
||||
| `` <ctrl+s> `` | 檢視篩選路徑選項 | 檢視用於過濾提交日誌的選項,以便僅顯示與過濾器匹配的提交。 |
|
||||
| `` W, <ctrl+e> `` | 開啟差異比較選單 | 檢視與比較兩個引用相關的選項,例如與選定的 ref 進行比較,輸入要比較的 ref,然後反轉比較方向。 |
|
||||
| `` 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'. |
|
||||
| `` <ctrl+z> `` | 掛起應用程式 | |
|
||||
| `` <ctrl+w> `` | 切換是否在差異檢視中顯示空格變更 | 切換是否在差異檢視中顯示空白字元更改。<br><br>預設值可在設定檔中透過鍵 'git.ignoreWhitespaceInDiffView' 更改。 |
|
||||
| `` <alt+shift+c> `` | 編輯設定檔案 | 使用外部編輯器開啟 |
|
||||
| `` z `` | 復原 | 將使用 reflog 確任 git 指令以復原。這不包括工作區更改;只考慮提交。 |
|
||||
| `` Z `` | 取消復原 | 將使用 reflog 確任 git 指令以重作。這不包括工作區更改;只考慮提交。 |
|
||||
@@ -44,45 +45,39 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <, <home> `` | 捲動到頂部 | |
|
||||
| `` >, <end> `` | 捲動到底部 | |
|
||||
| `` v `` | 切換拖曳選擇 | |
|
||||
| `` <shift+down> `` | Range select down | |
|
||||
| `` <shift+up> `` | Range select up | |
|
||||
| `` <shift+down> `` | 向下擴充套件選擇範圍 | |
|
||||
| `` <shift+up> `` | 向上擴充套件選擇範圍 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
| `` H `` | 向左捲動 | |
|
||||
| `` L `` | 向右捲動 | |
|
||||
| `` ] `` | 下一個索引標籤 | |
|
||||
| `` [ `` | 上一個索引標籤 | |
|
||||
|
||||
## Input prompt
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <enter> `` | 確認 | |
|
||||
| `` <esc> `` | 關閉/取消 | |
|
||||
|
||||
## 主面板 (補丁生成)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | 選擇上一段 | |
|
||||
| `` <right>, l `` | 選擇下一段 | |
|
||||
| `` v `` | 切換拖曳選擇 | |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` <ctrl+o> `` | 複製所選文本至剪貼簿 | |
|
||||
| `` o `` | 開啟檔案 | 使用預設軟體開啟 |
|
||||
| `` e `` | 編輯檔案 | 使用外部編輯器開啟 |
|
||||
| `` <space> `` | 向 (或從) 補丁中添加/刪除行 | |
|
||||
| `` 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> `` | 退出自訂補丁建立器 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 主面板(一般)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <mouse wheel down> (fn+up) `` | 向下捲動 | |
|
||||
| `` <mouse wheel up> (fn+down) `` | 向上捲動 | |
|
||||
| `` <tab> `` | 切換至另一個面板 (已預存/未預存更改) | Switch to other view (staged/unstaged changes). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | 切換程式碼塊選擇 | 切換逐行選擇與程式碼塊選擇模式。 |
|
||||
| `` v `` | 切換拖曳選擇 | |
|
||||
| `` e `` | 編輯檔案 | 使用外部編輯器開啟 |
|
||||
| `` <space> `` | 切換預存 | 切換現有行的狀態 (已預存/未預存) |
|
||||
| `` d `` | 刪除變更 (git reset) | 選取未暫存的變更時,使用 `git reset` 捨棄變更。選取已暫存的變更時,取消暫存變更。 |
|
||||
| `` E `` | 編輯程式碼塊 | 在外部編輯器中編輯選中的程式碼塊。 |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | 選擇上一段 | |
|
||||
| `` <right>, l `` | 選擇下一段 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | Jump to file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | 退出回到側邊面板 | |
|
||||
| `` c `` | 提交變更 | 提交暫存區變更 |
|
||||
| `` w `` | 沒有預提交 hook 就提交更改 | |
|
||||
| `` C `` | 使用 git 編輯器提交變更 | |
|
||||
| `` <ctrl+f> `` | 尋找 fixup 的基礎提交 | 找出目前變更所依據的提交,以便 amend/fixup。這樣不必逐一檢視分支中的提交來找出要 amend/fixup 的提交。請見文件:<https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 主面板(合併)
|
||||
@@ -90,39 +85,17 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <space> `` | 挑選程式碼片段 | |
|
||||
| `` b `` | Pick both hunks | |
|
||||
| `` b `` | 選取兩個區塊 | |
|
||||
| `` <up>, k `` | 選擇上一段 | |
|
||||
| `` <down>, j `` | 選擇下一段 | |
|
||||
| `` <left>, h `` | 選擇上一個衝突 | |
|
||||
| `` <right>, l `` | 選擇下一個衝突 | |
|
||||
| `` z `` | 復原 | Undo last merge conflict resolution. |
|
||||
| `` z `` | 復原 | 撤消上次合併衝突解決。 |
|
||||
| `` e `` | 編輯檔案 | 使用外部編輯器開啟 |
|
||||
| `` o `` | 開啟檔案 | 使用預設軟體開啟 |
|
||||
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
|
||||
| `` M `` | 檢視合併衝突選項 | 檢視用於解決合併衝突的選項。 |
|
||||
| `` <esc> `` | 返回檔案面板 | |
|
||||
|
||||
## 主面板(預存)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | 選擇上一段 | |
|
||||
| `` <right>, l `` | 選擇下一段 | |
|
||||
| `` v `` | 切換拖曳選擇 | |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` <ctrl+o> `` | 複製所選文本至剪貼簿 | |
|
||||
| `` <space> `` | 切換預存 | 切換現有行的狀態 (已預存/未預存) |
|
||||
| `` d `` | 刪除變更 (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
|
||||
| `` o `` | 開啟檔案 | 使用預設軟體開啟 |
|
||||
| `` e `` | 編輯檔案 | 使用外部編輯器開啟 |
|
||||
| `` <esc> `` | 返回檔案面板 | |
|
||||
| `` <tab> `` | 切換至另一個面板 (已預存/未預存更改) | Switch to other view (staged/unstaged changes). |
|
||||
| `` E `` | 編輯程式碼塊 | Edit selected hunk in external editor. |
|
||||
| `` c `` | 提交變更 | 提交暫存區變更 |
|
||||
| `` w `` | 沒有預提交 hook 就提交更改 | |
|
||||
| `` C `` | 使用 git 編輯器提交變更 | |
|
||||
| `` <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> |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 功能表
|
||||
|
||||
| Key | Action | Info |
|
||||
@@ -135,19 +108,19 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <ctrl+o> `` | Copy abbreviated commit hash to clipboard | |
|
||||
| `` <space> `` | 檢出 | Checkout the selected commit as a detached HEAD. |
|
||||
| `` y `` | 複製提交屬性 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` <ctrl+o> `` | 複製縮略提交雜湊值到剪貼簿 | |
|
||||
| `` <space> `` | 檢出 | 檢出所選擇的提交作為分離HEAD。 |
|
||||
| `` y `` | 複製提交屬性 | 複製提交屬性到剪貼簿(如hash、URL、diff、訊息、作者)。 |
|
||||
| `` 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. |
|
||||
| `` N `` | 移動提交至新分支 | 建立一個新分支,並將目前分支未推送的提交移動到該分支。如果您打算開始新工作但忘記先建立新分支,這會很有用。<br><br>請注意,此操作忽略選擇,新分支總是從主分支建立或堆疊在目前分支之上(您可以選擇哪種方式)。 |
|
||||
| `` w `` | 新建工作樹 | |
|
||||
| `` g `` | 檢視重設選項 | 檢視重置選項 (soft/mixed/hard) 用於重置到選擇項。 |
|
||||
| `` C `` | 複製提交 (揀選) | 標記提交為已複製。然後,在本地提交檢視中,您可以按 `V` (Cherry-Pick) 將已複製的提交貼上到已檢出的分支中。任何時候都可以按 `<esc>` 來取消選擇。 |
|
||||
| `` <ctrl+r> `` | 重設選定的揀選 (複製) 提交 | |
|
||||
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` * `` | 選擇目前分支的提交 | |
|
||||
| `` 0 `` | 聚焦主檢視 | |
|
||||
| `` <enter> `` | 檢視所選項目的檔案 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
@@ -156,12 +129,12 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <ctrl+o> `` | 複製子模組名稱到剪貼簿 | |
|
||||
| `` <enter> `` | Enter | 進入子模組 |
|
||||
| `` d `` | Remove | Remove the selected submodule and its corresponding directory. |
|
||||
| `` u `` | Update | 更新子模組 |
|
||||
| `` <enter> `` | 進入 | 進入子模組 |
|
||||
| `` d `` | 刪除 | 刪除選定的子模組及其相應的目錄。 |
|
||||
| `` u `` | 更新 | 更新子模組 |
|
||||
| `` n `` | 新增子模組 | |
|
||||
| `` e `` | 更新子模組 URL | |
|
||||
| `` i `` | Initialize | 初始化子模組 |
|
||||
| `` i `` | 初始化 | 初始化子模組 |
|
||||
| `` b `` | 查看批量子模組選項 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
@@ -169,27 +142,27 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` n `` | New worktree | |
|
||||
| `` <space> `` | Switch | Switch to the selected worktree. |
|
||||
| `` n `` | 新建工作樹 | |
|
||||
| `` <space> `` | 切換 | 切換到選中的工作樹。 |
|
||||
| `` o `` | 在編輯器中開啟 | |
|
||||
| `` 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. |
|
||||
| `` d `` | 刪除 | 刪除選定的工作樹。這將刪除工作樹的目錄以及 .git 目錄中有關工作樹的後設資料。 |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 提交
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <ctrl+o> `` | Copy abbreviated commit hash to clipboard | |
|
||||
| `` <ctrl+o> `` | 複製縮略提交雜湊值到剪貼簿 | |
|
||||
| `` <ctrl+r> `` | 重設選定的揀選 (複製) 提交 | |
|
||||
| `` b `` | 查看二分選項 | |
|
||||
| `` s `` | 壓縮 (Squash) | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
|
||||
| `` f `` | 修復 (Fixup) | Meld the selected commit into the commit below it. Similar to squash, but the selected commit's message will be discarded. |
|
||||
| `` c `` | Set fixup message | Set the message option for the fixup commit. The -C option means to use this commit's message instead of the target commit's message. |
|
||||
| `` s `` | 壓縮 (Squash) | 將已選提交壓縮到該提交之下。這些選定的提交的訊息會附加到該提交的訊息之下。 |
|
||||
| `` f `` | 修復 (Fixup) | 將選定的提交合併到其下面的提交中。與壓縮類似,但所選提交的訊息將被丟棄。 |
|
||||
| `` c `` | 設定修復提交資訊 | 設定修復提交的資訊選項。-C 選項表示使用此提交的資訊,而非目標提交的資訊。 |
|
||||
| `` r `` | 改寫提交 | 改寫選中的提交訊息 |
|
||||
| `` R `` | 使用編輯器改寫提交 | |
|
||||
| `` d `` | 刪除提交 | Drop the selected commit. This will remove the commit from the branch via a rebase. If the commit makes changes that later commits depend on, you may need to resolve merge conflicts. |
|
||||
| `` d `` | 刪除提交 | 刪除選中的提交。這將透過變基從分支中刪除該提交,如果該提交修改的內容依賴於後續的提交,則需要解決合併衝突。 |
|
||||
| `` e `` | 編輯(開始互動變基) | 編輯提交 |
|
||||
| `` i `` | 開始互動變基 | Start an interactive rebase for the commits on your branch. This will include all commits from the HEAD commit down to the first merge commit or main branch commit.<br>If you would instead like to start an interactive rebase from the selected commit, press `e`. |
|
||||
| `` i `` | 開始互動變基 | 為分支上的提交啟動互動式變基。這將包括從 HEAD 提交到第一個合併提交或主分支提交的所有提交。<br>如果您想從所選提交啟動互動式變基,請按 `e`。 |
|
||||
| `` p `` | 挑選 | 挑選提交 (於變基過程中) |
|
||||
| `` F `` | 建立修復提交 | 為此提交建立修復提交 |
|
||||
| `` S `` | 壓縮上方所有「fixup」提交(自動壓縮) | 是否壓縮上方 {{.commit}} 所有「fixup」提交? |
|
||||
@@ -198,22 +171,22 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` V `` | 貼上提交 (揀選) | |
|
||||
| `` B `` | 為了變基已標注提交為基準提交 | 請為了下一次變基選擇一項基準提交;此將執行 `git rebase --onto`。 |
|
||||
| `` A `` | 修改 | 使用已預存的更改修正提交 |
|
||||
| `` a `` | 設定/重設提交作者 | Set/Reset commit author or set co-author. |
|
||||
| `` t `` | 還原 | Create a revert commit for the selected commit, which applies the selected commit's changes in reverse. |
|
||||
| `` T `` | 打標籤到提交 | Create a new tag pointing at the selected commit. You'll be prompted to enter a tag name and optional description. |
|
||||
| `` <ctrl+l> `` | 開啟記錄選單 | View options for commit log e.g. changing sort order, hiding the git graph, showing the whole git graph. |
|
||||
| `` G `` | Open pull request in browser | |
|
||||
| `` <space> `` | 檢出 | Checkout the selected commit as a detached HEAD. |
|
||||
| `` y `` | 複製提交屬性 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` a `` | 設定/重設提交作者 | 設定或重置提交的作者,或新增其他作者。 |
|
||||
| `` t `` | 還原 | 為所選提交建立還原提交,這會反向應用所選提交的更改。 |
|
||||
| `` T `` | 打標籤到提交 | 建立一個新標籤指向所選提交。您可以在彈窗中輸入標籤名稱和描述(可選)。 |
|
||||
| `` <ctrl+l> `` | 開啟記錄選單 | 檢視提交日誌的選項,例如更改排序順序、隱藏 git graph、顯示整個 git graph。 |
|
||||
| `` G `` | 在瀏覽器中開啟拉取請求 | |
|
||||
| `` <space> `` | 檢出 | 檢出所選擇的提交作為分離HEAD。 |
|
||||
| `` y `` | 複製提交屬性 | 複製提交屬性到剪貼簿(如hash、URL、diff、訊息、作者)。 |
|
||||
| `` 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. |
|
||||
| `` N `` | 移動提交至新分支 | 建立一個新分支,並將目前分支未推送的提交移動到該分支。如果您打算開始新工作但忘記先建立新分支,這會很有用。<br><br>請注意,此操作忽略選擇,新分支總是從主分支建立或堆疊在目前分支之上(您可以選擇哪種方式)。 |
|
||||
| `` w `` | 新建工作樹 | |
|
||||
| `` g `` | 檢視重設選項 | 檢視重置選項 (soft/mixed/hard) 用於重置到選擇項。 |
|
||||
| `` C `` | 複製提交 (揀選) | 標記提交為已複製。然後,在本地提交檢視中,您可以按 `V` (Cherry-Pick) 將已複製的提交貼上到已檢出的分支中。任何時候都可以按 `<esc>` 來取消選擇。 |
|
||||
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` * `` | 選擇目前分支的提交 | |
|
||||
| `` 0 `` | 聚焦主檢視 | |
|
||||
| `` <enter> `` | 檢視所選項目的檔案 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
@@ -231,30 +204,30 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <ctrl+o> `` | 複製檔案名稱到剪貼簿 | |
|
||||
| `` y `` | 複製到剪貼簿 | |
|
||||
| `` c `` | 檢出 | 檢出檔案 |
|
||||
| `` d `` | 捨棄 | Discard this commit's changes to this file. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes this file. |
|
||||
| `` d `` | 捨棄 | 放棄對此檔案的提交變更。 |
|
||||
| `` o `` | 開啟檔案 | 使用預設軟體開啟 |
|
||||
| `` e `` | 編輯 | 使用外部編輯器開啟 |
|
||||
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
|
||||
| `` <space> `` | 切換檔案是否包含在補丁中 | Toggle whether the file is included in the custom patch. See https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
|
||||
| `` a `` | 切換所有檔案是否包含在補丁中 | Add/remove all commit's files to custom patch. See https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
|
||||
| `` <enter> `` | 輸入檔案以將選定的行添加至補丁(或切換目錄折疊) | 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 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 |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <space> `` | 切換檔案是否包含在補丁中 | 切換檔案是否包含在自定義補丁中。請參閱 https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches。 |
|
||||
| `` a `` | 切換所有檔案是否包含在補丁中 | 新增或刪除所有提交中的檔案到自定義的補丁中。請參閱 https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches。 |
|
||||
| `` <enter> `` | Focus file diff / Toggle directory | If a file is selected, focus its diff so you can act on individual lines. If it is a directory, collapse or expand it. |
|
||||
| `` ` `` | 顯示檔案樹狀視圖 | 在平面佈局和樹佈局之間切換檔案檢視。平面佈局在單個列表中顯示所有檔案路徑,樹佈局按目錄分組檔案。<br><br>可以在設定檔中使用 'gui.showFileTree' 鍵更改預設設定。 |
|
||||
| `` - `` | 摺疊全部檔案 | 摺疊檔案樹中的全部目錄 |
|
||||
| `` = `` | 展開全部檔案 | 展開檔案樹中的全部目錄 |
|
||||
| `` 0 `` | 聚焦主檢視 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 收藏 (Stash)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <space> `` | 套用 | Apply the stash entry to your working directory. |
|
||||
| `` 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 | |
|
||||
| `` <space> `` | 套用 | 將貯藏項應用到您的工作目錄。 |
|
||||
| `` g `` | 還原 | 將儲存項應用到工作目錄並刪除儲存項。 |
|
||||
| `` d `` | 捨棄 | 從貯藏列表中刪除該貯藏項。 |
|
||||
| `` n `` | 新分支 | 從選定的貯藏項建立一個新分支。這是透過 git 檢查建立貯藏項的提交,從該提交建立一個新分支,然後將貯藏項作為附加提交應用到新分支來實現的。 |
|
||||
| `` w `` | 新建工作樹 | |
|
||||
| `` r `` | 重新命名收藏 | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` 0 `` | 聚焦主檢視 | |
|
||||
| `` <enter> `` | 檢視所選項目的檔案 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
@@ -262,19 +235,19 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <ctrl+o> `` | Copy abbreviated commit hash to clipboard | |
|
||||
| `` <space> `` | 檢出 | Checkout the selected commit as a detached HEAD. |
|
||||
| `` y `` | 複製提交屬性 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` <ctrl+o> `` | 複製縮略提交雜湊值到剪貼簿 | |
|
||||
| `` <space> `` | 檢出 | 檢出所選擇的提交作為分離HEAD。 |
|
||||
| `` y `` | 複製提交屬性 | 複製提交屬性到剪貼簿(如hash、URL、diff、訊息、作者)。 |
|
||||
| `` 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. |
|
||||
| `` N `` | 移動提交至新分支 | 建立一個新分支,並將目前分支未推送的提交移動到該分支。如果您打算開始新工作但忘記先建立新分支,這會很有用。<br><br>請注意,此操作忽略選擇,新分支總是從主分支建立或堆疊在目前分支之上(您可以選擇哪種方式)。 |
|
||||
| `` w `` | 新建工作樹 | |
|
||||
| `` g `` | 檢視重設選項 | 檢視重置選項 (soft/mixed/hard) 用於重置到選擇項。 |
|
||||
| `` C `` | 複製提交 (揀選) | 標記提交為已複製。然後,在本地提交檢視中,您可以按 `V` (Cherry-Pick) 將已複製的提交貼上到已檢出的分支中。任何時候都可以按 `<esc>` 來取消選擇。 |
|
||||
| `` <ctrl+r> `` | 重設選定的揀選 (複製) 提交 | |
|
||||
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` * `` | 選擇目前分支的提交 | |
|
||||
| `` 0 `` | 聚焦主檢視 | |
|
||||
| `` <enter> `` | 檢視提交 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
@@ -286,18 +259,18 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` i `` | 顯示 git-flow 選項 | |
|
||||
| `` <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 | |
|
||||
| `` N `` | 移動提交至新分支 | 建立一個新分支,並將目前分支未推送的提交移動到該分支。如果您打算開始新工作但忘記先建立新分支,這會很有用。<br><br>請注意,此操作忽略選擇,新分支總是從主分支建立或堆疊在目前分支之上(您可以選擇哪種方式)。 |
|
||||
| `` w `` | 新建工作樹 | |
|
||||
| `` o `` | 建立拉取請求 | |
|
||||
| `` O `` | 建立拉取請求選項 | |
|
||||
| `` G `` | Open pull request in browser | |
|
||||
| `` G `` | 在瀏覽器中開啟拉取請求 | |
|
||||
| `` <ctrl+y> `` | 複製拉取請求的 URL 到剪貼板 | |
|
||||
| `` c `` | 根據名稱檢出 | Checkout by name. In the input box you can enter '-' to switch to the previous branch. |
|
||||
| `` - `` | Checkout previous branch | |
|
||||
| `` F `` | 強制檢出 | Force checkout selected branch. This will discard all local changes in your working directory before checking out the selected branch. |
|
||||
| `` d `` | 刪除 | View delete options for local/remote branch. |
|
||||
| `` r `` | 將已檢出的分支變基至此分支 | Rebase the checked-out branch onto the selected branch. |
|
||||
| `` M `` | 合併到當前檢出的分支 | View options for merging the selected item into the current branch (regular merge, squash merge) |
|
||||
| `` c `` | 根據名稱檢出 | 按名稱檢出。在輸入框中,您可以輸入'-' 來切換到最後一個分支。 |
|
||||
| `` - `` | 簽出上一個分支 | |
|
||||
| `` F `` | 強制檢出 | 強制檢出所選分支。這將在檢出所選分支之前放棄工作目錄中的所有本地更改。 |
|
||||
| `` d `` | 刪除 | 檢視本地/遠端分支的刪除選項。 |
|
||||
| `` r `` | 將已檢出的分支變基至此分支 | 將檢出的分支變基到所選的分支上。 |
|
||||
| `` M `` | 合併到當前檢出的分支 | 檢視將選中項合併到目前分支的選項(正常合併,壓縮合並) |
|
||||
| `` f `` | 從上游快進此分支 | 從遠端快進所選的分支 |
|
||||
| `` T `` | 建立標籤 | |
|
||||
| `` s `` | 排序規則 | |
|
||||
@@ -305,7 +278,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` R `` | 重新命名分支 | |
|
||||
| `` u `` | 檢視遠端設定 | 檢視有關遠端分支的設定(例如重設至遠端) |
|
||||
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` 0 `` | 聚焦主檢視 | |
|
||||
| `` <enter> `` | 檢視提交 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
@@ -313,15 +286,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> `` | 檢出 | 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+o> `` | 複製標籤到剪貼簿 | |
|
||||
| `` <space> `` | 檢出 | 檢出選擇的標籤作為分離的HEAD。 |
|
||||
| `` n `` | 建立標籤 | 基於目前提交建立一個新標籤。您將在彈窗中輸入標籤名稱和描述(可選)。 |
|
||||
| `` w `` | 新建工作樹 | |
|
||||
| `` d `` | 刪除 | 檢視本機/遠端標籤的刪除選項。 |
|
||||
| `` P `` | 推送標籤 | 推送選擇的標籤到遠端。您將在彈窗中選擇一個遠端。 |
|
||||
| `` g `` | 重設 | 檢視重置選項 (soft/mixed/hard) 用於重置到選擇項。 |
|
||||
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` 0 `` | 聚焦主檢視 | |
|
||||
| `` <enter> `` | 檢視提交 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
@@ -330,40 +303,57 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <ctrl+o> `` | 複製檔案名稱到剪貼簿 | |
|
||||
| `` <space> `` | 切換預存 | Toggle staged for selected file. |
|
||||
| `` <space> `` | 切換預存 | 切換所選檔案的暫存狀態。 |
|
||||
| `` <ctrl+b> `` | 篩選檔案 (預存/未預存) | |
|
||||
| `` y `` | 複製到剪貼簿 | |
|
||||
| `` c `` | 提交變更 | 提交暫存區變更 |
|
||||
| `` w `` | 沒有預提交 hook 就提交更改 | |
|
||||
| `` A `` | 修改上次提交 | |
|
||||
| `` C `` | 使用 git 編輯器提交變更 | |
|
||||
| `` <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> `` | 尋找 fixup 的基礎提交 | 找出目前變更所依據的提交,以便 amend/fixup。這樣不必逐一檢視分支中的提交來找出要 amend/fixup 的提交。請見文件:<https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` e `` | 編輯 | 使用外部編輯器開啟 |
|
||||
| `` o `` | 開啟檔案 | 使用預設軟體開啟 |
|
||||
| `` i `` | 忽略或排除檔案 | |
|
||||
| `` r `` | 重新整理檔案 | |
|
||||
| `` s `` | 收藏 | Stash all changes. For other variations of stashing, use the view stash options keybinding. |
|
||||
| `` S `` | 檢視收藏選項 | View stash options (e.g. stash all, stash staged, stash unstaged). |
|
||||
| `` a `` | 全部預存/取消預存 | Toggle staged/unstaged for all files in working tree. |
|
||||
| `` <enter> `` | 選擇檔案中的單個程式碼塊/行,或展開/折疊目錄 | 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. |
|
||||
| `` s `` | 收藏 | 貯藏所有變更.若要使用其他貯藏變體,請使用檢視貯藏選項快捷鍵。 |
|
||||
| `` S `` | 檢視收藏選項 | 檢視貯藏選項(例如:貯藏所有、貯藏已暫存變更、貯藏未暫存變更)。 |
|
||||
| `` a `` | 全部預存/取消預存 | 切換工作區中所有檔案的已暫存/未暫存狀態。 |
|
||||
| `` <enter> `` | 選擇檔案中的單個程式碼塊/行,或展開/折疊目錄 | 如果選中的是一個檔案,則會進入到暫存檢視,以便可以暫存單個程式碼塊/行。如果選中的是一個目錄,則會摺疊/展開這個目錄。 |
|
||||
| `` d `` | 捨棄 | 檢視選中變動進行捨棄復原 |
|
||||
| `` g `` | 檢視遠端重設選項 | |
|
||||
| `` D `` | 重設 | View reset options for working tree (e.g. nuking the working tree). |
|
||||
| `` ` `` | 顯示檔案樹狀視圖 | 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'. |
|
||||
| `` D `` | 重設 | 檢視工作樹的重置選項(例如:清除工作樹)。 |
|
||||
| `` ` `` | 顯示檔案樹狀視圖 | 在平面佈局和樹佈局之間切換檔案檢視。平面佈局在單個列表中顯示所有檔案路徑,樹佈局按目錄分組檔案。<br><br>可以在設定檔中使用 'gui.showFileTree' 鍵更改預設設定。 |
|
||||
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
|
||||
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
|
||||
| `` M `` | 檢視合併衝突選項 | 檢視用於解決合併衝突的選項。 |
|
||||
| `` f `` | 擷取 | 同步遠端異動 |
|
||||
| `` - `` | Collapse all files | Collapse all directories in the files tree |
|
||||
| `` = `` | Expand all files | Expand all directories in the file tree |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` - `` | 摺疊全部檔案 | 摺疊檔案樹中的全部目錄 |
|
||||
| `` = `` | 展開全部檔案 | 展開檔案樹中的全部目錄 |
|
||||
| `` 0 `` | 聚焦主檢視 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 次要
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <tab> `` | 切換至另一個面板 (已預存/未預存更改) | Switch to other view (staged/unstaged changes). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | 切換程式碼塊選擇 | 切換逐行選擇與程式碼塊選擇模式。 |
|
||||
| `` v `` | 切換拖曳選擇 | |
|
||||
| `` e `` | 編輯檔案 | 使用外部編輯器開啟 |
|
||||
| `` <space> `` | 切換預存 | 切換現有行的狀態 (已預存/未預存) |
|
||||
| `` d `` | 刪除變更 (git reset) | 選取未暫存的變更時,使用 `git reset` 捨棄變更。選取已暫存的變更時,取消暫存變更。 |
|
||||
| `` E `` | 編輯程式碼塊 | 在外部編輯器中編輯選中的程式碼塊。 |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | 選擇上一段 | |
|
||||
| `` <right>, l `` | 選擇下一段 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` <ctrl+g> `` | Jump to file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | 退出回到側邊面板 | |
|
||||
| `` c `` | 提交變更 | 提交暫存區變更 |
|
||||
| `` w `` | 沒有預提交 hook 就提交更改 | |
|
||||
| `` C `` | 使用 git 編輯器提交變更 | |
|
||||
| `` <ctrl+f> `` | 尋找 fixup 的基礎提交 | 找出目前變更所依據的提交,以便 amend/fixup。這樣不必逐一檢視分支中的提交來找出要 amend/fixup 的提交。請見文件:<https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 狀態
|
||||
@@ -373,9 +363,9 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` e `` | 編輯設定檔案 | 使用外部編輯器開啟 |
|
||||
| `` u `` | 檢查更新 | |
|
||||
| `` <enter> `` | 切換到最近使用的版本庫 | |
|
||||
| `` a `` | Show/cycle all branch logs | |
|
||||
| `` A `` | Show/cycle all branch logs (reverse) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` a `` | 顯示/迴圈所有分支日誌 | |
|
||||
| `` A `` | 顯示/迴圈所有分支日誌(反向) | |
|
||||
| `` 0 `` | 聚焦主檢視 | |
|
||||
|
||||
## 確認面板
|
||||
|
||||
@@ -385,16 +375,23 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <esc> `` | 關閉/取消 | |
|
||||
| `` <ctrl+o> `` | 複製到剪貼簿 | |
|
||||
|
||||
## 輸入提示
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <enter> `` | 確認 | |
|
||||
| `` <esc> `` | 關閉/取消 | |
|
||||
|
||||
## 遠端
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <enter> `` | View branches | |
|
||||
| `` <enter> `` | 檢視分支 | |
|
||||
| `` n `` | 新增遠端 | |
|
||||
| `` d `` | Remove | Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected. |
|
||||
| `` d `` | 刪除 | 刪除選中的遠端。從遠端跟蹤遠端分支的任何本地分支都不會受到影響。 |
|
||||
| `` e `` | 編輯 | 編輯遠端 |
|
||||
| `` f `` | 擷取 | 擷取遠端 |
|
||||
| `` 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. |
|
||||
| `` F `` | 新增復刻遠端倉庫 | 透過替換 origin URL 中的所有者來快速新增復刻遠端倉庫,並可選擇從新遠端倉庫檢出分支。 |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 遠端分支
|
||||
@@ -402,16 +399,16 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <ctrl+o> `` | 複製分支名稱到剪貼簿 | |
|
||||
| `` <space> `` | 檢出 | Checkout a new local branch based on the selected remote branch, or the remote branch as a detached head. |
|
||||
| `` <space> `` | 檢出 | 基於目前選中的遠端分支檢出一個新的本地分支,或者將遠端分支作分離的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. |
|
||||
| `` w `` | 新建工作樹 | |
|
||||
| `` M `` | 合併到當前檢出的分支 | 檢視將選中項合併到目前分支的選項(正常合併,壓縮合並) |
|
||||
| `` r `` | 將已檢出的分支變基至此分支 | 將檢出的分支變基到所選的分支上。 |
|
||||
| `` d `` | 刪除 | 從遠端刪除遠端分支。 |
|
||||
| `` u `` | 設置為遠端 | 將此分支設為當前分支之遠端 |
|
||||
| `` s `` | 排序規則 | |
|
||||
| `` g `` | 檢視重設選項 | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` g `` | 檢視重設選項 | 檢視重置選項 (soft/mixed/hard) 用於重置到選擇項。 |
|
||||
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` 0 `` | 聚焦主檢視 | |
|
||||
| `` <enter> `` | 檢視提交 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
@@ -27,7 +27,7 @@ Fields only for `extDiff`:
|
||||
|
||||
Fields only for `rawGit`:
|
||||
|
||||
- **args** The additional arguments to use in the `git diff` or `git show` call (e.g. `--color-words`)
|
||||
- **args** The additional arguments to use in the `git diff` or `git show` call (e.g. `--color-words`), as an array of strings.
|
||||
|
||||
Here's an example for a multi-renderer setup:
|
||||
|
||||
@@ -40,7 +40,7 @@ git:
|
||||
- type: extDiff
|
||||
command: difft --color=always --context={{diffContext}}
|
||||
- type: rawGit
|
||||
args: --color-words
|
||||
args: [--color-words]
|
||||
name: color-words
|
||||
- type: rawGit # git's default diff
|
||||
name: default
|
||||
|
||||
+5
-1
@@ -4,10 +4,14 @@
|
||||
|
||||
Depending on the currently focused view, hitting '/' will bring up a filter or search prompt. When filtering, the contents of the view will be filtered down to only those lines which match the query string. When searching, the contents of the view are not filtered, but matching lines are highlighted and you can iterate through matches with `n`/`N`.
|
||||
|
||||
We intend to support filtering for the files view soon, but at the moment it uses searching. We intend to continue using search for the commits view because you typically care about the commits that come before/after a matching commit.
|
||||
In the commits view we don't filter, but search; this is deliberate because you typically care about the commits that come before/after a matching commit.
|
||||
|
||||
If you would like both filtering and searching to be enabled on a given view, please raise an issue for this.
|
||||
|
||||
## Menu filtering
|
||||
|
||||
The keybindings (`?`) and recent repositories menus can be filtered simply by typing. The filter field appears at the bottom of the menu while you type; there is no need to press `/` or confirm the filter before navigating the results.
|
||||
|
||||
## Filtering files by status
|
||||
|
||||
You can filter the files view to only show staged/unstaged files by pressing `<c-b>` in the files view.
|
||||
|
||||
@@ -280,7 +280,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` 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 de uitgecheckte branch bovenop de geselecteerde branch. |
|
||||
| `` d `` | Verwijderen | Delete the remote branch from the remote. |
|
||||
| `` d `` | Verwijderen | Verwijder de remote branch van de 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. |
|
||||
@@ -295,7 +295,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|-----|--------|-------------|
|
||||
| `` <enter> `` | Bekijk branches | |
|
||||
| `` n `` | Voeg een nieuwe remote toe | |
|
||||
| `` d `` | Verwijderen | Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected. |
|
||||
| `` d `` | Verwijderen | Verwijder de geselecteerde remote. Locale branches die een branch tracken van de remote worden niet aangepast. |
|
||||
| `` 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. |
|
||||
|
||||
@@ -9,28 +9,28 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <ctrl+r> `` | 切換到最近使用的版本庫 | |
|
||||
| `` <pgup>, K, <ctrl+u> (fn+up/shift+k) `` | 向上捲動主面板 | |
|
||||
| `` <pgdown>, J, <ctrl+d> (fn+down/shift+j) `` | 向下捲動主面板 | |
|
||||
| `` @ `` | 開啟命令記錄選單 | View options for the command log e.g. show/hide the command log and focus the command log. |
|
||||
| `` @ `` | 開啟命令記錄選單 | 檢視命令日誌的選項,例如顯示/隱藏命令日誌以及聚焦命令日誌。 |
|
||||
| `` P `` | 推送 | 推送到遠端。如果沒有設定遠端,會開啟設定視窗。 |
|
||||
| `` p `` | 拉取 | 從遠端同步當前分支。如果沒有設定遠端,會開啟設定視窗。 |
|
||||
| `` ) `` | 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 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 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. |
|
||||
| `` ) `` | 提高重新命名相似度閾值 | 提高將刪除和新增對視為重新命名所需的相似度閾值。<br><br>預設值可在設定檔中透過鍵 'git.renameSimilarityThreshold' 更改。 |
|
||||
| `` ( `` | 降低重新命名相似度閾值 | 降低將刪除和新增對視為重新命名所需的相似度閾值。<br><br>預設值可在設定檔中透過鍵 'git.renameSimilarityThreshold' 更改。 |
|
||||
| `` } `` | 增加差異檢視中顯示變更周圍上下文的大小 | 增加差異檢視中變更周圍顯示的上下文量。<br><br>預設值可在設定檔中透過鍵 'git.diffContextSize' 更改。 |
|
||||
| `` { `` | 減小差異檢視中顯示變更周圍上下文的大小 | 減少差異檢視中變更周圍顯示的上下文量。<br><br>預設值可在設定檔中透過鍵 'git.diffContextSize' 更改。 |
|
||||
| `` : `` | 執行 Shell 命令 | 調出可輸入shell命令執行的提示符。 |
|
||||
| `` <ctrl+p> `` | 檢視自訂補丁選項 | |
|
||||
| `` m `` | 查看合併/變基選項 | View options to abort/continue/skip the current merge/rebase. |
|
||||
| `` 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`. |
|
||||
| `` m `` | 查看合併/變基選項 | 檢視目前合併或變基的中止、繼續、跳過選項。 |
|
||||
| `` R `` | 重新整理 | 重新整理Git狀態(即在背景執行`git status`、`git branch`等命令以更新面板內容)。此操作不會執行`git fetch`。 |
|
||||
| `` + `` | 下一個螢幕模式(常規/半螢幕/全螢幕) | |
|
||||
| `` _ `` | 上一個螢幕模式 | |
|
||||
| `` \| `` | Cycle diff renderers | Choose the next renderer in the list of configured diff renderers. |
|
||||
| `` \ `` | Cycle diff renderers (reverse) | Choose the previous renderer in the list of configured diff renderers. |
|
||||
| `` \| `` | 切換差異渲染器 | 選擇已設定的差異渲染器清單中的下一個渲染器。 |
|
||||
| `` \ `` | 切換差異渲染器(反向) | 選擇已設定的差異渲染器清單中的上一個渲染器。 |
|
||||
| `` <esc> `` | 取消 | |
|
||||
| `` ? `` | 開啟選單 | |
|
||||
| `` <ctrl+s> `` | 檢視篩選路徑選項 | View options for filtering the commit log, so that only commits matching the filter are shown. |
|
||||
| `` W, <ctrl+e> `` | 開啟差異比較選單 | View options relating to diffing two refs e.g. diffing against selected ref, entering ref to diff against, and reversing the diff direction. |
|
||||
| `` <ctrl+s> `` | 檢視篩選路徑選項 | 檢視用於過濾提交日誌的選項,以便僅顯示與過濾器匹配的提交。 |
|
||||
| `` W, <ctrl+e> `` | 開啟差異比較選單 | 檢視與比較兩個引用相關的選項,例如與選定的 ref 進行比較,輸入要比較的 ref,然後反轉比較方向。 |
|
||||
| `` 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'. |
|
||||
| `` <ctrl+z> `` | 掛起應用程式 | |
|
||||
| `` <ctrl+w> `` | 切換是否在差異檢視中顯示空格變更 | 切換是否在差異檢視中顯示空白字元更改。<br><br>預設值可在設定檔中透過鍵 'git.ignoreWhitespaceInDiffView' 更改。 |
|
||||
| `` <alt+shift+c> `` | 編輯設定檔案 | 使用外部編輯器開啟 |
|
||||
| `` z `` | 復原 | 將使用 reflog 確任 git 指令以復原。這不包括工作區更改;只考慮提交。 |
|
||||
| `` Z `` | 取消復原 | 將使用 reflog 確任 git 指令以重作。這不包括工作區更改;只考慮提交。 |
|
||||
@@ -44,21 +44,14 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <, <home> `` | 捲動到頂部 | |
|
||||
| `` >, <end> `` | 捲動到底部 | |
|
||||
| `` v `` | 切換拖曳選擇 | |
|
||||
| `` <shift+down> `` | Range select down | |
|
||||
| `` <shift+up> `` | Range select up | |
|
||||
| `` <shift+down> `` | 向下擴充套件選擇範圍 | |
|
||||
| `` <shift+up> `` | 向上擴充套件選擇範圍 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
| `` H `` | 向左捲動 | |
|
||||
| `` L `` | 向右捲動 | |
|
||||
| `` ] `` | 下一個索引標籤 | |
|
||||
| `` [ `` | 上一個索引標籤 | |
|
||||
|
||||
## Input prompt
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <enter> `` | 確認 | |
|
||||
| `` <esc> `` | 關閉/取消 | |
|
||||
|
||||
## 主面板 (補丁生成)
|
||||
|
||||
| Key | Action | Info |
|
||||
@@ -66,12 +59,12 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <left>, h `` | 選擇上一段 | |
|
||||
| `` <right>, l `` | 選擇下一段 | |
|
||||
| `` v `` | 切換拖曳選擇 | |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` a `` | 切換程式碼塊選擇 | 切換逐行選擇與程式碼塊選擇模式。 |
|
||||
| `` <ctrl+o> `` | 複製所選文本至剪貼簿 | |
|
||||
| `` o `` | 開啟檔案 | 使用預設軟體開啟 |
|
||||
| `` e `` | 編輯檔案 | 使用外部編輯器開啟 |
|
||||
| `` <space> `` | 向 (或從) 補丁中添加/刪除行 | |
|
||||
| `` 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. |
|
||||
| `` d `` | 從提交中移除行 | 從本次提交中移除所選行。此操作會在背景執行互動式變基,因此如果後續提交也修改了這些行,您可能會遇到合併衝突。 |
|
||||
| `` <esc> `` | 退出自訂補丁建立器 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
@@ -81,8 +74,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|-----|--------|-------------|
|
||||
| `` <mouse wheel down> (fn+up) `` | 向下捲動 | |
|
||||
| `` <mouse wheel up> (fn+down) `` | 向上捲動 | |
|
||||
| `` <tab> `` | 切換至另一個面板 (已預存/未預存更改) | Switch to other view (staged/unstaged changes). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` <tab> `` | 切換至另一個面板 (已預存/未預存更改) | 切換到其他檢視(已暫存/未暫存的變更)。 |
|
||||
| `` <esc> `` | 退出回到側邊面板 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 主面板(合併)
|
||||
@@ -90,15 +83,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <space> `` | 挑選程式碼片段 | |
|
||||
| `` b `` | Pick both hunks | |
|
||||
| `` b `` | 選取兩個區塊 | |
|
||||
| `` <up>, k `` | 選擇上一段 | |
|
||||
| `` <down>, j `` | 選擇下一段 | |
|
||||
| `` <left>, h `` | 選擇上一個衝突 | |
|
||||
| `` <right>, l `` | 選擇下一個衝突 | |
|
||||
| `` z `` | 復原 | Undo last merge conflict resolution. |
|
||||
| `` z `` | 復原 | 撤消上次合併衝突解決。 |
|
||||
| `` e `` | 編輯檔案 | 使用外部編輯器開啟 |
|
||||
| `` o `` | 開啟檔案 | 使用預設軟體開啟 |
|
||||
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
|
||||
| `` M `` | 檢視合併衝突選項 | 檢視用於解決合併衝突的選項。 |
|
||||
| `` <esc> `` | 返回檔案面板 | |
|
||||
|
||||
## 主面板(預存)
|
||||
@@ -108,19 +101,19 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <left>, h `` | 選擇上一段 | |
|
||||
| `` <right>, l `` | 選擇下一段 | |
|
||||
| `` v `` | 切換拖曳選擇 | |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` a `` | 切換程式碼塊選擇 | 切換逐行選擇與程式碼塊選擇模式。 |
|
||||
| `` <ctrl+o> `` | 複製所選文本至剪貼簿 | |
|
||||
| `` <space> `` | 切換預存 | 切換現有行的狀態 (已預存/未預存) |
|
||||
| `` d `` | 刪除變更 (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
|
||||
| `` d `` | 刪除變更 (git reset) | 選取未暫存的變更時,使用 `git reset` 捨棄變更。選取已暫存的變更時,取消暫存變更。 |
|
||||
| `` o `` | 開啟檔案 | 使用預設軟體開啟 |
|
||||
| `` e `` | 編輯檔案 | 使用外部編輯器開啟 |
|
||||
| `` <esc> `` | 返回檔案面板 | |
|
||||
| `` <tab> `` | 切換至另一個面板 (已預存/未預存更改) | Switch to other view (staged/unstaged changes). |
|
||||
| `` E `` | 編輯程式碼塊 | Edit selected hunk in external editor. |
|
||||
| `` <tab> `` | 切換至另一個面板 (已預存/未預存更改) | 切換到其他檢視(已暫存/未暫存的變更)。 |
|
||||
| `` E `` | 編輯程式碼塊 | 在外部編輯器中編輯選中的程式碼塊。 |
|
||||
| `` c `` | 提交變更 | 提交暫存區變更 |
|
||||
| `` w `` | 沒有預提交 hook 就提交更改 | |
|
||||
| `` C `` | 使用 git 編輯器提交變更 | |
|
||||
| `` <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> `` | 尋找 fixup 的基礎提交 | 找出目前變更所依據的提交,以便 amend/fixup。這樣不必逐一檢視分支中的提交來找出要 amend/fixup 的提交。請見文件:<https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 功能表
|
||||
@@ -135,19 +128,19 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <ctrl+o> `` | Copy abbreviated commit hash to clipboard | |
|
||||
| `` <space> `` | 檢出 | Checkout the selected commit as a detached HEAD. |
|
||||
| `` y `` | 複製提交屬性 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` <ctrl+o> `` | 複製縮略提交雜湊值到剪貼簿 | |
|
||||
| `` <space> `` | 檢出 | 檢出所選擇的提交作為分離HEAD。 |
|
||||
| `` y `` | 複製提交屬性 | 複製提交屬性到剪貼簿(如hash、URL、diff、訊息、作者)。 |
|
||||
| `` 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. |
|
||||
| `` N `` | 移動提交至新分支 | 建立一個新分支,並將目前分支未推送的提交移動到該分支。如果您打算開始新工作但忘記先建立新分支,這會很有用。<br><br>請注意,此操作忽略選擇,新分支總是從主分支建立或堆疊在目前分支之上(您可以選擇哪種方式)。 |
|
||||
| `` w `` | 新建工作樹 | |
|
||||
| `` g `` | 檢視重設選項 | 檢視重置選項 (soft/mixed/hard) 用於重置到選擇項。 |
|
||||
| `` C `` | 複製提交 (揀選) | 標記提交為已複製。然後,在本地提交檢視中,您可以按 `V` (Cherry-Pick) 將已複製的提交貼上到已檢出的分支中。任何時候都可以按 `<esc>` 來取消選擇。 |
|
||||
| `` <ctrl+r> `` | 重設選定的揀選 (複製) 提交 | |
|
||||
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` * `` | 選擇目前分支的提交 | |
|
||||
| `` 0 `` | 聚焦主檢視 | |
|
||||
| `` <enter> `` | 檢視所選項目的檔案 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
@@ -156,12 +149,12 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <ctrl+o> `` | 複製子模組名稱到剪貼簿 | |
|
||||
| `` <enter> `` | Enter | 進入子模組 |
|
||||
| `` d `` | Remove | Remove the selected submodule and its corresponding directory. |
|
||||
| `` u `` | Update | 更新子模組 |
|
||||
| `` <enter> `` | 進入 | 進入子模組 |
|
||||
| `` d `` | 刪除 | 刪除選定的子模組及其相應的目錄。 |
|
||||
| `` u `` | 更新 | 更新子模組 |
|
||||
| `` n `` | 新增子模組 | |
|
||||
| `` e `` | 更新子模組 URL | |
|
||||
| `` i `` | Initialize | 初始化子模組 |
|
||||
| `` i `` | 初始化 | 初始化子模組 |
|
||||
| `` b `` | 查看批量子模組選項 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
@@ -169,27 +162,27 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` n `` | New worktree | |
|
||||
| `` <space> `` | Switch | Switch to the selected worktree. |
|
||||
| `` n `` | 新建工作樹 | |
|
||||
| `` <space> `` | 切換 | 切換到選中的工作樹。 |
|
||||
| `` o `` | 在編輯器中開啟 | |
|
||||
| `` 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. |
|
||||
| `` d `` | 刪除 | 刪除選定的工作樹。這將刪除工作樹的目錄以及 .git 目錄中有關工作樹的後設資料。 |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 提交
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <ctrl+o> `` | Copy abbreviated commit hash to clipboard | |
|
||||
| `` <ctrl+o> `` | 複製縮略提交雜湊值到剪貼簿 | |
|
||||
| `` <ctrl+r> `` | 重設選定的揀選 (複製) 提交 | |
|
||||
| `` b `` | 查看二分選項 | |
|
||||
| `` s `` | 壓縮 (Squash) | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
|
||||
| `` f `` | 修復 (Fixup) | Meld the selected commit into the commit below it. Similar to squash, but the selected commit's message will be discarded. |
|
||||
| `` c `` | Set fixup message | Set the message option for the fixup commit. The -C option means to use this commit's message instead of the target commit's message. |
|
||||
| `` s `` | 壓縮 (Squash) | 將已選提交壓縮到該提交之下。這些選定的提交的訊息會附加到該提交的訊息之下。 |
|
||||
| `` f `` | 修復 (Fixup) | 將選定的提交合併到其下面的提交中。與壓縮類似,但所選提交的訊息將被丟棄。 |
|
||||
| `` c `` | 設定修復提交資訊 | 設定修復提交的資訊選項。-C 選項表示使用此提交的資訊,而非目標提交的資訊。 |
|
||||
| `` r `` | 改寫提交 | 改寫選中的提交訊息 |
|
||||
| `` R `` | 使用編輯器改寫提交 | |
|
||||
| `` d `` | 刪除提交 | Drop the selected commit. This will remove the commit from the branch via a rebase. If the commit makes changes that later commits depend on, you may need to resolve merge conflicts. |
|
||||
| `` d `` | 刪除提交 | 刪除選中的提交。這將透過變基從分支中刪除該提交,如果該提交修改的內容依賴於後續的提交,則需要解決合併衝突。 |
|
||||
| `` e `` | 編輯(開始互動變基) | 編輯提交 |
|
||||
| `` i `` | 開始互動變基 | Start an interactive rebase for the commits on your branch. This will include all commits from the HEAD commit down to the first merge commit or main branch commit.<br>If you would instead like to start an interactive rebase from the selected commit, press `e`. |
|
||||
| `` i `` | 開始互動變基 | 為分支上的提交啟動互動式變基。這將包括從 HEAD 提交到第一個合併提交或主分支提交的所有提交。<br>如果您想從所選提交啟動互動式變基,請按 `e`。 |
|
||||
| `` p `` | 挑選 | 挑選提交 (於變基過程中) |
|
||||
| `` F `` | 建立修復提交 | 為此提交建立修復提交 |
|
||||
| `` S `` | 壓縮上方所有「fixup」提交(自動壓縮) | 是否壓縮上方 {{.commit}} 所有「fixup」提交? |
|
||||
@@ -198,22 +191,22 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` V `` | 貼上提交 (揀選) | |
|
||||
| `` B `` | 為了變基已標注提交為基準提交 | 請為了下一次變基選擇一項基準提交;此將執行 `git rebase --onto`。 |
|
||||
| `` A `` | 修改 | 使用已預存的更改修正提交 |
|
||||
| `` a `` | 設定/重設提交作者 | Set/Reset commit author or set co-author. |
|
||||
| `` t `` | 還原 | Create a revert commit for the selected commit, which applies the selected commit's changes in reverse. |
|
||||
| `` T `` | 打標籤到提交 | Create a new tag pointing at the selected commit. You'll be prompted to enter a tag name and optional description. |
|
||||
| `` <ctrl+l> `` | 開啟記錄選單 | View options for commit log e.g. changing sort order, hiding the git graph, showing the whole git graph. |
|
||||
| `` G `` | Open pull request in browser | |
|
||||
| `` <space> `` | 檢出 | Checkout the selected commit as a detached HEAD. |
|
||||
| `` y `` | 複製提交屬性 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` a `` | 設定/重設提交作者 | 設定或重置提交的作者,或新增其他作者。 |
|
||||
| `` t `` | 還原 | 為所選提交建立還原提交,這會反向應用所選提交的更改。 |
|
||||
| `` T `` | 打標籤到提交 | 建立一個新標籤指向所選提交。您可以在彈窗中輸入標籤名稱和描述(可選)。 |
|
||||
| `` <ctrl+l> `` | 開啟記錄選單 | 檢視提交日誌的選項,例如更改排序順序、隱藏 git graph、顯示整個 git graph。 |
|
||||
| `` G `` | 在瀏覽器中開啟拉取請求 | |
|
||||
| `` <space> `` | 檢出 | 檢出所選擇的提交作為分離HEAD。 |
|
||||
| `` y `` | 複製提交屬性 | 複製提交屬性到剪貼簿(如hash、URL、diff、訊息、作者)。 |
|
||||
| `` 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. |
|
||||
| `` N `` | 移動提交至新分支 | 建立一個新分支,並將目前分支未推送的提交移動到該分支。如果您打算開始新工作但忘記先建立新分支,這會很有用。<br><br>請注意,此操作忽略選擇,新分支總是從主分支建立或堆疊在目前分支之上(您可以選擇哪種方式)。 |
|
||||
| `` w `` | 新建工作樹 | |
|
||||
| `` g `` | 檢視重設選項 | 檢視重置選項 (soft/mixed/hard) 用於重置到選擇項。 |
|
||||
| `` C `` | 複製提交 (揀選) | 標記提交為已複製。然後,在本地提交檢視中,您可以按 `V` (Cherry-Pick) 將已複製的提交貼上到已檢出的分支中。任何時候都可以按 `<esc>` 來取消選擇。 |
|
||||
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` * `` | 選擇目前分支的提交 | |
|
||||
| `` 0 `` | 聚焦主檢視 | |
|
||||
| `` <enter> `` | 檢視所選項目的檔案 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
@@ -231,30 +224,30 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <ctrl+o> `` | 複製檔案名稱到剪貼簿 | |
|
||||
| `` y `` | 複製到剪貼簿 | |
|
||||
| `` c `` | 檢出 | 檢出檔案 |
|
||||
| `` d `` | 捨棄 | Discard this commit's changes to this file. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes this file. |
|
||||
| `` d `` | 捨棄 | 放棄對此檔案的提交變更。 |
|
||||
| `` o `` | 開啟檔案 | 使用預設軟體開啟 |
|
||||
| `` e `` | 編輯 | 使用外部編輯器開啟 |
|
||||
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
|
||||
| `` <space> `` | 切換檔案是否包含在補丁中 | Toggle whether the file is included in the custom patch. See https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
|
||||
| `` a `` | 切換所有檔案是否包含在補丁中 | Add/remove all commit's files to custom patch. See https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
|
||||
| `` <enter> `` | 輸入檔案以將選定的行添加至補丁(或切換目錄折疊) | 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 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 |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` <space> `` | 切換檔案是否包含在補丁中 | 切換檔案是否包含在自定義補丁中。請參閱 https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches。 |
|
||||
| `` a `` | 切換所有檔案是否包含在補丁中 | 新增或刪除所有提交中的檔案到自定義的補丁中。請參閱 https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches。 |
|
||||
| `` <enter> `` | 輸入檔案以將選定的行添加至補丁(或切換目錄折疊) | 如果已選擇一個檔案,則Enter進入該檔案,以便您可以向自定義補丁新增/刪除單獨的行。如果選擇了目錄,則切換目錄。 |
|
||||
| `` ` `` | 顯示檔案樹狀視圖 | 在平面佈局和樹佈局之間切換檔案檢視。平面佈局在單個列表中顯示所有檔案路徑,樹佈局按目錄分組檔案。<br><br>可以在設定檔中使用 'gui.showFileTree' 鍵更改預設設定。 |
|
||||
| `` - `` | 摺疊全部檔案 | 摺疊檔案樹中的全部目錄 |
|
||||
| `` = `` | 展開全部檔案 | 展開檔案樹中的全部目錄 |
|
||||
| `` 0 `` | 聚焦主檢視 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 收藏 (Stash)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <space> `` | 套用 | Apply the stash entry to your working directory. |
|
||||
| `` 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 | |
|
||||
| `` <space> `` | 套用 | 將貯藏項應用到您的工作目錄。 |
|
||||
| `` g `` | 還原 | 將儲存項應用到工作目錄並刪除儲存項。 |
|
||||
| `` d `` | 捨棄 | 從貯藏列表中刪除該貯藏項。 |
|
||||
| `` n `` | 新分支 | 從選定的貯藏項建立一個新分支。這是透過 git 檢查建立貯藏項的提交,從該提交建立一個新分支,然後將貯藏項作為附加提交應用到新分支來實現的。 |
|
||||
| `` w `` | 新建工作樹 | |
|
||||
| `` r `` | 重新命名收藏 | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` 0 `` | 聚焦主檢視 | |
|
||||
| `` <enter> `` | 檢視所選項目的檔案 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
@@ -262,19 +255,19 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <ctrl+o> `` | Copy abbreviated commit hash to clipboard | |
|
||||
| `` <space> `` | 檢出 | Checkout the selected commit as a detached HEAD. |
|
||||
| `` y `` | 複製提交屬性 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
|
||||
| `` <ctrl+o> `` | 複製縮略提交雜湊值到剪貼簿 | |
|
||||
| `` <space> `` | 檢出 | 檢出所選擇的提交作為分離HEAD。 |
|
||||
| `` y `` | 複製提交屬性 | 複製提交屬性到剪貼簿(如hash、URL、diff、訊息、作者)。 |
|
||||
| `` 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. |
|
||||
| `` N `` | 移動提交至新分支 | 建立一個新分支,並將目前分支未推送的提交移動到該分支。如果您打算開始新工作但忘記先建立新分支,這會很有用。<br><br>請注意,此操作忽略選擇,新分支總是從主分支建立或堆疊在目前分支之上(您可以選擇哪種方式)。 |
|
||||
| `` w `` | 新建工作樹 | |
|
||||
| `` g `` | 檢視重設選項 | 檢視重置選項 (soft/mixed/hard) 用於重置到選擇項。 |
|
||||
| `` C `` | 複製提交 (揀選) | 標記提交為已複製。然後,在本地提交檢視中,您可以按 `V` (Cherry-Pick) 將已複製的提交貼上到已檢出的分支中。任何時候都可以按 `<esc>` 來取消選擇。 |
|
||||
| `` <ctrl+r> `` | 重設選定的揀選 (複製) 提交 | |
|
||||
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
|
||||
| `` * `` | Select commits of current branch | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` * `` | 選擇目前分支的提交 | |
|
||||
| `` 0 `` | 聚焦主檢視 | |
|
||||
| `` <enter> `` | 檢視提交 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
@@ -286,18 +279,18 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` i `` | 顯示 git-flow 選項 | |
|
||||
| `` <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 | |
|
||||
| `` N `` | 移動提交至新分支 | 建立一個新分支,並將目前分支未推送的提交移動到該分支。如果您打算開始新工作但忘記先建立新分支,這會很有用。<br><br>請注意,此操作忽略選擇,新分支總是從主分支建立或堆疊在目前分支之上(您可以選擇哪種方式)。 |
|
||||
| `` w `` | 新建工作樹 | |
|
||||
| `` o `` | 建立拉取請求 | |
|
||||
| `` O `` | 建立拉取請求選項 | |
|
||||
| `` G `` | Open pull request in browser | |
|
||||
| `` G `` | 在瀏覽器中開啟拉取請求 | |
|
||||
| `` <ctrl+y> `` | 複製拉取請求的 URL 到剪貼板 | |
|
||||
| `` c `` | 根據名稱檢出 | Checkout by name. In the input box you can enter '-' to switch to the previous branch. |
|
||||
| `` - `` | Checkout previous branch | |
|
||||
| `` F `` | 強制檢出 | Force checkout selected branch. This will discard all local changes in your working directory before checking out the selected branch. |
|
||||
| `` d `` | 刪除 | View delete options for local/remote branch. |
|
||||
| `` r `` | 將已檢出的分支變基至此分支 | Rebase the checked-out branch onto the selected branch. |
|
||||
| `` M `` | 合併到當前檢出的分支 | View options for merging the selected item into the current branch (regular merge, squash merge) |
|
||||
| `` c `` | 根據名稱檢出 | 按名稱檢出。在輸入框中,您可以輸入'-' 來切換到最後一個分支。 |
|
||||
| `` - `` | 簽出上一個分支 | |
|
||||
| `` F `` | 強制檢出 | 強制檢出所選分支。這將在檢出所選分支之前放棄工作目錄中的所有本地更改。 |
|
||||
| `` d `` | 刪除 | 檢視本地/遠端分支的刪除選項。 |
|
||||
| `` r `` | 將已檢出的分支變基至此分支 | 將檢出的分支變基到所選的分支上。 |
|
||||
| `` M `` | 合併到當前檢出的分支 | 檢視將選中項合併到目前分支的選項(正常合併,壓縮合並) |
|
||||
| `` f `` | 從上游快進此分支 | 從遠端快進所選的分支 |
|
||||
| `` T `` | 建立標籤 | |
|
||||
| `` s `` | 排序規則 | |
|
||||
@@ -305,7 +298,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` R `` | 重新命名分支 | |
|
||||
| `` u `` | 檢視遠端設定 | 檢視有關遠端分支的設定(例如重設至遠端) |
|
||||
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` 0 `` | 聚焦主檢視 | |
|
||||
| `` <enter> `` | 檢視提交 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
@@ -313,15 +306,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> `` | 檢出 | 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+o> `` | 複製標籤到剪貼簿 | |
|
||||
| `` <space> `` | 檢出 | 檢出選擇的標籤作為分離的HEAD。 |
|
||||
| `` n `` | 建立標籤 | 基於目前提交建立一個新標籤。您將在彈窗中輸入標籤名稱和描述(可選)。 |
|
||||
| `` w `` | 新建工作樹 | |
|
||||
| `` d `` | 刪除 | 檢視本機/遠端標籤的刪除選項。 |
|
||||
| `` P `` | 推送標籤 | 推送選擇的標籤到遠端。您將在彈窗中選擇一個遠端。 |
|
||||
| `` g `` | 重設 | 檢視重置選項 (soft/mixed/hard) 用於重置到選擇項。 |
|
||||
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` 0 `` | 聚焦主檢視 | |
|
||||
| `` <enter> `` | 檢視提交 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
@@ -330,40 +323,40 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <ctrl+o> `` | 複製檔案名稱到剪貼簿 | |
|
||||
| `` <space> `` | 切換預存 | Toggle staged for selected file. |
|
||||
| `` <space> `` | 切換預存 | 切換所選檔案的暫存狀態。 |
|
||||
| `` <ctrl+b> `` | 篩選檔案 (預存/未預存) | |
|
||||
| `` y `` | 複製到剪貼簿 | |
|
||||
| `` c `` | 提交變更 | 提交暫存區變更 |
|
||||
| `` w `` | 沒有預提交 hook 就提交更改 | |
|
||||
| `` A `` | 修改上次提交 | |
|
||||
| `` C `` | 使用 git 編輯器提交變更 | |
|
||||
| `` <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> `` | 尋找 fixup 的基礎提交 | 找出目前變更所依據的提交,以便 amend/fixup。這樣不必逐一檢視分支中的提交來找出要 amend/fixup 的提交。請見文件:<https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` e `` | 編輯 | 使用外部編輯器開啟 |
|
||||
| `` o `` | 開啟檔案 | 使用預設軟體開啟 |
|
||||
| `` i `` | 忽略或排除檔案 | |
|
||||
| `` r `` | 重新整理檔案 | |
|
||||
| `` s `` | 收藏 | Stash all changes. For other variations of stashing, use the view stash options keybinding. |
|
||||
| `` S `` | 檢視收藏選項 | View stash options (e.g. stash all, stash staged, stash unstaged). |
|
||||
| `` a `` | 全部預存/取消預存 | Toggle staged/unstaged for all files in working tree. |
|
||||
| `` <enter> `` | 選擇檔案中的單個程式碼塊/行,或展開/折疊目錄 | 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. |
|
||||
| `` s `` | 收藏 | 貯藏所有變更.若要使用其他貯藏變體,請使用檢視貯藏選項快捷鍵。 |
|
||||
| `` S `` | 檢視收藏選項 | 檢視貯藏選項(例如:貯藏所有、貯藏已暫存變更、貯藏未暫存變更)。 |
|
||||
| `` a `` | 全部預存/取消預存 | 切換工作區中所有檔案的已暫存/未暫存狀態。 |
|
||||
| `` <enter> `` | 選擇檔案中的單個程式碼塊/行,或展開/折疊目錄 | 如果選中的是一個檔案,則會進入到暫存檢視,以便可以暫存單個程式碼塊/行。如果選中的是一個目錄,則會摺疊/展開這個目錄。 |
|
||||
| `` d `` | 捨棄 | 檢視選中變動進行捨棄復原 |
|
||||
| `` g `` | 檢視遠端重設選項 | |
|
||||
| `` D `` | 重設 | View reset options for working tree (e.g. nuking the working tree). |
|
||||
| `` ` `` | 顯示檔案樹狀視圖 | 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'. |
|
||||
| `` D `` | 重設 | 檢視工作樹的重置選項(例如:清除工作樹)。 |
|
||||
| `` ` `` | 顯示檔案樹狀視圖 | 在平面佈局和樹佈局之間切換檔案檢視。平面佈局在單個列表中顯示所有檔案路徑,樹佈局按目錄分組檔案。<br><br>可以在設定檔中使用 'gui.showFileTree' 鍵更改預設設定。 |
|
||||
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
|
||||
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
|
||||
| `` M `` | 檢視合併衝突選項 | 檢視用於解決合併衝突的選項。 |
|
||||
| `` f `` | 擷取 | 同步遠端異動 |
|
||||
| `` - `` | Collapse all files | Collapse all directories in the files tree |
|
||||
| `` = `` | Expand all files | Expand all directories in the file tree |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` - `` | 摺疊全部檔案 | 摺疊檔案樹中的全部目錄 |
|
||||
| `` = `` | 展開全部檔案 | 展開檔案樹中的全部目錄 |
|
||||
| `` 0 `` | 聚焦主檢視 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 次要
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <tab> `` | 切換至另一個面板 (已預存/未預存更改) | Switch to other view (staged/unstaged changes). |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` <tab> `` | 切換至另一個面板 (已預存/未預存更改) | 切換到其他檢視(已暫存/未暫存的變更)。 |
|
||||
| `` <esc> `` | 退出回到側邊面板 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 狀態
|
||||
@@ -373,9 +366,9 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` e `` | 編輯設定檔案 | 使用外部編輯器開啟 |
|
||||
| `` u `` | 檢查更新 | |
|
||||
| `` <enter> `` | 切換到最近使用的版本庫 | |
|
||||
| `` a `` | Show/cycle all branch logs | |
|
||||
| `` A `` | Show/cycle all branch logs (reverse) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` a `` | 顯示/迴圈所有分支日誌 | |
|
||||
| `` A `` | 顯示/迴圈所有分支日誌(反向) | |
|
||||
| `` 0 `` | 聚焦主檢視 | |
|
||||
|
||||
## 確認面板
|
||||
|
||||
@@ -385,16 +378,23 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <esc> `` | 關閉/取消 | |
|
||||
| `` <ctrl+o> `` | 複製到剪貼簿 | |
|
||||
|
||||
## 輸入提示
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <enter> `` | 確認 | |
|
||||
| `` <esc> `` | 關閉/取消 | |
|
||||
|
||||
## 遠端
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <enter> `` | View branches | |
|
||||
| `` <enter> `` | 檢視分支 | |
|
||||
| `` n `` | 新增遠端 | |
|
||||
| `` d `` | Remove | Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected. |
|
||||
| `` d `` | 刪除 | 刪除選中的遠端。從遠端跟蹤遠端分支的任何本地分支都不會受到影響。 |
|
||||
| `` e `` | 編輯 | 編輯遠端 |
|
||||
| `` f `` | 擷取 | 擷取遠端 |
|
||||
| `` 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. |
|
||||
| `` F `` | 新增復刻遠端倉庫 | 透過替換 origin URL 中的所有者來快速新增復刻遠端倉庫,並可選擇從新遠端倉庫檢出分支。 |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 遠端分支
|
||||
@@ -402,16 +402,16 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <ctrl+o> `` | 複製分支名稱到剪貼簿 | |
|
||||
| `` <space> `` | 檢出 | Checkout a new local branch based on the selected remote branch, or the remote branch as a detached head. |
|
||||
| `` <space> `` | 檢出 | 基於目前選中的遠端分支檢出一個新的本地分支,或者將遠端分支作分離的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. |
|
||||
| `` w `` | 新建工作樹 | |
|
||||
| `` M `` | 合併到當前檢出的分支 | 檢視將選中項合併到目前分支的選項(正常合併,壓縮合並) |
|
||||
| `` r `` | 將已檢出的分支變基至此分支 | 將檢出的分支變基到所選的分支上。 |
|
||||
| `` d `` | 刪除 | 從遠端刪除遠端分支。 |
|
||||
| `` u `` | 設置為遠端 | 將此分支設為當前分支之遠端 |
|
||||
| `` s `` | 排序規則 | |
|
||||
| `` g `` | 檢視重設選項 | View reset options (soft/mixed/hard) for resetting onto selected item. |
|
||||
| `` g `` | 檢視重設選項 | 檢視重置選項 (soft/mixed/hard) 用於重置到選擇項。 |
|
||||
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
|
||||
| `` 0 `` | Focus main view | |
|
||||
| `` 0 `` | 聚焦主檢視 | |
|
||||
| `` <enter> `` | 檢視提交 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
@@ -5,6 +5,9 @@ go 1.25.0
|
||||
// This is necessary to ignore test files when executing gofumpt.
|
||||
ignore ./test
|
||||
|
||||
// Likewise for worktrees that are nested in the main tree.
|
||||
ignore ./.worktrees
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2
|
||||
github.com/adrg/xdg v0.5.3
|
||||
@@ -13,7 +16,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.1
|
||||
github.com/gdamore/tcell/v3 v3.5.0
|
||||
github.com/go-errors/errors v1.5.1
|
||||
github.com/gookit/color v1.6.1
|
||||
github.com/integrii/flaggy v1.8.0
|
||||
@@ -21,8 +24,8 @@ require (
|
||||
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
|
||||
github.com/karimkhaleel/jsonschema v0.0.0-20231001195015-d933f0d94ea3
|
||||
github.com/kyokomi/emoji/v2 v2.2.13
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0
|
||||
github.com/kyokomi/emoji/v2 v2.2.14
|
||||
github.com/lucasb-eyer/go-colorful v1.4.1
|
||||
github.com/mgutz/str v1.2.0
|
||||
github.com/mitchellh/go-ps v1.0.0
|
||||
github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe
|
||||
@@ -31,12 +34,12 @@ require (
|
||||
github.com/samber/lo v1.53.0
|
||||
github.com/sanity-io/litter v1.5.8
|
||||
github.com/sasha-s/go-deadlock v0.3.9
|
||||
github.com/sirupsen/logrus v1.9.4
|
||||
github.com/sirupsen/logrus v1.10.2
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/spkg/bom v1.0.1
|
||||
github.com/stefanhaller/git-todo-parser v0.0.7-0.20250905083220-c50528f08304
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e
|
||||
github.com/stretchr/testify v1.12.1
|
||||
github.com/xo/terminfo v1.0.0
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
|
||||
golang.org/x/sync v0.22.0
|
||||
golang.org/x/sys v0.47.0
|
||||
@@ -50,11 +53,9 @@ require (
|
||||
github.com/cli/safeexec v1.0.1 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fatih/color v1.9.0 // indirect
|
||||
github.com/gdamore/encoding v1.0.1 // indirect
|
||||
github.com/go-logfmt/logfmt v0.5.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/hpcloud/tail v1.0.0 // indirect
|
||||
github.com/invopop/jsonschema v0.10.0 // indirect
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 // indirect
|
||||
@@ -63,16 +64,16 @@ 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/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
golang.org/x/mod v0.37.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
golang.org/x/mod v0.38.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
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
golang.org/x/tools v0.48.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
|
||||
mvdan.cc/gofumpt v0.9.2 // indirect
|
||||
mvdan.cc/gofumpt v0.11.0 // indirect
|
||||
)
|
||||
|
||||
tool mvdan.cc/gofumpt
|
||||
|
||||
@@ -25,22 +25,20 @@ github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.7.1-0.20180516100307-2d684516a886/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/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.1 h1:22227t1EUwqxTlmCX9vw0RUE2IEPGw6oYcNan+bPe4w=
|
||||
github.com/gdamore/tcell/v3 v3.4.1/go.mod h1:YWwuxZNi14VGQC5g2VGNEDRXpBraTwvVjMovRH6G6hw=
|
||||
github.com/gdamore/tcell/v3 v3.5.0 h1:SCp9czLv2K2aPORgD6+4fjV0xNAzKIkiezCkp6bHLe4=
|
||||
github.com/gdamore/tcell/v3 v3.5.0/go.mod h1:Oe5U3S3jm3NzypswDNUhe+LUnF5CoFq2b4sepD++QHo=
|
||||
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=
|
||||
github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
|
||||
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
|
||||
github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474=
|
||||
github.com/go-quicktest/qt v1.102.0/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0=
|
||||
@@ -73,10 +71,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/kyokomi/emoji/v2 v2.2.13 h1:GhTfQa67venUUvmleTNFnb+bi7S3aocF7ZCXU9fSO7U=
|
||||
github.com/kyokomi/emoji/v2 v2.2.13/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/kyokomi/emoji/v2 v2.2.14 h1:YOF6VL52613M0Qr9v4puJDD9QQPmyyjXedDDlrGzH80=
|
||||
github.com/kyokomi/emoji/v2 v2.2.14/go.mod h1:1AnYl9IgmJZXKd5m1PEijyyUw85SqYsuAr8lpU/s+9s=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBqlTf+2hNss=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.1/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
@@ -100,12 +98,11 @@ github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeD
|
||||
github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe h1:vHpqOnPlnkba8iSxU4j/CvDSS9J4+F4473esQsYLGoE=
|
||||
github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc=
|
||||
github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
|
||||
github.com/sahilm/fuzzy v0.1.3 h1:juByESSS32nVD81vr6tHmKmA/8zde7gE+x5CLxrzXPU=
|
||||
github.com/sahilm/fuzzy v0.1.3/go.mod h1:au6//VbVSqu6DFrkL2CfjlJ5iURpNCPeE+1GwY3XsT8=
|
||||
github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
|
||||
@@ -114,8 +111,8 @@ github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rY
|
||||
github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
|
||||
github.com/sasha-s/go-deadlock v0.3.9 h1:fiaT9rB7g5sr5ddNZvlwheclN9IP86eFW9WgqlEQV+w=
|
||||
github.com/sasha-s/go-deadlock v0.3.9/go.mod h1:KuZj51ZFmx42q/mPaYbRk0P1xcwe697zsJKE03vD4/Y=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
|
||||
github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spkg/bom v1.0.1 h1:tl8kQ2sufL/wDEJa9me1jnQYEpDB7LqYGNkwCVR5GLs=
|
||||
@@ -125,28 +122,30 @@ github.com/stefanhaller/git-todo-parser v0.0.7-0.20250905083220-c50528f08304/go.
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
github.com/urfave/cli v1.20.1-0.20180226030253-8e01ec4cd3e2/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/xo/terminfo v1.0.0 h1:2ZpYzqWzyyytjk3TP6aJVDhkMAkc99/1xKQdA3TDTBY=
|
||||
github.com/xo/terminfo v1.0.0/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||
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.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
||||
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
|
||||
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.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
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=
|
||||
@@ -175,14 +174,14 @@ 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.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
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.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
|
||||
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
|
||||
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=
|
||||
@@ -196,5 +195,5 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
mvdan.cc/gofumpt v0.9.2 h1:zsEMWL8SVKGHNztrx6uZrXdp7AX8r421Vvp23sz7ik4=
|
||||
mvdan.cc/gofumpt v0.9.2/go.mod h1:iB7Hn+ai8lPvofHd9ZFGVg2GOr8sBUw1QUWjNbmIL/s=
|
||||
mvdan.cc/gofumpt v0.11.0 h1:0H01XB95PnN2QgCSR9ELdZyTlJqNZ7181B0BTMh5VZc=
|
||||
mvdan.cc/gofumpt v0.11.0/go.mod h1:BeT5wCsOJt6J9zT2MZIOGszjUHzFkn1/l9g6xAzqsXo=
|
||||
|
||||
+3
-4
@@ -61,10 +61,9 @@ func Run(
|
||||
}
|
||||
}
|
||||
|
||||
func NewCommon(config config.AppConfigurer) (*common.Common, error) {
|
||||
func NewCommon(config config.AppConfigurer, log *logrus.Entry) (*common.Common, error) {
|
||||
userConfig := config.GetUserConfig()
|
||||
appState := config.GetAppState()
|
||||
log := newLogger(config)
|
||||
// Initialize with English for the time being; the real translation set for
|
||||
// the configured language will be read after reading the user config
|
||||
tr := i18n.EnglishTranslationSet()
|
||||
@@ -80,8 +79,8 @@ func NewCommon(config config.AppConfigurer) (*common.Common, error) {
|
||||
return cmn, nil
|
||||
}
|
||||
|
||||
func newLogger(cfg config.AppConfigurer) *logrus.Entry {
|
||||
if cfg.GetDebug() {
|
||||
func NewLogger(debug bool) *logrus.Entry {
|
||||
if debug {
|
||||
logPath, err := config.LogPath()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
+21
-22
@@ -3,14 +3,13 @@ package daemon
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Sometimes lazygit will be invoked in daemon mode from a parent lazygit process.
|
||||
@@ -66,14 +65,14 @@ func getInstruction() Instruction {
|
||||
return mapping[getDaemonKind()](jsonData)
|
||||
}
|
||||
|
||||
func Handle(common *common.Common) {
|
||||
func Handle(log *logrus.Entry) {
|
||||
if !InDaemonMode() {
|
||||
return
|
||||
}
|
||||
|
||||
instruction := getInstruction()
|
||||
|
||||
if err := instruction.run(common); err != nil {
|
||||
if err := instruction.run(log); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -107,7 +106,7 @@ type Instruction interface {
|
||||
SerializedInstructions() string
|
||||
|
||||
// runs the instruction
|
||||
run(common *common.Common) error
|
||||
run(log *logrus.Entry) error
|
||||
}
|
||||
|
||||
func serializeInstruction[T any](instruction T) string {
|
||||
@@ -147,7 +146,7 @@ func (self *ExitImmediatelyInstruction) SerializedInstructions() string {
|
||||
return serializeInstruction(self)
|
||||
}
|
||||
|
||||
func (self *ExitImmediatelyInstruction) run(common *common.Common) error {
|
||||
func (self *ExitImmediatelyInstruction) run(log *logrus.Entry) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -165,8 +164,8 @@ func (self *RemoveUpdateRefsForCopiedBranchInstruction) SerializedInstructions()
|
||||
return serializeInstruction(self)
|
||||
}
|
||||
|
||||
func (self *RemoveUpdateRefsForCopiedBranchInstruction) run(common *common.Common) error {
|
||||
return handleInteractiveRebase(common, func(path string) error {
|
||||
func (self *RemoveUpdateRefsForCopiedBranchInstruction) run(log *logrus.Entry) error {
|
||||
return handleInteractiveRebase(log, func(path string) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -193,8 +192,8 @@ func (self *ChangeTodoActionsInstruction) SerializedInstructions() string {
|
||||
return serializeInstruction(self)
|
||||
}
|
||||
|
||||
func (self *ChangeTodoActionsInstruction) run(common *common.Common) error {
|
||||
return handleInteractiveRebase(common, func(path string) error {
|
||||
func (self *ChangeTodoActionsInstruction) run(log *logrus.Entry) error {
|
||||
return handleInteractiveRebase(log, func(path string) error {
|
||||
changes := lo.Map(self.Changes, func(c ChangeTodoAction, _ int) utils.TodoChange {
|
||||
return utils.TodoChange{
|
||||
Hash: c.Hash,
|
||||
@@ -225,8 +224,8 @@ func (self *DropMergeCommitInstruction) SerializedInstructions() string {
|
||||
return serializeInstruction(self)
|
||||
}
|
||||
|
||||
func (self *DropMergeCommitInstruction) run(common *common.Common) error {
|
||||
return handleInteractiveRebase(common, func(path string) error {
|
||||
func (self *DropMergeCommitInstruction) run(log *logrus.Entry) error {
|
||||
return handleInteractiveRebase(log, func(path string) error {
|
||||
return utils.DropMergeCommit(path, self.Hash, getCommentChar())
|
||||
})
|
||||
}
|
||||
@@ -256,8 +255,8 @@ func (self *MoveFixupCommitDownInstruction) SerializedInstructions() string {
|
||||
return serializeInstruction(self)
|
||||
}
|
||||
|
||||
func (self *MoveFixupCommitDownInstruction) run(common *common.Common) error {
|
||||
return handleInteractiveRebase(common, func(path string) error {
|
||||
func (self *MoveFixupCommitDownInstruction) run(log *logrus.Entry) error {
|
||||
return handleInteractiveRebase(log, func(path string) error {
|
||||
return utils.MoveFixupCommitDown(path, self.OriginalHash, self.FixupHash, self.ChangeToFixup, getCommentChar())
|
||||
})
|
||||
}
|
||||
@@ -282,14 +281,14 @@ func (self *MoveTodosUpInstruction) SerializedInstructions() string {
|
||||
return serializeInstruction(self)
|
||||
}
|
||||
|
||||
func (self *MoveTodosUpInstruction) run(common *common.Common) error {
|
||||
func (self *MoveTodosUpInstruction) run(log *logrus.Entry) error {
|
||||
todosToMove := lo.Map(self.Hashes, func(hash string, _ int) utils.Todo {
|
||||
return utils.Todo{
|
||||
Hash: hash,
|
||||
}
|
||||
})
|
||||
|
||||
return handleInteractiveRebase(common, func(path string) error {
|
||||
return handleInteractiveRebase(log, func(path string) error {
|
||||
return utils.MoveTodos(path, todosToMove, false, -self.Distance, getCommentChar())
|
||||
})
|
||||
}
|
||||
@@ -314,14 +313,14 @@ func (self *MoveTodosDownInstruction) SerializedInstructions() string {
|
||||
return serializeInstruction(self)
|
||||
}
|
||||
|
||||
func (self *MoveTodosDownInstruction) run(common *common.Common) error {
|
||||
func (self *MoveTodosDownInstruction) run(log *logrus.Entry) error {
|
||||
todosToMove := lo.Map(self.Hashes, func(hash string, _ int) utils.Todo {
|
||||
return utils.Todo{
|
||||
Hash: hash,
|
||||
}
|
||||
})
|
||||
|
||||
return handleInteractiveRebase(common, func(path string) error {
|
||||
return handleInteractiveRebase(log, func(path string) error {
|
||||
return utils.MoveTodos(path, todosToMove, false, self.Distance, getCommentChar())
|
||||
})
|
||||
}
|
||||
@@ -340,8 +339,8 @@ func (self *InsertBreakInstruction) SerializedInstructions() string {
|
||||
return serializeInstruction(self)
|
||||
}
|
||||
|
||||
func (self *InsertBreakInstruction) run(common *common.Common) error {
|
||||
return handleInteractiveRebase(common, func(path string) error {
|
||||
func (self *InsertBreakInstruction) run(log *logrus.Entry) error {
|
||||
return handleInteractiveRebase(log, func(path string) error {
|
||||
return utils.PrependStrToTodoFile(path, []byte("break\n"))
|
||||
})
|
||||
}
|
||||
@@ -364,8 +363,8 @@ func (self *WriteRebaseTodoInstruction) SerializedInstructions() string {
|
||||
return serializeInstruction(self)
|
||||
}
|
||||
|
||||
func (self *WriteRebaseTodoInstruction) run(common *common.Common) error {
|
||||
return handleInteractiveRebase(common, func(path string) error {
|
||||
func (self *WriteRebaseTodoInstruction) run(log *logrus.Entry) error {
|
||||
return handleInteractiveRebase(log, func(path string) error {
|
||||
return os.WriteFile(path, self.TodosFileContent, 0o644)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stefanhaller/git-todo-parser/todo"
|
||||
)
|
||||
|
||||
@@ -17,9 +17,9 @@ type ChangeTodoAction struct {
|
||||
Flag string
|
||||
}
|
||||
|
||||
func handleInteractiveRebase(common *common.Common, f func(path string) error) error {
|
||||
common.Log.Info("Lazygit invoked as interactive rebase demon")
|
||||
common.Log.Info("args: ", os.Args)
|
||||
func handleInteractiveRebase(log *logrus.Entry, f func(path string) error) error {
|
||||
log.Info("Lazygit invoked as interactive rebase demon")
|
||||
log.Info("args: ", os.Args)
|
||||
path := os.Args[1]
|
||||
|
||||
if strings.HasSuffix(path, "git-rebase-todo") {
|
||||
@@ -32,7 +32,7 @@ func handleInteractiveRebase(common *common.Common, f func(path string) error) e
|
||||
// if we are rebasing and squashing, we'll see a COMMIT_EDITMSG
|
||||
// but in this case we don't need to edit it, so we'll just return
|
||||
} else {
|
||||
common.Log.Info("Lazygit demon did not match on any use cases")
|
||||
log.Info("Lazygit demon did not match on any use cases")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
+10
-9
@@ -93,6 +93,15 @@ func Start(buildInfo *BuildInfo, integrationTest integrationTypes.IntegrationTes
|
||||
env.SetGitDirEnv(cliArgs.GitDir)
|
||||
}
|
||||
|
||||
// The log file lives in the config dir, so this must come after setting the
|
||||
// CONFIG_DIR env var above.
|
||||
logger := NewLogger(cliArgs.Debug)
|
||||
|
||||
if daemon.InDaemonMode() {
|
||||
daemon.Handle(logger)
|
||||
return
|
||||
}
|
||||
|
||||
if cliArgs.PrintVersionInfo {
|
||||
gitVersion := getGitVersionInfo()
|
||||
fmt.Printf("commit=%s, build date=%s, build source=%s, version=%s, os=%s, arch=%s, git version=%s\n", buildInfo.Commit, buildInfo.Date, buildInfo.BuildSource, buildInfo.Version, runtime.GOOS, runtime.GOARCH, gitVersion)
|
||||
@@ -143,9 +152,6 @@ func Start(buildInfo *BuildInfo, integrationTest integrationTypes.IntegrationTes
|
||||
|
||||
if integrationTest != nil {
|
||||
integrationTest.SetupConfig(appConfig)
|
||||
// Set this to true so that integration tests don't have to explicitly deal with the hunk
|
||||
// staging hint:
|
||||
appConfig.GetAppState().DidShowHunkStagingHint = true
|
||||
|
||||
// Preserve the changes that the test setup just made to the config, so
|
||||
// they don't get lost when we reload the config while running the test
|
||||
@@ -154,16 +160,11 @@ func Start(buildInfo *BuildInfo, integrationTest integrationTypes.IntegrationTes
|
||||
appConfig.SaveGlobalUserConfig()
|
||||
}
|
||||
|
||||
common, err := NewCommon(appConfig)
|
||||
common, err := NewCommon(appConfig, logger)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if daemon.InDaemonMode() {
|
||||
daemon.Handle(common)
|
||||
return
|
||||
}
|
||||
|
||||
if cliArgs.Profile {
|
||||
go func() {
|
||||
if err := http.ListenAndServe("localhost:6060", nil); err != nil {
|
||||
|
||||
@@ -19,12 +19,12 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/maps"
|
||||
"github.com/jesseduffield/lazycore/pkg/utils"
|
||||
"github.com/jesseduffield/lazygit/pkg/app"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
@@ -49,7 +49,7 @@ func CommandToRun() string {
|
||||
}
|
||||
|
||||
func GetKeybindingsDir() string {
|
||||
return utils.GetLazyRootDirectory() + "/docs-master/keybindings"
|
||||
return utils.MustFindLazygitRootDirectory() + "/docs-master/keybindings"
|
||||
}
|
||||
|
||||
func generateAtDir(cheatsheetDir string) {
|
||||
@@ -58,10 +58,11 @@ func generateAtDir(cheatsheetDir string) {
|
||||
log.Fatal(err)
|
||||
}
|
||||
mConfig := config.NewDummyAppConfig()
|
||||
logger := app.NewLogger(mConfig.GetDebug())
|
||||
|
||||
for lang := range translationSetsByLang {
|
||||
mConfig.GetUserConfig().Gui.Language = lang
|
||||
common, err := app.NewCommon(mConfig)
|
||||
common, err := app.NewCommon(mConfig, logger)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -119,9 +120,7 @@ func localisedTitle(tr *i18n.TranslationSet, str string) string {
|
||||
"prompt": tr.PromptTitle,
|
||||
"information": tr.InformationTitle,
|
||||
"main": tr.NormalTitle,
|
||||
"patchBuilding": tr.PatchBuildingTitle,
|
||||
"mergeConflicts": tr.MergingTitle,
|
||||
"staging": tr.StagingTitle,
|
||||
"menu": tr.MenuTitle,
|
||||
"search": tr.SearchTitle,
|
||||
"secondary": tr.SecondaryTitle,
|
||||
@@ -140,12 +139,7 @@ func localisedTitle(tr *i18n.TranslationSet, str string) string {
|
||||
}
|
||||
|
||||
func getBindingSections(bindings []*types.Binding, tr *i18n.TranslationSet) []*bindingSection {
|
||||
excludedViews := []string{"stagingSecondary", "patchBuildingSecondary"}
|
||||
bindingsToDisplay := lo.Filter(bindings, func(binding *types.Binding, _ int) bool {
|
||||
if lo.Contains(excludedViews, binding.ViewName) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (binding.Description != "" || binding.Alternative != "") && len(binding.Keys) > 0
|
||||
})
|
||||
|
||||
@@ -196,7 +190,7 @@ func getHeader(binding *types.Binding, tr *i18n.TranslationSet) header {
|
||||
|
||||
func formatSections(tr *i18n.TranslationSet, bindingSections []*bindingSection) string {
|
||||
var content strings.Builder
|
||||
content.WriteString(fmt.Sprintf("# Lazygit %s\n", tr.Keybindings))
|
||||
fmt.Fprintf(&content, "# Lazygit %s\n", tr.Keybindings)
|
||||
|
||||
for _, section := range bindingSections {
|
||||
content.WriteString(formatTitle(section.title))
|
||||
|
||||
+11
-4
@@ -135,8 +135,15 @@ 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, previousPath string, plain bool) (string, error) {
|
||||
return workingTreeCommands.ShowFileDiff(from, to, reverse, filename, previousPath, plain)
|
||||
func(from string, to string, reverse bool, filename string, previousPath string) (string, error) {
|
||||
// A patch is built from git's own diff: what a diff renderer would make of it
|
||||
// is a picture of it, not something that can be applied.
|
||||
return workingTreeCommands.ShowFileDiff(from, to, reverse, filename, previousPath, git_commands.DiffModePlain)
|
||||
},
|
||||
func() (string, error) {
|
||||
// Under lazygit's own temp dir, so that it honours the configured location
|
||||
// and is cleaned up with everything else when we exit.
|
||||
return os.MkdirTemp(osCommand.GetTempDir(), "custom-patch-")
|
||||
})
|
||||
patchCommands := git_commands.NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchBuilder)
|
||||
bisectCommands := git_commands.NewBisectCommands(gitCommon)
|
||||
@@ -145,11 +152,11 @@ func NewGitCommandAux(
|
||||
gitHubCommands := git_commands.NewGitHubCommands(gitCommon)
|
||||
hostingServiceCommands := git_commands.NewHostingServiceCommand(gitCommon)
|
||||
|
||||
branchLoader := git_commands.NewBranchLoader(cmn, gitCommon, cmd, branchCommands.CurrentBranchInfo, configCommands)
|
||||
branchLoader := git_commands.NewBranchLoader(cmn, gitCommon, cmd, branchCommands.CurrentBranchInfo, branchCommands.HasLocalOnlyCommits, configCommands)
|
||||
commitFileLoader := git_commands.NewCommitFileLoader(cmn, cmd)
|
||||
commitLoader := git_commands.NewCommitLoader(cmn, cmd, statusCommands.WorkingTreeState, gitCommon)
|
||||
reflogCommitLoader := git_commands.NewReflogCommitLoader(cmn, cmd)
|
||||
remoteLoader := git_commands.NewRemoteLoader(cmn, cmd)
|
||||
remoteLoader := git_commands.NewRemoteLoader(gitCommon)
|
||||
worktreeLoader := git_commands.NewWorktreeLoader(gitCommon)
|
||||
stashLoader := git_commands.NewStashLoader(cmn, cmd)
|
||||
tagLoader := git_commands.NewTagLoader(cmn, cmd)
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// Holds parsed values from a single %(ahead-behind:<base>) field.
|
||||
type aheadBehind struct {
|
||||
ahead, behind int
|
||||
valid bool
|
||||
}
|
||||
|
||||
type branchAheadBehind struct {
|
||||
refName string
|
||||
aheadBehinds []aheadBehind
|
||||
}
|
||||
|
||||
// Parses output produced by:
|
||||
//
|
||||
// git for-each-ref --format='%(refname)\x00%(ahead-behind:<base1>)\x00...' refs/heads
|
||||
//
|
||||
// Lines whose NUL-split column count doesn't match (1 + numBases) are dropped.
|
||||
// Blank lines are ignored.
|
||||
// Individual malformed ahead-behind fields produce {valid: false} entries, so
|
||||
// that the entries of a line stay aligned with the bases.
|
||||
func parseAheadBehindForEachRefOutput(
|
||||
output string,
|
||||
numBases int, // number of %(ahead-behind:...) tokens
|
||||
) []branchAheadBehind {
|
||||
if output == "" {
|
||||
return nil
|
||||
}
|
||||
lines := strings.Split(output, "\n")
|
||||
result := make([]branchAheadBehind, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
cols := strings.Split(line, "\x00")
|
||||
if len(cols) != numBases+1 {
|
||||
continue
|
||||
}
|
||||
refName := cols[0]
|
||||
aheadBehinds := lo.Map(cols[1:], func(col string, _ int) aheadBehind {
|
||||
return parseAheadBehindField(col)
|
||||
})
|
||||
entry := branchAheadBehind{
|
||||
refName: refName,
|
||||
aheadBehinds: aheadBehinds,
|
||||
}
|
||||
result = append(result, entry)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func parseAheadBehindField(s string) aheadBehind {
|
||||
parts := strings.Fields(s)
|
||||
if len(parts) != 2 {
|
||||
return aheadBehind{}
|
||||
}
|
||||
ahead, err1 := strconv.Atoi(parts[0])
|
||||
behind, err2 := strconv.Atoi(parts[1])
|
||||
if err1 != nil || err2 != nil {
|
||||
return aheadBehind{}
|
||||
}
|
||||
return aheadBehind{ahead: ahead, behind: behind, valid: true}
|
||||
}
|
||||
|
||||
// Picks the "closest" base by smallest ahead value (commits the branch
|
||||
// has that the base doesn't = roughly "since fork point") and returns
|
||||
// its behind value.
|
||||
// Ties are broken by index order
|
||||
func selectBehindForBranch(aheadBehinds []aheadBehind) int {
|
||||
validOnes := lo.Filter(aheadBehinds, func(ab aheadBehind, _ int) bool {
|
||||
return ab.valid
|
||||
})
|
||||
return lo.MinBy(validOnes, func(a, b aheadBehind) bool {
|
||||
return a.ahead < b.ahead
|
||||
}).behind
|
||||
}
|
||||
|
||||
// Builds a for-each-ref command that reports, for each ref matched by one of
|
||||
// refPatterns, how far it is ahead and behind each of the bases. A base is a
|
||||
// ref name or a commit hash. The output format is:
|
||||
//
|
||||
// <refname>\x00<ahead> <behind>\x00<ahead> <behind>...\n
|
||||
//
|
||||
// with one ahead-behind field per base, in the same order as bases.
|
||||
//
|
||||
// Requires git >= 2.41 (when %(ahead-behind:...) was added).
|
||||
func buildAheadBehindForEachRefArgs(bases []string, refPatterns []string) []string {
|
||||
formatParts := make([]string, 0, 1+len(bases))
|
||||
formatParts = append(formatParts, "%(refname)")
|
||||
for _, base := range bases {
|
||||
formatParts = append(formatParts, "%(ahead-behind:"+base+")")
|
||||
}
|
||||
format := strings.Join(formatParts, "%00")
|
||||
|
||||
return NewGitCmd("for-each-ref").
|
||||
Arg("--format=" + format).
|
||||
Arg(refPatterns...).
|
||||
ToArgv()
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseAheadBehindForEachRefOutput(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
input string
|
||||
numBases int
|
||||
expected []branchAheadBehind
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "single branch single base",
|
||||
input: "refs/heads/feat\x002 5\n",
|
||||
numBases: 1,
|
||||
expected: []branchAheadBehind{
|
||||
{
|
||||
refName: "refs/heads/feat",
|
||||
aheadBehinds: []aheadBehind{{ahead: 2, behind: 5, valid: true}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "multiple branches multiple bases",
|
||||
input: "refs/heads/feat\x002 5\x0010 1\n" +
|
||||
"refs/heads/main\x000 0\x000 0\n",
|
||||
numBases: 2,
|
||||
expected: []branchAheadBehind{
|
||||
{
|
||||
refName: "refs/heads/feat",
|
||||
aheadBehinds: []aheadBehind{
|
||||
{ahead: 2, behind: 5, valid: true},
|
||||
{ahead: 10, behind: 1, valid: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
refName: "refs/heads/main",
|
||||
aheadBehinds: []aheadBehind{
|
||||
{ahead: 0, behind: 0, valid: true},
|
||||
{ahead: 0, behind: 0, valid: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "empty ahead-behind field for unreachable base",
|
||||
input: "refs/heads/feat\x00\x002 5\n",
|
||||
numBases: 2,
|
||||
expected: []branchAheadBehind{
|
||||
{
|
||||
refName: "refs/heads/feat",
|
||||
aheadBehinds: []aheadBehind{
|
||||
{},
|
||||
{ahead: 2, behind: 5, valid: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "ref name containing slashes and dashes",
|
||||
input: "refs/heads/feat/foo-bar\x001 2\n",
|
||||
numBases: 1,
|
||||
expected: []branchAheadBehind{
|
||||
{
|
||||
refName: "refs/heads/feat/foo-bar",
|
||||
aheadBehinds: []aheadBehind{{ahead: 1, behind: 2, valid: true}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "trailing newline and blank lines are ignored",
|
||||
input: "refs/heads/feat\x001 2\n\n",
|
||||
numBases: 1,
|
||||
expected: []branchAheadBehind{
|
||||
{
|
||||
refName: "refs/heads/feat",
|
||||
aheadBehinds: []aheadBehind{{ahead: 1, behind: 2, valid: true}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "line with wrong column count is skipped",
|
||||
input: "refs/heads/good\x001 2\n" +
|
||||
"refs/heads/bad\n" +
|
||||
"refs/heads/also_good\x003 4\n",
|
||||
numBases: 1,
|
||||
expected: []branchAheadBehind{
|
||||
{
|
||||
refName: "refs/heads/good",
|
||||
aheadBehinds: []aheadBehind{{ahead: 1, behind: 2, valid: true}},
|
||||
},
|
||||
{
|
||||
refName: "refs/heads/also_good",
|
||||
aheadBehinds: []aheadBehind{{ahead: 3, behind: 4, valid: true}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "malformed ahead-behind field becomes invalid but line is kept",
|
||||
input: "refs/heads/feat\x00not_a_number\n",
|
||||
numBases: 1,
|
||||
expected: []branchAheadBehind{
|
||||
{
|
||||
refName: "refs/heads/feat",
|
||||
aheadBehinds: []aheadBehind{{}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "empty input",
|
||||
input: "",
|
||||
numBases: 1,
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
result := parseAheadBehindForEachRefOutput(s.input, s.numBases)
|
||||
assert.Equal(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectBehindForBranch(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
aheadBehinds []aheadBehind
|
||||
expected int
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "single base, valid value",
|
||||
aheadBehinds: []aheadBehind{{ahead: 3, behind: 7, valid: true}},
|
||||
expected: 7,
|
||||
},
|
||||
{
|
||||
testName: "multi-base, clear winner by ahead",
|
||||
aheadBehinds: []aheadBehind{
|
||||
{ahead: 50, behind: 10, valid: true}, // master
|
||||
{ahead: 5, behind: 2, valid: true}, // develop ← smallest ahead
|
||||
},
|
||||
expected: 2,
|
||||
},
|
||||
{
|
||||
testName: "develop forked from master case (ancestor-of-each-other)",
|
||||
// feat-x has 5 commits since fork from develop.
|
||||
// develop is 50 commits ahead of master.
|
||||
// ahead vs master = 5 + 50 = 55; behind vs master = 0
|
||||
// ahead vs develop = 5; behind vs develop = 5
|
||||
aheadBehinds: []aheadBehind{
|
||||
{ahead: 55, behind: 0, valid: true}, // master
|
||||
{ahead: 5, behind: 5, valid: true}, // develop ← smallest ahead
|
||||
},
|
||||
expected: 5,
|
||||
},
|
||||
{
|
||||
testName: "tie on ahead - first base wins (config order)",
|
||||
aheadBehinds: []aheadBehind{
|
||||
{ahead: 5, behind: 10, valid: true}, // first
|
||||
{ahead: 5, behind: 99, valid: true}, // second, same ahead
|
||||
},
|
||||
expected: 10,
|
||||
},
|
||||
{
|
||||
testName: "first base invalid, second valid",
|
||||
aheadBehinds: []aheadBehind{
|
||||
{},
|
||||
{ahead: 3, behind: 8, valid: true},
|
||||
},
|
||||
expected: 8,
|
||||
},
|
||||
{
|
||||
testName: "all invalid - returns 0",
|
||||
aheadBehinds: []aheadBehind{{}, {}},
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
testName: "empty - returns 0",
|
||||
aheadBehinds: nil,
|
||||
expected: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
result := selectBehindForBranch(s.aheadBehinds)
|
||||
assert.Equal(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAheadBehindForEachRefArgs(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
bases []string
|
||||
refPatterns []string
|
||||
expected []string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "single base",
|
||||
bases: []string{"refs/heads/master"},
|
||||
refPatterns: []string{"refs/heads"},
|
||||
expected: []string{
|
||||
"git",
|
||||
"for-each-ref",
|
||||
"--format=%(refname)%00%(ahead-behind:refs/heads/master)",
|
||||
"refs/heads",
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "two bases",
|
||||
bases: []string{"refs/heads/master", "refs/remotes/origin/develop"},
|
||||
refPatterns: []string{"refs/heads"},
|
||||
expected: []string{
|
||||
"git",
|
||||
"for-each-ref",
|
||||
"--format=%(refname)%00%(ahead-behind:refs/heads/master)%00%(ahead-behind:refs/remotes/origin/develop)",
|
||||
"refs/heads",
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "four bases",
|
||||
bases: []string{"refs/heads/a", "refs/heads/b", "refs/heads/c", "refs/heads/d"},
|
||||
refPatterns: []string{"refs/heads"},
|
||||
expected: []string{
|
||||
"git",
|
||||
"for-each-ref",
|
||||
"--format=%(refname)%00%(ahead-behind:refs/heads/a)%00%(ahead-behind:refs/heads/b)%00%(ahead-behind:refs/heads/c)%00%(ahead-behind:refs/heads/d)",
|
||||
"refs/heads",
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "commit hashes as bases, individual refs as patterns",
|
||||
bases: []string{"1234567", "89abcde"},
|
||||
refPatterns: []string{"refs/heads/a", "refs/remotes/origin/b"},
|
||||
expected: []string{
|
||||
"git",
|
||||
"for-each-ref",
|
||||
"--format=%(refname)%00%(ahead-behind:1234567)%00%(ahead-behind:89abcde)",
|
||||
"refs/heads/a",
|
||||
"refs/remotes/origin/b",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
result := buildAheadBehindForEachRefArgs(s.bases, s.refPatterns)
|
||||
assert.Equal(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -285,11 +285,26 @@ func (self *BranchCommands) Merge(branchName string, variant MergeVariant) error
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
// Returns whether refName can be fast-forward merged into the current branch
|
||||
func (self *BranchCommands) CanDoFastForwardMerge(refName string) bool {
|
||||
// Fast-forwards the branch that is checked out in the given worktree to the
|
||||
// given ref. Fails if that can't be done without a merge commit. Pass empty
|
||||
// strings for the worktree to use the current one.
|
||||
func (self *BranchCommands) FastForwardMerge(refName string, worktreeGitDir string, worktreePath string) error {
|
||||
cmdArgs := NewGitCmd("merge").
|
||||
Arg("--ff-only").
|
||||
Arg(refName).
|
||||
GitDirIf(worktreeGitDir != "", worktreeGitDir).
|
||||
WorktreePathIf(worktreePath != "", worktreePath).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
// Returns whether the first ref is an ancestor of the second one, which also
|
||||
// means that the second one can be fast-forward merged into the first one
|
||||
func (self *BranchCommands) IsAncestor(ancestorRefName string, refName string) bool {
|
||||
cmdArgs := NewGitCmd("merge-base").
|
||||
Arg("--is-ancestor").
|
||||
Arg("HEAD", refName).
|
||||
Arg(ancestorRefName, refName).
|
||||
ToArgv()
|
||||
err := self.cmd.New(cmdArgs).DontLog().Run()
|
||||
return err == nil
|
||||
@@ -353,9 +368,85 @@ func (self *BranchCommands) IsBranchMerged(branch *models.Branch, mainBranches *
|
||||
return stdout == "", nil
|
||||
}
|
||||
|
||||
func (self *BranchCommands) UpdateBranchRefs(updateCommands string) error {
|
||||
// Returns whether the given branch has commits of its own, meaning commits
|
||||
// that its remote branch never contained. Those are the commits that would be
|
||||
// lost if we reset the branch to its upstream.
|
||||
//
|
||||
// A branch that has diverged from its upstream doesn't necessarily have any
|
||||
// commits of its own. If somebody else rewrote the remote branch and
|
||||
// force-pushed it, our branch is still at the commits it had before, and all
|
||||
// of those were on the remote branch at some point. The reflog of the
|
||||
// remote-tracking branch records the values it had before it was rewritten, so
|
||||
// a commit that was ever on the remote branch is contained in one of them.
|
||||
func (self *BranchCommands) HasLocalOnlyCommits(branch *models.Branch) (bool, error) {
|
||||
upstreamValues := append(
|
||||
[]string{branch.FullUpstreamRefName()},
|
||||
self.previousUpstreamValues(branch.FullUpstreamRefName())...,
|
||||
)
|
||||
|
||||
cmdArgs := NewGitCmd("rev-list").
|
||||
Arg("--max-count=1").
|
||||
// A value that the remote-tracking branch had long ago might not be
|
||||
// available any more, e.g. in a partial clone. Skip it rather than
|
||||
// failing; it only means we exclude fewer commits.
|
||||
Arg("--ignore-missing").
|
||||
Arg(branch.FullRefName()).
|
||||
Arg(lo.Map(upstreamValues, func(value string, _ int) string {
|
||||
return "^" + value
|
||||
})...).
|
||||
Arg("--").
|
||||
ToArgv()
|
||||
|
||||
stdout, _, err := self.cmd.New(cmdArgs).DontLog().RunWithOutputs()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return stdout != "", nil
|
||||
}
|
||||
|
||||
// Returns the values that the given remote-tracking branch had before its
|
||||
// current one, as far back as its reflog goes. Returns nothing if the reflog
|
||||
// is unavailable, for example because core.logAllRefUpdates is false; a branch
|
||||
// that is strictly behind its upstream is recognized without it.
|
||||
func (self *BranchCommands) previousUpstreamValues(upstreamRef string) []string {
|
||||
cmdArgs := NewGitCmd("reflog").
|
||||
Arg("show").
|
||||
Arg("--format=%H").
|
||||
Arg(upstreamRef).
|
||||
ToArgv()
|
||||
|
||||
stdout, _, err := self.cmd.New(cmdArgs).DontLog().RunWithOutputs()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Each entry holds the value that the ref was updated to.
|
||||
values := utils.SplitLines(stdout)
|
||||
|
||||
// The value it had before the oldest entry is that entry's old value, and
|
||||
// the only way to name it is <ref>@{<number of entries>}. It doesn't exist
|
||||
// if the oldest entry is the one that created the ref, and asking for it
|
||||
// then is an error rather than an empty result.
|
||||
cmdArgs = NewGitCmd("rev-parse").
|
||||
Arg("-q", "--verify").
|
||||
Arg(fmt.Sprintf("%s@{%d}", upstreamRef, len(values))).
|
||||
ToArgv()
|
||||
|
||||
if stdout, _, err := self.cmd.New(cmdArgs).DontLog().RunWithOutputs(); err == nil {
|
||||
values = append(values, strings.TrimSpace(stdout))
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
// Moves branches by writing refs directly. The reflog message is what
|
||||
// `git reflog <branch>` shows for the update; it is the only hint about who
|
||||
// moved the branch, as no git command shows up in the reflog for this.
|
||||
func (self *BranchCommands) UpdateBranchRefs(updateCommands string, reflogMessage string) error {
|
||||
cmdArgs := NewGitCmd("update-ref").
|
||||
Arg("--stdin").
|
||||
Arg("-m", reflogMessage).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).SetStdin(updateCommands).Run()
|
||||
|
||||
@@ -44,6 +44,7 @@ type BranchLoader struct {
|
||||
*GitCommon
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
getCurrentBranchInfo func() (BranchInfo, error)
|
||||
hasLocalOnlyCommits func(*models.Branch) (bool, error)
|
||||
config BranchLoaderConfigCommands
|
||||
}
|
||||
|
||||
@@ -52,6 +53,7 @@ func NewBranchLoader(
|
||||
gitCommon *GitCommon,
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
getCurrentBranchInfo func() (BranchInfo, error),
|
||||
hasLocalOnlyCommits func(*models.Branch) (bool, error),
|
||||
config BranchLoaderConfigCommands,
|
||||
) *BranchLoader {
|
||||
return &BranchLoader{
|
||||
@@ -59,6 +61,7 @@ func NewBranchLoader(
|
||||
GitCommon: gitCommon,
|
||||
cmd: cmd,
|
||||
getCurrentBranchInfo: getCurrentBranchInfo,
|
||||
hasLocalOnlyCommits: hasLocalOnlyCommits,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
@@ -67,13 +70,21 @@ func NewBranchLoader(
|
||||
func (self *BranchLoader) Load(reflogCommits []*models.Commit,
|
||||
mainBranches *MainBranches,
|
||||
oldBranches []*models.Branch,
|
||||
loadBehindCounts bool,
|
||||
loadExtraInfo bool,
|
||||
onWorker func(func() error),
|
||||
renderFunc func(),
|
||||
) ([]*models.Branch, error) {
|
||||
branches := self.obtainBranches()
|
||||
branches, tips := self.obtainBranches()
|
||||
|
||||
if self.UserConfig().Git.LocalBranchSortOrder == "recency" {
|
||||
switch self.UserConfig().Git.LocalBranchSortOrder {
|
||||
case "date":
|
||||
if err := sortRefsWithEqualDatesByAncestry(
|
||||
self.cmd, self.version, branches, (*models.Branch).FullRefName, tips,
|
||||
); err != nil {
|
||||
self.Log.Errorf("Failed to sort branches by ancestry: %v", err)
|
||||
}
|
||||
|
||||
case "recency":
|
||||
reflogBranches := self.obtainReflogBranches(reflogCommits)
|
||||
// loop through reflog branches. If there is a match, merge them, then remove it from the branches and keep it in the reflog branches
|
||||
branchesWithRecency := make([]*models.Branch, 0)
|
||||
@@ -127,24 +138,63 @@ func (self *BranchLoader) Load(reflogCommits []*models.Commit,
|
||||
branch.UpstreamBranch = match.Merge
|
||||
}
|
||||
|
||||
// If the branch already existed, take over its BehindBaseBranch value
|
||||
// to reduce flicker
|
||||
// If the branch already existed, take over the values that are
|
||||
// determined in the background, to reduce flicker
|
||||
if oldBranch, found := lo.Find(oldBranches, func(b *models.Branch) bool {
|
||||
return b.Name == branch.Name
|
||||
}); found {
|
||||
branch.BehindBaseBranch.Store(oldBranch.BehindBaseBranch.Load())
|
||||
branch.UpstreamRewritten.Store(oldBranch.UpstreamRewritten.Load())
|
||||
}
|
||||
}
|
||||
|
||||
if loadBehindCounts && self.UserConfig().Gui.ShowDivergenceFromBaseBranch != "none" {
|
||||
if loadExtraInfo {
|
||||
if self.UserConfig().Gui.ShowDivergenceFromBaseBranch != "none" {
|
||||
onWorker(func() error {
|
||||
return self.GetBehindBaseBranchValuesForAllBranches(branches, mainBranches, renderFunc)
|
||||
})
|
||||
}
|
||||
|
||||
onWorker(func() error {
|
||||
return self.GetBehindBaseBranchValuesForAllBranches(branches, mainBranches, renderFunc)
|
||||
return self.checkForRewrittenUpstreams(branches, renderFunc)
|
||||
})
|
||||
}
|
||||
|
||||
return branches, nil
|
||||
}
|
||||
|
||||
// For each branch that has diverged from its upstream, determines whether the
|
||||
// divergence comes from the upstream branch having been rewritten, and stores
|
||||
// the answer in the branch. A branch that we can't determine it for keeps the
|
||||
// answer "no", so that we don't offer anything we aren't sure about.
|
||||
func (self *BranchLoader) checkForRewrittenUpstreams(branches []*models.Branch, renderFunc func()) error {
|
||||
t := time.Now()
|
||||
errg := errgroup.Group{}
|
||||
|
||||
for _, branch := range branches {
|
||||
if !branch.IsAheadForPull() || !branch.IsBehindForPull() {
|
||||
branch.UpstreamRewritten.Store(false)
|
||||
continue
|
||||
}
|
||||
|
||||
errg.Go(func() error {
|
||||
hasLocalOnlyCommits, err := self.hasLocalOnlyCommits(branch)
|
||||
if err != nil {
|
||||
// Not worth bothering the user about; it only means that we
|
||||
// don't show this branch differently.
|
||||
self.Log.Errorf("Failed to check whether branch %s has commits of its own: %v", branch.Name, err)
|
||||
}
|
||||
branch.UpstreamRewritten.Store(err == nil && !hasLocalOnlyCommits)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
err := errg.Wait()
|
||||
self.Log.Debugf("time to check for rewritten upstreams for all branches: %s", time.Since(t))
|
||||
renderFunc()
|
||||
return err
|
||||
}
|
||||
|
||||
func (self *BranchLoader) GetBehindBaseBranchValuesForAllBranches(
|
||||
branches []*models.Branch,
|
||||
mainBranches *MainBranches,
|
||||
@@ -206,94 +256,6 @@ func (self *BranchLoader) getBehindBaseBranchValuesLegacy(
|
||||
return err
|
||||
}
|
||||
|
||||
// Holds parsed values from a single %(ahead-behind:<base>) field.
|
||||
type aheadBehind struct {
|
||||
ahead, behind int
|
||||
}
|
||||
|
||||
type branchAheadBehind struct {
|
||||
refName string
|
||||
aheadBehinds []aheadBehind
|
||||
}
|
||||
|
||||
// Parses output produced by:
|
||||
//
|
||||
// git for-each-ref --format='%(refname)\x00%(ahead-behind:<base1>)\x00...' refs/heads
|
||||
//
|
||||
// Lines whose NUL-split column count doesn't match (1 + numBases) are dropped.
|
||||
// Blank lines are ignored.
|
||||
// Individual malformed ahead-behind fields produce {valid: false} entries
|
||||
func parseAheadBehindForEachRefOutput(
|
||||
output string,
|
||||
numBases int, // number of %(ahead-behind:...) tokens
|
||||
) []branchAheadBehind {
|
||||
if output == "" {
|
||||
return nil
|
||||
}
|
||||
lines := strings.Split(output, "\n")
|
||||
result := make([]branchAheadBehind, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
cols := strings.Split(line, "\x00")
|
||||
if len(cols) != numBases+1 {
|
||||
continue
|
||||
}
|
||||
refName := cols[0]
|
||||
aheadBehinds := lo.FilterMap(cols[1:], func(col string, _ int) (aheadBehind, bool) {
|
||||
return parseAheadBehindField(col)
|
||||
})
|
||||
entry := branchAheadBehind{
|
||||
refName: refName,
|
||||
aheadBehinds: aheadBehinds,
|
||||
}
|
||||
result = append(result, entry)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func parseAheadBehindField(s string) (aheadBehind, bool) {
|
||||
parts := strings.Fields(s)
|
||||
if len(parts) != 2 {
|
||||
return aheadBehind{}, false
|
||||
}
|
||||
ahead, err1 := strconv.Atoi(parts[0])
|
||||
behind, err2 := strconv.Atoi(parts[1])
|
||||
if err1 != nil || err2 != nil {
|
||||
return aheadBehind{}, false
|
||||
}
|
||||
return aheadBehind{ahead: ahead, behind: behind}, true
|
||||
}
|
||||
|
||||
// Picks the "closest" base by smallest ahead value (commits the branch
|
||||
// has that the base doesn't = roughly "since fork point") and returns
|
||||
// its behind value.
|
||||
// Ties are broken by index order
|
||||
func selectBehindForBranch(aheadBehinds []aheadBehind) int {
|
||||
return lo.MinBy(aheadBehinds, func(a, b aheadBehind) bool {
|
||||
return a.ahead < b.ahead
|
||||
}).behind
|
||||
}
|
||||
|
||||
// The output format is:
|
||||
//
|
||||
// <refname>\x00<ahead> <behind>\x00<ahead> <behind>...\n
|
||||
//
|
||||
// with one ahead-behind field per base, in the same order as mainBranchRefs.
|
||||
//
|
||||
// Requires git >= 2.41 (when %(ahead-behind:...) was added).
|
||||
func buildAheadBehindForEachRefArgs(mainBranchRefs []string) []string {
|
||||
formatParts := make([]string, 0, 1+len(mainBranchRefs))
|
||||
formatParts = append(formatParts, "%(refname)")
|
||||
for _, ref := range mainBranchRefs {
|
||||
formatParts = append(formatParts, "%(ahead-behind:"+ref+")")
|
||||
}
|
||||
format := strings.Join(formatParts, "%00")
|
||||
|
||||
return NewGitCmd("for-each-ref").
|
||||
Arg("--format=" + format).
|
||||
Arg("refs/heads").
|
||||
ToArgv()
|
||||
}
|
||||
|
||||
func (self *BranchLoader) getBehindBaseBranchValuesFast(
|
||||
branches []*models.Branch,
|
||||
mainBranchRefs []string,
|
||||
@@ -302,7 +264,7 @@ func (self *BranchLoader) getBehindBaseBranchValuesFast(
|
||||
t := time.Now()
|
||||
|
||||
output, err := self.cmd.New(
|
||||
buildAheadBehindForEachRefArgs(mainBranchRefs),
|
||||
buildAheadBehindForEachRefArgs(mainBranchRefs, []string{"refs/heads"}),
|
||||
).DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -362,7 +324,9 @@ func (self *BranchLoader) GetBaseBranch(branch *models.Branch, mainBranches *Mai
|
||||
return split[0], nil
|
||||
}
|
||||
|
||||
func (self *BranchLoader) obtainBranches() []*models.Branch {
|
||||
// Returns the branches, along with the tip of each of them, keyed by full ref
|
||||
// name
|
||||
func (self *BranchLoader) obtainBranches() ([]*models.Branch, map[string]refTip) {
|
||||
output, err := self.getRawBranches()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -371,7 +335,8 @@ func (self *BranchLoader) obtainBranches() []*models.Branch {
|
||||
trimmedOutput := strings.TrimSpace(output)
|
||||
outputLines := strings.Split(trimmedOutput, "\n")
|
||||
|
||||
return lo.FilterMap(outputLines, func(line string, _ int) (*models.Branch, bool) {
|
||||
tips := make(map[string]refTip, len(outputLines))
|
||||
branches := lo.FilterMap(outputLines, func(line string, _ int) (*models.Branch, bool) {
|
||||
if line == "" {
|
||||
return nil, false
|
||||
}
|
||||
@@ -385,8 +350,12 @@ func (self *BranchLoader) obtainBranches() []*models.Branch {
|
||||
}
|
||||
|
||||
storeCommitDateAsRecency := self.UserConfig().Git.LocalBranchSortOrder != "recency"
|
||||
return obtainBranch(split, storeCommitDateAsRecency), true
|
||||
branch, tip := obtainBranch(split, storeCommitDateAsRecency)
|
||||
tips[branch.FullRefName()] = tip
|
||||
return branch, true
|
||||
})
|
||||
|
||||
return branches, tips
|
||||
}
|
||||
|
||||
func (self *BranchLoader) getRawBranches() (string, error) {
|
||||
@@ -422,25 +391,28 @@ var branchFields = []string{
|
||||
"upstream:short",
|
||||
"upstream:track",
|
||||
"push:track",
|
||||
"push",
|
||||
"subject",
|
||||
"objectname",
|
||||
"committerdate:unix",
|
||||
}
|
||||
|
||||
// Obtain branch information from parsed line output of getRawBranches()
|
||||
func obtainBranch(split []string, storeCommitDateAsRecency bool) *models.Branch {
|
||||
func obtainBranch(split []string, storeCommitDateAsRecency bool) (*models.Branch, refTip) {
|
||||
headMarker := split[0]
|
||||
fullName := split[1]
|
||||
upstreamName := split[2]
|
||||
track := split[3]
|
||||
pushTrack := split[4]
|
||||
subject := split[5]
|
||||
commitHash := split[6]
|
||||
commitDate := split[7]
|
||||
pushRef := split[5]
|
||||
subject := split[6]
|
||||
commitHash := split[7]
|
||||
commitDate := split[8]
|
||||
|
||||
name := strings.TrimPrefix(fullName, "heads/")
|
||||
aheadForPull, behindForPull, gone := parseUpstreamInfo(upstreamName, track)
|
||||
aheadForPush, behindForPush, _ := parseUpstreamInfo(upstreamName, pushTrack)
|
||||
pushRemote, pushBranch := parsePushDestination(pushRef)
|
||||
|
||||
recency := ""
|
||||
if storeCommitDateAsRecency {
|
||||
@@ -449,18 +421,22 @@ func obtainBranch(split []string, storeCommitDateAsRecency bool) *models.Branch
|
||||
}
|
||||
}
|
||||
|
||||
return &models.Branch{
|
||||
branch := &models.Branch{
|
||||
Name: name,
|
||||
Recency: recency,
|
||||
AheadForPull: aheadForPull,
|
||||
BehindForPull: behindForPull,
|
||||
AheadForPush: aheadForPush,
|
||||
BehindForPush: behindForPush,
|
||||
PushRemote: pushRemote,
|
||||
PushBranch: pushBranch,
|
||||
UpstreamGone: gone,
|
||||
Head: headMarker == "*",
|
||||
Subject: subject,
|
||||
CommitHash: commitHash,
|
||||
}
|
||||
|
||||
return branch, refTip{hash: commitHash, committerDate: commitDate}
|
||||
}
|
||||
|
||||
func parseUpstreamInfo(upstreamName string, track string) (string, string, bool) {
|
||||
@@ -481,6 +457,25 @@ func parseUpstreamInfo(upstreamName string, track string) (string, string, bool)
|
||||
return ahead, behind, false
|
||||
}
|
||||
|
||||
// Splits the remote-tracking ref that the %(push) field names, e.g.
|
||||
// refs/remotes/origin/main, into the remote and the remote branch. Returns
|
||||
// empty strings if the field is empty because git has no push destination for
|
||||
// the branch, or if the ref isn't under refs/remotes/.
|
||||
func parsePushDestination(pushRef string) (string, string) {
|
||||
remoteAndBranch, ok := strings.CutPrefix(pushRef, "refs/remotes/")
|
||||
if !ok {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// Remote names can't contain slashes, so the first one ends the remote name
|
||||
remote, branch, ok := strings.Cut(remoteAndBranch, "/")
|
||||
if !ok {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
return remote, branch
|
||||
}
|
||||
|
||||
func parseDifference(track string, regexStr string) string {
|
||||
re := regexp.MustCompile(regexStr)
|
||||
match := re.FindStringSubmatch(track)
|
||||
|
||||
@@ -6,8 +6,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -26,7 +28,7 @@ func TestObtainBranch(t *testing.T) {
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "TrimHeads",
|
||||
input: []string{"", "heads/a_branch", "", "", "", "subject", "123", timeStamp},
|
||||
input: []string{"", "heads/a_branch", "", "", "", "", "subject", "123", timeStamp},
|
||||
storeCommitDateAsRecency: false,
|
||||
expectedBranch: &models.Branch{
|
||||
Name: "a_branch",
|
||||
@@ -41,7 +43,7 @@ func TestObtainBranch(t *testing.T) {
|
||||
},
|
||||
{
|
||||
testName: "NoUpstream",
|
||||
input: []string{"", "a_branch", "", "", "", "subject", "123", timeStamp},
|
||||
input: []string{"", "a_branch", "", "", "", "", "subject", "123", timeStamp},
|
||||
storeCommitDateAsRecency: false,
|
||||
expectedBranch: &models.Branch{
|
||||
Name: "a_branch",
|
||||
@@ -56,7 +58,7 @@ func TestObtainBranch(t *testing.T) {
|
||||
},
|
||||
{
|
||||
testName: "IsHead",
|
||||
input: []string{"*", "a_branch", "", "", "", "subject", "123", timeStamp},
|
||||
input: []string{"*", "a_branch", "", "", "", "", "subject", "123", timeStamp},
|
||||
storeCommitDateAsRecency: false,
|
||||
expectedBranch: &models.Branch{
|
||||
Name: "a_branch",
|
||||
@@ -71,7 +73,7 @@ func TestObtainBranch(t *testing.T) {
|
||||
},
|
||||
{
|
||||
testName: "IsBehindAndAhead",
|
||||
input: []string{"", "a_branch", "a_remote/a_branch", "[behind 2, ahead 3]", "[behind 2, ahead 3]", "subject", "123", timeStamp},
|
||||
input: []string{"", "a_branch", "a_remote/a_branch", "[behind 2, ahead 3]", "[behind 2, ahead 3]", "refs/remotes/a_remote/a_branch", "subject", "123", timeStamp},
|
||||
storeCommitDateAsRecency: false,
|
||||
expectedBranch: &models.Branch{
|
||||
Name: "a_branch",
|
||||
@@ -79,6 +81,40 @@ func TestObtainBranch(t *testing.T) {
|
||||
BehindForPull: "2",
|
||||
AheadForPush: "3",
|
||||
BehindForPush: "2",
|
||||
PushRemote: "a_remote",
|
||||
PushBranch: "a_branch",
|
||||
Head: false,
|
||||
Subject: "subject",
|
||||
CommitHash: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "PushDestinationDiffersFromUpstream",
|
||||
input: []string{"", "a_branch", "a_remote/a_branch", "[ahead 3]", "[ahead 5]", "refs/remotes/my_fork/feature/a_branch", "subject", "123", timeStamp},
|
||||
storeCommitDateAsRecency: false,
|
||||
expectedBranch: &models.Branch{
|
||||
Name: "a_branch",
|
||||
AheadForPull: "3",
|
||||
BehindForPull: "0",
|
||||
AheadForPush: "5",
|
||||
BehindForPush: "0",
|
||||
PushRemote: "my_fork",
|
||||
PushBranch: "feature/a_branch",
|
||||
Head: false,
|
||||
Subject: "subject",
|
||||
CommitHash: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "PushDestinationNotARemoteTrackingRef",
|
||||
input: []string{"", "a_branch", "a_remote/a_branch", "", "", "refs/published/a_branch", "subject", "123", timeStamp},
|
||||
storeCommitDateAsRecency: false,
|
||||
expectedBranch: &models.Branch{
|
||||
Name: "a_branch",
|
||||
AheadForPull: "0",
|
||||
BehindForPull: "0",
|
||||
AheadForPush: "0",
|
||||
BehindForPush: "0",
|
||||
Head: false,
|
||||
Subject: "subject",
|
||||
CommitHash: "123",
|
||||
@@ -86,7 +122,7 @@ func TestObtainBranch(t *testing.T) {
|
||||
},
|
||||
{
|
||||
testName: "RemoteBranchIsGone",
|
||||
input: []string{"", "a_branch", "a_remote/a_branch", "[gone]", "[gone]", "subject", "123", timeStamp},
|
||||
input: []string{"", "a_branch", "a_remote/a_branch", "[gone]", "[gone]", "refs/remotes/a_remote/a_branch", "subject", "123", timeStamp},
|
||||
storeCommitDateAsRecency: false,
|
||||
expectedBranch: &models.Branch{
|
||||
Name: "a_branch",
|
||||
@@ -95,6 +131,8 @@ func TestObtainBranch(t *testing.T) {
|
||||
BehindForPull: "?",
|
||||
AheadForPush: "?",
|
||||
BehindForPush: "?",
|
||||
PushRemote: "a_remote",
|
||||
PushBranch: "a_branch",
|
||||
Head: false,
|
||||
Subject: "subject",
|
||||
CommitHash: "123",
|
||||
@@ -102,7 +140,7 @@ func TestObtainBranch(t *testing.T) {
|
||||
},
|
||||
{
|
||||
testName: "WithCommitDateAsRecency",
|
||||
input: []string{"", "a_branch", "", "", "", "subject", "123", timeStamp},
|
||||
input: []string{"", "a_branch", "", "", "", "", "subject", "123", timeStamp},
|
||||
storeCommitDateAsRecency: true,
|
||||
expectedBranch: &models.Branch{
|
||||
Name: "a_branch",
|
||||
@@ -120,245 +158,9 @@ func TestObtainBranch(t *testing.T) {
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
branch := obtainBranch(s.input, s.storeCommitDateAsRecency)
|
||||
branch, tip := obtainBranch(s.input, s.storeCommitDateAsRecency)
|
||||
assert.EqualValues(t, s.expectedBranch, branch)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAheadBehindForEachRefOutput(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
input string
|
||||
numBases int
|
||||
expected []branchAheadBehind
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "single branch single base",
|
||||
input: "refs/heads/feat\x002 5\n",
|
||||
numBases: 1,
|
||||
expected: []branchAheadBehind{
|
||||
{
|
||||
refName: "refs/heads/feat",
|
||||
aheadBehinds: []aheadBehind{{ahead: 2, behind: 5}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "multiple branches multiple bases",
|
||||
input: "refs/heads/feat\x002 5\x0010 1\n" +
|
||||
"refs/heads/main\x000 0\x000 0\n",
|
||||
numBases: 2,
|
||||
expected: []branchAheadBehind{
|
||||
{
|
||||
refName: "refs/heads/feat",
|
||||
aheadBehinds: []aheadBehind{
|
||||
{ahead: 2, behind: 5},
|
||||
{ahead: 10, behind: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
refName: "refs/heads/main",
|
||||
aheadBehinds: []aheadBehind{
|
||||
{ahead: 0, behind: 0},
|
||||
{ahead: 0, behind: 0},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "empty ahead-behind field for unreachable base",
|
||||
input: "refs/heads/feat\x00\x002 5\n",
|
||||
numBases: 2,
|
||||
expected: []branchAheadBehind{
|
||||
{
|
||||
refName: "refs/heads/feat",
|
||||
aheadBehinds: []aheadBehind{
|
||||
{ahead: 2, behind: 5},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "ref name containing slashes and dashes",
|
||||
input: "refs/heads/feat/foo-bar\x001 2\n",
|
||||
numBases: 1,
|
||||
expected: []branchAheadBehind{
|
||||
{
|
||||
refName: "refs/heads/feat/foo-bar",
|
||||
aheadBehinds: []aheadBehind{{ahead: 1, behind: 2}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "trailing newline and blank lines are ignored",
|
||||
input: "refs/heads/feat\x001 2\n\n",
|
||||
numBases: 1,
|
||||
expected: []branchAheadBehind{
|
||||
{
|
||||
refName: "refs/heads/feat",
|
||||
aheadBehinds: []aheadBehind{{ahead: 1, behind: 2}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "line with wrong column count is skipped",
|
||||
input: "refs/heads/good\x001 2\n" +
|
||||
"refs/heads/bad\n" +
|
||||
"refs/heads/also_good\x003 4\n",
|
||||
numBases: 1,
|
||||
expected: []branchAheadBehind{
|
||||
{
|
||||
refName: "refs/heads/good",
|
||||
aheadBehinds: []aheadBehind{{ahead: 1, behind: 2}},
|
||||
},
|
||||
{
|
||||
refName: "refs/heads/also_good",
|
||||
aheadBehinds: []aheadBehind{{ahead: 3, behind: 4}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "malformed ahead-behind field becomes invalid but line is kept",
|
||||
input: "refs/heads/feat\x00not_a_number\n",
|
||||
numBases: 1,
|
||||
expected: []branchAheadBehind{
|
||||
{
|
||||
refName: "refs/heads/feat",
|
||||
aheadBehinds: []aheadBehind{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "empty input",
|
||||
input: "",
|
||||
numBases: 1,
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
result := parseAheadBehindForEachRefOutput(s.input, s.numBases)
|
||||
assert.Equal(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectBehindForBranch(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
aheadBehinds []aheadBehind
|
||||
expected int
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "single base, valid value",
|
||||
aheadBehinds: []aheadBehind{{ahead: 3, behind: 7}},
|
||||
expected: 7,
|
||||
},
|
||||
{
|
||||
testName: "multi-base, clear winner by ahead",
|
||||
aheadBehinds: []aheadBehind{
|
||||
{ahead: 50, behind: 10}, // master
|
||||
{ahead: 5, behind: 2}, // develop ← smallest ahead
|
||||
},
|
||||
expected: 2,
|
||||
},
|
||||
{
|
||||
testName: "develop forked from master case (ancestor-of-each-other)",
|
||||
// feat-x has 5 commits since fork from develop.
|
||||
// develop is 50 commits ahead of master.
|
||||
// ahead vs master = 5 + 50 = 55; behind vs master = 0
|
||||
// ahead vs develop = 5; behind vs develop = 5
|
||||
aheadBehinds: []aheadBehind{
|
||||
{ahead: 55, behind: 0}, // master
|
||||
{ahead: 5, behind: 5}, // develop ← smallest ahead
|
||||
},
|
||||
expected: 5,
|
||||
},
|
||||
{
|
||||
testName: "tie on ahead - first base wins (config order)",
|
||||
aheadBehinds: []aheadBehind{
|
||||
{ahead: 5, behind: 10}, // first
|
||||
{ahead: 5, behind: 99}, // second, same ahead
|
||||
},
|
||||
expected: 10,
|
||||
},
|
||||
{
|
||||
testName: "first base invalid, second valid",
|
||||
aheadBehinds: []aheadBehind{
|
||||
{ahead: 3, behind: 8},
|
||||
},
|
||||
expected: 8,
|
||||
},
|
||||
{
|
||||
testName: "all invalid - returns 0",
|
||||
aheadBehinds: []aheadBehind{},
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
testName: "empty - returns 0",
|
||||
aheadBehinds: nil,
|
||||
expected: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
result := selectBehindForBranch(s.aheadBehinds)
|
||||
assert.Equal(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAheadBehindForEachRefArgs(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
mainBranchRefs []string
|
||||
expected []string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "single base",
|
||||
mainBranchRefs: []string{"refs/heads/master"},
|
||||
expected: []string{
|
||||
"git",
|
||||
"for-each-ref",
|
||||
"--format=%(refname)%00%(ahead-behind:refs/heads/master)",
|
||||
"refs/heads",
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "two bases",
|
||||
mainBranchRefs: []string{"refs/heads/master", "refs/remotes/origin/develop"},
|
||||
expected: []string{
|
||||
"git",
|
||||
"for-each-ref",
|
||||
"--format=%(refname)%00%(ahead-behind:refs/heads/master)%00%(ahead-behind:refs/remotes/origin/develop)",
|
||||
"refs/heads",
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "four bases",
|
||||
mainBranchRefs: []string{"refs/heads/a", "refs/heads/b", "refs/heads/c", "refs/heads/d"},
|
||||
expected: []string{
|
||||
"git",
|
||||
"for-each-ref",
|
||||
"--format=%(refname)%00%(ahead-behind:refs/heads/a)%00%(ahead-behind:refs/heads/b)%00%(ahead-behind:refs/heads/c)%00%(ahead-behind:refs/heads/d)",
|
||||
"refs/heads",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
result := buildAheadBehindForEachRefArgs(s.mainBranchRefs)
|
||||
assert.Equal(t, s.expected, result)
|
||||
assert.Equal(t, refTip{hash: "123", committerDate: timeStamp}, tip)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -491,3 +293,62 @@ func TestGetBehindBaseBranchValuesForAllBranches_LegacyPath(t *testing.T) {
|
||||
|
||||
runner.CheckForMissingCalls()
|
||||
}
|
||||
|
||||
func TestCheckForRewrittenUpstreams(t *testing.T) {
|
||||
branch := func(name string, ahead string, behind string) *models.Branch {
|
||||
return &models.Branch{
|
||||
Name: name,
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: name,
|
||||
AheadForPull: ahead,
|
||||
BehindForPull: behind,
|
||||
}
|
||||
}
|
||||
|
||||
notDiverged := branch("not-diverged", "0", "2")
|
||||
rewritten := branch("rewritten", "3", "5")
|
||||
ownCommits := branch("own-commits", "3", "5")
|
||||
failing := branch("failing", "1", "1")
|
||||
|
||||
// A branch that is no longer diverged must lose the value it had before
|
||||
notDiverged.UpstreamRewritten.Store(true)
|
||||
|
||||
branches := []*models.Branch{notDiverged, rewritten, ownCommits, failing}
|
||||
|
||||
var mutex deadlock.Mutex
|
||||
queried := []string{}
|
||||
hasLocalOnlyCommits := func(branch *models.Branch) (bool, error) {
|
||||
mutex.Lock()
|
||||
queried = append(queried, branch.Name)
|
||||
mutex.Unlock()
|
||||
|
||||
switch branch.Name {
|
||||
case "own-commits":
|
||||
return true, nil
|
||||
case "failing":
|
||||
return false, errors.New("error")
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
gitCommon := buildGitCommon(commonDeps{})
|
||||
loader := &BranchLoader{
|
||||
Common: gitCommon.Common,
|
||||
GitCommon: gitCommon,
|
||||
cmd: gitCommon.cmd,
|
||||
hasLocalOnlyCommits: hasLocalOnlyCommits,
|
||||
}
|
||||
|
||||
rendered := false
|
||||
err := loader.checkForRewrittenUpstreams(branches, func() { rendered = true })
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, rendered, "renderFunc should have been called")
|
||||
|
||||
assert.ElementsMatch(t, []string{"rewritten", "own-commits", "failing"}, queried,
|
||||
"only diverged branches should be looked at")
|
||||
assert.False(t, notDiverged.UpstreamRewritten.Load())
|
||||
assert.True(t, rewritten.UpstreamRewritten.Load())
|
||||
assert.False(t, ownCommits.UpstreamRewritten.Load())
|
||||
assert.False(t, failing.UpstreamRewritten.Load(), "a failed check should not claim anything")
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -317,3 +318,111 @@ func TestBranchCurrentBranchInfo(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBranchHasLocalOnlyCommits(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
test func(bool, error)
|
||||
}
|
||||
|
||||
branch := &models.Branch{
|
||||
Name: "branch",
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "branch",
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"branch is strictly behind its upstream, and there are no reflogs",
|
||||
oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"reflog", "show", "--format=%H", "refs/remotes/origin/branch"}, "", nil).
|
||||
ExpectGitArgs([]string{"rev-parse", "-q", "--verify", "refs/remotes/origin/branch@{0}"}, "", errors.New("error")).
|
||||
ExpectGitArgs([]string{
|
||||
"rev-list", "--max-count=1", "--ignore-missing", "refs/heads/branch",
|
||||
"^refs/remotes/origin/branch", "--",
|
||||
}, "", nil),
|
||||
func(hasLocalOnlyCommits bool, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, hasLocalOnlyCommits)
|
||||
},
|
||||
},
|
||||
{
|
||||
"the upstream branch was rewritten, so all our commits were on it before",
|
||||
oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"reflog", "show", "--format=%H", "refs/remotes/origin/branch"},
|
||||
"1111111111111111111111111111111111111111\n2222222222222222222222222222222222222222\n", nil).
|
||||
ExpectGitArgs([]string{"rev-parse", "-q", "--verify", "refs/remotes/origin/branch@{2}"},
|
||||
"3333333333333333333333333333333333333333\n", nil).
|
||||
ExpectGitArgs([]string{
|
||||
"rev-list", "--max-count=1", "--ignore-missing", "refs/heads/branch",
|
||||
"^refs/remotes/origin/branch",
|
||||
"^1111111111111111111111111111111111111111",
|
||||
"^2222222222222222222222222222222222222222",
|
||||
"^3333333333333333333333333333333333333333",
|
||||
"--",
|
||||
}, "", nil),
|
||||
func(hasLocalOnlyCommits bool, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, hasLocalOnlyCommits)
|
||||
},
|
||||
},
|
||||
{
|
||||
"the oldest reflog entry is the one that created the ref",
|
||||
oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"reflog", "show", "--format=%H", "refs/remotes/origin/branch"},
|
||||
"1111111111111111111111111111111111111111\n", nil).
|
||||
ExpectGitArgs([]string{"rev-parse", "-q", "--verify", "refs/remotes/origin/branch@{1}"}, "", errors.New("error")).
|
||||
ExpectGitArgs([]string{
|
||||
"rev-list", "--max-count=1", "--ignore-missing", "refs/heads/branch",
|
||||
"^refs/remotes/origin/branch",
|
||||
"^1111111111111111111111111111111111111111",
|
||||
"--",
|
||||
}, "", nil),
|
||||
func(hasLocalOnlyCommits bool, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, hasLocalOnlyCommits)
|
||||
},
|
||||
},
|
||||
{
|
||||
"the branch has a commit that was never on the upstream branch",
|
||||
oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"reflog", "show", "--format=%H", "refs/remotes/origin/branch"},
|
||||
"1111111111111111111111111111111111111111\n", nil).
|
||||
ExpectGitArgs([]string{"rev-parse", "-q", "--verify", "refs/remotes/origin/branch@{1}"},
|
||||
"2222222222222222222222222222222222222222\n", nil).
|
||||
ExpectGitArgs([]string{
|
||||
"rev-list", "--max-count=1", "--ignore-missing", "refs/heads/branch",
|
||||
"^refs/remotes/origin/branch",
|
||||
"^1111111111111111111111111111111111111111",
|
||||
"^2222222222222222222222222222222222222222",
|
||||
"--",
|
||||
}, "4444444444444444444444444444444444444444\n", nil),
|
||||
func(hasLocalOnlyCommits bool, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, hasLocalOnlyCommits)
|
||||
},
|
||||
},
|
||||
{
|
||||
"bubbles up an error from rev-list",
|
||||
oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"reflog", "show", "--format=%H", "refs/remotes/origin/branch"}, "", nil).
|
||||
ExpectGitArgs([]string{"rev-parse", "-q", "--verify", "refs/remotes/origin/branch@{0}"}, "", errors.New("error")).
|
||||
ExpectGitArgs([]string{
|
||||
"rev-list", "--max-count=1", "--ignore-missing", "refs/heads/branch",
|
||||
"^refs/remotes/origin/branch", "--",
|
||||
}, "", errors.New("error")),
|
||||
func(hasLocalOnlyCommits bool, err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildBranchCommands(commonDeps{runner: s.runner})
|
||||
s.test(instance.HasLocalOnlyCommits(branch))
|
||||
s.runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,12 +240,12 @@ func (self *CommitCommands) AmendHeadCmdObj() *oscommands.CmdObj {
|
||||
return self.cmd.New(cmdArgs)
|
||||
}
|
||||
|
||||
func (self *CommitCommands) ShowCmdObj(hash string, filterPaths []string) *oscommands.CmdObj {
|
||||
func (self *CommitCommands) ShowCmdObj(hash string, filterPaths []string, mode DiffMode) *oscommands.CmdObj {
|
||||
cmdArgs := NewGitCmd("show").
|
||||
Config("diff.noprefix=false").
|
||||
AddCommonDiffArgs(self.diffRendererConfigManager, self.UserConfig(), true).
|
||||
AddCommonDiffArgs(self.diffRendererConfigManager, self.UserConfig(), mode).
|
||||
Arg("--submodule").
|
||||
Arg("--color=" + self.diffRendererConfigManager.GetColorArg()).
|
||||
Arg("--color=" + mode.colorArg(self.diffRendererConfigManager)).
|
||||
Arg("--stat").
|
||||
Arg("--decorate").
|
||||
Arg("-p").
|
||||
|
||||
@@ -174,7 +174,7 @@ func (self *CommitLoader) MergeRebasingCommits(hashPool *utils.StringPool, commi
|
||||
}
|
||||
|
||||
if workingTreeState.Rebasing {
|
||||
rebasingCommits, err := self.getHydratedRebasingCommits(hashPool, addConflictedRebasingCommit)
|
||||
rebasingCommits, err := self.getHydratedRebasingCommits(hashPool, commits, addConflictedRebasingCommit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -251,8 +251,8 @@ func (self *CommitLoader) extractCommitFromLine(hashPool *utils.StringPool, line
|
||||
})
|
||||
}
|
||||
|
||||
func (self *CommitLoader) getHydratedRebasingCommits(hashPool *utils.StringPool, addConflictingCommit bool) ([]*models.Commit, error) {
|
||||
return self.getHydratedTodoCommits(hashPool, self.getRebasingCommits(hashPool, addConflictingCommit), false)
|
||||
func (self *CommitLoader) getHydratedRebasingCommits(hashPool *utils.StringPool, existingCommits []*models.Commit, addConflictingCommit bool) ([]*models.Commit, error) {
|
||||
return self.getHydratedTodoCommits(hashPool, self.getRebasingCommits(hashPool, addConflictingCommit), existingCommits, false)
|
||||
}
|
||||
|
||||
func (self *CommitLoader) getHydratedSequencerCommits(hashPool *utils.StringPool, workingTreeState models.WorkingTreeState) ([]*models.Commit, error) {
|
||||
@@ -271,39 +271,56 @@ func (self *CommitLoader) getHydratedSequencerCommits(hashPool *utils.StringPool
|
||||
}
|
||||
}
|
||||
|
||||
return self.getHydratedTodoCommits(hashPool, commits, true)
|
||||
return self.getHydratedTodoCommits(hashPool, commits, nil, true)
|
||||
}
|
||||
|
||||
func (self *CommitLoader) getHydratedTodoCommits(hashPool *utils.StringPool, todoCommits []*models.Commit, todoFileHasShortHashes bool) ([]*models.Commit, error) {
|
||||
func (self *CommitLoader) getHydratedTodoCommits(
|
||||
hashPool *utils.StringPool,
|
||||
todoCommits []*models.Commit,
|
||||
existingCommits []*models.Commit,
|
||||
todoFileHasShortHashes bool,
|
||||
) ([]*models.Commit, error) {
|
||||
if len(todoCommits) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
commitHashes := lo.FilterMap(todoCommits, func(commit *models.Commit, _ int) (string, bool) {
|
||||
return commit.Hash(), commit.Hash() != ""
|
||||
})
|
||||
|
||||
// note that we're not filtering these as we do non-rebasing commits just because
|
||||
// I suspect that will cause some damage
|
||||
cmdObj := self.cmd.New(
|
||||
NewGitCmd("show").
|
||||
Config("log.showSignature=false").
|
||||
Arg("--no-patch", "--oneline", "--abbrev=20", prettyFormat).
|
||||
Arg(commitHashes...).
|
||||
ToArgv(),
|
||||
).DontLog()
|
||||
|
||||
// A refresh of only the rebasing todos should reuse the already loaded todos to avoid
|
||||
// unnecessary git show calls.
|
||||
fullCommits := map[string]*models.Commit{}
|
||||
err := cmdObj.RunAndProcessLines(func(line string) (bool, error) {
|
||||
if line == "" || line[0] != '+' {
|
||||
return false, nil
|
||||
for _, commit := range existingCommits {
|
||||
if commit.IsTODO() && commit.Hash() != "" {
|
||||
// Make a copy of the commit; that's necessary to avoid mutating the original commit
|
||||
// when we later reuse it in the loop at the end of this function.
|
||||
fullCommits[commit.Hash()] = lo.ToPtr(*commit)
|
||||
}
|
||||
commit := self.extractCommitFromLine(hashPool, line[1:], false)
|
||||
fullCommits[commit.Hash()] = commit
|
||||
return false, nil
|
||||
}
|
||||
|
||||
commitHashesToFetch := lo.FilterMap(todoCommits, func(commit *models.Commit, _ int) (string, bool) {
|
||||
return commit.Hash(), commit.Hash() != "" && fullCommits[commit.Hash()] == nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
if len(commitHashesToFetch) > 0 {
|
||||
// note that we're not filtering these as we do non-rebasing commits just because
|
||||
// I suspect that will cause some damage
|
||||
cmdObj := self.cmd.New(
|
||||
NewGitCmd("show").
|
||||
Config("log.showSignature=false").
|
||||
Arg("--no-patch", "--oneline", "--abbrev=20", prettyFormat).
|
||||
Arg(commitHashesToFetch...).
|
||||
ToArgv(),
|
||||
).DontLog()
|
||||
|
||||
err := cmdObj.RunAndProcessLines(func(line string) (bool, error) {
|
||||
if line == "" || line[0] != '+' {
|
||||
return false, nil
|
||||
}
|
||||
commit := self.extractCommitFromLine(hashPool, line[1:], false)
|
||||
fullCommits[commit.Hash()] = commit
|
||||
return false, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
findFullCommit := lo.Ternary(todoFileHasShortHashes,
|
||||
|
||||
@@ -538,6 +538,110 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitLoaderGetHydratedTodoCommitsReusesExistingCommit(t *testing.T) {
|
||||
hashPool := &utils.StringPool{}
|
||||
runner := oscommands.NewFakeRunner(t)
|
||||
loader := &CommitLoader{
|
||||
cmd: oscommands.NewDummyCmdObjBuilder(runner),
|
||||
}
|
||||
existingCommit := models.NewCommit(hashPool, models.NewCommitOpts{
|
||||
Hash: "0123456789012345678901234567890123456789",
|
||||
Name: "hydrated subject",
|
||||
AuthorName: "Jane Doe",
|
||||
AuthorEmail: "jane@example.com",
|
||||
UnixTimestamp: 1234,
|
||||
Parents: []string{"1123456789012345678901234567890123456789"},
|
||||
Status: models.StatusRebasing,
|
||||
Action: todo.Pick,
|
||||
})
|
||||
refreshedTodo := models.NewCommit(hashPool, models.NewCommitOpts{
|
||||
Hash: existingCommit.Hash(),
|
||||
Name: "subject from the todo file",
|
||||
Status: models.StatusConflicted,
|
||||
Action: todo.Fixup,
|
||||
ActionFlag: "-C",
|
||||
})
|
||||
|
||||
commits, err := loader.getHydratedTodoCommits(
|
||||
hashPool,
|
||||
[]*models.Commit{refreshedTodo},
|
||||
[]*models.Commit{existingCommit},
|
||||
false,
|
||||
)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []*models.Commit{
|
||||
models.NewCommit(hashPool, models.NewCommitOpts{
|
||||
Hash: existingCommit.Hash(),
|
||||
Name: "hydrated subject",
|
||||
AuthorName: "Jane Doe",
|
||||
AuthorEmail: "jane@example.com",
|
||||
UnixTimestamp: 1234,
|
||||
Parents: []string{"1123456789012345678901234567890123456789"},
|
||||
Status: models.StatusConflicted,
|
||||
Action: todo.Fixup,
|
||||
ActionFlag: "-C",
|
||||
}),
|
||||
}, commits)
|
||||
assert.Equal(t, todo.Pick, existingCommit.Action)
|
||||
assert.Equal(t, models.StatusRebasing, existingCommit.Status)
|
||||
runner.CheckForMissingCalls()
|
||||
}
|
||||
|
||||
func TestCommitLoaderGetHydratedTodoCommitsLoadsMissingCommit(t *testing.T) {
|
||||
hashPool := &utils.StringPool{}
|
||||
existingHash := "0123456789012345678901234567890123456789"
|
||||
missingHash := "2123456789012345678901234567890123456789"
|
||||
missingCommitOutput := strings.ReplaceAll(
|
||||
`+2123456789012345678901234567890123456789|1235|John Doe|john@example.com||>|tag: new|new subject`,
|
||||
"|",
|
||||
"\x00",
|
||||
)
|
||||
runner := oscommands.NewFakeRunner(t).ExpectGitArgs(
|
||||
[]string{
|
||||
"-c", "log.showSignature=false", "show", "--no-patch", "--oneline", "--abbrev=20",
|
||||
prettyFormat, missingHash,
|
||||
},
|
||||
missingCommitOutput,
|
||||
nil,
|
||||
)
|
||||
loader := &CommitLoader{
|
||||
cmd: oscommands.NewDummyCmdObjBuilder(runner),
|
||||
}
|
||||
existingCommit := models.NewCommit(hashPool, models.NewCommitOpts{
|
||||
Hash: existingHash,
|
||||
Name: "existing subject",
|
||||
Status: models.StatusRebasing,
|
||||
Action: todo.Pick,
|
||||
})
|
||||
refreshedTodos := []*models.Commit{
|
||||
models.NewCommit(hashPool, models.NewCommitOpts{
|
||||
Hash: existingHash,
|
||||
Status: models.StatusRebasing,
|
||||
Action: todo.Pick,
|
||||
}),
|
||||
models.NewCommit(hashPool, models.NewCommitOpts{
|
||||
Hash: missingHash,
|
||||
Status: models.StatusRebasing,
|
||||
Action: todo.Edit,
|
||||
}),
|
||||
}
|
||||
|
||||
commits, err := loader.getHydratedTodoCommits(
|
||||
hashPool,
|
||||
refreshedTodos,
|
||||
[]*models.Commit{existingCommit},
|
||||
false,
|
||||
)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, commits, 2)
|
||||
assert.Equal(t, "existing subject", commits[0].Name)
|
||||
assert.Equal(t, "new subject", commits[1].Name)
|
||||
assert.Equal(t, todo.Edit, commits[1].Action)
|
||||
runner.CheckForMissingCalls()
|
||||
}
|
||||
|
||||
func TestCommitLoader_setCommitStatuses(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
|
||||
@@ -312,7 +312,7 @@ func TestCommitShowCmdObj(t *testing.T) {
|
||||
similarityThreshold: 50,
|
||||
ignoreWhitespace: false,
|
||||
diffRendererConfig: &config.DiffRendererConfig{Type: "extDiff", Command: "difft --color=always"},
|
||||
expected: []string{"-C", "/path/to/worktree", "-c", "diff.external=difft --color=always", "-c", "diff.noprefix=false", "show", "--ext-diff", "--unified=3", "--find-renames=50%", "--submodule", "--color=always", "--stat", "--decorate", "-p", "1234567890", "--"},
|
||||
expected: []string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "show", "--ext-diff", "--unified=3", "--find-renames=50%", "--submodule", "--color=always", "--stat", "--decorate", "-p", "1234567890", "--"},
|
||||
},
|
||||
{
|
||||
testName: "Show diff using git's external diff config",
|
||||
@@ -341,7 +341,7 @@ func TestCommitShowCmdObj(t *testing.T) {
|
||||
}
|
||||
instance := buildCommitCommands(commonDeps{userConfig: userConfig, appState: &config.AppState{}, runner: runner, repoPaths: &repoPaths})
|
||||
|
||||
assert.NoError(t, instance.ShowCmdObj("1234567890", s.filterPaths).Run())
|
||||
assert.NoError(t, instance.ShowCmdObj("1234567890", s.filterPaths, DiffModeRendered).Run())
|
||||
runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,10 +2,135 @@ package git_commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/mgutz/str"
|
||||
)
|
||||
|
||||
// metadataHandshake is the record a diff renderer that speaks the OSC 1717 protocol
|
||||
// emits before anything else, to announce that it does: a version-only record, with
|
||||
// none of the fields a line's record has. See ProbeDiffRendererEmitsMetadata, and
|
||||
// gocui's escape interpreter for how it is kept off the screen on a real render.
|
||||
const metadataHandshake = "\x1b]1717"
|
||||
|
||||
// probeWidth is the width a probe asks the renderer to lay out to. A probe has
|
||||
// nothing to render, so any width does, as long as it is one a renderer will
|
||||
// accept: a renderer that lays its rendering out in columns may well refuse a
|
||||
// width of zero, and would then never get as far as announcing itself.
|
||||
const probeWidth = 80
|
||||
|
||||
// ProbeDiffRendererEmitsMetadata reports whether the configured diff renderer states
|
||||
// which line of which file it is rendering, by running it on empty input and looking
|
||||
// for the handshake. The answer decides whether a diff the renderer produced can be
|
||||
// acted on at all, or has to be replaced by git's own when the user wants to act on it
|
||||
// (see DiffLineHelper.MainViewDiffMode).
|
||||
//
|
||||
// Asking rather than watching a real render: the handshake is the renderer's first
|
||||
// output whatever the diff, so the answer is a property of the renderer, known before
|
||||
// we render anything — where watching would have to see a diff go by first, and would
|
||||
// be fooled by a diff with no lines to describe.
|
||||
//
|
||||
// No terminal is needed. git only invokes a stdin filter when it thinks it is talking
|
||||
// to one, but the renderer itself doesn't care: it announces itself whenever OSC1717 is
|
||||
// set, so it can be run directly with empty input.
|
||||
func (self *DiffCommands) ProbeDiffRendererEmitsMetadata() bool {
|
||||
manager := self.diffRendererConfigManager
|
||||
values := config.DiffRendererValues{Width: probeWidth, DiffContext: 3}
|
||||
|
||||
switch manager.GetDiffRendererType() {
|
||||
case config.DiffRendererType_StdinFilter:
|
||||
if command, err := manager.GetStdinFilterCommand(values); err == nil && command != "" {
|
||||
return self.probeEmitsMetadata(self.cmd.NewShell(command, ""))
|
||||
}
|
||||
case config.DiffRendererType_ExtDiff:
|
||||
// An empty command means git's own diff.external config, which picks a driver
|
||||
// per file through .gitattributes: there is no one renderer to ask, and a single
|
||||
// diff can be produced by several, so we take it that it says nothing.
|
||||
if command, err := manager.GetExternalDiffCommand(values); err == nil && command != "" {
|
||||
return self.externalDiffEmitsMetadata(command)
|
||||
}
|
||||
case config.DiffRendererType_RawGit:
|
||||
// git describes only the formats whose output can't be read back as a diff, and
|
||||
// asked with the renderer's own arguments it answers for exactly the format
|
||||
// those select: a handshake for a word diff, silence for a unified one. With no
|
||||
// arguments there is nothing to fall back to anyway, since this already is git's
|
||||
// own diff.
|
||||
if args := manager.GetRawGitArgs(); len(args) > 0 {
|
||||
return self.rawGitEmitsMetadata(args)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// rawGitEmitsMetadata asks git itself, run with the diff renderer's own arguments.
|
||||
func (self *DiffCommands) rawGitEmitsMetadata(rawGitArgs []string) bool {
|
||||
oldPath, newPath, cleanup, ok := self.probeFiles()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
return self.probeEmitsMetadata(self.cmd.New(
|
||||
NewGitCmd("diff").
|
||||
Arg("--no-index").
|
||||
Arg(rawGitArgs...).
|
||||
Arg(oldPath, newPath).
|
||||
ToArgv(),
|
||||
))
|
||||
}
|
||||
|
||||
// externalDiffEmitsMetadata asks an external diff command, invoking it the way git
|
||||
// invokes one — with the seven positional arguments of git's diff.external convention —
|
||||
// over two empty files, so that it announces itself without having a diff to render.
|
||||
func (self *DiffCommands) externalDiffEmitsMetadata(externalDiffCommand string) bool {
|
||||
oldPath, newPath, cleanup, ok := self.probeFiles()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
args := append(str.ToArgv(externalDiffCommand),
|
||||
"probe", oldPath, "0000000", "100644", newPath, "0000000", "100644")
|
||||
return self.probeEmitsMetadata(self.cmd.New(args))
|
||||
}
|
||||
|
||||
// probeFiles makes the two empty files a probe stands a diff up from, and the cleanup
|
||||
// that removes them. Empty, because what the probe wants is for the renderer to announce
|
||||
// itself, not for it to have anything to say.
|
||||
func (self *DiffCommands) probeFiles() (string, string, func(), bool) {
|
||||
tempDir := self.os.GetTempDir()
|
||||
|
||||
oldFile, err := os.CreateTemp(tempDir, "lazygit-probe-old-*")
|
||||
if err != nil {
|
||||
return "", "", nil, false
|
||||
}
|
||||
oldFile.Close()
|
||||
|
||||
newFile, err := os.CreateTemp(tempDir, "lazygit-probe-new-*")
|
||||
if err != nil {
|
||||
os.Remove(oldFile.Name())
|
||||
return "", "", nil, false
|
||||
}
|
||||
newFile.Close()
|
||||
|
||||
return oldFile.Name(), newFile.Name(), func() {
|
||||
os.Remove(oldFile.Name())
|
||||
os.Remove(newFile.Name())
|
||||
}, true
|
||||
}
|
||||
|
||||
func (self *DiffCommands) probeEmitsMetadata(cmdObj *oscommands.CmdObj) bool {
|
||||
cmdObj.AddEnvVars("OSC1717=V1")
|
||||
// A renderer may well object to being handed nothing to render. We want to know
|
||||
// whatever it said before objecting, and that is captured either way.
|
||||
output, _ := cmdObj.RunWithOutput()
|
||||
return strings.Contains(output, metadataHandshake)
|
||||
}
|
||||
|
||||
type DiffCommands struct {
|
||||
*GitCommon
|
||||
}
|
||||
@@ -18,19 +143,41 @@ func NewDiffCommands(gitCommon *GitCommon) *DiffCommands {
|
||||
|
||||
// This is for generating diffs to be shown in the UI (e.g. rendering a range
|
||||
// diff to the main view). It uses a custom diff renderer if one is configured.
|
||||
func (self *DiffCommands) DiffCmdObj(diffArgs []string) *oscommands.CmdObj {
|
||||
func (self *DiffCommands) DiffCmdObj(diffArgs []string, mode DiffMode) *oscommands.CmdObj {
|
||||
return self.cmd.New(
|
||||
NewGitCmd("diff").
|
||||
Config("diff.noprefix=false").
|
||||
AddCommonDiffArgs(self.diffRendererConfigManager, self.UserConfig(), true).
|
||||
AddCommonDiffArgs(self.diffRendererConfigManager, self.UserConfig(), mode).
|
||||
Arg("--submodule").
|
||||
Arg(fmt.Sprintf("--color=%s", self.diffRendererConfigManager.GetColorArg())).
|
||||
Arg(fmt.Sprintf("--color=%s", mode.colorArg(self.diffRendererConfigManager))).
|
||||
Arg(diffArgs...).
|
||||
Dir(self.repoPaths.worktreePath).
|
||||
ToArgv(),
|
||||
)
|
||||
}
|
||||
|
||||
// CustomPatchDiffCmdObj is the command that renders the custom patch being built: a diff
|
||||
// of the two trees the patch was materialized into (PatchCommands.WriteCustomPatchDiffTrees),
|
||||
// under the directory holding them. It goes through the same wiring as any other diff we
|
||||
// show, so the patch is rendered by whatever renders the rest of them, and git works out
|
||||
// how much context to give it.
|
||||
//
|
||||
// git's own path prefixes are suppressed because the trees are named a and b themselves,
|
||||
// which leaves the paths reading like an ordinary diff's over the repo's own paths.
|
||||
func (self *DiffCommands) CustomPatchDiffCmdObj(dir string, mode DiffMode) *oscommands.CmdObj {
|
||||
return self.cmd.New(
|
||||
NewGitCmd("diff").
|
||||
AddCommonDiffArgs(self.diffRendererConfigManager, self.UserConfig(), mode).
|
||||
NoLineEndingConversion().
|
||||
Arg("--no-index").
|
||||
Arg("--no-prefix").
|
||||
Arg(fmt.Sprintf("--color=%s", mode.colorArg(self.diffRendererConfigManager))).
|
||||
Arg("a", "b").
|
||||
Dir(dir).
|
||||
ToArgv(),
|
||||
)
|
||||
}
|
||||
|
||||
// This is a basic generic diff command that can be used for any diff operation
|
||||
// (e.g. copying a diff to the clipboard). It will not use a custom diff renderer,
|
||||
// and does not use user configs such as ignore whitespace.
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
)
|
||||
|
||||
// DiffMode says what a diff command's output is for. This decides whether the
|
||||
// configured diff renderer produces it, and whether it is coloured.
|
||||
type DiffMode int
|
||||
|
||||
const (
|
||||
// DiffModeRendered is the diff as the user has arranged for it to look: through the
|
||||
// diff renderer, with the renderer's own arguments and its preference about colour.
|
||||
DiffModeRendered DiffMode = iota
|
||||
// DiffModeRaw is git's own coloured diff, for showing a diff whose rendered form
|
||||
// couldn't be acted on.
|
||||
DiffModeRaw
|
||||
// DiffModePlain is git's own uncoloured diff, for building patches from and copying
|
||||
// text out of rather than for looking at.
|
||||
DiffModePlain
|
||||
)
|
||||
|
||||
// colorArg returns the value to pass to git's --color for this mode. Rendered output is
|
||||
// coloured however the renderer wants its input; a raw diff gets git's own colour, which
|
||||
// is the point of it; a plain one is for reading as text, not for looking at.
|
||||
func (self DiffMode) colorArg(diffRendererConfigManager *config.DiffRendererConfigManager) string {
|
||||
switch self {
|
||||
case DiffModeRendered:
|
||||
return diffRendererConfigManager.GetColorArg()
|
||||
case DiffModeRaw:
|
||||
return "always"
|
||||
default:
|
||||
return "never"
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package git_commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -91,26 +90,6 @@ func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File
|
||||
|
||||
self.setConflictMarkerSizes(files)
|
||||
|
||||
// Go through the files to see if any of these files are actually worktrees
|
||||
// so that we can render them correctly
|
||||
worktreePaths := linkedWortkreePaths(self.Fs, self.repoPaths.RepoGitDirPath())
|
||||
for _, file := range files {
|
||||
for _, worktreePath := range worktreePaths {
|
||||
absFilePath, err := filepath.Abs(file.Path)
|
||||
if err != nil {
|
||||
self.Log.Error(err)
|
||||
continue
|
||||
}
|
||||
if absFilePath == worktreePath {
|
||||
file.IsWorktree = true
|
||||
// `git status` renders this worktree as a folder with a trailing slash but we'll represent it as a singular worktree
|
||||
// If we include the slash, it will be rendered as a folder with a null file inside.
|
||||
file.Path = strings.TrimSuffix(file.Path, "/")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
|
||||
@@ -19,12 +19,12 @@ import (
|
||||
// that opts back in is the foreground files refresh; see FileLoader.gitStatus.
|
||||
const OptionalLocksEnvVar = "GIT_OPTIONAL_LOCKS"
|
||||
|
||||
// forOtherRepo prepares a command that operates on a repo other than the one
|
||||
// ForOtherRepo prepares a command that operates on a repo other than the one
|
||||
// we have open — a submodule, or another worktree. GIT_DIR and GIT_WORK_TREE
|
||||
// say where our repo is, and every command we run inherits them, so a command
|
||||
// pointed at a different repo would be resolved against ours instead: `git -C
|
||||
// <submodule> log` would silently log the superproject's commits.
|
||||
func forOtherRepo(cmdObj *oscommands.CmdObj) *oscommands.CmdObj {
|
||||
func ForOtherRepo(cmdObj *oscommands.CmdObj) *oscommands.CmdObj {
|
||||
return cmdObj.RemoveEnvVar(env.GitDirEnvVar).RemoveEnvVar(env.GitWorkTreeEnvVar)
|
||||
}
|
||||
|
||||
@@ -123,18 +123,42 @@ func (self *GitCommandBuilder) GitDirIf(condition bool, path string) *GitCommand
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *GitCommandBuilder) AddCommonDiffArgs(diffRendererConfigManager *config.DiffRendererConfigManager, userConfig *config.UserConfig, forUI bool) *GitCommandBuilder {
|
||||
// NoLineEndingConversion keeps git's line-ending machinery away from files that are not
|
||||
// a working tree's. The trees the custom patch is materialized into hold the bytes git
|
||||
// states the patch in, so a command over them has to read and write those bytes as they
|
||||
// are. On a machine that checks files out with CRLF, `git apply` writes the after tree
|
||||
// in that form while the before tree keeps the LF it was written with. git's own diff
|
||||
// converts both back, but it warns about a round trip through a working tree these
|
||||
// files never belong to, and an external diff renderer is handed the two files as they
|
||||
// stand, one line ending apart in every line.
|
||||
func (self *GitCommandBuilder) NoLineEndingConversion() *GitCommandBuilder {
|
||||
return self.
|
||||
// The setting that converts on most machines, and the one Git for Windows
|
||||
// installs itself with.
|
||||
Config("core.autocrlf=false").
|
||||
// An attributes file outside the repo can still mark the files as text. The
|
||||
// form to keep them in is then the form they are written in.
|
||||
Config("core.eol=lf").
|
||||
// An attribute naming CRLF outright overrides that, and git converts after all.
|
||||
// The warning it gives is about a checkout these files never have.
|
||||
Config("core.safecrlf=false")
|
||||
}
|
||||
|
||||
func (self *GitCommandBuilder) AddCommonDiffArgs(diffRendererConfigManager *config.DiffRendererConfigManager, userConfig *config.UserConfig, mode DiffMode) *GitCommandBuilder {
|
||||
contextSize := userConfig.Git.DiffContextSize
|
||||
extDiffCmd := diffRendererConfigManager.GetExternalDiffCommand(contextSize)
|
||||
useExtDiff := forUI && diffRendererConfigManager.GetDiffRendererType() == config.DiffRendererType_ExtDiff
|
||||
useExtDiff := mode == DiffModeRendered && diffRendererConfigManager.GetDiffRendererType() == config.DiffRendererType_ExtDiff
|
||||
|
||||
return self.
|
||||
ConfigIf(forUI && extDiffCmd != "", "diff.external="+extDiffCmd).
|
||||
ArgIfElse(useExtDiff, "--ext-diff", "--no-ext-diff").
|
||||
Arg(fmt.Sprintf("--unified=%d", contextSize)).
|
||||
ArgIf(forUI && userConfig.Git.IgnoreWhitespaceInDiffView, "--ignore-all-space").
|
||||
// Ignoring whitespace is about what the user wants to see, so it holds for a raw
|
||||
// diff as much as for a rendered one. Patches are built from a plain diff,
|
||||
// where a diff that leaves changes out would apply to nothing.
|
||||
ArgIf(mode != DiffModePlain && userConfig.Git.IgnoreWhitespaceInDiffView, "--ignore-all-space").
|
||||
Arg(fmt.Sprintf("--find-renames=%d%%", userConfig.Git.RenameSimilarityThreshold)).
|
||||
ArgIf(forUI, diffRendererConfigManager.GetRawGitArgs()...)
|
||||
// The renderer's own arguments to git — a word diff, say — are part of the
|
||||
// rendering, so they go with it.
|
||||
ArgIf(mode == DiffModeRendered, diffRendererConfigManager.GetRawGitArgs()...)
|
||||
}
|
||||
|
||||
func (self *GitCommandBuilder) ToArgv() []string {
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -160,9 +162,51 @@ func fetchPullRequestsQuery(branches []string, owner string, repo string) (strin
|
||||
return queryString, variables
|
||||
}
|
||||
|
||||
// GetAuthToken returns the token to authenticate against the given host with,
|
||||
// or an empty string if there is none.
|
||||
//
|
||||
// The token has to come from gh itself rather than from an in-process lookup
|
||||
// with go-gh: that reads gh's config file once per process and answers from
|
||||
// that snapshot ever after, whereas gh rewrites the file whenever the active
|
||||
// account changes, and keeps the active account's token either there or in the
|
||||
// system keyring. Under a long-running lazygit the snapshot therefore drifts
|
||||
// out of date, leaving us with a token for an account that is no longer active,
|
||||
// or with no token at all.
|
||||
func (self *GitHubCommands) GetAuthToken(host string) string {
|
||||
token, _ := auth.TokenForHost(host)
|
||||
return token
|
||||
ghExe := ghExecutable()
|
||||
if ghExe == "" {
|
||||
// Without gh installed, the environment variables and config file that
|
||||
// gh would have consulted are still worth a look.
|
||||
token, _ := auth.TokenFromEnvOrConfig(host)
|
||||
return token
|
||||
}
|
||||
|
||||
cmdArgs := []string{ghExe, "auth", "token", "--hostname", host}
|
||||
output, _, err := self.cmd.New(cmdArgs).DontLog().RunWithOutputs()
|
||||
if err != nil {
|
||||
// Not being logged in to this host is a normal state rather than
|
||||
// something to report; the runner logs gh's stderr for the rest.
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.TrimSpace(output)
|
||||
}
|
||||
|
||||
// ghExecutable returns the path of the gh binary, or an empty string if it
|
||||
// isn't installed.
|
||||
func ghExecutable() string {
|
||||
if ghExe := os.Getenv("GH_PATH"); ghExe != "" {
|
||||
return ghExe
|
||||
}
|
||||
|
||||
// A gh found in the current directory rather than on PATH comes back as
|
||||
// exec.ErrDot, which we treat as not having found one at all.
|
||||
ghExe, err := exec.LookPath("gh")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return ghExe
|
||||
}
|
||||
|
||||
// FetchRecentPRs fetches recent pull requests using GraphQL. serviceInfo
|
||||
|
||||
@@ -2,13 +2,16 @@ package git_commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/app/daemon"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/samber/lo"
|
||||
"github.com/stefanhaller/git-todo-parser/todo"
|
||||
)
|
||||
|
||||
@@ -20,6 +23,10 @@ type PatchCommands struct {
|
||||
stash *StashCommands
|
||||
|
||||
PatchBuilder *patch.PatchBuilder
|
||||
|
||||
// The version of the patch the diff trees were last written for, so that they are
|
||||
// written again when, and only when, the patch has changed since.
|
||||
treesWrittenForGeneration int
|
||||
}
|
||||
|
||||
func NewPatchCommands(
|
||||
@@ -40,6 +47,88 @@ func NewPatchCommands(
|
||||
}
|
||||
}
|
||||
|
||||
// EnsureCustomPatchDiffTrees writes the custom patch's diff trees if what is there no
|
||||
// longer describes the patch. Call it before rendering the patch, which is often — every
|
||||
// time the panel showing it re-renders — while the patch itself changes rarely.
|
||||
func (self *PatchCommands) EnsureCustomPatchDiffTrees() error {
|
||||
if self.PatchBuilder.Generation() == self.treesWrittenForGeneration {
|
||||
return nil
|
||||
}
|
||||
if err := self.WriteCustomPatchDiffTrees(); err != nil {
|
||||
return err
|
||||
}
|
||||
self.treesWrittenForGeneration = self.PatchBuilder.Generation()
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteCustomPatchDiffTrees materializes the custom patch as two file trees under the
|
||||
// directory the patch builder keeps for it: `a` holds each of the patch's files as it is
|
||||
// before the patch, `b` as it is after. Diffing those two trees against each other
|
||||
// (DiffCommands.CustomPatchDiffCmdObj) turns the patch into a diff of real files, which
|
||||
// can then be rendered exactly as any other diff is — through a diff renderer of any
|
||||
// kind, and with git's own idea of how much context to show.
|
||||
//
|
||||
// The trees are named a and b so that the diff's paths, with git's own prefixes
|
||||
// suppressed, come out reading like the a/ and b/ of an ordinary diff, over the real
|
||||
// repo-relative paths.
|
||||
func (self *PatchCommands) WriteCustomPatchDiffTrees() error {
|
||||
dir := self.PatchBuilder.TempDir()
|
||||
if dir == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
before := filepath.Join(dir, "a")
|
||||
after := filepath.Join(dir, "b")
|
||||
for _, tree := range []string{before, after} {
|
||||
if err := os.RemoveAll(tree); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(tree, 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, file := range self.PatchBuilder.FilesInPatch() {
|
||||
content, err := self.commit.ShowFileContentCmdObj(self.PatchBuilder.From, file.ContentPath).RunWithOutput()
|
||||
// A file the patch adds has no content on the before side, so git has nothing to
|
||||
// show for it.
|
||||
added := err != nil
|
||||
|
||||
// The before side holds an added file as an empty file rather than not at all, so
|
||||
// that the diff pairs the two sides up and states the file's real path, instead of
|
||||
// reporting a file that only one of the trees has.
|
||||
if err := self.os.CreateFileWithContent(filepath.Join(before, file.Path),
|
||||
lo.Ternary(added, "", content)); err != nil {
|
||||
return err
|
||||
}
|
||||
// The after side is seeded with the same content, for the patch to change; a file
|
||||
// the patch adds is left absent, for the patch to create.
|
||||
if !added {
|
||||
if err := self.os.CreateFileWithContent(filepath.Join(after, file.Path), content); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write added files as creations rather than as diffs against an empty file: the
|
||||
// patch is applied in one go, so a file it expects to be there already would make
|
||||
// the whole of it fail.
|
||||
patchText := self.PatchBuilder.PatchToApply(false, false)
|
||||
if strings.TrimSpace(patchText) == "" {
|
||||
// Nothing in the patch, so the two trees are alike and the diff is empty.
|
||||
return nil
|
||||
}
|
||||
patchFilePath, err := self.SaveTemporaryPatch(patchText)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return self.cmd.New(NewGitCmd("apply").
|
||||
NoLineEndingConversion().
|
||||
Arg(patchFilePath).
|
||||
Dir(after).
|
||||
ToArgv()).Run()
|
||||
}
|
||||
|
||||
type ApplyPatchOpts struct {
|
||||
ThreeWay bool
|
||||
Cached bool
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/jesseduffield/generics/set"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// The tip commit of a ref, as far as sorting refs by ancestry needs it. The
|
||||
// date is the raw %(committerdate:unix) field, and is only ever compared for
|
||||
// equality.
|
||||
type refTip struct {
|
||||
hash string
|
||||
committerDate string
|
||||
}
|
||||
|
||||
// Determining ancestry costs one %(ahead-behind:<tip>) field per tip, and git
|
||||
// evaluates each of those for every tip we ask about, so the work grows with
|
||||
// the square of the number of tips. Branches whose tips came out of one rebase
|
||||
// are nowhere near this many, so leave the refs in the order git returned them
|
||||
// once the tips add up to more than this.
|
||||
const maxTipsForAncestrySorting = 100
|
||||
|
||||
// Sorts each group of refs that share a committer date so that a ref comes
|
||||
// before the refs it is descended from. Refs that are not descended from one
|
||||
// another keep the order they came in.
|
||||
//
|
||||
// git sorts refs with equal committer dates by name, and a stack of branches
|
||||
// that gets rebased in one go ends up with the same committer date on all of
|
||||
// its tips. Without this, such a stack appears in alphabetical order.
|
||||
//
|
||||
// refs must be sorted by committer date already, and tips must have an entry
|
||||
// for each of them.
|
||||
func sortRefsWithEqualDatesByAncestry[T any](
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
version *GitVersion,
|
||||
refs []T,
|
||||
fullRefName func(T) string,
|
||||
tips map[string]refTip,
|
||||
) error {
|
||||
// %(ahead-behind:...) was added in git 2.41
|
||||
if !version.IsAtLeast(2, 41, 0) {
|
||||
return nil
|
||||
}
|
||||
|
||||
groups := lo.Filter(groupRefsWithEqualDates(refs, fullRefName, tips),
|
||||
func(group []T, _ int) bool {
|
||||
return pointAtMoreThanOneCommit(group, fullRefName, tips)
|
||||
})
|
||||
if len(groups) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ancestry is a property of the tip commits, so ask git about each of them
|
||||
// once, however many refs point at it. A repository with several remotes
|
||||
// has the same branches under each of them, and they all share a date.
|
||||
tipHashes := []string{}
|
||||
refNames := []string{}
|
||||
seenTips := set.New[string]()
|
||||
for _, ref := range lo.Flatten(groups) {
|
||||
refName := fullRefName(ref)
|
||||
if hash := tips[refName].hash; !seenTips.Includes(hash) {
|
||||
seenTips.Add(hash)
|
||||
tipHashes = append(tipHashes, hash)
|
||||
refNames = append(refNames, refName)
|
||||
}
|
||||
}
|
||||
if len(tipHashes) > maxTipsForAncestrySorting {
|
||||
return nil
|
||||
}
|
||||
|
||||
containedTips, err := loadContainedTips(cmd, tipHashes, refNames)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, group := range groups {
|
||||
sortGroupByAncestry(group, fullRefName, tips, containedTips)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Refs that all point at the same commit have no order to be put in, so
|
||||
// there is nothing to ask git about them
|
||||
func pointAtMoreThanOneCommit[T any](
|
||||
refs []T,
|
||||
fullRefName func(T) string,
|
||||
tips map[string]refTip,
|
||||
) bool {
|
||||
firstHash := tips[fullRefName(refs[0])].hash
|
||||
return lo.SomeBy(refs, func(ref T) bool {
|
||||
return tips[fullRefName(ref)].hash != firstHash
|
||||
})
|
||||
}
|
||||
|
||||
// Returns the runs of consecutive refs that share a committer date, for runs of
|
||||
// more than one ref. The returned slices share their backing array with refs,
|
||||
// so sorting a run sorts that part of refs.
|
||||
func groupRefsWithEqualDates[T any](
|
||||
refs []T,
|
||||
fullRefName func(T) string,
|
||||
tips map[string]refTip,
|
||||
) [][]T {
|
||||
committerDate := func(ref T) string {
|
||||
return tips[fullRefName(ref)].committerDate
|
||||
}
|
||||
|
||||
groups := [][]T{}
|
||||
start := 0
|
||||
for i := 1; i <= len(refs); i++ {
|
||||
if i < len(refs) && committerDate(refs[i]) == committerDate(refs[start]) {
|
||||
continue
|
||||
}
|
||||
if i-start > 1 {
|
||||
groups = append(groups, refs[start:i])
|
||||
}
|
||||
start = i
|
||||
}
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
// For each of the given tips, which of the tips its history contains. Keyed by
|
||||
// tip hash, with an entry for every tip passed in. refNames names, for each
|
||||
// tip, a ref that points at it; git reports the values by ref name.
|
||||
func loadContainedTips(
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
tipHashes []string,
|
||||
refNames []string,
|
||||
) (map[string]*set.Set[string], error) {
|
||||
output, err := cmd.New(
|
||||
buildAheadBehindForEachRefArgs(tipHashes, refNames),
|
||||
).DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
containedTips := make(map[string]*set.Set[string], len(tipHashes))
|
||||
for _, hash := range tipHashes {
|
||||
containedTips[hash] = set.New[string]()
|
||||
}
|
||||
tipByRefName := make(map[string]string, len(refNames))
|
||||
for i, refName := range refNames {
|
||||
tipByRefName[refName] = tipHashes[i]
|
||||
}
|
||||
|
||||
for _, entry := range parseAheadBehindForEachRefOutput(output, len(tipHashes)) {
|
||||
hash, ok := tipByRefName[entry.refName]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
contained := containedTips[hash]
|
||||
for i, ab := range entry.aheadBehinds {
|
||||
// The tip's history contains the other tip if it has commits the
|
||||
// other one doesn't have, and the other one has none it doesn't
|
||||
// have.
|
||||
if ab.valid && ab.ahead > 0 && ab.behind == 0 {
|
||||
contained.Add(tipHashes[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return containedTips, nil
|
||||
}
|
||||
|
||||
func sortGroupByAncestry[T any](
|
||||
group []T,
|
||||
fullRefName func(T) string,
|
||||
tips map[string]refTip,
|
||||
containedTips map[string]*set.Set[string],
|
||||
) {
|
||||
hashes := lo.Map(group, func(ref T, _ int) string {
|
||||
return tips[fullRefName(ref)].hash
|
||||
})
|
||||
contained := lo.Map(hashes, func(hash string, _ int) *set.Set[string] {
|
||||
if contained, ok := containedTips[hash]; ok {
|
||||
return contained
|
||||
}
|
||||
return set.New[string]()
|
||||
})
|
||||
isDescendedFrom := func(descendant int, ancestor int) bool {
|
||||
return contained[descendant].Includes(hashes[ancestor])
|
||||
}
|
||||
|
||||
// Indices into group, holding the refs we haven't placed yet
|
||||
remaining := lo.Range(len(group))
|
||||
// A ref can only go in once every ref that is descended from it is in
|
||||
canBePlaced := func(i int) bool {
|
||||
return !lo.SomeBy(remaining, func(j int) bool {
|
||||
return isDescendedFrom(j, i)
|
||||
})
|
||||
}
|
||||
|
||||
sorted := make([]T, 0, len(group))
|
||||
lastPlaced := -1
|
||||
for len(remaining) > 0 {
|
||||
next := -1
|
||||
if lastPlaced != -1 {
|
||||
// Walk from the ref we placed last down to the refs it is based on,
|
||||
// so that the branches of a stack come out as one run even when an
|
||||
// unrelated branch sorts into the middle of them by name
|
||||
next = slices.IndexFunc(remaining, func(i int) bool {
|
||||
return isDescendedFrom(lastPlaced, i) && canBePlaced(i)
|
||||
})
|
||||
}
|
||||
if next == -1 {
|
||||
next = slices.IndexFunc(remaining, canBePlaced)
|
||||
}
|
||||
if next == -1 {
|
||||
// Commits can't descend from each other in a circle; only take the
|
||||
// first ref so that we don't spin if the values ever say otherwise.
|
||||
next = 0
|
||||
}
|
||||
lastPlaced = remaining[next]
|
||||
sorted = append(sorted, group[lastPlaced])
|
||||
remaining = slices.Delete(remaining, next, next+1)
|
||||
}
|
||||
|
||||
copy(group, sorted)
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/samber/lo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// A branch as the tests below describe it: a name, the hash of its tip, and the
|
||||
// committer date of its tip
|
||||
type testRef struct {
|
||||
name string
|
||||
hash string
|
||||
date string
|
||||
}
|
||||
|
||||
func buildTestRefs(refs []testRef) ([]*models.Branch, map[string]refTip) {
|
||||
branches := lo.Map(refs, func(ref testRef, _ int) *models.Branch {
|
||||
return &models.Branch{Name: ref.name, CommitHash: ref.hash}
|
||||
})
|
||||
tips := make(map[string]refTip, len(refs))
|
||||
for _, ref := range refs {
|
||||
tips["refs/heads/"+ref.name] = refTip{hash: ref.hash, committerDate: ref.date}
|
||||
}
|
||||
return branches, tips
|
||||
}
|
||||
|
||||
func branchNames(branches []*models.Branch) []string {
|
||||
return lo.Map(branches, func(branch *models.Branch, _ int) string {
|
||||
return branch.Name
|
||||
})
|
||||
}
|
||||
|
||||
func TestGroupRefsWithEqualDates(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
testName string
|
||||
refs []testRef
|
||||
expected [][]string
|
||||
}{
|
||||
{
|
||||
testName: "no refs",
|
||||
refs: []testRef{},
|
||||
expected: [][]string{},
|
||||
},
|
||||
{
|
||||
testName: "all dates distinct",
|
||||
refs: []testRef{
|
||||
{name: "a", hash: "1", date: "300"},
|
||||
{name: "b", hash: "2", date: "200"},
|
||||
{name: "c", hash: "3", date: "100"},
|
||||
},
|
||||
expected: [][]string{},
|
||||
},
|
||||
{
|
||||
testName: "all dates equal",
|
||||
refs: []testRef{
|
||||
{name: "a", hash: "1", date: "100"},
|
||||
{name: "b", hash: "2", date: "100"},
|
||||
{name: "c", hash: "3", date: "100"},
|
||||
},
|
||||
expected: [][]string{{"a", "b", "c"}},
|
||||
},
|
||||
{
|
||||
testName: "two groups, with a lone ref between and after them",
|
||||
refs: []testRef{
|
||||
{name: "a", hash: "1", date: "300"},
|
||||
{name: "b", hash: "2", date: "300"},
|
||||
{name: "c", hash: "3", date: "200"},
|
||||
{name: "d", hash: "4", date: "100"},
|
||||
{name: "e", hash: "5", date: "100"},
|
||||
{name: "f", hash: "6", date: "50"},
|
||||
},
|
||||
expected: [][]string{{"a", "b"}, {"d", "e"}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
branches, tips := buildTestRefs(s.refs)
|
||||
groups := groupRefsWithEqualDates(branches, (*models.Branch).FullRefName, tips)
|
||||
assert.Equal(t, s.expected, lo.Map(groups,
|
||||
func(group []*models.Branch, _ int) []string {
|
||||
return branchNames(group)
|
||||
}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortRefsWithEqualDatesByAncestry(t *testing.T) {
|
||||
// A stack of three branches, all committed within the same second, as git
|
||||
// returns them: sorted by name. bottom is the base of middle, which is the
|
||||
// base of top.
|
||||
stack := []testRef{
|
||||
{name: "middle", hash: "m", date: "100"},
|
||||
{name: "top", hash: "t", date: "100"},
|
||||
{name: "bottom", hash: "b", date: "100"},
|
||||
}
|
||||
|
||||
// One %(ahead-behind:<hash>) field per tip, in the order the tips appear in
|
||||
// the branch list
|
||||
stackOutput := "refs/heads/middle\x000 0\x000 1\x001 0\n" +
|
||||
"refs/heads/top\x001 0\x000 0\x002 0\n" +
|
||||
"refs/heads/bottom\x000 1\x000 2\x000 0\n"
|
||||
|
||||
stackArgs := []string{
|
||||
"for-each-ref",
|
||||
"--format=%(refname)%00%(ahead-behind:m)%00%(ahead-behind:t)%00%(ahead-behind:b)",
|
||||
"refs/heads/middle", "refs/heads/top", "refs/heads/bottom",
|
||||
}
|
||||
|
||||
scenarios := []struct {
|
||||
testName string
|
||||
refs []testRef
|
||||
gitVersion *GitVersion
|
||||
expectedArgs []string
|
||||
output string
|
||||
outputErr error
|
||||
expectedOrder []string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
testName: "a stack of branches is sorted from the top down",
|
||||
refs: stack,
|
||||
gitVersion: &GitVersion{2, 41, 0, ""},
|
||||
expectedArgs: stackArgs,
|
||||
output: stackOutput,
|
||||
expectedOrder: []string{"top", "middle", "bottom"},
|
||||
},
|
||||
{
|
||||
testName: "git too old for %(ahead-behind:...), so nothing to do",
|
||||
refs: stack,
|
||||
gitVersion: &GitVersion{2, 40, 0, ""},
|
||||
expectedOrder: []string{"middle", "top", "bottom"},
|
||||
},
|
||||
{
|
||||
testName: "no two refs share a date, so nothing to do",
|
||||
refs: []testRef{
|
||||
{name: "middle", hash: "m", date: "300"},
|
||||
{name: "top", hash: "t", date: "200"},
|
||||
{name: "bottom", hash: "b", date: "100"},
|
||||
},
|
||||
gitVersion: &GitVersion{2, 41, 0, ""},
|
||||
expectedOrder: []string{"middle", "top", "bottom"},
|
||||
},
|
||||
{
|
||||
testName: "the command fails",
|
||||
refs: stack,
|
||||
gitVersion: &GitVersion{2, 41, 0, ""},
|
||||
expectedArgs: stackArgs,
|
||||
outputErr: errors.New("fatal: failed to find 'm'"),
|
||||
expectedOrder: []string{"middle", "top", "bottom"},
|
||||
expectedErr: "fatal: failed to find 'm'",
|
||||
},
|
||||
{
|
||||
testName: "a branch unrelated to the stack is placed by name",
|
||||
refs: []testRef{
|
||||
{name: "middle", hash: "m", date: "100"},
|
||||
{name: "other", hash: "o", date: "100"},
|
||||
{name: "top", hash: "t", date: "100"},
|
||||
{name: "bottom", hash: "b", date: "100"},
|
||||
},
|
||||
gitVersion: &GitVersion{2, 41, 0, ""},
|
||||
expectedArgs: []string{
|
||||
"for-each-ref",
|
||||
"--format=%(refname)%00%(ahead-behind:m)%00%(ahead-behind:o)%00%(ahead-behind:t)%00%(ahead-behind:b)",
|
||||
"refs/heads/middle", "refs/heads/other", "refs/heads/top", "refs/heads/bottom",
|
||||
},
|
||||
output: "refs/heads/middle\x000 0\x002 1\x000 1\x001 0\n" +
|
||||
"refs/heads/other\x001 2\x000 0\x001 3\x001 1\n" +
|
||||
"refs/heads/top\x001 0\x003 1\x000 0\x002 0\n" +
|
||||
"refs/heads/bottom\x000 1\x001 1\x000 2\x000 0\n",
|
||||
expectedOrder: []string{"other", "top", "middle", "bottom"},
|
||||
},
|
||||
{
|
||||
testName: "a stack stays together when a branch sorts into the middle of it",
|
||||
refs: []testRef{
|
||||
{name: "add-tests", hash: "t", date: "100"},
|
||||
{name: "cleanup", hash: "c", date: "100"},
|
||||
{name: "fix-parser", hash: "p", date: "100"},
|
||||
},
|
||||
gitVersion: &GitVersion{2, 41, 0, ""},
|
||||
expectedArgs: []string{
|
||||
"for-each-ref",
|
||||
"--format=%(refname)%00%(ahead-behind:t)%00%(ahead-behind:c)%00%(ahead-behind:p)",
|
||||
"refs/heads/add-tests", "refs/heads/cleanup", "refs/heads/fix-parser",
|
||||
},
|
||||
// add-tests is based on fix-parser, and cleanup is on a line of its
|
||||
// own, but its name sorts between the two
|
||||
output: "refs/heads/add-tests\x000 0\x002 1\x001 0\n" +
|
||||
"refs/heads/cleanup\x001 2\x000 0\x001 1\n" +
|
||||
"refs/heads/fix-parser\x000 1\x001 1\x000 0\n",
|
||||
expectedOrder: []string{"add-tests", "fix-parser", "cleanup"},
|
||||
},
|
||||
{
|
||||
testName: "each group is sorted on its own",
|
||||
refs: []testRef{
|
||||
{name: "middle", hash: "m", date: "200"},
|
||||
{name: "top", hash: "t", date: "200"},
|
||||
{name: "lone", hash: "l", date: "150"},
|
||||
{name: "base", hash: "a", date: "100"},
|
||||
{name: "derived", hash: "d", date: "100"},
|
||||
},
|
||||
gitVersion: &GitVersion{2, 41, 0, ""},
|
||||
expectedArgs: []string{
|
||||
"for-each-ref",
|
||||
"--format=%(refname)%00%(ahead-behind:m)%00%(ahead-behind:t)%00%(ahead-behind:a)%00%(ahead-behind:d)",
|
||||
"refs/heads/middle", "refs/heads/top", "refs/heads/base", "refs/heads/derived",
|
||||
},
|
||||
output: "refs/heads/middle\x000 0\x000 1\x002 0\x001 0\n" +
|
||||
"refs/heads/top\x001 0\x000 0\x003 0\x002 0\n" +
|
||||
"refs/heads/base\x000 2\x000 3\x000 0\x000 1\n" +
|
||||
"refs/heads/derived\x000 1\x000 2\x001 0\x000 0\n",
|
||||
expectedOrder: []string{"top", "middle", "lone", "derived", "base"},
|
||||
},
|
||||
{
|
||||
testName: "a group whose branches are all on one commit is left alone",
|
||||
refs: []testRef{
|
||||
{name: "a", hash: "x", date: "100"},
|
||||
{name: "b", hash: "x", date: "100"},
|
||||
},
|
||||
gitVersion: &GitVersion{2, 41, 0, ""},
|
||||
expectedOrder: []string{"a", "b"},
|
||||
},
|
||||
{
|
||||
testName: "branches on the same commit are asked about once",
|
||||
refs: []testRef{
|
||||
{name: "mirror-1", hash: "s", date: "100"},
|
||||
{name: "mirror-2", hash: "s", date: "100"},
|
||||
{name: "stack-bottom", hash: "u", date: "100"},
|
||||
{name: "stack-top", hash: "t", date: "100"},
|
||||
},
|
||||
gitVersion: &GitVersion{2, 41, 0, ""},
|
||||
expectedArgs: []string{
|
||||
"for-each-ref",
|
||||
"--format=%(refname)%00%(ahead-behind:s)%00%(ahead-behind:u)%00%(ahead-behind:t)",
|
||||
"refs/heads/mirror-1", "refs/heads/stack-bottom", "refs/heads/stack-top",
|
||||
},
|
||||
output: "refs/heads/mirror-1\x000 0\x001 1\x001 2\n" +
|
||||
"refs/heads/stack-bottom\x001 1\x000 0\x000 1\n" +
|
||||
"refs/heads/stack-top\x002 1\x001 0\x000 0\n",
|
||||
expectedOrder: []string{"mirror-1", "mirror-2", "stack-top", "stack-bottom"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
runner := oscommands.NewFakeRunner(t)
|
||||
if s.expectedArgs != nil {
|
||||
runner.ExpectGitArgs(s.expectedArgs, s.output, s.outputErr)
|
||||
}
|
||||
gitCommon := buildGitCommon(commonDeps{runner: runner, gitVersion: s.gitVersion})
|
||||
|
||||
branches, tips := buildTestRefs(s.refs)
|
||||
err := sortRefsWithEqualDatesByAncestry(gitCommon.cmd, gitCommon.version,
|
||||
branches, (*models.Branch).FullRefName, tips)
|
||||
|
||||
if s.expectedErr == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.ErrorContains(t, err, s.expectedErr)
|
||||
}
|
||||
assert.Equal(t, s.expectedOrder, branchNames(branches))
|
||||
runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortRefsWithEqualDatesByAncestry_TooManyTips(t *testing.T) {
|
||||
refs := lo.Map(lo.Range(maxTipsForAncestrySorting+1), func(i int, _ int) testRef {
|
||||
return testRef{
|
||||
name: fmt.Sprintf("branch-%03d", i),
|
||||
hash: fmt.Sprintf("hash-%03d", i),
|
||||
date: "100",
|
||||
}
|
||||
})
|
||||
|
||||
// The runner fails the test if the command runs at all
|
||||
runner := oscommands.NewFakeRunner(t)
|
||||
gitCommon := buildGitCommon(commonDeps{runner: runner, gitVersion: &GitVersion{2, 41, 0, ""}})
|
||||
|
||||
branches, tips := buildTestRefs(refs)
|
||||
err := sortRefsWithEqualDatesByAncestry(gitCommon.cmd, gitCommon.version,
|
||||
branches, (*models.Branch).FullRefName, tips)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, lo.Map(refs, func(ref testRef, _ int) string { return ref.name }),
|
||||
branchNames(branches))
|
||||
runner.CheckForMissingCalls()
|
||||
}
|
||||
|
||||
// A repository with many remotes has the same branches under each of them, so
|
||||
// a group can hold far more refs than the commits they point at
|
||||
func TestSortRefsWithEqualDatesByAncestry_ManyRefsOnTwoTips(t *testing.T) {
|
||||
refs := lo.Map(lo.Range(4*maxTipsForAncestrySorting), func(i int, _ int) testRef {
|
||||
return testRef{
|
||||
name: fmt.Sprintf("branch-%03d", i),
|
||||
hash: lo.Ternary(i%2 == 0, "bottom", "top"),
|
||||
date: "100",
|
||||
}
|
||||
})
|
||||
|
||||
runner := oscommands.NewFakeRunner(t).ExpectGitArgs([]string{
|
||||
"for-each-ref",
|
||||
"--format=%(refname)%00%(ahead-behind:bottom)%00%(ahead-behind:top)",
|
||||
"refs/heads/branch-000", "refs/heads/branch-001",
|
||||
},
|
||||
"refs/heads/branch-000\x000 0\x000 1\n"+
|
||||
"refs/heads/branch-001\x001 0\x000 0\n", nil)
|
||||
gitCommon := buildGitCommon(commonDeps{runner: runner, gitVersion: &GitVersion{2, 41, 0, ""}})
|
||||
|
||||
branches, tips := buildTestRefs(refs)
|
||||
err := sortRefsWithEqualDatesByAncestry(gitCommon.cmd, gitCommon.version,
|
||||
branches, (*models.Branch).FullRefName, tips)
|
||||
|
||||
assert.NoError(t, err)
|
||||
// the branches on the top commit first, each half still ordered by name
|
||||
expected := append(
|
||||
lo.FilterMap(refs, func(ref testRef, _ int) (string, bool) {
|
||||
return ref.name, ref.hash == "top"
|
||||
}),
|
||||
lo.FilterMap(refs, func(ref testRef, _ int) (string, bool) {
|
||||
return ref.name, ref.hash == "bottom"
|
||||
})...)
|
||||
assert.Equal(t, expected, branchNames(branches))
|
||||
runner.CheckForMissingCalls()
|
||||
}
|
||||
@@ -59,9 +59,10 @@ func (self *RemoteCommands) DeleteRemoteBranch(task gocui.Task, remoteName strin
|
||||
return self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).Run()
|
||||
}
|
||||
|
||||
func (self *RemoteCommands) DeleteRemoteTag(task gocui.Task, remoteName string, tagName string) error {
|
||||
func (self *RemoteCommands) DeleteRemoteTag(task gocui.Task, remoteName string, tagNames []string) error {
|
||||
cmdArgs := NewGitCmd("push").
|
||||
Arg(remoteName, "--delete", "refs/tags/"+tagName).
|
||||
Arg(remoteName, "--delete").
|
||||
Arg(lo.Map(tagNames, func(t string, _ int) string { return "refs/tags/" + t })...).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).Run()
|
||||
|
||||
@@ -5,53 +5,25 @@ import (
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type RemoteLoader struct {
|
||||
*common.Common
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
*GitCommon
|
||||
}
|
||||
|
||||
func NewRemoteLoader(
|
||||
common *common.Common,
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
) *RemoteLoader {
|
||||
return &RemoteLoader{
|
||||
Common: common,
|
||||
cmd: cmd,
|
||||
}
|
||||
func NewRemoteLoader(gitCommon *GitCommon) *RemoteLoader {
|
||||
return &RemoteLoader{GitCommon: gitCommon}
|
||||
}
|
||||
|
||||
func (self *RemoteLoader) GetRemotes() ([]*models.Remote, error) {
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
|
||||
var remoteBranchesByRemoteName map[string][]*models.RemoteBranch
|
||||
var remoteBranchesErr error
|
||||
go utils.Safe(func() {
|
||||
defer wg.Done()
|
||||
|
||||
remoteBranchesByRemoteName, remoteBranchesErr = self.getRemoteBranchesByRemoteName()
|
||||
})
|
||||
|
||||
// GetRemotes returns the repo's remotes, without their branches; those are
|
||||
// loaded separately with GetRemoteBranchesByRemoteName, which takes a lot longer
|
||||
// in a repo with many remote branches.
|
||||
func (self *RemoteLoader) GetRemotes() []*models.Remote {
|
||||
remotes := self.getRemotesFromConfig()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if remoteBranchesErr != nil {
|
||||
return nil, remoteBranchesErr
|
||||
}
|
||||
|
||||
for _, remote := range remotes {
|
||||
remote.Branches = remoteBranchesByRemoteName[remote.Name]
|
||||
}
|
||||
|
||||
// now lets sort our remotes by name alphabetically
|
||||
slices.SortFunc(remotes, func(a, b *models.Remote) int {
|
||||
// we want origin at the top because we'll be most likely to want it
|
||||
@@ -64,7 +36,7 @@ func (self *RemoteLoader) GetRemotes() ([]*models.Remote, error) {
|
||||
return strings.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name))
|
||||
})
|
||||
|
||||
return remotes, nil
|
||||
return remotes
|
||||
}
|
||||
|
||||
func (self *RemoteLoader) getRemotesFromConfig() []*models.Remote {
|
||||
@@ -111,29 +83,47 @@ func (self *RemoteLoader) getRemotesFromConfig() []*models.Remote {
|
||||
return slices.Collect(maps.Values(remotesByName))
|
||||
}
|
||||
|
||||
func (self *RemoteLoader) getRemoteBranchesByRemoteName() (map[string][]*models.RemoteBranch, error) {
|
||||
remoteBranchesByRemoteName := make(map[string][]*models.RemoteBranch)
|
||||
// GetRemoteBranchesByRemoteName returns all remote branches, keyed by the name
|
||||
// of the remote they belong to.
|
||||
func (self *RemoteLoader) GetRemoteBranchesByRemoteName() (map[string][]*models.RemoteBranch, error) {
|
||||
remoteBranches, err := self.getRemoteBranches()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var sortOrder string
|
||||
switch strings.ToLower(self.UserConfig().Git.RemoteBranchSortOrder) {
|
||||
case "alphabetical":
|
||||
sortOrder = "refname"
|
||||
case "date":
|
||||
return lo.GroupBy(remoteBranches, func(branch *models.RemoteBranch) string {
|
||||
return branch.RemoteName
|
||||
}), nil
|
||||
}
|
||||
|
||||
// Returns all remote branches, sorted the way the config asks for
|
||||
func (self *RemoteLoader) getRemoteBranches() ([]*models.RemoteBranch, error) {
|
||||
sortByDate := strings.ToLower(self.UserConfig().Git.RemoteBranchSortOrder) == "date"
|
||||
sortOrder := "refname"
|
||||
if sortByDate {
|
||||
sortOrder = "-committerdate"
|
||||
default:
|
||||
sortOrder = "refname"
|
||||
}
|
||||
|
||||
// Asking for the tip of a branch makes git read its commit, so only do it
|
||||
// when we are going to sort by ancestry below
|
||||
format := "%(refname)"
|
||||
if sortByDate {
|
||||
format += "%00%(objectname)%00%(committerdate:unix)"
|
||||
}
|
||||
|
||||
cmdArgs := NewGitCmd("for-each-ref").
|
||||
Arg(fmt.Sprintf("--sort=%s", sortOrder)).
|
||||
Arg("--format=%(refname)").
|
||||
Arg(fmt.Sprintf("--format=%s", format)).
|
||||
Arg("refs/remotes").
|
||||
ToArgv()
|
||||
|
||||
remoteBranches := []*models.RemoteBranch{}
|
||||
tips := map[string]refTip{}
|
||||
err := self.cmd.New(cmdArgs).DontLog().RunAndProcessLines(func(line string) (bool, error) {
|
||||
line = strings.TrimSpace(line)
|
||||
fields := strings.Split(strings.TrimSpace(line), "\x00")
|
||||
refName := fields[0]
|
||||
|
||||
split := strings.SplitN(line, "/", 4)
|
||||
split := strings.SplitN(refName, "/", 4)
|
||||
if len(split) != 4 {
|
||||
return false, nil
|
||||
}
|
||||
@@ -144,21 +134,27 @@ func (self *RemoteLoader) getRemoteBranchesByRemoteName() (map[string][]*models.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
_, ok := remoteBranchesByRemoteName[remoteName]
|
||||
if !ok {
|
||||
remoteBranchesByRemoteName[remoteName] = []*models.RemoteBranch{}
|
||||
}
|
||||
|
||||
remoteBranchesByRemoteName[remoteName] = append(remoteBranchesByRemoteName[remoteName],
|
||||
remoteBranches = append(remoteBranches,
|
||||
&models.RemoteBranch{
|
||||
Name: name,
|
||||
RemoteName: remoteName,
|
||||
})
|
||||
if len(fields) == 3 {
|
||||
tips[refName] = refTip{hash: fields[1], committerDate: fields[2]}
|
||||
}
|
||||
return false, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return remoteBranchesByRemoteName, nil
|
||||
if sortByDate {
|
||||
if err := sortRefsWithEqualDatesByAncestry(
|
||||
self.cmd, self.version, remoteBranches, (*models.RemoteBranch).FullRefName, tips,
|
||||
); err != nil {
|
||||
self.Log.Errorf("Failed to sort remote branches by ancestry: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return remoteBranches, nil
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -95,8 +94,7 @@ func TestGetRemotesFromConfig(t *testing.T) {
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.testName, func(t *testing.T) {
|
||||
loader := &RemoteLoader{
|
||||
Common: common.NewDummyCommon(),
|
||||
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
|
||||
GitCommon: buildGitCommon(commonDeps{runner: scenario.runner}),
|
||||
}
|
||||
|
||||
// map iteration order is non-deterministic, so compare unordered
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
ioFs "io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -10,7 +9,6 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
type RepoPaths struct {
|
||||
@@ -273,13 +271,13 @@ func callGitRevParseWithDir(
|
||||
return runGitRevParse(newGitRevParseCmd(cmd, dir, gitRevArgs...))
|
||||
}
|
||||
|
||||
// Asks git about a repo that isn't the one we have open; see forOtherRepo.
|
||||
// Asks git about a repo that isn't the one we have open; see ForOtherRepo.
|
||||
func callGitRevParseInOtherRepo(
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
dir string,
|
||||
gitRevArgs ...string,
|
||||
) (string, error) {
|
||||
return runGitRevParse(forOtherRepo(newGitRevParseCmd(cmd, dir, gitRevArgs...)))
|
||||
return runGitRevParse(ForOtherRepo(newGitRevParseCmd(cmd, dir, gitRevArgs...)))
|
||||
}
|
||||
|
||||
func newGitRevParseCmd(
|
||||
@@ -302,41 +300,3 @@ func runGitRevParse(gitCmd *oscommands.CmdObj) (string, error) {
|
||||
}
|
||||
return strings.TrimSpace(res), nil
|
||||
}
|
||||
|
||||
// Returns the paths of linked worktrees
|
||||
func linkedWortkreePaths(fs afero.Fs, repoGitDirPath string) []string {
|
||||
result := []string{}
|
||||
// For each directory in this path we're going to cat the `gitdir` file and append its contents to our result
|
||||
// That file points us to the `.git` file in the worktree.
|
||||
worktreeGitDirsPath := filepath.Join(repoGitDirPath, "worktrees")
|
||||
|
||||
// ensure the directory exists
|
||||
_, err := fs.Stat(worktreeGitDirsPath)
|
||||
if err != nil {
|
||||
return result
|
||||
}
|
||||
|
||||
_ = afero.Walk(fs, worktreeGitDirsPath, func(currPath string, info ioFs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
gitDirPath := filepath.Join(currPath, "gitdir")
|
||||
gitDirBytes, err := afero.ReadFile(fs, gitDirPath)
|
||||
if err != nil {
|
||||
// ignoring error
|
||||
return nil
|
||||
}
|
||||
trimmedGitDir := strings.TrimSpace(string(gitDirBytes))
|
||||
// removing the .git part
|
||||
worktreeDir := filepath.Dir(trimmedGitDir)
|
||||
result = append(result, worktreeDir)
|
||||
return nil
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -80,14 +80,14 @@ func (self *StashCommands) Hash(index int) (string, error) {
|
||||
return strings.Trim(hash, "\r\n"), err
|
||||
}
|
||||
|
||||
func (self *StashCommands) ShowStashEntryCmdObj(index int) *oscommands.CmdObj {
|
||||
func (self *StashCommands) ShowStashEntryCmdObj(index int, mode DiffMode) *oscommands.CmdObj {
|
||||
// "-u" is the same as "--include-untracked", but the latter fails in older git versions for some reason
|
||||
cmdArgs := NewGitCmd("stash").Arg("show").
|
||||
AddCommonDiffArgs(self.diffRendererConfigManager, self.UserConfig(), true).
|
||||
AddCommonDiffArgs(self.diffRendererConfigManager, self.UserConfig(), mode).
|
||||
Arg("-p").
|
||||
Arg("--stat").
|
||||
Arg("-u").
|
||||
Arg(fmt.Sprintf("--color=%s", self.diffRendererConfigManager.GetColorArg())).
|
||||
Arg(fmt.Sprintf("--color=%s", mode.colorArg(self.diffRendererConfigManager))).
|
||||
Arg(fmt.Sprintf("refs/stash@{%d}", index)).
|
||||
Dir(self.repoPaths.worktreePath).
|
||||
ToArgv()
|
||||
|
||||
@@ -139,7 +139,7 @@ func TestStashStashEntryCmdObj(t *testing.T) {
|
||||
similarityThreshold: 50,
|
||||
ignoreWhitespace: false,
|
||||
diffRendererConfig: &config.DiffRendererConfig{Type: "extDiff", Command: "difft --color=always"},
|
||||
expected: []string{"git", "-C", "/path/to/worktree", "-c", "diff.external=difft --color=always", "stash", "show", "--ext-diff", "--unified=3", "--find-renames=50%", "-p", "--stat", "-u", "--color=always", "refs/stash@{5}"},
|
||||
expected: []string{"git", "-C", "/path/to/worktree", "stash", "show", "--ext-diff", "--unified=3", "--find-renames=50%", "-p", "--stat", "-u", "--color=always", "refs/stash@{5}"},
|
||||
},
|
||||
{
|
||||
testName: "Show diff using git's external diff config",
|
||||
@@ -174,7 +174,7 @@ func TestStashStashEntryCmdObj(t *testing.T) {
|
||||
}
|
||||
instance := buildStashCommands(commonDeps{userConfig: userConfig, appState: &config.AppState{}, repoPaths: &repoPaths})
|
||||
|
||||
cmdStr := instance.ShowStashEntryCmdObj(s.index).Args()
|
||||
cmdStr := instance.ShowStashEntryCmdObj(s.index, DiffModeRendered).Args()
|
||||
assert.Equal(t, s.expected, cmdStr)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ func (self *SubmoduleCommands) GetCommitSummary(path string, sha string) (string
|
||||
Config("log.showsignature=false").
|
||||
ToArgv()
|
||||
|
||||
summary, err := forOtherRepo(self.cmd.New(cmdArgs)).DontLog().RunWithOutput()
|
||||
summary, err := ForOtherRepo(self.cmd.New(cmdArgs)).DontLog().RunWithOutput()
|
||||
return strings.TrimSpace(summary), err
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ func (self *SubmoduleCommands) GetCommitSummary(path string, sha string) (string
|
||||
// caller then stages the submodule to record the resolution.
|
||||
func (self *SubmoduleCommands) CheckoutConflictCommit(path string, sha string) error {
|
||||
cmdArgs := NewGitCmd("checkout").Dir(path).Arg(sha).ToArgv()
|
||||
return forOtherRepo(self.cmd.New(cmdArgs)).Run()
|
||||
return ForOtherRepo(self.cmd.New(cmdArgs)).Run()
|
||||
}
|
||||
|
||||
// ConflictSideLog returns a oneline log, run inside the submodule, of the commits
|
||||
@@ -179,7 +179,7 @@ func (self *SubmoduleCommands) ConflictSideLog(path string, side string, otherSi
|
||||
Arg("--oneline", "--color=always", otherSide+".."+side).
|
||||
ToArgv()
|
||||
|
||||
return forOtherRepo(self.cmd.New(cmdArgs)).DontLog().RunWithOutput()
|
||||
return ForOtherRepo(self.cmd.New(cmdArgs)).DontLog().RunWithOutput()
|
||||
}
|
||||
|
||||
func (self *SubmoduleCommands) Stash(submodule *models.SubmoduleConfig) error {
|
||||
@@ -195,7 +195,7 @@ func (self *SubmoduleCommands) Stash(submodule *models.SubmoduleConfig) error {
|
||||
Arg("--include-untracked").
|
||||
ToArgv()
|
||||
|
||||
return forOtherRepo(self.cmd.New(cmdArgs)).Run()
|
||||
return ForOtherRepo(self.cmd.New(cmdArgs)).Run()
|
||||
}
|
||||
|
||||
func (self *SubmoduleCommands) Reset(submodule *models.SubmoduleConfig) error {
|
||||
@@ -229,7 +229,7 @@ func (self *SubmoduleCommands) UpdateAll() error {
|
||||
// need not be.
|
||||
func (self *SubmoduleCommands) runInParentModule(submodule *models.SubmoduleConfig, cmdObj *oscommands.CmdObj) error {
|
||||
if submodule.ParentModule != nil {
|
||||
forOtherRepo(cmdObj.SetWd(submodule.ParentModule.FullPath()))
|
||||
ForOtherRepo(cmdObj.SetWd(submodule.ParentModule.FullPath()))
|
||||
}
|
||||
return cmdObj.Run()
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/gocui"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type SyncCommands struct {
|
||||
@@ -18,18 +19,21 @@ func NewSyncCommands(gitCommon *GitCommon) *SyncCommands {
|
||||
}
|
||||
}
|
||||
|
||||
// Push pushes to a branch
|
||||
type PushOpts struct {
|
||||
Force bool
|
||||
ForceWithLease bool
|
||||
CurrentBranch string
|
||||
UpstreamRemote string
|
||||
UpstreamBranch string
|
||||
SetUpstream bool
|
||||
// The remote to push to. If empty, git picks it from its configuration,
|
||||
// and Refspecs must be empty too.
|
||||
Remote string
|
||||
// What to push, each in the form "refs/heads/<local branch>:<remote ref>".
|
||||
// If empty, git decides what to push based on push.default and
|
||||
// remote.<name>.push.
|
||||
Refspecs []string
|
||||
}
|
||||
|
||||
func (self *SyncCommands) PushCmdObj(task gocui.Task, opts PushOpts) (*oscommands.CmdObj, error) {
|
||||
if opts.UpstreamBranch != "" && opts.UpstreamRemote == "" {
|
||||
if len(opts.Refspecs) > 0 && opts.Remote == "" {
|
||||
return nil, errors.New(self.Tr.MustSpecifyOriginError)
|
||||
}
|
||||
|
||||
@@ -37,8 +41,8 @@ func (self *SyncCommands) PushCmdObj(task gocui.Task, opts PushOpts) (*oscommand
|
||||
ArgIf(opts.Force, "--force").
|
||||
ArgIf(opts.ForceWithLease, "--force-with-lease").
|
||||
ArgIf(opts.SetUpstream, "--set-upstream").
|
||||
ArgIf(opts.UpstreamRemote != "", opts.UpstreamRemote).
|
||||
ArgIf(opts.UpstreamBranch != "", fmt.Sprintf("refs/heads/%s:%s", opts.CurrentBranch, opts.UpstreamBranch)).
|
||||
ArgIf(opts.Remote != "", opts.Remote).
|
||||
Arg(opts.Refspecs...).
|
||||
ToArgv()
|
||||
|
||||
cmdObj := self.cmd.New(cmdArgs).PromptOnCredentialRequest(task)
|
||||
@@ -110,15 +114,25 @@ func (self *SyncCommands) Pull(task gocui.Task, opts PullOptions) error {
|
||||
return self.cmd.New(cmdArgs).AddEnvVars("GIT_SEQUENCE_EDITOR=:").PromptOnCredentialRequest(task).Run()
|
||||
}
|
||||
|
||||
func (self *SyncCommands) FastForward(
|
||||
// Fetches the given branches of the given remote, updating their
|
||||
// remote-tracking branches. Local branches are left alone, including the ones
|
||||
// that track them.
|
||||
func (self *SyncCommands) FetchRemoteBranches(
|
||||
task gocui.Task,
|
||||
branchName string,
|
||||
remoteName string,
|
||||
remoteBranchName string,
|
||||
remoteBranchNames []string,
|
||||
) error {
|
||||
// The explicit destinations and the leading + make sure that the
|
||||
// remote-tracking branches are updated even when the remote branches were
|
||||
// rewritten, whatever the remote's fetch refspec says
|
||||
refspecs := lo.Map(remoteBranchNames, func(remoteBranchName string, _ int) string {
|
||||
return fmt.Sprintf("+refs/heads/%s:refs/remotes/%s/%s",
|
||||
remoteBranchName, remoteName, remoteBranchName)
|
||||
})
|
||||
|
||||
cmdArgs := self.fetchCommandBuilder(false).
|
||||
Arg(remoteName).
|
||||
Arg("refs/heads/" + remoteBranchName + ":" + branchName).
|
||||
Arg(refspecs...).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).Run()
|
||||
|
||||
@@ -41,12 +41,11 @@ func TestSyncPush(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Push with force disabled, upstream supplied",
|
||||
testName: "Push with force disabled, refspec supplied",
|
||||
opts: PushOpts{
|
||||
ForceWithLease: false,
|
||||
CurrentBranch: "master",
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "master",
|
||||
Remote: "origin",
|
||||
Refspecs: []string{"refs/heads/master:master"},
|
||||
},
|
||||
test: func(cmdObj *oscommands.CmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "origin", "refs/heads/master:master"})
|
||||
@@ -57,9 +56,8 @@ func TestSyncPush(t *testing.T) {
|
||||
testName: "Push with force disabled, setting upstream",
|
||||
opts: PushOpts{
|
||||
ForceWithLease: false,
|
||||
CurrentBranch: "master-local",
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "master",
|
||||
Remote: "origin",
|
||||
Refspecs: []string{"refs/heads/master-local:master"},
|
||||
SetUpstream: true,
|
||||
},
|
||||
test: func(cmdObj *oscommands.CmdObj, err error) {
|
||||
@@ -71,9 +69,8 @@ func TestSyncPush(t *testing.T) {
|
||||
testName: "Push with force-with-lease enabled, setting upstream",
|
||||
opts: PushOpts{
|
||||
ForceWithLease: true,
|
||||
CurrentBranch: "master",
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "master",
|
||||
Remote: "origin",
|
||||
Refspecs: []string{"refs/heads/master:master"},
|
||||
SetUpstream: true,
|
||||
},
|
||||
test: func(cmdObj *oscommands.CmdObj, err error) {
|
||||
@@ -82,11 +79,23 @@ func TestSyncPush(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Push with remote branch but no origin",
|
||||
testName: "Push several refspecs",
|
||||
opts: PushOpts{
|
||||
ForceWithLease: true,
|
||||
UpstreamRemote: "",
|
||||
UpstreamBranch: "master",
|
||||
Remote: "origin",
|
||||
Refspecs: []string{"refs/heads/a:refs/heads/a", "refs/heads/b:refs/heads/b"},
|
||||
},
|
||||
test: func(cmdObj *oscommands.CmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "--force-with-lease", "origin", "refs/heads/a:refs/heads/a", "refs/heads/b:refs/heads/b"})
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Push with refspec but no remote",
|
||||
opts: PushOpts{
|
||||
ForceWithLease: true,
|
||||
Remote: "",
|
||||
Refspecs: []string{"refs/heads/master:master"},
|
||||
SetUpstream: true,
|
||||
},
|
||||
test: func(cmdObj *oscommands.CmdObj, err error) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/gocui"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type TagCommands struct {
|
||||
@@ -46,15 +47,16 @@ func (self *TagCommands) HasTag(tagName string) bool {
|
||||
return self.cmd.New(cmdArgs).DontLog().Run() == nil
|
||||
}
|
||||
|
||||
func (self *TagCommands) LocalDelete(tagName string) error {
|
||||
cmdArgs := NewGitCmd("tag").Arg("-d", tagName).
|
||||
func (self *TagCommands) LocalDelete(tagNames []string) error {
|
||||
cmdArgs := NewGitCmd("tag").Arg("-d").Arg(tagNames...).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *TagCommands) Push(task gocui.Task, remoteName string, tagName string) error {
|
||||
cmdArgs := NewGitCmd("push").Arg(remoteName, "tag", tagName).
|
||||
func (self *TagCommands) Push(task gocui.Task, remoteName string, tagNames []string) error {
|
||||
cmdArgs := NewGitCmd("push").Arg(remoteName).
|
||||
Arg(lo.FlatMap(tagNames, func(t string, _ int) []string { return []string{"tag", t} })...).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).Run()
|
||||
|
||||
@@ -383,39 +383,28 @@ func (self *WorkingTreeCommands) Exclude(filename string) error {
|
||||
}
|
||||
|
||||
// WorktreeFileDiff returns the diff of a file
|
||||
func (self *WorkingTreeCommands) WorktreeFileDiff(file *models.File, plain bool, cached bool) string {
|
||||
func (self *WorkingTreeCommands) WorktreeFileDiff(file *models.File, mode DiffMode, cached bool) string {
|
||||
// for now we assume an error means the file was deleted
|
||||
s, _ := self.WorktreeFileDiffCmdObj(file, plain, cached, nil).RunWithOutput()
|
||||
s, _ := self.WorktreeFileDiffCmdObj(file, mode, cached, file.Names()).RunWithOutput()
|
||||
return s
|
||||
}
|
||||
|
||||
// WorktreeFileDiffCmdObj returns a command object for diffing a file or directory
|
||||
// in the working tree. When pathOverrides is non-empty, those paths are used instead of
|
||||
// the node's path (used to diff only filtered/visible files within a directory).
|
||||
func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain bool, cached bool, pathOverrides []string) *oscommands.CmdObj {
|
||||
colorArg := self.diffRendererConfigManager.GetColorArg()
|
||||
if plain {
|
||||
colorArg = "never"
|
||||
}
|
||||
|
||||
prevPath := node.GetPreviousPath()
|
||||
// WorktreeFileDiffCmdObj returns a command object for diffing the given paths
|
||||
// in the working tree. node is the item they belong to; all it decides is
|
||||
// whether git has to compare against /dev/null, which is the case for a file
|
||||
// that isn't in the index yet.
|
||||
func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, mode DiffMode, cached bool, paths []string) *oscommands.CmdObj {
|
||||
noIndex := !node.GetIsTracked() && !node.GetHasStagedChanges() && !cached && node.GetIsFile()
|
||||
|
||||
paths := pathOverrides
|
||||
if len(paths) == 0 {
|
||||
paths = []string{node.GetPath()}
|
||||
}
|
||||
|
||||
cmdArgs := NewGitCmd("diff").
|
||||
AddCommonDiffArgs(self.diffRendererConfigManager, self.UserConfig(), !plain).
|
||||
AddCommonDiffArgs(self.diffRendererConfigManager, self.UserConfig(), mode).
|
||||
Arg("--submodule").
|
||||
Arg(fmt.Sprintf("--color=%s", colorArg)).
|
||||
Arg(fmt.Sprintf("--color=%s", mode.colorArg(self.diffRendererConfigManager))).
|
||||
ArgIf(cached, "--cached").
|
||||
ArgIf(noIndex, "--no-index").
|
||||
Arg("--").
|
||||
ArgIf(noIndex, "/dev/null").
|
||||
Arg(paths...).
|
||||
ArgIf(prevPath != "", prevPath).
|
||||
Dir(self.repoPaths.worktreePath).
|
||||
ToArgv()
|
||||
|
||||
@@ -426,25 +415,20 @@ func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain
|
||||
// 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.
|
||||
// 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) {
|
||||
func (self *WorkingTreeCommands) ShowFileDiff(from string, to string, reverse bool, fileName string, previousPath string, mode DiffMode) (string, error) {
|
||||
fileNames := []string{fileName}
|
||||
if previousPath != "" {
|
||||
fileNames = append(fileNames, previousPath)
|
||||
}
|
||||
return self.ShowFileDiffCmdObj(from, to, reverse, fileNames, plain).RunWithOutput()
|
||||
return self.ShowFileDiffCmdObj(from, to, reverse, fileNames, mode).RunWithOutput()
|
||||
}
|
||||
|
||||
func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reverse bool, fileNames []string, plain bool) *oscommands.CmdObj {
|
||||
colorArg := self.diffRendererConfigManager.GetColorArg()
|
||||
if plain {
|
||||
colorArg = "never"
|
||||
}
|
||||
|
||||
func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reverse bool, fileNames []string, mode DiffMode) *oscommands.CmdObj {
|
||||
cmdArgs := NewGitCmd("diff").
|
||||
Config("diff.noprefix=false").
|
||||
AddCommonDiffArgs(self.diffRendererConfigManager, self.UserConfig(), !plain).
|
||||
AddCommonDiffArgs(self.diffRendererConfigManager, self.UserConfig(), mode).
|
||||
Arg("--submodule").
|
||||
Arg(fmt.Sprintf("--color=%s", colorArg)).
|
||||
Arg(fmt.Sprintf("--color=%s", mode.colorArg(self.diffRendererConfigManager))).
|
||||
Arg(from).
|
||||
Arg(to).
|
||||
ArgIf(reverse, "-R").
|
||||
@@ -530,6 +514,42 @@ func (self *WorkingTreeCommands) ResetSoft(ref string) error {
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
// ResetKeep runs `git reset --keep` in the given worktree, which moves the
|
||||
// checked out branch to the given ref while keeping local modifications. It
|
||||
// fails rather than overwriting a file that differs between the two commits.
|
||||
// Pass empty strings for the worktree to use the current one.
|
||||
func (self *WorkingTreeCommands) ResetKeep(ref string, worktreeGitDir string, worktreePath string) error {
|
||||
cmdArgs := NewGitCmd("reset").Arg("--keep", ref).
|
||||
GitDirIf(worktreeGitDir != "", worktreeGitDir).
|
||||
WorktreePathIf(worktreePath != "", worktreePath).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
// Returns whether the given worktree has changes to tracked files, either in
|
||||
// its working tree or in its index. Untracked files don't count, and neither do
|
||||
// submodules. A submodule that is checked out at a different commit than the
|
||||
// one recorded, or that has changes of its own, doesn't get in the way of
|
||||
// moving the branch, because moving it leaves the submodules alone. Pass empty
|
||||
// strings for the worktree to use the current one.
|
||||
func (self *WorkingTreeCommands) HasChangesToTrackedFiles(worktreeGitDir string, worktreePath string) (bool, error) {
|
||||
cmdArgs := NewGitCmd("status").
|
||||
Arg("--porcelain").
|
||||
Arg("--untracked-files=no").
|
||||
Arg("--ignore-submodules").
|
||||
GitDirIf(worktreeGitDir != "", worktreeGitDir).
|
||||
WorktreePathIf(worktreePath != "", worktreePath).
|
||||
ToArgv()
|
||||
|
||||
stdout, _, err := self.cmd.New(cmdArgs).DontLog().RunWithOutputs()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return stdout != "", nil
|
||||
}
|
||||
|
||||
func (self *WorkingTreeCommands) ResetMixed(ref string) error {
|
||||
cmdArgs := NewGitCmd("reset").Arg("--mixed", ref).
|
||||
ToArgv()
|
||||
|
||||
@@ -197,7 +197,7 @@ func TestWorkingTreeDiff(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
file *models.File
|
||||
plain bool
|
||||
mode DiffMode
|
||||
cached bool
|
||||
ignoreWhitespace bool
|
||||
contextSize uint64
|
||||
@@ -215,7 +215,7 @@ func TestWorkingTreeDiff(t *testing.T) {
|
||||
HasStagedChanges: false,
|
||||
Tracked: true,
|
||||
},
|
||||
plain: false,
|
||||
mode: DiffModeRendered,
|
||||
cached: false,
|
||||
ignoreWhitespace: false,
|
||||
contextSize: 3,
|
||||
@@ -230,7 +230,7 @@ func TestWorkingTreeDiff(t *testing.T) {
|
||||
HasStagedChanges: false,
|
||||
Tracked: true,
|
||||
},
|
||||
plain: false,
|
||||
mode: DiffModeRendered,
|
||||
cached: true,
|
||||
ignoreWhitespace: false,
|
||||
contextSize: 3,
|
||||
@@ -245,7 +245,7 @@ func TestWorkingTreeDiff(t *testing.T) {
|
||||
HasStagedChanges: false,
|
||||
Tracked: true,
|
||||
},
|
||||
plain: true,
|
||||
mode: DiffModePlain,
|
||||
cached: false,
|
||||
ignoreWhitespace: false,
|
||||
contextSize: 3,
|
||||
@@ -260,7 +260,7 @@ func TestWorkingTreeDiff(t *testing.T) {
|
||||
HasStagedChanges: false,
|
||||
Tracked: false,
|
||||
},
|
||||
plain: false,
|
||||
mode: DiffModeRendered,
|
||||
cached: false,
|
||||
ignoreWhitespace: false,
|
||||
contextSize: 3,
|
||||
@@ -275,7 +275,7 @@ func TestWorkingTreeDiff(t *testing.T) {
|
||||
HasStagedChanges: false,
|
||||
Tracked: true,
|
||||
},
|
||||
plain: false,
|
||||
mode: DiffModeRendered,
|
||||
cached: false,
|
||||
ignoreWhitespace: true,
|
||||
contextSize: 3,
|
||||
@@ -290,7 +290,7 @@ func TestWorkingTreeDiff(t *testing.T) {
|
||||
HasStagedChanges: false,
|
||||
Tracked: true,
|
||||
},
|
||||
plain: false,
|
||||
mode: DiffModeRendered,
|
||||
cached: false,
|
||||
ignoreWhitespace: false,
|
||||
contextSize: 17,
|
||||
@@ -305,7 +305,7 @@ func TestWorkingTreeDiff(t *testing.T) {
|
||||
HasStagedChanges: false,
|
||||
Tracked: true,
|
||||
},
|
||||
plain: false,
|
||||
mode: DiffModeRendered,
|
||||
cached: false,
|
||||
ignoreWhitespace: false,
|
||||
contextSize: 3,
|
||||
@@ -326,7 +326,7 @@ func TestWorkingTreeDiff(t *testing.T) {
|
||||
}
|
||||
|
||||
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner, userConfig: userConfig, appState: &config.AppState{}, repoPaths: &repoPaths})
|
||||
result := instance.WorktreeFileDiff(s.file, s.plain, s.cached)
|
||||
result := instance.WorktreeFileDiff(s.file, s.mode, s.cached)
|
||||
assert.Equal(t, expectedResult, result)
|
||||
s.runner.CheckForMissingCalls()
|
||||
})
|
||||
@@ -341,7 +341,7 @@ func TestWorkingTreeShowFileDiff(t *testing.T) {
|
||||
reverse bool
|
||||
fileName string
|
||||
previousPath string
|
||||
plain bool
|
||||
mode DiffMode
|
||||
ignoreWhitespace bool
|
||||
contextSize uint64
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
@@ -356,7 +356,7 @@ func TestWorkingTreeShowFileDiff(t *testing.T) {
|
||||
to: "0987654321",
|
||||
reverse: false,
|
||||
fileName: "test.txt",
|
||||
plain: false,
|
||||
mode: DiffModeRendered,
|
||||
ignoreWhitespace: false,
|
||||
contextSize: 3,
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
@@ -368,7 +368,7 @@ func TestWorkingTreeShowFileDiff(t *testing.T) {
|
||||
to: "0987654321",
|
||||
reverse: false,
|
||||
fileName: "test.txt",
|
||||
plain: false,
|
||||
mode: DiffModeRendered,
|
||||
ignoreWhitespace: false,
|
||||
contextSize: 123,
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
@@ -380,7 +380,7 @@ func TestWorkingTreeShowFileDiff(t *testing.T) {
|
||||
to: "0987654321",
|
||||
reverse: false,
|
||||
fileName: "test.txt",
|
||||
plain: false,
|
||||
mode: DiffModeRendered,
|
||||
ignoreWhitespace: true,
|
||||
contextSize: 3,
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
@@ -393,7 +393,7 @@ func TestWorkingTreeShowFileDiff(t *testing.T) {
|
||||
reverse: false,
|
||||
fileName: "new.txt",
|
||||
previousPath: "old.txt",
|
||||
plain: false,
|
||||
mode: DiffModeRendered,
|
||||
ignoreWhitespace: false,
|
||||
contextSize: 3,
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
@@ -412,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, s.fileName, s.previousPath, s.plain)
|
||||
result, err := instance.ShowFileDiff(s.from, s.to, s.reverse, s.fileName, s.previousPath, s.mode)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedResult, result)
|
||||
s.runner.CheckForMissingCalls()
|
||||
|
||||
@@ -51,7 +51,7 @@ func (self *WorktreeCommands) Delete(worktreePath string, force bool) error {
|
||||
func (self *WorktreeCommands) Detach(worktreePath string) error {
|
||||
cmdArgs := NewGitCmd("checkout").Arg("--detach").GitDir(filepath.Join(worktreePath, ".git")).ToArgv()
|
||||
|
||||
return forOtherRepo(self.cmd.New(cmdArgs)).Run()
|
||||
return ForOtherRepo(self.cmd.New(cmdArgs)).Run()
|
||||
}
|
||||
|
||||
func WorktreeForBranch(branch *models.Branch, worktrees []*models.Worktree) (*models.Worktree, bool) {
|
||||
|
||||
@@ -140,12 +140,14 @@ func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) {
|
||||
rebasedBranch, ok := self.rebasedBranch(worktree)
|
||||
if ok {
|
||||
worktree.Branch = rebasedBranch
|
||||
worktree.IsRebasingOrBisecting = true
|
||||
continue
|
||||
}
|
||||
|
||||
bisectedBranch, ok := self.bisectedBranch(worktree)
|
||||
if ok {
|
||||
worktree.Branch = bisectedBranch
|
||||
worktree.IsRebasingOrBisecting = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,13 @@ type Branch struct {
|
||||
// 'git@github.com:tiwood/lazygit.git'
|
||||
UpstreamRemote string
|
||||
UpstreamBranch string
|
||||
// The remote and the remote branch that `git push` would push this branch
|
||||
// to, as git determines them from push.default, remote.pushDefault and
|
||||
// branch.<name>.pushRemote. In a triangular workflow these differ from the
|
||||
// upstream. Both are empty if git has no push destination for the branch,
|
||||
// e.g. because push.default is "upstream" and the branch has no upstream.
|
||||
PushRemote string
|
||||
PushBranch string
|
||||
// subject line in commit message
|
||||
Subject string
|
||||
// commit hash
|
||||
@@ -40,6 +47,13 @@ type Branch struct {
|
||||
// determined yet, or up to date with base branch. (We don't need to
|
||||
// distinguish the two, as we don't draw anything in both cases.)
|
||||
BehindBaseBranch atomic.Int32
|
||||
|
||||
// Whether the branch has diverged from its upstream because the upstream
|
||||
// branch was rewritten, and not because the branch has commits of its own.
|
||||
// Such a branch can be reset to its upstream without losing anything.
|
||||
// False for branches that haven't diverged, and for those we haven't
|
||||
// determined it for yet.
|
||||
UpstreamRewritten atomic.Bool
|
||||
}
|
||||
|
||||
func (b *Branch) FullRefName() string {
|
||||
@@ -119,6 +133,13 @@ func (b *Branch) IsBehindForPush() bool {
|
||||
return b.RemoteBranchStoredLocally() && b.BehindForPush != "0"
|
||||
}
|
||||
|
||||
// Whether the branch has commits that its push destination doesn't have. False
|
||||
// if the remote branch it would be pushed to isn't stored locally, in which
|
||||
// case the count is "?".
|
||||
func (b *Branch) IsAheadForPush() bool {
|
||||
return b.RemoteBranchStoredLocally() && b.AheadForPush != "0" && b.AheadForPush != "?"
|
||||
}
|
||||
|
||||
// for when we're in a detached head state
|
||||
func (b *Branch) IsRealBranch() bool {
|
||||
return b.AheadForPull != "" && b.BehindForPull != ""
|
||||
|
||||
@@ -19,6 +19,9 @@ type Worktree struct {
|
||||
// * the worktree is mid-rebase on the branch
|
||||
// * the worktree is mid-bisect on the branch
|
||||
Branch string
|
||||
// If true, the worktree is mid-rebase or mid-bisect on Branch, so its HEAD
|
||||
// is detached rather than pointing at the branch
|
||||
IsRebasingOrBisecting bool
|
||||
// The HEAD sha of the worktree. Always populated (even when Branch is set).
|
||||
// Used for display when Branch is empty (detached HEAD state).
|
||||
Head string
|
||||
|
||||
@@ -108,6 +108,14 @@ func (self *CmdObj) GetEnvVars() []string {
|
||||
return self.cmd.Env
|
||||
}
|
||||
|
||||
// SetEnviron replaces the command's whole environment, for a command that has
|
||||
// to run in the same one as another command rather than in this process's.
|
||||
func (self *CmdObj) SetEnviron(env []string) *CmdObj {
|
||||
self.cmd.Env = env
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
// sets the working directory
|
||||
func (self *CmdObj) SetWd(wd string) *CmdObj {
|
||||
self.cmd.Dir = wd
|
||||
|
||||
@@ -33,6 +33,16 @@ func (self *CmdObjBuilder) New(args []string) *CmdObj {
|
||||
return cmdObj
|
||||
}
|
||||
|
||||
// NewFromCmd wraps a command that has already been built, for a caller that
|
||||
// holds an *exec.Cmd and needs it as a CmdObj. The command itself is shared,
|
||||
// not copied, so whatever was set on it still applies.
|
||||
func (self *CmdObjBuilder) NewFromCmd(cmd *exec.Cmd) *CmdObj {
|
||||
return &CmdObj{
|
||||
cmd: cmd,
|
||||
runner: self.runner,
|
||||
}
|
||||
}
|
||||
|
||||
// A command with explicit environment from env
|
||||
func (self *CmdObjBuilder) NewWithEnviron(args []string, env []string) *CmdObj {
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
|
||||
@@ -4,12 +4,10 @@ import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
@@ -203,61 +201,25 @@ func (c *OSCommand) FileExists(path string) (bool, error) {
|
||||
|
||||
// PipeCommands runs a heap of commands and pipes their inputs/outputs together like A | B | C
|
||||
func (c *OSCommand) PipeCommands(cmdObjs ...*CmdObj) error {
|
||||
cmds := lo.Map(cmdObjs, func(cmdObj *CmdObj, _ int) *exec.Cmd {
|
||||
return cmdObj.GetCmd()
|
||||
})
|
||||
c.logPipeline(cmdObjs)
|
||||
|
||||
logCmdStr := strings.Join(
|
||||
lo.Map(cmdObjs, func(cmdObj *CmdObj, _ int) string {
|
||||
return cmdObj.ToString()
|
||||
}),
|
||||
" | ",
|
||||
)
|
||||
|
||||
c.LogCommand(logCmdStr, true)
|
||||
|
||||
for i := range len(cmds) - 1 {
|
||||
stdout, err := cmds[i].StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmds[i+1].Stdin = stdout
|
||||
cmds, parentEnds, err := wirePipeline(cmdObjs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// keeping this here in case I adapt this code for some other purpose in the future
|
||||
// cmds[len(cmds)-1].Stdout = os.Stdout
|
||||
|
||||
stderrs := make([]bytes.Buffer, len(cmds))
|
||||
for i := range cmds {
|
||||
cmds[i].Stderr = &stderrs[i]
|
||||
}
|
||||
|
||||
// Start every command before waiting for any of them: waiting for a command
|
||||
// closes our end of the pipe that feeds the next one, and a command that
|
||||
// hasn't been started by then would inherit a closed stdin.
|
||||
started := 0
|
||||
var startErr error
|
||||
for _, cmd := range cmds {
|
||||
if err := cmd.Start(); err != nil {
|
||||
startErr = err
|
||||
break
|
||||
}
|
||||
|
||||
started++
|
||||
}
|
||||
started, startErr := startPipeline(cmds, parentEnds)
|
||||
|
||||
finalErrors := []string{}
|
||||
|
||||
if startErr != nil {
|
||||
c.Log.Error(startErr)
|
||||
finalErrors = append(finalErrors, startErr.Error())
|
||||
|
||||
// Without the rest of the pipeline to drain them, the commands we did
|
||||
// start could block forever writing to a full pipe.
|
||||
for _, cmd := range cmds[:started] {
|
||||
_ = cmd.Process.Kill()
|
||||
}
|
||||
}
|
||||
|
||||
for i, cmd := range cmds[:started] {
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// Pipeline is a chain of running commands, each one's output feeding the next
|
||||
// one's input, with the last one's output going somewhere the caller reads. Its
|
||||
// method set is the one a render task expects of a command (see tasks.Cmd), so
|
||||
// a pipeline can render a view just as a single command can.
|
||||
type Pipeline struct {
|
||||
cmds []*exec.Cmd
|
||||
cmdStr string
|
||||
}
|
||||
|
||||
// StartPipeline starts the given commands wired A | B | C and returns the
|
||||
// pipeline together with the reader for its output.
|
||||
//
|
||||
// Every command's stderr goes to that same output, so whatever a command
|
||||
// complains about is part of what the caller reads. A diff renderer's error
|
||||
// message belongs on screen with the diff it failed to render.
|
||||
//
|
||||
// Closing the reader is how a pipeline is brought down. The last command's next
|
||||
// write fails, so it exits, and the failure travels back up the chain as each
|
||||
// command in turn writes into a pipe whose reader is gone.
|
||||
func (c *OSCommand) StartPipeline(cmdObjs ...*CmdObj) (*Pipeline, io.ReadCloser, error) {
|
||||
c.logPipeline(cmdObjs)
|
||||
|
||||
cmds, parentEnds, err := wirePipeline(cmdObjs)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
reader, writer, err := os.Pipe()
|
||||
if err != nil {
|
||||
closeAll(parentEnds)
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, cmd := range cmds {
|
||||
cmd.Stderr = writer
|
||||
}
|
||||
cmds[len(cmds)-1].Stdout = writer
|
||||
parentEnds = append(parentEnds, writer)
|
||||
|
||||
started, err := startPipeline(cmds, parentEnds)
|
||||
if err != nil {
|
||||
for _, cmd := range cmds[:started] {
|
||||
_ = cmd.Wait()
|
||||
}
|
||||
_ = reader.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return &Pipeline{cmds: cmds, cmdStr: pipelineString(cmdObjs)}, reader, nil
|
||||
}
|
||||
|
||||
func (self *Pipeline) String() string {
|
||||
return self.cmdStr
|
||||
}
|
||||
|
||||
// Wait waits for every command to exit and reports the failure nearest the end
|
||||
// of the pipeline. A command that fails leaves the ones before it writing into
|
||||
// a pipe nobody reads, so their own broken-pipe failures are consequences of it
|
||||
// rather than the cause worth reporting, while a command that fails early
|
||||
// leaves the ones after it with nothing to read and no reason to fail at all.
|
||||
func (self *Pipeline) Wait() error {
|
||||
var lastErr error
|
||||
for _, cmd := range self.cmds {
|
||||
if err := cmd.Wait(); err != nil {
|
||||
lastErr = fmt.Errorf("%s: %w", cmd.String(), err)
|
||||
}
|
||||
}
|
||||
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// Terminate asks every command to stop, without waiting for any of them. On
|
||||
// platforms where that does nothing, the pipeline comes down when its output
|
||||
// reader is closed; see StartPipeline.
|
||||
func (self *Pipeline) Terminate() error {
|
||||
var firstErr error
|
||||
for _, cmd := range self.cmds {
|
||||
if err := TerminateProcessGracefully(cmd.Process); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
|
||||
return firstErr
|
||||
}
|
||||
|
||||
// logPipeline enters a chain of commands into the command log, unless the first
|
||||
// command was marked not to be logged; it speaks for the pipeline. A render runs
|
||||
// its pipeline again on every selection change, so a caller has to be able to
|
||||
// keep it out of the log.
|
||||
func (c *OSCommand) logPipeline(cmdObjs []*CmdObj) {
|
||||
if cmdObjs[0].ShouldLog() {
|
||||
c.LogCommand(pipelineString(cmdObjs), true)
|
||||
}
|
||||
}
|
||||
|
||||
// pipelineString names a chain of commands the way a shell would write it.
|
||||
func pipelineString(cmdObjs []*CmdObj) string {
|
||||
return strings.Join(
|
||||
lo.Map(cmdObjs, func(cmdObj *CmdObj, _ int) string {
|
||||
return cmdObj.ToString()
|
||||
}),
|
||||
" | ",
|
||||
)
|
||||
}
|
||||
|
||||
// wirePipeline connects each command's output to the next one's input, like
|
||||
// A | B | C, and returns the commands along with the parent's ends of those
|
||||
// pipes. The last command's output is left for the caller to direct.
|
||||
//
|
||||
// The parent's ends have to be closed once the commands are running.
|
||||
// startPipeline does that; see there for why it matters.
|
||||
func wirePipeline(cmdObjs []*CmdObj) ([]*exec.Cmd, []io.Closer, error) {
|
||||
cmds := lo.Map(cmdObjs, func(cmdObj *CmdObj, _ int) *exec.Cmd {
|
||||
return cmdObj.GetCmd()
|
||||
})
|
||||
|
||||
parentEnds := []io.Closer{}
|
||||
for i := range len(cmds) - 1 {
|
||||
reader, writer, err := os.Pipe()
|
||||
if err != nil {
|
||||
closeAll(parentEnds)
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
cmds[i].Stdout = writer
|
||||
cmds[i+1].Stdin = reader
|
||||
parentEnds = append(parentEnds, reader, writer)
|
||||
}
|
||||
|
||||
return cmds, parentEnds, nil
|
||||
}
|
||||
|
||||
// startPipeline starts every command and reports how many it got going. Every
|
||||
// one is started before any of them is waited for: waiting closes our end of
|
||||
// the pipe that feeds the next command, and one that hasn't been started by
|
||||
// then would inherit a closed stdin.
|
||||
//
|
||||
// Once they are all running, each of them holds its own ends of the pipes it
|
||||
// reads and writes, and the parent lets go of its copies. Both directions
|
||||
// matter. While the parent holds the read end of a link, a command writing
|
||||
// into it never learns that the command meant to read it is gone, and keeps
|
||||
// running after the pipeline has been brought down. While the parent holds the
|
||||
// write end, the command reading it never reaches the end of its input.
|
||||
//
|
||||
// When a command fails to start, the ones already running are killed, since
|
||||
// without the rest of the pipeline to drain them they could block forever
|
||||
// writing to a full pipe. They still have to be reaped, so the count covers
|
||||
// them too.
|
||||
func startPipeline(cmds []*exec.Cmd, parentEnds []io.Closer) (int, error) {
|
||||
defer closeAll(parentEnds)
|
||||
|
||||
for i, cmd := range cmds {
|
||||
if err := cmd.Start(); err != nil {
|
||||
for _, started := range cmds[:i] {
|
||||
_ = started.Process.Kill()
|
||||
}
|
||||
|
||||
return i, err
|
||||
}
|
||||
}
|
||||
|
||||
return len(cmds), nil
|
||||
}
|
||||
|
||||
func closeAll(closers []io.Closer) {
|
||||
for _, closer := range closers {
|
||||
_ = closer.Close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// The pipeline tests need programs to run, and the test binary is the one
|
||||
// program every platform we test on is sure to have. pipelineMember builds a
|
||||
// command that re-runs this binary in the role a member of the pipeline is to
|
||||
// play; the roles are in TestPipelineMember.
|
||||
const pipelineRoleEnvVar = "LAZYGIT_TEST_PIPELINE_ROLE"
|
||||
|
||||
func pipelineMember(role string) *CmdObj {
|
||||
return NewDummyOSCommand().Cmd.
|
||||
New([]string{os.Args[0], "-test.run=^TestPipelineMember$"}).
|
||||
AddEnvVars(pipelineRoleEnvVar + "=" + role)
|
||||
}
|
||||
|
||||
// TestPipelineMember is the program the pipeline tests run, not a test of its
|
||||
// own. It exits before the testing package reports anything, so that its output
|
||||
// is what the role wrote and nothing else.
|
||||
//
|
||||
// For the same reason it exits with syscall.Exit, which skips the exit hooks
|
||||
// that os.Exit runs. In a binary built with -cover, one of these hooks writes
|
||||
// coverage data to $GOCOVERDIR and prints an error to stderr if that fails. On
|
||||
// Windows it fails now and then if two members of a pipeline exit at the same
|
||||
// time, because both of them replace the same file in that directory.
|
||||
func TestPipelineMember(t *testing.T) {
|
||||
switch os.Getenv(pipelineRoleEnvVar) {
|
||||
case "":
|
||||
t.Skip("not a test; the pipeline tests run this binary in a role")
|
||||
case "count":
|
||||
for i := 1; i <= 3; i++ {
|
||||
fmt.Printf("line %d\n", i)
|
||||
}
|
||||
case "upcase":
|
||||
input, _ := io.ReadAll(os.Stdin)
|
||||
fmt.Print(strings.ToUpper(string(input)))
|
||||
case "copy":
|
||||
_, _ = io.Copy(os.Stdout, os.Stdin)
|
||||
case "complain":
|
||||
fmt.Fprintln(os.Stderr, "something went wrong")
|
||||
syscall.Exit(3)
|
||||
case "flood":
|
||||
// A failed write means the reader is gone, and there is no point
|
||||
// writing to nobody. On platforms that raise a signal for it instead,
|
||||
// this process is already dead by the time the write returns.
|
||||
for i := 1; ; i++ {
|
||||
if _, err := fmt.Printf("line %d\n", i); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
syscall.Exit(0)
|
||||
}
|
||||
|
||||
func TestStartPipelineStreamsTheOutputOfTheLastCommand(t *testing.T) {
|
||||
pipeline, reader, err := NewDummyOSCommand().StartPipeline(
|
||||
pipelineMember("count"),
|
||||
pipelineMember("upcase"),
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
output, err := io.ReadAll(reader)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "LINE 1\nLINE 2\nLINE 3\n", string(output))
|
||||
|
||||
assert.NoError(t, pipeline.Wait())
|
||||
assert.NoError(t, reader.Close())
|
||||
}
|
||||
|
||||
func TestStartPipelineReadsWhatTheCommandsComplainAbout(t *testing.T) {
|
||||
pipeline, reader, err := NewDummyOSCommand().StartPipeline(
|
||||
pipelineMember("count"),
|
||||
pipelineMember("complain"),
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
output, err := io.ReadAll(reader)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "something went wrong\n", string(output))
|
||||
|
||||
// The failure of the command nearest the output is the one reported, even
|
||||
// though the one feeding it was left writing into a pipe nobody reads.
|
||||
assert.ErrorContains(t, pipeline.Wait(), "exit status 3")
|
||||
|
||||
assert.NoError(t, reader.Close())
|
||||
}
|
||||
|
||||
func TestClosingAPipelinesOutputBringsItDown(t *testing.T) {
|
||||
pipeline, reader, err := NewDummyOSCommand().StartPipeline(
|
||||
pipelineMember("flood"),
|
||||
pipelineMember("copy"),
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Read some output first, so that both commands are past their startup and
|
||||
// really running when the reader goes.
|
||||
buf := make([]byte, len("line 1\n"))
|
||||
_, err = io.ReadFull(reader, buf)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "line 1\n", string(buf))
|
||||
|
||||
assert.NoError(t, reader.Close())
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- pipeline.Wait() }()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatal("the pipeline was still running long after its output was closed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartPipelineReportsACommandItCannotStart(t *testing.T) {
|
||||
osCommand := NewDummyOSCommand()
|
||||
|
||||
_, _, err := osCommand.StartPipeline(
|
||||
pipelineMember("count"),
|
||||
osCommand.Cmd.New([]string{"lazygit-no-such-command"}),
|
||||
)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestPipeCommandsReturnsWhenALaterCommandDiesEarly(t *testing.T) {
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- NewDummyOSCommand().PipeCommands(
|
||||
pipelineMember("flood"),
|
||||
pipelineMember("complain"),
|
||||
)
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
assert.ErrorContains(t, err, "something went wrong")
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatal("PipeCommands was still waiting for a command whose output nothing reads")
|
||||
}
|
||||
}
|
||||
@@ -144,9 +144,7 @@ func TerminateLivePtys() {
|
||||
// graceful signal worth waiting on — git and the common diff tools leave it
|
||||
// to the default handler, which calls ExitProcess at whatever instruction
|
||||
// the process happens to execute — so clients that got the event are
|
||||
// already dying. Killing at an arbitrary point cannot leak a stale
|
||||
// index.lock, because pty-rendered commands don't take that lock (see
|
||||
// withPtyGitConfig in pkg/gui/pty.go).
|
||||
// already dying.
|
||||
//
|
||||
// The pseudoconsole close gets its own goroutine because the kill must not
|
||||
// wait for it: on builds where ClosePseudoConsole blocks until the console
|
||||
@@ -204,7 +202,8 @@ func (p *winPty) Close() error {
|
||||
// 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).
|
||||
// the render would never reach its end of input, so the new content would
|
||||
// never be swapped in.
|
||||
func startWaiter(proc *os.Process, p *winPty) func() error {
|
||||
done := make(chan struct{})
|
||||
var waitErr error
|
||||
|
||||
@@ -16,6 +16,11 @@ type Hunk struct {
|
||||
newStart int
|
||||
// the context at the end of the header line (' func (f *CommitFile) Description() string {' in the above example)
|
||||
headerContext string
|
||||
// the lengths declared in the header line ('2' and '3' in the above example),
|
||||
// kept so that we can check the parsed body against them (see
|
||||
// Patch.IsWellFormed). Only set by Parse.
|
||||
declaredOldLength int
|
||||
declaredNewLength int
|
||||
// the body of the hunk, excluding the header line
|
||||
bodyLines []*PatchLine
|
||||
}
|
||||
@@ -44,7 +49,8 @@ func (self *Hunk) lineCount() int {
|
||||
|
||||
// Returns all lines in the hunk, including the header line
|
||||
func (self *Hunk) allLines() []*PatchLine {
|
||||
lines := []*PatchLine{{Content: self.formatHeaderLine(), Kind: HUNK_HEADER}}
|
||||
lines := make([]*PatchLine, 1, 1+len(self.bodyLines))
|
||||
lines[0] = &PatchLine{Content: self.formatHeaderLine(), Kind: HUNK_HEADER}
|
||||
lines = append(lines, self.bodyLines...)
|
||||
return lines
|
||||
}
|
||||
|
||||
+26
-11
@@ -7,7 +7,9 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
var hunkHeaderRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)[^\+]+\+(\d+)[^@]+@@(.*)$`)
|
||||
// Captures, in order: the old start, the old length (omitted by git when it is
|
||||
// 1), the new start, the new length (likewise), and the trailing context.
|
||||
var hunkHeaderRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$`)
|
||||
|
||||
func Parse(patchStr string) *Patch {
|
||||
// ignore trailing newline.
|
||||
@@ -19,13 +21,15 @@ func Parse(patchStr string) *Patch {
|
||||
var currentHunk *Hunk
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "@@") {
|
||||
oldStart, newStart, headerContext := headerInfo(line)
|
||||
oldStart, oldLength, newStart, newLength, headerContext := headerInfo(line)
|
||||
|
||||
currentHunk = &Hunk{
|
||||
oldStart: oldStart,
|
||||
newStart: newStart,
|
||||
headerContext: headerContext,
|
||||
bodyLines: []*PatchLine{},
|
||||
oldStart: oldStart,
|
||||
newStart: newStart,
|
||||
declaredOldLength: oldLength,
|
||||
declaredNewLength: newLength,
|
||||
headerContext: headerContext,
|
||||
bodyLines: []*PatchLine{},
|
||||
}
|
||||
hunks = append(hunks, currentHunk)
|
||||
} else if currentHunk != nil {
|
||||
@@ -41,14 +45,25 @@ func Parse(patchStr string) *Patch {
|
||||
}
|
||||
}
|
||||
|
||||
func headerInfo(header string) (int, int, string) {
|
||||
func headerInfo(header string) (oldStart int, oldLength int, newStart int, newLength int, headerContext string) {
|
||||
match := hunkHeaderRegexp.FindStringSubmatch(header)
|
||||
|
||||
oldStart := utils.MustConvertToInt(match[1])
|
||||
newStart := utils.MustConvertToInt(match[2])
|
||||
headerContext := match[3]
|
||||
oldStart = utils.MustConvertToInt(match[1])
|
||||
oldLength = declaredLength(match[2])
|
||||
newStart = utils.MustConvertToInt(match[3])
|
||||
newLength = declaredLength(match[4])
|
||||
headerContext = match[5]
|
||||
|
||||
return oldStart, newStart, headerContext
|
||||
return oldStart, oldLength, newStart, newLength, headerContext
|
||||
}
|
||||
|
||||
// declaredLength parses a length capture of a hunk header, which git omits when
|
||||
// it is 1 (e.g. "@@ -0,0 +1 @@").
|
||||
func declaredLength(match string) int {
|
||||
if match == "" {
|
||||
return 1
|
||||
}
|
||||
return utils.MustConvertToInt(match)
|
||||
}
|
||||
|
||||
func newHunkLine(line string) *PatchLine {
|
||||
|
||||
+69
-14
@@ -79,6 +79,44 @@ func (self *Patch) HunkEndIdx(hunkIndex int) int {
|
||||
return self.HunkStartIdx(hunkIndex) + self.hunks[hunkIndex].lineCount() - 1
|
||||
}
|
||||
|
||||
// IsWellFormed reports whether every hunk's body matches the lengths declared in
|
||||
// its header. A faithful unified diff always satisfies this; a rendering that
|
||||
// restructured the diff body does not — a diff renderer that puts line numbers in
|
||||
// a gutter, say, shifts the +/- marker off the start of each line, so every body
|
||||
// line reads as context and the computed lengths no longer match the header. That
|
||||
// makes this the test for whether a rendered diff can be parsed as a unified diff
|
||||
// at all, rather than trusting a mis-parse. Only meaningful for patches produced
|
||||
// by Parse, which is where the declared lengths come from.
|
||||
func (self *Patch) IsWellFormed() bool {
|
||||
return self.isWellFormed(false)
|
||||
}
|
||||
|
||||
// IsWellFormedSoFar is IsWellFormed for a patch parsed from a diff we have only the
|
||||
// beginning of. Its last hunk holds the first lines of a body that hasn't all arrived,
|
||||
// so every hunk but the last has to match its header exactly, as before, while the last
|
||||
// one only has to fit within what its header declares.
|
||||
//
|
||||
// The check exists to tell a faithful rendering from a restructured one, and it still
|
||||
// does that. A rendering that moves the +/- marker off the start of the line makes us
|
||||
// read a change as context, and a context line counts towards both lengths, so such a
|
||||
// hunk comes out longer than its header declares rather than shorter.
|
||||
func (self *Patch) IsWellFormedSoFar() bool {
|
||||
return self.isWellFormed(true)
|
||||
}
|
||||
|
||||
func (self *Patch) isWellFormed(lastHunkMayBeIncomplete bool) bool {
|
||||
for i, hunk := range self.hunks {
|
||||
if lastHunkMayBeIncomplete && i == len(self.hunks)-1 {
|
||||
return hunk.oldLength() <= hunk.declaredOldLength &&
|
||||
hunk.newLength() <= hunk.declaredNewLength
|
||||
}
|
||||
if hunk.oldLength() != hunk.declaredOldLength || hunk.newLength() != hunk.declaredNewLength {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (self *Patch) ContainsChanges() bool {
|
||||
return lo.SomeBy(self.hunks, func(hunk *Hunk) bool {
|
||||
return hunk.containsChanges()
|
||||
@@ -114,6 +152,37 @@ func (self *Patch) LineNumberOfLine(idx int) int {
|
||||
return hunk.newStart + offset
|
||||
}
|
||||
|
||||
// Takes a line index in the patch and returns the line number in the old file.
|
||||
// This is the old-file counterpart of LineNumberOfLine; for a deletion it gives
|
||||
// the line's position in the old file (additions get the position they sit at).
|
||||
// If the line is a header line, returns 1.
|
||||
// If the line is a hunk header line, returns the first old-file line number in that hunk.
|
||||
// If the line is out of range below, returns the last old-file line number in the last hunk.
|
||||
func (self *Patch) OldLineNumberOfLine(idx int) int {
|
||||
if idx < len(self.header) || len(self.hunks) == 0 {
|
||||
return 1
|
||||
}
|
||||
|
||||
hunkIdx := self.HunkContainingLine(idx)
|
||||
// cursor out of range, return last file line number
|
||||
if hunkIdx == -1 {
|
||||
lastHunk := self.hunks[len(self.hunks)-1]
|
||||
return lastHunk.oldStart + lastHunk.oldLength() - 1
|
||||
}
|
||||
|
||||
hunk := self.hunks[hunkIdx]
|
||||
hunkStartIdx := self.HunkStartIdx(hunkIdx)
|
||||
idxInHunk := idx - hunkStartIdx
|
||||
|
||||
if idxInHunk == 0 {
|
||||
return hunk.oldStart
|
||||
}
|
||||
|
||||
lines := hunk.bodyLines[:idxInHunk-1]
|
||||
offset := nLinesWithKind(lines, []PatchLineKind{DELETION, CONTEXT})
|
||||
return hunk.oldStart + offset
|
||||
}
|
||||
|
||||
// Returns hunk index containing the line at the given patch line index
|
||||
func (self *Patch) HunkContainingLine(idx int) int {
|
||||
for hunkIdx, hunk := range self.hunks {
|
||||
@@ -196,17 +265,3 @@ func (self *Patch) AdjustLineNumber(lineNumber int) int {
|
||||
|
||||
return adjustedLineNumber
|
||||
}
|
||||
|
||||
func (self *Patch) IsSingleHunkForWholeFile() bool {
|
||||
if len(self.hunks) != 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
// We consider a patch to be a single hunk for the whole file if it has only additions or
|
||||
// deletions but not both, and no context lines. This not quite correct, because it will also
|
||||
// return true for a block of added or deleted lines if the diff context size is 0, but in this
|
||||
// case you wouldn't be able to stage things anyway, so it doesn't matter.
|
||||
bodyLines := self.hunks[0].bodyLines
|
||||
return nLinesWithKind(bodyLines, []PatchLineKind{DELETION, CONTEXT}) == 0 ||
|
||||
nLinesWithKind(bodyLines, []PatchLineKind{ADDITION, CONTEXT}) == 0
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/maps"
|
||||
"github.com/jesseduffield/generics/set"
|
||||
"github.com/samber/lo"
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -33,7 +35,7 @@ type fileInfo struct {
|
||||
}
|
||||
|
||||
type (
|
||||
loadFileDiffFunc func(from string, to string, reverse bool, filename string, previousPath string, plain bool) (string, error)
|
||||
loadFileDiffFunc func(from string, to string, reverse bool, filename string, previousPath string) (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
|
||||
@@ -60,12 +62,27 @@ type PatchBuilder struct {
|
||||
|
||||
// loadFileDiff loads the diff of a file, for a given to (typically a commit hash)
|
||||
loadFileDiff loadFileDiffFunc
|
||||
|
||||
// newTempDir makes a directory for the current patch to be materialized into, as
|
||||
// two file trees that can be diffed against each other and so rendered like any
|
||||
// other diff (see PatchCommands.WriteCustomPatchDiffTrees). Its lifetime is the
|
||||
// patch's: made when one is started, removed when it is given up.
|
||||
newTempDir func() (string, error)
|
||||
tempDir string
|
||||
|
||||
// generation counts the changes made to the patch, so that whoever materializes it
|
||||
// can tell whether what they last built still describes it — and rebuild only then,
|
||||
// rather than on every render of it.
|
||||
generation int
|
||||
}
|
||||
|
||||
func NewPatchBuilder(log *logrus.Entry, loadFileDiff loadFileDiffFunc) *PatchBuilder {
|
||||
func NewPatchBuilder(
|
||||
log *logrus.Entry, loadFileDiff loadFileDiffFunc, newTempDir func() (string, error),
|
||||
) *PatchBuilder {
|
||||
return &PatchBuilder{
|
||||
Log: log,
|
||||
loadFileDiff: loadFileDiff,
|
||||
newTempDir: newTempDir,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +90,9 @@ func (p *PatchBuilder) Start(from, to string, reverse bool, canRebase bool) {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
p.generation++
|
||||
p.makeTempDir()
|
||||
|
||||
p.To = to
|
||||
p.From = from
|
||||
p.reverse = reverse
|
||||
@@ -91,6 +111,89 @@ func (p *PatchBuilder) snapshotFileInfoMap() map[string]*fileInfo {
|
||||
return p.fileInfoMap
|
||||
}
|
||||
|
||||
// TempDir is the directory the patch is materialized into for rendering, and "" when
|
||||
// there is none — no patch, or a directory we failed to make.
|
||||
func (p *PatchBuilder) TempDir() string {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
return p.tempDir
|
||||
}
|
||||
|
||||
// Generation says which version of the patch this is; see the field.
|
||||
func (p *PatchBuilder) Generation() int {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
return p.generation
|
||||
}
|
||||
|
||||
// makeTempDir replaces the directory the patch is materialized into with a fresh one.
|
||||
// Only call this with the lock held.
|
||||
func (p *PatchBuilder) makeTempDir() {
|
||||
p.removeTempDir()
|
||||
if p.newTempDir == nil {
|
||||
return
|
||||
}
|
||||
dir, err := p.newTempDir()
|
||||
if err != nil {
|
||||
p.Log.Error(err)
|
||||
return
|
||||
}
|
||||
p.tempDir = dir
|
||||
}
|
||||
|
||||
// removeTempDir takes the patch's materialized form away with the patch. Only call this
|
||||
// with the lock held.
|
||||
func (p *PatchBuilder) removeTempDir() {
|
||||
if p.tempDir == "" {
|
||||
return
|
||||
}
|
||||
if err := os.RemoveAll(p.tempDir); err != nil {
|
||||
p.Log.Error(err)
|
||||
}
|
||||
p.tempDir = ""
|
||||
}
|
||||
|
||||
// PatchFile records what materializing the patch needs to know about one of its files:
|
||||
// where the patch expects to find it, and where its content before the patch comes from.
|
||||
type PatchFile struct {
|
||||
// Path is the name the patch knows the file by: for a renamed file, the name it had
|
||||
// before where the patch carries the rename, and the name it was renamed to where the
|
||||
// patch keeps only a content change and leaves the rename behind.
|
||||
Path string
|
||||
// ContentPath is where the file's content before the patch is to be found in the
|
||||
// commit the patch is built from — for a renamed file always the name it had there,
|
||||
// whatever the patch calls it.
|
||||
ContentPath string
|
||||
}
|
||||
|
||||
// FilesInPatch says which files the patch touches, in a stable order, and where each of
|
||||
// them comes from.
|
||||
func (p *PatchBuilder) FilesInPatch() []PatchFile {
|
||||
fileInfoMap := p.snapshotFileInfoMap()
|
||||
|
||||
filenames := maps.Keys(fileInfoMap)
|
||||
sort.Strings(filenames)
|
||||
|
||||
files := make([]PatchFile, 0, len(filenames))
|
||||
for _, filename := range filenames {
|
||||
info := fileInfoMap[filename]
|
||||
if info.mode == UNSELECTED {
|
||||
continue
|
||||
}
|
||||
file := PatchFile{Path: filename, ContentPath: filename}
|
||||
if info.previousPath != "" {
|
||||
file.ContentPath = info.previousPath
|
||||
if info.mode == WHOLE {
|
||||
file.Path = info.previousPath
|
||||
}
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
func (p *PatchBuilder) PatchToApply(reverse bool, turnAddedFilesIntoDiffAgainstEmptyFile bool) string {
|
||||
var patch strings.Builder
|
||||
|
||||
@@ -135,6 +238,7 @@ func (p *PatchBuilder) AddFileWhole(filename string, previousPath string) error
|
||||
return err
|
||||
}
|
||||
|
||||
p.generation++
|
||||
p.addFileWhole(info)
|
||||
|
||||
return nil
|
||||
@@ -146,6 +250,7 @@ func (p *PatchBuilder) RemoveFile(filename string, previousPath string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
p.generation++
|
||||
p.removeFile(info)
|
||||
|
||||
return nil
|
||||
@@ -162,7 +267,7 @@ func (p *PatchBuilder) getFileInfo(filename string, previousPath string) (*fileI
|
||||
return info, nil
|
||||
}
|
||||
|
||||
diff, err := p.loadFileDiff(from, to, reverse, filename, previousPath, true)
|
||||
diff, err := p.loadFileDiff(from, to, reverse, filename, previousPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -182,6 +287,7 @@ func (p *PatchBuilder) AddFileLineRange(filename string, previousPath string, li
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.generation++
|
||||
info.mode = PART
|
||||
info.includedLineIndices = lo.Union(info.includedLineIndices, lineIndices)
|
||||
|
||||
@@ -193,6 +299,7 @@ func (p *PatchBuilder) RemoveFileLineRange(filename string, previousPath string,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.generation++
|
||||
info.mode = PART
|
||||
info.includedLineIndices, _ = lo.Difference(info.includedLineIndices, lineIndices)
|
||||
if len(info.includedLineIndices) == 0 {
|
||||
@@ -291,6 +398,110 @@ func (p *PatchBuilder) GetFileStatus(filename string, parent string) PatchStatus
|
||||
return info.mode
|
||||
}
|
||||
|
||||
// LineIdentity says which change line of a file is meant — the line number it has on
|
||||
// the side it belongs to, and whether it is a deletion — without reference to where
|
||||
// that line sits in the file's parsed diff.
|
||||
//
|
||||
// It is how a diff shown in the main view speaks about its lines: what a rendered row
|
||||
// resolves to is a line of a file, while the index of that line in the diff depends on
|
||||
// how much of the diff is being shown and in what order a renderer laid it out.
|
||||
type LineIdentity struct {
|
||||
LineNumber int
|
||||
IsDeletion bool
|
||||
}
|
||||
|
||||
// ChangeLineIndexByIdentity indexes a parsed diff's change lines by their identity. An
|
||||
// addition is numbered in the new file and a deletion in the old one. Two consecutive
|
||||
// deletions share the one new-file position between them, and numbering them in the
|
||||
// old file keeps them apart.
|
||||
func ChangeLineIndexByIdentity(parsed *Patch) map[LineIdentity]int {
|
||||
byIdentity := map[LineIdentity]int{}
|
||||
for idx, line := range parsed.Lines() {
|
||||
switch {
|
||||
case line.IsAddition():
|
||||
byIdentity[LineIdentity{parsed.LineNumberOfLine(idx), false}] = idx
|
||||
case line.IsDeletion():
|
||||
byIdentity[LineIdentity{parsed.OldLineNumberOfLine(idx), true}] = idx
|
||||
}
|
||||
}
|
||||
return byIdentity
|
||||
}
|
||||
|
||||
// ChangeLineIndicesForLines maps the given change lines of a parsed diff to their
|
||||
// indices in it. A line that names no change line of the diff — a context line, or a
|
||||
// line that isn't in the diff at all — contributes nothing.
|
||||
func ChangeLineIndicesForLines(parsed *Patch, lines []LineIdentity) []int {
|
||||
byIdentity := ChangeLineIndexByIdentity(parsed)
|
||||
indices := make([]int, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
if idx, ok := byIdentity[line]; ok {
|
||||
indices = append(indices, idx)
|
||||
}
|
||||
}
|
||||
return indices
|
||||
}
|
||||
|
||||
// PatchLineIndicesForLines maps change lines of filename to their indices in that
|
||||
// file's diff; the patch is built in terms of those indices. everyChange reports
|
||||
// whether the given lines cover all of the file's changes; this distinguishes acting
|
||||
// on some of a file's lines from acting on the file itself.
|
||||
func (p *PatchBuilder) PatchLineIndicesForLines(
|
||||
filename string, previousPath string, lines []LineIdentity,
|
||||
) (indices []int, everyChange bool, err error) {
|
||||
info, err := p.getFileInfo(filename, previousPath)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
parsed := Parse(info.diff)
|
||||
selected := set.NewFromSlice(lines)
|
||||
everyChange = lo.EveryBy(maps.Keys(ChangeLineIndexByIdentity(parsed)),
|
||||
func(identity LineIdentity) bool { return selected.Includes(identity) })
|
||||
return ChangeLineIndicesForLines(parsed, lines), everyChange, nil
|
||||
}
|
||||
|
||||
// IncludedLineIdentities says which change lines of filename are in the patch, as the
|
||||
// identities a diff of that file shown anywhere can be compared against. Empty for a
|
||||
// file that is no part of the patch.
|
||||
func (p *PatchBuilder) IncludedLineIdentities(filename string) []LineIdentity {
|
||||
info, ok := p.snapshotFileInfoMap()[filename]
|
||||
if !ok || info.mode == UNSELECTED {
|
||||
return nil
|
||||
}
|
||||
|
||||
included := set.NewFromSlice(info.includedLineIndices)
|
||||
identities := []LineIdentity{}
|
||||
for identity, idx := range ChangeLineIndexByIdentity(Parse(info.diff)) {
|
||||
if included.Includes(idx) {
|
||||
identities = append(identities, identity)
|
||||
}
|
||||
}
|
||||
return identities
|
||||
}
|
||||
|
||||
// IncludedChangeLineIndices says which of filename's change lines are in the patch, as
|
||||
// their indices in the file's diff and in the order the file has them.
|
||||
//
|
||||
// It is how a line of the patch as it is shown names the line of the diff it came from:
|
||||
// all that can be said about a line of the patch is which of the file's changes it is,
|
||||
// its line numbers being the patch's own — a patch that leaves an earlier addition out
|
||||
// numbers everything after it differently from the diff it was built from.
|
||||
func (p *PatchBuilder) IncludedChangeLineIndices(filename string) []int {
|
||||
info, ok := p.snapshotFileInfoMap()[filename]
|
||||
if !ok || info.mode == UNSELECTED {
|
||||
return nil
|
||||
}
|
||||
|
||||
included := set.NewFromSlice(info.includedLineIndices)
|
||||
indices := []int{}
|
||||
for idx, line := range Parse(info.diff).Lines() {
|
||||
if (line.IsAddition() || line.IsDeletion()) && included.Includes(idx) {
|
||||
indices = append(indices, idx)
|
||||
}
|
||||
}
|
||||
return indices
|
||||
}
|
||||
|
||||
func (p *PatchBuilder) GetFileIncLineIndices(filename string, previousPath string) ([]int, error) {
|
||||
info, err := p.getFileInfo(filename, previousPath)
|
||||
if err != nil {
|
||||
@@ -304,6 +515,9 @@ func (p *PatchBuilder) Reset() {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
p.generation++
|
||||
p.removeTempDir()
|
||||
|
||||
p.To = ""
|
||||
p.fileInfoMap = map[string]*fileInfo{}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// newTestPatchBuilder returns a patch builder started for a dummy commit, in which
|
||||
// every file's diff is the given one.
|
||||
func newTestPatchBuilder(diff string) *PatchBuilder {
|
||||
patchBuilder := NewPatchBuilder(logrus.New().WithField("test", "test"),
|
||||
func(from string, to string, reverse bool, filename string, previousPath string) (string, error) {
|
||||
return diff, nil
|
||||
},
|
||||
// Nothing here renders the patch, so it needs no directory to be
|
||||
// materialized into.
|
||||
nil)
|
||||
patchBuilder.Start("from", "to", false, true)
|
||||
return patchBuilder
|
||||
}
|
||||
|
||||
// In simpleDiff the deletion "-orange" is line index 6 of the parsed diff (line 2 of
|
||||
// the old file) and the addition "+grape" is index 7 (line 2 of the new file).
|
||||
func TestPatchLineIndicesForLines(t *testing.T) {
|
||||
patchBuilder := newTestPatchBuilder(simpleDiff)
|
||||
|
||||
indices, everyChange, err := patchBuilder.PatchLineIndicesForLines("filename", "", []LineIdentity{
|
||||
{LineNumber: 2, IsDeletion: true}, // -orange
|
||||
{LineNumber: 2, IsDeletion: false}, // +grape
|
||||
{LineNumber: 1, IsDeletion: false}, // " apple", a context line
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{6, 7}, indices, "the context line names no change line")
|
||||
assert.True(t, everyChange, "the two changes are all the diff has")
|
||||
}
|
||||
|
||||
// A renamed file's rename header makes its change lines sit further down the diff, and
|
||||
// its old-file line numbers are of the file under its previous name.
|
||||
func TestPatchLineIndicesForLinesOfARenamedFile(t *testing.T) {
|
||||
patchBuilder := newTestPatchBuilder(renameWithModificationDiff)
|
||||
|
||||
indices, everyChange, err := patchBuilder.PatchLineIndicesForLines("newname", "oldname", []LineIdentity{
|
||||
{LineNumber: 2, IsDeletion: true}, // -orange
|
||||
{LineNumber: 2, IsDeletion: false}, // +grape
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{9, 10}, indices)
|
||||
assert.True(t, everyChange)
|
||||
}
|
||||
|
||||
// everyChange is about the diff alone: whether anything the file changes was left out
|
||||
// of the selection. What that then means for the patch is the caller's question.
|
||||
func TestPatchLineIndicesForLinesEveryChange(t *testing.T) {
|
||||
patchBuilder := newTestPatchBuilder(newFile)
|
||||
|
||||
_, everyChange, err := patchBuilder.PatchLineIndicesForLines("newfile", "", []LineIdentity{
|
||||
{LineNumber: 1},
|
||||
{LineNumber: 2},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, everyChange, "the file's third added line is left out")
|
||||
|
||||
_, everyChange, err = patchBuilder.PatchLineIndicesForLines("newfile", "", []LineIdentity{
|
||||
{LineNumber: 1},
|
||||
{LineNumber: 2},
|
||||
{LineNumber: 3},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, everyChange)
|
||||
|
||||
// A line the diff doesn't have doesn't stand in for one it does.
|
||||
patchBuilder = newTestPatchBuilder(deletedFile)
|
||||
_, everyChange, err = patchBuilder.PatchLineIndicesForLines("newfile", "", []LineIdentity{
|
||||
{LineNumber: 1, IsDeletion: true},
|
||||
{LineNumber: 2, IsDeletion: true},
|
||||
{LineNumber: 4, IsDeletion: true},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, everyChange)
|
||||
}
|
||||
|
||||
func TestIncludedLineIdentities(t *testing.T) {
|
||||
patchBuilder := newTestPatchBuilder(simpleDiff)
|
||||
|
||||
// A file no part of the patch has nothing included.
|
||||
assert.Empty(t, patchBuilder.IncludedLineIdentities("filename"))
|
||||
|
||||
// With only the deletion in, only its identity comes back.
|
||||
assert.NoError(t, patchBuilder.AddFileLineRange("filename", "", []int{6}))
|
||||
assert.Equal(t,
|
||||
[]LineIdentity{{LineNumber: 2, IsDeletion: true}},
|
||||
patchBuilder.IncludedLineIdentities("filename"))
|
||||
|
||||
// With the addition in as well, both do.
|
||||
assert.NoError(t, patchBuilder.AddFileLineRange("filename", "", []int{7}))
|
||||
assert.ElementsMatch(t,
|
||||
[]LineIdentity{{LineNumber: 2, IsDeletion: true}, {LineNumber: 2, IsDeletion: false}},
|
||||
patchBuilder.IncludedLineIdentities("filename"))
|
||||
}
|
||||
|
||||
func TestFilesInPatch(t *testing.T) {
|
||||
patchBuilder := newTestPatchBuilder(simpleDiff)
|
||||
|
||||
// A file no part of the patch is no part of what the patch is materialized from.
|
||||
assert.Empty(t, patchBuilder.FilesInPatch())
|
||||
|
||||
assert.NoError(t, patchBuilder.AddFileLineRange("filename", "", []int{6}))
|
||||
assert.Equal(t,
|
||||
[]PatchFile{{Path: "filename", ContentPath: "filename"}},
|
||||
patchBuilder.FilesInPatch())
|
||||
}
|
||||
|
||||
// A renamed file's content is under the name it had before whatever the patch calls the
|
||||
// file, and the patch calls it by the name it had before only where it carries the
|
||||
// rename — a partial selection has the rename stripped and names the file by the new one.
|
||||
func TestFilesInPatchOfARenamedFile(t *testing.T) {
|
||||
patchBuilder := newTestPatchBuilder(renameWithModificationDiff)
|
||||
|
||||
assert.NoError(t, patchBuilder.AddFileLineRange("newname", "oldname", []int{9}))
|
||||
assert.Equal(t,
|
||||
[]PatchFile{{Path: "newname", ContentPath: "oldname"}},
|
||||
patchBuilder.FilesInPatch())
|
||||
|
||||
assert.NoError(t, patchBuilder.AddFileWhole("newname", "oldname"))
|
||||
assert.Equal(t,
|
||||
[]PatchFile{{Path: "oldname", ContentPath: "oldname"}},
|
||||
patchBuilder.FilesInPatch())
|
||||
}
|
||||
|
||||
// A file taken into the patch whole has every one of its change lines in it.
|
||||
func TestIncludedLineIdentitiesOfAWholeFile(t *testing.T) {
|
||||
patchBuilder := newTestPatchBuilder(simpleDiff)
|
||||
|
||||
assert.NoError(t, patchBuilder.AddFileWhole("filename", ""))
|
||||
assert.ElementsMatch(t,
|
||||
[]LineIdentity{{LineNumber: 2, IsDeletion: true}, {LineNumber: 2, IsDeletion: false}},
|
||||
patchBuilder.IncludedLineIdentities("filename"))
|
||||
}
|
||||
@@ -120,6 +120,20 @@ index 9320895..6d79956 100644
|
||||
lemon
|
||||
`
|
||||
|
||||
// Two deletions with no line between them: they share a new-file line number
|
||||
// (both sit at the same new-file position), so only their old-file line numbers
|
||||
// tell them apart.
|
||||
const consecutiveDeletions = `diff --git a/filename b/filename
|
||||
index 9320895..6d79956 100644
|
||||
--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,4 +1,2 @@
|
||||
apple
|
||||
-grape
|
||||
-pear
|
||||
lemon
|
||||
`
|
||||
|
||||
const newFile = `diff --git a/newfile b/newfile
|
||||
new file mode 100644
|
||||
index 0000000..4e680cc
|
||||
@@ -682,6 +696,148 @@ func TestLineNumberOfLine(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsWellFormed(t *testing.T) {
|
||||
// The body of a diff as rendered with the +/- markers moved out of the text
|
||||
// and into a gutter: every body line now reads as context, so the lengths no
|
||||
// longer match the header.
|
||||
const gutterMangled = `diff --git a/filename b/filename
|
||||
index 9320895..6d79956 100644
|
||||
--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,4 +1,2 @@
|
||||
apple
|
||||
grape
|
||||
pear
|
||||
lemon
|
||||
`
|
||||
|
||||
scenarios := []struct {
|
||||
testName string
|
||||
patchStr string
|
||||
expected bool
|
||||
}{
|
||||
{"simpleDiff", simpleDiff, true},
|
||||
{"renameWithModificationDiff", renameWithModificationDiff, true},
|
||||
{"addNewlineToEndOfFile", addNewlineToEndOfFile, true},
|
||||
{"twoHunks", twoHunks, true},
|
||||
{"consecutiveDeletions", consecutiveDeletions, true},
|
||||
{"newFile", newFile, true},
|
||||
{"deletedFile", deletedFile, true},
|
||||
{"addNewlineToPreviouslyEmptyFile", addNewlineToPreviouslyEmptyFile, true},
|
||||
{"exampleHunk", exampleHunk, true},
|
||||
{"gutterMangled", gutterMangled, false},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
assert.Equal(t, s.expected, Parse(s.patchStr).IsWellFormed())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsWellFormedSoFar(t *testing.T) {
|
||||
// A diff read only as far as the middle of its second hunk.
|
||||
const cutShort = `diff --git a/filename b/filename
|
||||
index e48a11c..b2ab81b 100644
|
||||
--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,5 @@
|
||||
apple
|
||||
-grape
|
||||
+orange
|
||||
...
|
||||
...
|
||||
...
|
||||
@@ -8,6 +8,8 @@ grape
|
||||
...
|
||||
...
|
||||
`
|
||||
|
||||
// The same diff cut short in its first hunk, so that the second is missing
|
||||
// entirely rather than short.
|
||||
const cutShortInTheFirstHunk = `diff --git a/filename b/filename
|
||||
index e48a11c..b2ab81b 100644
|
||||
--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,5 @@
|
||||
apple
|
||||
-grape
|
||||
`
|
||||
|
||||
// A rendering with the +/- markers moved into a gutter, cut short: reading the
|
||||
// changes as context makes the hunk longer than its header declares, not shorter,
|
||||
// so it doesn't pass for a diff we only have the beginning of.
|
||||
const gutterMangledAndCutShort = `diff --git a/filename b/filename
|
||||
index 9320895..6d79956 100644
|
||||
--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,4 +1,2 @@
|
||||
apple
|
||||
grape
|
||||
pear
|
||||
lemon
|
||||
melon
|
||||
`
|
||||
|
||||
scenarios := []struct {
|
||||
testName string
|
||||
patchStr string
|
||||
expected bool
|
||||
}{
|
||||
{"simpleDiff", simpleDiff, true},
|
||||
{"twoHunks", twoHunks, true},
|
||||
{"cutShort", cutShort, true},
|
||||
{"cutShortInTheFirstHunk", cutShortInTheFirstHunk, true},
|
||||
{"gutterMangledAndCutShort", gutterMangledAndCutShort, false},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
assert.Equal(t, s.expected, Parse(s.patchStr).IsWellFormedSoFar())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOldLineNumberOfLine(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
patchStr string
|
||||
indexes []int
|
||||
expecteds []int
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "twoChangesInOneHunk",
|
||||
patchStr: twoChangesInOneHunk,
|
||||
indexes: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 1000},
|
||||
expecteds: []int{1, 1, 1, 1, 1, 1, 2, 3, 3, 4, 5, 5, 5},
|
||||
},
|
||||
{
|
||||
testName: "consecutiveDeletions",
|
||||
patchStr: consecutiveDeletions,
|
||||
indexes: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 1000},
|
||||
expecteds: []int{1, 1, 1, 1, 1, 1, 2, 3, 4, 4},
|
||||
},
|
||||
{
|
||||
testName: "renameWithModificationDiff",
|
||||
patchStr: renameWithModificationDiff,
|
||||
indexes: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 1000},
|
||||
expecteds: []int{1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, 3, 4, 5, 5},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
for i, idx := range s.indexes {
|
||||
patch := Parse(s.patchStr)
|
||||
result := patch.OldLineNumberOfLine(idx)
|
||||
assert.Equal(t, s.expecteds[i], result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNextStageableLineIndex(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
@@ -765,64 +921,3 @@ func TestAdjustLineNumber(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsSingleHunkForWholeFile(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
testName string
|
||||
patchStr string
|
||||
expectedResult bool
|
||||
}{
|
||||
{
|
||||
testName: "simpleDiff",
|
||||
patchStr: simpleDiff,
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
testName: "addNewlineToEndOfFile",
|
||||
patchStr: addNewlineToEndOfFile,
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
testName: "removeNewlinefromEndOfFile",
|
||||
patchStr: removeNewlinefromEndOfFile,
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
testName: "twoHunks",
|
||||
patchStr: twoHunks,
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
testName: "twoChangesInOneHunk",
|
||||
patchStr: twoChangesInOneHunk,
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
testName: "newFile",
|
||||
patchStr: newFile,
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
testName: "deletedFile",
|
||||
patchStr: deletedFile,
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
testName: "addNewlineToPreviouslyEmptyFile",
|
||||
patchStr: addNewlineToPreviouslyEmptyFile,
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
testName: "exampleHunk",
|
||||
patchStr: exampleHunk,
|
||||
expectedResult: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
patch := Parse(s.patchStr)
|
||||
assert.Equal(t, s.expectedResult, patch.IsSingleHunkForWholeFile())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -296,6 +297,8 @@ func computeMigratedConfig(path string, content []byte, changes *ChangesSet) ([]
|
||||
{[]string{"keybinding", "universal", "cyclePagers"}, "cycleDiffRenderers"},
|
||||
{[]string{"keybinding", "universal", "cyclePagersReverse"}, "cycleDiffRenderersReverse"},
|
||||
{[]string{"gui", "windowSize"}, "screenMode"},
|
||||
{[]string{"gui", "wrapLinesInStagingView"}, "wrapLinesInDiffView"},
|
||||
{[]string{"gui", "useHunkModeInStagingView"}, "useHunkModeInDiffView"},
|
||||
{[]string{"keybinding", "files", "openMergeTool"}, "openMergeOptions"},
|
||||
}
|
||||
|
||||
@@ -309,6 +312,13 @@ func computeMigratedConfig(path string, content []byte, changes *ChangesSet) ([]
|
||||
}
|
||||
}
|
||||
|
||||
// This creates gui.branchColorPatterns, so it must run before the move of
|
||||
// that key into gui.theme below.
|
||||
err = migrateBranchColors(&rootNode, changes)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("Couldn't migrate config file at `%s`: %w", path, err)
|
||||
}
|
||||
|
||||
pathsToMove := []struct {
|
||||
oldPath []string
|
||||
newPath []string
|
||||
@@ -317,6 +327,14 @@ func computeMigratedConfig(path string, content []byte, changes *ChangesSet) ([]
|
||||
[]string{"keybinding", "worktrees", "viewWorktreeOptions"},
|
||||
[]string{"keybinding", "universal", "newWorktree"},
|
||||
},
|
||||
{
|
||||
[]string{"gui", "authorColors"},
|
||||
[]string{"gui", "theme", "authorColors"},
|
||||
},
|
||||
{
|
||||
[]string{"gui", "branchColorPatterns"},
|
||||
[]string{"gui", "theme", "branchColorPatterns"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, pathToMove := range pathsToMove {
|
||||
@@ -630,6 +648,43 @@ func migratePagersToDiffRenderers(rootNode *yaml.Node, changes *ChangesSet) erro
|
||||
})
|
||||
}
|
||||
|
||||
// The deprecated gui.branchColors matched its keys against the part of a branch
|
||||
// name before the first slash. Turn each key into a pattern that matches the
|
||||
// same branches. If the file has a non-empty gui.branchColorPatterns,
|
||||
// gui.branchColors was ignored, so remove it.
|
||||
func migrateBranchColors(rootNode *yaml.Node, changes *ChangesSet) error {
|
||||
return yaml_utils.TransformNode(rootNode, []string{"gui"}, func(guiNode *yaml.Node) error {
|
||||
branchColorsKeyNode, branchColorsValueNode := yaml_utils.LookupKey(guiNode, "branchColors")
|
||||
if branchColorsKeyNode == nil || branchColorsValueNode.Kind != yaml.MappingNode {
|
||||
return nil
|
||||
}
|
||||
|
||||
patternsKeyNode, patternsValueNode := yaml_utils.LookupKey(guiNode, "branchColorPatterns")
|
||||
if patternsKeyNode != nil {
|
||||
switch {
|
||||
case patternsValueNode.Kind == yaml.MappingNode && len(patternsValueNode.Content) > 0:
|
||||
yaml_utils.RemoveKey(guiNode, "branchColors")
|
||||
changes.Add("Removed 'gui.branchColors'; it had no effect because 'gui.branchColorPatterns' is set")
|
||||
return nil
|
||||
case patternsValueNode.Kind == yaml.MappingNode || patternsValueNode.Tag == "!!null":
|
||||
yaml_utils.RemoveKey(guiNode, "branchColorPatterns")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
branchColorsKeyNode.Value = "branchColorPatterns"
|
||||
for i := 0; i < len(branchColorsValueNode.Content)-1; i += 2 {
|
||||
keyNode := branchColorsValueNode.Content[i]
|
||||
keyNode.Value = "^" + regexp.QuoteMeta(keyNode.Value) + "(/|$)"
|
||||
keyNode.Tag = "!!str"
|
||||
}
|
||||
changes.Add("Converted 'gui.branchColors' to 'gui.branchColorPatterns'")
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func hasNonNullScalarValue(node *yaml.Node) bool {
|
||||
return node != nil && node.Kind == yaml.ScalarNode && node.Tag != "!!null" && node.Value != ""
|
||||
}
|
||||
@@ -845,11 +900,10 @@ func (c *AppConfig) SaveGlobalUserConfig() {
|
||||
// AppState stores data between runs of the app like when the last update check
|
||||
// was performed and which other repos have been checked out
|
||||
type AppState struct {
|
||||
LastUpdateCheck int64
|
||||
RecentRepos []string
|
||||
StartupPopupVersion int
|
||||
DidShowHunkStagingHint bool
|
||||
LastVersion string // this is the last version the user was using, for the purpose of showing release notes
|
||||
LastUpdateCheck int64
|
||||
RecentRepos []string
|
||||
StartupPopupVersion int
|
||||
LastVersion string // this is the last version the user was using, for the purpose of showing release notes
|
||||
|
||||
// these are for shell commands typed in directly, not for custom commands in the lazygit config.
|
||||
// For backwards compatibility we keep the old name in yaml files.
|
||||
|
||||
@@ -108,6 +108,22 @@ func TestMigrationOfRenamedKeys(t *testing.T) {
|
||||
"Renamed 'gui.windowSize' to 'screenMode'",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Rename staging view options",
|
||||
input: `gui:
|
||||
wrapLinesInStagingView: false
|
||||
useHunkModeInStagingView: true
|
||||
`,
|
||||
expected: `gui:
|
||||
wrapLinesInDiffView: false
|
||||
useHunkModeInDiffView: true
|
||||
`,
|
||||
expectedDidChange: true,
|
||||
expectedChanges: []string{
|
||||
"Renamed 'gui.wrapLinesInStagingView' to 'wrapLinesInDiffView'",
|
||||
"Renamed 'gui.useHunkModeInStagingView' to 'useHunkModeInDiffView'",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
@@ -175,6 +191,32 @@ func TestMigrationOfMovedKeys(t *testing.T) {
|
||||
expectedDidChange: true,
|
||||
expectedChanges: []string{"Moved 'keybinding.worktrees.viewWorktreeOptions' to 'keybinding.universal.newWorktree'"},
|
||||
},
|
||||
{
|
||||
name: "Move author and branch colors into the theme",
|
||||
input: `gui:
|
||||
authorColors:
|
||||
John Smith: red
|
||||
theme:
|
||||
activeBorderColor:
|
||||
- green
|
||||
branchColorPatterns:
|
||||
^docs/: blue
|
||||
`,
|
||||
expected: `gui:
|
||||
theme:
|
||||
activeBorderColor:
|
||||
- green
|
||||
authorColors:
|
||||
John Smith: red
|
||||
branchColorPatterns:
|
||||
^docs/: blue
|
||||
`,
|
||||
expectedDidChange: true,
|
||||
expectedChanges: []string{
|
||||
"Moved 'gui.authorColors' to 'gui.theme.authorColors'",
|
||||
"Moved 'gui.branchColorPatterns' to 'gui.theme.branchColorPatterns'",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
@@ -821,3 +863,119 @@ func TestPagerMigration(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBranchColorsMigration(t *testing.T) {
|
||||
moved := "Moved 'gui.branchColorPatterns' to 'gui.theme.branchColorPatterns'"
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
expectedDidChange bool
|
||||
expectedChanges []string
|
||||
}{
|
||||
{
|
||||
name: "No branchColors",
|
||||
input: "gui:\n" +
|
||||
" theme:\n" +
|
||||
" branchColorPatterns:\n" +
|
||||
" '^docs/': blue\n",
|
||||
expectedDidChange: false,
|
||||
expectedChanges: []string{},
|
||||
},
|
||||
{
|
||||
name: "branchColors is not an object",
|
||||
input: "gui:\n" +
|
||||
" branchColors: 5\n",
|
||||
expectedDidChange: false,
|
||||
expectedChanges: []string{},
|
||||
},
|
||||
{
|
||||
name: "branchColors is converted to patterns",
|
||||
input: "gui:\n" +
|
||||
" scrollHeight: 2\n" +
|
||||
" branchColors:\n" +
|
||||
" feature: green\n" +
|
||||
" v1.x: '#ff0000'\n" +
|
||||
" 123: red\n" +
|
||||
" mouseEvents: false\n",
|
||||
expected: "gui:\n" +
|
||||
" scrollHeight: 2\n" +
|
||||
" mouseEvents: false\n" +
|
||||
" theme:\n" +
|
||||
" branchColorPatterns:\n" +
|
||||
" ^feature(/|$): green\n" +
|
||||
" ^v1\\.x(/|$): '#ff0000'\n" +
|
||||
" ^123(/|$): red\n",
|
||||
expectedDidChange: true,
|
||||
expectedChanges: []string{"Converted 'gui.branchColors' to 'gui.branchColorPatterns'", moved},
|
||||
},
|
||||
{
|
||||
name: "branchColors is removed if branchColorPatterns is set",
|
||||
input: "gui:\n" +
|
||||
" branchColors:\n" +
|
||||
" feature: green\n" +
|
||||
" branchColorPatterns:\n" +
|
||||
" '^docs/': blue\n",
|
||||
expected: "gui:\n" +
|
||||
" theme:\n" +
|
||||
" branchColorPatterns:\n" +
|
||||
" '^docs/': blue\n",
|
||||
expectedDidChange: true,
|
||||
expectedChanges: []string{"Removed 'gui.branchColors'; it had no effect because 'gui.branchColorPatterns' is set", moved},
|
||||
},
|
||||
{
|
||||
name: "branchColors replaces an empty branchColorPatterns",
|
||||
input: "gui:\n" +
|
||||
" branchColorPatterns: {}\n" +
|
||||
" branchColors:\n" +
|
||||
" feature: green\n",
|
||||
expected: "gui:\n" +
|
||||
" theme:\n" +
|
||||
" branchColorPatterns:\n" +
|
||||
" ^feature(/|$): green\n",
|
||||
expectedDidChange: true,
|
||||
expectedChanges: []string{"Converted 'gui.branchColors' to 'gui.branchColorPatterns'", moved},
|
||||
},
|
||||
{
|
||||
name: "branchColors replaces a null branchColorPatterns",
|
||||
input: "gui:\n" +
|
||||
" branchColorPatterns:\n" +
|
||||
" branchColors:\n" +
|
||||
" feature: green\n",
|
||||
expected: "gui:\n" +
|
||||
" theme:\n" +
|
||||
" branchColorPatterns:\n" +
|
||||
" ^feature(/|$): green\n",
|
||||
expectedDidChange: true,
|
||||
expectedChanges: []string{"Converted 'gui.branchColors' to 'gui.branchColorPatterns'", moved},
|
||||
},
|
||||
{
|
||||
name: "branchColors is kept if branchColorPatterns is not an object",
|
||||
input: "gui:\n" +
|
||||
" branchColorPatterns: 5\n" +
|
||||
" branchColors:\n" +
|
||||
" feature: green\n",
|
||||
expected: "gui:\n" +
|
||||
" branchColors:\n" +
|
||||
" feature: green\n" +
|
||||
" theme:\n" +
|
||||
" branchColorPatterns: 5\n",
|
||||
expectedDidChange: true,
|
||||
expectedChanges: []string{moved},
|
||||
},
|
||||
}
|
||||
|
||||
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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/karimkhaleel/jsonschema"
|
||||
"github.com/samber/lo"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// ColorPatterns assigns colors to the names that match regular expressions.
|
||||
// It's written in YAML as a mapping from pattern to color, and keeps the
|
||||
// patterns in the order in which they are written, so that the first pattern
|
||||
// that matches a name can decide its color.
|
||||
type ColorPatterns []ColorPattern
|
||||
|
||||
type ColorPattern struct {
|
||||
Pattern string
|
||||
Color string
|
||||
}
|
||||
|
||||
// UnmarshalYAML puts the patterns it reads in front of the ones that are there
|
||||
// already, which come from config files that were loaded earlier.
|
||||
func (p *ColorPatterns) UnmarshalYAML(node *yaml.Node) error {
|
||||
// Decoding into a map reports malformed input the same way as for the
|
||||
// other maps in the config.
|
||||
var colors map[string]string
|
||||
if err := node.Decode(&colors); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
patterns := make(ColorPatterns, 0, len(colors))
|
||||
for i := 0; i < len(node.Content)-1; i += 2 {
|
||||
var pattern string
|
||||
if err := node.Content[i].Decode(&pattern); err != nil {
|
||||
return err
|
||||
}
|
||||
patterns = append(patterns, ColorPattern{Pattern: pattern, Color: colors[pattern]})
|
||||
}
|
||||
|
||||
*p = patterns.over(*p)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p ColorPatterns) MarshalYAML() (any, error) {
|
||||
node := &yaml.Node{Kind: yaml.MappingNode}
|
||||
for _, pattern := range p {
|
||||
node.Content = append(node.Content,
|
||||
&yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: pattern.Pattern},
|
||||
&yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: pattern.Color},
|
||||
)
|
||||
}
|
||||
return node, nil
|
||||
}
|
||||
|
||||
// JSONSchema describes the patterns as the mapping they are written as.
|
||||
func (ColorPatterns) JSONSchema() *jsonschema.Schema {
|
||||
return &jsonschema.Schema{
|
||||
Type: "object",
|
||||
AdditionalProperties: &jsonschema.Schema{Type: "string"},
|
||||
}
|
||||
}
|
||||
|
||||
// over returns p followed by the patterns of lower that p doesn't have.
|
||||
func (p ColorPatterns) over(lower ColorPatterns) ColorPatterns {
|
||||
return slices.Concat(p, lo.Reject(lower, func(l ColorPattern, _ int) bool {
|
||||
return slices.ContainsFunc(p, func(c ColorPattern) bool { return c.Pattern == l.Pattern })
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type colorPatternsConfig struct {
|
||||
Patterns ColorPatterns `yaml:"patterns"`
|
||||
}
|
||||
|
||||
func TestColorPatternsKeepTheirOrder(t *testing.T) {
|
||||
var config colorPatternsConfig
|
||||
err := yaml.Unmarshal([]byte("patterns:\n"+
|
||||
" '^b': red\n"+
|
||||
" '^a': '#00ff00'\n"+
|
||||
" '^c': blue\n"), &config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, ColorPatterns{
|
||||
{Pattern: "^b", Color: "red"},
|
||||
{Pattern: "^a", Color: "#00ff00"},
|
||||
{Pattern: "^c", Color: "blue"},
|
||||
}, config.Patterns)
|
||||
}
|
||||
|
||||
func TestColorPatternsOfALaterFileComeFirst(t *testing.T) {
|
||||
var config colorPatternsConfig
|
||||
err := yaml.Unmarshal([]byte("patterns:\n"+
|
||||
" '^a': red\n"+
|
||||
" '^b': green\n"), &config)
|
||||
assert.NoError(t, err)
|
||||
err = yaml.Unmarshal([]byte("patterns:\n"+
|
||||
" '^c': blue\n"+
|
||||
" '^b': yellow\n"), &config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, ColorPatterns{
|
||||
{Pattern: "^c", Color: "blue"},
|
||||
{Pattern: "^b", Color: "yellow"},
|
||||
{Pattern: "^a", Color: "red"},
|
||||
}, config.Patterns)
|
||||
}
|
||||
|
||||
func TestColorPatternsMustBeAMapping(t *testing.T) {
|
||||
var config colorPatternsConfig
|
||||
err := yaml.Unmarshal([]byte("patterns: 5\n"), &config)
|
||||
assert.ErrorContains(t, err, "cannot unmarshal !!int `5` into map[string]string")
|
||||
}
|
||||
|
||||
func TestColorPatternsSurviveMarshalling(t *testing.T) {
|
||||
config := colorPatternsConfig{Patterns: ColorPatterns{
|
||||
{Pattern: "^b", Color: "red"},
|
||||
{Pattern: "^a", Color: "#00ff00"},
|
||||
{Pattern: "true", Color: "blue"},
|
||||
}}
|
||||
content, err := yaml.Marshal(config)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var roundTripped colorPatternsConfig
|
||||
err = yaml.Unmarshal(content, &roundTripped)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, config, roundTripped)
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
@@ -60,18 +61,23 @@ func (self *DiffRendererConfigManager) GetDiffRendererType() DiffRendererType {
|
||||
return currentDiffRendererConfig.getType()
|
||||
}
|
||||
|
||||
func (self *DiffRendererConfigManager) GetStdinFilterCommand(width int) string {
|
||||
// DiffRendererValues are what the command of a diff renderer can refer to.
|
||||
type DiffRendererValues struct {
|
||||
// The width of the view that the diff is rendered into
|
||||
Width int
|
||||
// The number of lines of context around each hunk
|
||||
DiffContext uint64
|
||||
// Whether the terminal has a light background
|
||||
LightBackground bool
|
||||
}
|
||||
|
||||
func (self *DiffRendererConfigManager) GetStdinFilterCommand(values DiffRendererValues) (string, error) {
|
||||
currentDiffRendererConfig := self.currentDiffRendererConfig()
|
||||
if currentDiffRendererConfig == nil || currentDiffRendererConfig.getType() != DiffRendererType_StdinFilter {
|
||||
return ""
|
||||
return "", nil
|
||||
}
|
||||
|
||||
templateValues := map[string]string{
|
||||
"columnWidth": strconv.Itoa(width/2 - 6),
|
||||
}
|
||||
|
||||
commandTemplate := string(currentDiffRendererConfig.Command)
|
||||
return utils.ResolvePlaceholderString(commandTemplate, templateValues)
|
||||
return currentDiffRendererConfig.resolveCommand(values)
|
||||
}
|
||||
|
||||
func (self *DiffRendererConfigManager) GetColorArg() string {
|
||||
@@ -87,17 +93,46 @@ func (self *DiffRendererConfigManager) GetColorArg() string {
|
||||
return colorArg
|
||||
}
|
||||
|
||||
func (self *DiffRendererConfigManager) GetExternalDiffCommand(diffContext uint64) string {
|
||||
func (self *DiffRendererConfigManager) GetExternalDiffCommand(values DiffRendererValues) (string, error) {
|
||||
currentDiffRendererConfig := self.currentDiffRendererConfig()
|
||||
if currentDiffRendererConfig == nil || currentDiffRendererConfig.getType() != DiffRendererType_ExtDiff {
|
||||
return ""
|
||||
return "", nil
|
||||
}
|
||||
|
||||
templateValues := map[string]string{
|
||||
"diffContext": strconv.Itoa(int(diffContext)),
|
||||
return currentDiffRendererConfig.resolveCommand(values)
|
||||
}
|
||||
|
||||
// resolveCommand resolves the renderer's command, which is a Go template with
|
||||
// the values it can refer to as its variables. A variable can be written with
|
||||
// or without the leading dot, as in {{.width}} or {{width}}.
|
||||
func (self *DiffRendererConfig) resolveCommand(values DiffRendererValues) (string, error) {
|
||||
colorScheme := "dark"
|
||||
if values.LightBackground {
|
||||
colorScheme = "light"
|
||||
}
|
||||
variables := map[string]any{
|
||||
"width": values.Width,
|
||||
"colorScheme": colorScheme,
|
||||
}
|
||||
switch self.getType() {
|
||||
case DiffRendererType_StdinFilter:
|
||||
variables["columnWidth"] = values.Width/2 - 6
|
||||
case DiffRendererType_ExtDiff:
|
||||
variables["diffContext"] = values.DiffContext
|
||||
case DiffRendererType_RawGit:
|
||||
// has no command
|
||||
}
|
||||
|
||||
return utils.ResolvePlaceholderString(string(currentDiffRendererConfig.Command), templateValues)
|
||||
funcs := template.FuncMap{}
|
||||
for name, value := range variables {
|
||||
funcs[name] = func() any { return value }
|
||||
}
|
||||
|
||||
command, err := utils.ResolveTemplate(string(self.Command), variables, funcs)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("git.diffRenderers: can't use the command '%s': %w", self.Command, err)
|
||||
}
|
||||
return command, nil
|
||||
}
|
||||
|
||||
func (self *DiffRendererConfigManager) GetRawGitArgs() []string {
|
||||
|
||||
@@ -63,6 +63,158 @@ func TestCurrentDiffRendererName(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStdinFilterCommand(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
diffRendererConfig DiffRendererConfig
|
||||
width int
|
||||
lightBackground bool
|
||||
expected string
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "a command without template variables is passed through",
|
||||
diffRendererConfig: DiffRendererConfig{Command: "delta --paging=never"},
|
||||
width: 120,
|
||||
expected: "delta --paging=never",
|
||||
},
|
||||
{
|
||||
name: "the width the diff is rendered at",
|
||||
diffRendererConfig: DiffRendererConfig{Command: "delta --width={{width}}"},
|
||||
width: 120,
|
||||
expected: "delta --width=120",
|
||||
},
|
||||
{
|
||||
name: "the width of one side of a side-by-side rendering",
|
||||
diffRendererConfig: DiffRendererConfig{Command: "ydiff -p cat -w {{columnWidth}}"},
|
||||
width: 120,
|
||||
expected: "ydiff -p cat -w 54",
|
||||
},
|
||||
{
|
||||
name: "a template variable can also be written with a leading dot",
|
||||
diffRendererConfig: DiffRendererConfig{Command: "delta --width={{.width}}"},
|
||||
width: 120,
|
||||
expected: "delta --width=120",
|
||||
},
|
||||
{
|
||||
name: "the color scheme on a dark background",
|
||||
diffRendererConfig: DiffRendererConfig{Command: "delta --{{colorScheme}}"},
|
||||
width: 120,
|
||||
expected: "delta --dark",
|
||||
},
|
||||
{
|
||||
name: "the color scheme on a light background",
|
||||
diffRendererConfig: DiffRendererConfig{Command: "delta --{{colorScheme}}"},
|
||||
width: 120,
|
||||
lightBackground: true,
|
||||
expected: "delta --light",
|
||||
},
|
||||
{
|
||||
name: "the command can choose between options by the color scheme",
|
||||
diffRendererConfig: DiffRendererConfig{Command: `delta --syntax-theme={{if eq .colorScheme "light"}}GitHub{{else}}Dracula{{end}}`},
|
||||
width: 120,
|
||||
lightBackground: true,
|
||||
expected: "delta --syntax-theme=GitHub",
|
||||
},
|
||||
{
|
||||
name: "the command can use template expressions",
|
||||
diffRendererConfig: DiffRendererConfig{Command: "delta{{if gt .width 100}} --side-by-side{{end}}"},
|
||||
width: 120,
|
||||
expected: "delta --side-by-side",
|
||||
},
|
||||
{
|
||||
name: "an unknown template variable is an error",
|
||||
diffRendererConfig: DiffRendererConfig{Command: "delta --width={{.widht}}"},
|
||||
width: 120,
|
||||
expectedError: "can't use the command 'delta --width={{.widht}}'",
|
||||
},
|
||||
{
|
||||
name: "an unknown template variable without a leading dot is an error too",
|
||||
diffRendererConfig: DiffRendererConfig{Command: "delta --width={{widht}}"},
|
||||
width: 120,
|
||||
expectedError: "can't use the command 'delta --width={{widht}}'",
|
||||
},
|
||||
{
|
||||
name: "nothing is returned for a renderer of another type",
|
||||
diffRendererConfig: DiffRendererConfig{Type: "extDiff", Command: "difft --width={{width}}"},
|
||||
width: 120,
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
userConfig := &UserConfig{}
|
||||
userConfig.Git.DiffRenderers = []DiffRendererConfig{s.diffRendererConfig}
|
||||
config := NewDiffRendererConfigManager(func() *UserConfig { return userConfig })
|
||||
|
||||
command, err := config.GetStdinFilterCommand(DiffRendererValues{Width: s.width, LightBackground: s.lightBackground})
|
||||
if s.expectedError != "" {
|
||||
assert.ErrorContains(t, err, s.expectedError)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, s.expected, command)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetExternalDiffCommand(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
diffRendererConfig DiffRendererConfig
|
||||
expected string
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "a command without template variables is passed through",
|
||||
diffRendererConfig: DiffRendererConfig{Type: "extDiff", Command: "difft --color=always"},
|
||||
expected: "difft --color=always",
|
||||
},
|
||||
{
|
||||
name: "the width the diff is rendered at",
|
||||
diffRendererConfig: DiffRendererConfig{Type: "extDiff", Command: "difft --width={{width}}"},
|
||||
expected: "difft --width=120",
|
||||
},
|
||||
{
|
||||
name: "the color scheme",
|
||||
diffRendererConfig: DiffRendererConfig{Type: "extDiff", Command: "difft --background={{colorScheme}}"},
|
||||
expected: "difft --background=dark",
|
||||
},
|
||||
{
|
||||
name: "the width alongside the diff context size",
|
||||
diffRendererConfig: DiffRendererConfig{Type: "extDiff", Command: "difft --width={{width}} --context={{diffContext}}"},
|
||||
expected: "difft --width=120 --context=3",
|
||||
},
|
||||
{
|
||||
name: "a variable of stdin filters is an error",
|
||||
diffRendererConfig: DiffRendererConfig{Type: "extDiff", Command: "difft --width={{columnWidth}}"},
|
||||
expectedError: "can't use the command 'difft --width={{columnWidth}}'",
|
||||
},
|
||||
{
|
||||
name: "nothing is returned for a renderer of another type",
|
||||
diffRendererConfig: DiffRendererConfig{Command: "delta --width={{width}}"},
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
userConfig := &UserConfig{}
|
||||
userConfig.Git.DiffRenderers = []DiffRendererConfig{s.diffRendererConfig}
|
||||
config := NewDiffRendererConfigManager(func() *UserConfig { return userConfig })
|
||||
|
||||
command, err := config.GetExternalDiffCommand(DiffRendererValues{Width: 120, DiffContext: 3})
|
||||
if s.expectedError != "" {
|
||||
assert.ErrorContains(t, err, s.expectedError)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, s.expected, command)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentDiffRendererNameWithoutDiffRenderers(t *testing.T) {
|
||||
config := NewDiffRendererConfigManager(func() *UserConfig { return &UserConfig{} })
|
||||
|
||||
|
||||
@@ -50,13 +50,13 @@ type editPreset struct {
|
||||
suspend func() bool
|
||||
}
|
||||
|
||||
func returnBool(a bool) func() bool { return (func() bool { return a }) }
|
||||
func returnBool(a bool) func() bool { return func() bool { return a } }
|
||||
|
||||
// IF YOU ADD A PRESET TO THIS FUNCTION YOU MUST UPDATE THE `Supported presets` SECTION OF docs/Config.md
|
||||
func getPreset(shell string, osConfig *OSConfig, guessDefaultEditor func() string) *editPreset {
|
||||
var nvimRemoteEditTemplate, nvimRemoteEditAtLineTemplate, nvimRemoteOpenDirInEditorTemplate string
|
||||
// By default fish doesn't have SHELL variable set, but it does have FISH_VERSION since Nov 2012.
|
||||
if (strings.HasSuffix(shell, "fish")) || (os.Getenv("FISH_VERSION") != "") {
|
||||
if strings.HasSuffix(shell, "fish") || (os.Getenv("FISH_VERSION") != "") {
|
||||
nvimRemoteEditTemplate = `begin; if test -z "$NVIM"; nvim -- {{filename}}; else; nvim --server "$NVIM" --remote-send "q"; nvim --server "$NVIM" --remote-tab {{filename}}; end; end`
|
||||
nvimRemoteEditAtLineTemplate = `begin; if test -z "$NVIM"; nvim +{{line}} -- {{filename}}; else; nvim --server "$NVIM" --remote-send "q"; nvim --server "$NVIM" --remote-tab {{filename}}; nvim --server "$NVIM" --remote-send ":{{line}}<CR>"; end; end`
|
||||
nvimRemoteOpenDirInEditorTemplate = `begin; if test -z "$NVIM"; nvim -- {{dir}}; else; nvim --server "$NVIM" --remote-send "q"; nvim --server "$NVIM" --remote-tab {{dir}}; end; end`
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"math"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// ThemeForBackground returns gui.theme with the overrides for a dark or a light
|
||||
// background applied, and with the defaults for that background in the fields
|
||||
// that neither of them sets. backgroundColor is the terminal's background color
|
||||
// as #rrggbb, or empty if we don't know it.
|
||||
func (c *GuiConfig) ThemeForBackground(lightBackground bool, backgroundColor string) ThemeConfig {
|
||||
override := lo.Ternary(lightBackground, c.LightTheme, c.DarkTheme)
|
||||
return mergeThemes(override, c.Theme, themeDefaults(lightBackground, backgroundColor))
|
||||
}
|
||||
|
||||
// themeDefaults returns the defaults of the theme fields whose default depends
|
||||
// on the terminal's background. GetDefaultConfig leaves these fields empty in
|
||||
// gui.theme, so that a value there comes from the user, and wins over these.
|
||||
//
|
||||
// Most of them are derived from the background color, so that they keep the
|
||||
// same distance from it however dark or light it is. If we don't know the
|
||||
// background color, we assume black or white.
|
||||
func themeDefaults(lightBackground bool, backgroundColor string) ThemeConfig {
|
||||
if len(backgroundColor) != 7 || !utils.IsValidHexValue(backgroundColor) {
|
||||
backgroundColor = lo.Ternary(lightBackground, "#ffffff", "#000000")
|
||||
}
|
||||
|
||||
if lightBackground {
|
||||
return ThemeConfig{
|
||||
// The colors of a light palette are dark enough to read as text on
|
||||
// the background, so none of them works as a background for text.
|
||||
// Some blue mixed into the background keeps the colored text on the
|
||||
// selected line as readable as elsewhere.
|
||||
SelectedLineBgColor: []string{mixHexColors(backgroundColor, "#0064ff", 0.25)},
|
||||
InactiveViewSelectedLineBgColor: []string{mixHexColors(backgroundColor, "#000000", 0.15)},
|
||||
}
|
||||
}
|
||||
|
||||
return ThemeConfig{
|
||||
// Dark palettes make their blue dark enough to work as a background for
|
||||
// text. Unlike a color derived from a dark background, it doesn't turn
|
||||
// into black on terminals with only 8 colors.
|
||||
SelectedLineBgColor: []string{"blue"},
|
||||
InactiveViewSelectedLineBgColor: []string{mixHexColors(backgroundColor, "#ffffff", 0.3)},
|
||||
}
|
||||
}
|
||||
|
||||
// mixHexColors mixes two colors given as #rrggbb. amount is how much of b to
|
||||
// take: 0 gives a, 1 gives b.
|
||||
func mixHexColors(a, b string, amount float64) string {
|
||||
channel := func(color string, i int) float64 {
|
||||
value, _ := strconv.ParseUint(color[1+2*i:3+2*i], 16, 8)
|
||||
return float64(value)
|
||||
}
|
||||
|
||||
result := "#"
|
||||
for i := range 3 {
|
||||
mixed := channel(a, i) + (channel(b, i)-channel(a, i))*amount
|
||||
result += fmt.Sprintf("%02x", int(math.Round(mixed)))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// mergeThemes takes each field from the first of the themes that sets it. For
|
||||
// maps and color patterns it merges the entries instead, and an entry of an
|
||||
// earlier theme wins over one with the same key in a later theme.
|
||||
func mergeThemes(themes ...ThemeConfig) ThemeConfig {
|
||||
var result ThemeConfig
|
||||
resultValue := reflect.ValueOf(&result).Elem()
|
||||
for i := range resultValue.NumField() {
|
||||
values := lo.Map(themes, func(theme ThemeConfig, _ int) any {
|
||||
return reflect.ValueOf(theme).Field(i).Interface()
|
||||
})
|
||||
resultValue.Field(i).Set(reflect.ValueOf(mergeThemeField(values)))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func mergeThemeField(values []any) any {
|
||||
switch values[0].(type) {
|
||||
case []string:
|
||||
return lo.FindOrElse(lo.Map(values, func(v any, _ int) []string { return v.([]string) }), nil,
|
||||
func(v []string) bool { return len(v) > 0 })
|
||||
case map[string]string:
|
||||
merged := map[string]string{}
|
||||
for _, v := range slices.Backward(values) {
|
||||
maps.Copy(merged, v.(map[string]string))
|
||||
}
|
||||
return merged
|
||||
case ColorPatterns:
|
||||
return lo.Reduce(values, func(merged ColorPatterns, v any, _ int) ColorPatterns {
|
||||
return merged.over(v.(ColorPatterns))
|
||||
}, nil)
|
||||
default:
|
||||
panic(fmt.Sprintf("don't know how to merge a theme field of type %T", values[0]))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestThemeForBackground(t *testing.T) {
|
||||
gui := GuiConfig{
|
||||
Theme: ThemeConfig{
|
||||
ActiveBorderColor: []string{"green"},
|
||||
InactiveBorderColor: []string{"default"},
|
||||
AuthorColors: map[string]string{"Alice": "red", "Bob": "blue"},
|
||||
BranchColorPatterns: ColorPatterns{
|
||||
{Pattern: "^docs/", Color: "cyan"},
|
||||
{Pattern: "^feature/", Color: "green"},
|
||||
},
|
||||
},
|
||||
DarkTheme: ThemeConfig{
|
||||
ActiveBorderColor: []string{"yellow"},
|
||||
},
|
||||
LightTheme: ThemeConfig{
|
||||
InactiveBorderColor: []string{"#777777"},
|
||||
AuthorColors: map[string]string{"Bob": "#000080"},
|
||||
BranchColorPatterns: ColorPatterns{
|
||||
{Pattern: "ISSUE", Color: "red"},
|
||||
{Pattern: "^docs/", Color: "#008080"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dark := gui.ThemeForBackground(false, "")
|
||||
assert.Equal(t, []string{"yellow"}, dark.ActiveBorderColor)
|
||||
assert.Equal(t, []string{"default"}, dark.InactiveBorderColor)
|
||||
assert.Equal(t, map[string]string{"Alice": "red", "Bob": "blue"}, dark.AuthorColors)
|
||||
assert.Equal(t, gui.Theme.BranchColorPatterns, dark.BranchColorPatterns)
|
||||
|
||||
light := gui.ThemeForBackground(true, "")
|
||||
assert.Equal(t, []string{"green"}, light.ActiveBorderColor)
|
||||
assert.Equal(t, []string{"#777777"}, light.InactiveBorderColor)
|
||||
assert.Equal(t, map[string]string{"Alice": "red", "Bob": "#000080"}, light.AuthorColors)
|
||||
assert.Equal(t, ColorPatterns{
|
||||
{Pattern: "ISSUE", Color: "red"},
|
||||
{Pattern: "^docs/", Color: "#008080"},
|
||||
{Pattern: "^feature/", Color: "green"},
|
||||
}, light.BranchColorPatterns)
|
||||
|
||||
assert.Equal(t, map[string]string{"Alice": "red", "Bob": "blue"}, gui.Theme.AuthorColors,
|
||||
"merging must leave the themes it merges alone")
|
||||
}
|
||||
|
||||
func TestEveryThemeFieldCanBeOverridden(t *testing.T) {
|
||||
var override ThemeConfig
|
||||
overrideValue := reflect.ValueOf(&override).Elem()
|
||||
for i := range overrideValue.NumField() {
|
||||
field := overrideValue.Field(i)
|
||||
switch field.Interface().(type) {
|
||||
case []string:
|
||||
field.Set(reflect.ValueOf([]string{"#123456"}))
|
||||
case map[string]string:
|
||||
field.Set(reflect.ValueOf(map[string]string{"key": "#123456"}))
|
||||
case ColorPatterns:
|
||||
field.Set(reflect.ValueOf(ColorPatterns{{Pattern: "key", Color: "#123456"}}))
|
||||
default:
|
||||
t.Fatalf("no test value for theme field %s", overrideValue.Type().Field(i).Name)
|
||||
}
|
||||
}
|
||||
|
||||
gui := GetDefaultConfig().Gui
|
||||
gui.DarkTheme = override
|
||||
assert.Equal(t, override, gui.ThemeForBackground(false, ""))
|
||||
}
|
||||
|
||||
func TestThemeForBackgroundFallsBackToTheDefaultsForTheBackground(t *testing.T) {
|
||||
gui := GetDefaultConfig().Gui
|
||||
assert.Equal(t, []string{"#4d4d4d"}, gui.ThemeForBackground(false, "").InactiveViewSelectedLineBgColor)
|
||||
assert.Equal(t, []string{"#d9d9d9"}, gui.ThemeForBackground(true, "").InactiveViewSelectedLineBgColor)
|
||||
|
||||
gui.Theme.InactiveViewSelectedLineBgColor = []string{"bold"}
|
||||
assert.Equal(t, []string{"bold"}, gui.ThemeForBackground(false, "").InactiveViewSelectedLineBgColor)
|
||||
assert.Equal(t, []string{"bold"}, gui.ThemeForBackground(true, "").InactiveViewSelectedLineBgColor)
|
||||
|
||||
gui.LightTheme.InactiveViewSelectedLineBgColor = []string{"white"}
|
||||
assert.Equal(t, []string{"bold"}, gui.ThemeForBackground(false, "").InactiveViewSelectedLineBgColor)
|
||||
assert.Equal(t, []string{"white"}, gui.ThemeForBackground(true, "").InactiveViewSelectedLineBgColor)
|
||||
}
|
||||
|
||||
func TestBackgroundDefaultsAreDerivedFromTheBackgroundColor(t *testing.T) {
|
||||
gui := GetDefaultConfig().Gui
|
||||
assert.Equal(t, []string{"#626262"}, gui.ThemeForBackground(false, "#1e1e1e").InactiveViewSelectedLineBgColor)
|
||||
assert.Equal(t, []string{"#d7d1c1"}, gui.ThemeForBackground(true, "#fdf6e3").InactiveViewSelectedLineBgColor)
|
||||
|
||||
assert.Equal(t, []string{"blue"}, gui.ThemeForBackground(false, "#1e1e1e").SelectedLineBgColor)
|
||||
assert.Equal(t, []string{"#bed2ea"}, gui.ThemeForBackground(true, "#fdf6e3").SelectedLineBgColor)
|
||||
assert.Equal(t, []string{"#bfd8ff"}, gui.ThemeForBackground(true, "").SelectedLineBgColor)
|
||||
}
|
||||
|
||||
// If gui.theme had a default for a field that also has a default for the
|
||||
// terminal's background, the former would always win.
|
||||
func TestFieldsWithBackgroundDefaultsHaveNoDefaultInGuiTheme(t *testing.T) {
|
||||
genericDefaults := setThemeFields(GetDefaultConfig().Gui.Theme)
|
||||
for _, field := range setThemeFields(themeDefaults(false, "")) {
|
||||
assert.NotContains(t, genericDefaults, field)
|
||||
}
|
||||
for _, field := range setThemeFields(themeDefaults(true, "")) {
|
||||
assert.NotContains(t, genericDefaults, field)
|
||||
}
|
||||
}
|
||||
|
||||
// If only one of the backgrounds had a default for a field, the field would
|
||||
// have no value at all with the other.
|
||||
func TestDarkAndLightDefaultsSetTheSameFields(t *testing.T) {
|
||||
assert.Equal(t, setThemeFields(themeDefaults(false, "")), setThemeFields(themeDefaults(true, "")))
|
||||
}
|
||||
|
||||
func setThemeFields(theme ThemeConfig) []string {
|
||||
var fields []string
|
||||
themeValue := reflect.ValueOf(theme)
|
||||
for i := range themeValue.NumField() {
|
||||
if themeValue.Field(i).Len() > 0 {
|
||||
fields = append(fields, themeValue.Type().Field(i).Name)
|
||||
}
|
||||
}
|
||||
return fields
|
||||
}
|
||||
+45
-27
@@ -68,13 +68,6 @@ func (c *RefresherConfig) ExternalChangeCheckIntervalDuration() time.Duration {
|
||||
}
|
||||
|
||||
type GuiConfig struct {
|
||||
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-author-color
|
||||
AuthorColors map[string]string `yaml:"authorColors"`
|
||||
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-branch-color
|
||||
// Deprecated: use branchColorPatterns instead
|
||||
BranchColors map[string]string `yaml:"branchColors" jsonschema:"deprecated"`
|
||||
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-branch-color
|
||||
BranchColorPatterns map[string]string `yaml:"branchColorPatterns"`
|
||||
// Custom icons for filenames and file extensions
|
||||
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-files-icon--color
|
||||
CustomIcons CustomIconsConfig `yaml:"customIcons"`
|
||||
@@ -94,7 +87,7 @@ type GuiConfig struct {
|
||||
MouseEvents bool `yaml:"mouseEvents"`
|
||||
// If true, do not show a warning when amending a commit.
|
||||
SkipAmendWarning bool `yaml:"skipAmendWarning"`
|
||||
// If true, do not show a warning when discarding changes in the staging view.
|
||||
// If true, do not show a warning when discarding changes from a focused diff.
|
||||
SkipDiscardChangeWarning bool `yaml:"skipDiscardChangeWarning"`
|
||||
// If true, do not show warning when applying/popping the stash
|
||||
SkipStashWarning bool `yaml:"skipStashWarning"`
|
||||
@@ -129,10 +122,10 @@ type GuiConfig struct {
|
||||
// - 'left': split the window horizontally (side panel on the left, main view on the right)
|
||||
// - 'top': split the window vertically (side panel on top, main view below)
|
||||
EnlargedSideViewLocation string `yaml:"enlargedSideViewLocation"`
|
||||
// If true, wrap lines in the staging view to the width of the view. This makes it much easier to work with diffs that have long lines, e.g. paragraphs of markdown text.
|
||||
WrapLinesInStagingView bool `yaml:"wrapLinesInStagingView"`
|
||||
// If true, hunk selection mode will be enabled by default when entering the staging view.
|
||||
UseHunkModeInStagingView bool `yaml:"useHunkModeInStagingView"`
|
||||
// If true, wrap lines in focused diffs to the width of the view. This makes it much easier to work with diffs that have long lines, e.g. paragraphs of markdown text.
|
||||
WrapLinesInDiffView bool `yaml:"wrapLinesInDiffView"`
|
||||
// If true, hunk selection mode will be enabled by default when focusing a diff.
|
||||
UseHunkModeInDiffView bool `yaml:"useHunkModeInDiffView"`
|
||||
// One of 'auto' (default) | 'en' | 'zh-CN' | 'zh-TW' | 'pl' | 'nl' | 'ja' | 'ko' | 'ru' | 'pt'
|
||||
Language string `yaml:"language" jsonschema:"enum=auto,enum=en,enum=zh-TW,enum=zh-CN,enum=pl,enum=nl,enum=ja,enum=ko,enum=ru"`
|
||||
// Format used when displaying time e.g. commit time.
|
||||
@@ -141,9 +134,19 @@ type GuiConfig struct {
|
||||
// Format used when displaying time if the time is less than 24 hours ago.
|
||||
// Uses Go's time format syntax: https://pkg.go.dev/time#Time.Format
|
||||
ShortTimeFormat string `yaml:"shortTimeFormat"`
|
||||
// Whether the terminal has a dark or a light background. This decides whether 'darkTheme' or 'lightTheme' applies, and the colors of authors are picked to stand out against it.
|
||||
// One of: 'auto' (default) | 'dark' | 'light'
|
||||
// With 'auto', lazygit asks the terminal, and assumes a dark background if the terminal doesn't tell.
|
||||
ColorScheme string `yaml:"colorScheme" jsonschema:"enum=auto,enum=dark,enum=light"`
|
||||
// Config relating to colors and styles.
|
||||
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#color-attributes
|
||||
Theme ThemeConfig `yaml:"theme"`
|
||||
// Colors and styles that override those in 'theme' when the terminal has a dark background. It has the same fields as 'theme'.
|
||||
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#themes-for-dark-and-light-backgrounds
|
||||
DarkTheme ThemeConfig `yaml:"darkTheme"`
|
||||
// Colors and styles that override those in 'theme' when the terminal has a light background. It has the same fields as 'theme'.
|
||||
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#themes-for-dark-and-light-backgrounds
|
||||
LightTheme ThemeConfig `yaml:"lightTheme"`
|
||||
// Config relating to the commit length indicator
|
||||
CommitLength CommitLengthConfig `yaml:"commitLength"`
|
||||
// If true, show the '5 of 20' footer at the bottom of list views
|
||||
@@ -237,10 +240,15 @@ type ThemeConfig struct {
|
||||
SearchingActiveBorderColor []string `yaml:"searchingActiveBorderColor" jsonschema:"minItems=1,uniqueItems=true"`
|
||||
// Color of keybindings help text in the bottom line
|
||||
OptionsTextColor []string `yaml:"optionsTextColor" jsonschema:"minItems=1,uniqueItems=true"`
|
||||
// Color and attributes of the text of the selected line. The attributes are added to those of the text, and a color replaces the colors of the text.
|
||||
// Set it to 'default' to leave the text as it is, e.g. if you don't want the selected line in bold.
|
||||
SelectedLineFgColor []string `yaml:"selectedLineFgColor" jsonschema:"minItems=1,uniqueItems=true"`
|
||||
// Background color of selected line.
|
||||
// Default: 'blue' if the terminal has a dark background, or a suitable RGB blue computed from the background color if it is light.
|
||||
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#highlighting-the-selected-line
|
||||
SelectedLineBgColor []string `yaml:"selectedLineBgColor" jsonschema:"minItems=1,uniqueItems=true"`
|
||||
// Background color of selected line when view doesn't have focus.
|
||||
// Default: a suitable RGB grey computed from the terminal's background color.
|
||||
InactiveViewSelectedLineBgColor []string `yaml:"inactiveViewSelectedLineBgColor" jsonschema:"minItems=1,uniqueItems=true"`
|
||||
// Foreground color of copied commit
|
||||
CherryPickedCommitFgColor []string `yaml:"cherryPickedCommitFgColor" jsonschema:"minItems=1,uniqueItems=true"`
|
||||
@@ -254,6 +262,10 @@ type ThemeConfig struct {
|
||||
UnstagedChangesColor []string `yaml:"unstagedChangesColor" jsonschema:"minItems=1,uniqueItems=true"`
|
||||
// Default text color
|
||||
DefaultFgColor []string `yaml:"defaultFgColor" jsonschema:"minItems=1,uniqueItems=true"`
|
||||
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-author-color
|
||||
AuthorColors map[string]string `yaml:"authorColors"`
|
||||
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-branch-color
|
||||
BranchColorPatterns ColorPatterns `yaml:"branchColorPatterns"`
|
||||
}
|
||||
|
||||
type CommitLengthConfig struct {
|
||||
@@ -316,7 +328,7 @@ type GitConfig struct {
|
||||
AutoRefresh bool `yaml:"autoRefresh"`
|
||||
// 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 bool `yaml:"autoDetectExternalChanges"`
|
||||
// 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 (as opposed to diverged).
|
||||
// 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 (as opposed to diverged). A branch that is checked out in another worktree is fast-forwarded there, unless that worktree has changes to tracked files or is in the middle of a rebase or bisect.
|
||||
// Possible values: 'none' | 'onlyMainBranches' | 'allBranches'
|
||||
AutoForwardBranches string `yaml:"autoForwardBranches" jsonschema:"enum=none,enum=onlyMainBranches,enum=allBranches"`
|
||||
// If true, pass the --all arg to git fetch
|
||||
@@ -499,6 +511,7 @@ type KeybindingUniversalConfig struct {
|
||||
PrevBlockAlt2 Keybinding `yaml:"prevBlock-alt2"`
|
||||
JumpToBlock []Keybinding `yaml:"jumpToBlock"`
|
||||
FocusMainView Keybinding `yaml:"focusMainView"`
|
||||
JumpToFile Keybinding `yaml:"jumpToFile"`
|
||||
NextMatch Keybinding `yaml:"nextMatch"`
|
||||
PrevMatch Keybinding `yaml:"prevMatch"`
|
||||
StartSearch Keybinding `yaml:"startSearch"`
|
||||
@@ -660,6 +673,8 @@ type KeybindingCommitFilesConfig struct {
|
||||
type KeybindingMainConfig struct {
|
||||
PrevHunk Keybinding `yaml:"prevHunk"`
|
||||
NextHunk Keybinding `yaml:"nextHunk"`
|
||||
PrevFile Keybinding `yaml:"prevFile"`
|
||||
NextFile Keybinding `yaml:"nextFile"`
|
||||
ToggleSelectHunk Keybinding `yaml:"toggleSelectHunk"`
|
||||
PickBothHunks Keybinding `yaml:"pickBothHunks"`
|
||||
EditSelectHunk Keybinding `yaml:"editSelectHunk"`
|
||||
@@ -876,24 +891,24 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig {
|
||||
},
|
||||
MainPanelSplitMode: "flexible",
|
||||
EnlargedSideViewLocation: "left",
|
||||
WrapLinesInStagingView: true,
|
||||
UseHunkModeInStagingView: true,
|
||||
WrapLinesInDiffView: true,
|
||||
UseHunkModeInDiffView: true,
|
||||
Language: "auto",
|
||||
TimeFormat: "02 Jan 06",
|
||||
ShortTimeFormat: time.Kitchen,
|
||||
ColorScheme: "auto",
|
||||
Theme: ThemeConfig{
|
||||
ActiveBorderColor: []string{"green", "bold"},
|
||||
SearchingActiveBorderColor: []string{"cyan", "bold"},
|
||||
InactiveBorderColor: []string{"default"},
|
||||
OptionsTextColor: []string{"blue"},
|
||||
SelectedLineBgColor: []string{"blue"},
|
||||
InactiveViewSelectedLineBgColor: []string{"bold"},
|
||||
CherryPickedCommitBgColor: []string{"cyan"},
|
||||
CherryPickedCommitFgColor: []string{"blue"},
|
||||
MarkedBaseCommitBgColor: []string{"yellow"},
|
||||
MarkedBaseCommitFgColor: []string{"blue"},
|
||||
UnstagedChangesColor: []string{"red"},
|
||||
DefaultFgColor: []string{"default"},
|
||||
ActiveBorderColor: []string{"green", "bold"},
|
||||
SearchingActiveBorderColor: []string{"cyan", "bold"},
|
||||
InactiveBorderColor: []string{"dim"},
|
||||
OptionsTextColor: []string{"blue"},
|
||||
SelectedLineFgColor: []string{"bold"},
|
||||
CherryPickedCommitBgColor: []string{"cyan"},
|
||||
CherryPickedCommitFgColor: []string{"blue"},
|
||||
MarkedBaseCommitBgColor: []string{"yellow"},
|
||||
MarkedBaseCommitFgColor: []string{"blue"},
|
||||
UnstagedChangesColor: []string{"red"},
|
||||
DefaultFgColor: []string{"default"},
|
||||
},
|
||||
CommitLength: CommitLengthConfig{Show: true},
|
||||
SkipNoStagedFilesWarning: false,
|
||||
@@ -1023,6 +1038,7 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig {
|
||||
NextBlockAlt2: Keybinding{"<tab>"},
|
||||
JumpToBlock: []Keybinding{{"1"}, {"2"}, {"3"}, {"4"}, {"5"}},
|
||||
FocusMainView: Keybinding{"0"},
|
||||
JumpToFile: Keybinding{"<ctrl+g>"},
|
||||
NextMatch: Keybinding{"n"},
|
||||
PrevMatch: Keybinding{"N"},
|
||||
StartSearch: Keybinding{"/"},
|
||||
@@ -1170,6 +1186,8 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig {
|
||||
Main: KeybindingMainConfig{
|
||||
PrevHunk: Keybinding{"<left>", "h"},
|
||||
NextHunk: Keybinding{"<right>", "l"},
|
||||
PrevFile: Keybinding{"N"},
|
||||
NextFile: Keybinding{"n"},
|
||||
ToggleSelectHunk: Keybinding{"a"},
|
||||
PickBothHunks: Keybinding{"b"},
|
||||
EditSelectHunk: Keybinding{"E"},
|
||||
|
||||
@@ -18,6 +18,10 @@ func (config *UserConfig) Validate() error {
|
||||
[]string{"dashboard", "allBranchesLog"}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateEnum("gui.colorScheme", config.Gui.ColorScheme,
|
||||
[]string{"auto", "dark", "light"}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateEnum("gui.showDivergenceFromBaseBranch", config.Gui.ShowDivergenceFromBaseBranch,
|
||||
[]string{"none", "onlyArrow", "arrowAndNumber"}); err != nil {
|
||||
return err
|
||||
@@ -120,10 +124,16 @@ func validateDiffRenderers(diffRenderers []DiffRendererConfig) error {
|
||||
if len(diffRenderer.Args) > 0 {
|
||||
return errors.New("git.diffRenderers: 'args' cannot be used with diff renderer type 'stdinFilter'.")
|
||||
}
|
||||
if err := validateDiffRendererCommand(diffRenderer); err != nil {
|
||||
return err
|
||||
}
|
||||
case "extDiff":
|
||||
if len(diffRenderer.Args) > 0 {
|
||||
return errors.New("git.diffRenderers: 'args' cannot be used with diff renderer type 'extDiff'.")
|
||||
}
|
||||
if err := validateDiffRendererCommand(diffRenderer); err != nil {
|
||||
return err
|
||||
}
|
||||
case "rawGit":
|
||||
if diffRenderer.Command != "" {
|
||||
return errors.New("git.diffRenderers: 'command' cannot be used with diff renderer type 'rawGit'.")
|
||||
@@ -135,6 +145,14 @@ func validateDiffRenderers(diffRenderers []DiffRendererConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateDiffRendererCommand resolves the command with made-up values, so that
|
||||
// a mistake in it shows up when the config is loaded rather than when a diff
|
||||
// is rendered.
|
||||
func validateDiffRendererCommand(diffRenderer DiffRendererConfig) error {
|
||||
_, err := diffRenderer.resolveCommand(DiffRendererValues{Width: 80, DiffContext: 3})
|
||||
return err
|
||||
}
|
||||
|
||||
func validateEnum(name string, value string, allowedValues []string) error {
|
||||
if slices.Contains(allowedValues, value) {
|
||||
return nil
|
||||
@@ -191,6 +209,47 @@ func validateCustomCommandKey(key Keybinding) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidCustomCommandContexts lists the names a custom command's 'context' may
|
||||
// use. It mirrors context.AllContextKeys in the gui package, which this package
|
||||
// can't import; a test over there keeps the two in sync.
|
||||
var ValidCustomCommandContexts = []string{
|
||||
"global",
|
||||
"status",
|
||||
"files",
|
||||
"localBranches",
|
||||
"remotes",
|
||||
"worktrees",
|
||||
"remoteBranches",
|
||||
"tags",
|
||||
"commits",
|
||||
"reflogCommits",
|
||||
"subCommits",
|
||||
"commitFiles",
|
||||
"stash",
|
||||
"normal",
|
||||
"normalSecondary",
|
||||
"mergeConflicts",
|
||||
"menu",
|
||||
"confirmation",
|
||||
"prompt",
|
||||
"search",
|
||||
"commitMessage",
|
||||
"submodules",
|
||||
"suggestions",
|
||||
"cmdLog",
|
||||
}
|
||||
|
||||
func validateCustomCommandContext(context string) error {
|
||||
for _, name := range strings.Split(context, ",") {
|
||||
name = strings.TrimSpace(name)
|
||||
if !slices.Contains(ValidCustomCommandContexts, name) {
|
||||
return fmt.Errorf("Unknown context '%s' for custom command. Allowed values: %s",
|
||||
name, strings.Join(ValidCustomCommandContexts, ", "))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCustomCommands(customCommands []CustomCommand) error {
|
||||
for _, customCommand := range customCommands {
|
||||
if err := validateCustomCommandKey(customCommand.Key); err != nil {
|
||||
@@ -216,6 +275,15 @@ func validateCustomCommands(customCommands []CustomCommand) error {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// A command in a menu may leave the context out, in which case it is
|
||||
// offered whatever is focused; a top-level one may not, but that is
|
||||
// only noticed when the keybindings are built.
|
||||
if customCommand.Context != "" {
|
||||
if err := validateCustomCommandContext(customCommand.Context); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, prompt := range customCommand.Prompts {
|
||||
if err := validateCustomCommandPrompt(prompt); err != nil {
|
||||
return err
|
||||
|
||||
@@ -225,6 +225,47 @@ func TestUserConfigValidate_enums(t *testing.T) {
|
||||
{value: "invalid_value", valid: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Custom command context",
|
||||
setup: func(config *UserConfig, value string) {
|
||||
config.CustomCommands = []CustomCommand{
|
||||
{
|
||||
Context: value,
|
||||
},
|
||||
}
|
||||
},
|
||||
testCases: []testCase{
|
||||
{value: "", valid: true},
|
||||
{value: "global", valid: true},
|
||||
{value: "commits", valid: true},
|
||||
{value: "commits, subCommits", valid: true},
|
||||
{value: "commits,subCommits", valid: true},
|
||||
{value: "invalid_value", valid: false},
|
||||
{value: "commits, invalid_value", valid: false},
|
||||
// The staging and patch-building panels are gone, so a config that
|
||||
// still names their contexts is reported rather than fatal.
|
||||
{value: "staging", valid: false},
|
||||
{value: "patchBuilding", valid: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Custom command context in a sub menu",
|
||||
setup: func(config *UserConfig, value string) {
|
||||
config.CustomCommands = []CustomCommand{
|
||||
{
|
||||
Key: Keybinding{"X"},
|
||||
CommandMenu: []CustomCommand{
|
||||
{Key: Keybinding{"1"}, Command: "echo 'hello'", Context: value},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
testCases: []testCase{
|
||||
{value: "", valid: true},
|
||||
{value: "commits", valid: true},
|
||||
{value: "invalid_value", valid: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Custom command sub menu",
|
||||
setup: func(config *UserConfig, _ string) {
|
||||
@@ -372,9 +413,14 @@ func TestUserConfigValidate_diffRenderers(t *testing.T) {
|
||||
{name: "stdinFilter with explicit type", diffRenderer: DiffRendererConfig{Type: "stdinFilter"}, valid: false},
|
||||
{name: "stdinFilter with type default without command", diffRenderer: DiffRendererConfig{}, valid: false},
|
||||
{name: "stdinFilter with args", diffRenderer: DiffRendererConfig{Type: "stdinFilter", Command: "delta", Args: []string{"-x"}}, valid: false},
|
||||
{name: "stdinFilter with a template", diffRenderer: DiffRendererConfig{Command: "delta --width={{width}}{{if gt .width 100}} --side-by-side{{end}}"}, valid: true},
|
||||
{name: "stdinFilter with an unknown template variable", diffRenderer: DiffRendererConfig{Command: "delta --width={{.widht}}"}, valid: false},
|
||||
{name: "stdinFilter with a broken template", diffRenderer: DiffRendererConfig{Command: "delta {{if .width}}"}, valid: false},
|
||||
{name: "external diff", diffRenderer: DiffRendererConfig{Type: "extDiff", Command: "difft"}, valid: true},
|
||||
{name: "external diff without command", diffRenderer: DiffRendererConfig{Type: "extDiff"}, valid: true},
|
||||
{name: "external diff with args", diffRenderer: DiffRendererConfig{Type: "extDiff", Command: "difft", Args: []string{"-x"}}, valid: false},
|
||||
{name: "external diff with a template", diffRenderer: DiffRendererConfig{Type: "extDiff", Command: "difft --context={{diffContext}}"}, valid: true},
|
||||
{name: "external diff with a variable of stdin filters", diffRenderer: DiffRendererConfig{Type: "extDiff", Command: "difft --width={{columnWidth}}"}, valid: false},
|
||||
{name: "raw git", diffRenderer: DiffRendererConfig{Type: "rawGit"}, valid: true},
|
||||
{name: "raw git with args", diffRenderer: DiffRendererConfig{Type: "rawGit", Args: []string{"-x"}}, valid: true},
|
||||
{name: "raw git with command", diffRenderer: DiffRendererConfig{Type: "rawGit", Command: "delta"}, valid: false},
|
||||
|
||||
@@ -50,6 +50,22 @@ func setupKeyRecorder(t *testing.T, g *Gui) (GocuiEvent, *[]int) {
|
||||
return GocuiEvent{Type: eventKey, Key: key}, &fired
|
||||
}
|
||||
|
||||
// runQueuedWork runs what the gui has queued for the following passes of the
|
||||
// event loop, which is where EndBlockingEvents leaves the replay of the keys it
|
||||
// buffered.
|
||||
func runQueuedWork(t *testing.T, g *Gui) {
|
||||
t.Helper()
|
||||
|
||||
for {
|
||||
ev, ok := g.userEvents.dequeue()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
assert.NoError(t, ev.f(g))
|
||||
ev.task.Done()
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlockingEvents_KeysBufferedAndReplayed(t *testing.T) {
|
||||
g := newTestGui(t)
|
||||
keyEvent, fired := setupKeyRecorder(t, g)
|
||||
@@ -64,12 +80,37 @@ func TestBlockingEvents_KeysBufferedAndReplayed(t *testing.T) {
|
||||
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())
|
||||
// Unblocking queues the replay rather than dispatching from here.
|
||||
g.EndBlockingEvents()
|
||||
assert.Len(t, *fired, 1, "the replay must wait for the event loop")
|
||||
|
||||
runQueuedWork(t, g)
|
||||
assert.Len(t, *fired, 3, "both buffered keys should replay on unblock")
|
||||
assert.Empty(t, g.bufferedKeyEvents)
|
||||
}
|
||||
|
||||
func TestBlockingEvents_KeysArrivingBeforeTheReplayGoBehindIt(t *testing.T) {
|
||||
g := newTestGui(t)
|
||||
keyEvent, fired := setupKeyRecorder(t, g)
|
||||
other := GocuiEvent{Type: eventKey, Key: NewKeyRune('y')}
|
||||
g.SetKeybinding("main", other.Key, func(*Gui, *View) error {
|
||||
*fired = append(*fired, 0)
|
||||
return nil
|
||||
})
|
||||
|
||||
g.BeginBlockingEvents()
|
||||
assert.NoError(t, g.handleEvent(&keyEvent))
|
||||
g.EndBlockingEvents()
|
||||
|
||||
// A key pressed while the replay is still queued joins the end of the buffer:
|
||||
// dispatching it now would put it ahead of the keys buffered before it.
|
||||
assert.NoError(t, g.handleEvent(&other))
|
||||
assert.Empty(t, *fired)
|
||||
|
||||
runQueuedWork(t, g)
|
||||
assert.Equal(t, []int{1, 0}, *fired, "the keys should arrive in the order they were pressed")
|
||||
}
|
||||
|
||||
func TestBlockingEvents_NestsWithCounter(t *testing.T) {
|
||||
g := newTestGui(t)
|
||||
keyEvent, fired := setupKeyRecorder(t, g)
|
||||
@@ -79,11 +120,13 @@ func TestBlockingEvents_NestsWithCounter(t *testing.T) {
|
||||
assert.NoError(t, g.handleEvent(&keyEvent))
|
||||
|
||||
// The inner block ending still leaves us blocked: no replay yet.
|
||||
assert.NoError(t, g.EndBlockingEvents())
|
||||
g.EndBlockingEvents()
|
||||
runQueuedWork(t, g)
|
||||
assert.Empty(t, *fired)
|
||||
|
||||
// Only the outermost block ending replays.
|
||||
assert.NoError(t, g.EndBlockingEvents())
|
||||
g.EndBlockingEvents()
|
||||
runQueuedWork(t, g)
|
||||
assert.Len(t, *fired, 1)
|
||||
}
|
||||
|
||||
@@ -94,5 +137,6 @@ func TestBlockingEvents_MouseClicksDroppedNotBuffered(t *testing.T) {
|
||||
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())
|
||||
g.EndBlockingEvents()
|
||||
runQueuedWork(t, g)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,497 @@
|
||||
package gocui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell/v3"
|
||||
"github.com/lucasb-eyer/go-colorful"
|
||||
)
|
||||
|
||||
// ColorScheme says whether the terminal shows light text on a dark background,
|
||||
// or dark text on a light one.
|
||||
type ColorScheme int
|
||||
|
||||
const (
|
||||
ColorSchemeUnknown ColorScheme = iota
|
||||
ColorSchemeDark
|
||||
ColorSchemeLight
|
||||
)
|
||||
|
||||
func (self ColorScheme) String() string {
|
||||
switch self {
|
||||
case ColorSchemeDark:
|
||||
return "dark"
|
||||
case ColorSchemeLight:
|
||||
return "light"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// DetectedColorScheme is what the terminal has told us about its colors.
|
||||
type DetectedColorScheme struct {
|
||||
ColorScheme ColorScheme
|
||||
// The background color that ColorScheme was derived from, as #rrggbb. It is
|
||||
// empty if the terminal didn't tell us its background color; ColorScheme is
|
||||
// then whatever the terminal said about itself, if anything.
|
||||
Background string
|
||||
}
|
||||
|
||||
func (self DetectedColorScheme) String() string {
|
||||
if self.Background != "" {
|
||||
return fmt.Sprintf("%s (background %s)", self.ColorScheme, self.Background)
|
||||
}
|
||||
if self.ColorScheme != ColorSchemeUnknown {
|
||||
return fmt.Sprintf("%s (reported by the terminal)", self.ColorScheme)
|
||||
}
|
||||
return self.ColorScheme.String()
|
||||
}
|
||||
|
||||
const (
|
||||
// Makes the terminal send a color scheme report whenever its colors change
|
||||
enableColorSchemeReports = "\x1b[?2031h"
|
||||
disableColorSchemeReports = "\x1b[?2031l"
|
||||
// Asks for a color scheme report: CSI ? 997 ; 1 n for dark, 2 for light
|
||||
requestColorScheme = "\x1b[?996n"
|
||||
// Asks for the background color: OSC 11 ; rgb:RRRR/GGGG/BBBB, ended by BEL
|
||||
// or ST. Ending the request with BEL makes more terminals reply.
|
||||
requestBackgroundColor = "\x1b]11;?\a"
|
||||
)
|
||||
|
||||
// colorSchemeTty is a tcell.Tty that finds out the terminal's color scheme.
|
||||
//
|
||||
// Some terminals say whether they are dark or light, and can report it again
|
||||
// whenever that changes. What they base this on varies, though: some go by the
|
||||
// terminal's own colors, others by the dark or light mode of the operating
|
||||
// system, whether or not the terminal follows it. So we also ask for the
|
||||
// background color, and when the terminal tells us that, it decides; a color
|
||||
// scheme report is then only a sign that the background may have changed.
|
||||
//
|
||||
// The queries go out when tcell starts the tty, which is before tcell sends its
|
||||
// own queries during Screen.Init and waits for their answer. Terminals answer in
|
||||
// order, so the answers to ours have arrived by the time Init returns, and we
|
||||
// know the color scheme before drawing anything without waiting for it.
|
||||
//
|
||||
// tcell doesn't understand the answers and drops them, so all we need to do is
|
||||
// watch for them in the input as it goes by.
|
||||
type colorSchemeTty struct {
|
||||
tcell.Tty
|
||||
|
||||
// What we send when the tty starts
|
||||
queries string
|
||||
|
||||
// Guards all of the fields below, and serializes our writes to the terminal
|
||||
// with tcell's
|
||||
mutex sync.Mutex
|
||||
|
||||
started bool
|
||||
|
||||
background colorful.Color
|
||||
// Whether the terminal has told us its background color
|
||||
haveBackground bool
|
||||
// Whether we have asked for the background color and are waiting for the
|
||||
// answer
|
||||
backgroundRequested bool
|
||||
// The color scheme that the terminal last reported for itself
|
||||
reported ColorScheme
|
||||
// Whether we have asked for a color scheme report and are waiting for it
|
||||
colorSchemeRequested bool
|
||||
// Closed once no answer that waitForReplies waits for is outstanding
|
||||
repliesArrived chan struct{}
|
||||
|
||||
notified DetectedColorScheme
|
||||
onChange func(DetectedColorScheme)
|
||||
|
||||
// Only used by Read, which tcell never calls concurrently with itself
|
||||
scanner terminalReplyScanner
|
||||
}
|
||||
|
||||
var _ tcell.Tty = &colorSchemeTty{}
|
||||
|
||||
func newColorSchemeTty(tty tcell.Tty) *colorSchemeTty {
|
||||
return &colorSchemeTty{
|
||||
Tty: tty,
|
||||
queries: colorSchemeQueries(
|
||||
os.Getenv("TERM"),
|
||||
os.Getenv("TERM_PROGRAM"),
|
||||
os.Getenv("TCELL_NEGOTIATE"),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// colorSchemeQueries returns what to ask the terminal. It leaves out the
|
||||
// terminals that tcell doesn't send its own queries to (see
|
||||
// applyKnownTerminalProfile and the legacy terminals in tScreen.Init), except
|
||||
// for those that are known to answer a request for the background color.
|
||||
func colorSchemeQueries(term string, termProgram string, tcellNegotiate string) string {
|
||||
if tcellNegotiate == "disable" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if term == "st" || strings.HasPrefix(term, "st-") ||
|
||||
strings.HasPrefix(term, "vt") || strings.Contains(term, "ansi") ||
|
||||
term == "linux" || term == "sun" || term == "sun-color" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if termProgram == "Apple_Terminal" || termProgram == "WezTerm" {
|
||||
return requestBackgroundColor
|
||||
}
|
||||
|
||||
return enableColorSchemeReports + requestColorScheme + requestBackgroundColor
|
||||
}
|
||||
|
||||
// subscribe sets a function to call whenever the detected color scheme changes,
|
||||
// and returns the one detected so far. The function is called on the goroutine
|
||||
// that reads from the tty, so it must not block.
|
||||
func (self *colorSchemeTty) subscribe(onChange func(DetectedColorScheme)) DetectedColorScheme {
|
||||
self.mutex.Lock()
|
||||
defer self.mutex.Unlock()
|
||||
|
||||
self.onChange = onChange
|
||||
self.notified = self.detected()
|
||||
return self.notified
|
||||
}
|
||||
|
||||
func (self *colorSchemeTty) Start() error {
|
||||
if err := self.Tty.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
self.mutex.Lock()
|
||||
defer self.mutex.Unlock()
|
||||
|
||||
self.started = true
|
||||
self.writeLocked(self.queries)
|
||||
if strings.Contains(self.queries, requestBackgroundColor) {
|
||||
self.backgroundRequested = true
|
||||
}
|
||||
if strings.Contains(self.queries, requestColorScheme) {
|
||||
self.colorSchemeRequested = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// waitForReplies waits until the terminal has answered what we asked it, but
|
||||
// no longer than the timeout. Call it before handing the terminal to another
|
||||
// program, or the answers would reach that program as if they were typed. It
|
||||
// only waits for the answers that the terminal has given before, so a terminal
|
||||
// that doesn't answer at all costs no time.
|
||||
func (self *colorSchemeTty) waitForReplies(timeout time.Duration) {
|
||||
self.mutex.Lock()
|
||||
if !self.awaitingRepliesLocked() {
|
||||
self.mutex.Unlock()
|
||||
return
|
||||
}
|
||||
if self.repliesArrived == nil {
|
||||
self.repliesArrived = make(chan struct{})
|
||||
}
|
||||
repliesArrived := self.repliesArrived
|
||||
self.mutex.Unlock()
|
||||
|
||||
select {
|
||||
case <-repliesArrived:
|
||||
case <-time.After(timeout):
|
||||
}
|
||||
}
|
||||
|
||||
func (self *colorSchemeTty) awaitingRepliesLocked() bool {
|
||||
return (self.backgroundRequested && self.haveBackground) ||
|
||||
(self.colorSchemeRequested && self.reported != ColorSchemeUnknown)
|
||||
}
|
||||
|
||||
func (self *colorSchemeTty) Stop() error {
|
||||
self.mutex.Lock()
|
||||
// Otherwise, the program we hand the terminal to would receive the reports
|
||||
// as if they were typed
|
||||
if strings.Contains(self.queries, enableColorSchemeReports) {
|
||||
self.writeLocked(disableColorSchemeReports)
|
||||
}
|
||||
self.started = false
|
||||
self.mutex.Unlock()
|
||||
|
||||
return self.Tty.Stop()
|
||||
}
|
||||
|
||||
func (self *colorSchemeTty) Write(p []byte) (int, error) {
|
||||
self.mutex.Lock()
|
||||
defer self.mutex.Unlock()
|
||||
|
||||
return self.Tty.Write(p)
|
||||
}
|
||||
|
||||
func (self *colorSchemeTty) Read(p []byte) (int, error) {
|
||||
n, err := self.Tty.Read(p)
|
||||
|
||||
for _, reply := range self.scanner.scan(p[:n]) {
|
||||
self.handleReply(reply)
|
||||
}
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
// onFocusGained asks for the background color again, for the terminals that
|
||||
// tell us their background color but not when it changes. When the operating
|
||||
// system switches between dark and light mode, the user is usually busy
|
||||
// elsewhere, so coming back to the terminal is a good time to check.
|
||||
func (self *colorSchemeTty) onFocusGained() {
|
||||
self.mutex.Lock()
|
||||
defer self.mutex.Unlock()
|
||||
|
||||
self.requestBackgroundColorLocked()
|
||||
}
|
||||
|
||||
func (self *colorSchemeTty) handleReply(reply terminalReply) {
|
||||
self.mutex.Lock()
|
||||
|
||||
if reply.background != nil {
|
||||
self.background = *reply.background
|
||||
self.haveBackground = true
|
||||
self.backgroundRequested = false
|
||||
} else {
|
||||
self.reported = reply.colorScheme
|
||||
self.colorSchemeRequested = false
|
||||
self.requestBackgroundColorLocked()
|
||||
}
|
||||
|
||||
if self.repliesArrived != nil && !self.awaitingRepliesLocked() {
|
||||
close(self.repliesArrived)
|
||||
self.repliesArrived = nil
|
||||
}
|
||||
|
||||
detected := self.detected()
|
||||
onChange := self.onChange
|
||||
changed := detected != self.notified
|
||||
if changed {
|
||||
self.notified = detected
|
||||
}
|
||||
|
||||
self.mutex.Unlock()
|
||||
|
||||
if changed && onChange != nil {
|
||||
onChange(detected)
|
||||
}
|
||||
}
|
||||
|
||||
// requestBackgroundColorLocked asks for the background color, if the terminal
|
||||
// has told us its background color before and isn't about to do it anyway.
|
||||
func (self *colorSchemeTty) requestBackgroundColorLocked() {
|
||||
if !self.started || !self.haveBackground || self.backgroundRequested {
|
||||
return
|
||||
}
|
||||
|
||||
self.writeLocked(requestBackgroundColor)
|
||||
self.backgroundRequested = true
|
||||
}
|
||||
|
||||
func (self *colorSchemeTty) detected() DetectedColorScheme {
|
||||
if !self.haveBackground {
|
||||
return DetectedColorScheme{ColorScheme: self.reported}
|
||||
}
|
||||
|
||||
return DetectedColorScheme{
|
||||
ColorScheme: colorSchemeOfBackground(self.background),
|
||||
Background: self.background.Hex(),
|
||||
}
|
||||
}
|
||||
|
||||
func (self *colorSchemeTty) writeLocked(s string) {
|
||||
if s == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// If the terminal is gone, tcell finds out when it next writes or reads
|
||||
_, _ = self.Tty.Write([]byte(s))
|
||||
}
|
||||
|
||||
func colorSchemeOfBackground(background colorful.Color) ColorScheme {
|
||||
lightness, _, _ := background.Lab()
|
||||
if lightness < 0.5 {
|
||||
return ColorSchemeDark
|
||||
}
|
||||
return ColorSchemeLight
|
||||
}
|
||||
|
||||
// terminalReply is one of the answers that colorSchemeTty asks the terminal
|
||||
// for. Exactly one of its fields is set.
|
||||
type terminalReply struct {
|
||||
colorScheme ColorScheme
|
||||
background *colorful.Color
|
||||
}
|
||||
|
||||
type terminalReplyScannerState int
|
||||
|
||||
const (
|
||||
scanningText terminalReplyScannerState = iota
|
||||
scanningEscape
|
||||
scanningCsi
|
||||
scanningOsc
|
||||
scanningOscEscape
|
||||
)
|
||||
|
||||
// No reply we look for is longer than this; we don't look at longer sequences
|
||||
const maxTerminalReplyLength = 64
|
||||
|
||||
// terminalReplyScanner finds the terminal's answers to our color scheme queries
|
||||
// in its input. The input arrives in chunks, which may split an answer anywhere,
|
||||
// so the scanner keeps its state from one chunk to the next.
|
||||
type terminalReplyScanner struct {
|
||||
state terminalReplyScannerState
|
||||
sequence []byte
|
||||
oversized bool
|
||||
}
|
||||
|
||||
func (self *terminalReplyScanner) scan(input []byte) []terminalReply {
|
||||
var replies []terminalReply
|
||||
|
||||
for _, b := range input {
|
||||
switch self.state {
|
||||
case scanningText:
|
||||
if b == '\x1b' {
|
||||
self.state = scanningEscape
|
||||
}
|
||||
|
||||
case scanningEscape:
|
||||
self.scanEscape(b)
|
||||
|
||||
case scanningCsi:
|
||||
switch {
|
||||
case b == '\x1b':
|
||||
self.state = scanningEscape
|
||||
case b >= 0x20 && b <= 0x3f: // parameter and intermediate bytes
|
||||
self.appendToSequence(b)
|
||||
case b >= 0x40 && b <= 0x7e: // final byte
|
||||
self.state = scanningText
|
||||
if b == 'n' && !self.oversized {
|
||||
if colorScheme, ok := parseColorSchemeReport(string(self.sequence)); ok {
|
||||
replies = append(replies, terminalReply{colorScheme: colorScheme})
|
||||
}
|
||||
}
|
||||
default:
|
||||
self.state = scanningText
|
||||
}
|
||||
|
||||
case scanningOsc:
|
||||
switch b {
|
||||
case '\a':
|
||||
self.state = scanningText
|
||||
replies = self.appendOscReply(replies)
|
||||
case '\x1b':
|
||||
self.state = scanningOscEscape
|
||||
default:
|
||||
self.appendToSequence(b)
|
||||
}
|
||||
|
||||
case scanningOscEscape:
|
||||
if b == '\\' {
|
||||
self.state = scanningText
|
||||
replies = self.appendOscReply(replies)
|
||||
} else {
|
||||
// Not a string terminator, so a new escape sequence has begun and
|
||||
// the one before it was cut short
|
||||
self.scanEscape(b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return replies
|
||||
}
|
||||
|
||||
// scanEscape handles the byte after an ESC.
|
||||
func (self *terminalReplyScanner) scanEscape(b byte) {
|
||||
switch b {
|
||||
case '[':
|
||||
self.startSequence(scanningCsi)
|
||||
case ']':
|
||||
self.startSequence(scanningOsc)
|
||||
case '\x1b':
|
||||
self.state = scanningEscape
|
||||
default:
|
||||
self.state = scanningText
|
||||
}
|
||||
}
|
||||
|
||||
func (self *terminalReplyScanner) startSequence(state terminalReplyScannerState) {
|
||||
self.state = state
|
||||
self.sequence = self.sequence[:0]
|
||||
self.oversized = false
|
||||
}
|
||||
|
||||
func (self *terminalReplyScanner) appendToSequence(b byte) {
|
||||
if len(self.sequence) >= maxTerminalReplyLength {
|
||||
self.oversized = true
|
||||
return
|
||||
}
|
||||
self.sequence = append(self.sequence, b)
|
||||
}
|
||||
|
||||
func (self *terminalReplyScanner) appendOscReply(replies []terminalReply) []terminalReply {
|
||||
if self.oversized {
|
||||
return replies
|
||||
}
|
||||
|
||||
if background, ok := parseBackgroundColorReport(string(self.sequence)); ok {
|
||||
replies = append(replies, terminalReply{background: &background})
|
||||
}
|
||||
|
||||
return replies
|
||||
}
|
||||
|
||||
// parseColorSchemeReport parses the parameters of CSI ? 997 ; <n> n
|
||||
func parseColorSchemeReport(params string) (ColorScheme, bool) {
|
||||
switch params {
|
||||
case "?997;1":
|
||||
return ColorSchemeDark, true
|
||||
case "?997;2":
|
||||
return ColorSchemeLight, true
|
||||
default:
|
||||
return ColorSchemeUnknown, false
|
||||
}
|
||||
}
|
||||
|
||||
// parseBackgroundColorReport parses the content of OSC 11 ; rgb:R/G/B, where
|
||||
// each of R, G and B has from one to four hex digits. Some terminals send
|
||||
// rgba:R/G/B/A instead.
|
||||
func parseBackgroundColorReport(content string) (colorful.Color, bool) {
|
||||
spec, ok := strings.CutPrefix(content, "11;")
|
||||
if !ok {
|
||||
return colorful.Color{}, false
|
||||
}
|
||||
|
||||
var components []string
|
||||
if rgb, ok := strings.CutPrefix(spec, "rgb:"); ok {
|
||||
components = strings.Split(rgb, "/")
|
||||
if len(components) != 3 {
|
||||
return colorful.Color{}, false
|
||||
}
|
||||
} else if rgba, ok := strings.CutPrefix(spec, "rgba:"); ok {
|
||||
components = strings.Split(rgba, "/")
|
||||
if len(components) != 4 {
|
||||
return colorful.Color{}, false
|
||||
}
|
||||
} else {
|
||||
return colorful.Color{}, false
|
||||
}
|
||||
|
||||
values := [3]float64{}
|
||||
for i := range values {
|
||||
component := components[i]
|
||||
if len(component) < 1 || len(component) > 4 {
|
||||
return colorful.Color{}, false
|
||||
}
|
||||
value, err := strconv.ParseUint(component, 16, 16)
|
||||
if err != nil {
|
||||
return colorful.Color{}, false
|
||||
}
|
||||
maxValue := uint64(1)<<(4*len(component)) - 1
|
||||
values[i] = float64(value) / float64(maxValue)
|
||||
}
|
||||
|
||||
return colorful.Color{R: values[0], G: values[1], B: values[2]}, true
|
||||
}
|
||||
@@ -0,0 +1,402 @@
|
||||
package gocui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func describeReplies(replies []terminalReply) []string {
|
||||
result := []string{}
|
||||
for _, reply := range replies {
|
||||
if reply.background != nil {
|
||||
result = append(result, reply.background.Hex())
|
||||
} else {
|
||||
result = append(result, reply.colorScheme.String())
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func TestTerminalReplyScanner(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
input string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "dark report",
|
||||
input: "\x1b[?997;1n",
|
||||
expected: []string{"dark"},
|
||||
},
|
||||
{
|
||||
name: "light report",
|
||||
input: "\x1b[?997;2n",
|
||||
expected: []string{"light"},
|
||||
},
|
||||
{
|
||||
name: "background ended by ST",
|
||||
input: "\x1b]11;rgb:1e1e/1e1e/1e1e\x1b\\",
|
||||
expected: []string{"#1e1e1e"},
|
||||
},
|
||||
{
|
||||
name: "background ended by BEL",
|
||||
input: "\x1b]11;rgb:ffff/ffff/ffff\a",
|
||||
expected: []string{"#ffffff"},
|
||||
},
|
||||
{
|
||||
name: "background with two hex digits per component",
|
||||
input: "\x1b]11;rgb:fd/f6/e3\a",
|
||||
expected: []string{"#fdf6e3"},
|
||||
},
|
||||
{
|
||||
name: "background with one hex digit per component",
|
||||
input: "\x1b]11;rgb:f/0/f\a",
|
||||
expected: []string{"#ff00ff"},
|
||||
},
|
||||
{
|
||||
name: "background with alpha",
|
||||
input: "\x1b]11;rgba:0000/2b2b/3636/ffff\a",
|
||||
expected: []string{"#002b36"},
|
||||
},
|
||||
{
|
||||
name: "replies among other input",
|
||||
input: "j\x1b[?997;1n\x1b[A\x1b[<0;10;5M\x1b]11;rgb:0000/0000/0000\x1b\\\x1b[?62;22c",
|
||||
expected: []string{"dark", "#000000"},
|
||||
},
|
||||
{
|
||||
name: "other reports",
|
||||
input: "\x1b[?997;3n\x1b[0n\x1b[?996n\x1b]10;rgb:ffff/ffff/ffff\a\x1b]4;1;rgb:ffff/0000/0000\a",
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "malformed backgrounds",
|
||||
input: "\x1b]11;rgb:zz/00/00\a\x1b]11;rgb:10/20\a\x1b]11;rgb:12345/0/0\a\x1b]11;rgb://\a\x1b]11;#ffffff\a",
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "reply after a sequence that was cut short",
|
||||
input: "\x1b]11;rgb:\x1b[?997;2n\x1b[?99\x1b]11;rgb:00/00/00\a",
|
||||
expected: []string{"light", "#000000"},
|
||||
},
|
||||
{
|
||||
name: "reply after an oversized sequence",
|
||||
input: "\x1b]11;" + strings.Repeat("x", 100) + "\a\x1b[?" + strings.Repeat("9", 100) + "n\x1b[?997;1n",
|
||||
expected: []string{"dark"},
|
||||
},
|
||||
{
|
||||
name: "oversized sequence that starts like a reply",
|
||||
input: "\x1b]11;rgb:00/00/00" + strings.Repeat("x", 100) + "\a",
|
||||
expected: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
scanner := terminalReplyScanner{}
|
||||
assert.Equal(t, s.expected, describeReplies(scanner.scan([]byte(s.input))))
|
||||
})
|
||||
|
||||
t.Run(s.name+", split in two", func(t *testing.T) {
|
||||
for i := range len(s.input) {
|
||||
scanner := terminalReplyScanner{}
|
||||
replies := scanner.scan([]byte(s.input[:i]))
|
||||
replies = append(replies, scanner.scan([]byte(s.input[i:]))...)
|
||||
assert.Equal(t, s.expected, describeReplies(replies), "split at %d", i)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run(s.name+", byte by byte", func(t *testing.T) {
|
||||
scanner := terminalReplyScanner{}
|
||||
replies := []terminalReply{}
|
||||
for i := range len(s.input) {
|
||||
replies = append(replies, scanner.scan([]byte{s.input[i]})...)
|
||||
}
|
||||
assert.Equal(t, s.expected, describeReplies(replies))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestColorSchemeQueries(t *testing.T) {
|
||||
allQueries := enableColorSchemeReports + requestColorScheme + requestBackgroundColor
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
term string
|
||||
termProgram string
|
||||
tcellNegotiate string
|
||||
expected string
|
||||
}{
|
||||
{name: "xterm", term: "xterm-256color", expected: allQueries},
|
||||
{name: "tmux", term: "tmux-256color", termProgram: "tmux", expected: allQueries},
|
||||
{name: "Terminal.app", term: "xterm-256color", termProgram: "Apple_Terminal", expected: requestBackgroundColor},
|
||||
{name: "WezTerm", term: "xterm-256color", termProgram: "WezTerm", expected: requestBackgroundColor},
|
||||
{name: "st", term: "st-256color", expected: ""},
|
||||
{name: "Linux console", term: "linux", expected: ""},
|
||||
{name: "VT100", term: "vt100", expected: ""},
|
||||
{name: "negotiation disabled", term: "xterm-256color", tcellNegotiate: "disable", expected: ""},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
assert.Equal(t, s.expected, colorSchemeQueries(s.term, s.termProgram, s.tcellNegotiate))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type fakeTty struct {
|
||||
input string
|
||||
written strings.Builder
|
||||
}
|
||||
|
||||
var _ tcell.Tty = &fakeTty{}
|
||||
|
||||
func (self *fakeTty) Start() error { return nil }
|
||||
func (self *fakeTty) Stop() error { return nil }
|
||||
func (self *fakeTty) Drain() error { return nil }
|
||||
func (self *fakeTty) NotifyResize(chan<- bool) {}
|
||||
func (self *fakeTty) WindowSize() (tcell.WindowSize, error) { return tcell.WindowSize{}, nil }
|
||||
func (self *fakeTty) Close() error { return nil }
|
||||
func (self *fakeTty) Write(p []byte) (int, error) { return self.written.Write(p) }
|
||||
func (self *fakeTty) Read(p []byte) (int, error) {
|
||||
n := copy(p, self.input)
|
||||
self.input = self.input[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (self *fakeTty) takeWritten() string {
|
||||
written := self.written.String()
|
||||
self.written.Reset()
|
||||
return written
|
||||
}
|
||||
|
||||
type colorSchemeTtyTest struct {
|
||||
fake *fakeTty
|
||||
tty *colorSchemeTty
|
||||
notifications []string
|
||||
}
|
||||
|
||||
func newColorSchemeTtyTest(termProgram string) *colorSchemeTtyTest {
|
||||
test := &colorSchemeTtyTest{fake: &fakeTty{}}
|
||||
test.tty = &colorSchemeTty{
|
||||
Tty: test.fake,
|
||||
queries: colorSchemeQueries("xterm-256color", termProgram, ""),
|
||||
}
|
||||
test.tty.subscribe(func(colorScheme DetectedColorScheme) {
|
||||
test.notifications = append(test.notifications, colorScheme.String())
|
||||
})
|
||||
return test
|
||||
}
|
||||
|
||||
// feed has the terminal send input, and has tcell read it
|
||||
func (self *colorSchemeTtyTest) feed(input string) {
|
||||
self.fake.input = input
|
||||
_, _ = self.tty.Read(make([]byte, 128))
|
||||
}
|
||||
|
||||
func (self *colorSchemeTtyTest) takeNotifications() []string {
|
||||
notifications := self.notifications
|
||||
self.notifications = nil
|
||||
return notifications
|
||||
}
|
||||
|
||||
func TestColorSchemeTtyPrefersTheBackground(t *testing.T) {
|
||||
test := newColorSchemeTtyTest("")
|
||||
|
||||
assert.NoError(t, test.tty.Start())
|
||||
assert.Equal(t, enableColorSchemeReports+requestColorScheme+requestBackgroundColor, test.fake.takeWritten())
|
||||
|
||||
// The terminal says it's light, but its background is dark
|
||||
test.feed("\x1b[?997;2n")
|
||||
assert.Equal(t, []string{"light (reported by the terminal)"}, test.takeNotifications())
|
||||
assert.Equal(t, "", test.fake.takeWritten(), "the background was asked for already")
|
||||
|
||||
test.feed("\x1b]11;rgb:1e1e/1e1e/1e1e\x1b\\\x1b[?62;22c")
|
||||
assert.Equal(t, []string{"dark (background #1e1e1e)"}, test.takeNotifications())
|
||||
assert.Equal(t, "", test.fake.takeWritten())
|
||||
|
||||
// A later report only makes us ask for the background again
|
||||
test.feed("\x1b[?997;2n")
|
||||
assert.Nil(t, test.takeNotifications())
|
||||
assert.Equal(t, requestBackgroundColor, test.fake.takeWritten())
|
||||
|
||||
test.feed("\x1b[?997;2n")
|
||||
assert.Equal(t, "", test.fake.takeWritten(), "the background was asked for already")
|
||||
|
||||
test.feed("\x1b]11;rgb:ffff/ffff/ffff\x1b\\")
|
||||
assert.Equal(t, []string{"light (background #ffffff)"}, test.takeNotifications())
|
||||
|
||||
// The same background again is no change
|
||||
test.feed("\x1b[?997;2n")
|
||||
assert.Equal(t, requestBackgroundColor, test.fake.takeWritten())
|
||||
test.feed("\x1b]11;rgb:ffff/ffff/ffff\x1b\\")
|
||||
assert.Nil(t, test.takeNotifications())
|
||||
}
|
||||
|
||||
func TestColorSchemeTtyWithoutBackground(t *testing.T) {
|
||||
test := newColorSchemeTtyTest("")
|
||||
|
||||
assert.NoError(t, test.tty.Start())
|
||||
test.fake.takeWritten()
|
||||
|
||||
test.feed("\x1b[?997;1n\x1b[?62;22c")
|
||||
assert.Equal(t, []string{"dark (reported by the terminal)"}, test.takeNotifications())
|
||||
|
||||
test.feed("\x1b[?997;2n")
|
||||
assert.Equal(t, []string{"light (reported by the terminal)"}, test.takeNotifications())
|
||||
assert.Equal(t, "", test.fake.takeWritten(), "a terminal that didn't answer before isn't asked again")
|
||||
|
||||
test.tty.onFocusGained()
|
||||
assert.Equal(t, "", test.fake.takeWritten())
|
||||
}
|
||||
|
||||
func TestColorSchemeTtyAsksAgainOnFocus(t *testing.T) {
|
||||
test := newColorSchemeTtyTest("Apple_Terminal")
|
||||
|
||||
assert.NoError(t, test.tty.Start())
|
||||
assert.Equal(t, requestBackgroundColor, test.fake.takeWritten())
|
||||
|
||||
test.tty.onFocusGained()
|
||||
assert.Equal(t, "", test.fake.takeWritten(), "the background was asked for already")
|
||||
|
||||
test.feed("\x1b]11;rgb:0000/0000/0000\a")
|
||||
assert.Equal(t, []string{"dark (background #000000)"}, test.takeNotifications())
|
||||
|
||||
test.tty.onFocusGained()
|
||||
assert.Equal(t, requestBackgroundColor, test.fake.takeWritten())
|
||||
test.tty.onFocusGained()
|
||||
assert.Equal(t, "", test.fake.takeWritten(), "the background was asked for already")
|
||||
|
||||
test.feed("\x1b]11;rgb:ffff/ffff/ffff\a")
|
||||
assert.Equal(t, []string{"light (background #ffffff)"}, test.takeNotifications())
|
||||
}
|
||||
|
||||
func TestColorSchemeTtyStopAndStart(t *testing.T) {
|
||||
test := newColorSchemeTtyTest("")
|
||||
|
||||
assert.NoError(t, test.tty.Start())
|
||||
test.fake.takeWritten()
|
||||
test.feed("\x1b[?997;1n\x1b]11;rgb:0000/0000/0000\a")
|
||||
|
||||
assert.NoError(t, test.tty.Stop())
|
||||
assert.Equal(t, disableColorSchemeReports, test.fake.takeWritten())
|
||||
|
||||
test.tty.onFocusGained()
|
||||
assert.Equal(t, "", test.fake.takeWritten(), "nothing is written to a stopped tty")
|
||||
|
||||
// Anything may have changed while we were stopped, so ask again
|
||||
assert.NoError(t, test.tty.Start())
|
||||
assert.Equal(t, enableColorSchemeReports+requestColorScheme+requestBackgroundColor, test.fake.takeWritten())
|
||||
}
|
||||
|
||||
func TestColorSchemeTtyStopWithoutReports(t *testing.T) {
|
||||
test := newColorSchemeTtyTest("WezTerm")
|
||||
|
||||
assert.NoError(t, test.tty.Start())
|
||||
test.fake.takeWritten()
|
||||
|
||||
assert.NoError(t, test.tty.Stop())
|
||||
assert.Equal(t, "", test.fake.takeWritten(), "reports were never turned on")
|
||||
}
|
||||
|
||||
func TestColorSchemeOfBackground(t *testing.T) {
|
||||
for background, expected := range map[string]ColorScheme{
|
||||
"rgb:0000/0000/0000": ColorSchemeDark,
|
||||
"rgb:1e/1e/1e": ColorSchemeDark,
|
||||
"rgb:00/2b/36": ColorSchemeDark,
|
||||
"rgb:70/70/70": ColorSchemeDark,
|
||||
"rgb:80/80/80": ColorSchemeLight,
|
||||
"rgb:fd/f6/e3": ColorSchemeLight,
|
||||
"rgb:ff/ff/ff": ColorSchemeLight,
|
||||
} {
|
||||
color, ok := parseBackgroundColorReport("11;" + background)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, expected, colorSchemeOfBackground(color), background)
|
||||
}
|
||||
}
|
||||
|
||||
// startWaiting starts waiting for replies, with a timeout too long to matter
|
||||
func (self *colorSchemeTtyTest) startWaiting() <-chan struct{} {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
self.tty.waitForReplies(time.Minute)
|
||||
close(done)
|
||||
}()
|
||||
return done
|
||||
}
|
||||
|
||||
func isClosed(ch <-chan struct{}) bool {
|
||||
select {
|
||||
case <-ch:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func assertDoneWaiting(t *testing.T, done <-chan struct{}) {
|
||||
t.Helper()
|
||||
assert.Eventually(t, func() bool { return isClosed(done) }, time.Second, time.Millisecond)
|
||||
}
|
||||
|
||||
func assertStillWaiting(t *testing.T, done <-chan struct{}) {
|
||||
t.Helper()
|
||||
assert.Never(t, func() bool { return isClosed(done) }, 50*time.Millisecond, time.Millisecond)
|
||||
}
|
||||
|
||||
func TestColorSchemeTtyWaitsForReplies(t *testing.T) {
|
||||
test := newColorSchemeTtyTest("")
|
||||
|
||||
assert.NoError(t, test.tty.Start())
|
||||
test.feed("\x1b[?997;1n\x1b]11;rgb:0000/0000/0000\a")
|
||||
assertDoneWaiting(t, test.startWaiting())
|
||||
|
||||
assert.NoError(t, test.tty.Stop())
|
||||
assert.NoError(t, test.tty.Start())
|
||||
|
||||
done := test.startWaiting()
|
||||
assertStillWaiting(t, done)
|
||||
test.feed("\x1b[?997;1n")
|
||||
assertStillWaiting(t, done)
|
||||
test.feed("\x1b]11;rgb:0000/0000/0000\a")
|
||||
assertDoneWaiting(t, done)
|
||||
|
||||
test.tty.onFocusGained()
|
||||
done = test.startWaiting()
|
||||
assertStillWaiting(t, done)
|
||||
test.feed("\x1b]11;rgb:0000/0000/0000\a")
|
||||
assertDoneWaiting(t, done)
|
||||
}
|
||||
|
||||
func TestColorSchemeTtyGivesUpWaiting(t *testing.T) {
|
||||
test := newColorSchemeTtyTest("")
|
||||
|
||||
assert.NoError(t, test.tty.Start())
|
||||
test.feed("\x1b[?997;1n\x1b]11;rgb:0000/0000/0000\a")
|
||||
test.tty.onFocusGained()
|
||||
|
||||
start := time.Now()
|
||||
test.tty.waitForReplies(20 * time.Millisecond)
|
||||
assert.GreaterOrEqual(t, time.Since(start), 20*time.Millisecond)
|
||||
}
|
||||
|
||||
func TestColorSchemeTtyDoesntWaitForRepliesThatNeverCame(t *testing.T) {
|
||||
test := newColorSchemeTtyTest("")
|
||||
|
||||
// The terminal answers nothing
|
||||
assert.NoError(t, test.tty.Start())
|
||||
assertDoneWaiting(t, test.startWaiting())
|
||||
|
||||
// The terminal reports its color scheme, but not its background
|
||||
test.feed("\x1b[?997;1n\x1b[?62;22c")
|
||||
assert.NoError(t, test.tty.Stop())
|
||||
assert.NoError(t, test.tty.Start())
|
||||
|
||||
done := test.startWaiting()
|
||||
assertStillWaiting(t, done)
|
||||
test.feed("\x1b[?997;1n")
|
||||
assertDoneWaiting(t, done)
|
||||
}
|
||||
@@ -13,14 +13,14 @@ func TestMouseReleaseDoesNotBreakDoubleClickDetection(t *testing.T) {
|
||||
g := newTestGui(t)
|
||||
view, _ := g.SetView("list", 0, 0, 20, 10, 0)
|
||||
doubleClicks := []bool{}
|
||||
assert.NoError(t, g.SetViewClickBinding(&ViewMouseBinding{
|
||||
g.SetViewClickBinding(&ViewMouseBinding{
|
||||
ViewName: "list",
|
||||
Key: MouseLeft,
|
||||
Handler: func(opts ViewMouseBindingOpts) error {
|
||||
doubleClicks = append(doubleClicks, opts.IsDoubleClick)
|
||||
return nil
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
for _, event := range []GocuiEvent{
|
||||
gocuiEventFromTcellEvent(tcell.NewEventMouse(view.x0+1, view.y0+1, tcell.ButtonPrimary, tcell.ModNone)),
|
||||
@@ -32,3 +32,37 @@ func TestMouseReleaseDoesNotBreakDoubleClickDetection(t *testing.T) {
|
||||
|
||||
assert.Equal(t, []bool{false, true}, doubleClicks)
|
||||
}
|
||||
|
||||
func TestASwallowedClickIsNoHalfOfADoubleClick(t *testing.T) {
|
||||
t.Cleanup(resetMouseState)
|
||||
resetMouseState()
|
||||
g := newTestGui(t)
|
||||
view, _ := g.SetView("list", 0, 0, 20, 10, 0)
|
||||
doubleClicks := []bool{}
|
||||
g.SetViewClickBinding(&ViewMouseBinding{
|
||||
ViewName: "list",
|
||||
Key: MouseLeft,
|
||||
Handler: func(opts ViewMouseBindingOpts) error {
|
||||
doubleClicks = append(doubleClicks, opts.IsDoubleClick)
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
press := gocuiEventFromTcellEvent(
|
||||
tcell.NewEventMouse(view.x0+1, view.y0+1, tcell.ButtonPrimary, tcell.ModNone))
|
||||
release := gocuiEventFromTcellEvent(
|
||||
tcell.NewEventMouse(view.x0+1, view.y0+1, tcell.ButtonNone, tcell.ModNone))
|
||||
|
||||
// A click the client rejects, as it does for one landing behind a popup panel.
|
||||
g.ShouldHandleMouseEvent = func(*View, KeyName) bool { return false }
|
||||
assert.NoError(t, g.onKey(&press))
|
||||
assert.NoError(t, g.onKey(&release))
|
||||
assert.Empty(t, doubleClicks)
|
||||
|
||||
// The same spot clicked again once clicks are accepted. It is a click of its
|
||||
// own, not the second half of the one that nothing acted on.
|
||||
g.ShouldHandleMouseEvent = func(*View, KeyName) bool { return true }
|
||||
assert.NoError(t, g.onKey(&press))
|
||||
|
||||
assert.Equal(t, []bool{false}, doubleClicks)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user