Compare commits

...
Author SHA1 Message Date
Stefan HallerandClaude Fable 5 e81cfdba33 Add a keybinding for toggling mouse capture at runtime (%)
Some terminals provide a modifier key that bypasses mouse capture so
that text can be selected with the mouse and copied to the clipboard
(option in iTerm, shift in Ghostty), but others don't (e.g. the
terminals built into VS Code and Zed). In those, selecting text
requires setting gui.mouseEvents to false in the config file and back
again afterwards, which is inconvenient.

Add a global keybinding (% by default) that toggles mouse capture on
the fly. Like the whitespace toggle, it flips the in-memory config
value, so the toggled state lasts until the config file is changed and
reloaded.

The key needs to be free in every panel, and % is; as a plain rune it
also arrives reliably in every terminal on all platforms, and it can't
fire while typing in a text prompt, since editors consume plain runes.
An alt-based binding would have been more mnemonic, but doesn't reach
lazygit in the built-in terminals of VS Code and Zed on macOS (the
very terminals that need this feature) unless the option key is
configured to act as meta; and a ctrl-based binding would burn one of
the few remaining free ctrl keys, which are better saved for future
features that get used more frequently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:21:18 +02:00
Stefan Haller a61727cd5e Add a hint about how to use diff --color-words or --word-diff in lazygit (#5795)
Since this frequently comes up as a feature request (but there are
reasons why we don't want to add it), explain how to do this in lazygit
today.

See
https://github.com/jesseduffield/lazygit/pull/5784#issuecomment-4925324150.
2026-07-10 15:47:05 +02:00
Stefan Haller c81c08071f Add a hint about how to use diff --color-words or --word-diff in lazygit
Since this frequently comes up as a feature request (but there are
reasons why we don't want to add it), explain how to do this in lazygit
today.
2026-07-10 15:43:28 +02:00
Stefan Haller e59c1d1cb7 Make model<->view index conversions independent of rendering (#5785)
The model<->view index conversions were derived from arrays that only
renderLines populated. That made them depend on the list having been
rendered (so a conversion before the first render ignored the non-model
items), and it made them go stale whenever the model changed after a
render: converting an index then returned a wrong result, and once the
model had grown past the last rendered length the conversion indexed a
too-short array and panicked (seen in cherry_pick under -race).

The conversion is a pure function of the current list length and the
current non-model items, and needs none of the rendered display strings.
Compute it directly and drop the cached arrays, so the result is always
consistent with the current model and no longer depends on rendering.
2026-07-09 15:10:31 +02:00
Stefan HallerandClaude Opus 4.8 4e907c6b3e Compute list index conversions independently of rendering
The model<->view index conversions were derived from arrays that only
renderLines populated. That made them depend on the list having been
rendered (so a conversion before the first render ignored the non-model
items), and it made them go stale whenever the model changed after a
render: converting an index then returned a wrong result, and once the
model had grown past the last rendered length the conversion indexed a
too-short array and panicked (seen in cherry_pick under -race).

The conversion is a pure function of the current list length and the
current non-model items, and needs none of the rendered display strings.
Compute it directly and drop the cached arrays, so the result is always
consistent with the current model and no longer depends on rendering.

searchModelCommits converts every commit's index, and building the
non-model items can be O(len) mid-rebase, so it would now be quadratic;
snapshot the non-model items once via modelToViewIndexConverter instead
of rebuilding them per index.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 14:54:15 +02:00
Stefan HallerandClaude Opus 4.8 d4a606c685 Demonstrate that list index conversions depend on rendering
ModelIndexToViewIndex and ViewIndexToModelIndex read conversion arrays
that only renderLines populates. So converting an index before the list
has been rendered ignores the non-model items (e.g. section headers) and
returns a wrong result; the same staleness makes a conversion after the
model has grown index a too-short array and panic (seen in cherry_pick
under -race).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 14:54:15 +02:00
Stefan Haller d94ca63e6d Synchronize ViewBufferManager.Close with a starting task (#5786)
Fixes a race condition related to ViewBufferManager's stopCurrentTask
field.
2026-07-09 14:53:45 +02:00
Stefan HallerandClaude Opus 4.8 c21ce61729 Synchronize ViewBufferManager.Close with a starting task
Close read and called stopCurrentTask with no lock, while NewTask's
goroutine assigns it (and constructs the sync.Once it closes over) under
waitingMutex. On shutdown Close runs while a render task spawned by the
last layout is still starting, so the two raced on the field and the
once (three DATA RACE blocks under -race, e.g. cherry_pick).

Read stopCurrentTask once under waitingMutex and call the captured value
instead of re-reading the field, which establishes the happens-before
the once needs. This can't deadlock: no task holds waitingMutex across a
blocking UI-thread hop, so Close can always take it, and a task wedged in
such a hop is still bounded by the existing 3s timeout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 14:28:29 +02:00
Stefan Haller bda505148b Make integration tests using commits more robust (#5782)
Some tests assert that a specific commit subject does or doesn't occur
in the main view; interactive_rebase/outside_rebase_range_select.go is
an example for this, it asserts `t.Views().Main().Content(
DoesNotContain("commit 06"))`. The problem with this kind of assertion
and our test commit naming scheme is that the diff view begins with a
"commit <hash>" line, and when that hash happens to start with "06" the
assertion matched it and failed spuriously. This was usually masked by
our MaxAttempts=2 that we currently use for integration tests (it's
quite unlikely that the commit gets a hash beginning with "06" twice in
a row). However, we want to get to a state where we can set MaxAttempts
to 1, so make this more robust by changing our naming scheme.
2026-07-09 12:02:25 +02:00
Stefan Haller d181615c31 Make integration tests using commits more robust
Some tests assert that a specific commit subject does or doesn't occur
in the main view; interactive_rebase/outside_rebase_range_select.go is
an example for this, it asserts `t.Views().Main().Content(
DoesNotContain("commit 06"))`. The problem with this kind of assertion
and our test commit naming scheme is that the diff view begins with a
"commit <hash>" line, and when that hash happens to start with "06" the
assertion matched it and failed spuriously. This was usually masked by
our MaxAttempts=2 that we currently use for integration tests (it's
quite unlikely that the commit gets a hash beginning with "06" twice in
a row). However, we want to get to a state where we can set MaxAttempts
to 1, so make this more robust by changing our naming scheme.
2026-07-09 11:56:27 +02:00
Stefan Haller 8d6d1f0908 Make scrolling down a very long diff with the scroll wheel much smoother (#5780)
When showing a very long diff (thousands of lines), scrolling down with
the mouse wheel was rather choppy; now it's very smooth and fast.
2026-07-09 09:51:15 +02:00
Stefan HallerandClaude Opus 4.8 585c7f126d Cache each line's wrapping so scrolling doesn't re-wrap the whole buffer
refreshViewLinesIfNeeded re-wrapped every line of the buffer whenever
the view was tainted. That's cheap for short content, but scrolling a
long diff calls it constantly: adjustDownwardScrollAmount queries
ViewLinesHeight on every scroll event, and each newly-read line taints
the view, so every notch re-wrapped the entire buffer. Wrapping measures
each cell's width (uniseg) and allocates per line, so once you'd scrolled
far enough down the diff, scrolling turned sluggish - the cost grew with
how much had been read. (A CPU profile of scrolling deep in a long diff
put 77% of the time in lineWrap, reached almost entirely via
ViewLinesHeight rather than draw.)

Cache each line's wrapped result on the lineType, keyed by the width it
was wrapped at, and only re-wrap lines that have actually changed since
the last refresh. A firstDirtyLine index, updated in the same three
places that set `tainted` (write, clearViewLines' callers, SetHighlight),
marks the lowest line that might have changed; lines below it with a
matching cached width reuse their cached wrapping. The cache lives on the
line, so it's freed with the line when the view's content is replaced
(e.g. selecting a different commit) - it doesn't accumulate across a
session.

The wrapping cost per scroll now scales with the number of lines just
read, not with the total size of the buffer, so scrolling stays smooth
no matter how far down you are.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 09:32:35 +02:00
Stefan HallerandClaude Opus 4.8 73d7b443ec Render content-only when a task reads more lines into a view
Reading more lines into a lazy-loaded view (e.g. a diff being scrolled)
never changes the window layout, and after the first screenful it
doesn't even change the visible content - the new lines land below the
viewport, so the only thing that changes on screen is the scrollbar
thumb. Yet each read triggered a full render: a layout pass plus a
redraw of every view. On a slow terminal that full-screen repaint on
every read is a big part of why scrolling through a not-yet-fully-read
diff stutters.

Route the task's refresh through a content-only render instead. It
skips the layout pass and only redraws the views whose content changed,
leaving tcell's cell-level dirty tracking to emit just the cells that
actually differ (in the steady state, the scrollbar column).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 09:32:35 +02:00
Stefan HallerandClaude Opus 4.8 cbf220c497 Read lines based on scroll position instead of a fixed per-notch delta
When scrolling a lazy-loaded view (a diff in the main view, the command
log, etc.), we top up the view's line buffer by reading more lines from
the still-running task. This was driven by asking the task to read a
fixed number of *additional* lines on every scroll event, which had two
problems:

- It was decoupled from the scroll position. Scrolling down, back up,
  and down again re-read lines that had already been read, so the buffer
  crept towards the end of the input regardless of where the user
  actually scrolled.

- A single wheel notch only bought a single notch worth of runway, so
  fast scrolling constantly outran the reader and had to wait for the
  next read (and re-render) on every notch.

Make ReadLines take an absolute target total instead of a delta: the
task tracks how many lines it has read and only reads the shortfall, so
requests are idempotent. Callers now ask to fill the viewport at the
current scroll position plus a few screenfuls of read-ahead, which gives
scrolling enough runway to stay smooth.

The four call sites all wanted the same "fill this view" computation, so
consolidate them into a single ReadLinesToFillView helper.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 09:32:35 +02:00
Stefan Haller 3491a15f6e Fix data race with command log (#5779)
LogAction and LogCommand are called from git worker goroutines (every
command a worker runs logs itself, and controllers log an action before
kicking off their worker), where they set the Extras view's Autoscroll
flag and append to GuiLog while the UI thread reads both when it lays
out and draws the view. Bounce the writes onto the UI thread instead.

This doesn't fix any user-visible issue that I know of; labelling it as
"maintenance" rather than "bug" for that reason. It is one of many steps
that gets us closer to running our test suite with `-race`.
2026-07-09 08:45:59 +02:00
Stefan HallerandClaude Opus 4.8 1268a589d6 Write the command log on the UI thread
LogAction and LogCommand are called from git worker goroutines (every
command a worker runs logs itself, and controllers log an action before
kicking off their worker), where they set the Extras view's Autoscroll
flag and append to GuiLog while the UI thread reads both when it lays out
and draws the view. Bounce the writes onto the UI thread instead.

Use the background variant so the bounce doesn't count towards lazygit
being busy: writing the command log is incidental display work, and a
foreground task would let an in-flight log write refuse a concurrent repo
switch (the same reason view-buffer renders and toasts are backgrounded).
Ordering between successive log calls is preserved by the bounce FIFO.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 08:41:10 +02:00
Stefan Haller 73714a3b38 Fix data race with status string (#5777)
GetStatusString and HasStatus read the statuses slice without holding
the mutex that addStatus and removeStatus take when they mutate it. The
readers run on the spinner-render worker (which polls GetStatusString
every frame) while removeStatus fires from the waiting-status and
toast-expiry goroutines, so the unguarded reads race the concurrent
writes. Take the mutex in the readers too.

This doesn't fix any user-visible issue that I know of; labelling it as
"maintenance" rather than "bug" for that reason. It is one of many steps
that gets us closer to running our test suite with `-race`.
2026-07-09 07:06:46 +02:00
Stefan HallerandClaude Opus 4.8 2420fc7b76 Lock the status list when reading it
GetStatusString and HasStatus read the statuses slice without holding
the mutex that addStatus and removeStatus take when they mutate it. The
readers run on the spinner-render worker (which polls GetStatusString
every frame) while removeStatus fires from the waiting-status and
toast-expiry goroutines, so the unguarded reads race the concurrent
writes. Take the mutex in the readers too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 18:24:18 +02:00
Stefan Haller fe4c195370 Perform refresh model and view updates on the UI thread instead of using mutexes (#5767)
Refresh workers do their git work on background goroutines and then
mutate the model (`Model().Commits`, `.Branches`, …) and re-render views
directly from those goroutines, racing the UI thread's own cursor and
render code. This has been a long-standing source of flaky integration
tests, and it's what prevents us from running the e2e suite under the
race detector.

This PR removes that class of races by updating refresh state only on
the UI thread, and drops the mutexes that were standing in for that
discipline. It's an internal concurrency change with no intended
difference in normal use (the one small user-facing addition is noted at
the end).

- Each refresh scope does its git work on a worker, then enqueues its
model write onto the UI thread ("bouncing") through a single primitive,
so all model mutations are serialized on the one UI goroutine alongside
the cursor/render code they used to race.
- That primitive is generation-guarded: if you switch repos while a
refresh is in flight, the queued write is dropped instead of being
applied to the new repo.
- The inputs a refresh worker reads (model fields, selection, modes) are
now captured on the UI thread up front, so the worker computes from an
immutable snapshot. Worker-issued refreshes use a dedicated entry point,
and a debug-only assertion checks that the entry point matches the
calling goroutine.
- A few flags written from workers are made atomic rather than bounced.
- All six refresh mutexes are removed as redundant; the branches mutex
is replaced by a small branch-load sequence guard so the recency-sorted
result still wins at startup.
- Repo switching now runs on the UI thread rather than a worker,
removing a race on the shared gui state.

This is one step toward being able to run the test suite under `-race`
in CI — the remaining view-buffer rendering races are left for a
follow-up.

The one user-facing addition: switching repositories while a foreground
git operation is still running is now refused with a toast, instead of
running the operation's remaining commands against the newly-switched
repo.
2026-07-07 18:14:58 +02:00
Stefan Haller 4d33d9df8b Mention the Then rule in AGENTS.md 2026-07-07 18:10:45 +02:00
Stefan HallerandClaude Opus 4.8 19b34851ff Guard the view-render and prompt-dismiss bounces on the generation
The model-update bounces already drop themselves when the repo is
switched mid-refresh (onUIThreadUnlessRepoChanged), but three bounces
that touch the UI without writing the model did not: refreshView's
render, the staging-panel refresh, and the stale continue-rebase prompt
dismissal. All three ran unconditionally on the UI thread, so a
background refresh in flight across a repo switch could render the old
repo's data (through a context object belonging to the now-replaced
context tree), or pop the new repo's popup based on the old repo's
prompt state.

Route them through onUIThreadUnlessRepoChanged too, so they're dropped
alongside the model writes they accompany. This also fixes the dismiss
bounce using the raw foreground OnUIThread, which ignored the background
flag every other bounce in a background refresh respects.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 18:10:45 +02:00
Stefan HallerandClaude Opus 4.8 2c3a6acafa Thread a refreshEnv through the refresh scopes
Every refresh scope needs two ambient values to bounce its model and
view updates back to the UI thread safely: the background flag (which
picks the dispatch variant that doesn't count towards lazygit being
busy) and the repo generation that guards the bounce against a repo
switch. These were threaded separately — background as a parameter on
every refreshXxx function, generation re-read from the model inside each
one. Bundle them into a single refreshEnv passed through instead, so the
guard has a home to grow into (the next commit needs the generation in
refreshView, which currently has no access to it).

Capturing the generation once, at the start of the refresh, is also more
correct than the previous per-function re-read. The baseline should
reflect the repo whose inputs the refresh snapshotted (all captured up
front on the UI thread), not whenever each scope's worker happens to
wake. With the per-function read, a background refresh whose worker woke
after a repo switch would read the new generation and let its bounce
through, writing data computed from the old repo's inputs into the new
repo; capturing up front makes that bounce drop instead.

No behavior change for foreground refreshes, where the UI thread is held
for the whole refresh and the generation can't move under it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 18:10:45 +02:00
Stefan Haller fefb3b632e Clear the preserved commit message on the UI thread
The commit's gpg onSuccess runs on a worker when the command output is
streamed, so its ClearPreservedCommitMessage wrote commit-message
context state off the UI thread. Bounce that write through OnUIThread.
2026-07-07 18:10:45 +02:00
Stefan Haller 12757e2723 Swap the file-path suggestions trie on the UI thread
GetFilePathSuggestionsFunc builds the trie on a worker (the slow
AllRepoFiles walk) and then assigned Model().FilesTrie and refreshed the
suggestions panel from there, racing the UI thread that reads the trie.
Keep the build on the worker but bounce just the model assignment and
the refresh through OnUIThread.
2026-07-07 18:10:45 +02:00
Stefan Haller 6cd93de5b9 Cancel the commit-file range selection on the UI thread after discard
The discard handler cancelled the commit-files range selection from its
WithWaitingStatus worker. Bounce it through OnUIThread, keeping it after
the successful CheckMergeOrRebase as before.
2026-07-07 18:10:45 +02:00
Stefan Haller e7105a3138 Collapse the branch range selection on the UI thread after a delete
The three branch-delete handlers and the two worktree-removal
continuations collapsed the Branches/RemoteBranches range selection from
their worker goroutine, racing the UI thread. Wrap each collapse in
OnUIThread, keeping it in the same spot relative to the refresh (FIFO
preserves the collapse-then-refresh order the name-restore depends on).
2026-07-07 18:10:45 +02:00
Stefan Haller 67b0a6b1a4 Move the pull-patch panel close and focus off the worker
The pull-patch-into-new-commit handlers closed the commit-message panel
and, on success, pushed the local-commits context from inside the
WithWaitingStatus worker. Close the panel in OnConfirm before
dispatching (UI thread), and bounce the post-rebase context push through
OnUIThread, keeping it on the success path.
2026-07-07 18:10:45 +02:00
Stefan Haller f07e94afe0 Keep RebaseOntoRef's marked-base access on the UI thread
The three rebase-onto menu items read Modes().MarkedBaseCommit.GetHash()
(a bare string field) and, on success, cleared it via
ResetMarkedBaseCommit and pushed the commits context — all from the
WithWaitingStatus worker, racing the UI thread. Read the marked base
hash before dispatching, and bounce the post-rebase reset and context
push through OnUIThread, still guarded by the success check so they
don't run on the conflict path.
2026-07-07 18:10:45 +02:00
Stefan Haller 5d8c893497 Capture commits and set selection on the UI thread for squash/fixup/drop
interactiveRebaseWithFlag and dropMergeCommit ran inside the
WithWaitingStatus worker but read Model().Commits and wrote the
selection (SetSelection(startIdx)) there, racing the UI thread. Thread
the commits slice in from each caller, and hoist the pre-rebase
selection into a UI-thread helper (selectRebaseResultCommit) called
before dispatching — squash/fixup unconditionally, drop only on the
non-merge path, matching the previous action guard.
2026-07-07 18:10:45 +02:00
Stefan Haller 6c38ddc9a7 Set ResetToRef's post-reset selection via refresh intents
ResetToRef ran on a worker and wrote the local-commits and reflog
selection directly (SetSelection(0) on both) before its refresh, racing
the UI thread. Fold those into the refresh's selection intents:
SelectHeadCommit for the commits (after a reset HEAD is the top commit,
and mid-interactive-rebase it correctly picks the real head over the
first todo entry) and SelectTopReflogCommit for the reflog. The
now-atomic SetLimitCommits stays where it is.
2026-07-07 18:10:45 +02:00
Stefan Haller 6d21efb515 Make the local-commits limit-commits flag atomic
CheckoutRef and ResetToRef set this flag from their worker goroutine
(to load fewer commits for speed) while the commits refresh reads it on
the UI thread in captureCommitsState to decide how many to load — a data
race. Make it an atomic.Bool so those writes are safe where they are,
rather than routing the flag through a refresh intent. Precedent:
Branch.BehindBaseBranch.
2026-07-07 18:10:45 +02:00
Stefan Haller 2edfeac538 Capture the commit-file discard and patch-toggle reads on the UI thread
discard reads Model().Commits and the selected commit index from its
WithWaitingStatus worker; read them in HandleConfirm instead.

toggleForPatch reads the commit-files ref name from the worker, and its
startPatchBuilder call reads the context's canRebase and diff range from
there too. Capture the ref name and run startPatchBuilder in
HandleConfirm before dispatching; PatchBuilder.Start only assigns
fields, so moving it off the worker changes no timing.

discard still collapses the range selection from the worker; that write
is a separate concern, left for a follow-up.
2026-07-07 18:10:45 +02:00
Stefan Haller 462d75232b Look up the submodule file and branch worktree on the UI thread
ResetSubmodule and fastForward each call a helper that reads the model
from inside their worker: FileForSubmodule reads Model().Files and
worktreeForBranch reads Model().Worktrees, racing the UI thread's model
writes. Hoist both lookups above the worker dispatch.
2026-07-07 18:10:45 +02:00
Stefan Haller fceba31212 Capture moveCommitsToNewBranch's model reads on the UI thread
The two move helpers run inside the WithWaitingStatus worker that
withNewBranchNamePrompt dispatches to, but read Model().Files/Submodules
(to decide whether to auto-stash) and Model().Commits (the unpushed
commits to cherry-pick off the base branch) from there, racing the UI
thread's model writes. Compute mustStash — needed by both paths — at the
top, and the unpushed commits in the off-of-main menu item, on the UI
thread, and pass them into the helpers.
2026-07-07 18:10:45 +02:00
Stefan Haller b4a976834f Capture reword/amend/author commit reads on the UI thread
handleReword, amendTo, and the reset/set/add-co-author handlers pass
Model().Commits (and the selected line index) to a git rebase from
inside the WithWaitingStatus worker, racing the UI thread's model
writes. Read them on the UI thread before dispatching.

The author handlers index the full commit list by absolute start/end, so
the range sub-slice withItemsRange hands amendAttribute is not what they
need; capture the full Model().Commits there and thread it through.
2026-07-07 18:10:45 +02:00
Stefan Haller eb95ae15f3 Capture the custom-patch handlers' commit reads on the UI thread
These handlers dispatch their rebase to a worker via WithWaitingStatus
but read Model().Commits (and, for move-to-selected-commit, the selected
line index) from inside that worker, racing the UI thread's model
writes. Read them on the UI thread before dispatching and close over the
results.

getPatchCommitIndex stays as-is: moving its call out of the worker makes
its own Model().Commits read UI-thread-bound too, so the identical copy
in patch_building_controller.go needs no matching signature change.

The two pull-patch-into-new-commit handlers still push a context and
close the commit-message panel from the worker; those writes are a
separate concern, left for a follow-up.
2026-07-07 18:10:45 +02:00
Stefan HallerandClaude Opus 4.8 5162a768eb Guard every refresh's entry point, not just the commits scope
With every scope's worker reads now captured on the UI thread and every
worker caller on RefreshFromWorker, the debug entry-point assertion no longer
needs to be scoped to the commits refresh. Move it to the top of
performRefresh so it guards every refresh regardless of which scopes it
touches, and drop the per-scope gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 18:10:45 +02:00
Stefan HallerandClaude Opus 4.8 fd6b20847a Capture the files, reflog, branches and stash refresh inputs
The remaining refresh scopes each still read model, context, and mode state
directly on their worker, racing the UI thread — the same class of race the
commits refresh had:

- files reads Model.Files (to detect resolved conflicts and drive the
  auto-stage) and the Files context's ForceShowUntracked;
- reflog reads the existing reflog slices (for the incremental fetch),
  Model.HashPool and the filtering path/author;
- branches reads Model.MainBranches and the previous branches (for the
  BehindBaseBranch carry-over);
- stash reads the filtering path.

Gather each scope's inputs into an immutable snapshot on the UI thread (via
captureOnUIThread) before dispatching the git work, and have the refresh
compute from the snapshot — for branches, threaded through both the immediate
and the recency-sorted startup loads, which share one snapshot (the
BehindBaseBranch carry-over is identical either way). Status, tags and
worktrees read nothing UI-owned, so they're left alone.

For the snapshots to actually run on the UI thread, the worker callers that
reach these scopes must announce themselves: convert the submodule
operations, the submodule stash-and-reset, and the background files poller
to RefreshFromWorker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 18:10:45 +02:00
Stefan HallerandClaude Opus 4.8 3e5c99e1e4 Make the started-in-lazygit and startup-stage flags atomic
GuiRepoState.mergeOrRebaseStartedInLazygit and StartupStage are plain
fields, but they're written and read from worker goroutines: the former
from both the files refresh and the merge/rebase result path (which runs on
a worker for the async callers), the latter from the reflog/branches load as
it transitions the startup stage. Those are data races.

Make both atomic, like Branch.BehindBaseBranch. They're leaf flags, not
mutexes guarding model or view state, so an atomic is the natural fit and
keeps the merge/rebase result path out of this change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 18:10:45 +02:00
Stefan HallerandClaude Opus 4.8 56989922e4 Capture the remotes/sub-commits/commit-files/rebase-commits inputs
These four refreshes each read model, context, and mode state directly on
their worker — the same class of race the commits refresh had:

- remotes reads the selected remote (Contexts().Remotes.GetSelected), needed
  to keep the remote-branches selection valid;
- sub-commits reads the SubCommits ref/limit/divergence, the filtering
  path/author, and Model.MainBranches/HashPool;
- commit-files reads the diff endpoints (CommitFiles from/to and the diffing
  args);
- rebase-commits reads Model.HashPool/Commits.

Give each the same treatment as commits: gather its inputs into an
immutable snapshot on the UI thread (via captureOnUIThread, inline for a
UI-thread refresh, hopped for a worker one) before dispatching the git work,
and have the refresh compute from the snapshot. The commit-files re-init
inside the commits refresh captures its endpoints in the bounce, right after
ReInit sets them, before dispatching to the worker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 18:10:45 +02:00
Stefan HallerandClaude Opus 4.8 988d04bda9 Assert a refresh uses the entry point matching its goroutine
Now that every commits-reaching refresh issued from a worker goes through
RefreshFromWorker, guard the choice: in debug builds, panic if a refresh was
issued from the UI thread as RefreshFromWorker or from a worker as Refresh.

The caller's own goroutine is recorded at the top of performRefresh, before
a BLOCK_UI refresh dispatches onto the UI thread, so the check holds for
every mode rather than being fooled by BLOCK_UI. It's scoped to the commits
refresh for now, the only converted scope; once the rest are converted the
guard can move up to cover every refresh unconditionally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 18:10:45 +02:00
Stefan HallerandClaude Opus 4.8 558fd2c9d3 Route merge/rebase result handling to the right refresh entry point
CheckMergeOrRebaseWithRefreshOptions refreshes after a merge/rebase step,
and until now always via the UI-thread Refresh. Most of its callers are on a
worker (the WithWaitingStatus/WithInlineStatus merge, squash-merge, rebase,
pull, amend, drop, and patch-move handlers), so that refresh reads the
commits scope off the UI thread — the race the previous commit addresses for
everything else.

Split it: the default is for worker callers and refreshes via
RefreshFromWorker; a new CheckMergeOrRebaseWithRefreshOptionsFromUIThread is
for the handlers that run the step synchronously on the UI thread
(WithWaitingStatusSync, kept sync so rapid key presses batch): move up/down,
revert, squash-fixups, cherry-pick paste, and patch-discard.

The two share a private impl carrying which thread the caller is on, and the
auto-skip recursion (genericMergeCommandImpl for an empty commit) threads it
through so the follow-up step refreshes on the same thread. The
merge-and-commit refresh in SquashMergeCommitted, also on a worker, moves to
RefreshFromWorker to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 18:10:45 +02:00
Stefan HallerandClaude Opus 4.8 080542c9fb Capture the commits refresh's inputs on the UI thread
A commits refresh does its git work on a worker and then reads the model,
the contexts, and the modes for that work directly from there:
LocalCommits.GetSelectionRangeAndMode/GetLimitCommits/GetShowWholeGitGraph,
Model.Commits/MainBranches/HashPool, the filtering path/author. Those are
owned by the UI thread, which is concurrently running the cursor and render
code, so the reads race it — the dominant, confirmed source of the
commits-scope flakes (the startup ClampSelection vs GetSelectionRangeAndMode
race, for one).

Gather them into an immutable capturedCommitState on the UI thread, before
the git work is dispatched, and have refreshCommitsWithLimit compute from
that snapshot. UI-thread callers capture inline; worker callers can't (a
SYNC/BLOCK_UI refresh parks the UI thread at wg.Wait, so hopping from a
scope sub-worker would deadlock), so the capture is lifted out of the scope
worker into the refresh orchestration, and worker callers announce
themselves with a new RefreshFromWorker entry point that hops the capture to
the UI thread and blocks for it (OnUIThreadAndWait). BLOCK_UI runs the whole
refresh on the UI thread regardless of the caller, so it captures inline
too.

Every refresh issued from a worker that reaches the commits (or branches,
which pulls in commits) scope is converted: the fast-forward, branch/tag
delete, worktree remove/detach, push, reword-via-rebase, author edits,
custom-command, hard-reset-with-autostash, reset-to-ref, fetch-and-checkout,
gpg-stream, post-fetch, and external-change-poller refreshes, plus the
branch checkout and move-commits-to-new-branch refreshes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 18:10:45 +02:00
Stefan HallerandClaude Opus 4.8 1def541acb Add IsUIThread and OnUIThreadAndWait to gocui
The next commits move refresh workers to read UI-thread-owned state (the
model, contexts, selection) on the UI thread rather than off it. Two
primitives support that:

- OnUIThreadAndWait runs a function on the main event loop and blocks the
  caller until it has run, so a worker can read that state without racing.
  OnUIThreadAndWaitBackground is the same for background routines, whose
  work must not count towards the program being busy.
- IsUIThread reports whether the caller is on the main event loop, for a
  debug-only assertion that a refresh was issued from the thread it claims.
  It records the main loop's goroutine id in MainLoop and compares via
  goid, so it's promoted from an indirect to a direct dependency.

goid is used only by that debug assertion, never to drive production
control flow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Opus 4.8 f0ea537956 Fetch pull requests using the freshly-loaded branches and remotes
The PR fetch needs the current branches (for their upstreams) and
remotes to know what to query. It read them from Model().Branches /
Model().Remotes on its own worker, after waiting on branchesAndRemotesWg
for the branches and remotes refreshes to finish.

That wait no longer guarantees fresh data: those refreshes now write the
model in a bounce onto the UI thread, and Done() fires before the bounce
has been processed. So the fetch read the pre-refresh lists — most
visibly, checking out a branch that has a PR wouldn't show that PR until
the next refresh, because the fetch queried the old branch set.

Have refreshBranches / refreshReflogAndBranches / refreshRemotes return
what they loaded, stash it in locals in Refresh, and hand it to the
fetch. The wait on branchesAndRemotesWg orders the fetch after both
loads have stored their slices, so it fetches against exactly the
branches and remotes that were just loaded, with no model read on the
worker. The previous commit guarantees both are always in scope when
pull requests are, so no fallback is needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Opus 4.8 23cfa9b070 Also refresh branches and remotes when refreshing pull requests
The pull-request fetch queries GitHub for the tracking branches'
upstreams against the configured remotes. It therefore depends on the
branches and remotes being up to date; a refresh that asks for pull
requests but not for those (e.g. checking out a branch) would fetch
against a stale branch/remote list — for instance missing the PR of the
branch just checked out.

Expand the scope so pull requests always co-refresh branches and
remotes. This also sets up the next commit to hand the freshly-loaded
branches and remotes straight to the fetch, instead of reading them
back from the model (which, now that those writes are bounced onto the
UI thread, would be stale on the worker).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Opus 4.8 7c4d8045f9 Clamp the commit-file tree selection when the tree is rebuilt
CommitFileTreeViewModel embedded the low-level tree's SetTree, which
rebuilds the node list without touching the cursor. So after a shrinking
rebuild (e.g. moving a patch out into the index removes a file), the
selection index could be left past the end of the tree. GetSelectedItems
then indexes out of range and returns a nil node, which segfaults callers
such as canEditFiles when the options map is rendered during layout.

Override SetTree to ClampSelection after the rebuild. Unlike
FileTreeViewModel we deliberately don't also re-find the selected node by
path: that walk lands on the containing directory when a file is removed
from a dir that then collapses, whereas keeping the clamped index lands
on the sibling file (see discard_old_file_changes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Opus 4.8 bd6081d601 Select the checked-out branch via a refresh intent, not off-thread
Operations that check something out (checkout, create branch, move
commits to a new branch, fetch-and-checkout) selected the newly
checked-out branch by calling SelectFirstBranchAndFirstCommit() before
the refresh and passing KeepBranchSelectionIndex so the refresh wouldn't
override it. That set the selection directly, usually from a worker
goroutine (WithWaitingStatus/WithInlineStatus). Now that the refresh's
own selection write is bounced onto the UI thread, the two writes could
land in either order, and under load the refresh's "restore the
previously-selected branch" write would win — leaving the old branch
selected instead of the new one (flaky
move_commits_to_new_branch_from_base_branch).

Replace it with declarative selection intents applied inside the
refresh's own bounce, so the selection is set on the UI thread and
atomically with the list write (no off-thread write, and no BLOCK_UI
needed to avoid a flicker):

- BranchSelection: SelectCheckedOutBranch selects the checked-out branch
  (top of the list). The default, KeepBranchSelectionByName, restores
  the previously-selected branch by name as before. This replaces the
  KeepBranchSelectionIndex bool.
- CommitSelection: SelectHeadCommit (already existed) for the commit.
- SelectTopReflogCommit selects the top reflog entry, since a checkout
  adds a new entry there (reflog/checkout relies on this).

SelectFirstBranchAndFirstCommit is gone. The previously-selected branch
is now read at the top of the branches bounce, before the list is
overwritten, so that read moves onto the UI thread too.

fetchAndCheckout's refresh changes from ASYNC to SYNC so its
post-refresh focus switch can run in Then on the UI thread; SYNC keeps
the inline fetch spinner spinning (only BLOCK_UI would freeze it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Opus 4.8 5414daf492 Exclude toast rendering from the busy query
A toast keeps a foreground spinner task alive for its whole lifetime
(~2-4s): showing one calls renderAppStatus, whose OnWorker loop runs
until the status string clears. With the repo-switch guard in place that
made the guard's own "can't switch, operation in progress" toast keep
Busy() true, so the next escape/switch was refused until the toast
faded — you had to wait it out.

Render toasts in the background, like view-buffer content: a toast is a
transient notification, not lazygit driving an operation, so a switch
during one is fine. A real operation that shows a toast still keeps its
own foreground task busy independently.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan Haller 56932abe06 Refuse a repo switch while a foreground operation is in flight
Switching repos reassigns gui.git and the process cwd; doing it while a
foreground git operation (rebase/commit/push/…) is mid-flight would run
that operation's remaining commands against the wrong repo. The same
applies while the refresh an operation triggers is still settling: its
model writes are generation-guarded, but the client-side Then/OnUIThread
callbacks that run after it aren't, and shouldn't run against a repo that
changed underneath them.

Refuse the switch (with a toast) whenever gocui reports a busy foreground
task. DispatchSwitchTo carries the guard for the simple callers. The
callers that do work before the switch check up front instead, so a
refused switch doesn't leave that work half-done: worktree creation
checks before creating (its own waiting-status spinner would otherwise
make the query busy and refuse its own switch); submodule-enter and the
recent-repos menu check before mutating the repo-path stack (pushing /
clearing it); and escape-to-parent (SwitchToParentRepo) checks before
popping it, so a refusal doesn't consume the entry and strand the user
with nowhere to escape back to. All then call the unguarded switchTo,
which is safe because their own operation is complete by then.
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Opus 4.8 8655d3f5a5 Exclude view-buffer render tasks from the busy query
The repo-switch busy query must not count view-buffer content rendering:
those tasks paint a view rather than drive a git operation, so leaving
one running across a switch is harmless (the switch's own refresh
re-renders). More importantly, they fire on nearly every focus/selection
change — including the context activation that runs right before a
menu/prompt confirmation handler (e.g. confirming worktree creation).
A synchronous busy check in such a handler would otherwise see that
render and make the very switch the handler is about to request refuse
itself.

Route ViewBufferManager's tasks through a new gocui NewBackgroundTask so
they're tracked for idle detection but excluded from the busy query. The
task "background" flag now covers two kinds of non-blocking work: the
background routines (and their refreshes) tagged earlier, and view
rendering.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Opus 4.8 d95900ccd0 Tag background routines and their refreshes as background tasks
For the busy query to be usable as a repo-switch guard it has to be
false while the ongoing background routines run, or a switch would be
refused every time a background fetch or files refresh happened to be in
flight. Mark that work as background so it's excluded from the query.

The background routine dispatch in goEvery becomes OnWorkerBackground,
and the auto-fetch waiting status renders its spinner through the
background variants. Within a refresh, the background flag (which
Refresh already carries as options.Background, and which the files path
already threaded) is now threaded through every place that enqueues a
task: the async scope workers, the model-write bounces
(onUIThreadUnlessRepoChanged), refreshView, the staging bounce, the
Then dispatch, and the branch-loader's behind-count worker. Two
single-caller chains reached by a background files refresh get the flag
too: MergeConflictsHelper.EscapeMerge and BranchesHelper.
AutoForwardBranches (whose follow-up refresh must stay background when
triggered by the background fetch).

Nothing gates on the busy query yet, so this is behavior-preserving;
background tasks still count as busy for the test idle-listener, which
looks at every task regardless of the background flag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Opus 4.8 e352cafd43 Add background tasks and a synchronous busy query to gocui
Repo-switch safety needs to answer, synchronously on the UI thread,
"is any foreground work in flight right now?" so it can refuse a switch
that would run against a repo about to be swapped out. gocui already
tracks a task per OnWorker/Update for the test idle-listener; extend
that.

Tasks gain a background flag: background tasks (the ongoing routines
like auto-fetch, and the refreshes they trigger) don't count towards
busy, because their model writes are already guarded against a
concurrent switch by the repo generation. Add OnWorkerBackground,
UpdateBackground and UpdateContentOnlyBackground (plus the gui-layer
OnUIThreadBackground / OnUIThreadContentOnlyBackground / OnWorkerBackground
on IGuiCommon) so the few background call sites can opt in without
touching the hundreds of foreground callers.

TaskManager.hasBusyForegroundTaskExcept answers the query; Gui.Busy()
wraps it, excluding the event currently being processed (recorded as
currentTask) so a handler asking the question doesn't count itself.

Nothing gates on Busy() yet; this is the mechanism only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Opus 4.8 cf7c3d82e6 Run the repo switch on the UI thread
DispatchSwitchTo wrapped its whole body in WithWaitingStatus, so the
switch ran on a worker: it chdirs, reassigns gui.git, and swaps gui.State
(in resetState), all of which the UI thread also reads. The generation
guard prevents the refresh-in-flight logical corruption but not this
pointer data race on gui.State.

Run the switch synchronously on the UI thread instead. Every caller is
already a UI-thread handler except NewWorktreeCheckout, which must create
the worktree (git work) on a worker first; it now dispatches only the
switch via OnUIThread. The heavy data loading still happens
asynchronously via the refresh that onNewRepo triggers, so the
synchronous part is small (a couple of git rev-parse plus direnv).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Opus 4.8 3103fe97ea Replace RefreshingBranchesMutex with a branch-load sequence guard
This removes the last refresh mutex. RefreshingBranchesMutex wasn't
guarding a data race (Branch.BehindBaseBranch is atomic, and every model
write is now bounced onto the UI thread); it was serializing the two
branch loads that race at the INITIAL startup stage — an immediate one
sorted without the reflog, and an async one that loads the reflog and
sorts by recency — so that the recency-sorted write landed last and won.
That serialization was never a real guarantee, only "very likely": it
relied on the immediate load acquiring the lock before the async load,
which had to load the reflog first.

Instead, each branch load takes a monotonically increasing sequence
number, and its bounce drops the write if a later-started load has
already applied. Combined with the preceding commit (immediate load runs
before the async one is spawned), this is an actual guarantee: the
immediate non-recency load always has a lower sequence than its recency
async partner, so the highest sequence number is always held by a
recency-sorted load, and highest-wins converges on recency ordering —
even if more refreshes fire during the INITIAL window, since each
refresh's async out-sequences its own immediate.

The guard also subsumes what the mutex gave post-startup: a slow, stale
refresh's bounce can no longer clobber a newer refresh's branches.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Opus 4.8 6d8ab1d063 Run the immediate startup branch refresh before spawning the async one
At the INITIAL startup stage two branch refreshes happen: an immediate
one sorted by whatever reflog we have (empty, so not by recency), and an
async one that loads the reflog first and re-sorts by recency. Until now
the async one was spawned first and the immediate one ran afterwards;
this inverts that so the immediate refresh runs before the async one is
spawned.

With RefreshingBranchesMutex still in place this is behavior-preserving
(the mutex serializes the two either way). It's a preparatory step for
replacing that mutex with a branch-load sequence guard: running the
immediate refresh first establishes a happens-before relation between
the two loads' sequence numbers, so the recency-sorted one is guaranteed
the higher sequence.

This also lets refreshReflogCommitsConsideringStartup fold into
refreshReflogAndBranches, whose two-phase logic is now all in one place.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Opus 4.8 805738034f Remove refresh mutexes made redundant by bouncing
Now that every refresh scope writes its model updates on the UI thread
via onUIThreadUnlessRepoChanged, the per-scope mutexes that used to
serialize concurrent worker-goroutine access are redundant:
Model().Commits, .SubCommits, .Authors, the status view content, and
.PullRequests/.PullRequestsMap are all now written only on the UI
thread, and their readers already ran there. setSubCommits only existed
to take the lock, so it's inlined to match refreshSubCommitsWithLimit,
which writes Model().SubCommits directly.

The worker phases still *read* some of these fields (the commit
selection range, MergeRebasingCommits), but those reads race a
concurrent refresh's bounced write regardless of the mutex — the write
happens in the bounce, outside the locked region — so the mutex never
protected them. That residual read race belongs to the broader -race
effort, not to these locks.

RefreshingBranchesMutex is deliberately kept. It is load-bearing for a
reason unrelated to data races: at the INITIAL startup stage two
refreshBranches run concurrently — an immediate one with an empty
reflog (non-recency order) and an async one with the freshly-loaded
reflog (recency order). The mutex serializes them so the recency write's
bounce is enqueued last and wins. Without it the stale non-recency write
can land last, reordering the branches list (caught by the recency-sort
e2e tests: cherry_pick/*, branch/rebase_*).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Sonnet 5 f7a61443fa Bounce BRANCHES model updates onto the UI thread
refreshBranches now loads the branches (and worktrees) on the worker and
writes Model.Branches, the pull-requests map, Model.Worktrees, and the
restored branch selection in an onUIThreadUnlessRepoChanged bounce. The
selection restore and rebuildPullRequestsMap run in the bounce so they
see the branches we just wrote; the LocalCommits re-render (for branch
head visualization) moves into the same bounce.

refreshStatus is adjusted to read the checked-out branch and the linked
worktree name inside its bounce rather than on the worker: both derive
from models (Branches, Worktrees) that are now written via bounces, so
reading them on the worker would format the status from stale values —
which showed up as the status line dropping the "(worktree)" suffix right
after entering a submodule or switching worktrees. The git work
(WorkingTreeState) stays on the worker.

Two callers that read the branches model right after a SYNC branches
refresh move their reads into Then:
- BranchesHelper.PostFetchRefresh: AutoForwardBranches reads Model.Branches,
  so it runs in Then (preserving that a fetch error is still returned to
  the caller and that background auto-forward errors aren't surfaced as a
  popup).
- BranchesController rename: the re-select-by-name loop runs in Then.

RefreshingBranchesMutex is left in place for the mutex cleanup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Sonnet 5 549df17279 Bounce COMMITS model updates onto the UI thread
refreshCommitsWithLimit now loads the commits, working-tree state and
bisect info on the worker and writes them all — Model.Commits,
Model.BisectInfo, Model.WorkingTreeStateAtLastCommitRefresh,
Model.CheckedOutBranch, the authors, and the restored commit selection —
in a single onUIThreadUnlessRepoChanged bounce. The selection restore
(SelectHeadCommit / KeepCommitSelectionByHash) has to run in the bounce
because it reads the freshly-loaded commits; the FocusLine scroll is
enqueued from within the bounce so it still runs after refreshView's
re-render, as before.

refForLog no longer writes Model.BisectInfo as a side effect; it returns
the bisect info it read, and the bounce writes it, keeping that model
write on the UI thread. No caller reads Model.BisectInfo synchronously
after a refresh (the bisect controller reads Git().Bisect.GetInfo()
directly), so this is safe.

refreshCommitsAndCommitFiles's post-refresh re-init of the commit files
context depends on that restored selection, so it reads the selection in
a bounce and dispatches the commit-files git work back to a worker.

LocalCommitsMutex / AuthorsMutex are left in place for the mutex cleanup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Sonnet 5 4c3f8b51ea Bounce PULL_REQUESTS model updates onto the UI thread
refreshGithubPullRequests and setGithubPullRequests now do their network
work on the worker and write Model.PullRequests / PullRequestsMap in an
onUIThreadUnlessRepoChanged bounce (the "no github remotes" and "no base
remote" early-returns clear them the same way). rebuildPullRequestsMap
moves into the bounce so the map is built from Model.Branches and
Model.Remotes as they stand on the UI thread — after those scopes'
refreshes have applied their own bounces — rather than from whatever the
worker happened to see.

The remaining worker-side reads of Model.Branches (to pick which upstream
branches to query) are the same not-yet-addressed worker-read race that
applies to the other bounced scopes.

RefreshingPullRequestsMutex is left in place for the mutex cleanup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Sonnet 5 4c9fdc4221 Bounce STATUS view update onto the UI thread
refreshStatus computes the status line on the calling goroutine (as
before) but now writes it to the status view in an
onUIThreadUnlessRepoChanged bounce rather than calling SetViewContent
directly from the worker. RefreshingStatusMutex is left in place for now;
it only guards the compute phase between concurrent callers and comes out
in the mutex cleanup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Sonnet 5 063bba6b45 Bounce REFLOG model updates onto the UI thread
refreshReflogCommits now does the git fetch on the worker and computes
the new ReflogCommits / FilteredReflogCommits values (still reading the
existing slices for the incremental prepend), then writes them in an
onUIThreadUnlessRepoChanged bounce. The freshly-computed reflog is still
returned for the branches load, so recency sorting is unaffected by the
write now landing on the UI thread (see the previous commit).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Sonnet 5 bd47106d03 Thread reflog commits explicitly into the branches load
BranchLoader.Load reads the reflog commits to sort branches by recency.
Today it reads them straight from Model.ReflogCommits, which works
because in the recency path the reflog refresh writes that field
synchronously just before the branches refresh reads it (same goroutine,
sequential).

An upcoming commit bounces the reflog model write onto the UI thread, at
which point Model.ReflogCommits wouldn't be updated yet when branches
runs — branches would sort by the previous refresh's reflog. To decouple
the branches load from *when* that write lands, pass the reflog commits
to refreshBranches explicitly: refreshReflogCommits now returns the
commits it loaded, and the recency path hands them straight to
refreshBranches. The non-recency path (branches and reflog run
concurrently, as before) keeps passing Model.ReflogCommits.

Pure refactor: behavior is identical, since the value passed is exactly
what Load read from the model before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Sonnet 5 db5eb6fd39 Bounce REMOTES model updates onto the UI thread
refreshRemotes now loads the remotes on the worker and writes
Model.Remotes, rebuilds the pull-requests map, and updates the selected
remote's RemoteBranches inside an onUIThreadUnlessRepoChanged bounce.

RemotesController.addAndCheckoutRemote read Model.Remotes right after its
SYNC REMOTES refresh to select the newly-added remote; since that write
now bounces, the selection (and the follow-up fetch) move into Then so
they run against the post-refresh model rather than the stale one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Sonnet 5 0f85c2b2b4 Bounce SUB_COMMITS model updates onto the UI thread
refreshSubCommitsWithLimit now loads the sub-commits on the worker and
writes Model.SubCommits (and folds their authors into Model.Authors via
RefreshAuthors) inside an onUIThreadUnlessRepoChanged bounce.

SubCommitsMutex and AuthorsMutex are left in place: the former is shared
with setSubCommits, the latter with the commits refresh's RefreshAuthors
call, so both come out only once those other writers are on the UI thread
too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Sonnet 5 559b4bf298 Bounce REBASE_COMMITS model updates onto the UI thread
refreshRebaseCommits now computes the merged rebasing commits and working
tree state on the worker and writes Model.Commits /
WorkingTreeStateAtLastCommitRefresh in an onUIThreadUnlessRepoChanged
bounce. LocalCommitsMutex is left in place for now; it's shared with the
commits and branches refreshes and comes out once they're all bounced.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Sonnet 5 d6f6d0ceba Bounce WORKTREES model updates onto the UI thread
refreshWorktrees now writes Model.Worktrees in an
onUIThreadUnlessRepoChanged bounce. loadWorktrees becomes a pure loader
that returns the worktrees instead of writing them, since it's shared
with refreshBranches; refreshWorktrees bounces the result, and the
branches call site writes it directly for now (that write moves into
refreshBranches's own bounce when that scope is migrated).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Sonnet 5 ff7ecf2d2a Bounce STASH model updates onto the UI thread
refreshStashEntries now loads the stash entries on the worker and writes
Model.StashEntries in an onUIThreadUnlessRepoChanged bounce.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Sonnet 5 21f1dc3366 Bounce TAGS model updates onto the UI thread
refreshTags now captures the repo generation, loads the tags on the
worker, and writes Model.Tags in an onUIThreadUnlessRepoChanged bounce
rather than directly from the worker goroutine.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Sonnet 5 b203ec57ac Bounce COMMIT_FILES model updates onto the UI thread
refreshCommitFilesContext now enqueues the Model.CommitFiles write and
CommitFileTreeViewModel.SetTree() call via OnUIThread, instead of running
them directly on the worker goroutine that drives async refreshes. This is
what makes moving SwitchToDiffFilesController's post-refresh work into Then
(previous commit) actually necessary, rather than just future-proofing.

Same repo-switch hazard as the FILES bounce, closed the same way: it
captures the repo generation before the git work and bounces through
onUIThreadUnlessRepoChanged, so the write is dropped if the user switched
repos while it was in flight.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Sonnet 5 6203a4e411 Move post-COMMIT_FILES-refresh work into Then
SwitchToDiffFilesController.enter calls SelectPath and Context.Push
right after a (SYNC, by default) COMMIT_FILES refresh. This works today
because the model write currently happens synchronously in the worker
before Refresh's wg.Wait() returns, but an upcoming commit will bounce
that write onto the UI thread instead, at which point wg.Wait() no
longer guarantees it's been applied, and SelectPath would operate on a
stale tree.

Move both calls into Then ahead of that change, for the same reason as
the earlier FILES-scope commit: Then is already queued via OnUIThread,
so this is behavior-preserving on its own.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Sonnet 5 2c139b6ac1 Remove RefreshingFilesMutex/FileTreeViewModel.RWMutex, dead code
FileTreeViewModel.RWMutex is removed along with the
withFileTreeViewModelMutex wrapper in FilesController that RLocked it:
every writer (the bounce closure, previous commit) and every reader (key
handlers, disabled-reason callbacks) now runs on the UI thread, so the
mutex is redundant.

RefreshingFilesMutex is removed entirely, including its last use in
repos_helper's DispatchSwitchTo. That use predates the bounce and was
never about FilesController's optimistic-rendering concern; it serialized
a repo switch's onNewRepo() against an in-flight FILES refresh for the
repo being switched away from, so that a slow refresh from the old repo
couldn't write into the freshly-reset model for the new one. Bouncing the
write already broke that guarantee on its own terms — the mutex's critical
section never covered the bounced closure's actual execution, only the
(now-removed) code that enqueued it — so by this point it was only still
locked here without protecting anything real; the previous commit's
repo-generation guard is what now actually closes that race, making this
lock fully redundant rather than just relocated.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Sonnet 5 be897ce55e Bounce FILES model updates onto the UI thread
refreshStateFiles now does its git work on the worker and enqueues a
single OnUIThread closure that writes Model.Submodules, Model.Files, and
the FileTreeViewModel state together, instead of writing them directly
from the worker goroutine. refreshStateSubmoduleConfigs becomes a pure
getter (returns the configs; no model write) so the result can be
threaded into that same bounce.

The STAGING handler wraps RefreshStagingPanel in OnUIThread after
fileWg.Wait() so it sees the post-bounce file model rather than the stale
pre-refresh one — without this it would race the files bounce queued just
above it.

Bouncing the write opens a hazard the old synchronous write didn't have:
if the user switches repos while this refresh is in flight, the queued
closure would fire after resetState has replaced the model with a fresh
one for the new repo, silently overwriting it with the previous repo's
files. Guard against this with a repo generation: resetState bumps a
counter on every switch, refreshStateFiles captures it before its git
work, and onUIThreadUnlessRepoChanged drops the bounce if the generation
has moved on. This one helper is the general mechanism the remaining
scopes' bounces will use too; the same guard covers the rebase-continue
prompt, which reads Model.Files right after.

A generation counter, not a comparison of the *Model pointer: switching
away from and back to a repo reuses that repo's cached state (the same
Model pointer), which a pointer comparison would wrongly accept even
though the in-flight data is stale.

PromptToContinueRebase's Then callback (previous commit) now gets an
explanatory comment, since this is the commit that makes it necessary.

The explicit locking around these writes (RefreshingFilesMutex in
refreshFilesAndSubmodules, FileTreeViewModel.RWMutex around the write in
refreshStateFiles) is left in place for now even though it's becoming
redundant, to keep this commit focused on the bounce itself; it's removed
next.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Sonnet 5 ea83f50dc3 Move post-FILES-refresh model reads into Then
PromptToContinueRebase and WithEnsureCommittableFiles both read
Model.Files right after a SYNC FILES refresh. This works today because
the model write currently happens synchronously in the worker before
Refresh's wg.Wait() returns, but an upcoming commit will bounce that
write onto the UI thread instead, at which point wg.Wait() no longer
guarantees it's been applied.

Move both reads into Then ahead of that change. Then is already queued
via OnUIThread (previous commit), so this is a behavior-preserving
refactor on its own: the model is fully written by the time Then runs
either way, whether that write is still synchronous or gets bounced
later.

As part of restructuring WithEnsureCommittableFiles, prepareFilesForCommit
and syncRefresh are inlined into their single call sites.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Sonnet 5 717448f105 Make RefreshOptions.Then a func() error, queue it via OnUIThread
This is preparation for upcoming commits that will bounce refresh-scope
model updates (e.g. Model.Files) onto the UI thread by enqueuing the
write via OnUIThread instead of applying it directly on the worker
goroutine. Once that lands, a Then callback that reads the model must
run after that queued write has been processed, not synchronously at
wg.Wait() time — at that point the workers have returned, but a bounce
they queued may not have been processed yet.

Queuing Then via OnUIThread here, ahead of that change, guarantees the
right ordering once it lands: a bounce queued earlier in the same
refresh is already sitting in the channel by the time wg.Wait()
returns, so Then enqueued after it will always be processed after, and
see the post-refresh model.

The signature change to func() error lets Then propagate errors
through gocui's normal error handler (the same path key-handler errors
take).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan HallerandClaude Sonnet 5 b54d4c369b Remove unused IsRefreshingFiles state
GetIsRefreshingFiles() is never called anywhere in the codebase, so the
flag serves no purpose. Remove it from Gui, StateAccessor, and
IStateAccessor, and drop the two SetIsRefreshingFiles calls in
refreshFilesAndSubmodules that maintained it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00
Stefan Haller 5350b6c37b Remove unused CommitFileTreeViewModel.RWMutex 2026-07-07 18:09:33 +02:00
Stefan Haller dd1576138a AGENTS.md additions 2026-07-07 18:09:33 +02:00
Stefan Haller fa9429f8c7 Fix stuck inline status when pushing/fetching (#5768)
Operations that show an inline status next to the item they operate on
("Pushing", "Fast-forwarding", "Fetching", …) would sometimes fail to
remove this inline status when done.

Fixes #5534.
2026-07-07 18:08:19 +02:00
Stefan HallerandClaude Opus 4.8 78dd678ce5 Drop the now-redundant manual re-render in tag push
Pushing a tag triggers no refresh, so it used to redraw the tags view
by hand to remove the "Pushing" inline status. WithInlineStatus now
always re-renders after clearing the operation, so this is redundant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 17:46:16 +02:00
Stefan HallerandClaude Opus 4.8 775ebc9e44 Re-render to clear an inline status when its operation finishes
Operations that show an inline status ("Pushing", "Fast-forwarding",
"Fetching", …) removed it by relying on the async refresh they trigger
to redraw the view after the item operation had been cleared. That
ordering was never guaranteed: the item operation is cleared on the
worker once the operation's function returns, while the refresh redraws
the item from the UI thread whenever its (asynchronous) git work
happens to finish. If the refresh redrew before the clear, the status
was left on screen with no later redraw to remove it, so the branch (or
tag/remote) stayed stuck showing e.g. "Pushing" indefinitely even though
the operation had completed. This is timing-dependent, which is why it
surfaced as rare, hard-to-reproduce reports and as flaky CI failures.

Fix it by re-rendering in stop() right after clearing the operation,
and by making these refreshes synchronous rather than async. Because a
synchronous refresh has already updated the model and queued its own
redraw by the time stop() runs, and UI-thread callbacks run in order,
the redraw we queue here runs last and draws the up-to-date model with
the status removed. An async refresh couldn't give that guarantee: its
model update might not have landed yet, so the redraw could briefly
flash the pre-operation status.

Pull refreshes through the shared CheckMergeOrRebaseAndSelectHeadCommit,
so that helper becomes synchronous too; its only other caller,
RegularMerge, thereby also refreshes synchronously, which is fine: a
synchronous on-worker refresh is what we want anyway.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 17:46:16 +02:00
Stefan Haller 3537f855b0 Show a spinner for more long-running operations (#5765)
Some operations used to freeze lazygit while they ran, with no sign that
anything was happening — the UI would just lock up until they finished.

This affected:

- Merging a branch (including squash merges)
- Rebasing a branch interactively onto another ref
- Setting a commit to "edit"
- Resetting to a commit, branch, or tag
- Continuing, skipping, or aborting a merge or rebase

Many of these were usually fast under normal conditions (e.g. a hard
reset when the head doesn't change, or a "rebase --continue" when
there's only a handful of commits left), but in some cases they could
take long (e.g. a hard reset to some distant commit where lots of files
changed). Now each of these shows a spinner while it works and keeps the
UI responsive, matching how similar operations already behave.
2026-07-06 12:36:47 +02:00
Stefan HallerandClaude Opus 4.8 6681ba7eda Show a waiting status while resetting to a ref
Resetting to a commit/branch/tag from the reset menu ran inline on the UI
thread with no spinner; a hard reset to a distant commit can take a while
and blocks the UI meanwhile. Run it on a worker with a waiting status. The
undo/redo callers of ResetToRef already wrap it this way.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 11:56:07 +02:00
Stefan HallerandClaude Opus 4.8 ccf49c8112 Show a waiting status when editing a commit
Setting a single commit to "edit" ran the interactive rebase inline on the
UI thread with no spinner, while its sibling startInteractiveRebaseWithEdit
(used when editing multiple commits or quick-starting a rebase) already runs
on a worker with a waiting status. Make the direct path match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 11:55:28 +02:00
Stefan HallerandClaude Opus 4.8 2bae29c6fd Show a waiting status when starting an interactive rebase onto a ref
The interactive-rebase item in the rebase-onto-ref menu ran inline on the UI
thread with no spinner, unlike its two siblings in the same menu (simple
rebase and rebase onto base branch), which already run on a worker with a
waiting status. Make it consistent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 11:55:12 +02:00
Stefan HallerandClaude Opus 4.8 539ede2e1b Show a waiting status while merging a branch
The regular and squash merges from the merge menu ran inline on the UI
thread, freezing it with no spinner while git worked. Run them on a worker
with a waiting status, like the rebase entry points already do.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 11:54:38 +02:00
Stefan HallerandClaude Opus 4.8 44ba0d539f Show a waiting status while continuing a merge or rebase
Continuing, skipping, or aborting a merge/rebase from the options menu ran
the git command inline on the UI thread, freezing the UI with no spinner
while it worked (a continue can replay many commits). Run the
non-subprocess path on a worker with a waiting status instead, matching how
the other merge/rebase entry points already behave.

The auto-skip recursion in CheckMergeOrRebaseWithRefreshOptions must not
start its own worker: it already runs on the caller's thread (the worker of
the enclosing waiting status, or the UI thread for the synchronous callers).
Route it through genericMergeCommandImpl with the waiting status suppressed
so its behavior is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 11:54:14 +02:00
Stefan Haller ce5a8b61bd Update docs and schema for release (#5761) 2026-07-04 14:43:54 +02:00
Stefan Haller 440f357319 Update docs and schema for release 2026-07-04 14:40:33 +02:00
Stefan Haller 26dd82d8d4 Update translations from Crowdin (#5760) 2026-07-04 14:39:50 +02:00
Stefan Haller 8e36fba91e Update translations from Crowdin 2026-07-04 14:36:49 +02:00
Stefan Haller 24c6d38983 Show renamed files in the custom patch builder (#5759)
When showing the files of a commit, we used to display renamed files as
a pair of added and deleted files, rather than a single `R` entry. This
is inconvenient when just browsing the commit's files because you can't
see if the rename also has modifications; but it becomes a real problem
when trying to work with the renamed file in a custom patch if it also
had modifications; there was no way to discard them, for example.

The Files panel already shows renamed files as `R` and allows you to
stage/unstage/discard hunks in them, so there's no reason why the patch
building panel shouldn't allow the same; and this PR adds this.

To drop just the modifications of a rename, add the individual hunks to
the custom patch (the side panel shows a `◐` icon); reverting the patch
then only drops the modifications but not the rename. To also drop the
rename, add the entire `R` file to the custom patch from the side panel
(it gets a `●` icon).
2026-07-04 13:07:55 +02:00
Stefan Haller aa46a69f77 Remove unused function ExpectClipboard
This can't be used because it wouldn't work on CI; delete it so that
coding agents aren't tempted to use it.
2026-07-04 13:05:09 +02:00
Stefan HallerandClaude Opus 4.8 f84ada4941 Show renamed files in the custom patch builder
When loading the files of a commit we passed --no-renames, so a rename
showed up as a separate delete and add rather than a single R entry.
That made it impossible to work with a rename that also modifies the
file: the modifications were spread across a full deletion and a full
addition instead of appearing as the handful of lines that actually
changed. The staging view already shows renames and lets you stage
their hunks, so there was no good reason for the patch builder to
differ; the flag was only there because the commit-file parser couldn't
cope with the rename record format.

Switch the commit-file loader and the per-file diff to --find-renames,
teach the parser about the rename record (a status followed by two
paths), and carry the previous path through the patch builder so the
diff for a rename is loaded with both paths, which is what makes git
emit the rename in the first place.

A whole-file selection keeps the rename in the header, so the rename
moves or is discarded together with the file's contents. A partial
selection instead strips the rename metadata and points the header at
the new path, so applying the patch only changes the contents and
leaves the rename in place; the blob index line is kept so that a 3-way
apply can still fall back to a blob merge.

Discarding a renamed file from a commit now discards both the new and
the old path, so the new file is removed and the old one is restored.

Changing the rename similarity threshold refreshes the commit files
panel too, not just the files panel, so that a rename can turn into a
delete and add or back. It is disabled while building a patch, however,
because the patch builder caches each file's diff by path and would
desync if a rename changed into a delete and add underneath it.

Finally, copying a file's diff from the commit files panel now passes
both paths for a rename, so the copied diff shows the rename instead of
a new-file add.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 13:05:09 +02:00
Stefan Haller c085598100 Add gui.shrinkSidePanelsToContent option (#5754)
In many cases some of the side panels show a lot of empty space; for
example the branches panel when there is only a `main` branch. This gets
worse when `expandFocusedSidePanel` is on (accordion mode), in which
case the almost-empty panel gets even bigger when focused and steals
valuable space from the other panels that do have something to show.

This PR adds a new `gui.shrinkSidePanelsToContent` option which causes
side panels to never show more than their content (plus one blank line,
so it's clear there's nothing more below). This means that panel heights
are now dynamic and may change as their content changes; for example,
when the working tree is clean the Files panel shows only two blank
lines, but as you start changing file, it gets taller to show them.

The option is off by default because it takes some getting used to. It
is also independent of the `expandFocusedSidePanel` option; that one
just makes the effect even more pronounced.
2026-07-03 19:28:02 +02:00
Stefan HallerandClaude Opus 4.8 a929f34c84 Add gui.shrinkSidePanelsToContent option
Accordion mode expands the focused side panel, but when that panel has
little content (an empty Files panel, a Branches panel with only master)
it just fills the extra height with blank space. The same waste happens
for any panel that gets more height than it has content to show.

When this option is enabled, each side panel is sized to its own content
(plus a blank line, so it's clear there's nothing more below) rather than
to an equal share of the height. The height a small panel gives up flows
to the panels that have more content than fits; those grow up to their
content and then scroll, weighted toward the focused panel in accordion
mode so the two features compose. Only when every panel fits with room to
spare is the leftover shared out equally, regardless of focus: enlarging
the focused panel there would reveal no more content and would only make
the panels jump around as the focus moves.

The option is independent of expandFocusedSidePanel and off by default.
The status panel, and the stash panel when unfocused, keep their fixed
one-line height as before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:25:01 +02:00
Stefan HallerandClaude Opus 4.8 b039d98b09 Scale the side-panel layout height thresholds by panel count
The height thresholds that decide between the proportional layout and
the squashed layout (and, within the squashed layout, between 3-row and
1-row unfocused panels) were hard-coded constants tuned for the fixed
set of five side panels. Now that the panels are configurable, a layout
with fewer panels has less to fit, yet was still forced into the
squashed layout at the same height as five panels would be.

Scale the thresholds down in proportion to the panel count so a smaller
layout keeps using the proportional layout at smaller heights. Only ever
scale down: raising the thresholds for more panels would make them
squash sooner, which works against the reason someone adds panels in the
first place (they want to see them).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:25:01 +02:00
Stefan Haller 758b5dde1f Allow overriding the platform used for default keybindings (#5671)
A handful of default keybindings differ by platform (e.g. word-wise
cursor movement in text inputs uses alt on macOS but ctrl elsewhere).
Lazygit chooses these based on the OS it runs on, but that's the wrong
signal when the OS isn't where the user is actually typing: someone
running lazygit in a Linux container that they access over ssh from a
Mac gets the Linux bindings, when they'd rather have the Mac ones.
Remapping each binding by hand via config is tedious, so add a single
LAZYGIT_KEYBINDING_PLATFORM override.

Closes #5668.
2026-07-03 19:16:15 +02:00
Stefan Haller 9a4ef7d1a1 Allow overriding the platform used for default keybindings
A handful of default keybindings differ by platform (e.g. word-wise
cursor movement in text inputs uses alt on macOS but ctrl elsewhere).
Lazygit chooses these based on the OS it runs on, but that's the wrong
signal when the OS isn't where the user is actually typing: someone
running lazygit in a Linux container that they access over ssh from a
Mac gets the Linux bindings, when they'd rather have the Mac ones.
Remapping each binding by hand via config is tedious, so add a single
LAZYGIT_KEYBINDING_PLATFORM override.

An unrecognized value falls back to the real OS rather than to the
non-darwin default bindings, since the latter would be an arbitrary
choice.
2026-07-03 19:08:22 +02:00
Stefan Haller 0ecced93ea Improve deleting worktrees and their branches (#5748)
This PR improves three rough edges around deleting worktrees and the
branches checked out in them:

- **Deleting a branch that's checked out in another worktree now
actually deletes the branch.** The menu used to offer to remove or
detach the worktree but then stop there, leaving behind the very branch
you asked to delete. Both actions now delete the branch afterwards, and
the labels say so ("Remove worktree and delete branch" / "Detach
worktree and delete branch"). The old "Switch to worktree" entry is
dropped — it's not a useful option in the context of deleting a branch.

- **A branch's local branch, remote branch, and worktree can be deleted
in one step.** Picking "Delete local and remote branch" for a single
branch that's checked out in another worktree used to fail with a
confusing "select them one by one" error (which only makes sense for a
multi-selection). It now goes through the same worktree menu, with
labels that make clear the remote is deleted too.

- **Pressing `d` on a worktree can delete its branch.** The plain
confirmation becomes a menu: "Remove worktree", "Remove worktree and
delete branch", and "Remove worktree and delete local and remote
branch".

Throughout, the explicit menu choice acts as the confirmation, so the
separate "Are you sure you want to remove worktree?" prompt is gone; the
dirty-worktree force prompt and the not-fully-merged warning still show
up when relevant.

Closes #5205.
2026-07-03 19:07:25 +02:00
Stefan HallerandClaude Opus 4.8 0aed44c7f3 Offer to delete the branch when removing a worktree
Pressing `d` on a worktree only ever removed the worktree, leaving its branch
behind even though deleting it too is often what you want. Turn the confirmation
into a menu: "Remove worktree", "Remove worktree and delete branch", and "Remove
worktree and delete local and remote branch". The branch-deleting items come
after the plain removal (they do more harm if picked by accident); both are
greyed out for a detached-HEAD worktree, and the local-and-remote one is also
greyed when the branch has no upstream. The plain menu pick is the confirmation,
so the standalone "remove worktree?" prompt is gone (and its now-dead
translation string with it); the dirty-worktree force prompt and the
unmerged-branch warning still appear when relevant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:04:44 +02:00
Stefan HallerandClaude Opus 4.8 22914da8e5 Allow deleting local+remote of a worktree-checked-out branch at once
Picking "Delete local and remote branch" for a single branch that's checked
out in another worktree used to fail with "Some of the selected branches are
checked out by other worktrees. Select them one by one to delete them." That
message only makes sense for a multi-selection; for a single branch there's no
reason we can't remove the worktree and delete both the local and remote branch
in one go. Route that case through the same worktree menu as the local-only
delete, with labels that spell out that the remote goes too. The multi-select
error stays for actual multi-selections.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:04:44 +02:00
Stefan HallerandClaude Opus 4.8 4f078f5463 Delete the branch when deleting it via its worktree
When you delete a local branch that's checked out in another worktree, the
menu offered to remove or detach the worktree but then stopped there, leaving
the branch you asked to delete still around. Now both actions delete the branch
afterwards, and the labels say so ("Remove worktree and delete branch" /
"Detach worktree and delete branch") to avoid surprises.

Also drop the "Switch to worktree" item: switching abandons the delete the user
asked for, and it's already reachable by checking out the branch or via the
worktrees panel. And drop the now-redundant "remove worktree?" confirmation:
the explicit menu pick is the confirmation (the dirty-worktree force prompt and
the unmerged-branch warning still appear when relevant).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:04:44 +02:00
Stefan HallerandClaude Opus 4.8 9a8244110f Let worktree removal/detach chain follow-up work
Split the actual worktree removal out of the confirmation in Remove into a
non-confirming helper, and give both Remove and Detach an optional `then`
continuation that runs after a successful removal in place of the default
refresh. Upcoming flows need to delete the worktree's branch once the worktree
is out of the way; threading a continuation through (rather than the caller
firing branch deletion independently) keeps it ordered after the git command
that actually frees the branch. No behavior change yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:04:44 +02:00
Stefan HallerandClaude Opus 4.8 d6016d6286 Extract reusable branch-deletion helpers
Pull the merged-check-and-force-warning step and the actual git deletion
out of ConfirmLocalDelete and ConfirmLocalAndRemoteDelete into helpers, so
that the upcoming worktree-aware delete flows can reuse them instead of
duplicating the logic. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:04:44 +02:00
Stefan Haller 3f6a21f7f8 AGENTS.md additions 2026-07-03 19:04:44 +02:00
Stefan Haller 7458275820 Make creating worktrees simpler and less error-prone (#5741)
Creating a worktree in lazygit used to be more awkward than it needed to
be:

- The first thing you were asked was whether you wanted a "normal" or
"detached" worktree — a distinction that's confusing to face up front,
and meaningless when you're creating a worktree from a commit, tag or
stash (both choices did the same thing there).
- You then had to type the new worktree's path by hand. That's easy to
get wrong (a mistyped `.worktrees`, for instance), it wasn't clear
whether you were meant to enter the parent folder or the full path, and
it was ambiguous what a relative path would resolve against — especially
when you were already inside another worktree.
- If you picked a branch that was already checked out in another
worktree, lazygit still asked you for a path and only told you it
wouldn't work afterwards.
- There was no quick way to create a new branch and a worktree for it
(sharing the same name) in one step.

This PR reworks the whole flow:

- **No more up-front mode menu.** Pressing `w` on a branch, commit, tag,
stash or remote branch opens a menu whose options are tailored to what
you selected, and each option spells out exactly what it will do — e.g.
"New worktree for 'main'", or "New detached worktree at 'v1.2.0'".
- **You never type a path from scratch.** Lazygit offers ready-made
locations — the folders your existing worktrees already live in, plus an
optional configured default — and shows each as a full path before
anything happens. The folder name is filled in for you from the branch
(or the name you choose). You can still pick "Other…" to type a custom
path.
- **Create a branch and its worktree in one step**, with a single name
used for both. Slashes are preserved, so `feature/foo` gives you a
`feature/foo` branch and a matching nested folder.
- **Branches already checked out elsewhere are caught up front:** the
relevant option is greyed out with an explanation of why, instead of
letting you do the work and then failing.
- **The worktrees panel's `n` is now a single branch picker.** Start
typing, or pick from the list: choosing an existing branch checks it out
in a new worktree, choosing a remote branch creates a local tracking
branch for it, and typing a new name creates a new branch off your
current one — each then simply asks where to put the worktree. Branches
that are already checked out are left out of the suggestions.

There's also a new `worktree.defaultPath` setting to tell lazygit where
to create worktrees by default — useful before you have any existing
worktrees for it to take its cue from. Starting that path with `~/` is
supported (your home dir); this is also supported in the "Other…" path
prompt mentioned above.

Fixes #3230
Fixes #4708
Fixes #5664
2026-07-03 19:02:29 +02:00
Stefan HallerandClaude Opus 4.8 7a67cea687 Expand a leading ~ in worktree paths to the home directory
Lazygit runs git directly rather than through a shell, so a literal "~"
reaches `git worktree add` unexpanded and git creates a directory named
"~" instead of using the home directory.

Expand the tilde ourselves, both for paths typed into the "Other"
location prompt and for the worktree.defaultPath config value.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:53:05 +02:00
Stefan HallerandClaude Opus 4.8 737fb98967 Move the new-worktree keybinding from worktrees to universal
The command was renamed from "View worktree options" to "New worktree",
but its keybinding config key was still 'worktrees.viewWorktreeOptions'.
That name no longer matches the command, and the 'worktrees' section made
little sense: it held a single binding that isn't even used in the
worktrees panel (that panel uses universal.new), only in the branches,
remotes, tags, commits, and stash panels. Other keybinding sections are
named after the panel they're local to; this one wasn't local to any.

Move it to universal.newWorktree, which describes the action and drops the
spurious section, and migrate existing configs automatically.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:53:05 +02:00
Stefan HallerandClaude Opus 4.8 d53a9ea854 Add MoveYamlKey helper to move config keys between sections
RenameYamlKey can only rename a key in place, under the same parent. To
migrate a keybinding from one section to another we need to relocate the
key to a different parent mapping, which is a move, not a rename.

MoveYamlKey creates intermediate maps at the destination as needed and
prunes any maps left empty behind the key, so a section that held only
the moved key doesn't linger as an empty mapping in the user's config.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:53:05 +02:00
Stefan HallerandClaude Opus 4.8 768d9f1a3f Rework the worktrees-panel 'n' into a branch picker
The old 'n' flow opened a "normal vs detached" menu (the same meaningless
gate the 'w' flow used to have), then asked for a base ref, a path typed from
scratch, and a branch name in three separate prompts.

Replace it with a single picker prompt titled "New worktree for branch",
suggesting local branches not already checked out anywhere, plus remote
branches that don't yet have a local branch of the same name. The entered
value is classified on confirm: an existing local branch checks out into a
new worktree, a remote branch creates a new local tracking branch, and
anything else creates a new branch off the current ref. All three then feed
the same location menu the 'w' flow uses, so paths are chosen from candidates
rather than typed blind. Picking a remote or new branch needs no separate
name prompt — the picker value already is the name. Checked-out branches are
filtered from the suggestions, and a verbatim type-in of one is rejected with
an error.

createWorktree now takes the context to switch focus to once the worktree is
created, so 'n' lands back in the worktrees panel while 'w' still lands in
the branches panel.

This deletes the old NewWorktree / NewWorktreeCheckout core and the now-
orphaned i18n (CreateWorktreeFrom, CreateWorktreeFromDetached, NewWorktreeBase,
NewBranchNameLeaveBlank), completing the migration started for 'w'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:53:05 +02:00
Stefan HallerandClaude Opus 4.8 23d01ac1bd Redesign the 'w' worktree-creation flow
The old flow forced an up-front "normal vs detached" menu (meaningless for
commits, tags and stashes), then asked the user to type a worktree path from
scratch — easy to get wrong, and ambiguous about what relative paths resolve
against. It also offered the same two actions everywhere regardless of what
was selected.

Replace it with per-context "Worktree" menus whose items imply the intent
(new branch + worktree, worktree for an existing branch, detached worktree),
each feeding a shared name -> location -> create pipeline. The location menu
offers candidate parent directories as absolute paths instead of a blank
field, and "Worktree for a branch" is disabled (with a reason) when that
branch is already checked out somewhere, rather than failing after the fact.

Each ref/commit panel binds 'w' in its own controller and calls the matching
typed entry point on the worktree helper, so which menu opens is decided
statically by the call site rather than by dispatching on a ref's dynamic
type. The three commit panels share one menu through BasicCommitsController;
there is no longer a shared worktree-options controller.

The worktrees-panel 'n' flow and its old core (NewWorktree /
NewWorktreeCheckout) are left untouched here so nothing is written and then
rewritten; they migrate, and the dead i18n strings get removed, in a
follow-up commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:53:05 +02:00
Stefan HallerandClaude Opus 4.8 b02eca451c Add helper to compute candidate worktree parent directories
This is the core of "never type a path from scratch": from the repo root,
the configured default path, and the parents of existing worktrees, derive
the ordered list of directories under which a new worktree could be placed.
Pure and unit-tested here; wired into the creation flow in a later commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:53:05 +02:00
Stefan HallerandClaude Opus 4.8 ef73406a96 Add worktree.defaultPath config
The redesigned worktree-creation flow never asks the user to type a path
from scratch; instead it offers candidate parent directories. Until a repo
has any linked worktrees to learn from, there's nothing to offer, so let
users seed that list with a configured default location.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:52:09 +02:00
Stefan Haller 16d773fa40 Support custom pagers and passphrase prompts on Windows (#5740)
Custom pagers were not available on Windows because the feature depends
on a working PTY implementation, and the PTY library we are using
doesn't have support for Windows (see
https://github.com/creack/pty/pull/155). This PR adds this support on
our side; the same PTY library is still used on Mac and Linux, but on
Windows we now have our own implementation using ConPTY.

This also enables prompting for SSH passphrases for users who don't have
an ssh-agent running, which previously also didn't work on Windows.
There's one limitation here: pressing `f` in the Files panel means
`fetch --all` by default (unless you turn that off using `git.fetchAll:
false`), in which case passphrase prompting still doesn't work; the
reason is that git spawns a bunch of child processes for each remote to
be fetched, and on Windows these don't inherit the PTY like they do on
Mac and Linux.

Fixes #1453
Fixes #5525
2026-07-03 18:50:05 +02:00
Stefan Haller 20af6ad23c Remove the Windows limitation from Custom_Pagers.md 2026-07-03 18:47:15 +02:00
Stefan HallerandClaude Opus 4.7 2fce9c91b6 Materialize cursor-forward escapes as space runs
ConPTY compresses runs of default-colored spaces into ECH + CUF
(\x1b[NX\x1b[NC) instead of emitting them literally. ECH is still a
no-op for us — our buffer is built sequentially and has nothing to
erase — but CUF has to materialize as N visible space cells so the
gap actually appears, otherwise content the child wrote with leading
indentation slides left against the preceding cell.

The view's cursorForward branch reuses the same machinery as tab
expansion: substitute the trigger byte for a space and let the
repeatCount path emit the cells under the parser-tracked SGR. The
existing notifyCellsWritten plumbing then advances screenCol over
the gap, keeping subsequent CUP targets aligned.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-03 18:47:15 +02:00
Stefan HallerandClaude Opus 4.7 0c0c50c3f2 Demonstrate that cursor forward escapes collapse runs of spaces
ConPTY compresses runs of default-colored spaces into ECH + CUF
(\x1b[NX\x1b[NC) rather than emitting them literally. Both currently
fall through the parser's swallow path, so the gap they describe
collapses entirely and content that the child wrote with leading
indentation ends up slid left against the previous cell.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-03 18:47:15 +02:00
Stefan HallerandClaude Opus 4.7 180fe0cd26 Convert forward cursor-positioning escapes into row advances
ConPTY presents its child's output as a screen buffer and uses CUP /
CUD / CNL / VPA to skip over blank rows rather than emitting LFs. The
previous behaviour swallowed all of those and the visible content
collapsed together. Now the escape parser tracks the screen-relative
cursor row, and any CSI that moves the cursor past the current row
emits a cursorDown instruction that the view turns into the matching
number of empty lines.

Column tracking is deliberately omitted: doing it correctly would mean
duplicating the view's grapheme-cluster width math in the parser, and
ConPTY in practice positions to column 1 after a CR-equivalent, which
the existing wx-reset path already handles. ConPTY-internal scrolling
needs no special handling either: it only emits cursor-positioning
escapes within the first, un-scrolled screenful — once its screen
scrolls it switches to plain linefeeds, which the view advances on
directly regardless of the tracked cursor.

Backward cursor moves are silently dropped — the view's buffer is
append-style and can't undo earlier writes. The exception is cursor-home
(CUP to row 1): ConPTY emits it at the start of every screen, so rather
than drop it we re-anchor the row tracking to the current write position.
Without that, a view not rewound in lockstep with ConPTY's screen (the
command log, which streams pty output without a rewind) accumulates
drift, and every later absolute CUP becomes a dropped backward move that
collapses the rows ConPTY positioned with.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-03 18:47:15 +02:00
Stefan HallerandClaude Opus 4.7 79bc8e0bc6 Demonstrate that cursor positioning escapes collapse blank rows
ConPTY presents its child's stdout as a screen buffer and uses CUP
(`\x1b[<row>;<col>H`) to skip over blank rows rather than emitting LFs
for them. Our escape interpreter swallows CUP via the catch-all
"valid CSI final byte we don't implement" branch, so the blank rows
the child put between non-blank ones disappear and the surrounding
lines collapse together — which is what makes the delta-rendered diff
in the screenshot look like its blank lines and section breaks were
removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-03 18:47:15 +02:00
Stefan HallerandClaude Opus 4.7 8c035ebe60 Use ConPTY on Windows for pty-backed command execution
The per-platform getCmdHandlerPty split existed because the Unix side
had creack/pty and the Windows side had nothing — so it fell back to a
non-pty handler. Now that oscommands.StartPty provides a pty on both
platforms, the two files collapse into one cross-platform
implementation and the stub is gone.

cmdHandler grows a 'wait' field because the pty path on Windows spawns
via CreateProcess and never runs exec.Cmd.Start — so cmd.Wait wouldn't
work there. Non-pty handlers set wait = cmd.Wait; pty handlers set it
to the wait closure StartPty returns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-03 18:47:15 +02:00
Stefan HallerandClaude Opus 4.7 1935117141 Add pty support on Windows via ConPTY
Replace the StartPty stub with a real ConPTY implementation:
CreatePipe + CreatePseudoConsole + PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE
+ CreateProcess. Pagers and external diff tools now get real terminal
behavior instead of being handed pipes.

One Windows-specific quirk worth flagging: ConPTY does not EOF the
output pipe when the child exits; conhost keeps it alive until
ClosePseudoConsole is called explicitly. A background waiter goroutine
calls ClosePseudoConsole as soon as proc.Wait returns, so callers see
EOF on outRead — restoring the Unix master-fd-EOFs-when-slave-closes
semantics they depend on.

The ErrPtyUnsupported sentinel and the no-pty fallback in newPtyTask
are gone now that both platforms have a real implementation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-03 18:47:15 +02:00
Stefan HallerandClaude Opus 4.7 c85f7530bb Abstract pty startup behind a platform-specific primitive
Move the pty master behind a small interface (Read/Write/Close/Resize),
and push the actual startup into a platform-specific StartPty function
in pkg/commands/oscommands. The Unix implementation still uses
creack/pty; the Windows implementation is a stub that returns
ErrPtyUnsupported, at which point newPtyTask falls back to a plain cmd
task — matching the existing Windows behavior.

The primitive lives in oscommands rather than pkg/gui because the
cmd_obj_runner pty handler (also in oscommands) is going to consume it
too, and tasks → oscommands is the existing dependency direction.

Same observable behavior on every platform; this just carves out a seam
for a real ConPTY implementation on Windows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-03 18:47:15 +02:00
Stefan HallerandClaude Opus 4.7 8ec4283e55 Abstract task command over *exec.Cmd
Windows ConPTY can't attach a child process to a pseudoconsole via
os/exec — Go's stdlib doesn't expose PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE
(golang/go#62708). The ConPTY path has to call CreateProcess directly,
so it can't hand an *exec.Cmd back to the task runner.

Widen NewCmdTask to accept a small Cmd interface satisfied by both
*exec.Cmd (via the ExecCmd adapter) and the Windows ConPTY command type
we're about to add. Change TerminateProcessGracefully to take
*os.Process, which both cmd shapes can provide.

Behavior is unchanged on every platform.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-03 18:47:15 +02:00
Stefan HallerandClaude Opus 4.7 51c8f9e6ad Always set LAZYGIT_COLUMNS
The env var was previously set only on Windows, where the no-op pty
stub was just running the command without a pty and needed to expose
the width to pager scripts another way. With ConPTY coming to Windows
the rationale disappears there, but the env var is documented in
docs/Custom_Pagers.md for pager scripts that can't query the terminal
width directly. Set it on every platform so those scripts remain
portable, regardless of whether a pty is in play.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-03 18:47:15 +02:00
Stefan Haller 3c04b30336 Have "just lint" show all issues instead of just the first so many 2026-07-03 18:47:15 +02:00
Stefan Haller 2be6ba9391 Auto-dismiss the continue-rebase prompt when it becomes stale (#5758)
When conflicts of an in-progress rebase/merge/cherry-pick/revert are
resolved, lazygit pops up a prompt offering to continue it. This is
helpful when you started the operation in lazygit and resolved the
conflicts in your editor. However, the operation can change out from
under it: a coding agent (or the user in another terminal) might
continue or abort it, or advance it to a commit with new conflicts. The
prompt then becomes stale — pressing continue fails with "no rebase in
progress" or acts on the wrong state.

Track whether the prompt is showing, and on each refresh dismiss it if
the operation is no longer in the "resolved, ready to continue" state
that the prompt is offering to act on.

Also, don't even open the prompt in the first place if the rebase or
merge wasn't started from within lazygit. This covers the case where a
coding agent makes a rebase, stops at conflicts, resolves them, and then
takes some more time to fix the build or run tests, in which case
lazygit would show the "continue rebase?" prompt, which is confusing and
undesired.

The only legitimate case that I personally encounter where I start a
rebase myself outside of lazygit is something like `git rebase -x "make
test" <base-of-my-branch>`, in which case there can never be conflicts
if it's done in-place.

Closes #5197.
2026-07-03 18:45:33 +02:00
Stefan HallerandClaude Opus 4.8 90d8ef499c Auto-dismiss the continue-rebase prompt when it becomes stale
The prompt offering to continue a rebase/merge is opened from a refresh
and then left to sit until the user acts on it. But the operation can
change out from under it: a coding agent (or the user in another
terminal) might continue or abort it, or advance it to a commit with new
conflicts. The prompt then becomes stale — pressing continue fails with
"no rebase in progress" or acts on the wrong state.

Track whether the prompt is showing, and on each refresh dismiss it if
the operation is no longer in the "resolved, ready to continue" state
that the prompt is offering to act on. This runs on the same refreshes
that would open it (including the background poll and the refresh on
window focus), so the prompt disappears on its own shortly after the
operation moves on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:30:02 +02:00
Stefan HallerandClaude Opus 4.8 ad507d67f4 Only prompt to continue a rebase/merge if we started it
When conflicts of an in-progress rebase/merge/cherry-pick/revert are
resolved, lazygit pops up a prompt offering to continue it. This is
helpful when you started the operation in lazygit and resolved the
conflicts in your editor. But it's confusing when the operation was
started outside lazygit — e.g. by a coding agent in another terminal
that resolves the conflicts but hasn't continued yet because it's still
running tests or fixing the build. lazygit would then prompt unbidden.

Track whether the in-progress operation was started from within lazygit,
and only show the prompt in that case. We record this right after running
a merge/rebase step (in CheckMergeOrRebaseWithRefreshOptions, the
subprocess branch of genericMergeCommand, and the custom-command
conflict path), and clear it whenever a refresh observes that no
operation is in progress — which also handles an operation that was
finished or aborted externally.

The conflict-resolution tests start their operation by running git
directly (not through lazygit's UI), so they call the new test helper
Common.PretendMergeOrRebaseStartedInLazygit to have lazygit treat the
operation as its own and still get the prompt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:28:07 +02:00
Stefan Haller 3d912f058a Fix a few flaky tests (#5756)
These started out as a few intermittently-failing integration tests, but
the root cause was real: a handful of places where UI updates
(rendering, model updates, view content) could be applied in a different
order than they were issued, because the order depended on goroutine
scheduling rather than being deterministic. In rare cases that could
make the main view briefly show stale or wrong content — most likely
when moving quickly between items. This was never observed in normal
usage, but caused some integration tests to fail occasionally (both
locally and on CI).

Each commit fixes one of these orderings; see the commit messages for
details.
2026-07-02 17:50:49 +02:00
Stefan Haller 873804a37b Make Gui.Update a synchronous FIFO enqueue
Update spawned a goroutine per call that then sent on the user-events
channel, so multiple Update calls from the same goroutine could be
reordered by the scheduler — the doc comment even admitted "the order in
which the user events will be handled is not guaranteed." That
non-determinism is a latent source of flaky rendering: code that queues a
model update and then a render in source order could see them run in the
opposite order.

Send on the channel directly instead, so same-goroutine calls arrive in
source order. The send is non-blocking and panics on a full channel
rather than blocking (a blocked send from the UI goroutine would deadlock
against itself) or silently reordering; the buffer is sized generously so
this is unreachable in normal use. UpdateAsync is now identical to Update
and unused, so it's removed along with the shared updateAsyncAux helper.
2026-07-02 17:32:01 +02:00
Stefan Haller badf398a94 Run moveMainContextPairToTop before queueing main-view tasks
Copy the outgoing view's content into the target view (the flicker-
prevention step) before queuing the render task, rather than after. The
task writes the fresh content from a worker goroutine, so with the old
order the worker write races the UI-thread copy, and the copy can land
last and clobber the fresh content with stale output.

This is only needed while view writes happen concurrently. Once view
writes are serialized on the UI thread and the view write-mutex goes
away, the synchronous copy always precedes the FIFO-queued write
regardless of order, so the reorder becomes unnecessary. No code comment
is added for it, since that comment would be obsoleted by that work and
likely left behind.
2026-07-02 17:32:01 +02:00
Stefan HallerandClaude Opus 4.7 e53c72be52 Make ViewBufferManager.NewTask respect call order
NewTask was incrementing newTaskID and reading taskID inside the
spawned goroutine, so for two NewTask calls in quick succession the
assignment was determined by goroutine scheduling order rather than
call order. When the goroutines reordered, the first NewTask call
could end up with the higher taskID and "win" the staleness check,
superseding the second call's task even though the caller intended
the second to be the latest.

Worse, the staleness check ran after onNewKey, so a goroutine destined
to bail as stale would still reset the view buffer first, potentially
wiping the winning task's already-written output.

Take newTaskID++ synchronously in NewTask so taskIDs follow call order,
and move the first staleness check ahead of onNewKey so a stale task
doesn't side-effect the view before exiting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-02 17:24:48 +02:00
Stefan Haller f063832dd3 Fix submodule index lock contention (#5755)
Suppress optional locks by default again, except foreground refresh.

Commit d94f2f05 dropped the `GIT_OPTIONAL_LOCKS=0` env var that we used
to set on every git command, and re-added lock suppression only as a
`--no-optional-locks` flag on the background files refresh. The intent
was sound — a foreground `git status` should persist git's refreshed
stat-cache — but the change was too broad: it stopped suppressing
optional locks for every other command too.

The one that bites is the main-view diff. When a folder containing
submodules is selected, we render `git diff --submodule -- <dir>`, and
`--submodule` makes git run `git status` inside each submodule to
describe its "modified" state. That status now grabs the submodule's
index.lock. It runs as a PTY task on its own goroutine, so it races any
submodule-mutating action the user triggers — e.g. resetting a submodule
runs `git -C <submodule> stash`, which then fails with "index.lock: File
exists". This is what made submodule/reset_folder flaky. `git status` is
in fact the only command that takes the optional lock, but the env var
also covered its use inside `git diff --submodule`, inside PTY-run
commands, and inside git's own submodule child processes — none of which
a per-command flag reaches cleanly.

Invert the polarity to match how it worked before d94f2f05: the git
command builder disables optional locks on every command by default, and
the single command that benefits from taking the lock — the foreground
files refresh — opts back in. This restores the original contention
avoidance (including against the user's terminal git) while keeping
d94f2f05's stat-cache-persistence win for the foreground refresh.

This fixes a regression introduced by #5712; labelling this as
"ignore-for-release" because that PR hasn't been released yet.
2026-07-02 17:06:38 +02:00
Stefan Haller 4fcf6ddb5f Revert "Add GlobalArg/GlobalArgIf to GitCommandBuilder"
After the previous commit this is no longer needed.

This reverts commit 94db69f64b.
2026-07-02 16:49:36 +02:00
Stefan HallerandClaude Opus 4.8 ccaa96b29d Suppress optional locks by default again, except foreground refresh
Commit d94f2f05 dropped the GIT_OPTIONAL_LOCKS=0 env var that we used
to set on every git command, and re-added lock suppression only as a
--no-optional-locks flag on the background files refresh. The intent
was sound — a foreground `git status` should persist git's refreshed
stat-cache — but the change was too broad: it stopped suppressing
optional locks for every other command too.

The one that bites is the main-view diff. When a folder containing
submodules is selected, we render `git diff --submodule -- <dir>`, and
`--submodule` makes git run `git status` inside each submodule to
describe its "modified" state. That status now grabs the submodule's
index.lock. It runs as a PTY task on its own goroutine, so it races
any submodule-mutating action the user triggers — e.g. resetting a
submodule runs `git -C <submodule> stash`, which then fails with
"index.lock: File exists". This is what made submodule/reset_folder
flaky. `git status` is in fact the only command that takes the
optional lock, but the env var also covered its use inside `git diff
--submodule`, inside PTY-run commands, and inside git's own submodule
child processes — none of which a per-command flag reaches cleanly.

Invert the polarity to match how it worked before d94f2f05: the git
command builder disables optional locks on every command by default,
and the single command that benefits from taking the lock — the
foreground files refresh — opts back in. This restores the original
contention avoidance (including against the user's terminal git) while
keeping d94f2f05's stat-cache-persistence win for the foreground
refresh.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 16:05:08 +02:00
Stefan Haller 29b70105c2 Reintroduce the gitCmdObjBuilder wrapper
This reverts commit eb988395e6, which removed the wrapper on the
grounds that it no longer had anything to do but delegate. We're about
to give it a job again: hosting a git-specific default environment
variable on every command. Restore the scaffolding first, as a pure
behaviour-preserving step, so the behaviour change that follows is
minimal.
2026-07-02 15:29:57 +02:00
Stefan Haller 80c899a05c Pin gofumpt version to 0.9.2 (#5753)
It was quite random which version of gofumpt was used depending on how
you invoked it:
- CI would run it via golangci-lint, which we pin to a version that in
turn pins gofumpt to 0.8.0
- running `just format` would use whatever gofumpt is installed locally
(happened to be 0.9.1 in my case)
- saving in VS Code runs it via gopls, so it uses whatever version of
gopls is installed and whichever gofumpt version it bundles (happened to
be 0.10.0 in my case).

So in my case, saving files in VSCode would suddenly reformat them since
I last updated gopls, because the current 0.10.0 version formats code in
a different way. And I don't like some of its choices; this has been
made less aggressive in gofumpt's master (see
https://github.com/mvdan/gofumpt/issues/74), and I'm waiting for a new
release with that behavior before bumping the dependency to the latest
release; until then, pin to 0.9.2.
2026-07-02 11:19:44 +02:00
Stefan HallerandClaude Opus 4.8 4b082ed096 Run go mod tidy before go mod vendor
With the previous order, `go mod vendor` populated vendor/ from the
current go.mod, and only then did `go mod tidy` prune it. If tidy
changed go.mod, vendor/ was left matching the pre-tidy state, so a
single run could leave vendor/modules.txt inconsistent with go.mod
(it took a second run to converge). Tidying first settles
go.mod/go.sum, then vendor rebuilds vendor/ to match in one pass.

This applies both to the `vendor` recipe (justfile and Makefile)
and to scripts/bump_lazycore.sh.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 11:16:53 +02:00
Stefan HallerandClaude Opus 4.8 8af6104454 Check gofumpt formatting with the pinned version in CI and lint
golangci-lint bundles gofumpt v0.8.0, which formats code differently
from the v0.9.2 we pin in go.mod. Enforcing formatting through
golangci-lint may therefore disagree with `just format`.

Remove gofumpt from golangci-lint's formatters and instead run the
pinned `go tool gofumpt` as a standalone check via a new
scripts/gofumpt-check.sh, wired into CI, `just lint`, and `make lint`.
goimports stays in golangci-lint; it's stable across versions and
nothing runs a competing copy of it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 11:16:53 +02:00
Stefan Haller 9882e4a03e Add gofumpt-tool.sh script and use it in VS Code 2026-07-02 10:41:23 +02:00
Stefan Haller 2d4a4dcdc1 Use go tool gofumpt for make format
This way it always uses our pinned 0.9.2 version.
2026-07-02 10:41:23 +02:00
Stefan Haller 659908195e go get -tool mvdan.cc/gofumpt@v0.9.2 2026-07-02 10:41:23 +02:00
Stefan Haller 21ae632c9b Remove obsolete "errors" step from lint job on CI
When this was added (dafac52a4c), the job used
golangci-lint-action@v2, which had the problem that its step log was
sparse (it mostly produced PR annotations), so re-running `golanci-lint
run` was a workaround to dump readable errors into the console. The
current version of the action no longer has this problem, so we can
remove that fallback (it would conflict with what we are about to do in
this branch).
2026-07-02 10:41:00 +02:00
Stefan Haller 73aec42189 Remove obsolete bump_gocui.sh script
We copied the gocui sources into lazygit a while ago, so we no longer
need this.
2026-07-02 10:05:18 +02:00
Stefan Haller fce9b73856 Bump golang.org/x/net from 0.47.0 to 0.55.0 (#5752)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.47.0 to
0.55.0.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/golang/net/commit/7770ec48d03fec35e378665337b4faca93c38423"><code>7770ec4</code></a>
go.mod: update golang.org/x dependencies</li>
<li><a
href="https://github.com/golang/net/commit/4ece7b612ad44ad6c4d5e0d5d4df9c18cc211905"><code>4ece7b6</code></a>
html: escape greater-than symbol in doctype identifiers</li>
<li><a
href="https://github.com/golang/net/commit/08be507abce89191d78cd49da60f4501fc910472"><code>08be507</code></a>
html: improve Noah's Ark clause performance</li>
<li><a
href="https://github.com/golang/net/commit/a8fb2fe4f7378f816302b9f2f7b8290ce512e5dd"><code>a8fb2fe</code></a>
html: properly render fostered elements in foreign content</li>
<li><a
href="https://github.com/golang/net/commit/0dc5b7a5f81d7155ade6d5e9db35992998679932"><code>0dc5b7a</code></a>
html: properly check namespace in &quot;in body&quot; any other end
tag</li>
<li><a
href="https://github.com/golang/net/commit/a452f3cc17168a60bc3f439a3ae0fcffc32eca0e"><code>a452f3c</code></a>
html: ignore duplicate attributes during tokenization</li>
<li><a
href="https://github.com/golang/net/commit/f8651996b24ba47d89dd9eb97fd47758e6d1886f"><code>f865199</code></a>
quic: fix appendMaxDataFrame erroneously accumulating sentLimit</li>
<li><a
href="https://github.com/golang/net/commit/210ed3cb901cb549818aefa04b71dadaf149d05d"><code>210ed3c</code></a>
quic: establish a &quot;happened-before&quot; relationship between
stream write and read</li>
<li><a
href="https://github.com/golang/net/commit/ad8140e0aa2ec41b37ea478b4525a423bcc21af9"><code>ad8140e</code></a>
quic: fix buffer slicing when handling overlapping stream data</li>
<li><a
href="https://github.com/golang/net/commit/23ee2efe81a3ff183b4eca46c42f749af7efca45"><code>23ee2ef</code></a>
http2: avoid API changes when built with go1.27</li>
<li>Additional commits viewable in <a
href="https://github.com/golang/net/compare/v0.47.0...v0.55.0">compare
view</a></li>
</ul>
</details>
<br />
2026-07-02 08:31:47 +02:00
dependabot[bot] 3094f6621b Bump golang.org/x/net from 0.47.0 to 0.55.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.47.0 to 0.55.0.
- [Commits](https://github.com/golang/net/compare/v0.47.0...v0.55.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.55.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-02 06:29:12 +00:00
Stefan Haller 824d1e4387 Bump golangci/golangci-lint-action from 9.2.0 to 9.3.0 (#5746)
Bumps
[golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action)
from 9.2.0 to 9.3.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/golangci/golangci-lint-action/releases">golangci/golangci-lint-action's
releases</a>.</em></p>
<blockquote>
<h2>v9.3.0</h2>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<h3>Changes</h3>
<ul>
<li>feat: add no-run-logs-group as experimental option by <a
href="https://github.com/ldez"><code>@​ldez</code></a> in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1403">golangci/golangci-lint-action#1403</a></li>
</ul>
<h3>Dependencies</h3>
<ul>
<li>build(deps): bump github/codeql-action from 4.35.4 to 4.35.5 in the
github-actions group by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1395">golangci/golangci-lint-action#1395</a></li>
<li>build(deps): bump tmp from 0.2.5 to 0.2.6 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1397">golangci/golangci-lint-action#1397</a></li>
<li>build(deps): bump github/codeql-action from 4.35.5 to 4.36.0 in the
github-actions group by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1398">golangci/golangci-lint-action#1398</a></li>
<li>build(deps): bump tmp from 0.2.6 to 0.2.7 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1399">golangci/golangci-lint-action#1399</a></li>
<li>build(deps-dev): bump js-yaml from 4.1.1 to 4.2.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1400">golangci/golangci-lint-action#1400</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/golangci/golangci-lint-action/compare/v9.2.1...v9.3.0">https://github.com/golangci/golangci-lint-action/compare/v9.2.1...v9.3.0</a></p>
<h2>v9.2.1</h2>
<h2>What's Changed</h2>
<p>IMPORTANT: this is the first immutable release.</p>
<h3>Changes</h3>
<ul>
<li>chore: improve workflows by <a
href="https://github.com/ldez"><code>@​ldez</code></a> in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1394">golangci/golangci-lint-action#1394</a></li>
</ul>
<h3>Dependencies</h3>
<ul>
<li>build(deps-dev): bump the dev-dependencies group with 3 updates by
<a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1325">golangci/golangci-lint-action#1325</a></li>
<li>build(deps-dev): bump the dev-dependencies group with 2 updates by
<a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1326">golangci/golangci-lint-action#1326</a></li>
<li>build(deps): bump the dependencies group with 4 updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1327">golangci/golangci-lint-action#1327</a></li>
<li>build(deps-dev): bump the dev-dependencies group with 2 updates by
<a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1328">golangci/golangci-lint-action#1328</a></li>
<li>build(deps): bump <code>@​types/node</code> from 25.0.2 to 25.0.3 in
the dependencies group by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1329">golangci/golangci-lint-action#1329</a></li>
<li>build(deps-dev): bump the dev-dependencies group with 2 updates by
<a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1330">golangci/golangci-lint-action#1330</a></li>
<li>build(deps-dev): bump the dev-dependencies group with 2 updates by
<a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1332">golangci/golangci-lint-action#1332</a></li>
<li>build(deps-dev): bump the dev-dependencies group with 2 updates by
<a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1333">golangci/golangci-lint-action#1333</a></li>
<li>build(deps): bump the dependencies group with 6 updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1334">golangci/golangci-lint-action#1334</a></li>
<li>build(deps-dev): bump the dev-dependencies group with 4 updates by
<a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1335">golangci/golangci-lint-action#1335</a></li>
<li>build(deps): bump the dependencies group with 2 updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1336">golangci/golangci-lint-action#1336</a></li>
<li>build(deps-dev): bump the dev-dependencies group with 3 updates by
<a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1337">golangci/golangci-lint-action#1337</a></li>
<li>build(deps): bump <code>@​types/node</code> from 25.0.9 to 25.0.10
in the dependencies group by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1338">golangci/golangci-lint-action#1338</a></li>
<li>build(deps): bump fast-xml-parser from 5.3.3 to 5.3.4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1339">golangci/golangci-lint-action#1339</a></li>
<li>build(deps-dev): bump the dev-dependencies group with 2 updates by
<a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1340">golangci/golangci-lint-action#1340</a></li>
<li>build(deps-dev): bump the dev-dependencies group across 1 directory
with 3 updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1344">golangci/golangci-lint-action#1344</a></li>
<li>build(deps): bump fast-xml-parser from 5.3.4 to 5.3.6 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1346">golangci/golangci-lint-action#1346</a></li>
<li>build(deps): bump minimatch by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1348">golangci/golangci-lint-action#1348</a></li>
<li>build(deps): bump minimatch from 3.1.3 to 3.1.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1350">golangci/golangci-lint-action#1350</a></li>
<li>build(deps): bump fast-xml-parser from 5.3.6 to 5.4.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1351">golangci/golangci-lint-action#1351</a></li>
<li>build(deps): bump fast-xml-parser from 5.4.1 to 5.5.6 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1357">golangci/golangci-lint-action#1357</a></li>
<li>build(deps): bump fast-xml-parser from 5.5.6 to 5.5.7 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1358">golangci/golangci-lint-action#1358</a></li>
<li>build(deps-dev): bump flatted from 3.3.3 to 3.4.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1359">golangci/golangci-lint-action#1359</a></li>
<li>build(deps): bump picomatch from 4.0.3 to 4.0.4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1364">golangci/golangci-lint-action#1364</a></li>
<li>build(deps): bump yaml from 2.8.2 to 2.8.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/golangci/golangci-lint-action/pull/1365">golangci/golangci-lint-action#1365</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/golangci/golangci-lint-action/commit/ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a"><code>ba0d7d2</code></a>
chore: prepare release v9.3.0</li>
<li><a
href="https://github.com/golangci/golangci-lint-action/commit/efd0857dc49487d73b2e08936ea47b04e894a2b1"><code>efd0857</code></a>
feat: add no-run-logs-group as experimental option (<a
href="https://redirect.github.com/golangci/golangci-lint-action/issues/1403">#1403</a>)</li>
<li><a
href="https://github.com/golangci/golangci-lint-action/commit/ed485de1ff3b974c764ba768d82dda2e035d091e"><code>ed485de</code></a>
build(deps): bump undici from 6.24.0 to 6.27.0</li>
<li><a
href="https://github.com/golangci/golangci-lint-action/commit/8872e8d04c73fcdce3d942f99fbbe16ea722b6de"><code>8872e8d</code></a>
build(deps-dev): bump js-yaml from 4.1.1 to 4.2.0 (<a
href="https://redirect.github.com/golangci/golangci-lint-action/issues/1400">#1400</a>)</li>
<li><a
href="https://github.com/golangci/golangci-lint-action/commit/b163415b470349e75dbab89538e4165e7402f2b9"><code>b163415</code></a>
build(deps): bump tmp from 0.2.6 to 0.2.7 (<a
href="https://redirect.github.com/golangci/golangci-lint-action/issues/1399">#1399</a>)</li>
<li><a
href="https://github.com/golangci/golangci-lint-action/commit/e52a9f899623adbdb986c05b9a8f7a8cf4138fc3"><code>e52a9f8</code></a>
build(deps): bump github/codeql-action from 4.35.5 to 4.36.0 in the
github-ac...</li>
<li><a
href="https://github.com/golangci/golangci-lint-action/commit/8182aa34940a5c3d28dbb24ce802921d8aa7b434"><code>8182aa3</code></a>
build(deps): bump tmp from 0.2.5 to 0.2.6 (<a
href="https://redirect.github.com/golangci/golangci-lint-action/issues/1397">#1397</a>)</li>
<li><a
href="https://github.com/golangci/golangci-lint-action/commit/5403a413dee20909aad15f1ecb21377539fcc4df"><code>5403a41</code></a>
build(deps): bump github/codeql-action from 4.35.4 to 4.35.5 in the
github-ac...</li>
<li><a
href="https://github.com/golangci/golangci-lint-action/commit/82606bf257cbaff209d206a39f5134f0cfbfd2ee"><code>82606bf</code></a>
chore: prepare release v9.2.1</li>
<li><a
href="https://github.com/golangci/golangci-lint-action/commit/97c8387e660fa3be78f698fb592523e1f906a02c"><code>97c8387</code></a>
chore: improve workflows (<a
href="https://redirect.github.com/golangci/golangci-lint-action/issues/1394">#1394</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/golangci/golangci-lint-action/compare/1e7e51e771db61008b38414a730f564565cf7c20...ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a">compare
view</a></li>
</ul>
</details>
<br />
2026-07-02 08:27:46 +02:00
dependabot[bot] 0e2824baac Bump golangci/golangci-lint-action from 9.2.0 to 9.3.0
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 9.2.0 to 9.3.0.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/1e7e51e771db61008b38414a730f564565cf7c20...ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-version: 9.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-02 06:08:35 +00:00
Stefan Haller 3efae7d905 Bump goreleaser/goreleaser-action from 7.2.2 to 7.2.3 (#5745)
Bumps
[goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action)
from 7.2.2 to 7.2.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/goreleaser/goreleaser-action/releases">goreleaser/goreleaser-action's
releases</a>.</em></p>
<blockquote>
<h2>v7.2.3</h2>
<h2>What's Changed</h2>
<ul>
<li>ci(deps): bump the actions group with 3 updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/goreleaser/goreleaser-action/pull/563">goreleaser/goreleaser-action#563</a></li>
<li>chore(deps): bump undici from 6.24.1 to 6.27.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/goreleaser/goreleaser-action/pull/565">goreleaser/goreleaser-action#565</a></li>
<li>build: regenerate dist after undici 6.27.0 bump by <a
href="https://github.com/caarlos0"><code>@​caarlos0</code></a> in <a
href="https://redirect.github.com/goreleaser/goreleaser-action/pull/567">goreleaser/goreleaser-action#567</a></li>
<li>ci: auto-rebuild dist on dependabot PRs by <a
href="https://github.com/caarlos0"><code>@​caarlos0</code></a> in <a
href="https://redirect.github.com/goreleaser/goreleaser-action/pull/568">goreleaser/goreleaser-action#568</a></li>
<li>ci: use a GitHub App token to rebuild dist on dependabot PRs by <a
href="https://github.com/caarlos0"><code>@​caarlos0</code></a> in <a
href="https://redirect.github.com/goreleaser/goreleaser-action/pull/569">goreleaser/goreleaser-action#569</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/goreleaser/goreleaser-action/compare/v7.2.2...v7.2.3">https://github.com/goreleaser/goreleaser-action/compare/v7.2.2...v7.2.3</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/goreleaser/goreleaser-action/commit/f06c13b6b1a9625abc9e6e439d9c05a8f2190e94"><code>f06c13b</code></a>
chore: update dist</li>
<li><a
href="https://github.com/goreleaser/goreleaser-action/commit/d3934597cabdbe270e6519994809cd8565affd8b"><code>d393459</code></a>
ci: fix job</li>
<li><a
href="https://github.com/goreleaser/goreleaser-action/commit/ee731b17003202950327bd6320f70ffa1ddfc762"><code>ee731b1</code></a>
chore: workflow dispatch</li>
<li><a
href="https://github.com/goreleaser/goreleaser-action/commit/55de448558b6150b4f0992bf3e50af893c6cdae0"><code>55de448</code></a>
chore(deps): bump js-yaml from 4.1.1 to 4.2.0</li>
<li><a
href="https://github.com/goreleaser/goreleaser-action/commit/a4f614e65e0a697142391c9db49e89ecb675acb6"><code>a4f614e</code></a>
ci: use a GitHub App token to rebuild dist on dependabot PRs (<a
href="https://redirect.github.com/goreleaser/goreleaser-action/issues/569">#569</a>)</li>
<li><a
href="https://github.com/goreleaser/goreleaser-action/commit/d2d17a6c5d53a4728825071e3335a7fd4e6e2efd"><code>d2d17a6</code></a>
ci: auto-rebuild dist on dependabot PRs (<a
href="https://redirect.github.com/goreleaser/goreleaser-action/issues/568">#568</a>)</li>
<li><a
href="https://github.com/goreleaser/goreleaser-action/commit/d13def3a6e9d83b93e85c1c6cbcd81c90616f68b"><code>d13def3</code></a>
build: regenerate dist after undici 6.27.0 bump (<a
href="https://redirect.github.com/goreleaser/goreleaser-action/issues/567">#567</a>)</li>
<li><a
href="https://github.com/goreleaser/goreleaser-action/commit/21549b6bea946f588228bf9508eddb673059ed6a"><code>21549b6</code></a>
chore(deps): bump undici from 6.24.1 to 6.27.0 (<a
href="https://redirect.github.com/goreleaser/goreleaser-action/issues/565">#565</a>)</li>
<li><a
href="https://github.com/goreleaser/goreleaser-action/commit/47c416d5e8f73bfd91c48a1a86cf259f81a279f6"><code>47c416d</code></a>
ci(deps): bump the actions group with 3 updates (<a
href="https://redirect.github.com/goreleaser/goreleaser-action/issues/563">#563</a>)</li>
<li>See full diff in <a
href="https://github.com/goreleaser/goreleaser-action/compare/5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89...f06c13b6b1a9625abc9e6e439d9c05a8f2190e94">compare
view</a></li>
</ul>
</details>
<br />
2026-07-02 08:07:32 +02:00
dependabot[bot] 22810c920e Bump goreleaser/goreleaser-action from 7.2.2 to 7.2.3
Bumps [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) from 7.2.2 to 7.2.3.
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](https://github.com/goreleaser/goreleaser-action/compare/5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89...f06c13b6b1a9625abc9e6e439d9c05a8f2190e94)

---
updated-dependencies:
- dependency-name: goreleaser/goreleaser-action
  dependency-version: 7.2.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-02 05:29:20 +00:00
Stefan Haller a7e79c0c96 Bump actions/checkout from 6 to 7 (#5723)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to
7.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/releases">actions/checkout's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>block checking out fork pr for pull_request_target and workflow_run
by <a href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2454">actions/checkout#2454</a></li>
<li>Bump actions/publish-immutable-action from 0.0.3 to 0.0.4 in the
minor-actions-dependencies group across 1 directory by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2458">actions/checkout#2458</a></li>
<li>Bump flatted from 3.3.1 to 3.4.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2460">actions/checkout#2460</a></li>
<li>Bump js-yaml from 4.1.0 to 4.2.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2461">actions/checkout#2461</a></li>
<li>Bump <code>@​actions/core</code> and
<code>@​actions/tool-cache</code> and Remove uuid by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2459">actions/checkout#2459</a></li>
<li>upgrade module to esm and update dependencies by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2463">actions/checkout#2463</a></li>
<li>Bump the minor-npm-dependencies group across 1 directory with 3
updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2462">actions/checkout#2462</a></li>
<li>getting ready for checkout v7 release by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2464">actions/checkout#2464</a></li>
<li>update error wording by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2467">actions/checkout#2467</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/2454">actions/checkout#2454</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v6.0.3...v7.0.0">https://github.com/actions/checkout/compare/v6.0.3...v7.0.0</a></p>
<h2>v6.0.3</h2>
<h2>What's Changed</h2>
<ul>
<li>Update changelog by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2357">actions/checkout#2357</a></li>
<li>fix: expand merge commit SHA regex and add SHA-256 test cases by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
<li>Fix checkout init for SHA-256 repositories by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2439">actions/checkout#2439</a></li>
<li>Update changelog for v6.0.3 by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2446">actions/checkout#2446</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/yaananth"><code>@​yaananth</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v6...v6.0.3">https://github.com/actions/checkout/compare/v6...v6.0.3</a></p>
<h2>v6.0.2</h2>
<h2>What's Changed</h2>
<ul>
<li>Add orchestration_id to git user-agent when ACTIONS_ORCHESTRATION_ID
is set by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2355">actions/checkout#2355</a></li>
<li>Fix tag handling: preserve annotations and explicit fetch-tags by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2356">actions/checkout#2356</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v6.0.1...v6.0.2">https://github.com/actions/checkout/compare/v6.0.1...v6.0.2</a></p>
<h2>v6.0.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Update all references from v5 and v4 to v6 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2314">actions/checkout#2314</a></li>
<li>Add worktree support for persist-credentials includeIf by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2327">actions/checkout#2327</a></li>
<li>Clarify v6 README by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2328">actions/checkout#2328</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v6...v6.0.1">https://github.com/actions/checkout/compare/v6...v6.0.1</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/blob/main/CHANGELOG.md">actions/checkout's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>v7.0.0</h2>
<ul>
<li>Block checking out fork PR for pull_request_target and workflow_run
by <a href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2454">actions/checkout#2454</a></li>
<li>Bump actions/publish-immutable-action from 0.0.3 to 0.0.4 in the
minor-actions-dependencies group across 1 directory by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2458">actions/checkout#2458</a></li>
<li>Bump flatted from 3.3.1 to 3.4.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2460">actions/checkout#2460</a></li>
<li>Bump js-yaml from 4.1.0 to 4.2.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2461">actions/checkout#2461</a></li>
<li>Bump <code>@​actions/core</code> and
<code>@​actions/tool-cache</code> and Remove uuid by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2459">actions/checkout#2459</a></li>
<li>upgrade module to esm and update dependencies by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2463">actions/checkout#2463</a></li>
<li>Bump the minor-npm-dependencies group across 1 directory with 3
updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2462">actions/checkout#2462</a></li>
</ul>
<h2>v6.0.3</h2>
<ul>
<li>Fix checkout init for SHA-256 repositories by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2439">actions/checkout#2439</a></li>
<li>fix: expand merge commit SHA regex and add SHA-256 test cases by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
</ul>
<h2>v6.0.2</h2>
<ul>
<li>Fix tag handling: preserve annotations and explicit fetch-tags by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2356">actions/checkout#2356</a></li>
</ul>
<h2>v6.0.1</h2>
<ul>
<li>Add worktree support for persist-credentials includeIf by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2327">actions/checkout#2327</a></li>
</ul>
<h2>v6.0.0</h2>
<ul>
<li>Persist creds to a separate file by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2286">actions/checkout#2286</a></li>
<li>Update README to include Node.js 24 support details and requirements
by <a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2248">actions/checkout#2248</a></li>
</ul>
<h2>v5.0.1</h2>
<ul>
<li>Port v6 cleanup to v5 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2301">actions/checkout#2301</a></li>
</ul>
<h2>v5.0.0</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
</ul>
<h2>v4.3.1</h2>
<ul>
<li>Port v6 cleanup to v4 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2305">actions/checkout#2305</a></li>
</ul>
<h2>v4.3.0</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<h2>v4.2.2</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<h2>v4.2.1</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/checkout/commit/9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0"><code>9c091bb</code></a>
update error wording (<a
href="https://redirect.github.com/actions/checkout/issues/2467">#2467</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/1044a6dea927916f2c38ba5aeffbc0a847b1221a"><code>1044a6d</code></a>
getting ready for checkout v7 release (<a
href="https://redirect.github.com/actions/checkout/issues/2464">#2464</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/f0282184c7ce73ab54c7e4ab5a617122602e575f"><code>f028218</code></a>
Bump the minor-npm-dependencies group across 1 directory with 3 updates
(<a
href="https://redirect.github.com/actions/checkout/issues/2462">#2462</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/d914b262ffc244530a203ab40decab34c3abf34d"><code>d914b26</code></a>
upgrade module to esm and update dependencies (<a
href="https://redirect.github.com/actions/checkout/issues/2463">#2463</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/537c7ef99cef6e5ddb5e7ff5d16d14510503801d"><code>537c7ef</code></a>
Bump <code>@​actions/core</code> and <code>@​actions/tool-cache</code>
and Remove uuid (<a
href="https://redirect.github.com/actions/checkout/issues/2459">#2459</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/130a169078a413d3a5246a393625e8e742f387f6"><code>130a169</code></a>
Bump js-yaml from 4.1.0 to 4.2.0 (<a
href="https://redirect.github.com/actions/checkout/issues/2461">#2461</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/7d09575332117a40b46e5e020664df234cd416f3"><code>7d09575</code></a>
Bump flatted from 3.3.1 to 3.4.2 (<a
href="https://redirect.github.com/actions/checkout/issues/2460">#2460</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/0f9f3aa320cb53abeb534aeb54048075d9697a0e"><code>0f9f3aa</code></a>
Bump actions/publish-immutable-action (<a
href="https://redirect.github.com/actions/checkout/issues/2458">#2458</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/f9e715a95fcd1f9253f77dd28f11e88d2d6460c7"><code>f9e715a</code></a>
block checking out fork pr for pull_request_target and workflow_run (<a
href="https://redirect.github.com/actions/checkout/issues/2454">#2454</a>)</li>
<li>See full diff in <a
href="https://github.com/actions/checkout/compare/v6...v7">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=6&new-version=7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>
2026-07-02 07:27:31 +02:00
dependabot[bot] 943a4426a1 Bump actions/checkout from 6 to 7
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-02 05:24:40 +00:00
Stefan Haller 46427c39f1 Bump actions/cache from 5 to 6 (#5722)
Bumps [actions/cache](https://github.com/actions/cache) from 5 to 6.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/cache/releases">actions/cache's
releases</a>.</em></p>
<blockquote>
<h2>v6.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update packages, migrate to ESM by <a
href="https://github.com/Samirat"><code>@​Samirat</code></a> in <a
href="https://redirect.github.com/actions/cache/pull/1760">actions/cache#1760</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/cache/compare/v5...v6.0.0">https://github.com/actions/cache/compare/v5...v6.0.0</a></p>
<h2>v5.0.5</h2>
<h2>What's Changed</h2>
<ul>
<li>Update ts-http-runtime dependency by <a
href="https://github.com/yacaovsnc"><code>@​yacaovsnc</code></a> in <a
href="https://redirect.github.com/actions/cache/pull/1747">actions/cache#1747</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/cache/compare/v5...v5.0.5">https://github.com/actions/cache/compare/v5...v5.0.5</a></p>
<h2>v5.0.4</h2>
<h2>What's Changed</h2>
<ul>
<li>Add release instructions and update maintainer docs by <a
href="https://github.com/Link"><code>@​Link</code></a>- in <a
href="https://redirect.github.com/actions/cache/pull/1696">actions/cache#1696</a></li>
<li>Potential fix for code scanning alert no. 52: Workflow does not
contain permissions by <a
href="https://github.com/Link"><code>@​Link</code></a>- in <a
href="https://redirect.github.com/actions/cache/pull/1697">actions/cache#1697</a></li>
<li>Fix workflow permissions and cleanup workflow names / formatting by
<a href="https://github.com/Link"><code>@​Link</code></a>- in <a
href="https://redirect.github.com/actions/cache/pull/1699">actions/cache#1699</a></li>
<li>docs: Update examples to use the latest version by <a
href="https://github.com/XZTDean"><code>@​XZTDean</code></a> in <a
href="https://redirect.github.com/actions/cache/pull/1690">actions/cache#1690</a></li>
<li>Fix proxy integration tests by <a
href="https://github.com/Link"><code>@​Link</code></a>- in <a
href="https://redirect.github.com/actions/cache/pull/1701">actions/cache#1701</a></li>
<li>Fix cache key in examples.md for bun.lock by <a
href="https://github.com/RyPeck"><code>@​RyPeck</code></a> in <a
href="https://redirect.github.com/actions/cache/pull/1722">actions/cache#1722</a></li>
<li>Update dependencies &amp; patch security vulnerabilities by <a
href="https://github.com/Link"><code>@​Link</code></a>- in <a
href="https://redirect.github.com/actions/cache/pull/1738">actions/cache#1738</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/XZTDean"><code>@​XZTDean</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/cache/pull/1690">actions/cache#1690</a></li>
<li><a href="https://github.com/RyPeck"><code>@​RyPeck</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/cache/pull/1722">actions/cache#1722</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/cache/compare/v5...v5.0.4">https://github.com/actions/cache/compare/v5...v5.0.4</a></p>
<h2>v5.0.3</h2>
<h2>What's Changed</h2>
<ul>
<li>Bump <code>@actions/cache</code> to v5.0.5 (Resolves: <a
href="https://github.com/actions/cache/security/dependabot/33">https://github.com/actions/cache/security/dependabot/33</a>)</li>
<li>Bump <code>@actions/core</code> to v2.0.3</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/cache/compare/v5...v5.0.3">https://github.com/actions/cache/compare/v5...v5.0.3</a></p>
<h2>v.5.0.2</h2>
<h1>v5.0.2</h1>
<h2>What's Changed</h2>
<p>When creating cache entries, 429s returned from the cache service
will not be retried.</p>
<h2>v5.0.1</h2>
<blockquote>
<p>[!IMPORTANT]
<strong><code>actions/cache@v5</code> runs on the Node.js 24 runtime and
requires a minimum Actions Runner version of
<code>2.327.1</code>.</strong></p>
</blockquote>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/cache/blob/main/RELEASES.md">actions/cache's
changelog</a>.</em></p>
<blockquote>
<h1>Releases</h1>
<h2>How to prepare a release</h2>
<blockquote>
<p>[!NOTE]
Relevant for maintainers with write access only.</p>
</blockquote>
<ol>
<li>Switch to a new branch from <code>main</code>.</li>
<li>Run <code>npm test</code> to ensure all tests are passing.</li>
<li>Update the version in <a
href="https://github.com/actions/cache/blob/main/package.json"><code>https://github.com/actions/cache/blob/main/package.json</code></a>.</li>
<li>Run <code>npm run build</code> to update the compiled files.</li>
<li>Update this <a
href="https://github.com/actions/cache/blob/main/RELEASES.md"><code>https://github.com/actions/cache/blob/main/RELEASES.md</code></a>
with the new version and changes in the <code>## Changelog</code>
section.</li>
<li>Run <code>licensed cache</code> to update the license report.</li>
<li>Run <code>licensed status</code> and resolve any warnings by
updating the <a
href="https://github.com/actions/cache/blob/main/.licensed.yml"><code>https://github.com/actions/cache/blob/main/.licensed.yml</code></a>
file with the exceptions.</li>
<li>Commit your changes and push your branch upstream.</li>
<li>Open a pull request against <code>main</code> and get it reviewed
and merged.</li>
<li>Draft a new release <a
href="https://github.com/actions/cache/releases">https://github.com/actions/cache/releases</a>
use the same version number used in <code>package.json</code>
<ol>
<li>Create a new tag with the version number.</li>
<li>Auto generate release notes and update them to match the changes you
made in <code>RELEASES.md</code>.</li>
<li>Toggle the set as the latest release option.</li>
<li>Publish the release.</li>
</ol>
</li>
<li>Navigate to <a
href="https://github.com/actions/cache/actions/workflows/release-new-action-version.yml">https://github.com/actions/cache/actions/workflows/release-new-action-version.yml</a>
<ol>
<li>There should be a workflow run queued with the same version
number.</li>
<li>Approve the run to publish the new version and update the major tags
for this action.</li>
</ol>
</li>
</ol>
<h2>Changelog</h2>
<h3>6.1.0</h3>
<ul>
<li>Bump <code>@actions/cache</code> to v6.1.0 to pick up <a
href="https://redirect.github.com/actions/toolkit/pull/2435">actions/toolkit#2435
Handle cache write error due to read-only token</a></li>
<li>Switch redundant &quot;Cache save failed&quot; warning to debug log
in save-only</li>
</ul>
<h3>6.0.0</h3>
<ul>
<li>Updated <code>@actions/cache</code> to ^6.0.1,
<code>@actions/core</code> to ^3.0.1, <code>@actions/exec</code> to
^3.0.0, <code>@actions/io</code> to ^3.0.2</li>
<li>Migrated to ESM module system</li>
<li>Upgraded Jest to v30 and test infrastructure to be ESM
compatible</li>
</ul>
<h3>5.0.4</h3>
<ul>
<li>Bump <code>minimatch</code> to v3.1.5 (fixes ReDoS via globstar
patterns)</li>
<li>Bump <code>undici</code> to v6.24.1 (WebSocket decompression bomb
protection, header validation fixes)</li>
<li>Bump <code>fast-xml-parser</code> to v5.5.6</li>
</ul>
<h3>5.0.3</h3>
<ul>
<li>Bump <code>@actions/cache</code> to v5.0.5 (Resolves: <a
href="https://github.com/actions/cache/security/dependabot/33">https://github.com/actions/cache/security/dependabot/33</a>)</li>
<li>Bump <code>@actions/core</code> to v2.0.3</li>
</ul>
<h3>5.0.2</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/cache/commit/2c8a9bd7457de244a408f35966fab2fb45fda9c8"><code>2c8a9bd</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/cache/issues/1760">#1760</a>
from actions/samirat/esm_migration_and_package_update</li>
<li><a
href="https://github.com/actions/cache/commit/e9b91fdc3fea7d79165fceb79042ef45c2d51023"><code>e9b91fd</code></a>
Prettier fixes</li>
<li><a
href="https://github.com/actions/cache/commit/e4884b8ff7f92ef6b52c79eda480bbc86e685adb"><code>e4884b8</code></a>
Rebuild dist</li>
<li><a
href="https://github.com/actions/cache/commit/10baf0191a3c426ea0fa4a3253a5c04233b6e18f"><code>10baf01</code></a>
Fixed licenses</li>
<li><a
href="https://github.com/actions/cache/commit/e39b386c9004d72a15d864ade8c0b3a702d47a37"><code>e39b386</code></a>
Fix test mock return order</li>
<li><a
href="https://github.com/actions/cache/commit/b6928203372a8571ff984c0c883ef3a1adfb0c06"><code>b692820</code></a>
PR feedback</li>
<li><a
href="https://github.com/actions/cache/commit/60749128a44d25d3c520a489e576380cf00ff3f1"><code>6074912</code></a>
Rebuild dist bundles as ESM to match type:module</li>
<li><a
href="https://github.com/actions/cache/commit/5a912e8b4af820fa082a0e75cfd2c782f8fbfe0e"><code>5a912e8</code></a>
Fix lint and jest issues</li>
<li><a
href="https://github.com/actions/cache/commit/b9bf592b98b6a5d0cad9929c76247de1cac78abe"><code>b9bf592</code></a>
Update documentation for v6 release</li>
<li><a
href="https://github.com/actions/cache/commit/80f777761d0990932f64ed2740522d7226a49062"><code>80f7777</code></a>
Update packages, migrate to ESM</li>
<li>See full diff in <a
href="https://github.com/actions/cache/compare/v5...v6">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/cache&package-manager=github_actions&previous-version=5&new-version=6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>
2026-07-02 07:23:41 +02:00
dependabot[bot] 450c3c51af Bump actions/cache from 5 to 6
Bumps [actions/cache](https://github.com/actions/cache) from 5 to 6.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-02 05:20:45 +00:00
Stefan Haller 37ab23b680 Bump github.com/sahilm/fuzzy from 0.1.2 to 0.1.3 (#5706)
Bumps [github.com/sahilm/fuzzy](https://github.com/sahilm/fuzzy) from
0.1.2 to 0.1.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/sahilm/fuzzy/releases">github.com/sahilm/fuzzy's
releases</a>.</em></p>
<blockquote>
<h2>v0.1.3</h2>
<h2>What's Changed</h2>
<ul>
<li>Functions to use Iterators as Source by <a
href="https://github.com/wbrc"><code>@​wbrc</code></a> in <a
href="https://redirect.github.com/sahilm/fuzzy/pull/29">sahilm/fuzzy#29</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/wbrc"><code>@​wbrc</code></a> made their
first contribution in <a
href="https://redirect.github.com/sahilm/fuzzy/pull/29">sahilm/fuzzy#29</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/sahilm/fuzzy/compare/v0.1.2...v0.1.3">https://github.com/sahilm/fuzzy/compare/v0.1.2...v0.1.3</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/sahilm/fuzzy/commit/8aadc77d46cf09f5dc529d41358a94a24516c0b6"><code>8aadc77</code></a>
Functions to use Iterators as Source (<a
href="https://redirect.github.com/sahilm/fuzzy/issues/29">#29</a>)</li>
<li>See full diff in <a
href="https://github.com/sahilm/fuzzy/compare/v0.1.2...v0.1.3">compare
view</a></li>
</ul>
</details>
<br />
2026-07-02 07:19:38 +02:00
dependabot[bot] cfd8919c87 Bump github.com/sahilm/fuzzy from 0.1.2 to 0.1.3
Bumps [github.com/sahilm/fuzzy](https://github.com/sahilm/fuzzy) from 0.1.2 to 0.1.3.
- [Release notes](https://github.com/sahilm/fuzzy/releases)
- [Commits](https://github.com/sahilm/fuzzy/compare/v0.1.2...v0.1.3)

---
updated-dependencies:
- dependency-name: github.com/sahilm/fuzzy
  dependency-version: 0.1.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-02 05:08:14 +00:00
Stefan Haller db02d48204 Bump golang.org/x/sys from 0.45.0 to 0.46.0 (#5685)
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.45.0 to
0.46.0.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/golang/sys/commit/d58dcfa8a74514c0ef0fc401259156c5e2fc9ff5"><code>d58dcfa</code></a>
unix: add GPIO constants and structs</li>
<li>See full diff in <a
href="https://github.com/golang/sys/compare/v0.45.0...v0.46.0">compare
view</a></li>
</ul>
</details>
<br />
2026-07-02 07:06:31 +02:00
dependabot[bot] 12da065d59 Bump golang.org/x/sys from 0.45.0 to 0.46.0
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.45.0 to 0.46.0.
- [Commits](https://github.com/golang/sys/compare/v0.45.0...v0.46.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sys
  dependency-version: 0.46.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-02 05:03:31 +00:00
Stefan Haller 3631497cde Bump golang.org/x/sync from 0.20.0 to 0.21.0 (#5684)
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.20.0 to
0.21.0.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/golang/sync/commit/5071ed6a9f1617117556b66384f765c934de3698"><code>5071ed6</code></a>
all: fix some comments to improve readability</li>
<li>See full diff in <a
href="https://github.com/golang/sync/compare/v0.20.0...v0.21.0">compare
view</a></li>
</ul>
</details>
<br />
2026-07-02 07:01:59 +02:00
dependabot[bot] 4ad4f7a0df Bump golang.org/x/sync from 0.20.0 to 0.21.0
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.20.0 to 0.21.0.
- [Commits](https://github.com/golang/sync/compare/v0.20.0...v0.21.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sync
  dependency-version: 0.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-02 04:59:14 +00:00
Stefan Haller 722239abd1 Don't include common ancestor when picking "both" for a conflict in diff3 style (#5747)
When a merge conflict is rendered in the diff3 style, git includes the
common-ancestor section between the two sides. Pressing `b` used to pick
all three, which doesn't make sense; it now picks only the current and
incoming hunks.

Fixes #5486.
2026-07-01 10:06:05 +02:00
Stefan HallerandClaude Opus 4.8 ed3f4db4f9 Pick both hunks, not the common ancestor, in diff3 conflicts
`b` on a merge conflict is meant to keep both sides. With the diff3
conflict style git additionally renders the common ancestor between the
two sides, and the old ALL selection kept everything between the
outermost markers, dragging that ancestor into the resolved file.

Rename the selection from ALL to BOTH and restrict it to the top and
bottom hunks so the common base is dropped. Without the diff3 style
there is no ancestor section, so the behaviour there is unchanged.

The user-facing keybinding config was already named pickBothHunks; only
the internal enum, handler, translation and log string still said "all".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 09:57:26 +02:00
Stefan HallerandClaude Opus 4.8 d79f6c0b38 Add a test for picking both hunks in a diff3 conflict
Pressing `b` on a conflict is meant to pick both sides. With the diff3
conflict style, git also renders the common ancestor between the two
sides, and `b` currently keeps that ancestor section too, which is
wrong: the common base is neither side of the merge and must not end up
in the resolved file.

This test captures the current (buggy) behaviour so the follow-up fix
has a clear before/after.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 09:24:19 +02:00
Stefan Haller 760e9e587b Fix flaky TestNewCmdTaskInstantStop test (#5743)
TestNewCmdTaskInstantStop is flaky: it closes the stop channel from
within start() and asserts the stopped task touched nothing. But Go's
select picks uniformly at random among ready cases, so when opts.Stop
and a data channel are both ready the loop can pick the data channel,
call beforeStart() (which clears the view) and write the prefix before
bailing. In production a task that's already been superseded thereby
clobbers the output the incoming task is about to render.

Check stop with a non-blocking select before each blocking select, so
the stop signal wins whenever it's already closed (Go has no built-in
priority select; this is the idiomatic substitute). The selects keep
their own stop case for liveness, to unblock when stop closes while
parked waiting for data.
2026-06-30 17:40:06 +02:00
Stefan HallerandClaude Opus 4.8 adaef97ffe Give opts.Stop priority in NewCmdTask's read loop
TestNewCmdTaskInstantStop is flaky: it closes the stop channel from
within start() and asserts the stopped task touched nothing. But Go's
select picks uniformly at random among ready cases, so when opts.Stop
and a data channel are both ready the loop can pick the data channel,
call beforeStart() (which clears the view) and write the prefix before
bailing. In production a task that's already been superseded thereby
clobbers the output the incoming task is about to render.

Check stop with a non-blocking select before each blocking select, so
the stop signal wins whenever it's already closed (Go has no built-in
priority select; this is the idiomatic substitute). The selects keep
their own stop case for liveness, to unblock when stop closes while
parked waiting for data.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:34:57 +02:00
Stefan Haller e82dbf0fc6 Addition to AGENTS.md 2026-06-30 17:34:57 +02:00
Stefan Haller 62e1622f43 Silently consume unrecognized or malformed escape sequences (#5738)
Lazygit only recognizes a handful of escape sequences (mainly for
colors, erasing to the end of the line, and OSC-8 hyperlinks). It would
render all other ones as literal text in the UI, which doesn't make
sense. This wasn't a problem so far because other sequences tend not to
occur in pager output, but we are going to add ConPTY support for
Windows in a later PR, and ConPTY does emit a lot of those escape
sequences, which would then show up as junk in the UI.

While we're at it, also swallow malformed escape sequences instead of
printing them verbatim.
2026-06-30 09:41:11 +02:00
Stefan HallerandClaude Opus 4.7 06d2450459 Stop leaking other malformed and unimplemented escape sequences
After the previous commit, the escape interpreter still had five paths
that returned an error from parseOne, which view.go handles by rendering
whatever bytes it had accumulated as literal cells. Each of these is a
case where silently consuming the sequence is strictly better than
leaking garbage.

- ';' as the first CSI byte: '\x1b[;5H' is a valid sequence (row
  defaults to 1) but we errored on the leading ';'.
- Intermediate bytes in CSI ('\x1b[0 q' = DECSCUSR): the sequence ends
  in a final byte we don't implement, so consume and drop.
- Malformed SGR params (empty slot like '\x1b[1;;m'): if outputCSI
  fails mid-parse, reset state instead of re-emitting the sequence.
- OSC 8 that isn't actually OSC 8 ('\x1b]8x...'): treat as an OSC we
  don't understand and skip to its terminator rather than error-
  resetting mid-sequence, which used to leave the rest of the OSC body
  to be printed as text.
- The sanity-check overflow paths (too many params, param too long)
  now switch to a 'discard until final byte' state rather than
  returning the accumulated bytes.

A new stateCSIDiscard centralizes the 'consume bytes until the CSI
final' behavior used by both the intermediate-byte and overflow paths.
errCSITooLong and errOSCParseError are gone with their only callers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-30 09:20:15 +02:00
Stefan HallerandClaude Opus 4.7 31ed34a421 Silently consume unrecognized escape sequences
A text-mode escape interpreter can't do anything meaningful with cursor
positioning, DEC private modes, or terminal resets — but it must still
consume them, not print them as literal text. Before this change, any
sequence outside SGR / EL / OSC-8 errored out of parseOne, and view.go
rendered the unparsed bytes as visible cells. On Windows this would show
up as junk at the start of main-panel output once we add PTY support
using ConPTY, because ConPTY's session-init stream is full of such
sequences.

Three additions to the state machine:

- stateEscape: a single byte in 0x30–0x7E after ESC (e.g. ESC c = RIS)
  is a complete Fs/Fp sequence per ECMA-48; consume and reset.
- stateCSI: accept the DEC private-mode prefix bytes (<, =, >, ?), and
  accept a CSI final byte (0x40–0x7E) immediately after [ as the end of
  a zero-param sequence.
- stateParams: accept any CSI final byte we don't implement as the end
  of the sequence rather than a parse error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-30 09:20:15 +02:00
Stefan HallerandClaude Opus 4.7 8a8dacca14 Demonstrate that unknown escape sequences leak as literal text
The escape interpreter errors on anything outside the handful of
sequences it understands (SGR, EL, OSC 8 hyperlinks), and view.go then
renders the unparsed bytes as text cells.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-30 09:18:37 +02:00
Stefan Haller b31e572547 Improve resolving non-textual and submodule merge conflicts (#5735)
For a submodule conflict (where the two branches point the submodule at
different commits, resulting in a `UU` status), pressing space would
show a confusing "Nothing to stage…" error, and enter just stepped into
the submodule; neither is helpful for resolving the conflict. Now both
space and enter open a dialog where you can pick the current commit or
the incoming commit, each shown with its head commit subject so you know
what you're choosing. And the main panel explains the conflict and lists
the commits each side added, so you can see how they diverged before
deciding.

This now behaves very similarly to the way you resolve non-textual
conflicts for text files (e.g. `AU` or `UD`). Speaking of which: for
these it was still possible to press space to stage them, which simply
ran `git add`, whose behavior in this case is to keep the file. This is
neither obvious nor symmetric, so we change this to route space to the
conflict picker too, like enter.
2026-06-28 16:47:11 +02:00
Stefan HallerandClaude Opus 4.8 71a6396275 Extract per-case render helpers from FilesController.GetOnRenderToMain
GetOnRenderToMain had grown to handle five distinct rendering cases inline (no
selection, submodule conflict, inline text conflict, non-textual text conflict,
and the normal working-tree diff), which made it hard to follow. Split each case
into its own method so the function reads as a short dispatcher, and pull the
repeated main-view boilerplate into renderToMainWithTask. Pure refactor; no
behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 16:24:02 +02:00
Stefan HallerandClaude Opus 4.8 050225ffe6 Show per-side commit logs for submodule conflicts in the main view
When a conflicted submodule is selected, the main view shows the commits
each side added relative to their common ancestor as two indented logs,
labelled current and incoming, so it's clear which commit each side would
resolve to.

The logs aren't truncated (the view scrolls). If a side added no commits
of its own (e.g. it was rewound to an ancestor of the other), its head
commit is shown instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 16:12:36 +02:00
Stefan HallerandClaude Opus 4.8 afe4d14106 Resolve submodule conflicts through a picker
When both sides of a merge moved a submodule's gitlink, git reports it as "UU".
Pressing space used to fall into the submodule no-op guard and pop the confusing
"Nothing to stage..." error, and enter just entered the submodule, which does
nothing to resolve the superproject conflict.

Treat a conflicted submodule like the other non-textual conflicts: both space
and enter now open a picker offering the two candidate commits, "current" and
"incoming", each labelled with its summary. `git checkout --ours/--theirs` is a
no-op on gitlinks, so we resolve by checking the submodule out at the chosen
commit and staging it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 16:12:36 +02:00
Stefan HallerandClaude Opus 4.8 860f89e0c9 Route space to the conflict picker for non-textual conflicts
For a non-textual conflict (e.g. DD/AU/UA/UD/DU), pressing space used to run the
normal stage path, which did something unclear: `git add` happens to resolve the
conflict by keeping the file, but that's neither obvious nor symmetric. Route a
single such file to the same Keep/Delete picker that enter opens, so space and
enter agree.

For a range selection that includes one of these conflicts, staging makes no
sense, so disable it with a toast that points the user at resolving them one at
a time. (Entering a range was already disabled with the standard toast.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 16:12:36 +02:00
Stefan HallerandClaude Opus 4.8 41d92aac36 Extract a predicate for conflicts that need a resolution dialog
Some merge conflicts can't be resolved by editing markers in the merge view;
they require a dialog that picks one side (the "non-textual" conflicts like
DD/AU/UA/UD/DU). Both `enter` and, soon, `space` need to recognize these, so
pull the test into a shared predicate and rename handleNonInlineConflict to
openConflictResolutionMenu to match.

Restructure EnterFile so the predicate is checked first, ahead of the submodule
and inline-conflict branches. This is its final shape: upcoming commits only add
the submodule case to the predicate, with no further reordering. Behavior is
unchanged here, since the predicate is currently false for submodules.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 15:47:13 +02:00
Stefan Haller de6b6ef906 Addition to AGENTS.md 2026-06-28 15:47:13 +02:00
Stefan Haller bc61978175 Add a global keybinding for editing the config file (#5728)
The status panel already binds the universal edit key to "edit config
file", but that's only reachable while the status panel is focused. Add
a dedicated global binding (alt+shift+c) so the config file can be
opened from anywhere.
2026-06-24 22:14:11 +02:00
Stefan Haller 348224a96e Add a global keybinding for editing the config file
The status panel already binds the universal edit key to "edit config
file", but that's only reachable while the status panel is focused. Add
a dedicated global binding (alt+shift+c) so the config file can be
opened from anywhere.
2026-06-24 22:10:52 +02:00
Stefan Haller f3ea0ab902 Extract editConfig into a shared EditConfigAction
The "edit config file" command is about to gain a second, global
keybinding alongside the existing status-panel one. Moving its body into
an action struct (the convention GlobalController already follows for
all its handlers) lets both controllers delegate to one implementation.
2026-06-24 22:03:57 +02:00
Stefan Haller a8834930a0 Remove unnecessary askForConfigFile indirection 2026-06-24 22:02:55 +02:00
Stefan Haller 9b1acce0fe Remove the "Open config file" command
OpenFile (`o`) is for opening a file as if it was double-clicked in
Finder/Explorer; this is useful for binary files like PNGs, but never
for text files. You want to edit them, and there's `e` for that.
2026-06-24 22:02:52 +02:00
Stefan Haller 6be6c34d2a Make the side panels configurable (#5702)
Lazygit's left-hand side panels — and the tabs within them — have always
had a fixed arrangement. This adds a `gui.sidePanels` option so you can
lay them out the way you want.

You can:

- Reorder the panels
- Hide panels you don't use
- Regroup which panels share a slot as tabs
- Promote a tab to its own top-level panel — e.g. pull Worktrees out of
the Files panel so it's always visible

#### How it works

`gui.sidePanels` is a top-to-bottom list. Each entry is a list of names
that share one panel as tabs. The default is

```yml
gui:
  sidePanels:
    - [status]
    - [files, worktrees, submodules]
    - [branches, remotes, tags]
    - [commits, reflog]
    - [stash]
```

If you want to promote `worktrees` to a side panel of its own, and never
want to see stashes, use

```yml
gui:
  sidePanels:
    - [status]
    - [files, submodules]
    - [worktrees]
    - [branches, remotes, tags]
    - [commits, reflog]
```
2026-06-23 14:20:55 +02:00
Stefan HallerandClaude Opus 4.8 c6b8220772 Test per-repo side panel config and re-application on repo switch
Exercises the path the live reload relies on: a per-repo lazygit.yml
sets a different side panel layout, and switching between repos
re-applies each one's own layout (the new-repo path for the cloned repo,
the cached-repo path on switching back).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:15:18 +02:00
Stefan Haller 9aaff61b79 Re-apply the side panel config on a live config reload
When the config file changes and lazygit regains focus it reloads the
config, but the side panel window assignments, default views, tab
strips, and z-order were only ever set up on repo entry, so a changed
sidePanels wouldn't take effect until restart. Re-apply it from the
reload path: reassign windows and default views and restore each panel's
default tab.

The focused panel needs care: resetting it to its default tab would
leave the focused tab hidden behind that default tab, so the panel looks
unfocused even though its tab is selected. Re-focus the current context
so its tab stays shown and highlighted; only when the new config hides
the focused panel entirely do we move focus to the default side panel.
Tab strips are already refreshed via configureViewProperties.
2026-06-23 14:15:18 +02:00
Stefan HallerandClaude Opus 4.8 2614156b2f Add an IsActiveTab assertion for integration tests
Side panel tabs share a window, so which tab is shown is decided by view
z-order rather than the visibility flag (every tab in a window is
'visible'). Tests had no way to assert which tab is actually drawn in
front, which is distinct from which view has keyboard focus. Expose the
window's top view and add an IsActiveTab assertion built on it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:15:18 +02:00
Stefan HallerandClaude Opus 4.8 30559f1058 Let integration tests post a focus event
Lazygit reloads changed config files when its terminal window regains
focus, but the test harness had no way to simulate that focus event, so
the live config-reload path was untestable. Add a focus event to the
replayed-events queue and expose it through the GuiDriver as FocusIn.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:15:18 +02:00
Stefan HallerandClaude Opus 4.8 922861bb36 Clear tab strips on views that are no longer tabs
The tab-assignment loop only ever set a view's tabs; it never cleared them.
That was fine when the groupings were fixed, but with gui.sidePanels a
config reload can turn a tab into a standalone panel, and the old tab strip
would linger on its title. Index the tab strips by view name and assign to
every view, so views that dropped out of a multi-tab panel get their tabs
cleared. No change for a given config; this only matters across a reload.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:15:18 +02:00
Stefan HallerandClaude Opus 4.8 196f820af9 Add integration tests for configuring the side panels
Cover the three things gui.sidePanels enables: reordering the panels
(swapping branches and commits, checked via their jump keys), hiding a
panel (omitting stash, checked by cycling past the last panel and wrapping
to the first), and promoting a tab to its own panel (worktrees becomes a
top-level panel reachable by a jump key, and the files panel's remaining
tabs cycle straight to submodules).

The tests drive focus with explicit jump keys rather than ViewDriver.Focus,
which assumes the default panel layout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:15:18 +02:00
Stefan HallerandClaude Opus 4.8 ba0f7e8dba Show each panel's first configured tab by default
Within a window the visible tab is whichever view sits on top in the
z-order, and onRepoViewReset establishes that z-order from a fixed list
that needn't agree with the configured tab order. After ordering the
views, bring each panel's first configured tab to the top so that, for a
panel whose tabs have been reordered, the configured first tab is the one
shown before the panel is focused. No effect on the default layout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:15:18 +02:00
Stefan HallerandClaude Opus 4.8 e396483deb Scale the minimum window height with the panel count
In squashed mode (short terminals) the unfocused side panels each reserve
a row and the focused panel takes whatever is left, so once the unfocused
panels' rows fill the height the focused panel collapses to nothing and
panels below it render off-screen. The fixed floor of 9 was tuned for five
panels; with the panel count now configurable (and promotion allowing up
to ten), grow the floor by one per panel so we show the "not enough space"
view instead of a broken layout. Five panels still floor at 9.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:15:18 +02:00
Stefan HallerandClaude Opus 4.8 da8ef69133 Give the submodules and reflog views standalone titles
These two views only ever appeared as tabs (of the files and commits
panels), so unlike the other side views they had no title set; the tab
strip supplied their label. Once a tab can be promoted to its own panel
they can appear without a tab strip, so set their titles like the others.
This has no effect in the default layout, where both are always tabs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:15:18 +02:00
Stefan HallerandClaude Opus 4.8 56f3049af4 Drive the side panel layout from gui.sidePanels
Replace the hard-coded side panel order, tab groupings, and window
assignments with values resolved from the gui.sidePanels config. The panel
order (SideWindows and the layout boxes), the tab strips (viewTabMap), the
per-context window names, each window's default view, and the jump-label
groups all now come from the config rather than from five separate
hard-coded lists.

A panel's window name is the name of its first tab, and panels not listed
in the config get their own window name so their views stay hidden instead
of overlapping a visible panel. Three small lookups translate config names
into views, tab titles, and contexts; a test keeps them in sync with the
set of valid names. The lookups are split this way (rather than one
resolver) because configureViewProperties runs before the context tree
exists, so the title/view lookups must not depend on it.

The config is applied to a repo's contexts via applySidePanelConfig on
every repo entry, including the cached-repo path: a repo's per-repo config
can differ from the previously visited one's, so each repo's contexts must
be (re)assigned from its own config rather than kept from when they were
first built.

With the default config this reproduces today's layout exactly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:15:18 +02:00
Stefan HallerandClaude Opus 4.8 2f3ed7e0eb Stop requiring jumpToBlock to have exactly five entries
The number of side panels is about to become configurable, so a fixed
count of jump-to-panel keys no longer makes sense: a user who configures
six panels shouldn't be forced to also extend jumpToBlock, and one who
hides a panel shouldn't have to trim it. Drop the count check entirely
(individual keys are still validated) and assign keys to panels
positionally, for as many panels as there are keys. Surplus panels go
without a jump key but remain reachable via the next/previous-panel keys.
This also removes the log.Fatal that the count check guarded against.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:15:18 +02:00
Stefan HallerandClaude Opus 4.8 195b578fc1 Add the gui.sidePanels config option
This adds the user-facing surface for configuring the side panels: their
order, which ones are visible, and how tabs are grouped into panels. Each
entry is either a single panel name or a list of names sharing one panel
as tabs, mirroring how the Keybinding type accepts a scalar or a sequence;
the JSON schema restricts the names to the known set so editors can offer
completion and catch typos. The default reproduces today's layout exactly.

Validation rejects unknown or duplicated names, and requires the files,
branches, and commits panels to always be present: a lot of code focuses
those directly (e.g. after resolving a conflict or popping a stash), so
allowing them to be hidden would let that code focus a hidden panel.

Nothing reads the option yet; the layout still uses the hard-coded order.
Wiring follows in a later commit so the inert surface (and its generated
docs and schema) can be reviewed on its own.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:15:18 +02:00
Stefan HallerandClaude Opus 4.8 b7258958b8 Assign panel jump labels by iterating panel groups
The jump-label prefixes were assigned to each side view by name, twice
(once for the on case, once for off), so the panel-to-views grouping and
the panel order were baked into 28 positional statements. Express the
grouping once as a slice of view groups and loop over it, deriving each
panel's label from its index. The label lookup is now bounds-checked, so
it no longer assumes exactly as many jump bindings as panels. Behavior is
unchanged; this prepares the grouping to come from config.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:15:18 +02:00
Stefan HallerandClaude Opus 4.8 853f01eb3c Make the remote-branches view follow its parent's window
The remote-branches context is a transient guest that takes over a host
window when you drill into a remote. SubCommits and CommitFiles already
adopt their parent's window via SetWindowName when shown; RemoteBranches
relied instead on its static window name ("branches") matching the remotes
context's window. That assumption only holds while remotes lives in the
branches panel. Adopt the parent's window like the other transient guests
so remote branches render in the right place once panels are configurable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:15:18 +02:00
Stefan HallerandClaude Opus 4.8 dc9445014d Drive side-panel layout from a single window list
The three branches of sidePanelChildren each spelled out the five side
windows by name, so the panel order lived in three places and the
status/stash sizing special-cases were tangled into positional literals.
Map each branch over one `windows` slice instead, and fold the
normal-height special-cases (status's fixed height, stash's
collapse-unless-focused) into a single per-window function. Behavior is
unchanged; this isolates the ordering so it can later come from config.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:15:18 +02:00
Stefan HallerandClaude Opus 4.8 7a60f2de80 Ask agents to surface mid-implementation decisions
Record the working preference that calls which come up during
implementation (and weren't settled in planning) should be raised and
decided together, not made unilaterally and discovered later in the diff.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:15:18 +02:00
Stefan Haller 9f6db03fea Fix quoting of shell commands on Windows (#5704)
Lazygit builds a shell command by interpolating Quote'd arguments into a
template and running the result via `cmd /c`. Several things were wrong
on Windows:

- Quote emitted bash-style `\"…\"` quoting, which cmd.exe doesn't
understand. Making it usable at all previously required a fragile
round-trip through str.ToArgv and re-escaping.
- The assembled command line was handed to `cmd /c` without `/s`, so
cmd's default rules stripped the wrong quotes once the line contained
more than two of them (e.g. a quoted editor path at a location with
spaces, plus a quoted filename that also contains spaces).
- Shell metacharacters were escaped with `^` (`&` → `^&`, etc.), which
neutralised command chaining, pipes, redirection and `%VAR%` expansion
in custom commands.

Quote now emits the standard Windows convention directly, and NewShell
hands cmd.exe the fully-assembled line verbatim via SysProcAttr.CmdLine,
wrapped as `cmd /s /c "<command>"`. The /s flag strips exactly the outer
quote pair we add, leaving each argument's own quoting intact. With the
`^` escaping gone, metacharacters in a custom command reach cmd as the
author intended; this also removes the spurious `^` reported in #3092.

Fixes #5560
Fixes #2427
Fixes #4147
2026-06-23 14:14:07 +02:00
13817665aa Add end-to-end test for shell command quoting on Windows
The unit tests only assert the arguments lazygit constructs; they can't
catch cmd.exe's own quote-stripping, which is where #5560 actually
manifested. This test builds a small editor executable, places it and
the file it opens at paths containing spaces, runs it through real
cmd.exe via NewShell, and checks the editor received the intended args.
It runs only on Windows.

Co-Authored-By: Antoine Gaudreau Simard <a.simard@multidev.net>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:11:00 +02:00
Stefan HallerandClaude Opus 4.8 6b311ccb62 Fix quoting of shell commands on Windows
lazygit builds a shell command by interpolating Quote'd arguments into a
template and running the result via `cmd /c`. Several things were wrong
on Windows:

- Quote emitted bash-style `\"…\"` quoting, which cmd.exe doesn't
  understand. Making it usable at all previously required a fragile
  round-trip through str.ToArgv and re-escaping.
- The assembled command line was handed to `cmd /c` without `/s`, so
  cmd's default rules stripped the wrong quotes once the line contained
  more than two of them (e.g. a quoted editor path at a location with
  spaces, plus a quoted filename that also contains spaces).
- Shell metacharacters were escaped with `^` (`&` → `^&`, etc.), which
  neutralised command chaining, pipes, redirection and `%VAR%` expansion
  in custom commands.

Quote now emits the standard Windows convention directly, and NewShell
hands cmd.exe the fully-assembled line verbatim via SysProcAttr.CmdLine,
wrapped as `cmd /s /c "<command>"`. The /s flag strips exactly the outer
quote pair we add, leaving each argument's own quoting intact. With the
`^` escaping gone, metacharacters in a custom command reach cmd as the
author intended; this also removes the spurious `^` reported in #3092.

Fixes #5560
Fixes #2427
Fixes #4147

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:11:00 +02:00
Stefan HallerandClaude Opus 4.8 e0fcdf1c3f Demonstrate that shell metacharacters are mangled on Windows
On Windows, NewShell escapes shell metacharacters (`&`, `|`, `<`, `>`,
`%`) with `^` and splits the command into separate arguments. The
operators in a custom command therefore never reach cmd as operators,
so command chaining (`&&`), pipes, redirection and `%VAR%` expansion all
silently break (#2427, #4147, #5113; the stray `^` is also what #3092
reports).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:11:00 +02:00
Stefan HallerandClaude Opus 4.8 41efc9a37a Demonstrate that Quote produces invalid Windows quoting
On Windows, Quote wraps arguments in bash-style `\"…\"` and rewrites
embedded double quotes as `"'"'"`. Neither convention is understood by
cmd.exe or CommandLineToArgvW, so commands built from quoted arguments
are mis-parsed once they contain quotes or spaces (#5560).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:11:00 +02:00
Stefan Haller f103cf26dd Restructure the just recipes for running integration tests (#5720)
`just e2e` used to be the *visible-UI* test runner. Running it with no
arguments launched every integration test in a visible UI — painfully
slow — and it was easy to type `just e2e` out of habit when you actually
meant `just e2e-all` (the headless run-all). This PR reshuffles the
recipes so the common, fast path is the one with the shortest name.

## Recipe changes

| Command | Before | After |
| --- | --- | --- |
| `just e2e` | All tests, **visible UI** (very slow) | All tests,
**headless** (what CI runs) |
| `just e2e <name>` | — (no such target) | That one test, **headless** |
| `just e2e-cli [--slow\|--sandbox\|--debug] <name>` | — | A test in a
**visible UI** (the old `e2e`) |
| `just e2e-tui` | unchanged | unchanged |
| `just e2e-all` | All tests, headless | **removed** (now redundant) |

`just e2e` and `just e2e-cli` accept either a test name
(`submodule/reset`) or a full file path
(`pkg/integration/tests/submodule/reset.go`), and `e2e-cli`/`e2e-tui`
mirror the two `cmd/integration_test/main.go` subcommands (`cli` and
`tui`).

Also added zsh tab-completion for the test names, so `just e2e sub<Tab>`
expands to `just e2e submodule/`, then drills into the tests within.
2026-06-23 13:49:29 +02:00
Stefan Haller 131255ccf0 Use headless mode for the "Run current file integration test" vscode task
For running an integration test just to see if it fails or succeeds,
headless mode is sufficient and actually better, because it works in
small terminals like vscode's bottom panel; the "main.go cli" way of
running tests tends to fail there because the layout renders differently
in such a small window. Headless tests use a fixed window size, so they
don't have this problem. It's also slightly faster.

The other vscode tasks (slow and sandbox) are unchanged, they only make
sense with a visible UI.
2026-06-23 13:31:03 +02:00
Stefan Haller 6064c1091c Remove "Open deprecated test TUI" vscode task
I never use this.
2026-06-23 13:31:03 +02:00
Stefan Haller 23d378ddb9 Use just instead of make in AGENTS.md 2026-06-23 13:31:03 +02:00
Stefan HallerandClaude Opus 4.8 e33b6f9397 Document running the integration tests via the just recipes
The integration README still described the raw `go run cmd/integration_test`
and `go test` invocations, which are easy to get wrong (the headless go-test
command in particular) and don't match how we actually run the tests. Rewrite
the running/debugging/sandbox instructions around the justfile's e2e recipes
instead, and point at the optional zsh completion script. Also switch the
test-list regeneration hint to `just generate`, matching the rest of our docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 13:31:03 +02:00
Stefan HallerandClaude Opus 4.8 21f13fc3c7 Add zsh completion for the e2e integration-test recipes
just's completion is clap-dynamic and exposes no hook for completing a
recipe's arguments, so `just e2e <Tab>` couldn't suggest anything. Wrap
just's completer: for the e2e/e2e-cli recipes, complete the test names
found under pkg/integration/tests/, delegating everything else back to
just.

The names are fed to _multi_parts so they complete one "/"-separated
segment at a time — an empty <Tab> offers just the categories, then
drills into the tests within a category — and the .go extension and the
shared helper files are stripped so the candidates are exactly the names
the recipe accepts.

Source it from ~/.zshrc (after compinit) to enable; it's a no-op without
just installed and only activates inside a repo with a justfile and a
pkg/integration/tests/ directory.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 13:09:17 +02:00
Stefan HallerandClaude Opus 4.8 658a66e14b Restructure integration-test just targets
`just e2e` was the visible-UI runner, but it's only useful for a single test
(and even then only with --sandbox/--slow); running it without arguments is far
too slow, yet it was easy to invoke by reflex when `just e2e-all` (run all
headlessly) was meant.

Make `just e2e` the everyday headless runner: no arguments runs the whole
suite (what e2e-all did), and a test name runs just that one headlessly via
`go test -run` — which we had no target for before. The visible-UI runner moves
to `e2e-cli`, pairing with the existing `e2e-tui` (the two main.go subcommands).
e2e-all is now redundant and removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 13:09:05 +02:00
Stefan Haller f3eabb5351 Keep selected commits stable across refreshes (#5717)
With the recently added external change detection, it happens more often
now that we refresh the commits list because an agent made a commit in
the background. In this case, if we keep the selection index the same,
it now points at a different commit, making the main view show a
different commit too, which is confusing and annoying. To fix this,
track the selected commit and range anchor by hash before reloading,
then restore those rows if both hashes still exist. This also allows us
to get rid of some bespoke code that did this for the specific cases of
reverting a commit or cherry-picking commits, because those are now
handled by the generic mechanism.
2026-06-23 09:23:54 +02:00
Stefan Haller 7f96c8ff4f Remove bespoke commit selection restoration when starting a rebase
Starting an interactive rebase (the `edit` command and quick-start) used
to capture the selected commit range by hash before starting the rebase
and restore it afterwards, because new update-ref lines for stacked
branches can shift the commits' positions in the list. The generic
keep-selection-by-hash mechanism now does exactly this for every refresh,
including these, so the bespoke code is redundant.

This relies on the previous commit, which taught the generic matcher to
handle the case where the selected commit turns into a rebase TODO entry
while it's being edited - something the bespoke code handled implicitly
by matching on hash alone.
2026-06-23 09:20:05 +02:00
Stefan Haller b6063cff5b Restore commit selection even when the commit's TODO status changed
When restoring the commit selection after a refresh we match by hash and
TODO status. The TODO status is part of the match so that a commit being
reverted or cherry-picked is matched to the real commit rather than to
the rebase TODO entry that shares its hash.

But a selected commit can also change its TODO status across a refresh:
when starting an interactive rebase that stops to edit it, the real
commit becomes a TODO entry. Fall back to matching by hash alone when
there is no exact match, so the selection is still restored in that case.

The next commit relies on this to remove bespoke selection-restoration
code in the local commits controller that matched by hash alone, which
the generic mechanism otherwise wouldn't fully replace.
2026-06-23 09:20:05 +02:00
Stefan Haller c15ab5db5d Keep selected commits stable across refreshes
With the recently added external change detection, it happens more often
now that we refresh the commits list because an agent made a commit in
the background. In this case, if we keep the selection index the same,
it now points at a different commit, making the main view show a
different commit too, which is confusing and annoying. To fix this,
track the selected commit and range anchor by hash before reloading,
then restore those rows if both hashes still exist. This also allows us
to get rid of some bespoke code that did this for the specific cases of
reverting a commit or cherry-picking commits, because those are now
handled by the generic mechanism.
2026-06-23 09:20:05 +02:00
Stefan Haller 10d2f9f715 Allow GpgHelper to refresh differently on success and failure
Preparation for the next commit, which selects the newly created commit
after a commit succeeds, while leaving the selection alone on failure.
For now success and failure use the same refresh options, so behavior is
unchanged.
2026-06-23 08:43:51 +02:00
Stefan Haller 5d5aa0a865 Cleanup: wrap long parameter lists
This makes the following diff a little easier to read.
2026-06-23 08:43:51 +02:00
Stefan Haller 3f8dc527b5 Cleanup: remove unnecessary if statement 2026-06-23 08:43:51 +02:00
Stefan Haller cfb46f440c Add HeadCommitIdx helper function
Not used yet, we'll need it in the next commit.
2026-06-22 09:16:57 +02:00
Stefan Haller d673a0f3b9 Add tests for IsHeadCommit 2026-06-22 09:03:14 +02:00
Stefan Haller 02be5e74ed Tighten a test expectation
This guards against regressions from the changes that follow. We're
about to add a mechanism that keeps the selection anchored by commit
hash, but we need to make sure that it doesn't take effect here; after a
merge we want to select the newly added merge commit. In the current
state of the code this happens to work because we keep the selection
index the same, which happened to be 0 here; later we will change this
to explicitly select the head commit after the merge.
2026-06-22 09:00:15 +02:00
Stefan Haller 0b78438848 Prevent staging from becoming slower over time (#5712)
In a large repo, when touching (editing) more and more files, staging
hunks in lazygit could become slower and slower over time. Specifically,
this happened when you edited a lot of files and then discarded their
changes again. I have seen cases where staging a hunk began to take
seconds; the fix then was to type `git status` on the command line once,
this made it fast again.

The reason was that lazygit was trying too hard to be a good git
citizen, and used the `GIT_OPTIONAL_LOCKS=0` env var on every git
command it made. The consequence was that it never updated the mod date
cache in git's index file, which caused git to rehash every file whose
mod date doesn't match what it recorded in the index, on every refresh.
Typing `git status` updates that cache, which is why this was a
workaround.

Fix this by using the `GIT_OPTIONAL_LOCKS=0` flag only for refreshes
that are running unattended in the background, i.e. the periodic
autoRefresh and the newly external change detection. For those it is
important because it avoids "cannot lock index" errors for commands that
the user might issue at the same time. All other refreshes are user
initiated and no longer use the flag, which is in line with what `git
status` does, so this keeps performance from deteriorating over time.
2026-06-19 18:31:28 +02:00
Stefan HallerandClaude Opus 4.8 eb988395e6 Remove the now-redundant gitCmdObjBuilder wrapper
The wrapper existed to add a git-specific env var to every command. Now
that that's gone, its New/NewShell/Quote methods just delegated to the
inner builder. The only remaining git-specific behavior — the command
runner — is attached in the constructor via CloneWithNewRunner, which
already returns a complete builder, so we can return that directly and
drop the wrapper struct.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 18:14:24 +02:00
Stefan HallerandClaude Opus 4.8 d94f2f05ac Only pass --no-optional-locks for background status refreshes
We set GIT_OPTIONAL_LOCKS=0 for every git command we run. That env var
only affects `git status`: it tells git not to take the optional lock it
would otherwise use to write the index back after refreshing the cached
stat information. The intent was to avoid contending for index.lock with
git commands the user runs in a terminal.

The downside is that our `git status` never persists the refreshed
stat-cache. So whenever the working tree's cached stat info goes stale
(e.g. editing files and discarding the changes, or a checkout), every
subsequent status re-hashes the affected files to confirm they're clean,
and stays slow until something else writes the index (such as the user
running `git status` in a terminal).

Fix this by only suppressing optional locks for refreshes that run
unattended in the background; foreground refreshes triggered by a user
action now run a plain `git status` that writes the refreshed index back,
just like the command line does. Background refreshes keep passing
--no-optional-locks so they still can't cause lock contention.

RefreshOptions gains a Background flag that the background routines set,
threaded down to the status command.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 18:14:24 +02:00
Stefan Haller 94db69f64b Add GlobalArg/GlobalArgIf to GitCommandBuilder
This can be used to add a git argument that goes before the git
subcommand.
2026-06-19 18:14:24 +02:00
Stefan Haller 7c22c5220c Detect external repo changes via background polling (#5662)
Lazygit refreshes its UI when its terminal window gets the focus, which
is enough for the situation where the user makes a commit in their IDE
or in another git client. It isn't enough for the case that a coding
agent makes commits in the background. Improve this by doing a
light-weight poll of the git state in the background, and refresh when a
change is detected.

This adds two settings to control the mechanism:

- `git.autoDetectExternalChanges` (default true) is the on/off switch,
parallel to autoFetch/autoRefresh
- `refresher.externalChangeCheckInterval` (default 2 seconds) is the
poll cadence

Addresses #5554.
2026-06-19 18:10:38 +02:00
Stefan Haller 3050303ed2 Detect external ref changes via background polling
Add a 2-second background poll that calls Status.RefsSnapshot and
compares against the snapshot stored at the end of the last refs-
touching refresh. On a diff, trigger a full refresh — same scope as the
focus-in handler, because once we know something changed externally
we can't be sure what (an agent might have created a worktree or
stashed something alongside the commit we detected).

Refresh runs in SYNC mode because goEvery already serializes iterations
via <-done: a slow refresh delays the next tick naturally instead of
letting work stack. The post-refresh hook from the previous commit
updates the snapshot, so in-app commands don't cause the next poll to
spuriously re-fire.

Disabled in the integration test config, like autoRefresh and autoFetch,
because demo replays make repo changes throughout the run; at 2-second
cadence the resulting full refreshes compete with the demo's own
choreography and push some demos past their 40-second timeout.

Also list the two new config keys in checkForChangedConfigsThatDontAutoReload
so a config edit warns the user that lazygit needs a restart.
2026-06-19 18:07:48 +02:00
Stefan Haller c1eeacdfe8 Snapshot refs state before refs-touching refreshes
Add the storage and snapshot-update half of the external-change-detection
mechanism. RefreshHelper now keeps a mutex-protected snapshot string and exposes
accessors for it; Refresh captures a fresh snapshot at the start of any refresh
whose scope set includes COMMITS or BRANCHES.

We capture before reading the git state, not after. Capturing after would let an
external change that lands between the git state read and the snapshot (say, the
next step of a rebase running in another terminal) leave the stored snapshot
newer than what we actually rendered; the poller would then see no difference
and never refresh again, stranding the UI on the intermediate state. Capturing
first keeps the snapshot from running ahead of the render, so if disk moves
during the refresh the next poll catches it.

No reader of the snapshot exists yet — the polling goroutine that consumes it
comes in a later commit. Keeping the snapshot hook in its own commit isolates
the invariant that the snapshot stays in sync with what the UI has observed,
which is what makes the poller's change-detection predicate work across in-app
commands and focus-in refreshes.
2026-06-19 18:07:48 +02:00
Stefan Haller 661df80fe8 Add config options for external change detection
Two settings to control the upcoming background polling mechanism:

- git.autoDetectExternalChanges (default true) is the on/off switch, parallel to
  autoFetch/autoRefresh
- refresher.externalChangeCheckInterval (default 2 seconds) is the poll cadence

Disabling is the bool's job, not a magic 0 interval, matching the existing
convention.

Not yet referenced by any code.
2026-06-19 18:07:48 +02:00
Stefan Haller cb12cb2f6b Add Status.RefsSnapshot
A cheap fingerprint of local branches and HEAD that future code can poll to
detect when refs have moved externally.

Branches come from a porcelain for-each-ref. HEAD is read directly from
.git/HEAD: that avoids spawning a child process and captures the symref-or-hash
distinction we need to tell "detached at X" apart from "on a branch pointing at
X" — they share a commit hash, which is exactly the situation at the end of a
rebase when HEAD reattaches to the branch. The reftable backend doesn't keep a
real .git/HEAD (it writes a fixed stub), so when we see that stub or the file is
unreadable we fall back to porcelain commands, which are backend-agnostic.

Uses DontLog so a future polling caller won't spam the command log. Not yet
wired up to any caller.
2026-06-19 18:07:48 +02:00
Stefan Haller 93bd26b9a9 Centralize scope expansion in Refresh
Several downstream conditions in Refresh() relied on multi-scope predicates to
express "if X is in scope, Y also needs refreshing". This makes it hard to add
new code that needs to ask "does this refresh re-read refs?", because the answer
involves mirroring one of those predicates and keeping them in sync forever.

Expand the co-refreshing relationships once, up front, right after the scope set
is built. The downstream conditions then collapse to single-scope checks against
the (now-expanded) set. Behavior is preserved.

Two of the scattered multi-scope conditions are intentionally left as-is because
they express subsumption rather than co-refresh (one branch already does the
work of another internally — expanding would cause double-refresh), and one
expresses mid-function coupling on a flag set inside the COMMITS/BRANCHES block.
2026-06-19 18:07:48 +02:00
Stefan Haller d81b6d9e1d Log CPU time of external commands in addition to wall-clock time
For certain kinds of performance investigations it is useful to see this, and
doesn't terribly pollute the log, so just do this always.
2026-06-19 18:07:48 +02:00
Stefan Haller 04d62e072b Fix schema minimum for refresh and fetch intervals
The schema annotated refreshInterval and fetchInterval with minimum=0,
but the background routines reject a value of 0 (they require
interval > 0 and otherwise log it as invalid and disable the feature).
So 0 is not actually a valid value; switch to exclusiveMinimum=0 so the
schema matches what the code accepts.
2026-06-19 18:07:48 +02:00
Stefan Haller fcaf1512bd Fix Files Panel artefacts during rebase commands (#5661)
Several commands (rewording or amending an earlier commit, custom patch
operations, etc.) are implemented by starting an interactive rebase that
stops at a commit, amending it, and continuing. When no conflict occurs,
the user isn't meant to notice a rebase happened at all.

But a background file refresh can fire while the rebase is mid-flight
and render a dirty working copy of whatever the behind-the-scenes rebase
is doing (e.g. applying a custom patch).

To fix this, we pause the background routines for the duration of any
waiting-status operation — exactly the window in which lazygit is
driving the git operation itself and will refresh once at the end. The
boundary is also right for the conflict case: when a rebase stops on a
conflict the operation returns, the pause releases, and background
refreshes resume for the interactive resolution that follows.
2026-06-19 18:06:58 +02:00
Stefan Haller 3cf890b7d7 Pause background refreshes while driving a git operation
Several commands (rewording or amending an earlier commit, custom patch
operations, etc.) are implemented by starting an interactive rebase that stops
at a commit, amending it, and continuing. When no conflict occurs, the user
isn't meant to notice a rebase happened at all.

But a background file refresh can fire while the rebase is mid-flight and render
a dirty working copy of whatever the behind-the-scenes rebase is doing (e.g.
applying a custom patch).

To fix this, we pause the background routines for the duration of any
waiting-status operation — exactly the window in which lazygit is driving the
git operation itself and will refresh once at the end. The boundary is also
right for the conflict case: when a rebase stops on a conflict the operation
returns, the pause releases, and background refreshes resume for the interactive
resolution that follows.
2026-06-19 17:56:48 +02:00
Stefan Haller f9c81b655d Make background-refresh pausing reentrant
Replace the pauseBackgroundRefreshes bool with a count. The single existing
caller (subprocess suspend/resume) is unaffected, but we're about to add a
second, independent reason to pause — lazygit driving a git operation that the
background routines would otherwise catch mid-flight — and the two scopes can
overlap. A bool can't represent "two things both want refreshes paused"; a count
can.
2026-06-19 17:56:48 +02:00
Stefan Haller c81c653c55 Fix coloring of wrapped delta lines (#5711)
When using delta as a custom pager, and the view is so narrow that delta
needs to wrap long lines, only the last segment of such a wrapped line
would have its background color extended to the right edge of the view,
resulting in a block of lines with a frayed right edge, like this:

<img width="623" height="218" alt="Screenshot 2026-06-18 at 17 25 36"
src="https://github.com/user-attachments/assets/346156af-212f-44c3-b8aa-2a66c23ed836"
/>

Fix this by extending the wrapped line's background colors to the right
edge as well, making it look like this:

<img width="623" height="218" alt="Screenshot 2026-06-18 at 17 25 23"
src="https://github.com/user-attachments/assets/9af46ba3-701b-47ce-b38d-bdffaf5182fa"
/>
2026-06-18 17:32:11 +02:00
Stefan HallerandClaude Opus 4.7 51b409383c Extend the fill background to wrapped tail segments
Tools like delta paint each diff line's background with '\x1b[K' so
the color reaches the right edge. Up to now the '\x1b[K' handler
appended (InnerWidth - cx) explicit padding cells with the fill bg
so rendering picked up the color. That worked for short lines but
silently degraded once content exceeded InnerWidth: the repeat
count went non-positive, no cells were added, and after wrapping
the partial tail segment was left without any cells carrying the
fill color, so draw() fell back to the view's default bg.

Record the fill colors on the source line as optional
trailingFillAttributes. In the '\x1b[K' handler set them (and drop
the padding-cell loop — the metadata covers both the wrap and the
non-wrap cases). In draw(), once per source line, pick the trailing
cell's fg/bg from the metadata if present and otherwise from the
view defaults; then the inner-loop fills past-content cells with
that.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-18 17:22:31 +02:00
Stefan HallerandClaude Opus 4.7 9c8a02f901 Remove the '\n' sentinel cell
The sentinel was appended to every \n-terminated line solely so that
draw()'s prevFgColor tracking would reset to default for the trailing
area; without it, an AttrReverse-styled last cell would carry its
rendered bg past the end of the line.

The same prevFgColor mechanism propagated AttrReverse past content on
*unterminated* lines too — which doesn't match real terminal behavior
(try `print '\x1b[7m\x1b[31mfoo'` in a shell: the reverse stops at
the last character) and isn't relied on by anything in lazygit, since
all our writers terminate lines with \n.

Drop the sentinel cell, drop prevFgColor, and just have draw() paint
trailing cells with the view's default fg/bg. The
TestUnterminatedReverseLineExtendsToEdge regression test inverts to
document the new (terminal-matching) behavior, renamed accordingly.
TestWriteString expectations also drop the trailing "" that came
from the sentinel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-18 17:22:31 +02:00
Stefan HallerandClaude Opus 4.7 ad8335aaa8 Wrap v.lines cells in a line struct
The cells of a source line will soon need to carry metadata about how
the line was terminated (newline vs filled to edge via \x1b[K). Move
to a struct so there's somewhere to put it; this commit only renames
[][]cell to []line{cells: ...} with no behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-18 17:22:31 +02:00
Stefan Haller 1788eaa91f Rename lineType to cells
This is to free up the name lineType for something else in the next commit.
2026-06-18 17:22:31 +02:00
Stefan HallerandClaude Opus 4.7 d234d8b358 Remove dead \x00 filtering from string conversion helpers
The four ReplaceAll(str, "\x00", "") calls (and the equivalent
rune-by-rune skip in linesToString) are leftover from when cell.chr
was a rune and \x00 was used as an internal sentinel. With chr now
being a string and no code path writing \x00, the filtering never
strips anything.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-18 17:22:31 +02:00
Stefan HallerandClaude Opus 4.7 6c55823492 Demonstrate broken background fill on wrapped \x1b[K-padded lines
Tools like delta emit each diff line with the bg color set, then
\x1b[K to fill the rest of the row with that bg color. When the
content fits within the view's inner width, gocui's \x1b[K handling
appends explicit padding cells and rendering works. When the content
exceeds the inner width, \x1b[K adds no cells (negative repeat
count), the line is wrapped into multiple segments, and the partial
tail segment's trailing cells fall back to the view default bg
instead of continuing the fill color.

Add a test that drives draw() against a tcell mock terminal and
asserts the current (buggy) trailing background.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-18 17:22:31 +02:00
Stefan HallerandClaude Opus 4.7 2f18db4377 Add regression tests for trailing-fill rendering
The next few commits restructure how the view's draw() decides the
fg/bg of cells past the end of a line's content. Pin down three
existing behaviors first so the restructuring stays a refactor:

- '\n' should reset attributes for the trailing area so a reversed
  final cell doesn't bleed into empty space.
- An unterminated line with AttrReverse on its last cell should
  propagate that to the right edge (otherwise the rendered bg
  abruptly stops at the last character).
- '\x1b[K' on a line that fits within InnerWidth should fill the
  remaining cells with the current bg color.

Introduce a small WithSimulationScreen helper that swaps in a tcell
mock terminal so tests can call view.draw() and inspect rendered
cells via Screen.Get().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-18 17:21:27 +02:00
Stefan Haller 69004ce208 Some fixes to our infrastructure (#5705)
Minor things, mainly to improve the experience on Windows; see
individual commit messages.
2026-06-16 16:28:41 +02:00
Stefan Haller 643f169be2 Don't include integration tests in "just test" on Windows
This allows running "just check" on Windows, it just doesn't check quite
as much.
2026-06-16 16:21:13 +02:00
Stefan Haller fcce4de6fc Allow running check_script.sh when there are uncommitted changes 2026-06-16 16:21:13 +02:00
Stefan Haller df171722cb Make .go files have LF line endings on Windows
Since gofumpt expects and emits LF even on Windows, this makes it easier for
agents to gofumpt their files.
2026-06-16 16:21:13 +02:00
Stefan Haller 061665726e Fix Windows linter errors
Apparently we don't check Windows-only code for linter errors on CI.
2026-06-16 16:21:13 +02:00
Stefan Haller 8a7bef0975 Improve cycling through multiple pagers (#5678)
Lazygit lets you configure multiple pagers and switch between them with
the `|` key. The changes in this PR improve this for the case that you
have more than two.

- **You can see which pager you switched to.** The notification used to
just say "pager 2 of 3"; now it shows the pager's name, so you no longer
have to remember the order to know where you've landed.

- **You can name your pagers.** By default the name is taken from the
pager command, but you can set your own name in the config. This helps
when two entries run the same command with different options (for
example plain `delta` and `delta --side-by-side`).

- **You can cycle backwards.** Alongside `|`, which moves to the next
pager, the new `\` key moves to the previous one — so you can step back
instead of going all the way around the list to return to one you just
passed. This is especially useful when you have two pagers that you
alternate between often (e.g. `delta` and `delta --side-by-side`), but
also have several others in the list that you use only occasionally.

- **Invalid pager setups are caught early.** If an entry combines
options that can't be used together, lazygit now tells you about it on
startup instead of silently producing a broken diff.
2026-06-07 16:00:08 +02:00
Stefan HallerandClaude Opus 4.8 8534a05a2e Allow cycling pagers in reverse
With more than a couple of pagers, having to cycle forward through all
of them to reach the previous one (or to back out of an accidental press
of `|`) is tedious. Add a second binding that cycles backward.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:51:57 +02:00
Stefan HallerandClaude Opus 4.8 5a4247b234 Extract onPagerChanged helper from cyclePagers
A reverse-cycle handler is about to need the same re-render-and-toast
logic. Pull it out first so the behavior change that follows only has to
swap the cycle direction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:51:57 +02:00
Stefan HallerandClaude Opus 4.8 6316094d58 Show pager name in the cycle-pager toast, and let users name pagers
When cycling pagers, "Selected pager 2 of 3" gives no clue which pager
you landed on; with several configured you have to remember the order.
Include the pager's name in the toast instead.

The name is normally derived from the first word of the pager command,
but that isn't always enough: two entries can share a command but differ
in options (e.g. "delta" and "delta --side-by-side"), and an entry may
have no command at all (the default entry, or when using
useExternalDiffGitConfig). So add an optional `name` field that
overrides the derived name.

The message was also hardcoded in English; localize it while we're here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:51:57 +02:00
Stefan HallerandClaude Opus 4.8 81420ce362 Reject pager entries that combine multiple diff mechanisms
A pager (GIT_PAGER) formats the diff git produces, while
externalDiffCommand and useExternalDiffGitConfig change how git produces
the diff in the first place. They are different pipeline stages, not
alternatives, so combining them on one entry just pipes one through the
other and produces garbled output (e.g. delta trying to parse
difftastic's side-by-side output as a unified diff). The two external
mechanisms likewise conflict, with the explicit command silently
shadowing the git config one. Treat all three as mutually exclusive and
reject configs that set more than one on the same entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:51:56 +02:00
Stefan Haller 9739a43355 Terminate tooltip text with a full stop
This is our general convention for tooltips, it was just forgotten here.
2026-06-07 15:51:56 +02:00
Stefan Haller badb089a86 Add a "just check" command 2026-06-07 15:51:56 +02:00
Stefan Haller 68f3bcf53b Fix unstaging a submodule with dirty content (#5666)
Staging and unstaging submodules didn't work properly when the submodule
had uncommitted changes of its own (modified or untracked files inside
it). This PR fixes a few related problems:

- **Unstaging a submodule that has both a new commit and uncommitted
changes now works.** Previously, staging such a submodule left it
half-staged, and from there the stage key would only ever try to
re-stage it — there was no way to unstage it again. Now the stage key
toggles it back to unstaged as expected.

- **Trying to stage a submodule that has nothing stageable now explains
why.** When a submodule's only changes are uncommitted content inside it
(with no new commit), there's nothing the parent repository can stage.
Instead of the keypress silently doing nothing (except briefly flashing
the status to staged and then back to unstaged), lazygit now shows an
error explaining that you need to commit inside the submodule first.

- **The stage key (space) and the stage-all key (`a`) now behave
consistently.** All of the above applies equally whether you act on the
submodule directly or use "stage all", and "stage all" no longer gets
stuck or behaves differently from the stage key just because a submodule
with uncommitted changes is present in the list.

Fixes #3641.
2026-06-04 09:16:49 +02:00
Stefan HallerandClaude Opus 4.8 785c8a712c Explain when a submodule has nothing stageable
A submodule that only has dirty or untracked content (no new commit) can't
be staged from the parent repo, but it still shows up as having unstaged
changes. Pressing stage on it therefore briefly flashed as staged and then
reverted, without explaining why nothing was staged.

Detect this case (via `git submodule status`, where a '+' prefix marks a
stageable commit change) in the shared stage/unstage decision: if the only
thing that looks stageable is such a submodule, don't try to stage it.
Instead unstage if there's anything staged to unstage, so the toggle stays
symmetric; otherwise show an error explaining that there's nothing to stage.

Because the decision is shared, this covers both the stage (space) and
stage-all (a) keybindings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 09:12:09 +02:00
Stefan HallerandClaude Opus 4.8 8b5cfb0425 Optimistically render unstaging a dirty submodule
This map only feeds the optimistic rendering that makes staging feel
instant; it doesn't affect the eventual status, which git reports after
the refresh. The "MM" entry can never be reached for a regular file: a
file at "MM" has stageable unstaged changes, so pressing space stages it
rather than unstaging, and the unstage path is where this map is used. The
only thing that reaches the unstage path at "MM" is a submodule whose
commit is staged on top of dirty content, so this entry exists purely to
update that submodule instantly instead of waiting for the next git
status.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 09:12:09 +02:00
Stefan HallerandClaude Opus 4.8 c46c874442 Also verify stage-all can unstage a dirty submodule
Before the staging decision was unified, the stage (space) and stage-all
(a) keybindings each made their own decision, so a fix to one wouldn't
reach the other. Extend the test to drive the submodule through stage-all
as well, guarding against that asymmetry coming back.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 09:12:09 +02:00
Stefan HallerandClaude Opus 4.8 3f0a7512f8 Fix unstaging a submodule with dirty content
The stage/unstage toggle decides what to do based on whether a node has
unstaged changes: if it does, it stages; otherwise it unstages. For a
submodule this breaks down, because dirty or untracked content inside the
submodule always reports as an unstaged change in the parent repo but can
never be staged from there. Once such a submodule's commit pointer is
staged it sits at "MM", and every subsequent press keeps trying to stage
the unstageable dirty content, so it can never be unstaged.

Treat a submodule's unstaged change as stageable only when its commit
isn't already staged, so that a staged submodule unstages on the next
press regardless of leftover dirty content. Because the decision is now
shared by press and stage-all, this fixes both at once.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 09:12:09 +02:00
Stefan HallerandClaude Opus 4.8 66fe18dd59 Unify the stage/unstage decision for press and stage-all
pressWithLock (acting on the selection) and toggleStagedAllWithLock (acting
on the whole tree) each independently decided whether to stage or unstage,
ran the optimistic update, and logged the action. That duplicated decision
has already drifted: the tracked-files filter was added to press months
before it was applied to stage-all, and fixes to one have repeatedly had to
be chased into the other.

Extract that shared decision into toggleStaged, leaving each caller to
supply only the git commands it runs (per-path for the selection, bulk
add -A / reset for the whole tree — the latter is required because the tree
root node has an empty path, so a per-path stage wouldn't work). This is a
pure refactor: the two callers' decisions were already equivalent, so
behavior is unchanged. It exists so the next change to the staging logic
only has to be made once.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 09:12:09 +02:00
Stefan HallerandClaude Opus 4.8 c588c5507c Add a test demonstrating that you can't unstage a dirty submodule
When a submodule has both a new commit (which the parent repo can stage)
and dirty working-tree content (which it can't), staging it lands on a
"MM" status. Pressing space again should unstage it, but instead it tries
to stage the dirty content over and over, so you can never get back to an
unstaged state.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 09:12:09 +02:00
Stefan Haller 38526c9ec4 Add direnv support (#5660)
Supports loading direnv's environment files (`.envrc`) when switching
repos or worktrees, or when entering or exiting submodules.

There's no configuration for this; the functionality is automatically
enabled when direnv is installed.

Closes #3653.
2026-06-04 09:10:50 +02:00
Stefan HallerandClaude Opus 4.7 b76c1072ff Offer direnv .envrc approval from inside lazygit
When a user switches into a repo whose .envrc hasn't been approved with
`direnv allow`, the previous behavior was to drop a "blocked" error
popup and leave the user to fix it externally. That meant opening a
terminal, running `direnv allow`, and then either restarting lazygit or
switching repos and back to refresh the env — easy to get wrong, easy
to forget.

When `direnv export json` exits non-zero, follow up with `direnv status
--json` to ask direnv whether the current directory has a not-yet-
allowed .envrc, and if so, get its path. Then show a confirmation popup
with the .envrc contents inline so the user can read what they're
approving. Confirming runs `direnv allow <path>` and re-runs the load
so the new env reaches subprocesses immediately; cancelling leaves the
env unloaded (the same state as before this commit when direnv refused
to load the .envrc).

Using `direnv status --json` instead of parsing the "is blocked"
stderr line means we rely on direnv's structured output rather than
its human-readable error format, which is more stable across versions
and avoids assumptions about output formatting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 09:05:01 +02:00
Stefan HallerandClaude Opus 4.7 bb8955f2de Load direnv environment when switching repos
When a user opens a repo from the recent-repos menu or jumps between
worktrees inside lazygit, only the env vars present at process startup
reach subprocesses. That breaks pre-commit hooks and other tools whose
dependencies are pulled in by a per-repo .envrc — users were left with
read-only operations because the env their shell would normally load via
direnv never made it into lazygit's git invocations.

Shell out to `direnv export json` after each chdir and apply the JSON
delta via os.Setenv/Unsetenv. direnv tracks the previous load in its own
DIRENV_DIFF env var, so the delta also unloads vars from the old repo
when entering one without a matching .envrc. If direnv isn't on PATH the
call is a no-op, so users who don't use direnv pay nothing and users who
do need no config to opt in. Any stderr direnv emits (loading messages,
"blocked .envrc" errors, etc.) goes to the command log.

The integration test puts a fake direnv on PATH and asserts that a value
it exports reaches a custom command after switching repos. Wiring this
up needed runner.go to support `{{actualPath}}` placeholders in
ExtraEnvVars, mirroring the existing support for ExtraCmdArgs, so the
test can prepend a fixture-relative directory to PATH.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 09:05:01 +02:00
Stefan Haller 2601556189 Dedupe the recent-repos fallback in setupRepo
The for-loop here was a verbatim copy of openRecentRepo, so call that instead.
2026-06-04 09:05:01 +02:00
Stefan Haller 685dfc87a4 Cleanup: drop unneeded variable 2026-06-04 09:05:01 +02:00
Stefan Haller d86a49ba3f When RecordCurrentDirectory fails, only log the error
If we return the error here, we don't switch repos, but the chdir
happened already, so this would be an inconsistent state (a lot of
lazygit's code assumes that the current directory is always the worktree
root). Only log the error; failing to record the current directory is
not the end of the world.

Also, it is very unlikely to happen; RecordCurrentDirectory only writes
to a small file, and if this fails, then either there is filesystem
corruption of the disk is full, and in both cases the user likely has
much bigger problems.
2026-06-04 09:05:01 +02:00
Stefan Haller 009c8975be Additions to AGENTS.md (#5667) 2026-06-03 09:21:42 +02:00
Stefan Haller cc6a0374e8 Additions to AGENTS.md 2026-06-03 09:18:31 +02:00
Stefan Haller 16cf2ff1a9 Fix the waiting status display for synchronous operations (#5659)
Commit 4f0393f97b caused a regression: for operations that use
WithWaitingStatusSync (examples are squashing fixups, moving commits up
or down, cherry-picking, creating fixup commits, and more), the waiting
status wouldn't show during the operation; however, it would show after
the operation was done, and then linger forever.

The cause: since 4f0393f97b, layout sizes the bottom line from the
actual content of the AppStatus view rather than from the status
manager. The async render path keeps the view in sync (it sets the
buffer on the first tick and clears it to "" when the status ends), but
the sync path used by WithWaitingStatusSync did not:

- It called ForceLayoutAndRedraw before writing anything to the view, so
layout saw an empty buffer and left no room; the status never appeared
during the operation.
- When the operation finished it just broke out of the loop, leaving the
last spinner frame in the buffer. Every subsequent layout kept reserving
room for that stale content, so the status stuck around forever.

Fix this by writing the status into the view before the initial layout,
and clearing it again when stopping.
2026-05-30 14:25:21 +02:00
Stefan Haller 101d7965ae Fix the waiting status display for synchronous operations
Commit 4f0393f97b caused a regression: for operations that use
WithWaitingStatusSync (examples are squashing fixups, moving commits up or down,
cherry-picking, creating fixup commits, and more), the waiting status wouldn't
show during the operation; however, it would show after the operation was done,
and then linger forever.

The cause: since 4f0393f97b, layout sizes the bottom line from the actual
content of the AppStatus view rather than from the status manager. The async
render path keeps the view in sync (it sets the buffer on the first tick and
clears it to "" when the status ends), but the sync path used by
WithWaitingStatusSync did not:

- It called ForceLayoutAndRedraw before writing anything to the view, so layout
  saw an empty buffer and left no room; the status never appeared during the
  operation.
- When the operation finished it just broke out of the loop, leaving the last
  spinner frame in the buffer. Every subsequent layout kept reserving room for
  that stale content, so the status stuck around forever.

Fix this by writing the status into the view before the initial layout, and
clearing it again when stopping.
2026-05-30 14:20:05 +02:00
Stefan Haller 64d244cfcb Refactor: extract private setAppStatusContent helper method 2026-05-30 13:59:17 +02:00
400 changed files with 34916 additions and 3346 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
*.go text
*.go text eol=lf
*.md text eol=lf
*.json text eol=lf
+15 -13
View File
@@ -28,7 +28,7 @@ jobs:
GOFLAGS: -mod=vendor
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Setup Go
uses: actions/setup-go@v6
with:
@@ -59,11 +59,11 @@ jobs:
GOFLAGS: -mod=vendor
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Restore Git cache
if: matrix.git-version != 'latest'
id: cache-git-restore
uses: actions/cache/restore@v5
uses: actions/cache/restore@v6
with:
path: ~/git-${{matrix.git-version}}
key: ${{runner.os}}-git-${{matrix.git-version}}
@@ -80,7 +80,7 @@ jobs:
run: sudo make -C "$HOME/git-${{matrix.git-version}}" -j install
- name: Save Git cache
if: steps.cache-git-restore.outputs.cache-hit != 'true' && matrix.git-version != 'latest'
uses: actions/cache/save@v5
uses: actions/cache/save@v6
with:
path: ~/git-${{matrix.git-version}}
key: ${{runner.os}}-git-${{matrix.git-version}}
@@ -109,7 +109,7 @@ jobs:
GOARCH: amd64
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Setup Go
uses: actions/setup-go@v6
with:
@@ -136,7 +136,7 @@ jobs:
GOARCH: amd64
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Setup Go
uses: actions/setup-go@v6
with:
@@ -162,19 +162,21 @@ jobs:
GOFLAGS: -mod=vendor
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: 1.25.x
- name: Check formatting
run: ./scripts/gofumpt-check.sh
- name: Lint
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9
# Run even if the formatting check failed, so that both sets of
# problems are reported in a single CI run.
if: ${{ !cancelled() }}
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
- name: errors
run: golangci-lint run
if: ${{ failure() }}
upload-coverage:
# List all jobs that produce coverage files
needs: [unit-tests, integration-tests]
@@ -182,7 +184,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Setup Go
uses: actions/setup-go@v6
@@ -221,7 +223,7 @@ jobs:
run: echo "PR_FETCH_DEPTH=$(( ${{ github.event.pull_request.commits }} ))" >> "${GITHUB_ENV}"
- name: "Checkout PR branch and all PR commits"
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.ref }}
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Annotate locations with typos
uses: codespell-project/codespell-problem-matcher@9ba2c57125d4908eade4308f32c4ff814c184633 # v1.2.0
- name: Codespell
+2 -2
View File
@@ -46,7 +46,7 @@ jobs:
fi
- name: Checkout Code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
repository: jesseduffield/lazygit
token: ${{ secrets.LAZYGIT_RELEASE_PAT }}
@@ -159,7 +159,7 @@ jobs:
go-version: 1.25.x
- name: Run goreleaser
uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
with:
distribution: goreleaser
version: v2
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
if: ${{ github.repository == 'jesseduffield/lazygit' }}
steps:
- name: Checkout 🛎️
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Generate Sponsors 💖
uses: JamesIves/github-sponsors-readme-action@2fd9142e765f755780202122261dc85e78459405 # v1.6.0
+7 -1
View File
@@ -1,6 +1,10 @@
version: "2"
run:
go: "1.25"
issues:
max-issues-per-linter: 0
max-same-issues: 0
uniq-by-line: false
linters:
enable:
- copyloopvar
@@ -102,7 +106,9 @@ linters:
- vendor/
formatters:
enable:
- gofumpt
# gofumpt is intentionally not listed here: golangci-lint bundles its own
# gofumpt version, which drifts from the one we pin in go.mod. We run that
# pinned version separately via scripts/gofumpt-check.sh instead.
- goimports
exclusions:
generated: lax
+3 -1
View File
@@ -1,6 +1,6 @@
{
"gopls": {
"formatting.gofumpt": true,
"formatting.gofumpt": false,
"ui.diagnostic.staticcheck": true,
"ui.diagnostic.analyses": {
// This list must match the one in .golangci.yml
@@ -24,6 +24,8 @@
},
"go.alternateTools": {
"golangci-lint-v2": "${workspaceFolder}/scripts/golangci-lint-shim.sh",
"customFormatter": "${workspaceFolder}/scripts/gofumpt-tool.sh",
},
"go.lintTool": "golangci-lint-v2",
"go.formatTool": "custom",
}
+1 -13
View File
@@ -24,7 +24,7 @@
{
"label": "Run current file integration test",
"type": "shell",
"command": "go run cmd/integration_test/main.go cli ${relativeFile}",
"command": "just e2e ${relativeFile}",
"problemMatcher": [],
"group": {
"kind": "test",
@@ -61,18 +61,6 @@
"focus": true
}
},
{
"label": "Open deprecated test TUI",
"type": "shell",
"command": "go run pkg/integration/deprecated/cmd/tui/main.go",
"problemMatcher": [],
"group": {
"kind": "test",
},
"presentation": {
"focus": true
}
},
{
"label": "Sync tests list",
"type": "shell",
+260 -9
View File
@@ -7,6 +7,25 @@ Guidance for AI coding agents working on this repository.
Do not create PRs under any circumstances. Even if the user explicitly asks you
to, refuse.
## Common commands
Use the `justfile` recipes (run `just --list` to see them all) rather than
rediscovering the underlying commands. Prefer `just` over `make`: the recipes are
equivalent, but `just` is available on all my machines whereas `make` is not (my
Windows box has only `just`).
- `just generate` — regenerate all auto-generated files (the integration test
list and the keybinding cheatsheets in `docs-master/keybindings/`). Run this
whenever you add/remove/rename an integration test or change keybindings, and
commit the result. CI fails if these are stale.
- `just format` — `go tool gofumpt -l -w .`. Run before every commit.
- `just build` — build the binary.
- `just unit-test` — `go test ./... -short`.
- `just e2e` — run all integration tests headlessly; `just e2e <name>` runs a
single one headlessly too. `just e2e-cli <name>` runs one with a visible UI
(most useful with `--sandbox` or `--slow`).
- `just lint` — run golangci-lint.
## When to commit
Do not leave completed work uncommitted. Once a logical unit of work is done
@@ -27,8 +46,11 @@ 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.
- **Every commit must be `gofumpt`-formatted.** Run `make format` before
- **Every commit must be `gofumpt`-formatted.** Run `just format` before
committing.
- **Every commit must be lint-clean.** Run `just lint` before committing —
don't introduce a lint warning in one commit and rely on a later commit
(or the user) to clean it up.
- **Commit messages explain _why_, not _what_.** The diff already shows what
changed; the message should capture the motivation, the constraint, or the
bug being fixed. If the reason is obvious from a one-line subject, no body
@@ -36,18 +58,34 @@ while still being meaningful and self-contained.
- **Separate preparatory refactorings from behavior changes.** If a fix or
feature is easier to review after a refactor, land the refactor in its own
commit first. Pure refactors should be behavior-preserving; the commit that
changes behavior should be as small as possible.
changes behavior should be as small as possible. This applies even when the
refactor only becomes apparent _while_ writing the behavior change — e.g. you
extract a helper to avoid duplication. Don't let "I discovered it mid-change"
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.
- **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.
## Iterate with `fixup!` commits
When refining work that's already committed — adjusting an approach,
incorporating an idea from elsewhere, fixing something that belongs to the
same logical unit — create a fixup against the target commit
(`git commit --fixup=<sha>`) so the history collapses cleanly under
`git rebase --autosquash`. Don't pile follow-up commits on top with the
intent of squashing them later.
(`git commit --fixup=<sha>`) so it sits alongside its target, ready for the
user to fold in later with `git rebase --autosquash`. Don't pile follow-up
commits on top with the intent of squashing them later.
This holds **even when the target is the most recent commit (HEAD)**: use
`git commit --fixup`, not `git commit --amend`. A direct `--amend`
produces the same end state, which makes it tempting, but the point of a
fixup isn't only clean autosquash — it's that the refinement lands as a
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.
If the changes don't map cleanly onto existing commits — say they cut
across several of them, or restructure something at a different layer
@@ -57,10 +95,80 @@ call, but it's the user's call to make.
After writing a fixup, re-read the target commit's message. If anything in
that message has become inaccurate or misleading because of the fixup, use
an `amend!` commit instead (its subject is `amend! <original subject>` and
its body becomes the target's new full message after autosquash). A plain
`fixup!` keeps the original message verbatim, so message drift stays in
unless you explicitly correct it.
an `amend!` commit instead. The safest way to create one is
`git commit --fixup=amend:<sha>`, which opens the editor prefilled with the
target's existing message for you to revise.
An `amend!` commit's message has this exact shape:
```
amend! <original subject>
<new subject>
<new body>
```
The first line (`amend! <original subject>`) is **only the matcher** that
ties the commit to its target — it must equal the target's current subject.
Everything after the blank line is the **complete replacement message**, so
it must begin with a subject line of its own. Even when you only mean to
change the body, you still repeat the (unchanged) subject as that first line.
This is the trap when writing the message by hand with `-m` instead of using
the prefilled editor: if you pass only the body, there is no replacement
subject line, so after autosquash the target loses its subject and the first
body paragraph silently gets promoted to the subject. By hand it must be
`-m "amend! <subject>" -m "<subject>" -m "<body>"` — note the subject appears
twice, once in the matcher and once as the start of the replacement message.
A plain `fixup!` keeps the original message verbatim, so message drift stays
in unless you explicitly correct it.
**Never squash the fixups yourself.** Leave them in the history as separate
commits. Do not run `git rebase --autosquash`, do not `git commit --amend`
them into their targets, do not reorder or otherwise collapse them — not as
a "finishing" step, not to tidy up before handing off, not because the tree
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
run, never a step for you to perform. If you think the history is ready to
collapse, say so and leave it to them.
The same commit-structure rules apply to `fixup!` and `amend!` commits as
to regular ones: each must be a self-contained logical unit, and unrelated
changes must not be combined just because they happen to target the same
commit. If you have two independent refinements for the same target, make
two separate fixups. Reviewability of the intermediate state matters even
when the end state after autosquash would be identical.
## Surface mid-implementation decisions; decide them together
Planning can't anticipate everything. When a decision surfaces while you're
implementing — a design choice, a tradeoff, a scope cut, a "this turned out
harder than expected, so maybe X" — don't quietly make the call and keep
going, even if you have a clear recommendation and even if the call seems
small. Stop, lay out the options and your recommendation, and let me weigh in.
I want to make these calls _with_ you, not discover them after the fact in the
diff.
This isn't a request to stop and ask about every trivial detail; obvious
mechanical choices with one sensible answer don't need a checkpoint. It's about
genuine forks — the ones where a reasonable person might pick differently, or
where you'd be trading away something the plan assumed (scope, UX, performance,
reload behavior, …). When in doubt, surface it.
This applies with equal force to unforeseen _discoveries_, not just to
decisions you set out to make. If you find something the plan didn't account
for — a latent bug, a race, a wrong assumption, a case that turns out
unhandled — stop and raise it before designing or writing a fix, even when the
fix seems obvious and even when it's "just correctness." Finding the problem is
itself the fork: whether to fix it here or in a separate change, how generally
to solve it, and whether it reshapes the current work are all calls for me to
make with you. Don't quietly fold a self-directed fix for a newly-found problem
into the branch and let me discover it in the diff.
## Prefer the cleaner design over the smaller diff
@@ -131,6 +239,51 @@ and fix it instead of papering over it with a binding.
Use this pattern only where it makes sense; don't apply it by default.
## Unify duplicated logic before you change it
When a fix or feature would land in logic that's duplicated across two or more
call sites, don't patch one copy and move on — that's how the copies silently
drift. (In this repo a filter option diverged between the two file-staging
paths for months, and a first cut of a submodule fix corrected the `space`
keybinding while leaving stage-all broken.) Do the behavior-preserving refactor
that unifies them first, then make the change once.
Keep that refactor at the foundation of the branch, before the change. Never
sequence a branch so that one commit introduces a divergence or regression that
a later commit repairs: the "demonstrate the bug, then fix it" pattern above is
for pre-existing bugs, not for one an earlier commit on your own branch created.
Follow this even when the need for the refactor is only discovered in the middle
of working on the branch; suggest to the user to rewrite the history to move the
refactor to an earlier commit (but don't do it without asking first).
## Don't read model state right after a `Refresh`
A `Refresh` (or `RefreshFromWorker`) does its git work on a worker and then
*enqueues* the model update onto the UI thread. So when `Refresh` returns, the
model is **not** updated yet — the write is still queued. Reading a field
synchronously right after refreshing its scope reads the stale, pre-refresh
value (and this is true even for SYNC refreshes):
```go
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}})
files := self.c.Model().Files // BUG: still the pre-refresh value
```
Put the read in `RefreshOptions.Then` instead — it's queued after the scope's
model writes, so it sees the fresh value:
```go
self.c.Refresh(types.RefreshOptions{
Scope: []types.RefreshableView{types.FILES},
Then: func() error {
files := self.c.Model().Files // fresh
return nil
},
})
```
`Then` is a `func() error` and works with any non-`ASYNC` mode.
## Integration test conventions
Don't bind views to local variables. Always chain method calls directly from
@@ -143,6 +296,74 @@ keep the call site fluent.
Prefer `assert.Equal` (and friends) over hand-rolled `if` checks. The failure
messages are more useful and the intent is clearer at a glance.
## Translatable strings use Go templates, not `%s`
Never put `fmt.Sprintf`-style placeholders (`%s`, `%d`, …) in translatable
strings — the fields of `TranslationSet` and `Actions` in
`pkg/i18n/english.go`. Use named Go-template placeholders and fill them in with
`utils.ResolvePlaceholderString`:
```go
// in english.go
DeleteBranchTitle: "Delete branch '{{.selectedBranchName}}'?",
// at the call site
utils.ResolvePlaceholderString(
self.c.Tr.DeleteBranchTitle,
map[string]string{"selectedBranchName": branchName},
)
```
Named placeholders tell localizers what each value is (a bare `%s` says
nothing, and translators can't safely reorder positional verbs across
languages), and the map form extends cleanly when a string later needs more
than one placeholder. This holds for every user-facing string, including short
ones like disabled-action reasons and toasts.
## Only edit the English translations
`pkg/i18n/english.go` is the one translation file you edit; add, change, and
remove strings there. The other languages under `pkg/i18n/translations/` are
maintained by Crowdin and synced automatically — never edit them by hand, not
even to add a key you just introduced or to delete one you just removed. A
removed English string simply leaves an orphan key in those files, which
Crowdin cleans up on its own; an unknown key in a translation file is ignored
at load time, so it does no harm in the meantime.
## Try to keep new english.go strings within the existing column alignment
`gofumpt` aligns the `TranslationSet` struct fields and the `EnglishTranslationSet`
literal into columns, so a new field whose name is longer than the widest one in
its alignment block re-indents every line in that block. When there are several
feature branches in flight that all add strings, that reformatting churn turns
english.go into a rebase-conflict magnet. So when it's cheap to do so, make an
effort to keep a new field name within the current widest name in the block
(measure it; it's around 40 characters today), shortening the Go field name to
fit. This is a soft preference, not a rule: the usual "best name wins" still
applies, so don't mangle a name past the point of readability just to save a
column. Applies only to `pkg/i18n/english.go`.
## Code comments are for future readers, not development history
Comments in source code explain *why this code is shaped the way it is*. They
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
everyone who reads the file later: the rejected alternative is nowhere in the
file, so the comparison is meaningless to them.
Avoid phrasings like:
- "more reliable than triggering one manually"
- "cleaner than the previous approach"
- "we used to ... but ..."
- "after trying X, we found Y"
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
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.
## Don't present "live with the bug" as an option
When you're investigating a defect and laying out fix options for the user,
@@ -154,6 +375,36 @@ present actual fixes. If a real fix is genuinely out of reach (e.g. it
requires API changes you can't make), say so plainly; don't dress "no fix"
up as a viable option in a numbered list alongside real ones.
## Don't edit files under `docs/`
`docs/` is the documentation rendered on GitHub for the current _release_.
Users read it as the reference for the version they're running. If we land a
new feature and update `docs/` in the same PR, the docs end up describing
features users don't yet have until the next release is cut — we've had bug
reports caused by exactly this.
So:
- Document new features in `docs-master/` only. The release process
(`scripts/update_docs_for_release.sh`) copies `docs-master/` to `docs/` at
release time.
- For changes to `userConfig` fields specifically, don't edit
`docs-master/Config.md` by hand either — the relevant section is
auto-generated from the struct field doc comments. After editing the
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
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
`Config.md` (see `wrapLine` in `pkg/jsonschema/generate_config_docs.go`).
Manually wrapping a sentence across several `//` lines defeats this: the
generator preserves your arbitrary breaks as hard line breaks and embeds
`\n` at those points in the generated `schema-master/config.json`
description. (Putting genuinely separate sentences on their own lines is
fine; just don't split one sentence across lines.)
## Don't search outside the working tree
Never run `find` (or similar) from `/` or other paths outside the project. All
+3 -2
View File
@@ -36,10 +36,11 @@ generate:
.PHONY: format
format:
gofumpt -l -w .
go tool gofumpt -l -w .
.PHONY: lint
lint:
./scripts/gofumpt-check.sh
./scripts/golangci-lint-shim.sh run
# For more details about integration test, see https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md.
@@ -69,4 +70,4 @@ record-demo:
.PHONY: vendor
vendor:
go mod vendor && go mod tidy
go mod tidy && go mod vendor
BIN
View File
Binary file not shown.
+59 -2
View File
@@ -73,6 +73,8 @@ gui:
# If true, capture mouse events.
# When mouse events are captured, it's a little harder to select text: e.g.
# requiring you to hold the option key when on macOS.
# Mouse capture can also be toggled at runtime with the 'toggleMouseCapture'
# keybinding ('%' by default).
mouseEvents: true
# If true, do not show a warning when amending a commit.
@@ -110,6 +112,26 @@ gui:
# is true.
expandedSidePanelWeight: 2
# If true, don't give a side panel more height than it needs to show its
# content; when all panels fit, the leftover height is shared among them so that
# they still fill the screen.
shrinkSidePanelsToContent: false
# The side panels, in the order they appear from top to bottom.
# Each entry is a list of one or more names that share a single panel as tabs
# (cycle through them with the next-tab/previous-tab keys).
# Omit a name to hide it; give a name its own one-element list to promote a tab
# to a top-level panel.
# Valid names are: 'status', 'files', 'worktrees', 'submodules', 'branches',
# 'remotes', 'tags', 'commits', 'reflog', 'stash'. 'files', 'branches', and
# 'commits' must always be included; they can't be hidden.
sidePanels:
- [status]
- [files, worktrees, submodules]
- [branches, remotes, tags]
- [commits, reflog]
- [stash]
# Sometimes the main window is split in two (e.g. when the selected file has
# both staged and unstaged changes). This setting controls how the two sections
# are split.
@@ -342,6 +364,11 @@ gui:
git:
# Array of pagers. Each entry has the following format:
#
# # A name for the pager, shown in the notification when cycling pagers.
# # If not set, the name is derived from the first word of the pager
# # command (or of the external diff command).
# name: ""
#
# # Value of the --color arg in the git diff command. Some pagers want
# # this to be set to 'always' and some want it set to 'never'
# colorArg: "always"
@@ -361,6 +388,9 @@ git:
# # https://git-scm.com/docs/gitattributes#_defining_an_external_diff_driver.
# useExternalDiffGitConfig: false
#
# 'pager', 'externalDiffCommand', and 'useExternalDiffGitConfig' are mutually
# exclusive; set at most one per entry.
#
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Custom_Pagers.md
# for more information.
pagers: []
@@ -406,6 +436,11 @@ git:
# If true, periodically refresh files and submodules
autoRefresh: true
# If true, poll the repo periodically for external ref changes (commits, branch
# updates, checkouts made outside lazygit) and refresh when one is detected.
# Independent of autoRefresh, which only governs the files panel.
autoDetectExternalChanges: true
# If not "none", lazygit will automatically fast-forward local branches to match
# their upstream after fetching. Applies to branches that are not the currently
# checked out branch, and only to those that are strictly behind their upstream
@@ -499,6 +534,15 @@ git:
# to 40 to disable truncation.
truncateCopiedCommitHashesTo: 12
# Config relating to git worktrees
worktree:
# Default parent directory for new worktrees. It is offered as a candidate
# location alongside the parent directories of any worktrees you already have.
# A relative path is resolved against the repository's root directory, so
# "../worktrees" sits beside the repo and ".worktrees" sits inside it.
# A leading "~" is expanded to your home directory, so "~/worktrees" works.
defaultPath: ""
# Periodic update checks
update:
# One of: 'prompt' (default) | 'background' | 'never'
@@ -517,6 +561,11 @@ refresher:
# Auto-fetch can be disabled via option 'git.autoFetch'.
fetchInterval: 60
# Interval in seconds at which lazygit polls for external ref changes (commits,
# branch updates, checkouts made outside lazygit).
# Detection can be disabled via option 'git.autoDetectExternalChanges'.
externalChangeCheckInterval: 2
# If true, show a confirmation popup before quitting Lazygit
confirmOnQuit: false
@@ -648,6 +697,7 @@ keybinding:
confirmInEditor: [<ctrl+enter>, <ctrl+s>]
remove: d
new: "n"
newWorktree: w
edit: e
openFile: o
scrollUpMain: [<pgup>, K, <ctrl+u>]
@@ -667,6 +717,7 @@ keybinding:
nextScreenMode: +
prevScreenMode: _
cyclePagers: '|'
cyclePagersReverse: \
undo: z
redo: Z
filteringMenu: <ctrl+s>
@@ -676,11 +727,13 @@ keybinding:
submitEditorText: <enter>
extrasMenu: '@'
toggleWhitespaceInDiffView: <ctrl+w>
toggleMouseCapture: '%'
increaseContextInDiffView: '}'
decreaseContextInDiffView: '{'
increaseRenameSimilarityThreshold: )
decreaseRenameSimilarityThreshold: (
openDiffTool: <ctrl+t>
editConfig: <alt+shift+c>
status:
checkForUpdate: u
recentRepos: <enter>
@@ -726,8 +779,6 @@ keybinding:
fetchRemote: f
addForkRemote: F
sortOrder: s
worktrees:
viewWorktreeOptions: w
commits:
squashDown: s
renameCommit: r
@@ -1059,6 +1110,12 @@ keybinding:
edit: <disabled> # disable 'edit file'
```
### Overriding the platform for default keybindings
A few keybindings have different defaults on macOS than on Linux and Windows (e.g. word-wise cursor movement in text inputs uses `alt` on macOS but `ctrl` elsewhere). Lazygit picks these based on the OS it's running on, but you can override that with the `LAZYGIT_KEYBINDING_PLATFORM` environment variable. Set it to `darwin`, `linux`, or `windows`; any other value is ignored and the actual OS is used.
This is useful when running lazygit in a Linux container that you access over ssh from a Mac, where you'd rather use the macOS keybindings.
### Example Keybindings For Colemak Users
```yaml
+18 -29
View File
@@ -2,8 +2,6 @@
Lazygit supports custom pagers, [configured](/docs/Config.md) in the config.yml file (which can be opened by pressing `e` in the Status panel).
Support does not extend to Windows users, because we're making use of a package which doesn't have Windows support. However, see [below](#emulating-custom-pagers-on-windows) for a workaround.
Multiple pagers are supported; you can cycle through them with the `|` key. This can be useful if you usually prefer a particular pager, but want to use a different one for certain kinds of diffs.
Pagers are configured with the `pagers` array in the git section; here's an example for a multi-pager setup (use an empty object `{}` for the default builtin diff display that doesn't use a pager):
@@ -71,7 +69,7 @@ git:
- externalDiffCommand: difft --color=always
```
The `colorArg` and `pager` options are not used in this case.
The `colorArg` option is not used in this case.
You can add whatever extra arguments you prefer for your difftool; for instance
@@ -81,6 +79,22 @@ git:
- externalDiffCommand: difft --color=always --display=inline --syntax-highlight=off
```
This can also be used for normal git diffs with custom parameters, such as `--color-words` or `--word-diff` which some people find useful. To do that, save a script like this to, say, `~/bin/color-words.sh`:
```sh
#!/bin/sh
git diff --color-words --no-index --color=always --no-ext-diff "$2" "$5"
```
And then use it in your git config like so:
```yaml
git:
pagers:
- externalDiffCommand: ~/bin/color-words.sh
```
Instead of setting this command in lazygit's `externalDiffCommand` config, you can also tell lazygit to use the external diff command that is configured in git itself (`diff.external`), by using
```yaml
@@ -91,29 +105,4 @@ git:
This can be useful if you also want to use it for diffs on the command line, and it also has the advantage that you can configure it per file type in `.gitattributes`; see https://git-scm.com/docs/gitattributes#_defining_an_external_diff_driver.
## Emulating custom pagers on Windows
There is a trick to emulate custom pagers on Windows using a Powershell script configured as an external diff command. It's not perfect, but certainly better than nothing. To do this, save the following script as `lazygit-pager.ps1` at a convenient place on your disk:
```pwsh
#!/usr/bin/env pwsh
$old = $args[1].Replace('\', '/')
$new = $args[4].Replace('\', '/')
$path = $args[0]
git diff --no-index --no-ext-diff $old $new
| %{ $_.Replace($old, $path).Replace($new, $path) }
| delta --width=$env:LAZYGIT_COLUMNS
```
Use the pager of your choice with the arguments you like in the last line of the script. Personally I wouldn't want to use lazygit anymore without delta's `--hyperlinks --hyperlinks-file-link-format="lazygit-edit://{path}:{line}"` args, see [above](#delta).
In your lazygit config, use
```yml
git:
pagers:
- externalDiffCommand: "C:/wherever/lazygit-pager.ps1"
```
The main limitation of this approach compared to a "real" pager is that renames are not displayed correctly; they are shown as if they were modifications of the old file. (This affects only the hunk headers; the diff itself is always correct.)
`pager`, `externalDiffCommand`, and `useExternalDiffGitConfig` are alternative ways of producing the diff, so a pager entry may use at most one of them.
+12 -10
View File
@@ -22,7 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` R `` | Refresh | Refresh the git state (i.e. run `git status`, `git branch`, etc in background to update the contents of panels). This does not run `git fetch`. |
| `` + `` | Next screen mode (normal/half/fullscreen) | |
| `` _ `` | Prev screen mode | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` <esc> `` | Cancel | |
| `` ? `` | Open keybindings menu | |
| `` <ctrl+s> `` | View filter options | View options for filtering the commit log, so that only commits matching the filter are shown. |
@@ -30,6 +31,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` q, <ctrl+c> `` | Quit | |
| `` <ctrl+z> `` | Suspend the application | |
| `` <ctrl+w> `` | Toggle whitespace | Toggle whether or not whitespace changes are shown in the diff view.<br><br>The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. |
| `` % `` | Toggle mouse capture | Toggle whether lazygit captures mouse events. When mouse capture is off, the terminal handles the mouse, so you can select text with it; this is useful in terminals that don't have a modifier key to bypass the capture.<br><br>The default can be changed in the config file with the key 'gui.mouseEvents'. |
| `` <alt+shift+c> `` | Edit config file | Open file in external editor. |
| `` z `` | Undo | The reflog will be used to determine what git command to run to undo the last git command. This does not include changes to the working tree; only commits are taken into consideration. |
| `` Z `` | Redo | The reflog will be used to determine what git command to run to redo the last git command. This does not include changes to the working tree; only commits are taken into consideration. |
@@ -110,13 +113,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | Open commit in browser | |
| `` n `` | Create new branch off of commit | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | Copy (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View files | |
| `` w `` | View worktree options | |
| `` / `` | Search the current view by text | |
## Confirmation panel
@@ -176,6 +179,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <space> `` | Checkout | Checkout selected item. |
| `` n `` | New branch | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` o `` | Create pull request | |
| `` O `` | View create pull request options | |
| `` G `` | Open pull request in browser | |
@@ -195,7 +199,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View commits | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Main panel (merging)
@@ -203,7 +206,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <space> `` | Pick hunk | |
| `` b `` | Pick all hunks | |
| `` b `` | Pick both hunks | |
| `` <up>, k `` | Previous hunk | |
| `` <down>, j `` | Next hunk | |
| `` <left>, h `` | Previous conflict | |
@@ -280,6 +283,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | Open commit in browser | |
| `` n `` | Create new branch off of commit | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | Copy (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <ctrl+r> `` | Reset copied (cherry-picked) commits selection | |
@@ -287,7 +291,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View commits | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Remote branches
@@ -297,6 +300,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | Copy branch name to clipboard | |
| `` <space> `` | Checkout | Checkout a new local branch based on the selected remote branch, or the remote branch as a detached head. |
| `` n `` | New branch | |
| `` w `` | New worktree | |
| `` M `` | Merge | View options for merging the selected item into the current branch (regular merge, squash merge) |
| `` r `` | Rebase | Rebase the checked-out branch onto the selected branch. |
| `` d `` | Delete | Delete the remote branch from the remote. |
@@ -306,7 +310,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View commits | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Remotes
@@ -337,17 +340,16 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` g `` | Pop | Apply the stash entry to your working directory and remove the stash entry. |
| `` d `` | Drop | Remove the stash entry from the stash list. |
| `` n `` | New branch | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
| `` w `` | New worktree | |
| `` r `` | Rename stash | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View files | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Status
| Key | Action | Info |
|-----|--------|-------------|
| `` o `` | Open config file | Open file in default application. |
| `` e `` | Edit config file | Open file in external editor. |
| `` u `` | Check for update | |
| `` <enter> `` | Switch to a recent repo | |
@@ -365,6 +367,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | Open commit in browser | |
| `` n `` | Create new branch off of commit | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | Copy (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <ctrl+r> `` | Reset copied (cherry-picked) commits selection | |
@@ -372,7 +375,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View files | |
| `` w `` | View worktree options | |
| `` / `` | Search the current view by text | |
## Submodules
@@ -396,13 +398,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | Copy tag to clipboard | |
| `` <space> `` | Checkout | Checkout the selected tag as a detached HEAD. |
| `` n `` | New tag | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. |
| `` w `` | New worktree | |
| `` d `` | Delete | View delete options for local/remote tag. |
| `` P `` | Push tag | Push the selected tag to a remote. You'll be prompted to select a remote. |
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View commits | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Worktrees
+12 -10
View File
@@ -22,7 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` R `` | 更新 | Gitの状態を更新します(`git status`、`git branch`などをバックグラウンドで実行してパネルの内容を更新します)。これは`git fetch`を実行しません。 |
| `` + `` | 次の画面モード(通常/半分/全画面) | |
| `` _ `` | 前の画面モード | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` <esc> `` | キャンセル | |
| `` ? `` | キーバインディングメニューを開く | |
| `` <ctrl+s> `` | フィルターオプションを表示 | コミットログのフィルタリングオプションを表示し、フィルタに一致するコミットのみを表示します。 |
@@ -30,6 +31,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` q, <ctrl+c> `` | 終了 | |
| `` <ctrl+z> `` | Suspend the application | |
| `` <ctrl+w> `` | 空白表示の切り替え | Toggle whether or not whitespace changes are shown in the diff view.<br><br>The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. |
| `` % `` | Toggle mouse capture | Toggle whether lazygit captures mouse events. When mouse capture is off, the terminal handles the mouse, so you can select text with it; this is useful in terminals that don't have a modifier key to bypass the capture.<br><br>The default can be changed in the config file with the key 'gui.mouseEvents'. |
| `` <alt+shift+c> `` | 設定ファイルを編集 | 外部エディタでファイルを開きます。 |
| `` z `` | 元に戻す | 最後のgitコマンドを元に戻すために実行するgitコマンドを決定するためにreflogが使用されます。これにはワーキングツリーへの変更は含まれません。コミットのみが考慮されます。 |
| `` Z `` | やり直す | 最後のgitコマンドをやり直すために実行するgitコマンドを決定するためにreflogが使用されます。これにはワーキングツリーへの変更は含まれません。コミットのみが考慮されます。 |
@@ -90,13 +93,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | ブラウザでコミットを開く | |
| `` n `` | コミットから新しいブランチを作成 | |
| `` N `` | コミットを新しいブランチに移動 | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | 新しいワークツリー | |
| `` g `` | リセット | 選択した項目へのリセットオプション(ソフト/ミックス/ハード)を表示します。各リセットタイプの詳細は次の通りです:<br>- ソフトリセット:変更を保持し、ステージされた状態にします<br>- ミックスリセット:変更を保持し、ステージされていない状態にします<br>- ハードリセット:すべての変更を破棄します |
| `` C `` | コピー(チェリーピック) | コミットをコピーとしてマークします。ローカルコミットビューで `V` を押すと、コピーしたコミットをチェックアウトしたブランチにペースト(チェリーピック)できます。いつでも `<esc>` を押して選択をキャンセルできます。 |
| `` <ctrl+t> `` | 外部差分ツールを開く(git difftool) | |
| `` * `` | 現在のブランチのコミットを選択 | |
| `` 0 `` | メインビューにフォーカス | |
| `` <enter> `` | ファイルを表示 | |
| `` w `` | ワークツリーオプションを表示 | |
| `` / `` | 現在のビューをテキストで検索 | |
## コミットファイル
@@ -136,6 +139,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | ブラウザでコミットを開く | |
| `` n `` | コミットから新しいブランチを作成 | |
| `` N `` | コミットを新しいブランチに移動 | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | 新しいワークツリー | |
| `` g `` | リセット | 選択した項目へのリセットオプション(ソフト/ミックス/ハード)を表示します。各リセットタイプの詳細は次の通りです:<br>- ソフトリセット:変更を保持し、ステージされた状態にします<br>- ミックスリセット:変更を保持し、ステージされていない状態にします<br>- ハードリセット:すべての変更を破棄します |
| `` C `` | コピー(チェリーピック) | コミットをコピーとしてマークします。ローカルコミットビューで `V` を押すと、コピーしたコミットをチェックアウトしたブランチにペースト(チェリーピック)できます。いつでも `<esc>` を押して選択をキャンセルできます。 |
| `` <ctrl+r> `` | コピーされた(チェリーピックされた)コミットの選択をリセット | |
@@ -143,7 +147,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | 現在のブランチのコミットを選択 | |
| `` 0 `` | メインビューにフォーカス | |
| `` <enter> `` | ファイルを表示 | |
| `` w `` | ワークツリーオプションを表示 | |
| `` / `` | 現在のビューをテキストで検索 | |
## サブモジュール
@@ -168,17 +171,16 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` g `` | ポップ | スタッシュエントリをワーキングディレクトリに適用し、スタッシュエントリを削除します。 |
| `` d `` | 削除 | スタッシュリストからスタッシュエントリを削除します。 |
| `` n `` | 新しいブランチ | 選択したスタッシュエントリから新しいブランチを作成します。これは、スタッシュエントリが作成されたコミットをgitがチェックアウトし、そのコミットから新しいブランチを作成した後、スタッシュエントリを追加のコミットとして新しいブランチに適用することで機能します。 |
| `` w `` | 新しいワークツリー | |
| `` r `` | スタッシュの名前を変更 | |
| `` 0 `` | メインビューにフォーカス | |
| `` <enter> `` | ファイルを表示 | |
| `` w `` | ワークツリーオプションを表示 | |
| `` / `` | 現在のビューをテキストでフィルタリング | |
## ステータス
| Key | Action | Info |
|-----|--------|-------------|
| `` o `` | 設定ファイルを開く | デフォルトのアプリケーションでファイルを開きます。 |
| `` e `` | 設定ファイルを編集 | 外部エディタでファイルを開きます。 |
| `` u `` | 更新を確認 | |
| `` <enter> `` | 最近のリポジトリをチェックアウト | |
@@ -201,13 +203,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | タグをクリップボードにコピー | |
| `` <space> `` | チェックアウト(ブランチの切り替え) | 選択したタグをデタッチドHEADとしてチェックアウトします。 |
| `` n `` | 新しいタグを作成 | 現在のコミットから新しいタグを作成します。タグ名とオプションの説明を入力するよう促されます。 |
| `` w `` | 新しいワークツリー | |
| `` d `` | 削除 | ローカル/リモートタグの削除オプションを表示します。 |
| `` P `` | タグをプッシュ | 選択したタグをリモートにプッシュします。リモートを選択するよう促されます。 |
| `` g `` | リセット | 選択した項目へのリセットオプション(ソフト/ミックス/ハード)を表示します。各リセットタイプの詳細は次の通りです:<br>- ソフトリセット:変更を保持し、ステージされた状態にします<br>- ミックスリセット:変更を保持し、ステージされていない状態にします<br>- ハードリセット:すべての変更を破棄します |
| `` <ctrl+t> `` | 外部差分ツールを開く(git difftool) | |
| `` 0 `` | メインビューにフォーカス | |
| `` <enter> `` | コミットを表示 | |
| `` w `` | ワークツリーオプションを表示 | |
| `` / `` | 現在のビューをテキストでフィルタリング | |
## ファイル
@@ -286,7 +288,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <space> `` | ハンクを選択 | |
| `` b `` | すべてのハンクを選択 | |
| `` b `` | Pick both hunks | |
| `` <up>, k `` | 前のハンク | |
| `` <down>, j `` | 次のハンク | |
| `` <left>, h `` | 前のコンフリクト | |
@@ -325,6 +327,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | ブラウザでコミットを開く | |
| `` n `` | コミットから新しいブランチを作成 | |
| `` N `` | コミットを新しいブランチに移動 | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | 新しいワークツリー | |
| `` g `` | リセット | 選択した項目へのリセットオプション(ソフト/ミックス/ハード)を表示します。各リセットタイプの詳細は次の通りです:<br>- ソフトリセット:変更を保持し、ステージされた状態にします<br>- ミックスリセット:変更を保持し、ステージされていない状態にします<br>- ハードリセット:すべての変更を破棄します |
| `` C `` | コピー(チェリーピック) | コミットをコピーとしてマークします。ローカルコミットビューで `V` を押すと、コピーしたコミットをチェックアウトしたブランチにペースト(チェリーピック)できます。いつでも `<esc>` を押して選択をキャンセルできます。 |
| `` <ctrl+r> `` | コピーされた(チェリーピックされた)コミットの選択をリセット | |
@@ -332,7 +335,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | 現在のブランチのコミットを選択 | |
| `` 0 `` | メインビューにフォーカス | |
| `` <enter> `` | コミットを表示 | |
| `` w `` | ワークツリーオプションを表示 | |
| `` / `` | 現在のビューをテキストでフィルタリング | |
## リモート
@@ -354,6 +356,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | ブランチ名をクリップボードにコピー | |
| `` <space> `` | チェックアウト(ブランチの切り替え) | 選択したリモートブランチに基づいて新しいローカルブランチをチェックアウトするか、リモートブランチをデタッチドヘッドとしてチェックアウトします。 |
| `` n `` | 新しいブランチ | |
| `` w `` | 新しいワークツリー | |
| `` M `` | マージ | 選択した項目を現在のブランチにマージするためのオプションを表示します(通常のマージ、スカッシュマージ) |
| `` r `` | リベース | チェックアウトしたブランチを選択したブランチ上にリベースします。 |
| `` d `` | 削除 | リモートからリモートブランチを削除します。 |
@@ -363,7 +366,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | 外部差分ツールを開く(git difftool) | |
| `` 0 `` | メインビューにフォーカス | |
| `` <enter> `` | コミットを表示 | |
| `` w `` | ワークツリーオプションを表示 | |
| `` / `` | 現在のビューをテキストでフィルタリング | |
## ローカルブランチ
@@ -375,6 +377,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <space> `` | チェックアウト(ブランチの切り替え) | 選択した項目をチェックアウトします。 |
| `` n `` | 新しいブランチ | |
| `` N `` | コミットを新しいブランチに移動 | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | 新しいワークツリー | |
| `` o `` | プルリクエストを作成 | |
| `` O `` | プルリクエスト作成オプションを表示 | |
| `` G `` | Open pull request in browser | |
@@ -394,7 +397,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | 外部差分ツールを開く(git difftool) | |
| `` 0 `` | メインビューにフォーカス | |
| `` <enter> `` | コミットを表示 | |
| `` w `` | ワークツリーオプションを表示 | |
| `` / `` | 現在のビューをテキストでフィルタリング | |
## ワークツリー
+12 -10
View File
@@ -22,7 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` R `` | 새로고침 | Refresh the git state (i.e. run `git status`, `git branch`, etc in background to update the contents of panels). This does not run `git fetch`. |
| `` + `` | 다음 스크린 모드 (normal/half/fullscreen) | |
| `` _ `` | 이전 스크린 모드 | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` <esc> `` | 취소 | |
| `` ? `` | 매뉴 열기 | |
| `` <ctrl+s> `` | View filter-by-path options | View options for filtering the commit log, so that only commits matching the filter are shown. |
@@ -30,6 +31,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` q, <ctrl+c> `` | 종료 | |
| `` <ctrl+z> `` | Suspend the application | |
| `` <ctrl+w> `` | 공백문자를 Diff 뷰에서 표시 여부 전환 | Toggle whether or not whitespace changes are shown in the diff view.<br><br>The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. |
| `` % `` | Toggle mouse capture | Toggle whether lazygit captures mouse events. When mouse capture is off, the terminal handles the mouse, so you can select text with it; this is useful in terminals that don't have a modifier key to bypass the capture.<br><br>The default can be changed in the config file with the key 'gui.mouseEvents'. |
| `` <alt+shift+c> `` | 설정 파일 수정 | Open file in external editor. |
| `` z `` | 되돌리기 (reflog) (실험적) | The reflog will be used to determine what git command to run to undo the last git command. This does not include changes to the working tree; only commits are taken into consideration. |
| `` Z `` | 다시 실행 (reflog) (실험적) | The reflog will be used to determine what git command to run to redo the last git command. This does not include changes to the working tree; only commits are taken into consideration. |
@@ -67,6 +70,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | 브라우저에서 커밋 열기 | |
| `` n `` | 커밋에서 새 브랜치를 만듭니다. | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` g `` | View reset options | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | 커밋을 복사 (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <ctrl+r> `` | Reset cherry-picked (copied) commits selection | |
@@ -74,7 +78,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 커밋 보기 | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Secondary
@@ -93,10 +96,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` g `` | Pop | Apply the stash entry to your working directory and remove the stash entry. |
| `` d `` | Drop | Remove the stash entry from the stash list. |
| `` n `` | 새 브랜치 생성 | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
| `` w `` | New worktree | |
| `` r `` | Rename stash | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View selected item's files | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Sub-commits
@@ -109,6 +112,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | 브라우저에서 커밋 열기 | |
| `` n `` | 커밋에서 새 브랜치를 만듭니다. | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` g `` | View reset options | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | 커밋을 복사 (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <ctrl+r> `` | Reset cherry-picked (copied) commits selection | |
@@ -116,7 +120,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View selected item's files | |
| `` w `` | View worktree options | |
| `` / `` | 검색 시작 | |
## Worktrees
@@ -142,7 +145,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <space> `` | Pick hunk | |
| `` b `` | Pick all hunks | |
| `` b `` | Pick both hunks | |
| `` <up>, k `` | 이전 hunk를 선택 | |
| `` <down>, j `` | 다음 hunk를 선택 | |
| `` <left>, h `` | 이전 충돌을 선택 | |
@@ -210,6 +213,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <space> `` | 체크아웃 | Checkout selected item. |
| `` n `` | 새 브랜치 생성 | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` o `` | 풀 리퀘스트 생성 | |
| `` O `` | 풀 리퀘스트 생성 옵션 | |
| `` G `` | Open pull request in browser | |
@@ -229,14 +233,12 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 커밋 보기 | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## 상태
| Key | Action | Info |
|-----|--------|-------------|
| `` o `` | 설정 파일 열기 | Open file in default application. |
| `` e `` | 설정 파일 수정 | Open file in external editor. |
| `` u `` | 업데이트 확인 | |
| `` <enter> `` | 최근에 사용한 저장소로 전환 | |
@@ -277,6 +279,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | 브랜치명을 클립보드에 복사 | |
| `` <space> `` | 체크아웃 | Checkout a new local branch based on the selected remote branch, or the remote branch as a detached head. |
| `` n `` | 새 브랜치 생성 | |
| `` w `` | New worktree | |
| `` M `` | 현재 브랜치에 병합 | View options for merging the selected item into the current branch (regular merge, squash merge) |
| `` r `` | 체크아웃된 브랜치를 이 브랜치에 리베이스 | Rebase the checked-out branch onto the selected branch. |
| `` d `` | 삭제 | Delete the remote branch from the remote. |
@@ -286,7 +289,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 커밋 보기 | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## 커밋
@@ -322,13 +324,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | 브라우저에서 커밋 열기 | |
| `` n `` | 커밋에서 새 브랜치를 만듭니다. | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` g `` | View reset options | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | 커밋을 복사 (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View selected item's files | |
| `` w `` | View worktree options | |
| `` / `` | 검색 시작 | |
## 커밋 파일
@@ -365,13 +367,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | Copy tag to clipboard | |
| `` <space> `` | 체크아웃 | Checkout the selected tag as a detached HEAD. |
| `` n `` | 태그를 생성 | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. |
| `` w `` | New worktree | |
| `` d `` | 삭제 | View delete options for local/remote tag. |
| `` P `` | 태그를 push | Push the selected tag to a remote. You'll be prompted to select a remote. |
| `` g `` | 초기화 | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 커밋 보기 | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## 파일
+72 -70
View File
@@ -10,26 +10,29 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <pgup>, K, <ctrl+u> (fn+up/shift+k) `` | Scroll naar beneden vanaf hoofdpaneel | |
| `` <pgdown>, J, <ctrl+d> (fn+down/shift+j) `` | Scroll naar beneden vanaf hoofdpaneel | |
| `` @ `` | View command log options | View options for the command log e.g. show/hide the command log and focus the command log. |
| `` P `` | Push | Push the current branch to its upstream branch. If no upstream is configured, you will be prompted to configure an upstream branch. |
| `` p `` | Pull | Pull changes from the remote for the current branch. If no upstream is configured, you will be prompted to configure an upstream branch. |
| `` P `` | Push | Push de huidige branch naar de bijbehorende upstream-branch. Als er geen upstream is geconfigureerd wordt er gevraagd om een upstream-branch te configureren. |
| `` p `` | Pull | Pull wijzigingen van de remote voor de huidige branch. Als er geen upstream is geconfigureerd wordt er gevraagd om een upstream-branch te configureren. |
| `` ) `` | Increase rename similarity threshold | Increase the similarity threshold for a deletion and addition pair to be treated as a rename.<br><br>The default can be changed in the config file with the key 'git.renameSimilarityThreshold'. |
| `` ( `` | Decrease rename similarity threshold | Decrease the similarity threshold for a deletion and addition pair to be treated as a rename.<br><br>The default can be changed in the config file with the key 'git.renameSimilarityThreshold'. |
| `` } `` | Increase diff context size | Increase the amount of the context shown around changes in the diff view.<br><br>The default can be changed in the config file with the key 'git.diffContextSize'. |
| `` { `` | Decrease diff context size | Decrease the amount of the context shown around changes in the diff view.<br><br>The default can be changed in the config file with the key 'git.diffContextSize'. |
| `` : `` | Execute shell command | Bring up a prompt where you can enter a shell command to execute. |
| `` <ctrl+p> `` | Bekijk aangepaste patch opties | |
| `` m `` | Bekijk merge/rebase opties | View options to abort/continue/skip the current merge/rebase. |
| `` m `` | Bekijk merge/rebase opties | Toon abort/continue/skip opties voor huidige merge/rebase. |
| `` R `` | Verversen | Refresh the git state (i.e. run `git status`, `git branch`, etc in background to update the contents of panels). This does not run `git fetch`. |
| `` + `` | Volgende scherm modus (normaal/half/groot) | |
| `` _ `` | Vorige scherm modus | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` <esc> `` | Annuleren | |
| `` ? `` | Open menu | |
| `` <ctrl+s> `` | Bekijk scoping opties | View options for filtering the commit log, so that only commits matching the filter are shown. |
| `` W, <ctrl+e> `` | Open diff menu | View options relating to diffing two refs e.g. diffing against selected ref, entering ref to diff against, and reversing the diff direction. |
| `` q, <ctrl+c> `` | Quit | |
| `` <ctrl+z> `` | Suspend the application | |
| `` q, <ctrl+c> `` | Afsluiten | |
| `` <ctrl+z> `` | Pauzeer de applicatie | |
| `` <ctrl+w> `` | Toggle whitespace | Toggle whether or not whitespace changes are shown in the diff view.<br><br>The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. |
| `` % `` | Toggle mouse capture | Toggle whether lazygit captures mouse events. When mouse capture is off, the terminal handles the mouse, so you can select text with it; this is useful in terminals that don't have a modifier key to bypass the capture.<br><br>The default can be changed in the config file with the key 'gui.mouseEvents'. |
| `` <alt+shift+c> `` | Verander config bestand | Open bestand in externe editor. |
| `` z `` | Ongedaan maken (via reflog) (experimenteel) | The reflog will be used to determine what git command to run to undo the last git command. This does not include changes to the working tree; only commits are taken into consideration. |
| `` Z `` | Redo (via reflog) (experimenteel) | The reflog will be used to determine what git command to run to redo the last git command. This does not include changes to the working tree; only commits are taken into consideration. |
@@ -45,8 +48,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <shift+down> `` | Range select down | |
| `` <shift+up> `` | Range select up | |
| `` / `` | Start met zoeken | |
| `` H `` | Scroll left | |
| `` L `` | Scroll right | |
| `` H `` | Scroll naar links | |
| `` L `` | Scroll naar rechts | |
| `` ] `` | Volgende tabblad | |
| `` [ `` | Vorige tabblad | |
@@ -56,15 +59,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|-----|--------|-------------|
| `` <ctrl+o> `` | Kopieer de bestandsnaam naar het klembord | |
| `` <space> `` | Toggle staged | Toggle staged for selected file. |
| `` <ctrl+b> `` | Filter files by status | |
| `` y `` | Copy to clipboard | |
| `` c `` | Commit veranderingen | Commit staged changes. |
| `` <ctrl+b> `` | Filter bestanden op status | |
| `` y `` | Kopieer naar klembord | |
| `` c `` | Commit veranderingen | Commit gestagede wijzigingen. |
| `` w `` | Commit veranderingen zonder pre-commit hook | |
| `` A `` | Wijzig laatste commit | |
| `` C `` | Commit veranderingen met de git editor | |
| `` <ctrl+f> `` | Find base commit for fixup | Find the commit that your current changes are building upon, for the sake of amending/fixing up the commit. This spares you from having to look through your branch's commits one-by-one to see which commit should be amended/fixed up. See docs: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
| `` e `` | Edit | Open file in external editor. |
| `` o `` | Open bestand | Open file in default application. |
| `` <ctrl+f> `` | Find base commit for fixup | Vind de commit waar je huidige wijzigingen bovenop zijn gebouwd met als doel die commit te amenden/fixen. Hierdoor hoef je dit niet met de hand te doen. Zie: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
| `` e `` | Edit | Open bestand in externe editor. |
| `` o `` | Open bestand | Open bestand in standaardapplicatie. |
| `` i `` | Ignore or exclude file | |
| `` r `` | Refresh bestanden | |
| `` s `` | Stash | Stash all changes. For other variations of stashing, use the view stash options keybinding. |
@@ -73,13 +76,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <enter> `` | Stage individuele hunks/lijnen | If the selected item is a file, focus the staging view so you can stage individual hunks/lines. If the selected item is a directory, collapse/expand it. |
| `` d `` | Bekijk 'veranderingen ongedaan maken' opties | View options for discarding changes to the selected file. |
| `` g `` | Bekijk upstream reset opties | |
| `` D `` | Reset | View reset options for working tree (e.g. nuking the working tree). |
| `` D `` | Resetten | View reset options for working tree (e.g. nuking the working tree). |
| `` ` `` | Toggle bestandsboom weergave | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.<br><br>The default can be changed in the config file with the key 'gui.showFileTree'. |
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
| `` M `` | Bekijk merge conflict opties | Bekijk opties voor het oplossen van mergeconflicten. |
| `` f `` | Fetch | Fetch changes from remote. |
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` = `` | Vouw alle bestanden uit | Vouw alle mappen in de bestandsstructuur uit |
| `` 0 `` | Focus main view | |
| `` / `` | Filter the current view by text | |
@@ -89,7 +92,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|-----|--------|-------------|
| `` <enter> `` | Bevestig | |
| `` <esc> `` | Sluiten | |
| `` <ctrl+o> `` | Copy to clipboard | |
| `` <ctrl+o> `` | Kopieer naar klembord | |
## Branches
@@ -97,18 +100,19 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|-----|--------|-------------|
| `` <ctrl+o> `` | Kopieer branch name naar klembord | |
| `` i `` | Laat git-flow opties zien | |
| `` <space> `` | Uitchecken | Checkout selected item. |
| `` <space> `` | Uitchecken | Geselecteerd item uitchecken. |
| `` n `` | Nieuwe branch | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` N `` | Verplaats commits naar nieuwe branch | Maak een nieuwe branch en verplaats niet-gepushte commits van de huidige branch hier naar toe. Gebruik dit in het geval dat je deze commits eigenlijk op een nieuwe branch had willen maken.<br><br>Let op dat de selectie genegeerd wordt. De nieuwe branch komt ofwel bovenop de main branch, of bovenop de huidige branch (je kan kiezen). |
| `` w `` | New worktree | |
| `` o `` | Maak een pull-request | |
| `` O `` | Bekijk opties voor pull-aanvraag | |
| `` G `` | Open pull request in browser | |
| `` <ctrl+y> `` | Kopieer de URL van het pull-verzoek naar het klembord | |
| `` c `` | Uitchecken bij naam | Checkout by name. In the input box you can enter '-' to switch to the previous branch. |
| `` - `` | Checkout previous branch | |
| `` - `` | Vorige branch uitchecken | |
| `` F `` | Forceer checkout | Force checkout selected branch. This will discard all local changes in your working directory before checking out the selected branch. |
| `` d `` | Delete | View delete options for local/remote branch. |
| `` r `` | Rebase branch | Rebase the checked-out branch onto the selected branch. |
| `` d `` | Verwijderen | View delete options for local/remote branch. |
| `` r `` | Rebase branch | Rebase de uitgecheckte branch bovenop de geselecteerde branch. |
| `` M `` | Merge in met huidige checked out branch | View options for merging the selected item into the current branch (regular merge, squash merge) |
| `` f `` | Fast-forward deze branch vanaf zijn upstream | Fast-forward selected branch from its upstream. |
| `` T `` | Creëer tag | |
@@ -116,10 +120,9 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` g `` | Bekijk reset opties | |
| `` R `` | Hernoem branch | |
| `` u `` | View upstream options | View options relating to the branch's upstream e.g. setting/unsetting the upstream and resetting to the upstream. |
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Bekijk commits | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Commit bericht
@@ -134,18 +137,18 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <ctrl+o> `` | Kopieer de bestandsnaam naar het klembord | |
| `` y `` | Copy to clipboard | |
| `` y `` | Kopieer naar klembord | |
| `` c `` | Uitchecken | Bestand uitchecken |
| `` d `` | Bekijk 'veranderingen ongedaan maken' opties | Uitsluit deze commit zijn veranderingen aan dit bestand |
| `` o `` | Open bestand | Open file in default application. |
| `` e `` | Edit | Open file in external editor. |
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` o `` | Open bestand | Open bestand in standaardapplicatie. |
| `` e `` | Edit | Open bestand in externe editor. |
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
| `` <space> `` | Toggle bestand inbegrepen in patch | Toggle whether the file is included in the custom patch. See https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
| `` a `` | Toggle all files | Add/remove all commit's files to custom patch. See https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
| `` <enter> `` | Enter bestand om geselecteerde regels toe te voegen aan de patch | If a file is selected, enter the file so that you can add/remove individual lines to the custom patch. If a directory is selected, toggle the directory. |
| `` ` `` | Toggle bestandsboom weergave | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.<br><br>The default can be changed in the config file with the key 'gui.showFileTree'. |
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` = `` | Vouw alle bestanden uit | Vouw alle mappen in de bestandsstructuur uit |
| `` 0 `` | Focus main view | |
| `` / `` | Filter the current view by text | |
@@ -181,14 +184,14 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Open commit in browser | |
| `` n `` | Creëer nieuwe branch van commit | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` N `` | Verplaats commits naar nieuwe branch | Maak een nieuwe branch en verplaats niet-gepushte commits van de huidige branch hier naar toe. Gebruik dit in het geval dat je deze commits eigenlijk op een nieuwe branch had willen maken.<br><br>Let op dat de selectie genegeerd wordt. De nieuwe branch komt ofwel bovenop de main branch, of bovenop de huidige branch (je kan kiezen). |
| `` w `` | New worktree | |
| `` g `` | Bekijk reset opties | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | Kopieer commit (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Bekijk gecommite bestanden | |
| `` w `` | View worktree options | |
| `` / `` | Start met zoeken | |
## Input prompt
@@ -211,15 +214,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <space> `` | Kies stuk | |
| `` b `` | Kies beide stukken | |
| `` b `` | Pick both hunks | |
| `` <up>, k `` | Selecteer bovenste hunk | |
| `` <down>, j `` | Selecteer onderste hunk | |
| `` <left>, h `` | Selecteer voorgaand conflict | |
| `` <right>, l `` | Selecteer volgende conflict | |
| `` z `` | Ongedaan maken | Undo last merge conflict resolution. |
| `` e `` | Verander bestand | Open file in external editor. |
| `` o `` | Open bestand | Open file in default application. |
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
| `` e `` | Verander bestand | Open bestand in externe editor. |
| `` o `` | Open bestand | Open bestand in standaardapplicatie. |
| `` M `` | Bekijk merge conflict opties | Bekijk opties voor het oplossen van mergeconflicten. |
| `` <esc> `` | Ga terug naar het bestanden paneel | |
## Normaal
@@ -241,8 +244,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` v `` | Toggle drag selecteer | |
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
| `` <ctrl+o> `` | Copy selected text to clipboard | |
| `` o `` | Open bestand | Open file in default application. |
| `` e `` | Verander bestand | Open file in external editor. |
| `` o `` | Open bestand | Open bestand in standaardapplicatie. |
| `` e `` | Verander bestand | Open bestand in externe editor. |
| `` <space> `` | Voeg toe/verwijder lijn(en) in patch | |
| `` d `` | Remove lines from commit | Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines. |
| `` <esc> `` | Sluit lijn-bij-lijn modus | |
@@ -257,15 +260,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Open commit in browser | |
| `` n `` | Creëer nieuwe branch van commit | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` N `` | Verplaats commits naar nieuwe branch | Maak een nieuwe branch en verplaats niet-gepushte commits van de huidige branch hier naar toe. Gebruik dit in het geval dat je deze commits eigenlijk op een nieuwe branch had willen maken.<br><br>Let op dat de selectie genegeerd wordt. De nieuwe branch komt ofwel bovenop de main branch, of bovenop de huidige branch (je kan kiezen). |
| `` w `` | New worktree | |
| `` g `` | Bekijk reset opties | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | Kopieer commit (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <ctrl+r> `` | Reset cherry-picked (gekopieerde) commits selectie | |
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Bekijk commits | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Remote branches
@@ -273,27 +276,27 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <ctrl+o> `` | Kopieer branch name naar klembord | |
| `` <space> `` | Uitchecken | Checkout a new local branch based on the selected remote branch, or the remote branch as a detached head. |
| `` <space> `` | Uitchecken | Geselecteerde remote branch uitchecken als nieuwe locale branch of als detached head. |
| `` n `` | Nieuwe branch | |
| `` w `` | New worktree | |
| `` M `` | Merge in met huidige checked out branch | View options for merging the selected item into the current branch (regular merge, squash merge) |
| `` r `` | Rebase branch | Rebase the checked-out branch onto the selected branch. |
| `` d `` | Delete | Delete the remote branch from the remote. |
| `` u `` | Set as upstream | Stel in als upstream van uitgecheckte branch |
| `` r `` | Rebase branch | Rebase de uitgecheckte branch bovenop de geselecteerde branch. |
| `` d `` | Verwijderen | Delete the remote branch from the remote. |
| `` u `` | Instellen als upstream | Stel in als upstream van uitgecheckte branch |
| `` s `` | Sort order | |
| `` g `` | Bekijk reset opties | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Bekijk commits | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Remotes
| Key | Action | Info |
|-----|--------|-------------|
| `` <enter> `` | View branches | |
| `` <enter> `` | Bekijk branches | |
| `` n `` | Voeg een nieuwe remote toe | |
| `` d `` | Remove | Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected. |
| `` d `` | Verwijderen | Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected. |
| `` e `` | Edit | Wijzig remote |
| `` f `` | Fetch | Fetch remote |
| `` F `` | Add fork remote | Quickly add a fork remote by replacing the owner in the origin URL and optionally check out a branch from new remote. |
@@ -318,15 +321,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | Copy selected text to clipboard | |
| `` <space> `` | Toggle staged | Toggle lijnen staged / unstaged |
| `` d `` | Verwijdert change (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
| `` o `` | Open bestand | Open file in default application. |
| `` e `` | Verander bestand | Open file in external editor. |
| `` o `` | Open bestand | Open bestand in standaardapplicatie. |
| `` e `` | Verander bestand | Open bestand in externe editor. |
| `` <esc> `` | Ga terug naar het bestanden paneel | |
| `` <tab> `` | Ga naar een ander paneel | Switch to other view (staged/unstaged changes). |
| `` E `` | Edit hunk | Edit selected hunk in external editor. |
| `` c `` | Commit veranderingen | Commit staged changes. |
| `` c `` | Commit veranderingen | Commit gestagede wijzigingen. |
| `` w `` | Commit veranderingen zonder pre-commit hook | |
| `` C `` | Commit veranderingen met de git editor | |
| `` <ctrl+f> `` | Find base commit for fixup | Find the commit that your current changes are building upon, for the sake of amending/fixing up the commit. This spares you from having to look through your branch's commits one-by-one to see which commit should be amended/fixed up. See docs: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
| `` <ctrl+f> `` | Find base commit for fixup | Vind de commit waar je huidige wijzigingen bovenop zijn gebouwd met als doel die commit te amenden/fixen. Hierdoor hoef je dit niet met de hand te doen. Zie: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
| `` / `` | Start met zoeken | |
## Stash
@@ -337,18 +340,17 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` g `` | Pop | Apply the stash entry to your working directory and remove the stash entry. |
| `` d `` | Laten vallen | Remove the stash entry from the stash list. |
| `` n `` | Nieuwe branch | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
| `` r `` | Rename stash | |
| `` w `` | New worktree | |
| `` r `` | Hernoem stash | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Bekijk gecommite bestanden | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Status
| Key | Action | Info |
|-----|--------|-------------|
| `` o `` | Open config bestand | Open file in default application. |
| `` e `` | Verander config bestand | Open file in external editor. |
| `` e `` | Verander config bestand | Open bestand in externe editor. |
| `` u `` | Check voor updates | |
| `` <enter> `` | Wissel naar een recente repo | |
| `` a `` | Show/cycle all branch logs | |
@@ -364,15 +366,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Open commit in browser | |
| `` n `` | Creëer nieuwe branch van commit | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` N `` | Verplaats commits naar nieuwe branch | Maak een nieuwe branch en verplaats niet-gepushte commits van de huidige branch hier naar toe. Gebruik dit in het geval dat je deze commits eigenlijk op een nieuwe branch had willen maken.<br><br>Let op dat de selectie genegeerd wordt. De nieuwe branch komt ofwel bovenop de main branch, of bovenop de huidige branch (je kan kiezen). |
| `` w `` | New worktree | |
| `` g `` | Bekijk reset opties | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | Kopieer commit (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <ctrl+r> `` | Reset cherry-picked (gekopieerde) commits selectie | |
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Bekijk gecommite bestanden | |
| `` w `` | View worktree options | |
| `` / `` | Start met zoeken | |
## Submodules
@@ -381,7 +383,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|-----|--------|-------------|
| `` <ctrl+o> `` | Kopieer submodule naam naar klembord | |
| `` <enter> `` | Enter | Enter submodule |
| `` d `` | Remove | Remove the selected submodule and its corresponding directory. |
| `` d `` | Verwijderen | Remove the selected submodule and its corresponding directory. |
| `` u `` | Update | Update selected submodule. |
| `` n `` | Voeg nieuwe submodule toe | |
| `` e `` | Update submodule URL | |
@@ -394,15 +396,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <ctrl+o> `` | Copy tag to clipboard | |
| `` <space> `` | Uitchecken | Checkout the selected tag as a detached HEAD. |
| `` <space> `` | Uitchecken | Geselecteerde tag uitchecken als detached HEAD. |
| `` n `` | Creëer tag | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. |
| `` d `` | Delete | View delete options for local/remote tag. |
| `` w `` | New worktree | |
| `` d `` | Verwijderen | View delete options for local/remote tag. |
| `` P `` | Push tag | Push the selected tag to a remote. You'll be prompted to select a remote. |
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` g `` | Resetten | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Bekijk commits | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Worktrees
@@ -411,6 +413,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|-----|--------|-------------|
| `` n `` | New worktree | |
| `` <space> `` | Switch | Switch to the selected worktree. |
| `` o `` | Open in editor | |
| `` d `` | Remove | Remove the selected worktree. This will both delete the worktree's directory, as well as metadata about the worktree in the .git directory. |
| `` o `` | Openen in editor | |
| `` d `` | Verwijderen | Remove the selected worktree. This will both delete the worktree's directory, as well as metadata about the worktree in the .git directory. |
| `` / `` | Filter the current view by text | |
+12 -10
View File
@@ -22,7 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` R `` | Odśwież | Odśwież stan git (tj. uruchom `git status`, `git branch`, itp. w tle, aby zaktualizować zawartość paneli). To nie uruchamia `git fetch`. |
| `` + `` | Następny tryb ekranu (normalny/półpełny/pełnoekranowy) | |
| `` _ `` | Poprzedni tryb ekranu | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` <esc> `` | Anuluj | |
| `` ? `` | Otwórz menu przypisań klawiszy | |
| `` <ctrl+s> `` | Pokaż opcje filtrowania | Pokaż opcje filtrowania dziennika commitów, tak aby pokazywane były tylko commity pasujące do filtra. |
@@ -30,6 +31,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` q, <ctrl+c> `` | Wyjdź | |
| `` <ctrl+z> `` | Suspend the application | |
| `` <ctrl+w> `` | Przełącz białe znaki | Toggle whether or not whitespace changes are shown in the diff view.<br><br>The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. |
| `` % `` | Toggle mouse capture | Toggle whether lazygit captures mouse events. When mouse capture is off, the terminal handles the mouse, so you can select text with it; this is useful in terminals that don't have a modifier key to bypass the capture.<br><br>The default can be changed in the config file with the key 'gui.mouseEvents'. |
| `` <alt+shift+c> `` | Edytuj plik konfiguracyjny | Otwórz plik w zewnętrznym edytorze. |
| `` z `` | Cofnij | Dziennik reflog zostanie użyty do określenia, jakie polecenie git należy uruchomić, aby cofnąć ostatnie polecenie git. Nie obejmuje to zmian w drzewie roboczym; brane są pod uwagę tylko commity. |
| `` Z `` | Ponów | Dziennik reflog zostanie użyty do określenia, jakie polecenie git należy uruchomić, aby ponowić ostatnie polecenie git. Nie obejmuje to zmian w drzewie roboczym; brane są pod uwagę tylko commity. |
@@ -83,13 +86,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | Otwórz commit w przeglądarce | |
| `` n `` | Utwórz nową gałąź z commita | |
| `` N `` | Przenieś commity do nowej gałęzi | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | Nowe drzewo pracy | |
| `` g `` | Reset | Wyświetl opcje resetu (miękki/mieszany/twardy) do wybranego elementu. |
| `` C `` | Kopiuj (cherry-pick) | Oznacz commit jako skopiowany. Następnie, w widoku lokalnych commitów, możesz nacisnąć `V`, aby wkleić (cherry-pick) skopiowane commity do sprawdzonej gałęzi. W dowolnym momencie możesz nacisnąć `<esc>`, aby anulować zaznaczenie. |
| `` <ctrl+t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Wyświetl pliki | |
| `` w `` | Zobacz opcje drzewa pracy | |
| `` / `` | Szukaj w bieżącym widoku po tekście | |
## Dodatkowy
@@ -120,6 +123,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | Otwórz commit w przeglądarce | |
| `` n `` | Utwórz nową gałąź z commita | |
| `` N `` | Przenieś commity do nowej gałęzi | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | Nowe drzewo pracy | |
| `` g `` | Reset | Wyświetl opcje resetu (miękki/mieszany/twardy) do wybranego elementu. |
| `` C `` | Kopiuj (cherry-pick) | Oznacz commit jako skopiowany. Następnie, w widoku lokalnych commitów, możesz nacisnąć `V`, aby wkleić (cherry-pick) skopiowane commity do sprawdzonej gałęzi. W dowolnym momencie możesz nacisnąć `<esc>`, aby anulować zaznaczenie. |
| `` <ctrl+r> `` | Resetuj wybrane (cherry-picked) commity | |
@@ -127,7 +131,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Pokaż commity | |
| `` w `` | Zobacz opcje drzewa pracy | |
| `` / `` | Filtruj bieżący widok po tekście | |
## Główny panel (budowanie łatki)
@@ -162,6 +165,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <space> `` | Przełącz | Przełącz wybrany element. |
| `` n `` | Nowa gałąź | |
| `` N `` | Przenieś commity do nowej gałęzi | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | Nowe drzewo pracy | |
| `` o `` | Utwórz żądanie ściągnięcia | |
| `` O `` | Zobacz opcje tworzenia pull requesta | |
| `` G `` | Otwórz żądanie ściągnięcia w przeglądarce | |
@@ -181,7 +185,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Pokaż commity | |
| `` w `` | Zobacz opcje drzewa pracy | |
| `` / `` | Filtruj bieżący widok po tekście | |
## Menu
@@ -207,7 +210,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <space> `` | Wybierz fragment | |
| `` b `` | Wybierz wszystkie fragmenty | |
| `` b `` | Pick both hunks | |
| `` <up>, k `` | Poprzedni fragment | |
| `` <down>, j `` | Następny fragment | |
| `` <left>, h `` | Poprzedni konflikt | |
@@ -316,17 +319,16 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` g `` | Wyciągnij | Zastosuj wpis schowka do katalogu roboczego i usuń wpis schowka. |
| `` d `` | Usuń | Usuń wpis schowka z listy schowka. |
| `` n `` | Nowa gałąź | Utwórz nową gałąź z wybranego wpisu schowka. Działa poprzez przełączenie git na commit, na którym wpis schowka został utworzony, tworzenie nowej gałęzi z tego commita, a następnie zastosowanie wpisu schowka do nowej gałęzi jako dodatkowego commita. |
| `` w `` | Nowe drzewo pracy | |
| `` r `` | Zmień nazwę schowka | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Wyświetl pliki | |
| `` w `` | Zobacz opcje drzewa pracy | |
| `` / `` | Filtruj bieżący widok po tekście | |
## Status
| Key | Action | Info |
|-----|--------|-------------|
| `` o `` | Otwórz plik konfiguracyjny | Otwórz plik w domyślnej aplikacji. |
| `` e `` | Edytuj plik konfiguracyjny | Otwórz plik w zewnętrznym edytorze. |
| `` u `` | Sprawdź aktualizacje | |
| `` <enter> `` | Przełącz na ostatnie repozytorium | |
@@ -344,6 +346,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | Otwórz commit w przeglądarce | |
| `` n `` | Utwórz nową gałąź z commita | |
| `` N `` | Przenieś commity do nowej gałęzi | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | Nowe drzewo pracy | |
| `` g `` | Reset | Wyświetl opcje resetu (miękki/mieszany/twardy) do wybranego elementu. |
| `` C `` | Kopiuj (cherry-pick) | Oznacz commit jako skopiowany. Następnie, w widoku lokalnych commitów, możesz nacisnąć `V`, aby wkleić (cherry-pick) skopiowane commity do sprawdzonej gałęzi. W dowolnym momencie możesz nacisnąć `<esc>`, aby anulować zaznaczenie. |
| `` <ctrl+r> `` | Resetuj wybrane (cherry-picked) commity | |
@@ -351,7 +354,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Wyświetl pliki | |
| `` w `` | Zobacz opcje drzewa pracy | |
| `` / `` | Szukaj w bieżącym widoku po tekście | |
## Submoduły
@@ -375,13 +377,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | Skopiuj tag do schowka | |
| `` <space> `` | Przełącz | Przełącz wybrany tag jako odłączoną głowę (detached HEAD). |
| `` n `` | Nowy tag | Utwórz nowy tag z bieżącego commita. Zostaniesz poproszony o wprowadzenie nazwy tagu i opcjonalnego opisu. |
| `` w `` | Nowe drzewo pracy | |
| `` d `` | Usuń | Wyświetl opcje usuwania lokalnego/odległego tagu. |
| `` P `` | Wyślij tag | Wyślij wybrany tag do zdalnego. Zostaniesz poproszony o wybranie zdalnego. |
| `` g `` | Reset | Wyświetl opcje resetu (miękki/mieszany/twardy) do wybranego elementu. |
| `` <ctrl+t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Pokaż commity | |
| `` w `` | Zobacz opcje drzewa pracy | |
| `` / `` | Filtruj bieżący widok po tekście | |
## Zdalne
@@ -403,6 +405,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | Kopiuj nazwę gałęzi do schowka | |
| `` <space> `` | Przełącz | Przełącz na nową lokalną gałąź na podstawie wybranej gałęzi zdalnej. Nowa gałąź będzie śledzić gałąź zdalną. |
| `` n `` | Nowa gałąź | |
| `` w `` | Nowe drzewo pracy | |
| `` M `` | Scal | Scal wybraną gałąź z aktualnie sprawdzoną gałęzią. |
| `` r `` | Przebazuj | Przebazuj przełączoną gałąź na wybraną gałąź. |
| `` d `` | Usuń | Usuń gałąź zdalną ze zdalnego. |
@@ -412,5 +415,4 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Pokaż commity | |
| `` w `` | Zobacz opcje drzewa pracy | |
| `` / `` | Filtruj bieżący widok po tekście | |
+12 -10
View File
@@ -22,7 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` R `` | Atualizar | Atualize o estado do git (ou seja, execute `git status`, `git branch`, etc em segundo plano para atualizar o conteúdo de painéis). Isso não executa `git fetch`. |
| `` + `` | Modo de tela seguinte (normal/metade/tela cheia) | |
| `` _ `` | Modo de tela anterior | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` <esc> `` | Cancelar | |
| `` ? `` | Abrir o menu de atalhos do teclado | |
| `` <ctrl+s> `` | Ver opções de filtro | View options for filtering the commit log, so that only commits matching the filter are shown. |
@@ -30,6 +31,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` q, <ctrl+c> `` | Sair | |
| `` <ctrl+z> `` | Suspender a aplicação | |
| `` <ctrl+w> `` | Toggle whitespace | Toggle whether or not whitespace changes are shown in the diff view.<br><br>The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. |
| `` % `` | Toggle mouse capture | Toggle whether lazygit captures mouse events. When mouse capture is off, the terminal handles the mouse, so you can select text with it; this is useful in terminals that don't have a modifier key to bypass the capture.<br><br>The default can be changed in the config file with the key 'gui.mouseEvents'. |
| `` <alt+shift+c> `` | Editar arquivo de configuração | Abrir arquivo no editor externo. |
| `` z `` | Desfazer | O reflog será usado para determinar qual comando git para executar para desfazer o último comando git. Isto não inclui mudanças na árvore de trabalho; apenas compromissos são tidos em consideração. |
| `` Z `` | Refazer | O reflog será usado para determinar qual comando git para executar para refazer o último comando git. Isto não inclui mudanças na árvore de trabalho; apenas compromissos são tidos em consideração. |
@@ -92,6 +95,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <space> `` | Verificar | Checar item selecionado |
| `` n `` | Nova branch | |
| `` N `` | Mover commits para uma nova branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | Nova árvore de trabalho | |
| `` o `` | Criar solicitação de pull | |
| `` O `` | View create pull request options | |
| `` G `` | Open pull request in browser | |
@@ -111,7 +115,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | Abrir ferramenta de diff externa (git difftool) | |
| `` 0 `` | Focar visualização principal | |
| `` <enter> `` | Ver commits | |
| `` w `` | Ver opções da árvore de trabalho | |
| `` / `` | Filtrar a visualização atual por texto | |
## Branches remotos
@@ -121,6 +124,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | Copiar nome da branch para área de transferência | |
| `` <space> `` | Verificar | Checar a nova branch baseada na brach remota selecionada, ou a branch remota como HEAD, desanexado |
| `` n `` | Nova branch | |
| `` w `` | Nova árvore de trabalho | |
| `` M `` | Mesclar | Ver opções para mesclar o item selecionado no branch atual (mesclar regularmente, mesclar squash) |
| `` r `` | Refazer | Refazer a branch checada na branch selecionada |
| `` d `` | Apagar | Excluir o branch remoto do controle remoto. |
@@ -130,7 +134,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | Abrir ferramenta de diff externa (git difftool) | |
| `` 0 `` | Focar visualização principal | |
| `` <enter> `` | Ver commits | |
| `` w `` | Ver opções da árvore de trabalho | |
| `` / `` | Filtrar a visualização atual por texto | |
## Commit arquivos
@@ -186,13 +189,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | Abrir commit no navegador | |
| `` n `` | Create new branch off of commit | |
| `` N `` | Mover commits para uma nova branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | Nova árvore de trabalho | |
| `` g `` | Restaurar | Ver opções de redefinição (soft/mixed/hard) para redefinir para o item selecionado. |
| `` C `` | Copiar (cherry-pick) | Marcar commit como copiado. Então, dentro da visualização local de commits, você pode pressionar `V` para colar (cherry-pick) o(s) commit(s) copiado(s) em seu branch de check-out. A qualquer momento você pode pressionar `<esc>` para cancelar a seleção. |
| `` <ctrl+t> `` | Abrir ferramenta de diff externa (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focar visualização principal | |
| `` <enter> `` | Ver arquivos | |
| `` w `` | Ver opções da árvore de trabalho | |
| `` / `` | Pesquisar na visualização atual por texto | |
## Etiquetas
@@ -202,13 +205,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | Copiar etiqueta para área de transferência | |
| `` <space> `` | Verificar | Checar a tag selecionada como um HEAD, desanexado |
| `` n `` | Nova etiqueta | Crie uma nova etiqueta a partir do commit atual. Você será solicitado a digitar um nome e uma descrição opcional. |
| `` w `` | Nova árvore de trabalho | |
| `` d `` | Apagar | Ver opções de exclusão para tag local/remoto. |
| `` P `` | Empurrar etiqueta | Push the selected tag to a remote. You'll be prompted to select a remote. |
| `` g `` | Restaurar | Ver opções de redefinição (soft/mixed/hard) para redefinir para o item selecionado. |
| `` <ctrl+t> `` | Abrir ferramenta de diff externa (git difftool) | |
| `` 0 `` | Focar visualização principal | |
| `` <enter> `` | Ver commits | |
| `` w `` | Ver opções da árvore de trabalho | |
| `` / `` | Filtrar a visualização atual por texto | |
## Input prompt
@@ -271,7 +274,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <space> `` | Escolha o local | |
| `` b `` | Pegar todos os pedaços | |
| `` b `` | Pick both hunks | |
| `` <up>, k `` | Trecho anterior | |
| `` <down>, j `` | Próximo trecho | |
| `` <left>, h `` | Conflito anterior | |
@@ -308,6 +311,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | Abrir commit no navegador | |
| `` n `` | Create new branch off of commit | |
| `` N `` | Mover commits para uma nova branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | Nova árvore de trabalho | |
| `` g `` | Restaurar | Ver opções de redefinição (soft/mixed/hard) para redefinir para o item selecionado. |
| `` C `` | Copiar (cherry-pick) | Marcar commit como copiado. Então, dentro da visualização local de commits, você pode pressionar `V` para colar (cherry-pick) o(s) commit(s) copiado(s) em seu branch de check-out. A qualquer momento você pode pressionar `<esc>` para cancelar a seleção. |
| `` <ctrl+r> `` | Reset copied (cherry-picked) commits selection | |
@@ -315,7 +319,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focar visualização principal | |
| `` <enter> `` | Ver commits | |
| `` w `` | Ver opções da árvore de trabalho | |
| `` / `` | Filtrar a visualização atual por texto | |
## Remotes
@@ -346,17 +349,16 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` g `` | Pop | Aplique a entrada de stash no seu diretório de trabalho e remova a entrada de stash. |
| `` d `` | Descartar | Remova a entrada do stash da lista de armazenamento. |
| `` n `` | Nova branch | Criar um novo ramo a partir da entrada de lixo selecionada. Isso funciona verificando o commit do qual a entrada de lixo foi criada, criar um novo branch a partir desse commit e, em seguida, aplicar a entrada de lixo ao novo branch como um commit adicional. |
| `` w `` | Nova árvore de trabalho | |
| `` r `` | Renomear o stash | |
| `` 0 `` | Focar visualização principal | |
| `` <enter> `` | Ver arquivos | |
| `` w `` | Ver opções da árvore de trabalho | |
| `` / `` | Filtrar a visualização atual por texto | |
## Status
| Key | Action | Info |
|-----|--------|-------------|
| `` o `` | Abrir o ficheiro de config | Abrir arquivo no aplicativo padrão. |
| `` e `` | Editar arquivo de configuração | Abrir arquivo no editor externo. |
| `` u `` | Verificar atualização | |
| `` <enter> `` | Mudar para um repositório recente | |
@@ -374,6 +376,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | Abrir commit no navegador | |
| `` n `` | Create new branch off of commit | |
| `` N `` | Mover commits para uma nova branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | Nova árvore de trabalho | |
| `` g `` | Restaurar | Ver opções de redefinição (soft/mixed/hard) para redefinir para o item selecionado. |
| `` C `` | Copiar (cherry-pick) | Marcar commit como copiado. Então, dentro da visualização local de commits, você pode pressionar `V` para colar (cherry-pick) o(s) commit(s) copiado(s) em seu branch de check-out. A qualquer momento você pode pressionar `<esc>` para cancelar a seleção. |
| `` <ctrl+r> `` | Reset copied (cherry-picked) commits selection | |
@@ -381,7 +384,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focar visualização principal | |
| `` <enter> `` | Ver arquivos | |
| `` w `` | Ver opções da árvore de trabalho | |
| `` / `` | Pesquisar na visualização atual por texto | |
## Submódulos
+12 -10
View File
@@ -22,7 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` R `` | Обновить | Refresh the git state (i.e. run `git status`, `git branch`, etc in background to update the contents of panels). This does not run `git fetch`. |
| `` + `` | Следующий режим экрана (нормальный/полуэкранный/полноэкранный) | |
| `` _ `` | Предыдущий режим экрана | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` <esc> `` | Отменить | |
| `` ? `` | Открыть меню | |
| `` <ctrl+s> `` | Просмотреть параметры фильтрации по пути | View options for filtering the commit log, so that only commits matching the filter are shown. |
@@ -30,6 +31,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` q, <ctrl+c> `` | Выйти | |
| `` <ctrl+z> `` | Suspend the application | |
| `` <ctrl+w> `` | Переключить отображение изменении пробелов в просмотрщике сравнении | Toggle whether or not whitespace changes are shown in the diff view.<br><br>The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. |
| `` % `` | Toggle mouse capture | Toggle whether lazygit captures mouse events. When mouse capture is off, the terminal handles the mouse, so you can select text with it; this is useful in terminals that don't have a modifier key to bypass the capture.<br><br>The default can be changed in the config file with the key 'gui.mouseEvents'. |
| `` <alt+shift+c> `` | Редактировать файл конфигурации | Open file in external editor. |
| `` z `` | Отменить (через reflog) (экспериментальный) | Журнал ссылок (reflog) будет использоваться для определения того, какую команду git запустить, чтобы отменить последнюю команду git. Сюда не входят изменения в рабочем дереве; учитываются только коммиты. |
| `` Z `` | Повторить (через reflog) (экспериментальный) | Журнал ссылок (reflog) будет использоваться для определения того, какую команду git нужно запустить, чтобы повторить последнюю команду git. Сюда не входят изменения в рабочем дереве; учитываются только коммиты. |
@@ -112,7 +115,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <space> `` | Выбрать эту часть | |
| `` b `` | Выбрать все части | |
| `` b `` | Pick both hunks | |
| `` <up>, k `` | Выбрать предыдущую часть | |
| `` <down>, j `` | Выбрать следующую часть | |
| `` <left>, h `` | Выбрать предыдущий конфликт | |
@@ -149,6 +152,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | Открыть коммит в браузере | |
| `` n `` | Создать новую ветку с этого коммита | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` g `` | Просмотреть параметры сброса | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | Скопировать отобранные коммит (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <ctrl+r> `` | Сбросить отобранную (скопированную \| cherry-picked) выборку коммитов | |
@@ -156,7 +160,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Просмотреть коммиты | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Коммиты
@@ -192,13 +195,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | Открыть коммит в браузере | |
| `` n `` | Создать новую ветку с этого коммита | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` g `` | Просмотреть параметры сброса | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | Скопировать отобранные коммит (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Просмотреть файлы выбранного элемента | |
| `` w `` | View worktree options | |
| `` / `` | Найти | |
## Локальные Ветки
@@ -210,6 +213,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <space> `` | Переключить | Checkout selected item. |
| `` n `` | Новая ветка | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` o `` | Создать запрос на принятие изменений | |
| `` O `` | Создать параметры запроса принятие изменений | |
| `` G `` | Open pull request in browser | |
@@ -229,7 +233,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Просмотреть коммиты | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Меню
@@ -258,6 +261,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | Открыть коммит в браузере | |
| `` n `` | Создать новую ветку с этого коммита | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` g `` | Просмотреть параметры сброса | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | Скопировать отобранные коммит (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <ctrl+r> `` | Сбросить отобранную (скопированную \| cherry-picked) выборку коммитов | |
@@ -265,7 +269,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Просмотреть файлы выбранного элемента | |
| `` w `` | View worktree options | |
| `` / `` | Найти | |
## Подмодули
@@ -313,7 +316,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` o `` | Открыть файл конфигурации | Open file in default application. |
| `` e `` | Редактировать файл конфигурации | Open file in external editor. |
| `` u `` | Проверить обновления | |
| `` <enter> `` | Переключиться на последний репозиторий | |
@@ -328,13 +330,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | Copy tag to clipboard | |
| `` <space> `` | Переключить | Checkout the selected tag as a detached HEAD. |
| `` n `` | Создать тег | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. |
| `` w `` | New worktree | |
| `` d `` | Delete | View delete options for local/remote tag. |
| `` P `` | Отправить тег | Push the selected tag to a remote. You'll be prompted to select a remote. |
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Просмотреть коммиты | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Удалённые ветки
@@ -344,6 +346,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | Скопировать название ветки в буфер обмена | |
| `` <space> `` | Переключить | Checkout a new local branch based on the selected remote branch, or the remote branch as a detached head. |
| `` n `` | Новая ветка | |
| `` w `` | New worktree | |
| `` M `` | Слияние с текущей переключённой веткой | View options for merging the selected item into the current branch (regular merge, squash merge) |
| `` r `` | Перебазировать переключённую ветку на эту ветку | Rebase the checked-out branch onto the selected branch. |
| `` d `` | Delete | Delete the remote branch from the remote. |
@@ -353,7 +356,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Просмотреть коммиты | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Удалённые репозитории
@@ -409,8 +411,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` g `` | Применить припрятанные изменения и тут же удалить их из хранилища | Apply the stash entry to your working directory and remove the stash entry. |
| `` d `` | Удалить припрятанные изменения из хранилища | Remove the stash entry from the stash list. |
| `` n `` | Новая ветка | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
| `` w `` | New worktree | |
| `` r `` | Переименовать хранилище | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Просмотреть файлы выбранного элемента | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
+11 -9
View File
@@ -23,6 +23,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` + `` | 下一屏模式(正常/半屏/全屏) | |
| `` _ `` | 上一屏模式 | |
| `` \| `` | 切换分页器 | 从已配置的分页器列表中选择下一个分页器 |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` <esc> `` | 取消 | |
| `` ? `` | 打开菜单 | |
| `` <ctrl+s> `` | 查看按路径过滤选项 | 查看用于过滤提交日志的选项,以便仅显示与过滤器匹配的提交。 |
@@ -30,6 +31,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` q, <ctrl+c> `` | 退出 | |
| `` <ctrl+z> `` | 挂起应用程序 | |
| `` <ctrl+w> `` | 切换是否在差异视图中显示空白字符差异 | 切换是否在差异视图中显示空白字符更改。<br><br>默认值可在配置文件中通过键 'git.ignoreWhitespaceInDiffView' 更改。 |
| `` % `` | Toggle mouse capture | Toggle whether lazygit captures mouse events. When mouse capture is off, the terminal handles the mouse, so you can select text with it; this is useful in terminals that don't have a modifier key to bypass the capture.<br><br>The default can be changed in the config file with the key 'gui.mouseEvents'. |
| `` <alt+shift+c> `` | 编辑配置文件 | 使用外部编辑器打开文件 |
| `` z `` | 撤销 | Reflog将用于确定运行哪个git命令来撤消最后一个git命令。这并不包括对工作树的更改,只考虑提交。 |
| `` Z `` | 重做 | Reflog将用于确定运行哪个git命令来重做上一个git命令。这并不包括对工作树的更改,只考虑提交。 |
@@ -60,6 +63,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | 在浏览器中打开提交 | |
| `` n `` | 从提交创建新分支 | |
| `` N `` | 移动提交至新分支 | 创建一个新分支,并将当前分支未推送的提交移动到该分支。如果您打算开始新工作但忘记先创建新分支,这会很有用。<br><br>请注意,此操作忽略选择,新分支总是从主分支创建或堆叠在当前分支之上(您可以选择哪种方式)。 |
| `` w `` | 新建工作树 | |
| `` g `` | 查看重置选项 | 查看重置选项 (soft/mixed/hard) 用于重置到选择项 |
| `` C `` | 复制提交(拣选) | 标记提交为已复制。然后,在本地提交视图中,您可以按 `V` (Cherry-Pick) 将已复制的提交粘贴到已检出的分支中。任何时候都可以按 `<esc>` 来取消选择。 |
| `` <ctrl+r> `` | 重置已拣选(复制)的提交 | |
@@ -67,7 +71,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | 选择当前分支的提交 | |
| `` 0 `` | 聚焦主视图 | |
| `` <enter> `` | 查看提交的文件 | |
| `` w `` | 查看工作区选项 | |
| `` / `` | 开始搜索 | |
## 子模块
@@ -104,6 +107,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | 在浏览器中打开提交 | |
| `` n `` | 从提交创建新分支 | |
| `` N `` | 移动提交至新分支 | 创建一个新分支,并将当前分支未推送的提交移动到该分支。如果您打算开始新工作但忘记先创建新分支,这会很有用。<br><br>请注意,此操作忽略选择,新分支总是从主分支创建或堆叠在当前分支之上(您可以选择哪种方式)。 |
| `` w `` | 新建工作树 | |
| `` g `` | 查看重置选项 | 查看重置选项 (soft/mixed/hard) 用于重置到选择项 |
| `` C `` | 复制提交(拣选) | 标记提交为已复制。然后,在本地提交视图中,您可以按 `V` (Cherry-Pick) 将已复制的提交粘贴到已检出的分支中。任何时候都可以按 `<esc>` 来取消选择。 |
| `` <ctrl+r> `` | 重置已拣选(复制)的提交 | |
@@ -111,7 +115,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | 选择当前分支的提交 | |
| `` 0 `` | 聚焦主视图 | |
| `` <enter> `` | 查看提交 | |
| `` w `` | 查看工作区选项 | |
| `` / `` | 通过文本过滤当前视图 | |
## 提交
@@ -147,13 +150,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | 在浏览器中打开提交 | |
| `` n `` | 从提交创建新分支 | |
| `` N `` | 移动提交至新分支 | 创建一个新分支,并将当前分支未推送的提交移动到该分支。如果您打算开始新工作但忘记先创建新分支,这会很有用。<br><br>请注意,此操作忽略选择,新分支总是从主分支创建或堆叠在当前分支之上(您可以选择哪种方式)。 |
| `` w `` | 新建工作树 | |
| `` g `` | 查看重置选项 | 查看重置选项 (soft/mixed/hard) 用于重置到选择项 |
| `` C `` | 复制提交(拣选) | 标记提交为已复制。然后,在本地提交视图中,您可以按 `V` (Cherry-Pick) 将已复制的提交粘贴到已检出的分支中。任何时候都可以按 `<esc>` 来取消选择。 |
| `` <ctrl+t> `` | 使用外部差异比较工具(git difftool) | |
| `` * `` | 选择当前分支的提交 | |
| `` 0 `` | 聚焦主视图 | |
| `` <enter> `` | 查看提交的文件 | |
| `` w `` | 查看工作区选项 | |
| `` / `` | 开始搜索 | |
## 提交信息
@@ -225,6 +228,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <space> `` | 检出 | 检出选中的项目 |
| `` n `` | 新分支 | |
| `` N `` | 移动提交至新分支 | 创建一个新分支,并将当前分支未推送的提交移动到该分支。如果您打算开始新工作但忘记先创建新分支,这会很有用。<br><br>请注意,此操作忽略选择,新分支总是从主分支创建或堆叠在当前分支之上(您可以选择哪种方式)。 |
| `` w `` | 新建工作树 | |
| `` o `` | 创建拉取请求 | |
| `` O `` | 创建拉取请求选项 | |
| `` G `` | 在浏览器中打开拉取请求 | |
@@ -244,7 +248,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | 使用外部差异比较工具(git difftool) | |
| `` 0 `` | 聚焦主视图 | |
| `` <enter> `` | 查看提交 | |
| `` w `` | 查看工作区选项 | |
| `` / `` | 通过文本过滤当前视图 | |
## 构建补丁中
@@ -270,13 +273,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | 复制标签到剪贴板 | |
| `` <space> `` | 检出 | 检出选择的标签作为分离的HEAD |
| `` n `` | 创建标签 | 基于当前提交创建一个新标签。您将在弹窗中输入标签名称和描述(可选)。 |
| `` w `` | 新建工作树 | |
| `` d `` | 删除 | 查看本地/远程标签的删除选项 |
| `` P `` | 推送标签 | 推送选择的标签到远端。您将在弹窗中选择一个远端。 |
| `` g `` | 重置 | 查看重置选项 (soft/mixed/hard) 用于重置到选择项 |
| `` <ctrl+t> `` | 使用外部差异比较工具(git difftool) | |
| `` 0 `` | 聚焦主视图 | |
| `` <enter> `` | 查看提交 | |
| `` w `` | 查看工作区选项 | |
| `` / `` | 通过文本过滤当前视图 | |
## 次要
@@ -292,7 +295,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <space> `` | 选中区块 | |
| `` b `` | 选中所有区块 | |
| `` b `` | Pick both hunks | |
| `` <up>, k `` | 选择顶部块 | |
| `` <down>, j `` | 选择底部块 | |
| `` <left>, h `` | 选择上一个冲突 | |
@@ -339,7 +342,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` o `` | 打开配置文件 | 使用默认程序打开该文件 |
| `` e `` | 编辑配置文件 | 使用外部编辑器打开文件 |
| `` u `` | 检查更新 | |
| `` <enter> `` | 切换到最近的仓库 | |
@@ -371,10 +373,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` g `` | 应用并删除 | 将存储项应用到工作目录并删除存储项。 |
| `` d `` | 删除 | 从贮藏列表中删除该贮藏项 |
| `` n `` | 新分支 | 从选定的贮藏项创建一个新分支。这是通过 git 检查创建贮藏项的提交,从该提交创建一个新分支,然后将贮藏项作为附加提交应用到新分支来实现的。 |
| `` w `` | 新建工作树 | |
| `` r `` | 重命名贮藏 | |
| `` 0 `` | 聚焦主视图 | |
| `` <enter> `` | 查看提交的文件 | |
| `` w `` | 查看工作区选项 | |
| `` / `` | 通过文本过滤当前视图 | |
## 输入提示
@@ -403,6 +405,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | 复制分支名称到剪贴板 | |
| `` <space> `` | 检出 | 基于当前选中的远程分支检出一个新的本地分支,或者将远程分支作分离的HEAD。 |
| `` n `` | 新分支 | |
| `` w `` | 新建工作树 | |
| `` M `` | 合并到当前检出的分支 | 查看将选中项合并到当前分支的选项(正常合并,压缩合并) |
| `` r `` | 变基 | 将检出的分支变基到所选的分支上。 |
| `` d `` | 删除 | 从远程删除远程分支。 |
@@ -412,5 +415,4 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | 使用外部差异比较工具(git difftool) | |
| `` 0 `` | 聚焦主视图 | |
| `` <enter> `` | 查看提交 | |
| `` w `` | 查看工作区选项 | |
| `` / `` | 通过文本过滤当前视图 | |
+12 -10
View File
@@ -22,7 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` R `` | 重新整理 | Refresh the git state (i.e. run `git status`, `git branch`, etc in background to update the contents of panels). This does not run `git fetch`. |
| `` + `` | 下一個螢幕模式(常規/半螢幕/全螢幕) | |
| `` _ `` | 上一個螢幕模式 | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` <esc> `` | 取消 | |
| `` ? `` | 開啟選單 | |
| `` <ctrl+s> `` | 檢視篩選路徑選項 | View options for filtering the commit log, so that only commits matching the filter are shown. |
@@ -30,6 +31,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` q, <ctrl+c> `` | 結束 | |
| `` <ctrl+z> `` | Suspend the application | |
| `` <ctrl+w> `` | 切換是否在差異檢視中顯示空格變更 | Toggle whether or not whitespace changes are shown in the diff view.<br><br>The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. |
| `` % `` | Toggle mouse capture | Toggle whether lazygit captures mouse events. When mouse capture is off, the terminal handles the mouse, so you can select text with it; this is useful in terminals that don't have a modifier key to bypass the capture.<br><br>The default can be changed in the config file with the key 'gui.mouseEvents'. |
| `` <alt+shift+c> `` | 編輯設定檔案 | 使用外部編輯器開啟 |
| `` z `` | 復原 | 將使用 reflog 確任 git 指令以復原。這不包括工作區更改;只考慮提交。 |
| `` Z `` | 取消復原 | 將使用 reflog 確任 git 指令以重作。這不包括工作區更改;只考慮提交。 |
@@ -88,7 +91,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <space> `` | 挑選程式碼片段 | |
| `` b `` | 挑選所有程式碼片段 | |
| `` b `` | Pick both hunks | |
| `` <up>, k `` | 選擇上一段 | |
| `` <down>, j `` | 選擇下一段 | |
| `` <left>, h `` | 選擇上一個衝突 | |
@@ -139,6 +142,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | 在瀏覽器中開啟提交 | |
| `` n `` | 從提交建立新分支 | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` g `` | 檢視重設選項 | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | 複製提交 (揀選) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <ctrl+r> `` | 重設選定的揀選 (複製) 提交 | |
@@ -146,7 +150,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 檢視所選項目的檔案 | |
| `` w `` | 檢視工作目錄選項 | |
| `` / `` | 搜尋 | |
## 子模組
@@ -206,13 +209,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | 在瀏覽器中開啟提交 | |
| `` n `` | 從提交建立新分支 | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` g `` | 檢視重設選項 | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | 複製提交 (揀選) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 檢視所選項目的檔案 | |
| `` w `` | 檢視工作目錄選項 | |
| `` / `` | 搜尋 | |
## 提交摘要
@@ -250,10 +253,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` g `` | 還原 | Apply the stash entry to your working directory and remove the stash entry. |
| `` d `` | 捨棄 | Remove the stash entry from the stash list. |
| `` n `` | 新分支 | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
| `` w `` | New worktree | |
| `` r `` | 重新命名收藏 | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 檢視所選項目的檔案 | |
| `` w `` | 檢視工作目錄選項 | |
| `` / `` | 搜尋 | |
## 日誌
@@ -266,6 +269,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | 在瀏覽器中開啟提交 | |
| `` n `` | 從提交建立新分支 | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` g `` | 檢視重設選項 | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | 複製提交 (揀選) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <ctrl+r> `` | 重設選定的揀選 (複製) 提交 | |
@@ -273,7 +277,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 檢視提交 | |
| `` w `` | 檢視工作目錄選項 | |
| `` / `` | 搜尋 | |
## 本地分支
@@ -285,6 +288,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <space> `` | 檢出 | 檢出選定的項目。 |
| `` n `` | 新分支 | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` o `` | 建立拉取請求 | |
| `` O `` | 建立拉取請求選項 | |
| `` G `` | Open pull request in browser | |
@@ -304,7 +308,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 檢視提交 | |
| `` w `` | 檢視工作目錄選項 | |
| `` / `` | 搜尋 | |
## 標籤
@@ -314,13 +317,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | Copy tag to clipboard | |
| `` <space> `` | 檢出 | Checkout the selected tag as a detached HEAD. |
| `` n `` | 建立標籤 | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. |
| `` w `` | New worktree | |
| `` d `` | 刪除 | View delete options for local/remote tag. |
| `` P `` | 推送標籤 | Push the selected tag to a remote. You'll be prompted to select a remote. |
| `` g `` | 重設 | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 檢視提交 | |
| `` w `` | 檢視工作目錄選項 | |
| `` / `` | 搜尋 | |
## 檔案
@@ -368,7 +371,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` o `` | 開啟設定檔案 | 使用預設軟體開啟 |
| `` e `` | 編輯設定檔案 | 使用外部編輯器開啟 |
| `` u `` | 檢查更新 | |
| `` <enter> `` | 切換到最近使用的版本庫 | |
@@ -403,6 +405,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | 複製分支名稱到剪貼簿 | |
| `` <space> `` | 檢出 | Checkout a new local branch based on the selected remote branch, or the remote branch as a detached head. |
| `` n `` | 新分支 | |
| `` w `` | New worktree | |
| `` M `` | 合併到當前檢出的分支 | View options for merging the selected item into the current branch (regular merge, squash merge) |
| `` r `` | 將已檢出的分支變基至此分支 | Rebase the checked-out branch onto the selected branch. |
| `` d `` | 刪除 | Delete the remote branch from the remote. |
@@ -412,5 +415,4 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 檢視提交 | |
| `` w `` | 檢視工作目錄選項 | |
| `` / `` | 搜尋 | |
+56 -2
View File
@@ -110,6 +110,26 @@ gui:
# is true.
expandedSidePanelWeight: 2
# If true, don't give a side panel more height than it needs to show its
# content; when all panels fit, the leftover height is shared among them so that
# they still fill the screen.
shrinkSidePanelsToContent: false
# The side panels, in the order they appear from top to bottom.
# Each entry is a list of one or more names that share a single panel as tabs
# (cycle through them with the next-tab/previous-tab keys).
# Omit a name to hide it; give a name its own one-element list to promote a tab
# to a top-level panel.
# Valid names are: 'status', 'files', 'worktrees', 'submodules', 'branches',
# 'remotes', 'tags', 'commits', 'reflog', 'stash'. 'files', 'branches', and
# 'commits' must always be included; they can't be hidden.
sidePanels:
- [status]
- [files, worktrees, submodules]
- [branches, remotes, tags]
- [commits, reflog]
- [stash]
# Sometimes the main window is split in two (e.g. when the selected file has
# both staged and unstaged changes). This setting controls how the two sections
# are split.
@@ -342,6 +362,11 @@ gui:
git:
# Array of pagers. Each entry has the following format:
#
# # A name for the pager, shown in the notification when cycling pagers.
# # If not set, the name is derived from the first word of the pager
# # command (or of the external diff command).
# name: ""
#
# # Value of the --color arg in the git diff command. Some pagers want
# # this to be set to 'always' and some want it set to 'never'
# colorArg: "always"
@@ -361,6 +386,9 @@ git:
# # https://git-scm.com/docs/gitattributes#_defining_an_external_diff_driver.
# useExternalDiffGitConfig: false
#
# 'pager', 'externalDiffCommand', and 'useExternalDiffGitConfig' are mutually
# exclusive; set at most one per entry.
#
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Custom_Pagers.md
# for more information.
pagers: []
@@ -406,6 +434,11 @@ git:
# If true, periodically refresh files and submodules
autoRefresh: true
# If true, poll the repo periodically for external ref changes (commits, branch
# updates, checkouts made outside lazygit) and refresh when one is detected.
# Independent of autoRefresh, which only governs the files panel.
autoDetectExternalChanges: true
# If not "none", lazygit will automatically fast-forward local branches to match
# their upstream after fetching. Applies to branches that are not the currently
# checked out branch, and only to those that are strictly behind their upstream
@@ -499,6 +532,15 @@ git:
# to 40 to disable truncation.
truncateCopiedCommitHashesTo: 12
# Config relating to git worktrees
worktree:
# Default parent directory for new worktrees. It is offered as a candidate
# location alongside the parent directories of any worktrees you already have.
# A relative path is resolved against the repository's root directory, so
# "../worktrees" sits beside the repo and ".worktrees" sits inside it.
# A leading "~" is expanded to your home directory, so "~/worktrees" works.
defaultPath: ""
# Periodic update checks
update:
# One of: 'prompt' (default) | 'background' | 'never'
@@ -517,6 +559,11 @@ refresher:
# Auto-fetch can be disabled via option 'git.autoFetch'.
fetchInterval: 60
# Interval in seconds at which lazygit polls for external ref changes (commits,
# branch updates, checkouts made outside lazygit).
# Detection can be disabled via option 'git.autoDetectExternalChanges'.
externalChangeCheckInterval: 2
# If true, show a confirmation popup before quitting Lazygit
confirmOnQuit: false
@@ -648,6 +695,7 @@ keybinding:
confirmInEditor: [<ctrl+enter>, <ctrl+s>]
remove: d
new: "n"
newWorktree: w
edit: e
openFile: o
scrollUpMain: [<pgup>, K, <ctrl+u>]
@@ -667,6 +715,7 @@ keybinding:
nextScreenMode: +
prevScreenMode: _
cyclePagers: '|'
cyclePagersReverse: \
undo: z
redo: Z
filteringMenu: <ctrl+s>
@@ -681,6 +730,7 @@ keybinding:
increaseRenameSimilarityThreshold: )
decreaseRenameSimilarityThreshold: (
openDiffTool: <ctrl+t>
editConfig: <alt+shift+c>
status:
checkForUpdate: u
recentRepos: <enter>
@@ -726,8 +776,6 @@ keybinding:
fetchRemote: f
addForkRemote: F
sortOrder: s
worktrees:
viewWorktreeOptions: w
commits:
squashDown: s
renameCommit: r
@@ -1059,6 +1107,12 @@ keybinding:
edit: <disabled> # disable 'edit file'
```
### Overriding the platform for default keybindings
A few keybindings have different defaults on macOS than on Linux and Windows (e.g. word-wise cursor movement in text inputs uses `alt` on macOS but `ctrl` elsewhere). Lazygit picks these based on the OS it's running on, but you can override that with the `LAZYGIT_KEYBINDING_PLATFORM` environment variable. Set it to `darwin`, `linux`, or `windows`; any other value is ignored and the actual OS is used.
This is useful when running lazygit in a Linux container that you access over ssh from a Mac, where you'd rather use the macOS keybindings.
### Example Keybindings For Colemak Users
```yaml
+18 -29
View File
@@ -2,8 +2,6 @@
Lazygit supports custom pagers, [configured](/docs/Config.md) in the config.yml file (which can be opened by pressing `e` in the Status panel).
Support does not extend to Windows users, because we're making use of a package which doesn't have Windows support. However, see [below](#emulating-custom-pagers-on-windows) for a workaround.
Multiple pagers are supported; you can cycle through them with the `|` key. This can be useful if you usually prefer a particular pager, but want to use a different one for certain kinds of diffs.
Pagers are configured with the `pagers` array in the git section; here's an example for a multi-pager setup (use an empty object `{}` for the default builtin diff display that doesn't use a pager):
@@ -71,7 +69,7 @@ git:
- externalDiffCommand: difft --color=always
```
The `colorArg` and `pager` options are not used in this case.
The `colorArg` option is not used in this case.
You can add whatever extra arguments you prefer for your difftool; for instance
@@ -81,6 +79,22 @@ git:
- externalDiffCommand: difft --color=always --display=inline --syntax-highlight=off
```
This can also be used for normal git diffs with custom parameters, such as `--color-words` or `--word-diff` which some people find useful. To do that, save a script like this to, say, `~/bin/color-words.sh`:
```sh
#!/bin/sh
git diff --color-words --no-index --color=always --no-ext-diff "$2" "$5"
```
And then use it in your git config like so:
```yaml
git:
pagers:
- externalDiffCommand: ~/bin/color-words.sh
```
Instead of setting this command in lazygit's `externalDiffCommand` config, you can also tell lazygit to use the external diff command that is configured in git itself (`diff.external`), by using
```yaml
@@ -91,29 +105,4 @@ git:
This can be useful if you also want to use it for diffs on the command line, and it also has the advantage that you can configure it per file type in `.gitattributes`; see https://git-scm.com/docs/gitattributes#_defining_an_external_diff_driver.
## Emulating custom pagers on Windows
There is a trick to emulate custom pagers on Windows using a Powershell script configured as an external diff command. It's not perfect, but certainly better than nothing. To do this, save the following script as `lazygit-pager.ps1` at a convenient place on your disk:
```pwsh
#!/usr/bin/env pwsh
$old = $args[1].Replace('\', '/')
$new = $args[4].Replace('\', '/')
$path = $args[0]
git diff --no-index --no-ext-diff $old $new
| %{ $_.Replace($old, $path).Replace($new, $path) }
| delta --width=$env:LAZYGIT_COLUMNS
```
Use the pager of your choice with the arguments you like in the last line of the script. Personally I wouldn't want to use lazygit anymore without delta's `--hyperlinks --hyperlinks-file-link-format="lazygit-edit://{path}:{line}"` args, see [above](#delta).
In your lazygit config, use
```yml
git:
pagers:
- externalDiffCommand: "C:/wherever/lazygit-pager.ps1"
```
The main limitation of this approach compared to a "real" pager is that renames are not displayed correctly; they are shown as if they were modifications of the old file. (This affects only the hunk headers; the diff itself is always correct.)
`pager`, `externalDiffCommand`, and `useExternalDiffGitConfig` are alternative ways of producing the diff, so a pager entry may use at most one of them.
+11 -10
View File
@@ -22,7 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` R `` | Refresh | Refresh the git state (i.e. run `git status`, `git branch`, etc in background to update the contents of panels). This does not run `git fetch`. |
| `` + `` | Next screen mode (normal/half/fullscreen) | |
| `` _ `` | Prev screen mode | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` <esc> `` | Cancel | |
| `` ? `` | Open keybindings menu | |
| `` <ctrl+s> `` | View filter options | View options for filtering the commit log, so that only commits matching the filter are shown. |
@@ -30,6 +31,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` q, <ctrl+c> `` | Quit | |
| `` <ctrl+z> `` | Suspend the application | |
| `` <ctrl+w> `` | Toggle whitespace | Toggle whether or not whitespace changes are shown in the diff view.<br><br>The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. |
| `` <alt+shift+c> `` | Edit config file | Open file in external editor. |
| `` z `` | Undo | The reflog will be used to determine what git command to run to undo the last git command. This does not include changes to the working tree; only commits are taken into consideration. |
| `` Z `` | Redo | The reflog will be used to determine what git command to run to redo the last git command. This does not include changes to the working tree; only commits are taken into consideration. |
@@ -110,13 +112,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | Open commit in browser | |
| `` n `` | Create new branch off of commit | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | Copy (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View files | |
| `` w `` | View worktree options | |
| `` / `` | Search the current view by text | |
## Confirmation panel
@@ -176,6 +178,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <space> `` | Checkout | Checkout selected item. |
| `` n `` | New branch | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` o `` | Create pull request | |
| `` O `` | View create pull request options | |
| `` G `` | Open pull request in browser | |
@@ -195,7 +198,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View commits | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Main panel (merging)
@@ -203,7 +205,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <space> `` | Pick hunk | |
| `` b `` | Pick all hunks | |
| `` b `` | Pick both hunks | |
| `` <up>, k `` | Previous hunk | |
| `` <down>, j `` | Next hunk | |
| `` <left>, h `` | Previous conflict | |
@@ -280,6 +282,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | Open commit in browser | |
| `` n `` | Create new branch off of commit | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | Copy (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <ctrl+r> `` | Reset copied (cherry-picked) commits selection | |
@@ -287,7 +290,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View commits | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Remote branches
@@ -297,6 +299,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | Copy branch name to clipboard | |
| `` <space> `` | Checkout | Checkout a new local branch based on the selected remote branch, or the remote branch as a detached head. |
| `` n `` | New branch | |
| `` w `` | New worktree | |
| `` M `` | Merge | View options for merging the selected item into the current branch (regular merge, squash merge) |
| `` r `` | Rebase | Rebase the checked-out branch onto the selected branch. |
| `` d `` | Delete | Delete the remote branch from the remote. |
@@ -306,7 +309,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View commits | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Remotes
@@ -337,17 +339,16 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` g `` | Pop | Apply the stash entry to your working directory and remove the stash entry. |
| `` d `` | Drop | Remove the stash entry from the stash list. |
| `` n `` | New branch | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
| `` w `` | New worktree | |
| `` r `` | Rename stash | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View files | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Status
| Key | Action | Info |
|-----|--------|-------------|
| `` o `` | Open config file | Open file in default application. |
| `` e `` | Edit config file | Open file in external editor. |
| `` u `` | Check for update | |
| `` <enter> `` | Switch to a recent repo | |
@@ -365,6 +366,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | Open commit in browser | |
| `` n `` | Create new branch off of commit | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | Copy (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <ctrl+r> `` | Reset copied (cherry-picked) commits selection | |
@@ -372,7 +374,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View files | |
| `` w `` | View worktree options | |
| `` / `` | Search the current view by text | |
## Submodules
@@ -396,13 +397,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | Copy tag to clipboard | |
| `` <space> `` | Checkout | Checkout the selected tag as a detached HEAD. |
| `` n `` | New tag | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. |
| `` w `` | New worktree | |
| `` d `` | Delete | View delete options for local/remote tag. |
| `` P `` | Push tag | Push the selected tag to a remote. You'll be prompted to select a remote. |
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View commits | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Worktrees
+11 -10
View File
@@ -22,7 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` R `` | 更新 | Gitの状態を更新します(`git status`、`git branch`などをバックグラウンドで実行してパネルの内容を更新します)。これは`git fetch`を実行しません。 |
| `` + `` | 次の画面モード(通常/半分/全画面) | |
| `` _ `` | 前の画面モード | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` <esc> `` | キャンセル | |
| `` ? `` | キーバインディングメニューを開く | |
| `` <ctrl+s> `` | フィルターオプションを表示 | コミットログのフィルタリングオプションを表示し、フィルタに一致するコミットのみを表示します。 |
@@ -30,6 +31,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` q, <ctrl+c> `` | 終了 | |
| `` <ctrl+z> `` | Suspend the application | |
| `` <ctrl+w> `` | 空白表示の切り替え | Toggle whether or not whitespace changes are shown in the diff view.<br><br>The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. |
| `` <alt+shift+c> `` | 設定ファイルを編集 | 外部エディタでファイルを開きます。 |
| `` z `` | 元に戻す | 最後のgitコマンドを元に戻すために実行するgitコマンドを決定するためにreflogが使用されます。これにはワーキングツリーへの変更は含まれません。コミットのみが考慮されます。 |
| `` Z `` | やり直す | 最後のgitコマンドをやり直すために実行するgitコマンドを決定するためにreflogが使用されます。これにはワーキングツリーへの変更は含まれません。コミットのみが考慮されます。 |
@@ -90,13 +92,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | ブラウザでコミットを開く | |
| `` n `` | コミットから新しいブランチを作成 | |
| `` N `` | コミットを新しいブランチに移動 | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | 新しいワークツリー | |
| `` g `` | リセット | 選択した項目へのリセットオプション(ソフト/ミックス/ハード)を表示します。各リセットタイプの詳細は次の通りです:<br>- ソフトリセット:変更を保持し、ステージされた状態にします<br>- ミックスリセット:変更を保持し、ステージされていない状態にします<br>- ハードリセット:すべての変更を破棄します |
| `` C `` | コピー(チェリーピック) | コミットをコピーとしてマークします。ローカルコミットビューで `V` を押すと、コピーしたコミットをチェックアウトしたブランチにペースト(チェリーピック)できます。いつでも `<esc>` を押して選択をキャンセルできます。 |
| `` <ctrl+t> `` | 外部差分ツールを開く(git difftool) | |
| `` * `` | 現在のブランチのコミットを選択 | |
| `` 0 `` | メインビューにフォーカス | |
| `` <enter> `` | ファイルを表示 | |
| `` w `` | ワークツリーオプションを表示 | |
| `` / `` | 現在のビューをテキストで検索 | |
## コミットファイル
@@ -136,6 +138,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | ブラウザでコミットを開く | |
| `` n `` | コミットから新しいブランチを作成 | |
| `` N `` | コミットを新しいブランチに移動 | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | 新しいワークツリー | |
| `` g `` | リセット | 選択した項目へのリセットオプション(ソフト/ミックス/ハード)を表示します。各リセットタイプの詳細は次の通りです:<br>- ソフトリセット:変更を保持し、ステージされた状態にします<br>- ミックスリセット:変更を保持し、ステージされていない状態にします<br>- ハードリセット:すべての変更を破棄します |
| `` C `` | コピー(チェリーピック) | コミットをコピーとしてマークします。ローカルコミットビューで `V` を押すと、コピーしたコミットをチェックアウトしたブランチにペースト(チェリーピック)できます。いつでも `<esc>` を押して選択をキャンセルできます。 |
| `` <ctrl+r> `` | コピーされた(チェリーピックされた)コミットの選択をリセット | |
@@ -143,7 +146,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | 現在のブランチのコミットを選択 | |
| `` 0 `` | メインビューにフォーカス | |
| `` <enter> `` | ファイルを表示 | |
| `` w `` | ワークツリーオプションを表示 | |
| `` / `` | 現在のビューをテキストで検索 | |
## サブモジュール
@@ -168,17 +170,16 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` g `` | ポップ | スタッシュエントリをワーキングディレクトリに適用し、スタッシュエントリを削除します。 |
| `` d `` | 削除 | スタッシュリストからスタッシュエントリを削除します。 |
| `` n `` | 新しいブランチ | 選択したスタッシュエントリから新しいブランチを作成します。これは、スタッシュエントリが作成されたコミットをgitがチェックアウトし、そのコミットから新しいブランチを作成した後、スタッシュエントリを追加のコミットとして新しいブランチに適用することで機能します。 |
| `` w `` | 新しいワークツリー | |
| `` r `` | スタッシュの名前を変更 | |
| `` 0 `` | メインビューにフォーカス | |
| `` <enter> `` | ファイルを表示 | |
| `` w `` | ワークツリーオプションを表示 | |
| `` / `` | 現在のビューをテキストでフィルタリング | |
## ステータス
| Key | Action | Info |
|-----|--------|-------------|
| `` o `` | 設定ファイルを開く | デフォルトのアプリケーションでファイルを開きます。 |
| `` e `` | 設定ファイルを編集 | 外部エディタでファイルを開きます。 |
| `` u `` | 更新を確認 | |
| `` <enter> `` | 最近のリポジトリをチェックアウト | |
@@ -201,13 +202,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | タグをクリップボードにコピー | |
| `` <space> `` | チェックアウト(ブランチの切り替え) | 選択したタグをデタッチドHEADとしてチェックアウトします。 |
| `` n `` | 新しいタグを作成 | 現在のコミットから新しいタグを作成します。タグ名とオプションの説明を入力するよう促されます。 |
| `` w `` | 新しいワークツリー | |
| `` d `` | 削除 | ローカル/リモートタグの削除オプションを表示します。 |
| `` P `` | タグをプッシュ | 選択したタグをリモートにプッシュします。リモートを選択するよう促されます。 |
| `` g `` | リセット | 選択した項目へのリセットオプション(ソフト/ミックス/ハード)を表示します。各リセットタイプの詳細は次の通りです:<br>- ソフトリセット:変更を保持し、ステージされた状態にします<br>- ミックスリセット:変更を保持し、ステージされていない状態にします<br>- ハードリセット:すべての変更を破棄します |
| `` <ctrl+t> `` | 外部差分ツールを開く(git difftool) | |
| `` 0 `` | メインビューにフォーカス | |
| `` <enter> `` | コミットを表示 | |
| `` w `` | ワークツリーオプションを表示 | |
| `` / `` | 現在のビューをテキストでフィルタリング | |
## ファイル
@@ -286,7 +287,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <space> `` | ハンクを選択 | |
| `` b `` | すべてのハンクを選択 | |
| `` b `` | Pick both hunks | |
| `` <up>, k `` | 前のハンク | |
| `` <down>, j `` | 次のハンク | |
| `` <left>, h `` | 前のコンフリクト | |
@@ -325,6 +326,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | ブラウザでコミットを開く | |
| `` n `` | コミットから新しいブランチを作成 | |
| `` N `` | コミットを新しいブランチに移動 | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | 新しいワークツリー | |
| `` g `` | リセット | 選択した項目へのリセットオプション(ソフト/ミックス/ハード)を表示します。各リセットタイプの詳細は次の通りです:<br>- ソフトリセット:変更を保持し、ステージされた状態にします<br>- ミックスリセット:変更を保持し、ステージされていない状態にします<br>- ハードリセット:すべての変更を破棄します |
| `` C `` | コピー(チェリーピック) | コミットをコピーとしてマークします。ローカルコミットビューで `V` を押すと、コピーしたコミットをチェックアウトしたブランチにペースト(チェリーピック)できます。いつでも `<esc>` を押して選択をキャンセルできます。 |
| `` <ctrl+r> `` | コピーされた(チェリーピックされた)コミットの選択をリセット | |
@@ -332,7 +334,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | 現在のブランチのコミットを選択 | |
| `` 0 `` | メインビューにフォーカス | |
| `` <enter> `` | コミットを表示 | |
| `` w `` | ワークツリーオプションを表示 | |
| `` / `` | 現在のビューをテキストでフィルタリング | |
## リモート
@@ -354,6 +355,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | ブランチ名をクリップボードにコピー | |
| `` <space> `` | チェックアウト(ブランチの切り替え) | 選択したリモートブランチに基づいて新しいローカルブランチをチェックアウトするか、リモートブランチをデタッチドヘッドとしてチェックアウトします。 |
| `` n `` | 新しいブランチ | |
| `` w `` | 新しいワークツリー | |
| `` M `` | マージ | 選択した項目を現在のブランチにマージするためのオプションを表示します(通常のマージ、スカッシュマージ) |
| `` r `` | リベース | チェックアウトしたブランチを選択したブランチ上にリベースします。 |
| `` d `` | 削除 | リモートからリモートブランチを削除します。 |
@@ -363,7 +365,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | 外部差分ツールを開く(git difftool) | |
| `` 0 `` | メインビューにフォーカス | |
| `` <enter> `` | コミットを表示 | |
| `` w `` | ワークツリーオプションを表示 | |
| `` / `` | 現在のビューをテキストでフィルタリング | |
## ローカルブランチ
@@ -375,6 +376,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <space> `` | チェックアウト(ブランチの切り替え) | 選択した項目をチェックアウトします。 |
| `` n `` | 新しいブランチ | |
| `` N `` | コミットを新しいブランチに移動 | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | 新しいワークツリー | |
| `` o `` | プルリクエストを作成 | |
| `` O `` | プルリクエスト作成オプションを表示 | |
| `` G `` | Open pull request in browser | |
@@ -394,7 +396,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | 外部差分ツールを開く(git difftool) | |
| `` 0 `` | メインビューにフォーカス | |
| `` <enter> `` | コミットを表示 | |
| `` w `` | ワークツリーオプションを表示 | |
| `` / `` | 現在のビューをテキストでフィルタリング | |
## ワークツリー
+11 -10
View File
@@ -22,7 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` R `` | 새로고침 | Refresh the git state (i.e. run `git status`, `git branch`, etc in background to update the contents of panels). This does not run `git fetch`. |
| `` + `` | 다음 스크린 모드 (normal/half/fullscreen) | |
| `` _ `` | 이전 스크린 모드 | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` <esc> `` | 취소 | |
| `` ? `` | 매뉴 열기 | |
| `` <ctrl+s> `` | View filter-by-path options | View options for filtering the commit log, so that only commits matching the filter are shown. |
@@ -30,6 +31,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` q, <ctrl+c> `` | 종료 | |
| `` <ctrl+z> `` | Suspend the application | |
| `` <ctrl+w> `` | 공백문자를 Diff 뷰에서 표시 여부 전환 | Toggle whether or not whitespace changes are shown in the diff view.<br><br>The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. |
| `` <alt+shift+c> `` | 설정 파일 수정 | Open file in external editor. |
| `` z `` | 되돌리기 (reflog) (실험적) | The reflog will be used to determine what git command to run to undo the last git command. This does not include changes to the working tree; only commits are taken into consideration. |
| `` Z `` | 다시 실행 (reflog) (실험적) | The reflog will be used to determine what git command to run to redo the last git command. This does not include changes to the working tree; only commits are taken into consideration. |
@@ -67,6 +69,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | 브라우저에서 커밋 열기 | |
| `` n `` | 커밋에서 새 브랜치를 만듭니다. | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` g `` | View reset options | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | 커밋을 복사 (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <ctrl+r> `` | Reset cherry-picked (copied) commits selection | |
@@ -74,7 +77,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 커밋 보기 | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Secondary
@@ -93,10 +95,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` g `` | Pop | Apply the stash entry to your working directory and remove the stash entry. |
| `` d `` | Drop | Remove the stash entry from the stash list. |
| `` n `` | 새 브랜치 생성 | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
| `` w `` | New worktree | |
| `` r `` | Rename stash | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View selected item's files | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Sub-commits
@@ -109,6 +111,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | 브라우저에서 커밋 열기 | |
| `` n `` | 커밋에서 새 브랜치를 만듭니다. | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` g `` | View reset options | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | 커밋을 복사 (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <ctrl+r> `` | Reset cherry-picked (copied) commits selection | |
@@ -116,7 +119,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View selected item's files | |
| `` w `` | View worktree options | |
| `` / `` | 검색 시작 | |
## Worktrees
@@ -142,7 +144,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <space> `` | Pick hunk | |
| `` b `` | Pick all hunks | |
| `` b `` | Pick both hunks | |
| `` <up>, k `` | 이전 hunk를 선택 | |
| `` <down>, j `` | 다음 hunk를 선택 | |
| `` <left>, h `` | 이전 충돌을 선택 | |
@@ -210,6 +212,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <space> `` | 체크아웃 | Checkout selected item. |
| `` n `` | 새 브랜치 생성 | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` o `` | 풀 리퀘스트 생성 | |
| `` O `` | 풀 리퀘스트 생성 옵션 | |
| `` G `` | Open pull request in browser | |
@@ -229,14 +232,12 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 커밋 보기 | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## 상태
| Key | Action | Info |
|-----|--------|-------------|
| `` o `` | 설정 파일 열기 | Open file in default application. |
| `` e `` | 설정 파일 수정 | Open file in external editor. |
| `` u `` | 업데이트 확인 | |
| `` <enter> `` | 최근에 사용한 저장소로 전환 | |
@@ -277,6 +278,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | 브랜치명을 클립보드에 복사 | |
| `` <space> `` | 체크아웃 | Checkout a new local branch based on the selected remote branch, or the remote branch as a detached head. |
| `` n `` | 새 브랜치 생성 | |
| `` w `` | New worktree | |
| `` M `` | 현재 브랜치에 병합 | View options for merging the selected item into the current branch (regular merge, squash merge) |
| `` r `` | 체크아웃된 브랜치를 이 브랜치에 리베이스 | Rebase the checked-out branch onto the selected branch. |
| `` d `` | 삭제 | Delete the remote branch from the remote. |
@@ -286,7 +288,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 커밋 보기 | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## 커밋
@@ -322,13 +323,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | 브라우저에서 커밋 열기 | |
| `` n `` | 커밋에서 새 브랜치를 만듭니다. | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` g `` | View reset options | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | 커밋을 복사 (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View selected item's files | |
| `` w `` | View worktree options | |
| `` / `` | 검색 시작 | |
## 커밋 파일
@@ -365,13 +366,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | Copy tag to clipboard | |
| `` <space> `` | 체크아웃 | Checkout the selected tag as a detached HEAD. |
| `` n `` | 태그를 생성 | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. |
| `` w `` | New worktree | |
| `` d `` | 삭제 | View delete options for local/remote tag. |
| `` P `` | 태그를 push | Push the selected tag to a remote. You'll be prompted to select a remote. |
| `` g `` | 초기화 | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 커밋 보기 | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## 파일
+71 -70
View File
@@ -10,26 +10,28 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <pgup>, K, <ctrl+u> (fn+up/shift+k) `` | Scroll naar beneden vanaf hoofdpaneel | |
| `` <pgdown>, J, <ctrl+d> (fn+down/shift+j) `` | Scroll naar beneden vanaf hoofdpaneel | |
| `` @ `` | View command log options | View options for the command log e.g. show/hide the command log and focus the command log. |
| `` P `` | Push | Push the current branch to its upstream branch. If no upstream is configured, you will be prompted to configure an upstream branch. |
| `` p `` | Pull | Pull changes from the remote for the current branch. If no upstream is configured, you will be prompted to configure an upstream branch. |
| `` P `` | Push | Push de huidige branch naar de bijbehorende upstream-branch. Als er geen upstream is geconfigureerd wordt er gevraagd om een upstream-branch te configureren. |
| `` p `` | Pull | Pull wijzigingen van de remote voor de huidige branch. Als er geen upstream is geconfigureerd wordt er gevraagd om een upstream-branch te configureren. |
| `` ) `` | Increase rename similarity threshold | Increase the similarity threshold for a deletion and addition pair to be treated as a rename.<br><br>The default can be changed in the config file with the key 'git.renameSimilarityThreshold'. |
| `` ( `` | Decrease rename similarity threshold | Decrease the similarity threshold for a deletion and addition pair to be treated as a rename.<br><br>The default can be changed in the config file with the key 'git.renameSimilarityThreshold'. |
| `` } `` | Increase diff context size | Increase the amount of the context shown around changes in the diff view.<br><br>The default can be changed in the config file with the key 'git.diffContextSize'. |
| `` { `` | Decrease diff context size | Decrease the amount of the context shown around changes in the diff view.<br><br>The default can be changed in the config file with the key 'git.diffContextSize'. |
| `` : `` | Execute shell command | Bring up a prompt where you can enter a shell command to execute. |
| `` <ctrl+p> `` | Bekijk aangepaste patch opties | |
| `` m `` | Bekijk merge/rebase opties | View options to abort/continue/skip the current merge/rebase. |
| `` m `` | Bekijk merge/rebase opties | Toon abort/continue/skip opties voor huidige merge/rebase. |
| `` R `` | Verversen | Refresh the git state (i.e. run `git status`, `git branch`, etc in background to update the contents of panels). This does not run `git fetch`. |
| `` + `` | Volgende scherm modus (normaal/half/groot) | |
| `` _ `` | Vorige scherm modus | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` <esc> `` | Annuleren | |
| `` ? `` | Open menu | |
| `` <ctrl+s> `` | Bekijk scoping opties | View options for filtering the commit log, so that only commits matching the filter are shown. |
| `` W, <ctrl+e> `` | Open diff menu | View options relating to diffing two refs e.g. diffing against selected ref, entering ref to diff against, and reversing the diff direction. |
| `` q, <ctrl+c> `` | Quit | |
| `` <ctrl+z> `` | Suspend the application | |
| `` q, <ctrl+c> `` | Afsluiten | |
| `` <ctrl+z> `` | Pauzeer de applicatie | |
| `` <ctrl+w> `` | Toggle whitespace | Toggle whether or not whitespace changes are shown in the diff view.<br><br>The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. |
| `` <alt+shift+c> `` | Verander config bestand | Open bestand in externe editor. |
| `` z `` | Ongedaan maken (via reflog) (experimenteel) | The reflog will be used to determine what git command to run to undo the last git command. This does not include changes to the working tree; only commits are taken into consideration. |
| `` Z `` | Redo (via reflog) (experimenteel) | The reflog will be used to determine what git command to run to redo the last git command. This does not include changes to the working tree; only commits are taken into consideration. |
@@ -45,8 +47,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <shift+down> `` | Range select down | |
| `` <shift+up> `` | Range select up | |
| `` / `` | Start met zoeken | |
| `` H `` | Scroll left | |
| `` L `` | Scroll right | |
| `` H `` | Scroll naar links | |
| `` L `` | Scroll naar rechts | |
| `` ] `` | Volgende tabblad | |
| `` [ `` | Vorige tabblad | |
@@ -56,15 +58,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|-----|--------|-------------|
| `` <ctrl+o> `` | Kopieer de bestandsnaam naar het klembord | |
| `` <space> `` | Toggle staged | Toggle staged for selected file. |
| `` <ctrl+b> `` | Filter files by status | |
| `` y `` | Copy to clipboard | |
| `` c `` | Commit veranderingen | Commit staged changes. |
| `` <ctrl+b> `` | Filter bestanden op status | |
| `` y `` | Kopieer naar klembord | |
| `` c `` | Commit veranderingen | Commit gestagede wijzigingen. |
| `` w `` | Commit veranderingen zonder pre-commit hook | |
| `` A `` | Wijzig laatste commit | |
| `` C `` | Commit veranderingen met de git editor | |
| `` <ctrl+f> `` | Find base commit for fixup | Find the commit that your current changes are building upon, for the sake of amending/fixing up the commit. This spares you from having to look through your branch's commits one-by-one to see which commit should be amended/fixed up. See docs: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
| `` e `` | Edit | Open file in external editor. |
| `` o `` | Open bestand | Open file in default application. |
| `` <ctrl+f> `` | Find base commit for fixup | Vind de commit waar je huidige wijzigingen bovenop zijn gebouwd met als doel die commit te amenden/fixen. Hierdoor hoef je dit niet met de hand te doen. Zie: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
| `` e `` | Edit | Open bestand in externe editor. |
| `` o `` | Open bestand | Open bestand in standaardapplicatie. |
| `` i `` | Ignore or exclude file | |
| `` r `` | Refresh bestanden | |
| `` s `` | Stash | Stash all changes. For other variations of stashing, use the view stash options keybinding. |
@@ -73,13 +75,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <enter> `` | Stage individuele hunks/lijnen | If the selected item is a file, focus the staging view so you can stage individual hunks/lines. If the selected item is a directory, collapse/expand it. |
| `` d `` | Bekijk 'veranderingen ongedaan maken' opties | View options for discarding changes to the selected file. |
| `` g `` | Bekijk upstream reset opties | |
| `` D `` | Reset | View reset options for working tree (e.g. nuking the working tree). |
| `` D `` | Resetten | View reset options for working tree (e.g. nuking the working tree). |
| `` ` `` | Toggle bestandsboom weergave | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.<br><br>The default can be changed in the config file with the key 'gui.showFileTree'. |
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
| `` M `` | Bekijk merge conflict opties | Bekijk opties voor het oplossen van mergeconflicten. |
| `` f `` | Fetch | Fetch changes from remote. |
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` = `` | Vouw alle bestanden uit | Vouw alle mappen in de bestandsstructuur uit |
| `` 0 `` | Focus main view | |
| `` / `` | Filter the current view by text | |
@@ -89,7 +91,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|-----|--------|-------------|
| `` <enter> `` | Bevestig | |
| `` <esc> `` | Sluiten | |
| `` <ctrl+o> `` | Copy to clipboard | |
| `` <ctrl+o> `` | Kopieer naar klembord | |
## Branches
@@ -97,18 +99,19 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|-----|--------|-------------|
| `` <ctrl+o> `` | Kopieer branch name naar klembord | |
| `` i `` | Laat git-flow opties zien | |
| `` <space> `` | Uitchecken | Checkout selected item. |
| `` <space> `` | Uitchecken | Geselecteerd item uitchecken. |
| `` n `` | Nieuwe branch | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` N `` | Verplaats commits naar nieuwe branch | Maak een nieuwe branch en verplaats niet-gepushte commits van de huidige branch hier naar toe. Gebruik dit in het geval dat je deze commits eigenlijk op een nieuwe branch had willen maken.<br><br>Let op dat de selectie genegeerd wordt. De nieuwe branch komt ofwel bovenop de main branch, of bovenop de huidige branch (je kan kiezen). |
| `` w `` | New worktree | |
| `` o `` | Maak een pull-request | |
| `` O `` | Bekijk opties voor pull-aanvraag | |
| `` G `` | Open pull request in browser | |
| `` <ctrl+y> `` | Kopieer de URL van het pull-verzoek naar het klembord | |
| `` c `` | Uitchecken bij naam | Checkout by name. In the input box you can enter '-' to switch to the previous branch. |
| `` - `` | Checkout previous branch | |
| `` - `` | Vorige branch uitchecken | |
| `` F `` | Forceer checkout | Force checkout selected branch. This will discard all local changes in your working directory before checking out the selected branch. |
| `` d `` | Delete | View delete options for local/remote branch. |
| `` r `` | Rebase branch | Rebase the checked-out branch onto the selected branch. |
| `` d `` | Verwijderen | View delete options for local/remote branch. |
| `` r `` | Rebase branch | Rebase de uitgecheckte branch bovenop de geselecteerde branch. |
| `` M `` | Merge in met huidige checked out branch | View options for merging the selected item into the current branch (regular merge, squash merge) |
| `` f `` | Fast-forward deze branch vanaf zijn upstream | Fast-forward selected branch from its upstream. |
| `` T `` | Creëer tag | |
@@ -116,10 +119,9 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` g `` | Bekijk reset opties | |
| `` R `` | Hernoem branch | |
| `` u `` | View upstream options | View options relating to the branch's upstream e.g. setting/unsetting the upstream and resetting to the upstream. |
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Bekijk commits | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Commit bericht
@@ -134,18 +136,18 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <ctrl+o> `` | Kopieer de bestandsnaam naar het klembord | |
| `` y `` | Copy to clipboard | |
| `` y `` | Kopieer naar klembord | |
| `` c `` | Uitchecken | Bestand uitchecken |
| `` d `` | Bekijk 'veranderingen ongedaan maken' opties | Uitsluit deze commit zijn veranderingen aan dit bestand |
| `` o `` | Open bestand | Open file in default application. |
| `` e `` | Edit | Open file in external editor. |
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` o `` | Open bestand | Open bestand in standaardapplicatie. |
| `` e `` | Edit | Open bestand in externe editor. |
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
| `` <space> `` | Toggle bestand inbegrepen in patch | Toggle whether the file is included in the custom patch. See https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
| `` a `` | Toggle all files | Add/remove all commit's files to custom patch. See https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
| `` <enter> `` | Enter bestand om geselecteerde regels toe te voegen aan de patch | If a file is selected, enter the file so that you can add/remove individual lines to the custom patch. If a directory is selected, toggle the directory. |
| `` ` `` | Toggle bestandsboom weergave | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.<br><br>The default can be changed in the config file with the key 'gui.showFileTree'. |
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` = `` | Vouw alle bestanden uit | Vouw alle mappen in de bestandsstructuur uit |
| `` 0 `` | Focus main view | |
| `` / `` | Filter the current view by text | |
@@ -181,14 +183,14 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Open commit in browser | |
| `` n `` | Creëer nieuwe branch van commit | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` N `` | Verplaats commits naar nieuwe branch | Maak een nieuwe branch en verplaats niet-gepushte commits van de huidige branch hier naar toe. Gebruik dit in het geval dat je deze commits eigenlijk op een nieuwe branch had willen maken.<br><br>Let op dat de selectie genegeerd wordt. De nieuwe branch komt ofwel bovenop de main branch, of bovenop de huidige branch (je kan kiezen). |
| `` w `` | New worktree | |
| `` g `` | Bekijk reset opties | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | Kopieer commit (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Bekijk gecommite bestanden | |
| `` w `` | View worktree options | |
| `` / `` | Start met zoeken | |
## Input prompt
@@ -211,15 +213,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <space> `` | Kies stuk | |
| `` b `` | Kies beide stukken | |
| `` b `` | Pick both hunks | |
| `` <up>, k `` | Selecteer bovenste hunk | |
| `` <down>, j `` | Selecteer onderste hunk | |
| `` <left>, h `` | Selecteer voorgaand conflict | |
| `` <right>, l `` | Selecteer volgende conflict | |
| `` z `` | Ongedaan maken | Undo last merge conflict resolution. |
| `` e `` | Verander bestand | Open file in external editor. |
| `` o `` | Open bestand | Open file in default application. |
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
| `` e `` | Verander bestand | Open bestand in externe editor. |
| `` o `` | Open bestand | Open bestand in standaardapplicatie. |
| `` M `` | Bekijk merge conflict opties | Bekijk opties voor het oplossen van mergeconflicten. |
| `` <esc> `` | Ga terug naar het bestanden paneel | |
## Normaal
@@ -241,8 +243,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` v `` | Toggle drag selecteer | |
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
| `` <ctrl+o> `` | Copy selected text to clipboard | |
| `` o `` | Open bestand | Open file in default application. |
| `` e `` | Verander bestand | Open file in external editor. |
| `` o `` | Open bestand | Open bestand in standaardapplicatie. |
| `` e `` | Verander bestand | Open bestand in externe editor. |
| `` <space> `` | Voeg toe/verwijder lijn(en) in patch | |
| `` d `` | Remove lines from commit | Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines. |
| `` <esc> `` | Sluit lijn-bij-lijn modus | |
@@ -257,15 +259,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Open commit in browser | |
| `` n `` | Creëer nieuwe branch van commit | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` N `` | Verplaats commits naar nieuwe branch | Maak een nieuwe branch en verplaats niet-gepushte commits van de huidige branch hier naar toe. Gebruik dit in het geval dat je deze commits eigenlijk op een nieuwe branch had willen maken.<br><br>Let op dat de selectie genegeerd wordt. De nieuwe branch komt ofwel bovenop de main branch, of bovenop de huidige branch (je kan kiezen). |
| `` w `` | New worktree | |
| `` g `` | Bekijk reset opties | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | Kopieer commit (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <ctrl+r> `` | Reset cherry-picked (gekopieerde) commits selectie | |
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Bekijk commits | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Remote branches
@@ -273,27 +275,27 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <ctrl+o> `` | Kopieer branch name naar klembord | |
| `` <space> `` | Uitchecken | Checkout a new local branch based on the selected remote branch, or the remote branch as a detached head. |
| `` <space> `` | Uitchecken | Geselecteerde remote branch uitchecken als nieuwe locale branch of als detached head. |
| `` n `` | Nieuwe branch | |
| `` w `` | New worktree | |
| `` M `` | Merge in met huidige checked out branch | View options for merging the selected item into the current branch (regular merge, squash merge) |
| `` r `` | Rebase branch | Rebase the checked-out branch onto the selected branch. |
| `` d `` | Delete | Delete the remote branch from the remote. |
| `` u `` | Set as upstream | Stel in als upstream van uitgecheckte branch |
| `` r `` | Rebase branch | Rebase de uitgecheckte branch bovenop de geselecteerde branch. |
| `` d `` | Verwijderen | Delete the remote branch from the remote. |
| `` u `` | Instellen als upstream | Stel in als upstream van uitgecheckte branch |
| `` s `` | Sort order | |
| `` g `` | Bekijk reset opties | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Bekijk commits | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Remotes
| Key | Action | Info |
|-----|--------|-------------|
| `` <enter> `` | View branches | |
| `` <enter> `` | Bekijk branches | |
| `` n `` | Voeg een nieuwe remote toe | |
| `` d `` | Remove | Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected. |
| `` d `` | Verwijderen | Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected. |
| `` e `` | Edit | Wijzig remote |
| `` f `` | Fetch | Fetch remote |
| `` F `` | Add fork remote | Quickly add a fork remote by replacing the owner in the origin URL and optionally check out a branch from new remote. |
@@ -318,15 +320,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | Copy selected text to clipboard | |
| `` <space> `` | Toggle staged | Toggle lijnen staged / unstaged |
| `` d `` | Verwijdert change (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
| `` o `` | Open bestand | Open file in default application. |
| `` e `` | Verander bestand | Open file in external editor. |
| `` o `` | Open bestand | Open bestand in standaardapplicatie. |
| `` e `` | Verander bestand | Open bestand in externe editor. |
| `` <esc> `` | Ga terug naar het bestanden paneel | |
| `` <tab> `` | Ga naar een ander paneel | Switch to other view (staged/unstaged changes). |
| `` E `` | Edit hunk | Edit selected hunk in external editor. |
| `` c `` | Commit veranderingen | Commit staged changes. |
| `` c `` | Commit veranderingen | Commit gestagede wijzigingen. |
| `` w `` | Commit veranderingen zonder pre-commit hook | |
| `` C `` | Commit veranderingen met de git editor | |
| `` <ctrl+f> `` | Find base commit for fixup | Find the commit that your current changes are building upon, for the sake of amending/fixing up the commit. This spares you from having to look through your branch's commits one-by-one to see which commit should be amended/fixed up. See docs: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
| `` <ctrl+f> `` | Find base commit for fixup | Vind de commit waar je huidige wijzigingen bovenop zijn gebouwd met als doel die commit te amenden/fixen. Hierdoor hoef je dit niet met de hand te doen. Zie: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
| `` / `` | Start met zoeken | |
## Stash
@@ -337,18 +339,17 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` g `` | Pop | Apply the stash entry to your working directory and remove the stash entry. |
| `` d `` | Laten vallen | Remove the stash entry from the stash list. |
| `` n `` | Nieuwe branch | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
| `` r `` | Rename stash | |
| `` w `` | New worktree | |
| `` r `` | Hernoem stash | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Bekijk gecommite bestanden | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Status
| Key | Action | Info |
|-----|--------|-------------|
| `` o `` | Open config bestand | Open file in default application. |
| `` e `` | Verander config bestand | Open file in external editor. |
| `` e `` | Verander config bestand | Open bestand in externe editor. |
| `` u `` | Check voor updates | |
| `` <enter> `` | Wissel naar een recente repo | |
| `` a `` | Show/cycle all branch logs | |
@@ -364,15 +365,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Open commit in browser | |
| `` n `` | Creëer nieuwe branch van commit | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` N `` | Verplaats commits naar nieuwe branch | Maak een nieuwe branch en verplaats niet-gepushte commits van de huidige branch hier naar toe. Gebruik dit in het geval dat je deze commits eigenlijk op een nieuwe branch had willen maken.<br><br>Let op dat de selectie genegeerd wordt. De nieuwe branch komt ofwel bovenop de main branch, of bovenop de huidige branch (je kan kiezen). |
| `` w `` | New worktree | |
| `` g `` | Bekijk reset opties | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | Kopieer commit (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <ctrl+r> `` | Reset cherry-picked (gekopieerde) commits selectie | |
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Bekijk gecommite bestanden | |
| `` w `` | View worktree options | |
| `` / `` | Start met zoeken | |
## Submodules
@@ -381,7 +382,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|-----|--------|-------------|
| `` <ctrl+o> `` | Kopieer submodule naam naar klembord | |
| `` <enter> `` | Enter | Enter submodule |
| `` d `` | Remove | Remove the selected submodule and its corresponding directory. |
| `` d `` | Verwijderen | Remove the selected submodule and its corresponding directory. |
| `` u `` | Update | Update selected submodule. |
| `` n `` | Voeg nieuwe submodule toe | |
| `` e `` | Update submodule URL | |
@@ -394,15 +395,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <ctrl+o> `` | Copy tag to clipboard | |
| `` <space> `` | Uitchecken | Checkout the selected tag as a detached HEAD. |
| `` <space> `` | Uitchecken | Geselecteerde tag uitchecken als detached HEAD. |
| `` n `` | Creëer tag | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. |
| `` d `` | Delete | View delete options for local/remote tag. |
| `` w `` | New worktree | |
| `` d `` | Verwijderen | View delete options for local/remote tag. |
| `` P `` | Push tag | Push the selected tag to a remote. You'll be prompted to select a remote. |
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` g `` | Resetten | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Bekijk commits | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Worktrees
@@ -411,6 +412,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|-----|--------|-------------|
| `` n `` | New worktree | |
| `` <space> `` | Switch | Switch to the selected worktree. |
| `` o `` | Open in editor | |
| `` d `` | Remove | Remove the selected worktree. This will both delete the worktree's directory, as well as metadata about the worktree in the .git directory. |
| `` o `` | Openen in editor | |
| `` d `` | Verwijderen | Remove the selected worktree. This will both delete the worktree's directory, as well as metadata about the worktree in the .git directory. |
| `` / `` | Filter the current view by text | |
+11 -10
View File
@@ -22,7 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` R `` | Odśwież | Odśwież stan git (tj. uruchom `git status`, `git branch`, itp. w tle, aby zaktualizować zawartość paneli). To nie uruchamia `git fetch`. |
| `` + `` | Następny tryb ekranu (normalny/półpełny/pełnoekranowy) | |
| `` _ `` | Poprzedni tryb ekranu | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` <esc> `` | Anuluj | |
| `` ? `` | Otwórz menu przypisań klawiszy | |
| `` <ctrl+s> `` | Pokaż opcje filtrowania | Pokaż opcje filtrowania dziennika commitów, tak aby pokazywane były tylko commity pasujące do filtra. |
@@ -30,6 +31,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` q, <ctrl+c> `` | Wyjdź | |
| `` <ctrl+z> `` | Suspend the application | |
| `` <ctrl+w> `` | Przełącz białe znaki | Toggle whether or not whitespace changes are shown in the diff view.<br><br>The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. |
| `` <alt+shift+c> `` | Edytuj plik konfiguracyjny | Otwórz plik w zewnętrznym edytorze. |
| `` z `` | Cofnij | Dziennik reflog zostanie użyty do określenia, jakie polecenie git należy uruchomić, aby cofnąć ostatnie polecenie git. Nie obejmuje to zmian w drzewie roboczym; brane są pod uwagę tylko commity. |
| `` Z `` | Ponów | Dziennik reflog zostanie użyty do określenia, jakie polecenie git należy uruchomić, aby ponowić ostatnie polecenie git. Nie obejmuje to zmian w drzewie roboczym; brane są pod uwagę tylko commity. |
@@ -83,13 +85,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | Otwórz commit w przeglądarce | |
| `` n `` | Utwórz nową gałąź z commita | |
| `` N `` | Przenieś commity do nowej gałęzi | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | Nowe drzewo pracy | |
| `` g `` | Reset | Wyświetl opcje resetu (miękki/mieszany/twardy) do wybranego elementu. |
| `` C `` | Kopiuj (cherry-pick) | Oznacz commit jako skopiowany. Następnie, w widoku lokalnych commitów, możesz nacisnąć `V`, aby wkleić (cherry-pick) skopiowane commity do sprawdzonej gałęzi. W dowolnym momencie możesz nacisnąć `<esc>`, aby anulować zaznaczenie. |
| `` <ctrl+t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Wyświetl pliki | |
| `` w `` | Zobacz opcje drzewa pracy | |
| `` / `` | Szukaj w bieżącym widoku po tekście | |
## Dodatkowy
@@ -120,6 +122,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | Otwórz commit w przeglądarce | |
| `` n `` | Utwórz nową gałąź z commita | |
| `` N `` | Przenieś commity do nowej gałęzi | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | Nowe drzewo pracy | |
| `` g `` | Reset | Wyświetl opcje resetu (miękki/mieszany/twardy) do wybranego elementu. |
| `` C `` | Kopiuj (cherry-pick) | Oznacz commit jako skopiowany. Następnie, w widoku lokalnych commitów, możesz nacisnąć `V`, aby wkleić (cherry-pick) skopiowane commity do sprawdzonej gałęzi. W dowolnym momencie możesz nacisnąć `<esc>`, aby anulować zaznaczenie. |
| `` <ctrl+r> `` | Resetuj wybrane (cherry-picked) commity | |
@@ -127,7 +130,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Pokaż commity | |
| `` w `` | Zobacz opcje drzewa pracy | |
| `` / `` | Filtruj bieżący widok po tekście | |
## Główny panel (budowanie łatki)
@@ -162,6 +164,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <space> `` | Przełącz | Przełącz wybrany element. |
| `` n `` | Nowa gałąź | |
| `` N `` | Przenieś commity do nowej gałęzi | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | Nowe drzewo pracy | |
| `` o `` | Utwórz żądanie ściągnięcia | |
| `` O `` | Zobacz opcje tworzenia pull requesta | |
| `` G `` | Otwórz żądanie ściągnięcia w przeglądarce | |
@@ -181,7 +184,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Pokaż commity | |
| `` w `` | Zobacz opcje drzewa pracy | |
| `` / `` | Filtruj bieżący widok po tekście | |
## Menu
@@ -207,7 +209,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <space> `` | Wybierz fragment | |
| `` b `` | Wybierz wszystkie fragmenty | |
| `` b `` | Pick both hunks | |
| `` <up>, k `` | Poprzedni fragment | |
| `` <down>, j `` | Następny fragment | |
| `` <left>, h `` | Poprzedni konflikt | |
@@ -316,17 +318,16 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` g `` | Wyciągnij | Zastosuj wpis schowka do katalogu roboczego i usuń wpis schowka. |
| `` d `` | Usuń | Usuń wpis schowka z listy schowka. |
| `` n `` | Nowa gałąź | Utwórz nową gałąź z wybranego wpisu schowka. Działa poprzez przełączenie git na commit, na którym wpis schowka został utworzony, tworzenie nowej gałęzi z tego commita, a następnie zastosowanie wpisu schowka do nowej gałęzi jako dodatkowego commita. |
| `` w `` | Nowe drzewo pracy | |
| `` r `` | Zmień nazwę schowka | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Wyświetl pliki | |
| `` w `` | Zobacz opcje drzewa pracy | |
| `` / `` | Filtruj bieżący widok po tekście | |
## Status
| Key | Action | Info |
|-----|--------|-------------|
| `` o `` | Otwórz plik konfiguracyjny | Otwórz plik w domyślnej aplikacji. |
| `` e `` | Edytuj plik konfiguracyjny | Otwórz plik w zewnętrznym edytorze. |
| `` u `` | Sprawdź aktualizacje | |
| `` <enter> `` | Przełącz na ostatnie repozytorium | |
@@ -344,6 +345,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | Otwórz commit w przeglądarce | |
| `` n `` | Utwórz nową gałąź z commita | |
| `` N `` | Przenieś commity do nowej gałęzi | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | Nowe drzewo pracy | |
| `` g `` | Reset | Wyświetl opcje resetu (miękki/mieszany/twardy) do wybranego elementu. |
| `` C `` | Kopiuj (cherry-pick) | Oznacz commit jako skopiowany. Następnie, w widoku lokalnych commitów, możesz nacisnąć `V`, aby wkleić (cherry-pick) skopiowane commity do sprawdzonej gałęzi. W dowolnym momencie możesz nacisnąć `<esc>`, aby anulować zaznaczenie. |
| `` <ctrl+r> `` | Resetuj wybrane (cherry-picked) commity | |
@@ -351,7 +353,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Wyświetl pliki | |
| `` w `` | Zobacz opcje drzewa pracy | |
| `` / `` | Szukaj w bieżącym widoku po tekście | |
## Submoduły
@@ -375,13 +376,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | Skopiuj tag do schowka | |
| `` <space> `` | Przełącz | Przełącz wybrany tag jako odłączoną głowę (detached HEAD). |
| `` n `` | Nowy tag | Utwórz nowy tag z bieżącego commita. Zostaniesz poproszony o wprowadzenie nazwy tagu i opcjonalnego opisu. |
| `` w `` | Nowe drzewo pracy | |
| `` d `` | Usuń | Wyświetl opcje usuwania lokalnego/odległego tagu. |
| `` P `` | Wyślij tag | Wyślij wybrany tag do zdalnego. Zostaniesz poproszony o wybranie zdalnego. |
| `` g `` | Reset | Wyświetl opcje resetu (miękki/mieszany/twardy) do wybranego elementu. |
| `` <ctrl+t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Pokaż commity | |
| `` w `` | Zobacz opcje drzewa pracy | |
| `` / `` | Filtruj bieżący widok po tekście | |
## Zdalne
@@ -403,6 +404,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | Kopiuj nazwę gałęzi do schowka | |
| `` <space> `` | Przełącz | Przełącz na nową lokalną gałąź na podstawie wybranej gałęzi zdalnej. Nowa gałąź będzie śledzić gałąź zdalną. |
| `` n `` | Nowa gałąź | |
| `` w `` | Nowe drzewo pracy | |
| `` M `` | Scal | Scal wybraną gałąź z aktualnie sprawdzoną gałęzią. |
| `` r `` | Przebazuj | Przebazuj przełączoną gałąź na wybraną gałąź. |
| `` d `` | Usuń | Usuń gałąź zdalną ze zdalnego. |
@@ -412,5 +414,4 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Pokaż commity | |
| `` w `` | Zobacz opcje drzewa pracy | |
| `` / `` | Filtruj bieżący widok po tekście | |
+11 -10
View File
@@ -22,7 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` R `` | Atualizar | Atualize o estado do git (ou seja, execute `git status`, `git branch`, etc em segundo plano para atualizar o conteúdo de painéis). Isso não executa `git fetch`. |
| `` + `` | Modo de tela seguinte (normal/metade/tela cheia) | |
| `` _ `` | Modo de tela anterior | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` <esc> `` | Cancelar | |
| `` ? `` | Abrir o menu de atalhos do teclado | |
| `` <ctrl+s> `` | Ver opções de filtro | View options for filtering the commit log, so that only commits matching the filter are shown. |
@@ -30,6 +31,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` q, <ctrl+c> `` | Sair | |
| `` <ctrl+z> `` | Suspender a aplicação | |
| `` <ctrl+w> `` | Toggle whitespace | Toggle whether or not whitespace changes are shown in the diff view.<br><br>The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. |
| `` <alt+shift+c> `` | Editar arquivo de configuração | Abrir arquivo no editor externo. |
| `` z `` | Desfazer | O reflog será usado para determinar qual comando git para executar para desfazer o último comando git. Isto não inclui mudanças na árvore de trabalho; apenas compromissos são tidos em consideração. |
| `` Z `` | Refazer | O reflog será usado para determinar qual comando git para executar para refazer o último comando git. Isto não inclui mudanças na árvore de trabalho; apenas compromissos são tidos em consideração. |
@@ -92,6 +94,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <space> `` | Verificar | Checar item selecionado |
| `` n `` | Nova branch | |
| `` N `` | Mover commits para uma nova branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | Nova árvore de trabalho | |
| `` o `` | Criar solicitação de pull | |
| `` O `` | View create pull request options | |
| `` G `` | Open pull request in browser | |
@@ -111,7 +114,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | Abrir ferramenta de diff externa (git difftool) | |
| `` 0 `` | Focar visualização principal | |
| `` <enter> `` | Ver commits | |
| `` w `` | Ver opções da árvore de trabalho | |
| `` / `` | Filtrar a visualização atual por texto | |
## Branches remotos
@@ -121,6 +123,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | Copiar nome da branch para área de transferência | |
| `` <space> `` | Verificar | Checar a nova branch baseada na brach remota selecionada, ou a branch remota como HEAD, desanexado |
| `` n `` | Nova branch | |
| `` w `` | Nova árvore de trabalho | |
| `` M `` | Mesclar | Ver opções para mesclar o item selecionado no branch atual (mesclar regularmente, mesclar squash) |
| `` r `` | Refazer | Refazer a branch checada na branch selecionada |
| `` d `` | Apagar | Excluir o branch remoto do controle remoto. |
@@ -130,7 +133,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | Abrir ferramenta de diff externa (git difftool) | |
| `` 0 `` | Focar visualização principal | |
| `` <enter> `` | Ver commits | |
| `` w `` | Ver opções da árvore de trabalho | |
| `` / `` | Filtrar a visualização atual por texto | |
## Commit arquivos
@@ -186,13 +188,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | Abrir commit no navegador | |
| `` n `` | Create new branch off of commit | |
| `` N `` | Mover commits para uma nova branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | Nova árvore de trabalho | |
| `` g `` | Restaurar | Ver opções de redefinição (soft/mixed/hard) para redefinir para o item selecionado. |
| `` C `` | Copiar (cherry-pick) | Marcar commit como copiado. Então, dentro da visualização local de commits, você pode pressionar `V` para colar (cherry-pick) o(s) commit(s) copiado(s) em seu branch de check-out. A qualquer momento você pode pressionar `<esc>` para cancelar a seleção. |
| `` <ctrl+t> `` | Abrir ferramenta de diff externa (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focar visualização principal | |
| `` <enter> `` | Ver arquivos | |
| `` w `` | Ver opções da árvore de trabalho | |
| `` / `` | Pesquisar na visualização atual por texto | |
## Etiquetas
@@ -202,13 +204,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | Copiar etiqueta para área de transferência | |
| `` <space> `` | Verificar | Checar a tag selecionada como um HEAD, desanexado |
| `` n `` | Nova etiqueta | Crie uma nova etiqueta a partir do commit atual. Você será solicitado a digitar um nome e uma descrição opcional. |
| `` w `` | Nova árvore de trabalho | |
| `` d `` | Apagar | Ver opções de exclusão para tag local/remoto. |
| `` P `` | Empurrar etiqueta | Push the selected tag to a remote. You'll be prompted to select a remote. |
| `` g `` | Restaurar | Ver opções de redefinição (soft/mixed/hard) para redefinir para o item selecionado. |
| `` <ctrl+t> `` | Abrir ferramenta de diff externa (git difftool) | |
| `` 0 `` | Focar visualização principal | |
| `` <enter> `` | Ver commits | |
| `` w `` | Ver opções da árvore de trabalho | |
| `` / `` | Filtrar a visualização atual por texto | |
## Input prompt
@@ -271,7 +273,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <space> `` | Escolha o local | |
| `` b `` | Pegar todos os pedaços | |
| `` b `` | Pick both hunks | |
| `` <up>, k `` | Trecho anterior | |
| `` <down>, j `` | Próximo trecho | |
| `` <left>, h `` | Conflito anterior | |
@@ -308,6 +310,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | Abrir commit no navegador | |
| `` n `` | Create new branch off of commit | |
| `` N `` | Mover commits para uma nova branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | Nova árvore de trabalho | |
| `` g `` | Restaurar | Ver opções de redefinição (soft/mixed/hard) para redefinir para o item selecionado. |
| `` C `` | Copiar (cherry-pick) | Marcar commit como copiado. Então, dentro da visualização local de commits, você pode pressionar `V` para colar (cherry-pick) o(s) commit(s) copiado(s) em seu branch de check-out. A qualquer momento você pode pressionar `<esc>` para cancelar a seleção. |
| `` <ctrl+r> `` | Reset copied (cherry-picked) commits selection | |
@@ -315,7 +318,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focar visualização principal | |
| `` <enter> `` | Ver commits | |
| `` w `` | Ver opções da árvore de trabalho | |
| `` / `` | Filtrar a visualização atual por texto | |
## Remotes
@@ -346,17 +348,16 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` g `` | Pop | Aplique a entrada de stash no seu diretório de trabalho e remova a entrada de stash. |
| `` d `` | Descartar | Remova a entrada do stash da lista de armazenamento. |
| `` n `` | Nova branch | Criar um novo ramo a partir da entrada de lixo selecionada. Isso funciona verificando o commit do qual a entrada de lixo foi criada, criar um novo branch a partir desse commit e, em seguida, aplicar a entrada de lixo ao novo branch como um commit adicional. |
| `` w `` | Nova árvore de trabalho | |
| `` r `` | Renomear o stash | |
| `` 0 `` | Focar visualização principal | |
| `` <enter> `` | Ver arquivos | |
| `` w `` | Ver opções da árvore de trabalho | |
| `` / `` | Filtrar a visualização atual por texto | |
## Status
| Key | Action | Info |
|-----|--------|-------------|
| `` o `` | Abrir o ficheiro de config | Abrir arquivo no aplicativo padrão. |
| `` e `` | Editar arquivo de configuração | Abrir arquivo no editor externo. |
| `` u `` | Verificar atualização | |
| `` <enter> `` | Mudar para um repositório recente | |
@@ -374,6 +375,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | Abrir commit no navegador | |
| `` n `` | Create new branch off of commit | |
| `` N `` | Mover commits para uma nova branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | Nova árvore de trabalho | |
| `` g `` | Restaurar | Ver opções de redefinição (soft/mixed/hard) para redefinir para o item selecionado. |
| `` C `` | Copiar (cherry-pick) | Marcar commit como copiado. Então, dentro da visualização local de commits, você pode pressionar `V` para colar (cherry-pick) o(s) commit(s) copiado(s) em seu branch de check-out. A qualquer momento você pode pressionar `<esc>` para cancelar a seleção. |
| `` <ctrl+r> `` | Reset copied (cherry-picked) commits selection | |
@@ -381,7 +383,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focar visualização principal | |
| `` <enter> `` | Ver arquivos | |
| `` w `` | Ver opções da árvore de trabalho | |
| `` / `` | Pesquisar na visualização atual por texto | |
## Submódulos
+11 -10
View File
@@ -22,7 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` R `` | Обновить | Refresh the git state (i.e. run `git status`, `git branch`, etc in background to update the contents of panels). This does not run `git fetch`. |
| `` + `` | Следующий режим экрана (нормальный/полуэкранный/полноэкранный) | |
| `` _ `` | Предыдущий режим экрана | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` <esc> `` | Отменить | |
| `` ? `` | Открыть меню | |
| `` <ctrl+s> `` | Просмотреть параметры фильтрации по пути | View options for filtering the commit log, so that only commits matching the filter are shown. |
@@ -30,6 +31,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` q, <ctrl+c> `` | Выйти | |
| `` <ctrl+z> `` | Suspend the application | |
| `` <ctrl+w> `` | Переключить отображение изменении пробелов в просмотрщике сравнении | Toggle whether or not whitespace changes are shown in the diff view.<br><br>The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. |
| `` <alt+shift+c> `` | Редактировать файл конфигурации | Open file in external editor. |
| `` z `` | Отменить (через reflog) (экспериментальный) | Журнал ссылок (reflog) будет использоваться для определения того, какую команду git запустить, чтобы отменить последнюю команду git. Сюда не входят изменения в рабочем дереве; учитываются только коммиты. |
| `` Z `` | Повторить (через reflog) (экспериментальный) | Журнал ссылок (reflog) будет использоваться для определения того, какую команду git нужно запустить, чтобы повторить последнюю команду git. Сюда не входят изменения в рабочем дереве; учитываются только коммиты. |
@@ -112,7 +114,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <space> `` | Выбрать эту часть | |
| `` b `` | Выбрать все части | |
| `` b `` | Pick both hunks | |
| `` <up>, k `` | Выбрать предыдущую часть | |
| `` <down>, j `` | Выбрать следующую часть | |
| `` <left>, h `` | Выбрать предыдущий конфликт | |
@@ -149,6 +151,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | Открыть коммит в браузере | |
| `` n `` | Создать новую ветку с этого коммита | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` g `` | Просмотреть параметры сброса | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | Скопировать отобранные коммит (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <ctrl+r> `` | Сбросить отобранную (скопированную \| cherry-picked) выборку коммитов | |
@@ -156,7 +159,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Просмотреть коммиты | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Коммиты
@@ -192,13 +194,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | Открыть коммит в браузере | |
| `` n `` | Создать новую ветку с этого коммита | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` g `` | Просмотреть параметры сброса | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | Скопировать отобранные коммит (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Просмотреть файлы выбранного элемента | |
| `` w `` | View worktree options | |
| `` / `` | Найти | |
## Локальные Ветки
@@ -210,6 +212,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <space> `` | Переключить | Checkout selected item. |
| `` n `` | Новая ветка | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` o `` | Создать запрос на принятие изменений | |
| `` O `` | Создать параметры запроса принятие изменений | |
| `` G `` | Open pull request in browser | |
@@ -229,7 +232,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Просмотреть коммиты | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Меню
@@ -258,6 +260,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | Открыть коммит в браузере | |
| `` n `` | Создать новую ветку с этого коммита | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` g `` | Просмотреть параметры сброса | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | Скопировать отобранные коммит (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <ctrl+r> `` | Сбросить отобранную (скопированную \| cherry-picked) выборку коммитов | |
@@ -265,7 +268,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Просмотреть файлы выбранного элемента | |
| `` w `` | View worktree options | |
| `` / `` | Найти | |
## Подмодули
@@ -313,7 +315,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` o `` | Открыть файл конфигурации | Open file in default application. |
| `` e `` | Редактировать файл конфигурации | Open file in external editor. |
| `` u `` | Проверить обновления | |
| `` <enter> `` | Переключиться на последний репозиторий | |
@@ -328,13 +329,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | Copy tag to clipboard | |
| `` <space> `` | Переключить | Checkout the selected tag as a detached HEAD. |
| `` n `` | Создать тег | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. |
| `` w `` | New worktree | |
| `` d `` | Delete | View delete options for local/remote tag. |
| `` P `` | Отправить тег | Push the selected tag to a remote. You'll be prompted to select a remote. |
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Просмотреть коммиты | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Удалённые ветки
@@ -344,6 +345,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | Скопировать название ветки в буфер обмена | |
| `` <space> `` | Переключить | Checkout a new local branch based on the selected remote branch, or the remote branch as a detached head. |
| `` n `` | Новая ветка | |
| `` w `` | New worktree | |
| `` M `` | Слияние с текущей переключённой веткой | View options for merging the selected item into the current branch (regular merge, squash merge) |
| `` r `` | Перебазировать переключённую ветку на эту ветку | Rebase the checked-out branch onto the selected branch. |
| `` d `` | Delete | Delete the remote branch from the remote. |
@@ -353,7 +355,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Просмотреть коммиты | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Удалённые репозитории
@@ -409,8 +410,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` g `` | Применить припрятанные изменения и тут же удалить их из хранилища | Apply the stash entry to your working directory and remove the stash entry. |
| `` d `` | Удалить припрятанные изменения из хранилища | Remove the stash entry from the stash list. |
| `` n `` | Новая ветка | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
| `` w `` | New worktree | |
| `` r `` | Переименовать хранилище | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Просмотреть файлы выбранного элемента | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
+10 -9
View File
@@ -23,6 +23,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` + `` | 下一屏模式(正常/半屏/全屏) | |
| `` _ `` | 上一屏模式 | |
| `` \| `` | 切换分页器 | 从已配置的分页器列表中选择下一个分页器 |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` <esc> `` | 取消 | |
| `` ? `` | 打开菜单 | |
| `` <ctrl+s> `` | 查看按路径过滤选项 | 查看用于过滤提交日志的选项,以便仅显示与过滤器匹配的提交。 |
@@ -30,6 +31,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` q, <ctrl+c> `` | 退出 | |
| `` <ctrl+z> `` | 挂起应用程序 | |
| `` <ctrl+w> `` | 切换是否在差异视图中显示空白字符差异 | 切换是否在差异视图中显示空白字符更改。<br><br>默认值可在配置文件中通过键 'git.ignoreWhitespaceInDiffView' 更改。 |
| `` <alt+shift+c> `` | 编辑配置文件 | 使用外部编辑器打开文件 |
| `` z `` | 撤销 | Reflog将用于确定运行哪个git命令来撤消最后一个git命令。这并不包括对工作树的更改,只考虑提交。 |
| `` Z `` | 重做 | Reflog将用于确定运行哪个git命令来重做上一个git命令。这并不包括对工作树的更改,只考虑提交。 |
@@ -60,6 +62,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | 在浏览器中打开提交 | |
| `` n `` | 从提交创建新分支 | |
| `` N `` | 移动提交至新分支 | 创建一个新分支,并将当前分支未推送的提交移动到该分支。如果您打算开始新工作但忘记先创建新分支,这会很有用。<br><br>请注意,此操作忽略选择,新分支总是从主分支创建或堆叠在当前分支之上(您可以选择哪种方式)。 |
| `` w `` | 新建工作树 | |
| `` g `` | 查看重置选项 | 查看重置选项 (soft/mixed/hard) 用于重置到选择项 |
| `` C `` | 复制提交(拣选) | 标记提交为已复制。然后,在本地提交视图中,您可以按 `V` (Cherry-Pick) 将已复制的提交粘贴到已检出的分支中。任何时候都可以按 `<esc>` 来取消选择。 |
| `` <ctrl+r> `` | 重置已拣选(复制)的提交 | |
@@ -67,7 +70,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | 选择当前分支的提交 | |
| `` 0 `` | 聚焦主视图 | |
| `` <enter> `` | 查看提交的文件 | |
| `` w `` | 查看工作区选项 | |
| `` / `` | 开始搜索 | |
## 子模块
@@ -104,6 +106,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | 在浏览器中打开提交 | |
| `` n `` | 从提交创建新分支 | |
| `` N `` | 移动提交至新分支 | 创建一个新分支,并将当前分支未推送的提交移动到该分支。如果您打算开始新工作但忘记先创建新分支,这会很有用。<br><br>请注意,此操作忽略选择,新分支总是从主分支创建或堆叠在当前分支之上(您可以选择哪种方式)。 |
| `` w `` | 新建工作树 | |
| `` g `` | 查看重置选项 | 查看重置选项 (soft/mixed/hard) 用于重置到选择项 |
| `` C `` | 复制提交(拣选) | 标记提交为已复制。然后,在本地提交视图中,您可以按 `V` (Cherry-Pick) 将已复制的提交粘贴到已检出的分支中。任何时候都可以按 `<esc>` 来取消选择。 |
| `` <ctrl+r> `` | 重置已拣选(复制)的提交 | |
@@ -111,7 +114,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | 选择当前分支的提交 | |
| `` 0 `` | 聚焦主视图 | |
| `` <enter> `` | 查看提交 | |
| `` w `` | 查看工作区选项 | |
| `` / `` | 通过文本过滤当前视图 | |
## 提交
@@ -147,13 +149,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | 在浏览器中打开提交 | |
| `` n `` | 从提交创建新分支 | |
| `` N `` | 移动提交至新分支 | 创建一个新分支,并将当前分支未推送的提交移动到该分支。如果您打算开始新工作但忘记先创建新分支,这会很有用。<br><br>请注意,此操作忽略选择,新分支总是从主分支创建或堆叠在当前分支之上(您可以选择哪种方式)。 |
| `` w `` | 新建工作树 | |
| `` g `` | 查看重置选项 | 查看重置选项 (soft/mixed/hard) 用于重置到选择项 |
| `` C `` | 复制提交(拣选) | 标记提交为已复制。然后,在本地提交视图中,您可以按 `V` (Cherry-Pick) 将已复制的提交粘贴到已检出的分支中。任何时候都可以按 `<esc>` 来取消选择。 |
| `` <ctrl+t> `` | 使用外部差异比较工具(git difftool) | |
| `` * `` | 选择当前分支的提交 | |
| `` 0 `` | 聚焦主视图 | |
| `` <enter> `` | 查看提交的文件 | |
| `` w `` | 查看工作区选项 | |
| `` / `` | 开始搜索 | |
## 提交信息
@@ -225,6 +227,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <space> `` | 检出 | 检出选中的项目 |
| `` n `` | 新分支 | |
| `` N `` | 移动提交至新分支 | 创建一个新分支,并将当前分支未推送的提交移动到该分支。如果您打算开始新工作但忘记先创建新分支,这会很有用。<br><br>请注意,此操作忽略选择,新分支总是从主分支创建或堆叠在当前分支之上(您可以选择哪种方式)。 |
| `` w `` | 新建工作树 | |
| `` o `` | 创建拉取请求 | |
| `` O `` | 创建拉取请求选项 | |
| `` G `` | 在浏览器中打开拉取请求 | |
@@ -244,7 +247,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | 使用外部差异比较工具(git difftool) | |
| `` 0 `` | 聚焦主视图 | |
| `` <enter> `` | 查看提交 | |
| `` w `` | 查看工作区选项 | |
| `` / `` | 通过文本过滤当前视图 | |
## 构建补丁中
@@ -270,13 +272,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | 复制标签到剪贴板 | |
| `` <space> `` | 检出 | 检出选择的标签作为分离的HEAD |
| `` n `` | 创建标签 | 基于当前提交创建一个新标签。您将在弹窗中输入标签名称和描述(可选)。 |
| `` w `` | 新建工作树 | |
| `` d `` | 删除 | 查看本地/远程标签的删除选项 |
| `` P `` | 推送标签 | 推送选择的标签到远端。您将在弹窗中选择一个远端。 |
| `` g `` | 重置 | 查看重置选项 (soft/mixed/hard) 用于重置到选择项 |
| `` <ctrl+t> `` | 使用外部差异比较工具(git difftool) | |
| `` 0 `` | 聚焦主视图 | |
| `` <enter> `` | 查看提交 | |
| `` w `` | 查看工作区选项 | |
| `` / `` | 通过文本过滤当前视图 | |
## 次要
@@ -292,7 +294,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <space> `` | 选中区块 | |
| `` b `` | 选中所有区块 | |
| `` b `` | Pick both hunks | |
| `` <up>, k `` | 选择顶部块 | |
| `` <down>, j `` | 选择底部块 | |
| `` <left>, h `` | 选择上一个冲突 | |
@@ -339,7 +341,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` o `` | 打开配置文件 | 使用默认程序打开该文件 |
| `` e `` | 编辑配置文件 | 使用外部编辑器打开文件 |
| `` u `` | 检查更新 | |
| `` <enter> `` | 切换到最近的仓库 | |
@@ -371,10 +372,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` g `` | 应用并删除 | 将存储项应用到工作目录并删除存储项。 |
| `` d `` | 删除 | 从贮藏列表中删除该贮藏项 |
| `` n `` | 新分支 | 从选定的贮藏项创建一个新分支。这是通过 git 检查创建贮藏项的提交,从该提交创建一个新分支,然后将贮藏项作为附加提交应用到新分支来实现的。 |
| `` w `` | 新建工作树 | |
| `` r `` | 重命名贮藏 | |
| `` 0 `` | 聚焦主视图 | |
| `` <enter> `` | 查看提交的文件 | |
| `` w `` | 查看工作区选项 | |
| `` / `` | 通过文本过滤当前视图 | |
## 输入提示
@@ -403,6 +404,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | 复制分支名称到剪贴板 | |
| `` <space> `` | 检出 | 基于当前选中的远程分支检出一个新的本地分支,或者将远程分支作分离的HEAD。 |
| `` n `` | 新分支 | |
| `` w `` | 新建工作树 | |
| `` M `` | 合并到当前检出的分支 | 查看将选中项合并到当前分支的选项(正常合并,压缩合并) |
| `` r `` | 变基 | 将检出的分支变基到所选的分支上。 |
| `` d `` | 删除 | 从远程删除远程分支。 |
@@ -412,5 +414,4 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | 使用外部差异比较工具(git difftool) | |
| `` 0 `` | 聚焦主视图 | |
| `` <enter> `` | 查看提交 | |
| `` w `` | 查看工作区选项 | |
| `` / `` | 通过文本过滤当前视图 | |
+11 -10
View File
@@ -22,7 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` R `` | 重新整理 | Refresh the git state (i.e. run `git status`, `git branch`, etc in background to update the contents of panels). This does not run `git fetch`. |
| `` + `` | 下一個螢幕模式(常規/半螢幕/全螢幕) | |
| `` _ `` | 上一個螢幕模式 | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` <esc> `` | 取消 | |
| `` ? `` | 開啟選單 | |
| `` <ctrl+s> `` | 檢視篩選路徑選項 | View options for filtering the commit log, so that only commits matching the filter are shown. |
@@ -30,6 +31,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` q, <ctrl+c> `` | 結束 | |
| `` <ctrl+z> `` | Suspend the application | |
| `` <ctrl+w> `` | 切換是否在差異檢視中顯示空格變更 | Toggle whether or not whitespace changes are shown in the diff view.<br><br>The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. |
| `` <alt+shift+c> `` | 編輯設定檔案 | 使用外部編輯器開啟 |
| `` z `` | 復原 | 將使用 reflog 確任 git 指令以復原。這不包括工作區更改;只考慮提交。 |
| `` Z `` | 取消復原 | 將使用 reflog 確任 git 指令以重作。這不包括工作區更改;只考慮提交。 |
@@ -88,7 +90,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <space> `` | 挑選程式碼片段 | |
| `` b `` | 挑選所有程式碼片段 | |
| `` b `` | Pick both hunks | |
| `` <up>, k `` | 選擇上一段 | |
| `` <down>, j `` | 選擇下一段 | |
| `` <left>, h `` | 選擇上一個衝突 | |
@@ -139,6 +141,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | 在瀏覽器中開啟提交 | |
| `` n `` | 從提交建立新分支 | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` g `` | 檢視重設選項 | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | 複製提交 (揀選) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <ctrl+r> `` | 重設選定的揀選 (複製) 提交 | |
@@ -146,7 +149,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 檢視所選項目的檔案 | |
| `` w `` | 檢視工作目錄選項 | |
| `` / `` | 搜尋 | |
## 子模組
@@ -206,13 +208,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | 在瀏覽器中開啟提交 | |
| `` n `` | 從提交建立新分支 | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` g `` | 檢視重設選項 | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | 複製提交 (揀選) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 檢視所選項目的檔案 | |
| `` w `` | 檢視工作目錄選項 | |
| `` / `` | 搜尋 | |
## 提交摘要
@@ -250,10 +252,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` g `` | 還原 | Apply the stash entry to your working directory and remove the stash entry. |
| `` d `` | 捨棄 | Remove the stash entry from the stash list. |
| `` n `` | 新分支 | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
| `` w `` | New worktree | |
| `` r `` | 重新命名收藏 | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 檢視所選項目的檔案 | |
| `` w `` | 檢視工作目錄選項 | |
| `` / `` | 搜尋 | |
## 日誌
@@ -266,6 +268,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` o `` | 在瀏覽器中開啟提交 | |
| `` n `` | 從提交建立新分支 | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` g `` | 檢視重設選項 | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | 複製提交 (揀選) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <ctrl+r> `` | 重設選定的揀選 (複製) 提交 | |
@@ -273,7 +276,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 檢視提交 | |
| `` w `` | 檢視工作目錄選項 | |
| `` / `` | 搜尋 | |
## 本地分支
@@ -285,6 +287,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <space> `` | 檢出 | 檢出選定的項目。 |
| `` n `` | 新分支 | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.<br><br>Note that this disregards the selection, the new branch is always created either from the main branch or stacked on top of the current branch (you get to choose which). |
| `` w `` | New worktree | |
| `` o `` | 建立拉取請求 | |
| `` O `` | 建立拉取請求選項 | |
| `` G `` | Open pull request in browser | |
@@ -304,7 +307,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 檢視提交 | |
| `` w `` | 檢視工作目錄選項 | |
| `` / `` | 搜尋 | |
## 標籤
@@ -314,13 +316,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | Copy tag to clipboard | |
| `` <space> `` | 檢出 | Checkout the selected tag as a detached HEAD. |
| `` n `` | 建立標籤 | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. |
| `` w `` | New worktree | |
| `` d `` | 刪除 | View delete options for local/remote tag. |
| `` P `` | 推送標籤 | Push the selected tag to a remote. You'll be prompted to select a remote. |
| `` g `` | 重設 | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 檢視提交 | |
| `` w `` | 檢視工作目錄選項 | |
| `` / `` | 搜尋 | |
## 檔案
@@ -368,7 +370,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` o `` | 開啟設定檔案 | 使用預設軟體開啟 |
| `` e `` | 編輯設定檔案 | 使用外部編輯器開啟 |
| `` u `` | 檢查更新 | |
| `` <enter> `` | 切換到最近使用的版本庫 | |
@@ -403,6 +404,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+o> `` | 複製分支名稱到剪貼簿 | |
| `` <space> `` | 檢出 | Checkout a new local branch based on the selected remote branch, or the remote branch as a detached head. |
| `` n `` | 新分支 | |
| `` w `` | New worktree | |
| `` M `` | 合併到當前檢出的分支 | View options for merging the selected item into the current branch (regular merge, squash merge) |
| `` r `` | 將已檢出的分支變基至此分支 | Rebase the checked-out branch onto the selected branch. |
| `` d `` | 刪除 | Delete the remote branch from the remote. |
@@ -412,5 +414,4 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 檢視提交 | |
| `` w `` | 檢視工作目錄選項 | |
| `` / `` | 搜尋 | |
+10 -7
View File
@@ -25,8 +25,9 @@ require (
github.com/lucasb-eyer/go-colorful v1.4.0
github.com/mgutz/str v1.2.0
github.com/mitchellh/go-ps v1.0.0
github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe
github.com/rivo/uniseg v0.4.7
github.com/sahilm/fuzzy v0.1.2
github.com/sahilm/fuzzy v0.1.3
github.com/samber/lo v1.53.0
github.com/sanity-io/litter v1.5.8
github.com/sasha-s/go-deadlock v0.3.9
@@ -37,8 +38,8 @@ require (
github.com/stretchr/testify v1.11.1
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
golang.org/x/sync v0.20.0
golang.org/x/sys v0.45.0
golang.org/x/sync v0.21.0
golang.org/x/sys v0.46.0
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -57,20 +58,22 @@ require (
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
github.com/kr/pretty v0.3.1 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/onsi/ginkgo v1.10.3 // indirect
github.com/onsi/gomega v1.34.1 // indirect
github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/term v0.43.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/tools v0.44.0 // indirect
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
)
tool mvdan.cc/gofumpt
+16 -11
View File
@@ -21,7 +21,6 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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=
@@ -40,6 +39,8 @@ github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3Bop
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/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=
@@ -98,17 +99,15 @@ github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
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.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
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/sahilm/fuzzy v0.1.2 h1:kdSkz23lx1meNjEl+SLJULeSbjTI4Dn14K/YxdGrIww=
github.com/sahilm/fuzzy v0.1.2/go.mod h1:au6//VbVSqu6DFrkL2CfjlJ5iURpNCPeE+1GwY3XsT8=
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=
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg=
@@ -140,17 +139,19 @@ golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20170407050850-f3918c30c5c2/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -162,8 +163,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -180,6 +181,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/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=
@@ -193,3 +196,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=
+28 -7
View File
@@ -22,29 +22,50 @@ unit-test:
go test ./... -short
# Run both unit tests and integration tests.
test: unit-test e2e-all
[unix]
test: unit-test e2e
# On Windows, integration tests are not supported right now
[windows]
test: unit-test
# Generate all our auto-generated files (test list, cheatsheets, json schema, maybe other things in the future)
generate:
go generate ./...
format:
gofumpt -l -w .
go tool gofumpt -l -w .
lint:
./scripts/gofumpt-check.sh
./scripts/golangci-lint-shim.sh run
# Run integration tests with a visible UI. Most useful for running a single test; for running all tests, use `e2e-all` instead.
e2e-test-command := "go test pkg/integration/clients/*.go"
# Run integration tests headlessly: no args runs all tests, a test name (or path) runs just that one. Use e2e-cli for a visible UI.
e2e *args:
{{ if args == "" { e2e-test-command } else { \
e2e-test-command + " -run 'TestIntegration/" + \
replace( \
replace_regex( \
replace_regex(args, '\S*pkg/integration/tests/', ''), \
'\.go( |$)', '${1}' \
), \
" ", "$' && " + e2e-test-command + " -run 'TestIntegration/" \
) + "$'" \
} }}
# Run a single integration test with a visible UI; most useful with --sandbox or --slow.
e2e-cli *args:
go run cmd/integration_test/main.go cli {{ args }}
# Open the TUI for running integration tests.
e2e-tui *args:
go run cmd/integration_test/main.go tui {{ args }}
# Run all integration tests headlessly (without a visible UI).
e2e-all:
go test pkg/integration/clients/*.go
# Run some tests on the current commit, similar to what CI does.
check:
./scripts/check_commit.sh
bump-gocui:
scripts/bump_gocui.sh
@@ -54,4 +75,4 @@ demo *args:
demo/record_demo.sh {{ args }}
vendor:
go mod vendor && go mod tidy
go mod tidy && go mod vendor
+15 -7
View File
@@ -14,6 +14,7 @@ import (
"github.com/spf13/afero"
appTypes "github.com/jesseduffield/lazygit/pkg/app/types"
"github.com/jesseduffield/lazygit/pkg/commands/direnv"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
@@ -171,6 +172,17 @@ func openRecentRepo(app *App) bool {
for _, repoDir := range app.Config.GetAppState().RecentRepos {
if isRepo, _ := isDirectoryAGitRepository(repoDir); isRepo {
if err := os.Chdir(repoDir); err == nil {
// We're still in setup, before the gui exists, so we can't show the approval popup
// that DispatchSwitchTo offers for blocked .envrc files; just log and move on.
// Also, the logs only go to the debug log, not the Command Log, because that's not
// available yet, either.
result := direnv.Load(app.OSCommand.Cmd)
if result.Message != "" {
app.Log.WithField("message", result.Message).Info("direnv")
}
if result.Err != nil {
app.Log.WithError(result.Err).Warn("direnv load failed")
}
return true
}
}
@@ -239,12 +251,8 @@ func (app *App) setupRepo(
}
// check if we have a recent repo we can open
for _, repoDir := range app.Config.GetAppState().RecentRepos {
if isRepo, _ := isDirectoryAGitRepository(repoDir); isRepo {
if err := os.Chdir(repoDir); err == nil {
return true, nil
}
}
if openRecentRepo(app) {
return true, nil
}
fmt.Fprintln(os.Stderr, app.Tr.NoRecentRepositories)
@@ -262,7 +270,7 @@ func (app *App) setupRepo(
os.Exit(0)
}
if didOpenRepo := openRecentRepo(app); didOpenRepo {
if openRecentRepo(app) {
return true, nil
}
+1 -1
View File
@@ -102,7 +102,7 @@ func Start(buildInfo *BuildInfo, integrationTest integrationTypes.IntegrationTes
if cliArgs.PrintDefaultConfig {
var buf bytes.Buffer
encoder := yaml.NewEncoder(&buf)
err := encoder.Encode(config.GetDefaultConfigForPlatform(runtime.GOOS))
err := encoder.Encode(config.GetDefaultConfigForPlatform(config.KeybindingPlatform()))
if err != nil {
log.Fatal(err.Error())
}
+130
View File
@@ -0,0 +1,130 @@
package direnv
import (
"bytes"
"encoding/json"
"os"
"os/exec"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
)
// LoadResult bundles everything callers might want to know about a direnv
// invocation. The env-var delta has already been applied to the process by
// the time Load returns.
type LoadResult struct {
// Message is whatever direnv printed to stderr — useful to log
// (success: "direnv: loading .envrc"; error: the error text).
Message string
// Err is non-nil when direnv exited non-zero or its stdout could
// not be parsed.
Err error
// Blocked is true when the target .envrc exists but hasn't been
// approved with `direnv allow` yet. EnvrcPath then holds the path
// direnv said was blocked, suitable for passing to Allow.
Blocked bool
EnvrcPath string
}
// Load runs `direnv export json` for the current working directory and applies
// the resulting env-var delta to the current process. If direnv isn't on PATH,
// it's a no-op — users who don't use direnv pay nothing, and users who do need
// no config to opt in.
func Load(cmd oscommands.ICmdObjBuilder) LoadResult {
if _, lookupErr := exec.LookPath("direnv"); lookupErr != nil {
return LoadResult{}
}
stdout, stderr, runErr := cmd.New([]string{
"direnv", "export", "json",
}).DontLog().RunWithOutputs()
result := LoadResult{Message: strings.TrimRight(stderr, "\n")}
// Apply whatever delta direnv produced even if it exited non-zero.
// When the new dir's .envrc is blocked, direnv still emits a valid
// JSON delta on stdout that unloads vars from the previous dir;
// without applying it the old env would leak into the new repo.
delta, parseErr := parseDirenvExport([]byte(stdout))
for k, v := range delta {
if v == nil {
_ = os.Unsetenv(k)
} else {
_ = os.Setenv(k, *v)
}
}
// Prefer the runtime error (whose Error() text is direnv's stderr)
// over a parse error, since it's the more actionable signal.
if runErr != nil {
result.Err = runErr
if envrcPath := queryBlockedEnvrc(cmd); envrcPath != "" {
result.Blocked = true
result.EnvrcPath = envrcPath
}
} else {
result.Err = parseErr
}
return result
}
// Allow runs `direnv allow <envrcPath>` to approve a .envrc file so the next
// Load can read it.
func Allow(cmd oscommands.ICmdObjBuilder, envrcPath string) error {
return cmd.New([]string{"direnv", "allow", envrcPath}).DontLog().Run()
}
func parseDirenvExport(stdout []byte) (map[string]*string, error) {
trimmed := bytes.TrimSpace(stdout)
if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
return nil, nil
}
var delta map[string]*string
if err := json.Unmarshal(trimmed, &delta); err != nil {
return nil, err
}
return delta, nil
}
// queryBlockedEnvrc asks direnv (via `status --json`) whether the current
// directory has a found-but-not-yet-allowed .envrc, and returns its path
// if so. We use direnv's structured output rather than parsing the
// human-readable "is blocked" line because the status output is more
// stable across versions and locales.
func queryBlockedEnvrc(cmd oscommands.ICmdObjBuilder) string {
stdout, _, err := cmd.New([]string{
"direnv", "status", "--json",
}).DontLog().RunWithOutputs()
if err != nil {
return ""
}
return parseDirenvStatus([]byte(stdout))
}
func parseDirenvStatus(stdout []byte) string {
var status struct {
State struct {
FoundRC *struct {
Allowed int `json:"allowed"`
Path string `json:"path"`
} `json:"foundRC"`
} `json:"state"`
}
if err := json.Unmarshal(stdout, &status); err != nil {
return ""
}
if status.State.FoundRC == nil {
return ""
}
// direnv's AllowStatus enum (`internal/cmd/rc.go`): 0=Allowed,
// 1=NotAllowed, 2=Denied. Only NotAllowed is something the user
// can approve; Denied means they already said no.
const notAllowed = 1
if status.State.FoundRC.Allowed != notAllowed {
return ""
}
return status.State.FoundRC.Path
}
+88
View File
@@ -0,0 +1,88 @@
package direnv
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseDirenvExport(t *testing.T) {
hello := "hello"
empty := ""
scenarios := []struct {
name string
input string
want map[string]*string
wantErr bool
}{
{name: "empty stdout means no .envrc was loaded", input: "", want: nil},
{name: "literal null from direnv means no delta", input: "null", want: nil},
{name: "empty object means no delta", input: "{}", want: map[string]*string{}},
{name: "string value is a set", input: `{"FOO":"hello"}`, want: map[string]*string{"FOO": &hello}},
{name: "null value is an unset", input: `{"FOO":null}`, want: map[string]*string{"FOO": nil}},
{
name: "set and unset can coexist",
input: `{"FOO":"hello","BAR":null,"BAZ":""}`,
want: map[string]*string{"FOO": &hello, "BAR": nil, "BAZ": &empty},
},
{name: "malformed JSON is an error", input: `{not json`, wantErr: true},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
got, err := parseDirenvExport([]byte(s.input))
if s.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, s.want, got)
}
})
}
}
func TestParseDirenvStatus(t *testing.T) {
scenarios := []struct {
name string
input string
want string
}{
{
name: "no .envrc found",
input: `{"state":{"foundRC":null}}`,
want: "",
},
{
name: "found and allowed (0)",
input: `{"state":{"foundRC":{"allowed":0,"path":"/repo/.envrc"}}}`,
want: "",
},
{
name: "found but not allowed (1) — eligible for approval",
input: `{"state":{"foundRC":{"allowed":1,"path":"/repo/.envrc"}}}`,
want: "/repo/.envrc",
},
{
name: "found but denied (2) — user already said no",
input: `{"state":{"foundRC":{"allowed":2,"path":"/repo/.envrc"}}}`,
want: "",
},
{
name: "malformed JSON",
input: `{not json`,
want: "",
},
{
name: "empty input",
input: "",
want: "",
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
assert.Equal(t, s.want, parseDirenvStatus([]byte(s.input)))
})
}
}
+2 -2
View File
@@ -117,8 +117,8 @@ func NewGitCommandAux(
rebaseCommands := git_commands.NewRebaseCommands(gitCommon, commitCommands, workingTreeCommands)
stashCommands := git_commands.NewStashCommands(gitCommon, fileLoader, workingTreeCommands)
patchBuilder := patch.NewPatchBuilder(cmn.Log,
func(from string, to string, reverse bool, filename string, plain bool) (string, error) {
return workingTreeCommands.ShowFileDiff(from, to, reverse, filename, plain)
func(from string, to string, reverse bool, filename string, previousPath string, plain bool) (string, error) {
return workingTreeCommands.ShowFileDiff(from, to, reverse, filename, previousPath, plain)
})
patchCommands := git_commands.NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchBuilder)
bisectCommands := git_commands.NewBisectCommands(gitCommon)
+7 -2
View File
@@ -1,6 +1,7 @@
package commands
import (
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/sirupsen/logrus"
)
@@ -14,6 +15,12 @@ type gitCmdObjBuilder struct {
var _ oscommands.ICmdObjBuilder = &gitCmdObjBuilder{}
// We disable git's optional locks on every command by default so that our git
// invocations never contend for index.lock. See git_commands.OptionalLocksEnvVar
// for the full rationale. Individual commands that do want the lock (currently
// only the foreground files refresh) opt back in via CmdObj.RemoveEnvVar.
var defaultEnvVar = git_commands.OptionalLocksEnvVar + "=0"
func NewGitCmdObjBuilder(log *logrus.Entry, innerBuilder *oscommands.CmdObjBuilder) *gitCmdObjBuilder {
// the price of having a convenient interface where we can say .New(...).Run() is that our builder now depends on our runner, so when we want to wrap the default builder/runner in new functionality we need to jump through some hoops. We could avoid the use of a decorator function here by just exporting the runner field on the default builder but that would be misleading because we don't want anybody using that to run commands (i.e. we want there to be a single API used across the codebase)
updatedBuilder := innerBuilder.CloneWithNewRunner(func(runner oscommands.ICmdObjRunner) oscommands.ICmdObjRunner {
@@ -28,8 +35,6 @@ func NewGitCmdObjBuilder(log *logrus.Entry, innerBuilder *oscommands.CmdObjBuild
}
}
var defaultEnvVar = "GIT_OPTIONAL_LOCKS=0"
func (self *gitCmdObjBuilder) New(args []string) *oscommands.CmdObj {
return self.innerBuilder.New(args).AddEnvVars(defaultEnvVar)
}
+24
View File
@@ -0,0 +1,24 @@
package commands
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
)
// Every git command we build disables optional locks by default, so that our
// invocations never contend for index.lock (see git_commands.OptionalLocksEnvVar
// for the rationale). Commands that want the lock opt back in with
// CmdObj.RemoveEnvVar.
func TestGitCmdObjBuilderDisablesOptionalLocksByDefault(t *testing.T) {
builder := NewGitCmdObjBuilder(
utils.NewDummyLog(),
oscommands.NewDummyCmdObjBuilder(oscommands.NewFakeRunner(t)),
)
assert.Contains(t, builder.New([]string{"git", "status"}).GetEnvVars(), git_commands.OptionalLocksEnvVar+"=0")
assert.Contains(t, builder.NewShell("git status", "").GetEnvVars(), git_commands.OptionalLocksEnvVar+"=0")
}
+30 -11
View File
@@ -1,12 +1,12 @@
package git_commands
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/samber/lo"
)
type CommitFileLoader struct {
@@ -29,7 +29,7 @@ func (self *CommitFileLoader) GetFilesInDiff(from string, to string, reverse boo
Arg("--no-ext-diff").
Arg("--name-status").
Arg("-z").
Arg("--no-renames").
Arg(fmt.Sprintf("--find-renames=%d%%", self.UserConfig().Git.RenameSimilarityThreshold)).
ArgIf(reverse, "-R").
Arg(from).
Arg(to).
@@ -44,18 +44,37 @@ func (self *CommitFileLoader) GetFilesInDiff(from string, to string, reverse boo
}
// filenames string is something like "MM\x00file1\x00MU\x00file2\x00AA\x00file3\x00"
// so we need to split it by the null character and then map each status-name pair to a commit file
// so we need to split it by the null character and then map each status-name pair
// to a commit file. Renames (and copies) are special: their status is followed by
// two paths (the old one and the new one) rather than one, e.g.
// "R100\x00old\x00new\x00".
func getCommitFilesFromFilenames(filenames string) []*models.CommitFile {
lines := strings.Split(strings.TrimRight(filenames, "\x00"), "\x00")
if len(lines) == 1 {
fields := strings.Split(strings.TrimRight(filenames, "\x00"), "\x00")
if len(fields) == 1 {
return []*models.CommitFile{}
}
// typical result looks like 'A my_file' meaning my_file was added
return lo.Map(lo.Chunk(lines, 2), func(chunk []string, _ int) *models.CommitFile {
return &models.CommitFile{
ChangeStatus: chunk[0],
Path: chunk[1],
commitFiles := make([]*models.CommitFile, 0, len(fields)/2)
for i := 0; i < len(fields)-1; {
changeStatus := fields[i]
if changeStatus[0] == 'R' || changeStatus[0] == 'C' {
// The status has a similarity score appended (e.g. "R100"); drop it
// so the rest of the code only has to deal with a plain "R" or "C".
commitFiles = append(commitFiles, &models.CommitFile{
ChangeStatus: changeStatus[:1],
PreviousPath: fields[i+1],
Path: fields[i+2],
})
i += 3
} else {
// typical result looks like 'A my_file' meaning my_file was added
commitFiles = append(commitFiles, &models.CommitFile{
ChangeStatus: changeStatus,
Path: fields[i+1],
})
i += 2
}
})
}
return commitFiles
}
@@ -60,6 +60,25 @@ func TestGetCommitFilesFromFilenames(t *testing.T) {
},
},
},
{
testName: "a rename among regular files",
input: "M\x00Myfile\x00R100\x00before\x00after\x00A\x00Added\x00",
output: []*models.CommitFile{
{
Path: "Myfile",
ChangeStatus: "M",
},
{
Path: "after",
PreviousPath: "before",
ChangeStatus: "R",
},
{
Path: "Added",
ChangeStatus: "A",
},
},
},
}
for _, test := range tests {
+6
View File
@@ -168,6 +168,12 @@ func buildBranchCommands(deps commonDeps) *BranchCommands {
return NewBranchCommands(gitCommon)
}
func buildStatusCommands(deps commonDeps) *StatusCommands {
gitCommon := buildGitCommon(deps)
return NewStatusCommands(gitCommon)
}
func buildFlowCommands(deps commonDeps) *FlowCommands {
gitCommon := buildGitCommon(deps)
+19 -2
View File
@@ -36,6 +36,12 @@ type GetStatusFileOptions struct {
// This is useful for users with bare repos for dotfiles who default to hiding untracked files,
// but want to occasionally see them to `git add` a new file.
ForceShowUntracked bool
// When true, this status is part of an unattended background refresh, so it
// keeps the default suppression of optional locks (avoiding index.lock
// contention with git commands the user runs in a terminal, at the cost of
// not persisting git's refreshed stat-cache). A foreground status opts back
// in; see gitStatus.
Background bool
}
func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
@@ -47,7 +53,7 @@ func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File
}
untrackedFilesArg := fmt.Sprintf("--untracked-files=%s", untrackedFilesSetting)
statuses, err := self.gitStatus(GitStatusOptions{NoRenames: opts.NoRenames, UntrackedFilesArg: untrackedFilesArg})
statuses, err := self.gitStatus(GitStatusOptions{NoRenames: opts.NoRenames, UntrackedFilesArg: untrackedFilesArg, Background: opts.Background})
if err != nil {
self.Log.Error(err)
}
@@ -148,6 +154,7 @@ func (self *FileLoader) getFileDiffs() (map[string]FileDiff, error) {
type GitStatusOptions struct {
NoRenames bool
UntrackedFilesArg string
Background bool
}
type FileStatus struct {
@@ -179,7 +186,17 @@ func (self *FileLoader) gitStatus(opts GitStatusOptions) ([]FileStatus, error) {
).
ToArgv()
statusLines, _, err := self.cmd.New(cmdArgs).DontLog().RunWithOutputs()
cmdObj := self.cmd.New(cmdArgs).DontLog()
if !opts.Background {
// Every git command suppresses optional locks by default (see
// OptionalLocksEnvVar). A foreground refresh is the one exception: we let
// it take the lock so it persists git's refreshed stat-cache, which keeps
// subsequent status calls fast. Background refreshes leave it suppressed so
// they can't contend for index.lock.
cmdObj.RemoveEnvVar(OptionalLocksEnvVar)
}
statusLines, _, err := cmdObj.RunWithOutputs()
if err != nil {
return []FileStatus{}, err
}
@@ -6,6 +6,16 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
)
// OptionalLocksEnvVar is the name of the environment variable that tells git
// whether it may take "optional" locks — chiefly the index.lock that `git
// status` grabs to write back a refreshed stat-cache. We set it to 0 on every
// git command by default (see NewGitCmdObjBuilder) so our invocations never
// contend for index.lock, neither with each other (e.g. a main-view `git diff
// --submodule`, which runs `git status` inside submodules, racing a submodule
// action) nor with git commands the user runs in a terminal. The one command
// that opts back in is the foreground files refresh; see FileLoader.gitStatus.
const OptionalLocksEnvVar = "GIT_OPTIONAL_LOCKS"
// convenience struct for building git commands. Especially useful when
// including conditional args
type GitCommandBuilder struct {
+62
View File
@@ -4,8 +4,10 @@ import (
"os"
"path/filepath"
"strings"
"time"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/spf13/afero"
)
type StatusCommands struct {
@@ -82,6 +84,66 @@ func (self *StatusCommands) IsInRevert() (bool, error) {
return self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "REVERT_HEAD"))
}
// RefsSnapshot returns a string fingerprint of the current state of local
// branches and HEAD. Comparing two snapshots byte-for-byte tells us whether
// any local ref or HEAD has moved since the last snapshot.
func (self *StatusCommands) RefsSnapshot() (string, error) {
t := time.Now()
defer func() { self.Log.Infof("RefsSnapshot took %s", time.Since(t)) }()
refsArgs := NewGitCmd("for-each-ref").
Arg("--format=%(objectname) %(refname)").
Arg("refs/heads").
ToArgv()
refs, err := self.cmd.New(refsArgs).DontLog().RunWithOutput()
if err != nil {
return "", err
}
head, err := self.headSnapshot()
if err != nil {
return "", err
}
return refs + head, nil
}
// headSnapshot returns a fingerprint of HEAD that distinguishes "detached at
// commit X" from "on a branch that points at X". The commit hash alone can't
// tell those apart, which matters at the end of a rebase: HEAD reattaches to
// the branch without the hash changing, and we'd otherwise miss that refresh.
//
// We read .git/HEAD directly rather than shelling out: it's faster (no child
// process) and its content is exactly the symref-or-hash distinction we want
// ("ref: refs/heads/foo" when attached, the raw hash when detached). The
// reftable backend, however, doesn't keep a real .git/HEAD — it writes a fixed
// stub ("ref: refs/heads/.invalid") that never reflects the actual HEAD. When
// we see that stub (or the file is missing/unreadable) we fall back to
// porcelain commands, which are backend-agnostic.
func (self *StatusCommands) headSnapshot() (string, error) {
headPath := filepath.Join(self.repoPaths.WorktreeGitDirPath(), "HEAD")
if content, err := afero.ReadFile(self.Fs, headPath); err == nil {
head := strings.TrimSpace(string(content))
if head != "" && head != "ref: refs/heads/.invalid" {
return head, nil
}
}
// symbolic-ref gives the branch when HEAD is attached and fails when it's
// detached, in which case rev-parse gives the commit HEAD points at.
symbolicRefArgs := NewGitCmd("symbolic-ref").Arg("HEAD").ToArgv()
if symref, err := self.cmd.New(symbolicRefArgs).DontLog().RunWithOutput(); err == nil {
return strings.TrimSpace(symref), nil
}
revParseArgs := NewGitCmd("rev-parse").Arg("HEAD").ToArgv()
head, err := self.cmd.New(revParseArgs).DontLog().RunWithOutput()
if err != nil {
return "", err
}
return strings.TrimSpace(head), nil
}
// Full ref (e.g. "refs/heads/mybranch") of the branch that is currently
// being rebased, or empty string when we're not in a rebase
func (self *StatusCommands) BranchBeingRebased() string {
+91
View File
@@ -0,0 +1,91 @@
package git_commands
import (
"testing"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/samber/lo"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
)
func TestStatusRefsSnapshot(t *testing.T) {
const forEachRefOutput = "aaaa refs/heads/main\nbbbb refs/heads/topic\n"
forEachRefArgs := []string{"for-each-ref", "--format=%(objectname) %(refname)", "refs/heads"}
scenarios := []struct {
testName string
headFile *string // nil means: don't create a .git/HEAD file (simulates it being unreadable).
runner *oscommands.FakeCmdObjRunner
expectedHead string
}{
{
// files backend, on a branch: read straight from .git/HEAD, no
// child process for HEAD.
testName: "attached, read from HEAD file",
headFile: lo.ToPtr("ref: refs/heads/main\n"),
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs(forEachRefArgs, forEachRefOutput, nil),
expectedHead: "ref: refs/heads/main",
},
{
// files backend, detached: .git/HEAD holds the raw hash.
testName: "detached, read from HEAD file",
headFile: lo.ToPtr("aaaa\n"),
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs(forEachRefArgs, forEachRefOutput, nil),
expectedHead: "aaaa",
},
{
// reftable backend (HEAD is a fixed stub), attached: fall back to
// symbolic-ref, which succeeds.
testName: "reftable stub, attached, fall back to symbolic-ref",
headFile: lo.ToPtr("ref: refs/heads/.invalid\n"),
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs(forEachRefArgs, forEachRefOutput, nil).
ExpectGitArgs([]string{"symbolic-ref", "HEAD"}, "refs/heads/main\n", nil),
expectedHead: "refs/heads/main",
},
{
// reftable backend, detached: symbolic-ref fails, fall back to
// rev-parse.
testName: "reftable stub, detached, fall back to rev-parse",
headFile: lo.ToPtr("ref: refs/heads/.invalid\n"),
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs(forEachRefArgs, forEachRefOutput, nil).
ExpectGitArgs([]string{"symbolic-ref", "HEAD"}, "", errors.New("fatal: ref HEAD is not a symbolic ref")).
ExpectGitArgs([]string{"rev-parse", "HEAD"}, "aaaa\n", nil),
expectedHead: "aaaa",
},
{
// HEAD file missing/unreadable: same fallback as reftable.
testName: "no HEAD file, fall back to symbolic-ref",
headFile: nil,
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs(forEachRefArgs, forEachRefOutput, nil).
ExpectGitArgs([]string{"symbolic-ref", "HEAD"}, "refs/heads/main\n", nil),
expectedHead: "refs/heads/main",
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
fs := afero.NewMemMapFs()
if s.headFile != nil {
assert.NoError(t, afero.WriteFile(fs, "/repo/.git/HEAD", []byte(*s.headFile), 0o600))
}
instance := buildStatusCommands(commonDeps{
runner: s.runner,
fs: fs,
repoPaths: MockRepoPaths("/repo"),
})
snapshot, err := instance.RefsSnapshot()
assert.NoError(t, err)
assert.Equal(t, forEachRefOutput+s.expectedHead, snapshot)
s.runner.CheckForMissingCalls()
})
}
}
+91
View File
@@ -9,6 +9,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/samber/lo"
)
// .gitmodules looks like this:
@@ -86,6 +87,96 @@ func (self *SubmoduleCommands) GetConfigs(parentModule *models.SubmoduleConfig)
return configs, nil
}
// AnyHaveStageableChanges reports whether any of the given submodule paths has
// a checked-out commit that differs from the one recorded in the
// superproject's index, i.e. a change that `git add <path>` would actually
// stage. A submodule that only has dirty or untracked content (with no new
// commit) can't be staged from the superproject, so it won't be reported here.
func (self *SubmoduleCommands) AnyHaveStageableChanges(paths []string) (bool, error) {
if len(paths) == 0 {
return false, nil
}
cmdArgs := NewGitCmd("submodule").Arg("status", "--").Arg(paths...).ToArgv()
output, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
if err != nil {
return false, err
}
// Each line looks like "<prefix><sha> <path> (<describe>)". A '+' prefix
// means the checked-out commit differs from the index, i.e. there's a
// commit change to stage.
return lo.SomeBy(strings.Split(output, "\n"), func(line string) bool {
return strings.HasPrefix(line, "+")
}), nil
}
// GetConflictCommits returns the three gitlink commits of a conflicted submodule
// from the index: the merge base, our (current) commit, and their (incoming)
// commit. Any of them can be empty if that stage is absent (e.g. a submodule
// that was added on only one side). The path is relative to the repo root.
func (self *SubmoduleCommands) GetConflictCommits(path string) (base string, ours string, theirs string, err error) {
cmdArgs := NewGitCmd("ls-files").Arg("-u", "-z", "--", path).ToArgv()
output, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
if err != nil {
return "", "", "", err
}
// Each NUL-terminated entry looks like "<mode> <sha> <stage>\t<path>".
for _, entry := range strings.Split(output, "\x00") {
// fields are split on the tab and the spaces, so the leading three are
// always mode, sha, stage regardless of what the path contains.
fields := strings.Fields(entry)
if len(fields) < 3 {
continue
}
switch fields[2] {
case "1":
base = fields[1]
case "2":
ours = fields[1]
case "3":
theirs = fields[1]
}
}
return base, ours, theirs, nil
}
// GetCommitSummary returns "<short-sha> <subject>" for a commit inside the
// submodule at the given path, for display in the conflict menu.
func (self *SubmoduleCommands) GetCommitSummary(path string, sha string) (string, error) {
cmdArgs := NewGitCmd("log").
Dir(path).
Arg("--format=%h %s", "--max-count=1", sha).
Config("log.showsignature=false").
ToArgv()
summary, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
return strings.TrimSpace(summary), err
}
// CheckoutConflictCommit resolves a submodule conflict by checking the submodule
// out at the given commit. `git checkout --ours/--theirs` is a no-op on
// gitlinks, so we check out the chosen commit in the submodule itself; the
// 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 self.cmd.New(cmdArgs).Run()
}
// ConflictSideLog returns a oneline log, run inside the submodule, of the commits
// that `side` has but `otherSide` does not (i.e. `otherSide..side`) — the commits
// unique to one side of a commit conflict, relative to their common ancestor. It
// is empty if `side` is an ancestor of `otherSide` (e.g. that side was rewound).
func (self *SubmoduleCommands) ConflictSideLog(path string, side string, otherSide string) (string, error) {
cmdArgs := NewGitCmd("log").Dir(path).
Arg("--oneline", "--color=always", otherSide+".."+side).
ToArgv()
return self.cmd.New(cmdArgs).DontLog().RunWithOutput()
}
func (self *SubmoduleCommands) Stash(submodule *models.SubmoduleConfig) error {
// if the path does not exist then it hasn't yet been initialized so we'll swallow the error
// because the intention here is to have no dirty worktree state
@@ -0,0 +1,92 @@
package git_commands
import (
"testing"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/stretchr/testify/assert"
)
func TestSubmoduleGetConflictCommits(t *testing.T) {
type scenario struct {
testName string
output string
expectedBase string
expectedOurs string
expectedTheirs string
}
scenarios := []scenario{
{
testName: "all three stages present (both modified)",
output: "160000 aaaaaaa 1\tmysub\x00160000 bbbbbbb 2\tmysub\x00160000 ccccccc 3\tmysub\x00",
expectedBase: "aaaaaaa",
expectedOurs: "bbbbbbb",
expectedTheirs: "ccccccc",
},
{
testName: "only our and their stages (added on both sides)",
output: "160000 bbbbbbb 2\tmysub\x00160000 ccccccc 3\tmysub\x00",
expectedBase: "",
expectedOurs: "bbbbbbb",
expectedTheirs: "ccccccc",
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"ls-files", "-u", "-z", "--", "mysub"}, s.output, nil)
instance := buildSubmoduleCommands(commonDeps{runner: runner})
base, ours, theirs, err := instance.GetConflictCommits("mysub")
assert.NoError(t, err)
assert.Equal(t, s.expectedBase, base)
assert.Equal(t, s.expectedOurs, ours)
assert.Equal(t, s.expectedTheirs, theirs)
runner.CheckForMissingCalls()
})
}
}
func TestSubmoduleGetConflictCommitsError(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"ls-files", "-u", "-z", "--", "mysub"}, "", errors.New("error"))
instance := buildSubmoduleCommands(commonDeps{runner: runner})
_, _, _, err := instance.GetConflictCommits("mysub")
assert.Error(t, err)
runner.CheckForMissingCalls()
}
func TestSubmoduleGetCommitSummary(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"-c", "log.showsignature=false", "-C", "mysub", "log", "--format=%h %s", "--max-count=1", "bbbbbbb"}, "bbbbbbb the subject\n", nil)
instance := buildSubmoduleCommands(commonDeps{runner: runner})
summary, err := instance.GetCommitSummary("mysub", "bbbbbbb")
assert.NoError(t, err)
assert.Equal(t, "bbbbbbb the subject", summary)
runner.CheckForMissingCalls()
}
func TestSubmoduleCheckoutConflictCommit(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"-C", "mysub", "checkout", "bbbbbbb"}, "", nil)
instance := buildSubmoduleCommands(commonDeps{runner: runner})
assert.NoError(t, instance.CheckoutConflictCommit("mysub", "bbbbbbb"))
runner.CheckForMissingCalls()
}
func TestSubmoduleConflictSideLog(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"-C", "mysub", "log", "--oneline", "--color=always", "ccccccc..bbbbbbb"}, "bbbbbbb left\n", nil)
instance := buildSubmoduleCommands(commonDeps{runner: runner})
output, err := instance.ConflictSideLog("mysub", "bbbbbbb", "ccccccc")
assert.NoError(t, err)
assert.Equal(t, "bbbbbbb left\n", output)
runner.CheckForMissingCalls()
}
+9 -3
View File
@@ -432,8 +432,14 @@ func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain
// ShowFileDiff get the diff of specified from and to. Typically this will be used for a single commit so it'll be 123abc^..123abc
// but when we're in diff mode it could be any 'from' to any 'to'. The reverse flag is also here thanks to diff mode.
func (self *WorkingTreeCommands) ShowFileDiff(from string, to string, reverse bool, fileName string, plain bool) (string, error) {
return self.ShowFileDiffCmdObj(from, to, reverse, []string{fileName}, plain).RunWithOutput()
// For a renamed file, previousPath is the path it was renamed from (empty otherwise);
// both paths must be passed to git for the rename to be detected.
func (self *WorkingTreeCommands) ShowFileDiff(from string, to string, reverse bool, fileName string, previousPath string, plain bool) (string, error) {
fileNames := []string{fileName}
if previousPath != "" {
fileNames = append(fileNames, previousPath)
}
return self.ShowFileDiffCmdObj(from, to, reverse, fileNames, plain).RunWithOutput()
}
func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reverse bool, fileNames []string, plain bool) *oscommands.CmdObj {
@@ -454,7 +460,7 @@ func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reve
ArgIfElse(useExtDiff || useExtDiffGitConfig, "--ext-diff", "--no-ext-diff").
Arg("--submodule").
Arg(fmt.Sprintf("--unified=%d", contextSize)).
Arg("--no-renames").
Arg(fmt.Sprintf("--find-renames=%d%%", self.UserConfig().Git.RenameSimilarityThreshold)).
Arg(fmt.Sprintf("--color=%s", colorArg)).
Arg(from).
Arg(to).
+22 -4
View File
@@ -339,6 +339,8 @@ func TestWorkingTreeShowFileDiff(t *testing.T) {
from string
to string
reverse bool
fileName string
previousPath string
plain bool
ignoreWhitespace bool
contextSize uint64
@@ -353,33 +355,49 @@ func TestWorkingTreeShowFileDiff(t *testing.T) {
from: "1234567890",
to: "0987654321",
reverse: false,
fileName: "test.txt",
plain: false,
ignoreWhitespace: false,
contextSize: 3,
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "diff", "--no-ext-diff", "--submodule", "--unified=3", "--no-renames", "--color=always", "1234567890", "0987654321", "--", "test.txt"}, expectedResult, nil),
ExpectGitArgs([]string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "diff", "--no-ext-diff", "--submodule", "--unified=3", "--find-renames=50%", "--color=always", "1234567890", "0987654321", "--", "test.txt"}, expectedResult, nil),
},
{
testName: "Show diff with custom context size",
from: "1234567890",
to: "0987654321",
reverse: false,
fileName: "test.txt",
plain: false,
ignoreWhitespace: false,
contextSize: 123,
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "diff", "--no-ext-diff", "--submodule", "--unified=123", "--no-renames", "--color=always", "1234567890", "0987654321", "--", "test.txt"}, expectedResult, nil),
ExpectGitArgs([]string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "diff", "--no-ext-diff", "--submodule", "--unified=123", "--find-renames=50%", "--color=always", "1234567890", "0987654321", "--", "test.txt"}, expectedResult, nil),
},
{
testName: "Default case (ignore whitespace)",
from: "1234567890",
to: "0987654321",
reverse: false,
fileName: "test.txt",
plain: false,
ignoreWhitespace: true,
contextSize: 3,
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "diff", "--no-ext-diff", "--submodule", "--unified=3", "--no-renames", "--color=always", "1234567890", "0987654321", "--ignore-all-space", "--", "test.txt"}, expectedResult, nil),
ExpectGitArgs([]string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "diff", "--no-ext-diff", "--submodule", "--unified=3", "--find-renames=50%", "--color=always", "1234567890", "0987654321", "--ignore-all-space", "--", "test.txt"}, expectedResult, nil),
},
{
testName: "Renamed file passes both paths so the rename is detected",
from: "1234567890",
to: "0987654321",
reverse: false,
fileName: "new.txt",
previousPath: "old.txt",
plain: false,
ignoreWhitespace: false,
contextSize: 3,
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "diff", "--no-ext-diff", "--submodule", "--unified=3", "--find-renames=50%", "--color=always", "1234567890", "0987654321", "--", "new.txt", "old.txt"}, expectedResult, nil),
},
}
@@ -394,7 +412,7 @@ func TestWorkingTreeShowFileDiff(t *testing.T) {
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner, userConfig: userConfig, appState: &config.AppState{}, repoPaths: &repoPaths})
result, err := instance.ShowFileDiff(s.from, s.to, s.reverse, "test.txt", s.plain)
result, err := instance.ShowFileDiff(s.from, s.to, s.reverse, s.fileName, s.previousPath, s.plain)
assert.NoError(t, err)
assert.Equal(t, expectedResult, result)
s.runner.CheckForMissingCalls()
+10
View File
@@ -160,3 +160,13 @@ func (c *Commit) IsTODO() bool {
func IsHeadCommit(commits []*Commit, index int) bool {
return !commits[index].IsTODO() && (index == 0 || commits[index-1].IsTODO())
}
func HeadCommitIdx(commits []*Commit) int {
for index, commit := range commits {
if !commit.IsTODO() {
return index
}
}
return -1
}
+21
View File
@@ -4,6 +4,9 @@ package models
type CommitFile struct {
Path string
// For a renamed file, the path it was renamed from; empty otherwise.
PreviousPath string
ChangeStatus string // e.g. 'A' for added or 'M' for modified. This is based on the result from git diff --name-status
}
@@ -23,6 +26,24 @@ func (f *CommitFile) Deleted() bool {
return f.ChangeStatus == "D"
}
func (f *CommitFile) IsRename() bool {
return f.PreviousPath != ""
}
// Names returns an array containing just the path, or in the case of a rename,
// the after path and the before path.
func (f *CommitFile) Names() []string {
result := []string{f.Path}
if f.PreviousPath != "" {
result = append(result, f.PreviousPath)
}
return result
}
func (f *CommitFile) GetPath() string {
return f.Path
}
func (f *CommitFile) GetPreviousPath() string {
return f.PreviousPath
}
+81
View File
@@ -0,0 +1,81 @@
package models
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stefanhaller/git-todo-parser/todo"
"github.com/stretchr/testify/assert"
)
func TestHeadCommitIdx(t *testing.T) {
testCases := []struct {
name string
commits []*Commit
expected int
}{
{
name: "first commit without rebase todos",
commits: makeTestCommits("a", "b"),
expected: 0,
},
{
name: "first non-todo commit during an interactive rebase",
commits: []*Commit{
makeTestTodoCommit(todo.Pick),
makeTestTodoCommit(todo.Reword),
makeTestCommit("a"),
makeTestCommit("b"),
},
expected: 2,
},
{
name: "no commits",
commits: nil,
expected: -1,
},
{
name: "only rebase todos",
commits: []*Commit{
makeTestTodoCommit(todo.Pick),
makeTestTodoCommit(todo.Reword),
},
expected: -1,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
assert.Equal(t, testCase.expected, HeadCommitIdx(testCase.commits))
})
}
}
func TestIsHeadCommit(t *testing.T) {
commits := []*Commit{
makeTestTodoCommit(todo.Pick),
makeTestCommit("a"),
makeTestCommit("b"),
}
assert.False(t, IsHeadCommit(commits, 0))
assert.True(t, IsHeadCommit(commits, 1))
assert.False(t, IsHeadCommit(commits, 2))
}
func makeTestCommits(hashes ...string) []*Commit {
commits := make([]*Commit, 0, len(hashes))
for _, hash := range hashes {
commits = append(commits, makeTestCommit(hash))
}
return commits
}
func makeTestCommit(hash string) *Commit {
return NewCommit(&utils.StringPool{}, NewCommitOpts{Hash: hash})
}
func makeTestTodoCommit(action todo.TodoCommand) *Commit {
return NewCommit(&utils.StringPool{}, NewCommitOpts{Action: action})
}
+14 -1
View File
@@ -91,6 +91,19 @@ func (self *CmdObj) AddEnvVars(vars ...string) *CmdObj {
return self
}
// RemoveEnvVar removes every occurrence of the named environment variable from
// the command's environment. It's the counterpart to AddEnvVars, used to opt a
// single command out of a variable that the builder sets on every command by
// default.
func (self *CmdObj) RemoveEnvVar(name string) *CmdObj {
prefix := name + "="
self.cmd.Env = lo.Filter(self.cmd.Env, func(envVar string, _ int) bool {
return !strings.HasPrefix(envVar, prefix)
})
return self
}
func (self *CmdObj) GetEnvVars() []string {
return self.cmd.Env
}
@@ -144,7 +157,7 @@ func (self *CmdObj) ShouldStreamOutput() bool {
}
// when you call this, then call Run(), we'll use a PTY to run the command. Only
// has an effect if StreamOutput() was also called. Ignored on Windows.
// has an effect if StreamOutput() was also called.
func (self *CmdObj) UsePty() *CmdObj {
self.usePty = true
+63 -29
View File
@@ -48,26 +48,34 @@ func (self *CmdObjBuilder) NewShell(commandStr string, shellFunctionsFile string
if len(shellFunctionsFile) > 0 {
commandStr = fmt.Sprintf("%ssource %s\n%s", self.platform.PrefixForShellFunctionsFile, shellFunctionsFile, commandStr)
}
quotedCommand := self.quotedCommandString(commandStr)
if self.platform.OS == "windows" {
return self.newWindowsShell(commandStr)
}
quotedCommand := self.Quote(commandStr)
cmdArgs := str.ToArgv(fmt.Sprintf("%s %s %s", self.platform.Shell, self.platform.ShellArg, quotedCommand))
return self.New(cmdArgs)
}
func (self *CmdObjBuilder) quotedCommandString(commandStr string) string {
// Windows does not seem to like quotes around the command
if self.platform.OS == "windows" {
return strings.NewReplacer(
"^", "^^",
"&", "^&",
"|", "^|",
"<", "^<",
">", "^>",
"%", "^%",
).Replace(commandStr)
}
// newWindowsShell wraps the command in `cmd.exe /s /c "<command>"`. The /s
// flag tells cmd to strip exactly the outermost pair of quotes and pass the
// rest through unchanged, which preserves any quoting the command itself
// contains (e.g. `"C:\Program Files\my-editor.exe" file.txt`). Without /s,
// cmd's default rules drop the wrong quotes once the command line contains
// more than two of them.
//
// We bypass Go's standard arg quoting via SysProcAttr.CmdLine: it follows the
// CommandLineToArgvW convention (`\"` for inner quotes), but cmd.exe doesn't.
func (self *CmdObjBuilder) newWindowsShell(commandStr string) *CmdObj {
args := []string{self.platform.Shell, "/s", self.platform.ShellArg, commandStr}
cmdObj := self.New(args)
return self.Quote(commandStr)
cmdLine := fmt.Sprintf(`%s /s %s "%s"`, self.platform.Shell, self.platform.ShellArg, commandStr)
setRawCmdLine(cmdObj.GetCmd(), cmdLine)
return cmdObj
}
func (self *CmdObjBuilder) CloneWithNewRunner(decorate func(ICmdObjRunner) ICmdObjRunner) *CmdObjBuilder {
@@ -80,21 +88,47 @@ func (self *CmdObjBuilder) CloneWithNewRunner(decorate func(ICmdObjRunner) ICmdO
}
func (self *CmdObjBuilder) Quote(message string) string {
var quote string
if self.platform.OS == "windows" {
quote = `\"`
message = strings.NewReplacer(
`"`, `"'"'"`,
`\"`, `\\"`,
).Replace(message)
} else {
quote = `"`
message = strings.NewReplacer(
`\`, `\\`,
`"`, `\"`,
`$`, `\$`,
"`", "\\`",
).Replace(message)
return quoteForWindows(message)
}
return quote + message + quote
message = strings.NewReplacer(
`\`, `\\`,
`"`, `\"`,
`$`, `\$`,
"`", "\\`",
).Replace(message)
return `"` + message + `"`
}
// quoteForWindows encodes a value using the standard Windows command-line
// convention (the algorithm behind syscall.EscapeArg, reimplemented here so
// it's available on all platforms). The result is always wrapped in double
// quotes so cmd.exe and CommandLineToArgvW treat it as a single argument
// regardless of what shell metacharacters it contains.
func quoteForWindows(s string) string {
var b strings.Builder
b.WriteByte('"')
slashes := 0
for i := range len(s) {
c := s[i]
switch c {
case '\\':
slashes++
b.WriteByte(c)
case '"':
for ; slashes > 0; slashes-- {
b.WriteByte('\\')
}
b.WriteByte('\\')
b.WriteByte(c)
default:
slashes = 0
b.WriteByte(c)
}
}
for ; slashes > 0; slashes-- {
b.WriteByte('\\')
}
b.WriteByte('"')
return b.String()
}
+30 -6
View File
@@ -105,12 +105,18 @@ func (self *cmdObjRunner) RunWithOutputAux(cmdObj *CmdObj) (string, error) {
}
t := time.Now()
output, err := sanitisedCommandOutput(cmdObj.GetCmd().CombinedOutput())
cmd := cmdObj.GetCmd()
output, err := sanitisedCommandOutput(cmd.CombinedOutput())
if err != nil {
self.log.WithField("command", cmdObj.ToString()).Error(output)
}
self.log.Infof("%s (%s)", cmdObj.ToString(), time.Since(t))
wall := time.Since(t)
if ps := cmd.ProcessState; ps != nil {
self.log.Infof("%s (wall %s, cpu %s)", cmdObj.ToString(), wall, ps.UserTime()+ps.SystemTime())
} else {
self.log.Infof("%s (wall %s)", cmdObj.ToString(), wall)
}
return output, err
}
@@ -213,6 +219,10 @@ type cmdHandler struct {
stdoutPipe io.Reader
stdinPipe io.Writer
close func() error
// wait blocks until the child process exits. Needed as a separate
// field because the pty path on Windows spawns via CreateProcess and
// never runs *exec.Cmd.Start — so cmd.Wait wouldn't work there.
wait func() error
}
func (self *cmdObjRunner) runAndStream(cmdObj *CmdObj) error {
@@ -268,7 +278,7 @@ func (self *cmdObjRunner) runAndStreamAux(
onRun(handler, cmdWriter)
err = cmd.Wait()
err = handler.wait()
self.log.Infof("%s (%s)", cmdObj.ToString(), time.Since(t))
@@ -370,9 +380,7 @@ func (self *cmdObjRunner) processOutput(
responseChan := promptUserForCredential(askFor)
if responseChan == nil {
// Returning a nil channel means we should terminate the process.
// We achieve this by closing the pty that it's running in. Note that this won't
// work for the case where we're not running in a pty (i.e. on Windows), but
// in that case we'll never be prompted for credentials, so it's not a concern.
// We achieve this by closing the pty that it's running in.
if err := closeFunc(); err != nil {
self.log.Error(err)
}
@@ -475,5 +483,21 @@ func (self *cmdObjRunner) getCmdHandlerNonPty(cmd *exec.Cmd) (*cmdHandler, error
stdoutPipe: stdoutReader,
stdinPipe: buf,
close: func() error { return nil },
wait: cmd.Wait,
}, nil
}
func (self *cmdObjRunner) getCmdHandlerPty(cmd *exec.Cmd) (*cmdHandler, error) {
// Size will be adjusted by the caller if it cares; this just avoids a
// zero-size pty.
sp, err := StartPty(cmd, 80, 24)
if err != nil {
return nil, err
}
return &cmdHandler{
stdoutPipe: sp.Pty,
stdinPipe: sp.Pty,
close: sp.Pty.Close,
wait: sp.Wait,
}, nil
}
@@ -1,24 +0,0 @@
//go:build !windows
package oscommands
import (
"os/exec"
"github.com/creack/pty"
)
// we define this separately for windows and non-windows given that windows does
// not have great PTY support and we need a PTY to handle a credential request
func (self *cmdObjRunner) getCmdHandlerPty(cmd *exec.Cmd) (*cmdHandler, error) {
ptmx, err := pty.Start(cmd)
if err != nil {
return nil, err
}
return &cmdHandler{
stdoutPipe: ptmx,
stdinPipe: ptmx,
close: ptmx.Close,
}, nil
}
@@ -1,10 +0,0 @@
package oscommands
import (
"os/exec"
)
func (self *cmdObjRunner) getCmdHandlerPty(cmd *exec.Cmd) (*cmdHandler, error) {
// We don't have PTY support on Windows yet, so we just return a non-PTY handler.
return self.getCmdHandlerNonPty(cmd)
}
+21
View File
@@ -5,8 +5,29 @@ import (
"testing"
"github.com/jesseduffield/lazygit/pkg/gocui"
"github.com/stretchr/testify/assert"
)
func TestRemoveEnvVar(t *testing.T) {
cmd := exec.Command("git", "status")
cmd.Env = []string{
"PATH=/usr/bin",
"GIT_OPTIONAL_LOCKS=0",
"GIT_OPTIONAL_LOCKS_OTHER=1", // name is a prefix of ours but not the same var
"GIT_OPTIONAL_LOCKS=0", // duplicates must all be removed
"HOME=/home/me",
}
cmdObj := &CmdObj{cmd: cmd}
cmdObj.RemoveEnvVar("GIT_OPTIONAL_LOCKS")
assert.Equal(t, []string{
"PATH=/usr/bin",
"GIT_OPTIONAL_LOCKS_OTHER=1",
"HOME=/home/me",
}, cmdObj.GetEnvVars())
}
func TestCmdObjToString(t *testing.T) {
quote := func(s string) string {
return "\"" + s + "\""
@@ -0,0 +1,389 @@
//go:build windows
package oscommands
import (
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"testing"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
)
// These tests run only on Windows because they exercise real cmd.exe
// quote-parsing behaviour, which has only been a problem on Windows.
// makeWindowsShellBuilder returns a CmdObjBuilder configured for a real
// Windows cmd shell, bypassing the test "dummy" platform (which is darwin).
func makeWindowsShellBuilder() *CmdObjBuilder {
log := utils.NewDummyLog()
return &CmdObjBuilder{
runner: &cmdObjRunner{log: log, guiIO: NewNullGuiIO(log)},
platform: &Platform{OS: "windows", Shell: "cmd", ShellArg: "/c"},
}
}
// fakeEditorSrc is a minimal Go program that records the args it received,
// one per line, to marker.txt in its own directory. Using a real .exe (not a
// .bat) means args are parsed by Go's runtime via CommandLineToArgvW — the
// same algorithm used by ~all real Windows GUI editors. A .bat would parse
// args via cmd.exe's own rules, which can hide bugs that affect editors.
const fakeEditorSrc = `package main
import (
"os"
"path/filepath"
"strings"
)
func main() {
exe, err := os.Executable()
if err != nil {
os.Exit(2)
}
marker := filepath.Join(filepath.Dir(exe), "marker.txt")
body := strings.Join(os.Args[1:], "\n")
if err := os.WriteFile(marker, []byte(body), 0o644); err != nil {
os.Exit(3)
}
}
`
var (
fakeEditorOnce sync.Once
fakeEditorBytes []byte
fakeEditorErr error
)
// loadFakeEditorBytes builds the fake editor exactly once per test process
// and returns its bytes. Tests then drop a copy at a path containing spaces.
func loadFakeEditorBytes(t *testing.T) []byte {
t.Helper()
fakeEditorOnce.Do(func() {
buildDir, err := os.MkdirTemp("", "lazygit-fake-editor-build-*")
if err != nil {
fakeEditorErr = err
return
}
defer os.RemoveAll(buildDir)
srcPath := filepath.Join(buildDir, "main.go")
binPath := filepath.Join(buildDir, "fake-editor.exe")
if err := os.WriteFile(srcPath, []byte(fakeEditorSrc), 0o644); err != nil {
fakeEditorErr = err
return
}
cmd := exec.Command("go", "build", "-o", binPath, srcPath)
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fakeEditorErr = err
return
}
fakeEditorBytes, fakeEditorErr = os.ReadFile(binPath)
})
if fakeEditorErr != nil {
t.Fatalf("failed to build fake editor helper: %v", fakeEditorErr)
}
return fakeEditorBytes
}
// placeFakeEditor builds the fake editor and places it at a path containing
// a space (mirroring `C:\Program Files\...`). The marker the editor writes
// lives next to the exe.
func placeFakeEditor(t *testing.T) (exe, markerFile string) {
t.Helper()
bin := loadFakeEditorBytes(t)
exeDir := filepath.Join(t.TempDir(), "Program Files", "FakeEditor")
if err := os.MkdirAll(exeDir, 0o755); err != nil {
t.Fatalf("mkdir exeDir: %v", err)
}
exe = filepath.Join(exeDir, "fake-editor.exe")
markerFile = filepath.Join(exeDir, "marker.txt")
if err := os.WriteFile(exe, bin, 0o755); err != nil {
t.Fatalf("write fake editor: %v", err)
}
return exe, markerFile
}
// placeTargetFile creates a file at <freshTempDir>/<dirName>/<basename> with
// a trivial body. Use a dirName containing a space (e.g. "my repo") to put
// the file at a path with spaces.
func placeTargetFile(t *testing.T, dirName, basename string) string {
t.Helper()
dir := filepath.Join(t.TempDir(), dirName)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("mkdir target dir: %v", err)
}
target := filepath.Join(dir, basename)
if err := os.WriteFile(target, []byte("hello"), 0o644); err != nil {
t.Fatalf("write target: %v", err)
}
return target
}
// setupFakeEditor is a convenience wrapper for the common case: editor at a
// spacey path AND target file at a spacey path — the conditions that
// trigger the cmd.exe quote-stripping bug.
func setupFakeEditor(t *testing.T) (fakeExe, targetFile, markerFile string) {
t.Helper()
fakeExe, markerFile = placeFakeEditor(t)
targetFile = placeTargetFile(t, "my repo", "file.txt")
return fakeExe, targetFile, markerFile
}
// resolveTemplate mirrors what pkg/commands/git_commands/file.go does: it
// substitutes {{filename}} with the Windows-quoted filename and {{line}}
// with a line number.
func resolveTemplate(builder *CmdObjBuilder, template, filename, line string) string {
out := strings.ReplaceAll(template, "{{filename}}", builder.Quote(filename))
out = strings.ReplaceAll(out, "{{line}}", line)
return out
}
// readMarkerArgs reads the args the fake editor recorded. Each arg is on its
// own line (so an arg that itself contains a space stays one element). An
// empty file means the editor ran with zero args.
func readMarkerArgs(t *testing.T, markerFile string) []string {
t.Helper()
data, err := os.ReadFile(markerFile)
if err != nil {
t.Fatalf("marker file was not written; the fake editor never ran: %v", err)
}
s := strings.TrimRight(string(data), "\r\n")
if s == "" {
return nil
}
return strings.Split(s, "\n")
}
func TestNewShell_QuotedExePath_FilenameWithSpaces_EditAtLineAndWait(t *testing.T) {
builder := makeWindowsShellBuilder()
fakeExe, targetFile, markerFile := setupFakeEditor(t)
template := `"` + fakeExe + `" -multiInst -nosession -noPlugin -n{{line}} {{filename}}`
cmdStr := resolveTemplate(builder, template, targetFile, "42")
out, err := builder.NewShell(cmdStr, "").GetCmd().CombinedOutput()
if err != nil {
t.Fatalf("shell command failed: %v\ncmd.exe output:\n%s", err, string(out))
}
assert.Equal(t,
[]string{"-multiInst", "-nosession", "-noPlugin", "-n42", targetFile},
readMarkerArgs(t, markerFile),
)
}
func TestNewShell_QuotedExePath_FilenameWithSpaces_EditAtLine(t *testing.T) {
builder := makeWindowsShellBuilder()
fakeExe, targetFile, markerFile := setupFakeEditor(t)
template := `"` + fakeExe + `" -n{{line}} {{filename}}`
cmdStr := resolveTemplate(builder, template, targetFile, "42")
out, err := builder.NewShell(cmdStr, "").GetCmd().CombinedOutput()
if err != nil {
t.Fatalf("shell command failed: %v\ncmd.exe output:\n%s", err, string(out))
}
assert.Equal(t,
[]string{"-n42", targetFile},
readMarkerArgs(t, markerFile),
)
}
func TestNewShell_QuotedExePath_FilenameWithSpaces_Edit(t *testing.T) {
builder := makeWindowsShellBuilder()
fakeExe, targetFile, markerFile := setupFakeEditor(t)
template := `"` + fakeExe + `" {{filename}}`
cmdStr := resolveTemplate(builder, template, targetFile, "")
out, err := builder.NewShell(cmdStr, "").GetCmd().CombinedOutput()
if err != nil {
t.Fatalf("shell command failed: %v\ncmd.exe output:\n%s", err, string(out))
}
assert.Equal(t,
[]string{targetFile},
readMarkerArgs(t, markerFile),
)
}
// Sanity check: for a filename WITHOUT spaces the same templates already work,
// because the resulting cmd.exe line has exactly two quote characters and
// cmd /c keeps them. This pins the difference down to filename quoting and
// guards against a regression where the no-spaces case starts failing too.
func TestNewShell_QuotedExePath_FilenameWithoutSpaces_StillWorks(t *testing.T) {
builder := makeWindowsShellBuilder()
fakeExe, markerFile := placeFakeEditor(t)
plainTarget := placeTargetFile(t, "repo", "plain.txt") // no-space dir
template := `"` + fakeExe + `" -multiInst -nosession -noPlugin -n{{line}} {{filename}}`
cmdStr := resolveTemplate(builder, template, plainTarget, "42")
out, err := builder.NewShell(cmdStr, "").GetCmd().CombinedOutput()
if err != nil {
t.Fatalf("shell command failed: %v\ncmd.exe output:\n%s", err, string(out))
}
assert.Equal(t,
[]string{"-multiInst", "-nosession", "-noPlugin", "-n42", plainTarget},
readMarkerArgs(t, markerFile),
)
}
// TestNewShell_VarietyOfEditorTemplates exercises NewShell with a range of
// realistic editor templates, all with the trigger conditions of the bug
// (quoted exe at a spacey path + filename at a spacey path). Each subtest
// asserts the editor receives the exact args lazygit intended.
//
// Args in `wantArgs` may use the literal "<file>" placeholder; it gets
// substituted with the resolved target file path before comparison.
func TestNewShell_VarietyOfEditorTemplates(t *testing.T) {
const filePlaceholder = "<file>"
cases := []struct {
name string
template string // <exe> stands for the fake editor's full path
line string
wantArgs []string
}{
{
name: "vim/nvim style: +line filename",
template: `"<exe>" +{{line}} {{filename}}`,
line: "42",
wantArgs: []string{"+42", filePlaceholder},
},
{
name: "emacs-like with explicit +N",
template: `"<exe>" +{{line}} -nw {{filename}}`,
line: "7",
wantArgs: []string{"+7", "-nw", filePlaceholder},
},
{
name: "long flag with =value",
template: `"<exe>" --line={{line}} --tab-size=4 {{filename}}`,
line: "42",
wantArgs: []string{"--line=42", "--tab-size=4", filePlaceholder},
},
{
name: "many short and long flags before filename",
template: `"<exe>" -a -b -c --foo --bar -n{{line}} {{filename}}`,
line: "42",
wantArgs: []string{"-a", "-b", "-c", "--foo", "--bar", "-n42", filePlaceholder},
},
{
name: "flag after filename",
template: `"<exe>" {{filename}} --readonly`,
line: "",
wantArgs: []string{filePlaceholder, "--readonly"},
},
{
name: "single short flag attached to value",
template: `"<exe>" -n{{line}} {{filename}}`,
line: "1",
wantArgs: []string{"-n1", filePlaceholder},
},
{
name: "no flags, just filename",
template: `"<exe>" {{filename}}`,
line: "",
wantArgs: []string{filePlaceholder},
},
{
name: "flag with separate value (space-separated)",
template: `"<exe>" --goto {{line}} {{filename}}`,
line: "42",
wantArgs: []string{"--goto", "42", filePlaceholder},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
builder := makeWindowsShellBuilder()
fakeExe, targetFile, markerFile := setupFakeEditor(t)
template := strings.ReplaceAll(tc.template, "<exe>", fakeExe)
cmdStr := resolveTemplate(builder, template, targetFile, tc.line)
out, err := builder.NewShell(cmdStr, "").GetCmd().CombinedOutput()
if err != nil {
t.Fatalf("shell command failed: %v\ncmd.exe output:\n%s", err, string(out))
}
want := make([]string, len(tc.wantArgs))
for i, a := range tc.wantArgs {
want[i] = strings.ReplaceAll(a, filePlaceholder, targetFile)
}
assert.Equal(t, want, readMarkerArgs(t, markerFile))
})
}
}
// TestNewShell_FilenameSpecialCharacters varies the basename of the target
// file across characters that are legal in Windows filenames but might
// interact badly with cmd.exe / Quote(): parentheses, brackets, single
// quote, comma, semicolon, equals, etc. The exe is at a spacey path and
// the target dir has spaces, so the bug-trigger conditions are still met.
func TestNewShell_FilenameSpecialCharacters(t *testing.T) {
cases := []struct {
name string
basename string
}{
{"parens", "file (1).txt"},
{"brackets", "file[v2].txt"},
{"single quote", "it's a file.txt"},
{"comma", "a,b,c.txt"},
{"semicolon", "a;b.txt"},
{"equals", "key=value.txt"},
{"plus", "a+b.txt"},
{"hash", "issue#42.txt"},
{"at sign", "user@host.txt"},
{"tilde", "~backup.txt"},
{"dot leading", ".gitignore.txt"},
{"multiple dots", "v1.2.3.txt"},
{"dash leading", "-flag-looking.txt"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
builder := makeWindowsShellBuilder()
fakeExe, markerFile := placeFakeEditor(t)
targetFile := placeTargetFile(t, "my repo", tc.basename)
template := `"` + fakeExe + `" -n{{line}} {{filename}}`
cmdStr := resolveTemplate(builder, template, targetFile, "42")
out, err := builder.NewShell(cmdStr, "").GetCmd().CombinedOutput()
if err != nil {
t.Fatalf("shell command failed: %v\ncmd.exe output:\n%s", err, string(out))
}
assert.Equal(t,
[]string{"-n42", targetFile},
readMarkerArgs(t, markerFile),
)
})
}
}
// Command chaining with && must work: cmd /s /c runs the assembled line verbatim,
// so cmd treats && as a separator and runs both commands. The two echoes
// therefore produce two separate output lines.
func TestNewShell_CommandChaining(t *testing.T) {
builder := makeWindowsShellBuilder()
out, err := builder.NewShell("echo first&&echo second", "").GetCmd().CombinedOutput()
if err != nil {
t.Fatalf("shell command failed: %v\ncmd.exe output:\n%s", err, string(out))
}
normalized := strings.ReplaceAll(string(out), "\r\n", "\n")
lines := strings.Split(strings.TrimSpace(normalized), "\n")
assert.Equal(t, []string{"first", "second"}, lines)
}
@@ -40,10 +40,15 @@ func (c *OSCommand) UpdateWindowTitle() error {
return nil
}
func TerminateProcessGracefully(cmd *exec.Cmd) error {
if cmd.Process == nil {
// setRawCmdLine is the non-Windows no-op counterpart of the Windows shim
// (see the comment there). NewShell's shell-building logic is portable, so
// this call is reached on every host; only the Windows build does anything.
func setRawCmdLine(cmd *exec.Cmd, cmdLine string) {}
func TerminateProcessGracefully(proc *os.Process) error {
if proc == nil {
return nil
}
return cmd.Process.Signal(syscall.SIGTERM)
return proc.Signal(syscall.SIGTERM)
}
+16 -1
View File
@@ -75,11 +75,26 @@ func TestOSCommandQuoteWindows(t *testing.T) {
actual := osCommand.Quote(`hello "test" 'test2'`)
expected := `\"hello "'"'"test"'"'" 'test2'\"`
expected := `"hello \"test\" 'test2'"`
assert.EqualValues(t, expected, actual)
}
// On Windows, NewShell must hand the command to cmd.exe verbatim.
func TestNewShellWindowsPassesMetacharactersVerbatim(t *testing.T) {
osCommand := NewDummyOSCommand()
platform := &Platform{OS: "windows", Shell: "cmd", ShellArg: "/c"}
osCommand.Platform = platform
osCommand.Cmd.platform = platform
command := `echo a && echo b | sort > out.txt < in.txt %PATH%`
assert.Equal(t,
[]string{"cmd", "/s", "/c", command},
osCommand.Cmd.NewShell(command, "").Args(),
)
}
func TestOSCommandFileType(t *testing.T) {
type scenario struct {
path string
+18 -1
View File
@@ -5,8 +5,25 @@ import (
"os"
"os/exec"
"path/filepath"
"syscall"
)
// setRawCmdLine hands cmd.exe the exact command line we built, bypassing
// os/exec's default composition (which quotes args with the
// CommandLineToArgvW `\"` convention that cmd.exe doesn't understand).
//
// The shell-building logic in NewShell is portable and dispatches on
// platform.OS, which keeps it (and its quoting) unit-testable on any host.
// Assigning SysProcAttr.CmdLine is the only step that needs a Windows-only
// field, so it's the single piece split out behind a build tag; every other
// platform gets the no-op in os_default_platform.go.
func setRawCmdLine(cmd *exec.Cmd, cmdLine string) {
if cmd.SysProcAttr == nil {
cmd.SysProcAttr = &syscall.SysProcAttr{}
}
cmd.SysProcAttr.CmdLine = cmdLine
}
func GetPlatform() *Platform {
return &Platform{
OS: "windows",
@@ -24,7 +41,7 @@ func (c *OSCommand) UpdateWindowTitle() error {
return c.Cmd.NewShell(argString, c.UserConfig().OS.ShellFunctionsFile).Run()
}
func TerminateProcessGracefully(cmd *exec.Cmd) error {
func TerminateProcessGracefully(proc *os.Process) error {
// Signals other than SIGKILL are not supported on Windows
return nil
}
+5 -5
View File
@@ -20,7 +20,7 @@ func TestOSCommandOpenFileWindows(t *testing.T) {
{
filename: "test",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/c", "start", "", "test"}, "", errors.New("error")),
ExpectArgs([]string{"cmd", "/s", "/c", `start "" "test"`}, "", errors.New("error")),
test: func(err error) {
assert.Error(t, err)
},
@@ -28,7 +28,7 @@ func TestOSCommandOpenFileWindows(t *testing.T) {
{
filename: "test",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/c", "start", "", "test"}, "", nil),
ExpectArgs([]string{"cmd", "/s", "/c", `start "" "test"`}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
@@ -36,7 +36,7 @@ func TestOSCommandOpenFileWindows(t *testing.T) {
{
filename: "filename with spaces",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/c", "start", "", "filename with spaces"}, "", nil),
ExpectArgs([]string{"cmd", "/s", "/c", `start "" "filename with spaces"`}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
@@ -44,7 +44,7 @@ func TestOSCommandOpenFileWindows(t *testing.T) {
{
filename: "let's_test_with_single_quote",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/c", "start", "", "let's_test_with_single_quote"}, "", nil),
ExpectArgs([]string{"cmd", "/s", "/c", `start "" "let's_test_with_single_quote"`}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
@@ -52,7 +52,7 @@ func TestOSCommandOpenFileWindows(t *testing.T) {
{
filename: "$USER.txt",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/c", "start", "", "$USER.txt"}, "", nil),
ExpectArgs([]string{"cmd", "/s", "/c", `start "" "$USER.txt"`}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
+34
View File
@@ -0,0 +1,34 @@
package oscommands
import (
"io"
"os"
)
// Pty is the master side of a pseudo-terminal running a subprocess. The
// concrete implementation is platform-specific: creack/pty on Unix and
// ConPTY on Windows.
type Pty interface {
io.ReadWriteCloser
Resize(cols, rows uint16) error
}
// StartedPty is the result of StartPty.
type StartedPty struct {
// Pty is the master side of the pseudo-terminal; read from it to get
// the child's combined stdout/stderr and write to it to feed stdin.
Pty Pty
// Process is the spawned child. Useful for signalling; on Windows the
// original *exec.Cmd was not Start()ed (ConPTY spawns via
// CreateProcess, not os/exec) so cmd.Process is nil and this is the
// only handle.
Process *os.Process
// Wait blocks until the child exits and returns a non-nil error on a
// nonzero exit status, matching *exec.Cmd.Wait semantics.
Wait func() error
}
// StartPty runs cmd in a pseudo-terminal with the given initial dimensions.
// Implemented per-platform in pty_unix.go / pty_windows.go.
//
// func StartPty(cmd *exec.Cmd, cols, rows uint16) (StartedPty, error)
+34
View File
@@ -0,0 +1,34 @@
//go:build !windows
package oscommands
import (
"os"
"os/exec"
creackpty "github.com/creack/pty"
)
type unixPty struct {
master *os.File
}
func (u *unixPty) Read(p []byte) (int, error) { return u.master.Read(p) }
func (u *unixPty) Write(p []byte) (int, error) { return u.master.Write(p) }
func (u *unixPty) Close() error { return u.master.Close() }
func (u *unixPty) Resize(cols, rows uint16) error {
return creackpty.Setsize(u.master, &creackpty.Winsize{Cols: cols, Rows: rows})
}
func StartPty(cmd *exec.Cmd, cols, rows uint16) (StartedPty, error) {
f, err := creackpty.StartWithSize(cmd, &creackpty.Winsize{Cols: cols, Rows: rows})
if err != nil {
return StartedPty{}, err
}
return StartedPty{
Pty: &unixPty{master: f},
Process: cmd.Process,
Wait: cmd.Wait,
}, nil
}
+259
View File
@@ -0,0 +1,259 @@
package oscommands
import (
"fmt"
"os"
"os/exec"
"sync"
"unsafe"
"golang.org/x/sys/windows"
)
type winPty struct {
hpc windows.Handle
inWrite *os.File
outRead *os.File
// mu guards the teardown state below and serializes it against Resize.
// hpcClosed gates ClosePseudoConsole (it must run exactly once) and also
// keeps Resize from touching the HPCON once it's been freed: the
// background waiter in StartPty closes the pseudoconsole on child exit,
// which would otherwise race a concurrent onResize and hand
// ResizePseudoConsole a freed handle.
mu sync.Mutex
hpcClosed bool
closed bool
}
func (p *winPty) Read(buf []byte) (int, error) { return p.outRead.Read(buf) }
func (p *winPty) Write(buf []byte) (int, error) { return p.inWrite.Write(buf) }
func (p *winPty) Resize(cols, rows uint16) error {
p.mu.Lock()
defer p.mu.Unlock()
if p.hpcClosed {
// The child already exited and the pseudoconsole was torn down, so
// there is nothing left to resize.
return nil
}
return windows.ResizePseudoConsole(p.hpc, windows.Coord{X: int16(cols), Y: int16(rows)})
}
// closeHpc closes the pseudoconsole exactly once. Safe to call from multiple
// goroutines and at any time. We need this separately from Close because the
// background waiter in StartPty closes the pseudoconsole as soon as the child
// exits — that's what makes outRead return EOF, matching the Unix behavior
// where the master fd EOFs when the slave closes — while the pipe fds stay
// open until somebody explicitly tears the pty down.
func (p *winPty) closeHpc() {
p.mu.Lock()
defer p.mu.Unlock()
p.closeHpcLocked()
}
// closeHpcLocked closes the pseudoconsole; the caller must hold p.mu.
func (p *winPty) closeHpcLocked() {
if p.hpcClosed {
return
}
p.hpcClosed = true
windows.ClosePseudoConsole(p.hpc)
}
func (p *winPty) Close() error {
p.mu.Lock()
defer p.mu.Unlock()
if p.closed {
return nil
}
p.closed = true
// Closing the pseudoconsole breaks the pipes; the child's next write
// fails and it exits. Then we close our ends of the pipes.
p.closeHpcLocked()
p.inWrite.Close()
p.outRead.Close()
return nil
}
// startWaiter runs proc.Wait in a goroutine and, as soon as the child exits,
// closes the pseudoconsole so that any pending Read on outRead returns EOF
// after buffered output drains. Returns a Wait func that blocks until the
// child has exited and reports its exit status with *exec.Cmd.Wait semantics.
//
// This shape exists because on Unix the master fd EOFs naturally when the
// slave closes on child exit, but ConPTY keeps the pipe alive until we call
// ClosePseudoConsole explicitly. Without doing that on child exit, the
// scanner in pkg/tasks.NewCmdTask would block forever on the next read and
// the post-content view never gets cleared (FlushStaleCells never fires).
func startWaiter(proc *os.Process, p *winPty) func() error {
done := make(chan struct{})
var waitErr error
go func() {
defer close(done)
state, err := proc.Wait()
p.closeHpc()
if err != nil {
waitErr = err
return
}
if !state.Success() {
waitErr = fmt.Errorf("exit status %d", state.ExitCode())
}
}()
return func() error {
<-done
return waitErr
}
}
func StartPty(cmd *exec.Cmd, cols, rows uint16) (sp StartedPty, err error) {
// Two pipes: one for the child's stdin (we never write to it, but ConPTY
// needs a handle), one for the child's stdout/stderr multiplexed through
// the pseudoconsole.
var inRead, inWrite, outRead, outWrite windows.Handle
if err = windows.CreatePipe(&inRead, &inWrite, nil, 0); err != nil {
return StartedPty{}, fmt.Errorf("CreatePipe (in): %w", err)
}
defer func() {
if err != nil {
_ = windows.CloseHandle(inWrite)
}
}()
if err = windows.CreatePipe(&outRead, &outWrite, nil, 0); err != nil {
_ = windows.CloseHandle(inRead)
return StartedPty{}, fmt.Errorf("CreatePipe (out): %w", err)
}
defer func() {
if err != nil {
_ = windows.CloseHandle(outRead)
}
}()
// CreatePseudoConsole dupes the handles it needs internally; we release
// our references to the child-side ends immediately after.
var hpc windows.Handle
size := windows.Coord{X: int16(cols), Y: int16(rows)}
if err = windows.CreatePseudoConsole(size, inRead, outWrite, 0, &hpc); err != nil {
_ = windows.CloseHandle(inRead)
_ = windows.CloseHandle(outWrite)
return StartedPty{}, fmt.Errorf("CreatePseudoConsole: %w", err)
}
_ = windows.CloseHandle(inRead)
_ = windows.CloseHandle(outWrite)
defer func() {
if err != nil {
windows.ClosePseudoConsole(hpc)
}
}()
// Attach the pseudoconsole to the child via a process attribute list.
attrList, err := windows.NewProcThreadAttributeList(1)
if err != nil {
return StartedPty{}, fmt.Errorf("NewProcThreadAttributeList: %w", err)
}
defer attrList.Delete()
// PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE wants the HPCON value itself as
// the attribute value, not a pointer to it — an HPCON is already a
// pointer-sized handle, per Microsoft's ConPTY sample. Spelling that as
// unsafe.Pointer(hpc) trips go vet's unsafeptr check (a uintptr-based
// type converted straight to unsafe.Pointer), which gopls surfaces in
// the editor. Reinterpret the handle's bits through its address instead:
// &hpc is a real pointer, so none of these conversions is the flagged
// uintptr→unsafe.Pointer cast, while the resulting value is identical.
if err = attrList.Update(
windows.PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
*(*unsafe.Pointer)(unsafe.Pointer(&hpc)),
unsafe.Sizeof(hpc),
); err != nil {
return StartedPty{}, fmt.Errorf("UpdateProcThreadAttribute: %w", err)
}
var si windows.StartupInfoEx
si.Cb = uint32(unsafe.Sizeof(si))
si.ProcThreadAttributeList = attrList.List()
var appNamePtr *uint16
if cmd.Path != "" {
if appNamePtr, err = windows.UTF16PtrFromString(cmd.Path); err != nil {
return StartedPty{}, err
}
}
cmdLinePtr, err := windows.UTF16PtrFromString(windows.ComposeCommandLine(cmd.Args))
if err != nil {
return StartedPty{}, err
}
var dirPtr *uint16
if cmd.Dir != "" {
if dirPtr, err = windows.UTF16PtrFromString(cmd.Dir); err != nil {
return StartedPty{}, err
}
}
envBlock, err := createEnvBlock(cmd.Env)
if err != nil {
return StartedPty{}, err
}
var envPtr *uint16
if envBlock != nil {
envPtr = &envBlock[0]
}
var pi windows.ProcessInformation
err = windows.CreateProcess(
appNamePtr,
cmdLinePtr,
nil, // process security
nil, // thread security
false,
windows.EXTENDED_STARTUPINFO_PRESENT|windows.CREATE_UNICODE_ENVIRONMENT,
envPtr,
dirPtr,
&si.StartupInfo,
&pi,
)
if err != nil {
return StartedPty{}, fmt.Errorf("CreateProcess: %w", err)
}
_ = windows.CloseHandle(pi.Thread)
// Re-open the process by PID to get an *os.Process to wait on. Do this
// while pi.Process is still open: Windows won't recycle a PID while any
// handle to the process remains, so FindProcess can't latch onto a
// different process that has since reused the PID. Release the original
// handle once we have our own.
proc, err := os.FindProcess(int(pi.ProcessId))
_ = windows.CloseHandle(pi.Process)
if err != nil {
return StartedPty{}, err
}
wp := &winPty{
hpc: hpc,
inWrite: os.NewFile(uintptr(inWrite), "conpty-in"),
outRead: os.NewFile(uintptr(outRead), "conpty-out"),
}
return StartedPty{
Pty: wp,
Process: proc,
Wait: startWaiter(proc, wp),
}, nil
}
// createEnvBlock packs env vars into the UTF-16 double-null-terminated block
// that CreateProcess expects. Returns nil if env is empty, which tells
// CreateProcess to inherit the parent's environment.
func createEnvBlock(env []string) ([]uint16, error) {
if len(env) == 0 {
return nil, nil
}
var block []uint16
for _, s := range env {
utf16s, err := windows.UTF16FromString(s)
if err != nil {
return nil, err
}
block = append(block, utf16s...)
}
block = append(block, 0)
return block, nil
}
+30 -17
View File
@@ -25,10 +25,14 @@ type fileInfo struct {
mode PatchStatus
includedLineIndices []int
diff string
// For a renamed file, the path it was renamed from; empty otherwise. We
// need to keep hold of it so we can re-render the file's patch (which is
// keyed by the new path) without the caller having to supply it again.
previousPath string
}
type (
loadFileDiffFunc func(from string, to string, reverse bool, filename string, plain bool) (string, error)
loadFileDiffFunc func(from string, to string, reverse bool, filename string, previousPath string, plain bool) (string, error)
)
// PatchBuilder manages the building of a patch for a commit to be applied to another commit (or the working tree, or removed from the current commit). We also support building patches from things like stashes, for which there is less flexibility
@@ -75,6 +79,7 @@ func (p *PatchBuilder) PatchToApply(reverse bool, turnAddedFilesIntoDiffAgainstE
patch.WriteString(p.RenderPatchForFile(RenderPatchForFileOpts{
Filename: filename,
PreviousPath: info.previousPath,
Plain: true,
Reverse: reverse,
TurnAddedFilesIntoDiffAgainstEmptyFile: turnAddedFilesIntoDiffAgainstEmptyFile,
@@ -102,8 +107,8 @@ func (p *PatchBuilder) removeFile(info *fileInfo) {
info.includedLineIndices = nil
}
func (p *PatchBuilder) AddFileWhole(filename string) error {
info, err := p.getFileInfo(filename)
func (p *PatchBuilder) AddFileWhole(filename string, previousPath string) error {
info, err := p.getFileInfo(filename, previousPath)
if err != nil {
return err
}
@@ -113,8 +118,8 @@ func (p *PatchBuilder) AddFileWhole(filename string) error {
return nil
}
func (p *PatchBuilder) RemoveFile(filename string) error {
info, err := p.getFileInfo(filename)
func (p *PatchBuilder) RemoveFile(filename string, previousPath string) error {
info, err := p.getFileInfo(filename, previousPath)
if err != nil {
return err
}
@@ -124,19 +129,20 @@ func (p *PatchBuilder) RemoveFile(filename string) error {
return nil
}
func (p *PatchBuilder) getFileInfo(filename string) (*fileInfo, error) {
func (p *PatchBuilder) getFileInfo(filename string, previousPath string) (*fileInfo, error) {
info, ok := p.fileInfoMap[filename]
if ok {
return info, nil
}
diff, err := p.loadFileDiff(p.From, p.To, p.reverse, filename, true)
diff, err := p.loadFileDiff(p.From, p.To, p.reverse, filename, previousPath, true)
if err != nil {
return nil, err
}
info = &fileInfo{
mode: UNSELECTED,
diff: diff,
mode: UNSELECTED,
diff: diff,
previousPath: previousPath,
}
p.fileInfoMap[filename] = info
@@ -144,8 +150,8 @@ func (p *PatchBuilder) getFileInfo(filename string) (*fileInfo, error) {
return info, nil
}
func (p *PatchBuilder) AddFileLineRange(filename string, lineIndices []int) error {
info, err := p.getFileInfo(filename)
func (p *PatchBuilder) AddFileLineRange(filename string, previousPath string, lineIndices []int) error {
info, err := p.getFileInfo(filename, previousPath)
if err != nil {
return err
}
@@ -155,8 +161,8 @@ func (p *PatchBuilder) AddFileLineRange(filename string, lineIndices []int) erro
return nil
}
func (p *PatchBuilder) RemoveFileLineRange(filename string, lineIndices []int) error {
info, err := p.getFileInfo(filename)
func (p *PatchBuilder) RemoveFileLineRange(filename string, previousPath string, lineIndices []int) error {
info, err := p.getFileInfo(filename, previousPath)
if err != nil {
return err
}
@@ -171,13 +177,14 @@ func (p *PatchBuilder) RemoveFileLineRange(filename string, lineIndices []int) e
type RenderPatchForFileOpts struct {
Filename string
PreviousPath string
Plain bool
Reverse bool
TurnAddedFilesIntoDiffAgainstEmptyFile bool
}
func (p *PatchBuilder) RenderPatchForFile(opts RenderPatchForFileOpts) string {
info, err := p.getFileInfo(opts.Filename)
info, err := p.getFileInfo(opts.Filename, opts.PreviousPath)
if err != nil {
p.Log.Error(err)
return ""
@@ -198,7 +205,12 @@ func (p *PatchBuilder) RenderPatchForFile(opts RenderPatchForFileOpts) string {
Transform(TransformOpts{
Reverse: opts.Reverse,
TurnAddedFilesIntoDiffAgainstEmptyFile: opts.TurnAddedFilesIntoDiffAgainstEmptyFile,
IncludedLineIndices: info.includedLineIndices,
// For a partial selection of a renamed file we keep only the
// content change and drop the rename, so that the rename stays in
// the commit. A whole-file selection keeps the rename (and short-
// circuits before this for plain output).
StripRename: info.mode == PART && info.previousPath != "",
IncludedLineIndices: info.includedLineIndices,
})
if opts.Plain {
@@ -215,6 +227,7 @@ func (p *PatchBuilder) renderEachFilePatch(plain bool) []string {
patches := lo.Map(filenames, func(filename string, _ int) string {
return p.RenderPatchForFile(RenderPatchForFileOpts{
Filename: filename,
PreviousPath: p.fileInfoMap[filename].previousPath,
Plain: plain,
Reverse: false,
TurnAddedFilesIntoDiffAgainstEmptyFile: true,
@@ -244,8 +257,8 @@ func (p *PatchBuilder) GetFileStatus(filename string, parent string) PatchStatus
return info.mode
}
func (p *PatchBuilder) GetFileIncLineIndices(filename string) ([]int, error) {
info, err := p.getFileInfo(filename)
func (p *PatchBuilder) GetFileIncLineIndices(filename string, previousPath string) ([]int, error) {
info, err := p.getFileInfo(filename, previousPath)
if err != nil {
return nil, err
}
+55
View File
@@ -19,6 +19,22 @@ index dcd3485..1ba5540 100644
...
`
const renameWithModificationDiff = `diff --git a/oldname b/newname
similarity index 62%
rename from oldname
rename to newname
index dcd3485..1ba5540 100644
--- a/oldname
+++ b/newname
@@ -1,5 +1,5 @@
apple
-orange
+grape
...
...
...
`
const addNewlineToEndOfFile = `diff --git a/filename b/filename
index 80a73f1..e48a11c 100644
--- a/filename
@@ -152,6 +168,7 @@ func TestTransform(t *testing.T) {
firstLineIndex int
lastLineIndex int
reverse bool
stripRename bool
expected string
}
@@ -515,6 +532,43 @@ func TestTransform(t *testing.T) {
orange
banana
lemon
`,
},
{
testName: "renamed file, whole change selected, strips the rename so only the content change is applied",
firstLineIndex: 9,
lastLineIndex: 10,
stripRename: true,
diffText: renameWithModificationDiff,
expected: `diff --git a/newname b/newname
index dcd3485..1ba5540 100644
--- a/newname
+++ b/newname
@@ -1,5 +1,5 @@
apple
-orange
+grape
...
...
...
`,
},
{
testName: "renamed file, only removal selected, strips the rename",
firstLineIndex: 9,
lastLineIndex: 9,
stripRename: true,
diffText: renameWithModificationDiff,
expected: `diff --git a/newname b/newname
index dcd3485..1ba5540 100644
--- a/newname
+++ b/newname
@@ -1,5 +1,4 @@
apple
-orange
...
...
...
`,
},
}
@@ -527,6 +581,7 @@ func TestTransform(t *testing.T) {
Transform(TransformOpts{
Reverse: s.reverse,
FileNameOverride: s.filename,
StripRename: s.stripRename,
IncludedLineIndices: lineIndices,
}).
FormatPlain()
+53 -6
View File
@@ -33,6 +33,13 @@ type TransformOpts struct {
// treat it as a diff against an empty file.
TurnAddedFilesIntoDiffAgainstEmptyFile bool
// When building a partial patch for a renamed file, strip the rename
// metadata from the header and point it at the new path. Applying the
// resulting patch then only changes the file's contents and leaves the
// rename itself in place. (For a whole-file selection we keep the rename
// so that it moves or is discarded together with the contents.)
StripRename bool
// The indices of lines that should be included in the patch.
IncludedLineIndices []int
}
@@ -72,21 +79,61 @@ func (self *patchTransformer) transformHeader() []string {
"--- a/" + self.opts.FileNameOverride,
"+++ b/" + self.opts.FileNameOverride,
}
} else if self.opts.TurnAddedFilesIntoDiffAgainstEmptyFile {
result := make([]string, 0, len(self.patch.header))
for idx, line := range self.patch.header {
}
header := self.patch.header
if self.opts.StripRename {
header = stripRenameFromHeader(header)
}
if self.opts.TurnAddedFilesIntoDiffAgainstEmptyFile {
result := make([]string, 0, len(header))
for idx, line := range header {
if strings.HasPrefix(line, "new file mode") {
continue
}
if line == "--- /dev/null" && strings.HasPrefix(self.patch.header[idx+1], "+++ b/") {
line = "--- a/" + self.patch.header[idx+1][6:]
if line == "--- /dev/null" && strings.HasPrefix(header[idx+1], "+++ b/") {
line = "--- a/" + header[idx+1][6:]
}
result = append(result, line)
}
return result
}
return self.patch.header
return header
}
// stripRenameFromHeader rewrites a rename diff header so that it looks like a
// plain modification of the new path: it drops the rename metadata and points
// the diff at the new path on both sides, while keeping the blob index line so
// that `git apply --3way` can still fall back to a blob merge. See the
// StripRename option for why we do this.
func stripRenameFromHeader(header []string) []string {
newPath := ""
for _, line := range header {
if path, ok := strings.CutPrefix(line, "+++ b/"); ok {
newPath = path
break
}
}
result := make([]string, 0, len(header))
for _, line := range header {
switch {
case strings.HasPrefix(line, "similarity index "),
strings.HasPrefix(line, "dissimilarity index "),
strings.HasPrefix(line, "rename from "),
strings.HasPrefix(line, "rename to "):
// drop the rename metadata
case strings.HasPrefix(line, "diff --git "):
result = append(result, "diff --git a/"+newPath+" b/"+newPath)
case strings.HasPrefix(line, "--- "):
result = append(result, "--- a/"+newPath)
default:
result = append(result, line)
}
}
return result
}
func (self *patchTransformer) transformHunks() []*Hunk {
+38 -2
View File
@@ -136,8 +136,23 @@ func findOrCreateConfigDir() (string, error) {
return folder, os.MkdirAll(folder, 0o755)
}
// KeybindingPlatform returns the platform whose default keybindings should be
// used. Normally this is the OS we're running on, but it can be overridden with
// the LAZYGIT_KEYBINDING_PLATFORM environment variable; this is useful e.g. when
// running lazygit in a Linux container that you access over ssh from a Mac, and
// you'd rather use the Mac keybindings. An unrecognized value falls back to the
// real OS, which gives meaningful bindings, rather than to the (arbitrary)
// non-darwin defaults.
func KeybindingPlatform() string {
platform := os.Getenv("LAZYGIT_KEYBINDING_PLATFORM")
if lo.Contains([]string{"darwin", "linux", "windows"}, platform) {
return platform
}
return runtime.GOOS
}
func loadUserConfigWithDefaults(configFiles []*ConfigFile, isGuiInitialized bool) (*UserConfig, error) {
return loadUserConfig(configFiles, GetDefaultConfigForPlatform(runtime.GOOS), isGuiInitialized)
return loadUserConfig(configFiles, GetDefaultConfigForPlatform(KeybindingPlatform()), isGuiInitialized)
}
func loadUserConfig(configFiles []*ConfigFile, base *UserConfig, isGuiInitialized bool) (*UserConfig, error) {
@@ -287,6 +302,26 @@ func computeMigratedConfig(path string, content []byte, changes *ChangesSet) ([]
}
}
pathsToMove := []struct {
oldPath []string
newPath []string
}{
{
[]string{"keybinding", "worktrees", "viewWorktreeOptions"},
[]string{"keybinding", "universal", "newWorktree"},
},
}
for _, pathToMove := range pathsToMove {
err, didMove := yaml_utils.MoveYamlKey(&rootNode, pathToMove.oldPath, pathToMove.newPath)
if err != nil {
return nil, false, fmt.Errorf("Couldn't migrate config file at `%s` for key %s: %w", path, strings.Join(pathToMove.oldPath, "."), err)
}
if didMove {
changes.Add(fmt.Sprintf("Moved '%s' to '%s'", strings.Join(pathToMove.oldPath, "."), strings.Join(pathToMove.newPath, ".")))
}
}
err = changeNullKeybindingsToDisabled(&rootNode, changes)
if err != nil {
return nil, false, fmt.Errorf("Couldn't migrate config file at `%s`: %w", path, err)
@@ -449,7 +484,8 @@ func migrateAllBranchesLogCmd(rootNode *yaml.Node, changes *ChangesSet) error {
// We will later populate it with the individual allBranchesLogCmd record
cmdsKeyNode = &yaml.Node{Kind: yaml.ScalarNode, Value: "allBranchesLogCmds"}
cmdsValueNode = &yaml.Node{Kind: yaml.SequenceNode, Content: []*yaml.Node{}}
gitNode.Content = append(gitNode.Content,
gitNode.Content = append(
gitNode.Content,
cmdsKeyNode,
cmdsValueNode,
)
+120
View File
@@ -1,11 +1,53 @@
package config
import (
"runtime"
"testing"
"github.com/stretchr/testify/assert"
)
func TestKeybindingPlatform(t *testing.T) {
scenarios := []struct {
name string
envValue string
expected string
}{
{
name: "Not set falls back to the host OS",
envValue: "",
expected: runtime.GOOS,
},
{
name: "darwin is honored",
envValue: "darwin",
expected: "darwin",
},
{
name: "linux is honored",
envValue: "linux",
expected: "linux",
},
{
name: "windows is honored",
envValue: "windows",
expected: "windows",
},
{
name: "An unrecognized value falls back to the host OS",
envValue: "mac",
expected: runtime.GOOS,
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
t.Setenv("LAZYGIT_KEYBINDING_PLATFORM", s.envValue)
assert.Equal(t, s.expected, KeybindingPlatform())
})
}
}
func TestMigrationOfRenamedKeys(t *testing.T) {
scenarios := []struct {
name string
@@ -78,6 +120,73 @@ keybinding:
}
}
func TestMigrationOfMovedKeys(t *testing.T) {
scenarios := []struct {
name string
input string
expected string
expectedDidChange bool
expectedChanges []string
}{
{
name: "Empty String",
input: "",
expectedDidChange: false,
expectedChanges: []string{},
},
{
name: "No move needed",
input: `foo:
bar: 5
`,
expectedDidChange: false,
expectedChanges: []string{},
},
{
name: "Move worktree keybinding into the universal section",
input: `keybinding:
universal:
quit: q
worktrees:
viewWorktreeOptions: w
`,
expected: `keybinding:
universal:
quit: q
newWorktree: w
`,
expectedDidChange: true,
expectedChanges: []string{"Moved 'keybinding.worktrees.viewWorktreeOptions' to 'keybinding.universal.newWorktree'"},
},
{
name: "Create the universal section if it doesn't exist",
input: `keybinding:
worktrees:
viewWorktreeOptions: w
`,
expected: `keybinding:
universal:
newWorktree: w
`,
expectedDidChange: true,
expectedChanges: []string{"Moved 'keybinding.worktrees.viewWorktreeOptions' to 'keybinding.universal.newWorktree'"},
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
changes := NewChangesSet()
actual, didChange, err := computeMigratedConfig("path doesn't matter", []byte(s.input), changes)
assert.NoError(t, err)
assert.Equal(t, s.expectedDidChange, didChange)
if didChange {
assert.Equal(t, s.expected, string(actual))
}
assert.Equal(t, s.expectedChanges, changes.ToSliceFromOldest())
})
}
}
func TestMigrateNullKeybindingsToDisabled(t *testing.T) {
scenarios := []struct {
name string
@@ -654,6 +763,12 @@ git:
# If true, periodically refresh files and submodules
autoRefresh: true
# If true, poll the repo periodically for external ref changes (commits,
# branch updates, checkouts made outside lazygit) and refresh when one
# is detected. Independent of autoRefresh, which only governs the files
# panel.
autoDetectExternalChanges: true
# If true, pass the --all arg to git fetch
fetchAll: true
@@ -723,6 +838,11 @@ refresher:
# Auto-fetch can be disabled via option 'git.autoFetch'.
fetchInterval: 60
# Interval in seconds at which lazygit polls for external ref changes
# (commits, branch updates, checkouts made outside lazygit).
# Detection can be disabled via option 'git.autoDetectExternalChanges'.
externalChangeCheckInterval: 2
# If true, show a confirmation popup before quitting Lazygit
confirmOnQuit: false
+44
View File
@@ -2,6 +2,7 @@ package config
import (
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -77,6 +78,49 @@ func (self *PagerConfig) CyclePagers() {
self.pagerIndex = (self.pagerIndex + 1) % len(self.getUserConfig().Git.Pagers)
}
func (self *PagerConfig) CyclePagersBackward() {
n := len(self.getUserConfig().Git.Pagers)
self.pagerIndex = (self.pagerIndex - 1 + n) % n
}
func (self *PagerConfig) CurrentPagerIndex() (int, int) {
return self.pagerIndex, len(self.getUserConfig().Git.Pagers)
}
// CurrentPagerName returns a name for the current pager, suitable for showing
// to the user. It returns an empty string if no name can be derived; callers
// should substitute a localized fallback in that case.
func (self *PagerConfig) CurrentPagerName() string {
currentPagerConfig := self.currentPagerConfig()
if currentPagerConfig == nil {
return ""
}
return currentPagerConfig.displayName()
}
// CurrentPagerUsesGitConfigDiff reports whether the current pager defers to
// git's own external diff config. Such an entry has no name we can derive (the
// actual command may even vary per file via .gitattributes), so callers show a
// generic label rather than treating it like the default no-pager entry.
func (self *PagerConfig) CurrentPagerUsesGitConfigDiff() bool {
currentPagerConfig := self.currentPagerConfig()
return currentPagerConfig != nil && currentPagerConfig.UseExternalDiffGitConfig
}
func (self *PagingConfig) displayName() string {
if self.Name != "" {
return self.Name
}
if word := firstWord(string(self.Pager)); word != "" {
return word
}
return firstWord(self.ExternalDiffCommand)
}
func firstWord(command string) string {
fields := strings.Fields(command)
if len(fields) == 0 {
return ""
}
return fields[0]
}
+82
View File
@@ -0,0 +1,82 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCurrentPagerName(t *testing.T) {
scenarios := []struct {
name string
pager PagingConfig
expected string
}{
{
name: "explicit name takes precedence over the command",
pager: PagingConfig{Name: "delta side-by-side", Pager: "delta --side-by-side"},
expected: "delta side-by-side",
},
{
name: "derived from the first word of the pager command",
pager: PagingConfig{Pager: "delta --side-by-side"},
expected: "delta",
},
{
name: "surrounding whitespace in the command is ignored",
pager: PagingConfig{Pager: " diff-so-fancy "},
expected: "diff-so-fancy",
},
{
name: "falls back to the external diff command when there is no pager",
pager: PagingConfig{ExternalDiffCommand: "difft --color=always"},
expected: "difft",
},
{
name: "no name can be derived",
pager: PagingConfig{UseExternalDiffGitConfig: true},
expected: "",
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
userConfig := &UserConfig{}
userConfig.Git.Pagers = []PagingConfig{s.pager}
config := NewPagerConfig(func() *UserConfig { return userConfig })
assert.Equal(t, s.expected, config.CurrentPagerName())
})
}
}
func TestCurrentPagerNameWithoutPagers(t *testing.T) {
config := NewPagerConfig(func() *UserConfig { return &UserConfig{} })
assert.Equal(t, "", config.CurrentPagerName())
}
func TestCyclePagers(t *testing.T) {
userConfig := &UserConfig{}
userConfig.Git.Pagers = []PagingConfig{{Name: "a"}, {Name: "b"}, {Name: "c"}}
config := NewPagerConfig(func() *UserConfig { return userConfig })
currentIndex := func() int {
index, _ := config.CurrentPagerIndex()
return index
}
assert.Equal(t, 0, currentIndex())
config.CyclePagers()
assert.Equal(t, 1, currentIndex())
config.CyclePagers()
assert.Equal(t, 2, currentIndex())
config.CyclePagers()
assert.Equal(t, 0, currentIndex(), "cycling forward past the last pager wraps to the first")
config.CyclePagersBackward()
assert.Equal(t, 2, currentIndex(), "cycling backward past the first pager wraps to the last")
config.CyclePagersBackward()
assert.Equal(t, 1, currentIndex())
}
+54
View File
@@ -0,0 +1,54 @@
package config
import (
"github.com/karimkhaleel/jsonschema"
"github.com/samber/lo"
"gopkg.in/yaml.v3"
)
// SidePanel is one entry in gui.sidePanels: a side panel made up of one or more
// tabs, written in YAML as a list of tab names (e.g. [files, worktrees]).
type SidePanel []string
// ValidSidePanelTabs lists every name that may appear in gui.sidePanels. Each
// names a list that can stand alone as a panel or be grouped with others as the
// tabs of one panel. The resolver in the gui package must handle every entry
// here; a test enforces that the two stay in sync.
var ValidSidePanelTabs = []string{
"status",
"files",
"worktrees",
"submodules",
"branches",
"remotes",
"tags",
"commits",
"reflog",
"stash",
}
func (p SidePanel) MarshalYAML() (any, error) {
// Render in flow style (`[a, b]`) rather than the default block style, which
// is more compact and reads better in the generated docs.
node := &yaml.Node{
Kind: yaml.SequenceNode,
Style: yaml.FlowStyle,
}
for _, s := range p {
node.Content = append(node.Content, &yaml.Node{
Kind: yaml.ScalarNode,
Value: s,
})
}
return node, nil
}
// JSONSchema describes a side panel as a list of tab names, restricted to the
// known names.
func (SidePanel) JSONSchema() *jsonschema.Schema {
names := lo.Map(ValidSidePanelTabs, func(name string, _ int) any { return name })
return &jsonschema.Schema{
Type: "array",
Items: &jsonschema.Schema{Type: "string", Enum: names},
}
}
+72 -24
View File
@@ -11,6 +11,8 @@ type UserConfig struct {
Gui GuiConfig `yaml:"gui"`
// Config relating to git
Git GitConfig `yaml:"git"`
// Config relating to git worktrees
Worktree WorktreeConfig `yaml:"worktree"`
// Periodic update checks
Update UpdateConfig `yaml:"update"`
// Background refreshes
@@ -44,10 +46,13 @@ type UserConfig struct {
type RefresherConfig struct {
// File/submodule refresh interval in seconds.
// Auto-refresh can be disabled via option 'git.autoRefresh'.
RefreshInterval int `yaml:"refreshInterval" jsonschema:"minimum=0"`
RefreshInterval int `yaml:"refreshInterval" jsonschema:"exclusiveMinimum=0"`
// Re-fetch interval in seconds.
// Auto-fetch can be disabled via option 'git.autoFetch'.
FetchInterval int `yaml:"fetchInterval" jsonschema:"minimum=0"`
FetchInterval int `yaml:"fetchInterval" jsonschema:"exclusiveMinimum=0"`
// Interval in seconds at which lazygit polls for external ref changes (commits, branch updates, checkouts made outside lazygit).
// Detection can be disabled via option 'git.autoDetectExternalChanges'.
ExternalChangeCheckInterval int `yaml:"externalChangeCheckInterval" jsonschema:"exclusiveMinimum=0"`
}
func (c *RefresherConfig) RefreshIntervalDuration() time.Duration {
@@ -58,6 +63,10 @@ func (c *RefresherConfig) FetchIntervalDuration() time.Duration {
return time.Second * time.Duration(c.FetchInterval)
}
func (c *RefresherConfig) ExternalChangeCheckIntervalDuration() time.Duration {
return time.Second * time.Duration(c.ExternalChangeCheckInterval)
}
type GuiConfig struct {
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-author-color
AuthorColors map[string]string `yaml:"authorColors"`
@@ -82,6 +91,7 @@ type GuiConfig struct {
TabWidth int `yaml:"tabWidth" jsonschema:"minimum=1"`
// If true, capture mouse events.
// When mouse events are captured, it's a little harder to select text: e.g. requiring you to hold the option key when on macOS.
// Mouse capture can also be toggled at runtime with the 'toggleMouseCapture' keybinding ('%' by default).
MouseEvents bool `yaml:"mouseEvents"`
// If true, do not show a warning when amending a commit.
SkipAmendWarning bool `yaml:"skipAmendWarning"`
@@ -102,6 +112,13 @@ type GuiConfig struct {
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
// The weight of the expanded side panel, relative to the other panels. 2 means twice as tall as the other panels. Only relevant if `expandFocusedSidePanel` is true.
ExpandedSidePanelWeight int `yaml:"expandedSidePanelWeight"`
// If true, don't give a side panel more height than it needs to show its content; when all panels fit, the leftover height is shared among them so that they still fill the screen.
ShrinkSidePanelsToContent bool `yaml:"shrinkSidePanelsToContent"`
// The side panels, in the order they appear from top to bottom.
// Each entry is a list of one or more names that share a single panel as tabs (cycle through them with the next-tab/previous-tab keys).
// Omit a name to hide it; give a name its own one-element list to promote a tab to a top-level panel.
// Valid names are: 'status', 'files', 'worktrees', 'submodules', 'branches', 'remotes', 'tags', 'commits', 'reflog', 'stash'. 'files', 'branches', and 'commits' must always be included; they can't be hidden.
SidePanels []SidePanel `yaml:"sidePanels"`
// Sometimes the main window is split in two (e.g. when the selected file has both staged and unstaged changes). This setting controls how the two sections are split.
// Options are:
// - 'horizontal': split the window horizontally
@@ -256,6 +273,11 @@ type GitConfig struct {
// Array of pagers. Each entry has the following format:
// [dev] The following documentation is duplicated from the PagingConfig struct below.
//
// # A name for the pager, shown in the notification when cycling pagers.
// # If not set, the name is derived from the first word of the pager
// # command (or of the external diff command).
// name: ""
//
// # Value of the --color arg in the git diff command. Some pagers want
// # this to be set to 'always' and some want it set to 'never'
// colorArg: "always"
@@ -275,6 +297,8 @@ type GitConfig struct {
// # https://git-scm.com/docs/gitattributes#_defining_an_external_diff_driver.
// useExternalDiffGitConfig: false
//
// 'pager', 'externalDiffCommand', and 'useExternalDiffGitConfig' are mutually exclusive; set at most one per entry.
//
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Custom_Pagers.md for more information.
Pagers []PagingConfig `yaml:"pagers"`
// Config relating to committing
@@ -289,6 +313,8 @@ type GitConfig struct {
AutoFetch bool `yaml:"autoFetch"`
// If true, periodically refresh files and submodules
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).
// Possible values: 'none' | 'onlyMainBranches' | 'allBranches'
AutoForwardBranches string `yaml:"autoForwardBranches" jsonschema:"enum=none,enum=onlyMainBranches,enum=allBranches"`
@@ -345,6 +371,8 @@ func (PagerType) JSONSchemaExtend(schema *jsonschema.Schema) {
// [dev] This documentation is duplicated in the GitConfig struct. If you make changes here, make them there too.
type PagingConfig struct {
// A name for the pager, shown in the notification when cycling pagers. If not set, the name is derived from the first word of the pager command (or of the external diff command).
Name string `yaml:"name"`
// Value of the --color arg in the git diff command. Some pagers want this to be set to 'always' and some want it set to 'never'
ColorArg string `yaml:"colorArg" jsonschema:"enum=always,enum=never"`
// e.g.
@@ -399,6 +427,13 @@ type CommitPrefixConfig struct {
Replace string `yaml:"replace" jsonschema:"example=[$1]"`
}
type WorktreeConfig struct {
// Default parent directory for new worktrees. It is offered as a candidate location alongside the parent directories of any worktrees you already have.
// A relative path is resolved against the repository's root directory, so "../worktrees" sits beside the repo and ".worktrees" sits inside it.
// A leading "~" is expanded to your home directory, so "~/worktrees" works.
DefaultPath string `yaml:"defaultPath"`
}
type UpdateConfig struct {
// One of: 'prompt' (default) | 'background' | 'never'
Method string `yaml:"method" jsonschema:"enum=prompt,enum=background,enum=never"`
@@ -411,7 +446,6 @@ type KeybindingConfig struct {
Status KeybindingStatusConfig `yaml:"status"`
Files KeybindingFilesConfig `yaml:"files"`
Branches KeybindingBranchesConfig `yaml:"branches"`
Worktrees KeybindingWorktreesConfig `yaml:"worktrees"`
Commits KeybindingCommitsConfig `yaml:"commits"`
AmendAttribute KeybindingAmendAttributeConfig `yaml:"amendAttribute"`
Stash KeybindingStashConfig `yaml:"stash"`
@@ -479,6 +513,7 @@ type KeybindingUniversalConfig struct {
ConfirmInEditorAlt Keybinding `yaml:"confirmInEditor-alt"`
Remove Keybinding `yaml:"remove"`
New Keybinding `yaml:"new"`
NewWorktree Keybinding `yaml:"newWorktree"`
Edit Keybinding `yaml:"edit"`
OpenFile Keybinding `yaml:"openFile"`
ScrollUpMain Keybinding `yaml:"scrollUpMain"`
@@ -502,6 +537,7 @@ type KeybindingUniversalConfig struct {
NextScreenMode Keybinding `yaml:"nextScreenMode"`
PrevScreenMode Keybinding `yaml:"prevScreenMode"`
CyclePagers Keybinding `yaml:"cyclePagers"`
CyclePagersReverse Keybinding `yaml:"cyclePagersReverse"`
Undo Keybinding `yaml:"undo"`
Redo Keybinding `yaml:"redo"`
FilteringMenu Keybinding `yaml:"filteringMenu"`
@@ -513,11 +549,13 @@ type KeybindingUniversalConfig struct {
SubmitEditorText Keybinding `yaml:"submitEditorText"`
ExtrasMenu Keybinding `yaml:"extrasMenu"`
ToggleWhitespaceInDiffView Keybinding `yaml:"toggleWhitespaceInDiffView"`
ToggleMouseCapture Keybinding `yaml:"toggleMouseCapture"`
IncreaseContextInDiffView Keybinding `yaml:"increaseContextInDiffView"`
DecreaseContextInDiffView Keybinding `yaml:"decreaseContextInDiffView"`
IncreaseRenameSimilarityThreshold Keybinding `yaml:"increaseRenameSimilarityThreshold"`
DecreaseRenameSimilarityThreshold Keybinding `yaml:"decreaseRenameSimilarityThreshold"`
OpenDiffTool Keybinding `yaml:"openDiffTool"`
EditConfig Keybinding `yaml:"editConfig"`
}
type KeybindingStatusConfig struct {
@@ -571,10 +609,6 @@ type KeybindingBranchesConfig struct {
SortOrder Keybinding `yaml:"sortOrder"`
}
type KeybindingWorktreesConfig struct {
ViewWorktreeOptions Keybinding `yaml:"viewWorktreeOptions"`
}
type KeybindingCommitsConfig struct {
SquashDown Keybinding `yaml:"squashDown"`
RenameCommit Keybinding `yaml:"renameCommit"`
@@ -817,18 +851,26 @@ func GetDefaultConfig() *UserConfig {
func GetDefaultConfigForPlatform(platform string) *UserConfig {
return &UserConfig{
Gui: GuiConfig{
ScrollHeight: 2,
ScrollPastBottom: true,
ScrollOffMargin: 2,
ScrollOffBehavior: "margin",
TabWidth: 4,
MouseEvents: true,
SkipAmendWarning: false,
SkipDiscardChangeWarning: false,
SkipStashWarning: false,
SidePanelWidth: 0.3333,
ExpandFocusedSidePanel: false,
ExpandedSidePanelWeight: 2,
ScrollHeight: 2,
ScrollPastBottom: true,
ScrollOffMargin: 2,
ScrollOffBehavior: "margin",
TabWidth: 4,
MouseEvents: true,
SkipAmendWarning: false,
SkipDiscardChangeWarning: false,
SkipStashWarning: false,
SidePanelWidth: 0.3333,
ExpandFocusedSidePanel: false,
ExpandedSidePanelWeight: 2,
ShrinkSidePanelsToContent: false,
SidePanels: []SidePanel{
{"status"},
{"files", "worktrees", "submodules"},
{"branches", "remotes", "tags"},
{"commits", "reflog"},
{"stash"},
},
MainPanelSplitMode: "flexible",
EnlargedSideViewLocation: "left",
WrapLinesInStagingView: true,
@@ -912,6 +954,7 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig {
MainBranches: []string{"master", "main"},
AutoFetch: true,
AutoRefresh: true,
AutoDetectExternalChanges: true,
AutoForwardBranches: "onlyMainBranches",
FetchAll: true,
AutoStageResolvedConflicts: true,
@@ -926,9 +969,13 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig {
ParseEmoji: false,
TruncateCopiedCommitHashesTo: 12,
},
Worktree: WorktreeConfig{
DefaultPath: "",
},
Refresher: RefresherConfig{
RefreshInterval: 10,
FetchInterval: 60,
RefreshInterval: 10,
FetchInterval: 60,
ExternalChangeCheckInterval: 2,
},
Update: UpdateConfig{
Method: "prompt",
@@ -990,6 +1037,7 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig {
ConfirmInEditorAlt: Keybinding{"<ctrl+s>"},
Remove: Keybinding{"d"},
New: Keybinding{"n"},
NewWorktree: Keybinding{"w"},
Edit: Keybinding{"e"},
OpenFile: Keybinding{"o"},
OpenRecentRepos: Keybinding{"<ctrl+r>"},
@@ -1010,6 +1058,7 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig {
NextScreenMode: Keybinding{"+"},
PrevScreenMode: Keybinding{"_"},
CyclePagers: Keybinding{"|"},
CyclePagersReverse: Keybinding{"\\"},
Undo: Keybinding{"z"},
Redo: Keybinding{"Z"},
FilteringMenu: Keybinding{"<ctrl+s>"},
@@ -1019,11 +1068,13 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig {
SubmitEditorText: Keybinding{"<enter>"},
ExtrasMenu: Keybinding{"@"},
ToggleWhitespaceInDiffView: Keybinding{"<ctrl+w>"},
ToggleMouseCapture: Keybinding{"%"},
IncreaseContextInDiffView: Keybinding{"}"},
DecreaseContextInDiffView: Keybinding{"{"},
IncreaseRenameSimilarityThreshold: Keybinding{")"},
DecreaseRenameSimilarityThreshold: Keybinding{"("},
OpenDiffTool: Keybinding{"<ctrl+t>"},
EditConfig: Keybinding{"<alt+shift+c>"},
},
Status: KeybindingStatusConfig{
CheckForUpdate: Keybinding{"u"},
@@ -1073,9 +1124,6 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig {
AddForkRemote: Keybinding{"F"},
SortOrder: Keybinding{"s"},
},
Worktrees: KeybindingWorktreesConfig{
ViewWorktreeOptions: Keybinding{"w"},
},
Commits: KeybindingCommitsConfig{
SquashDown: Keybinding{"s"},
RenameCommit: Keybinding{"r"},
+64 -10
View File
@@ -46,6 +46,9 @@ func (config *UserConfig) Validate() error {
[]string{"always", "never", "when-maximised"}); err != nil {
return err
}
if err := validatePagers(config.Git.Pagers); err != nil {
return err
}
if err := validateKeybindings(config.Keybinding); err != nil {
return err
}
@@ -55,6 +58,42 @@ func (config *UserConfig) Validate() error {
if err := validateSpinner(config.Gui.Spinner); err != nil {
return err
}
if err := validateSidePanels(config.Gui.SidePanels); err != nil {
return err
}
return nil
}
func validateSidePanels(panels []SidePanel) error {
seen := map[string]bool{}
total := 0
for _, panel := range panels {
if len(panel) == 0 {
return errors.New("gui.sidePanels: a side panel must have at least one tab.")
}
for _, name := range panel {
if !slices.Contains(ValidSidePanelTabs, name) {
return fmt.Errorf("gui.sidePanels: unknown side panel '%s'. Allowed values: %s",
name, strings.Join(ValidSidePanelTabs, ", "))
}
if seen[name] {
return fmt.Errorf("gui.sidePanels: '%s' is listed more than once; each side panel may appear only once.", name)
}
seen[name] = true
total++
}
}
if total == 0 {
return errors.New("gui.sidePanels must not be empty.")
}
// A lot of code focuses these panels directly (e.g. after resolving a
// conflict or popping a stash), so they must always be present; otherwise
// that code would focus a hidden panel.
for _, required := range []string{"files", "branches", "commits"} {
if !seen[required] {
return fmt.Errorf("gui.sidePanels: '%s' must be included; it can't be hidden.", required)
}
}
return nil
}
@@ -71,6 +110,30 @@ func validateSpinner(spinner SpinnerConfig) error {
return nil
}
// validatePagers rejects pager entries that combine more than one diff
// mechanism. A pager (GIT_PAGER) formats the diff that git produces, whereas
// externalDiffCommand and useExternalDiffGitConfig change how git produces the
// diff in the first place; piping one through the other almost always yields
// garbled output, so we treat the three as mutually exclusive.
func validatePagers(pagers []PagingConfig) error {
for i, pager := range pagers {
count := 0
if pager.Pager != "" {
count++
}
if pager.ExternalDiffCommand != "" {
count++
}
if pager.UseExternalDiffGitConfig {
count++
}
if count > 1 {
return fmt.Errorf("git.pagers[%d]: at most one of 'pager', 'externalDiffCommand', and 'useExternalDiffGitConfig' may be set; they are mutually exclusive", i)
}
}
return nil
}
func validateEnum(name string, value string, allowedValues []string) error {
if slices.Contains(allowedValues, value) {
return nil
@@ -114,16 +177,7 @@ func validateKeybindingsRecurse(path string, node any) error {
}
func validateKeybindings(keybindingConfig KeybindingConfig) error {
if err := validateKeybindingsRecurse("", keybindingConfig); err != nil {
return err
}
if len(keybindingConfig.Universal.JumpToBlock) != 5 {
return fmt.Errorf("keybinding.universal.jumpToBlock must have 5 elements; found %d.",
len(keybindingConfig.Universal.JumpToBlock))
}
return nil
return validateKeybindingsRecurse("", keybindingConfig)
}
func validateCustomCommandKey(key Keybinding) error {
+71 -3
View File
@@ -134,11 +134,12 @@ func TestUserConfigValidate_enums(t *testing.T) {
})
},
testCases: []testCase{
{value: "", valid: false},
{value: "1,2,3", valid: false},
// The number of entries no longer has to match the number of side
// panels, so only the validity of the individual keys matters.
{value: "1,2,3", valid: true},
{value: "1,2,3,4,5", valid: true},
{value: "1,2,3,4,5,6", valid: true},
{value: "1,2,3,4,invalid", valid: false},
{value: "1,2,3,4,5,6", valid: false},
},
},
{
@@ -323,3 +324,70 @@ func TestUserConfigValidate_spinnerFrames(t *testing.T) {
})
}
}
func TestUserConfigValidate_sidePanels(t *testing.T) {
scenarios := []struct {
name string
panels []SidePanel
valid bool
}{
{name: "default layout", panels: []SidePanel{{"status"}, {"files", "worktrees", "submodules"}, {"branches", "remotes", "tags"}, {"commits", "reflog"}, {"stash"}}, valid: true},
{name: "reordered", panels: []SidePanel{{"status"}, {"files"}, {"commits"}, {"branches"}, {"stash"}}, valid: true},
{name: "hidden stash panel", panels: []SidePanel{{"status"}, {"files"}, {"branches"}, {"commits"}}, valid: true},
{name: "promoted tab", panels: []SidePanel{{"files", "submodules"}, {"worktrees"}, {"branches"}, {"commits"}}, valid: true},
{name: "core panels only", panels: []SidePanel{{"files"}, {"branches"}, {"commits"}}, valid: true},
{name: "empty", panels: []SidePanel{}, valid: false},
{name: "empty panel", panels: []SidePanel{{"files"}, {"branches"}, {"commits"}, {}}, valid: false},
{name: "unknown name", panels: []SidePanel{{"files"}, {"branches"}, {"commits"}, {"bogus"}}, valid: false},
{name: "duplicate within panel", panels: []SidePanel{{"files", "files"}, {"branches"}, {"commits"}}, valid: false},
{name: "duplicate across panels", panels: []SidePanel{{"files"}, {"branches", "files"}, {"commits"}}, valid: false},
{name: "missing files", panels: []SidePanel{{"branches"}, {"commits"}}, valid: false},
{name: "missing branches", panels: []SidePanel{{"files"}, {"commits"}}, valid: false},
{name: "missing commits", panels: []SidePanel{{"files"}, {"branches"}}, valid: false},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
config := GetDefaultConfig()
config.Gui.SidePanels = s.panels
err := config.Validate()
if s.valid {
assert.NoError(t, err)
} else {
assert.Error(t, err)
}
})
}
}
func TestUserConfigValidate_pagers(t *testing.T) {
scenarios := []struct {
name string
pager PagingConfig
valid bool
}{
{name: "empty", pager: PagingConfig{}, valid: true},
{name: "pager only", pager: PagingConfig{Pager: "delta"}, valid: true},
{name: "external diff command only", pager: PagingConfig{ExternalDiffCommand: "difft"}, valid: true},
{name: "git config external diff only", pager: PagingConfig{UseExternalDiffGitConfig: true}, valid: true},
{name: "pager and external diff command", pager: PagingConfig{Pager: "delta", ExternalDiffCommand: "difft"}, valid: false},
{name: "pager and git config external diff", pager: PagingConfig{Pager: "delta", UseExternalDiffGitConfig: true}, valid: false},
{name: "both external diff mechanisms", pager: PagingConfig{ExternalDiffCommand: "difft", UseExternalDiffGitConfig: true}, valid: false},
{name: "all three", pager: PagingConfig{Pager: "delta", ExternalDiffCommand: "difft", UseExternalDiffGitConfig: true}, valid: false},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
config := GetDefaultConfig()
config.Git.Pagers = []PagingConfig{s.pager}
err := config.Validate()
if s.valid {
assert.NoError(t, err)
} else {
assert.Error(t, err)
}
})
}
}
+229 -14
View File
@@ -19,6 +19,21 @@ type escapeInterpreter struct {
mode OutputMode
instruction instruction
hyperlink strings.Builder
// ConPTY emits cursor-positioning escapes (CUP) to skip over blank
// rows rather than emitting LFs for them. To convert those into row
// advances the view can act on, we track where in the pseudo-terminal
// screen the cursor currently is. 1-based to match the escape
// sequences.
//
// We also have to track the column, but only well enough to count
// soft-wraps when written content runs past the right edge: ConPTY's
// CUPs are addressed against its post-wrap screen, so a logical line
// long enough to wrap in ConPTY's screen counts for two rows from the
// next CUP's perspective. Column accuracy past wrap-counting isn't
// modelled — we don't track the col argument of CUPs, and most
// pager-style emitters use col 1 anyway.
screenRow, screenCol int
}
type (
@@ -32,6 +47,21 @@ type eraseInLineFromCursor struct{}
func (self eraseInLineFromCursor) isInstruction() {}
// cursorDown asks the view to advance N rows. Emitted when CUP / CUD /
// CNL / VPA targets a row past the current one; backward moves are
// ignored because the view's buffer is line-based and can't undo.
type cursorDown struct{ n int }
func (self cursorDown) isInstruction() {}
// cursorForward asks the view to materialize N space cells. Emitted
// when CUF advances the cursor right — ConPTY uses CUF (often paired
// with ECH) to encode runs of default-colored spaces compactly, so we
// have to render the gap, not just bump a counter.
type cursorForward struct{ n int }
func (self cursorForward) isInstruction() {}
type noInstruction struct{}
func (self noInstruction) isInstruction() {}
@@ -42,6 +72,7 @@ const (
stateCharacterSetDesignation
stateCSI
stateParams
stateCSIDiscard
stateOSC
stateOSCWaitForParams
stateOSCParams
@@ -66,8 +97,6 @@ const (
var (
errNotCSI = errors.New("Not a CSI escape sequence")
errCSIParseError = errors.New("CSI escape sequence parsing error")
errCSITooLong = errors.New("CSI escape sequence is too long")
errOSCParseError = errors.New("OSC escape sequence parsing error")
)
// characters in case of error will output the non-parsed characters as a string.
@@ -100,11 +129,15 @@ func newEscapeInterpreter(mode OutputMode) *escapeInterpreter {
curBgColor: ColorDefault,
mode: mode,
instruction: noInstruction{},
screenRow: 1,
screenCol: 1,
}
return ei
}
// reset sets the escapeInterpreter in initial state.
// reset sets the escapeInterpreter in initial state. Note: this only resets
// escape-parsing state. Screen cursor state survives so that mid-stream
// malformed escapes don't desync the row tracking from the view.
func (ei *escapeInterpreter) reset() {
ei.state = stateNone
ei.curFgColor = ColorDefault
@@ -112,6 +145,80 @@ func (ei *escapeInterpreter) reset() {
ei.csiParam = nil
}
// resetScreenCursor returns the screen-cursor tracking to the top of the
// pseudo-terminal screen. Called when the view is rewound before a fresh pty
// render, and on cursor-home (which ConPTY emits at the start of each screen)
// for views that aren't rewound in lockstep — see the CUP handling in parseOne.
func (ei *escapeInterpreter) resetScreenCursor() {
ei.screenRow = 1
ei.screenCol = 1
}
// notifyRowAdvance must be called by the view whenever it advances to the
// next row in response to an LF / CRLF outside of an escape sequence
// (i.e. the row transitions the parser doesn't see directly). Keeps the
// parser's notion of the current screen row in sync with the view.
func (ei *escapeInterpreter) notifyRowAdvance() {
ei.screenRow++
ei.screenCol = 1
}
// notifyColumnReset must be called when the view processes a bare CR
// (column reset without row advance). Keeps screenCol in sync so wrap
// counting starts over from col 1.
func (ei *escapeInterpreter) notifyColumnReset() {
ei.screenCol = 1
}
// notifyCellsWritten must be called after the view writes visible cells
// to its buffer. Advances the parser's idea of the cursor by `width`
// columns; if that crosses the right edge of a `screenColMax`-wide pty
// screen, the corresponding number of soft-wraps are added to screenRow
// so subsequent CUPs land on the right line.
func (ei *escapeInterpreter) notifyCellsWritten(width, screenColMax int) {
if screenColMax <= 0 {
return
}
// One column at a time: matches ConPTY's "pending wrap" semantics
// where the cursor stays at col max+1 after writing the rightmost
// cell and only wraps on the next cell. Loops over individual
// columns rather than doing the math in one shot so wide cells on a
// row boundary still wrap cleanly.
for range width {
if ei.screenCol > screenColMax {
ei.screenRow++
ei.screenCol = 1
}
ei.screenCol++
}
}
// emitCursorAdvance schedules a cursorDown instruction for the next time
// the view checks ei.instruction, advancing the parser's screen row by
// the same amount. n <= 0 is a no-op (backward / same-row CUPs are
// ignored — the view's buffer is line-based and can't undo).
func (ei *escapeInterpreter) emitCursorAdvance(n int) {
if n <= 0 {
return
}
ei.instruction = cursorDown{n: n}
ei.screenRow += n
ei.screenCol = 1
}
// firstParamOrDefault returns the first CSI parameter parsed as an int,
// or dflt if it's absent / empty / unparseable.
func (ei *escapeInterpreter) firstParamOrDefault(dflt int) int {
if len(ei.csiParam) == 0 || ei.csiParam[0] == "" {
return dflt
}
n, err := strconv.Atoi(ei.csiParam[0])
if err != nil {
return dflt
}
return n
}
func (ei *escapeInterpreter) instructionRead() {
ei.instruction = noInstruction{}
}
@@ -120,12 +227,13 @@ func (ei *escapeInterpreter) instructionRead() {
// is part of an escape sequence, and as such should not be printed verbatim. Otherwise, it's not an
// escape sequence.
func (ei *escapeInterpreter) parseOne(ch []byte) (isEscape bool, err error) {
// Sanity checks
if len(ei.csiParam) > 20 {
return false, errCSITooLong
}
if len(ei.csiParam) > 0 && len(ei.csiParam[len(ei.csiParam)-1]) > 255 {
return false, errCSITooLong
// Sanity checks: if a sequence has grown absurdly long, stop
// accumulating state and just swallow bytes until its final byte —
// much better than leaking the accumulated garbage into the view.
if len(ei.csiParam) > 20 || (len(ei.csiParam) > 0 && len(ei.csiParam[len(ei.csiParam)-1]) > 255) {
ei.state = stateCSIDiscard
ei.csiParam = nil
return true, nil
}
ei.curch = string(ch)
@@ -151,6 +259,12 @@ func (ei *escapeInterpreter) parseOne(ch []byte) (isEscape bool, err error) {
characterEquals(ch, '+'):
ei.state = stateCharacterSetDesignation
return true, nil
case len(ch) == 1 && ch[0] >= 0x30 && ch[0] <= 0x7E:
// Single-byte ESC sequence (e.g. ESC c = RIS). We don't
// interpret these, but we must consume them so they don't
// leak into the view as literal text.
ei.state = stateNone
return true, nil
default:
return false, errNotCSI
}
@@ -164,8 +278,38 @@ func (ei *escapeInterpreter) parseOne(ch []byte) (isEscape bool, err error) {
ei.csiParam = append(ei.csiParam, "")
case characterEquals(ch, 'm'):
ei.csiParam = append(ei.csiParam, "0")
case characterEquals(ch, 'K'):
// fall through
case characterEquals(ch, 'K'),
characterEquals(ch, 'H'), characterEquals(ch, 'f'), characterEquals(ch, 'd'),
characterEquals(ch, 'B'), characterEquals(ch, 'E'),
characterEquals(ch, 'C'):
// fall through — let stateParams handle these with default
// params (CUP/VPA default to row 1, CUD/CNL/CUF default to
// advance by 1).
case characterEquals(ch, ';'):
// Empty first param ([;Xm ≡ [0;Xm). Seed a slot for the
// empty param; stateParams will append the next one when it
// re-reads this ';' via the fallthrough.
ei.csiParam = append(ei.csiParam, "")
case len(ch) == 1 && ch[0] >= 0x3C && ch[0] <= 0x3F:
// Private-mode prefix byte (<, =, >, ?). We don't interpret
// DEC private-mode sequences, but must consume them so they
// don't leak into the view as literal text. Seed an empty
// param so the subsequent digits land on a valid slot.
ei.csiParam = append(ei.csiParam, "")
ei.state = stateParams
return true, nil
case len(ch) == 1 && ch[0] >= 0x20 && ch[0] <= 0x2F:
// CSI intermediate byte. A sequence with intermediates is
// one we don't implement; consume the rest until the final
// byte.
ei.state = stateCSIDiscard
ei.csiParam = nil
return true, nil
case len(ch) == 1 && ch[0] >= 0x40 && ch[0] <= 0x7E:
// Valid CSI final byte we don't implement — swallow.
ei.state = stateNone
ei.csiParam = nil
return true, nil
default:
return false, errCSIParseError
}
@@ -180,10 +324,16 @@ func (ei *escapeInterpreter) parseOne(ch []byte) (isEscape bool, err error) {
ei.csiParam = append(ei.csiParam, "")
return true, nil
case characterEquals(ch, 'm'):
// outputCSI applies params left-to-right and mutates as it
// goes, so on failure some leading params may already have
// taken effect (e.g. `[1;;m` would leave AttrBold set before
// hitting the empty param). Snapshot the colors beforehand
// and restore them on error so a malformed SGR is truly a
// no-op rather than a partial apply.
savedFg, savedBg := ei.curFgColor, ei.curBgColor
if err := ei.outputCSI(); err != nil {
return false, errCSIParseError
ei.curFgColor, ei.curBgColor = savedFg, savedBg
}
ei.state = stateNone
ei.csiParam = nil
return true, nil
@@ -203,12 +353,71 @@ func (ei *escapeInterpreter) parseOne(ch []byte) (isEscape bool, err error) {
ei.instruction = noInstruction{}
}
ei.state = stateNone
ei.csiParam = nil
return true, nil
case characterEquals(ch, 'H'), characterEquals(ch, 'f'),
characterEquals(ch, 'd'):
// CUP / HVP (absolute (row, col), col ignored) or VPA (absolute row).
targetRow := ei.firstParamOrDefault(1)
if targetRow <= 1 {
// Cursor home. ConPTY emits this (after [2J) at the start of
// every screen, so it marks where ConPTY's coordinate origin
// now sits. Re-anchor our row tracking to the current write
// position rather than treating it as a backward move: a view
// that isn't rewound in lockstep with ConPTY's screen (the
// command log) would otherwise carry stale drift, making every
// later absolute CUP compute a negative, dropped advance and
// collapsing the blank rows ConPTY positioned with.
ei.resetScreenCursor()
} else {
// Skip forward to the target row; ignore backward moves.
ei.emitCursorAdvance(targetRow - ei.screenRow)
}
ei.state = stateNone
ei.csiParam = nil
return true, nil
case characterEquals(ch, 'B'), characterEquals(ch, 'E'):
// CUD / CNL — relative row advance by N. CNL also resets
// the column, which we don't track, so the two are
// equivalent for our purposes.
ei.emitCursorAdvance(ei.firstParamOrDefault(1))
ei.state = stateNone
ei.csiParam = nil
return true, nil
case characterEquals(ch, 'C'):
// CUF — cursor forward N. Emit space cells so the gap
// renders. (screenCol is updated by the view via
// notifyCellsWritten as those spaces are emitted.)
ei.instruction = cursorForward{n: ei.firstParamOrDefault(1)}
ei.state = stateNone
ei.csiParam = nil
return true, nil
case len(ch) == 1 && ch[0] >= 0x20 && ch[0] <= 0x2F:
// CSI intermediate byte after params. The final byte will
// have a semantic we don't implement (e.g. `[0 q` =
// DECSCUSR); consume everything until it arrives.
ei.state = stateCSIDiscard
ei.csiParam = nil
return true, nil
case len(ch) == 1 && ch[0] >= 0x40 && ch[0] <= 0x7E:
// Valid CSI final byte we don't implement — swallow the
// whole sequence rather than printing it as text.
ei.state = stateNone
ei.csiParam = nil
return true, nil
default:
return false, errCSIParseError
}
case stateCSIDiscard:
// Consume the rest of a CSI sequence whose semantic we don't
// interpret (one with intermediate bytes, or one the sanity
// checks at the top of parseOne bailed out of). Any byte in the
// final-byte range ends it.
if len(ch) == 1 && ch[0] >= 0x40 && ch[0] <= 0x7E {
ei.state = stateNone
}
return true, nil
case stateOSC:
if characterEquals(ch, '8') {
ei.state = stateOSCWaitForParams
@@ -220,7 +429,13 @@ func (ei *escapeInterpreter) parseOne(ch []byte) (isEscape bool, err error) {
return true, nil
case stateOSCWaitForParams:
if !characterEquals(ch, ';') {
return true, errOSCParseError
// Malformed OSC 8 (expected ';' after '8'). Rather than
// erroring — which would reset state mid-OSC and cause the
// rest of the sequence to leak as literal text — treat the
// whole OSC as one we don't understand and skip to its
// terminator.
ei.state = stateOSCSkipUnknown
return true, nil
}
ei.state = stateOSCParams
+116
View File
@@ -1,6 +1,7 @@
package gocui
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
@@ -150,6 +151,121 @@ func TestParseOneColours(t *testing.T) {
}
}
func TestParseOneIgnoresUnknownSequences(t *testing.T) {
// Escape sequences the interpreter doesn't implement -- whether well-formed-but-unsupported
// (private modes, DECSCUSR, …) or outright malformed -- must be silently
// consumed rather than leaked into the view as literal text.
scenarios := []string{
"\x1b[?9001h", // DEC private-mode set (?-prefix)
"\x1b[?25l", // hide cursor
"\x1b[?25h", // show cursor
"\x1b[2;J", // erase display (unusual 2;J variant)
"\x1b[H", // cursor home — re-anchors to row 1 (no-op when already there)
"\x1bc", // RIS — single-char ESC sequence
"\x1b[;5H", // empty first param — defaults to row 1, no-op
"\x1b[ q", // intermediate byte with no params (DECSCUSR family)
"\x1b[0 q", // intermediate byte after a param
"\x1b[1;;m", // malformed SGR: empty middle param
"\x1b]8bogus\x07", // OSC 8 missing ';'
"\x1b[" + strings.Repeat("0", 300) + "m", // single param overflows length cap
"\x1b[" + strings.Repeat("1;", 25) + "1m", // too many params
}
for _, input := range scenarios {
ei := newEscapeInterpreter(OutputNormal)
parseEscRunes(t, ei, input)
// An unimplemented/malformed sequence must leave no trace: no
// pending instruction, no color change.
_, noop := ei.instruction.(noInstruction)
assert.True(t, noop, "input %q left a pending instruction", input)
assert.Equal(t, ColorDefault, ei.curFgColor, "input %q mutated fg color", input)
assert.Equal(t, ColorDefault, ei.curBgColor, "input %q mutated bg color", input)
}
}
func TestParseOneCursorPositioning(t *testing.T) {
// Cursor-positioning escapes that advance the row forward emit a
// cursorDown instruction; backward / same-row moves are ignored
// because the view's buffer is line-based.
scenarios := []struct {
input string
startRow int // parser's screenRow before parsing
wantAdvance int // 0 means "no instruction emitted"
}{
{"\x1b[5;1H", 1, 4}, // CUP — absolute row 5 from row 1
{"\x1b[5H", 1, 4}, // CUP with only the row param
{"\x1b[5;1H", 5, 0}, // CUP to the same row we're on — no-op
{"\x1b[2;1H", 5, 0}, // CUP backward — ignored
{"\x1b[5;1f", 1, 4}, // HVP alias for CUP
{"\x1b[5d", 1, 4}, // VPA — absolute row
{"\x1b[2d", 5, 0}, // VPA backward — ignored
{"\x1b[3B", 1, 3}, // CUD — relative
{"\x1b[B", 1, 1}, // CUD with default param of 1
{"\x1b[2E", 1, 2}, // CNL — relative
}
for _, s := range scenarios {
ei := newEscapeInterpreter(OutputNormal)
ei.screenRow = s.startRow
parseEscRunes(t, ei, s.input)
if s.wantAdvance == 0 {
_, noop := ei.instruction.(noInstruction)
assert.True(t, noop, "input %q at row %d should be a no-op", s.input, s.startRow)
} else {
cd, ok := ei.instruction.(cursorDown)
if assert.True(t, ok, "input %q at row %d should emit cursorDown", s.input, s.startRow) {
assert.Equal(t, s.wantAdvance, cd.n, "input %q at row %d", s.input, s.startRow)
}
}
}
}
func TestParseOneCursorHomeReanchors(t *testing.T) {
// ConPTY emits cursor-home ([H) after [2J at the start of every screen.
// In a view that isn't rewound in lockstep with ConPTY (the command log)
// screenRow has drifted, so home must re-anchor it to the current write
// position rather than be dropped as a backward move — otherwise the
// absolute CUPs that follow compute negative, dropped advances and the
// rows ConPTY positioned with collapse together.
ei := newEscapeInterpreter(OutputNormal)
ei.screenRow = 12 // accumulated drift from earlier command-log output
parseEscRunes(t, ei, "\x1b[H")
assert.Equal(t, 1, ei.screenRow, "home should re-anchor screenRow")
_, noop := ei.instruction.(noInstruction)
assert.True(t, noop, "home should not emit an instruction")
// A subsequent CUP now advances relative to the re-anchored origin.
parseEscRunes(t, ei, "\x1b[3;1H")
cd, ok := ei.instruction.(cursorDown)
if assert.True(t, ok, "CUP after home should emit cursorDown") {
assert.Equal(t, 2, cd.n)
}
}
func TestParseOneCursorForward(t *testing.T) {
// CUF (\x1b[NC) emits a cursorForward instruction so the view can
// materialize the N-cell gap as spaces. ConPTY uses this (often
// paired with ECH) to encode runs of default-colored spaces.
scenarios := []struct {
input string
wantN int
}{
{"\x1b[5C", 5},
{"\x1b[1C", 1},
{"\x1b[C", 1}, // no param defaults to 1
}
for _, s := range scenarios {
ei := newEscapeInterpreter(OutputNormal)
parseEscRunes(t, ei, s.input)
cf, ok := ei.instruction.(cursorForward)
if assert.True(t, ok, "input %q should emit cursorForward", s.input) {
assert.Equal(t, s.wantN, cf.n, "input %q", s.input)
}
}
}
func parseEscRunes(t *testing.T, ei *escapeInterpreter, runes string) {
t.Helper()
for _, b := range []byte(runes) {
+135 -24
View File
@@ -9,11 +9,13 @@ import (
"runtime"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/gdamore/tcell/v3"
"github.com/go-errors/errors"
"github.com/jesseduffield/generics/set"
"github.com/petermattis/goid"
"github.com/rivo/uniseg"
"github.com/samber/lo"
)
@@ -103,6 +105,7 @@ type replayedEvents struct {
Keys chan *TcellKeyEventWrapper
Resizes chan *TcellResizeEventWrapper
MouseEvents chan *TcellMouseEventWrapper
FocusEvents chan *TcellFocusEventWrapper
}
type RecordingConfig struct {
@@ -192,7 +195,18 @@ type Gui struct {
taskManager *TaskManager
// The task of the event currently being processed on the main goroutine, if
// any. Only touched from the main goroutine (in processEvent). It's excluded
// from the Busy() check so that an event handler asking "is anything else
// busy?" doesn't count itself.
currentTask Task
lastHoverView *View
// uiThreadID is the goroutine id of the main event loop, recorded when
// MainLoop starts. IsUIThread compares against it. Written once, read from
// worker goroutines, so it's atomic.
uiThreadID atomic.Int64
}
type NewGuiOpts struct {
@@ -237,7 +251,12 @@ func NewGui(opts NewGuiOpts) (*Gui, error) {
g.stop = make(chan struct{})
g.gEvents = make(chan GocuiEvent, 20)
g.userEvents = make(chan userEvent, 20)
// Update does a non-blocking send and panics on a full channel rather than
// blocking (which would deadlock the UI goroutine against itself) or
// silently reordering. The buffer is sized well above the peak occupancy we
// see in practice, so the panic stays unreachable in normal use; if it ever
// fires, that's a real anomaly to investigate, not a cue to grow the buffer.
g.userEvents = make(chan userEvent, 256)
g.taskManager = newTaskManager()
if opts.PlayRecording {
@@ -245,6 +264,7 @@ func NewGui(opts NewGuiOpts) (*Gui, error) {
Keys: make(chan *TcellKeyEventWrapper),
Resizes: make(chan *TcellResizeEventWrapper),
MouseEvents: make(chan *TcellMouseEventWrapper),
FocusEvents: make(chan *TcellFocusEventWrapper),
}
}
@@ -266,7 +286,22 @@ func NewGui(opts NewGuiOpts) (*Gui, error) {
}
func (g *Gui) NewTask() *TaskImpl {
return g.taskManager.NewTask()
return g.taskManager.NewTask(false)
}
// NewBackgroundTask creates a task that is tracked for idle detection but does
// not count towards the program being busy for repo-switch safety. See
// TaskImpl.background.
func (g *Gui) NewBackgroundTask() *TaskImpl {
return g.taskManager.NewTask(true)
}
// Busy reports whether any foreground work is in flight, ignoring the event
// currently being processed on the main goroutine (see currentTask). Background
// routines (auto-fetch etc.) don't count. It's used to decide whether it's safe
// to switch repos. Must be called on the main goroutine.
func (g *Gui) Busy() bool {
return g.taskManager.hasBusyForegroundTaskExcept(g.currentTask)
}
// An idle listener listens for when the program is idle. This is useful for
@@ -611,36 +646,91 @@ type userEvent struct {
contentOnly bool
}
// Update executes the passed function. This method can be called safely from a
// goroutine in order to update the GUI. It is important to note that the
// passed function won't be executed immediately, instead it will be added to
// the user events queue. Given that Update spawns a goroutine, the order in
// which the user events will be handled is not guaranteed.
// Update enqueues f on the user-events channel for the UI loop to run on its
// next iteration. Multiple Update calls from the same goroutine arrive in
// source order via the channel's FIFO. The send is non-blocking — if the
// channel is full we panic rather than block or silently reorder, since a
// blocked send from the UI goroutine would deadlock against itself and
// silently switching to inline execution would break the ordering guarantee
// callers rely on. The buffer is sized generously enough that this should
// never fire in practice; if it does, that's a signal to investigate, not
// to grow the buffer reflexively.
func (g *Gui) Update(f func(*Gui) error) {
task := g.NewTask()
go g.updateAsyncAux(f, task)
g.update(f, false)
}
// UpdateAsync is a version of Update that does not spawn a go routine, it can
// be a bit more efficient in cases where Update is called many times like when
// tailing a file. In general you should use Update()
func (g *Gui) UpdateAsync(f func(*Gui) error) {
task := g.NewTask()
g.updateAsyncAux(f, task)
// Like Update, but the enqueued work is a background routine (or triggered by
// one), so it doesn't count towards the program being busy for repo-switch
// safety. See TaskImpl.background.
func (g *Gui) UpdateBackground(f func(*Gui) error) {
g.update(f, true)
}
func (g *Gui) updateAsyncAux(f func(*Gui) error, task Task) {
g.userEvents <- userEvent{f: f, task: task}
func (g *Gui) update(f func(*Gui) error, background bool) {
task := g.taskManager.NewTask(background)
select {
case g.userEvents <- userEvent{f: f, task: task}:
default:
panic("gocui: userEvents channel full; refusing to block or reorder")
}
}
// Like Update, but signals that the callback only modifies content.
func (g *Gui) UpdateContentOnly(f func(*Gui) error) {
task := g.NewTask()
g.updateContentOnly(f, false)
}
// Like UpdateContentOnly, but for background work (see UpdateBackground).
func (g *Gui) UpdateContentOnlyBackground(f func(*Gui) error) {
g.updateContentOnly(f, true)
}
func (g *Gui) updateContentOnly(f func(*Gui) error, background bool) {
task := g.taskManager.NewTask(background)
g.userEvents <- userEvent{f: f, task: task, contentOnly: true}
}
// IsUIThread reports whether the caller is running on the main event-loop
// goroutine (the one running MainLoop). It calls goid.Get, so use it only for
// debug assertions, not to drive production control flow.
func (g *Gui) IsUIThread() bool {
return goid.Get() == g.uiThreadID.Load()
}
// OnUIThreadAndWait runs f on the main event-loop goroutine and blocks the
// caller until f has run, returning f's error. Use it to read UI-thread-owned
// state (the model, contexts) from a worker without racing the UI thread.
//
// It must be called from a worker goroutine, never from the UI thread itself:
// the UI thread would block waiting for a callback only it can run, which
// deadlocks. Callers arrange this by construction (see the refresh helper's
// RefreshFromWorker); a debug-only assertion there guards against getting it
// wrong.
func (g *Gui) OnUIThreadAndWait(f func() error) error {
return g.onUIThreadAndWait(f, false)
}
// Like OnUIThreadAndWait, but the enqueued work belongs to a background routine,
// so it doesn't count towards the program being busy (see UpdateBackground).
func (g *Gui) OnUIThreadAndWaitBackground(f func() error) error {
return g.onUIThreadAndWait(f, true)
}
func (g *Gui) onUIThreadAndWait(f func() error, background bool) error {
enqueue := g.Update
if background {
enqueue = g.UpdateBackground
}
result := make(chan error, 1)
enqueue(func(*Gui) error {
result <- f()
return nil
})
return <-result
}
// Calls a function in a goroutine. Handles panics gracefully and tracks
// number of background tasks.
// Always use this when you want to spawn a goroutine and you want lazygit to
@@ -648,7 +738,18 @@ func (g *Gui) UpdateContentOnly(f func(*Gui) error) {
// background goroutines where you wouldn't want lazygit to be considered busy
// (i.e. when you wouldn't want a loader to be shown to the user)
func (g *Gui) OnWorker(f func(Task) error) {
task := g.NewTask()
g.onWorker(f, false)
}
// Like OnWorker, but for a background routine (or work triggered by one), so it
// doesn't count towards the program being busy for repo-switch safety. See
// TaskImpl.background.
func (g *Gui) OnWorkerBackground(f func(Task) error) {
g.onWorker(f, true)
}
func (g *Gui) onWorker(f func(Task) error, background bool) {
task := g.taskManager.NewTask(background)
go func() {
g.onWorkerAux(f, task)
task.Done()
@@ -712,6 +813,8 @@ func (g *Gui) SetManagerFunc(manager func(*Gui) error) {
// MainLoop runs the main loop until an error is returned. A successful
// finish should return ErrQuit.
func (g *Gui) MainLoop() error {
g.uiThreadID.Store(goid.Get())
go func() {
for {
select {
@@ -756,17 +859,25 @@ func (g *Gui) handleError(err error) error {
func (g *Gui) processEvent() error {
contentOnly := false
// currentTask is the task of the event we're about to handle; recording it
// lets Busy() ignore it, so a handler asking "is anything else busy?" (the
// repo-switch guard does) doesn't count itself. Handlers of the remaining
// events drained below run with currentTask still set to this primary event;
// that's fine because the only Busy() callers are keybinding handlers, which
// are always the primary event here.
select {
case ev := <-g.gEvents:
task := g.NewTask()
defer func() { task.Done() }()
g.currentTask = task
defer func() { g.currentTask = nil; task.Done() }()
if err := g.handleError(g.handleEvent(&ev)); err != nil {
return err
}
case ev := <-g.userEvents:
contentOnly = ev.contentOnly
defer func() { ev.task.Done() }()
g.currentTask = ev.task
defer func() { g.currentTask = nil; ev.task.Done() }()
if err := g.handleError(ev.f(g)); err != nil {
return err
@@ -1345,7 +1456,7 @@ func (g *Gui) onKey(ev *GocuiEvent) error {
}
visibleLineWidth := 0
for _, c := range v.lines[newY] {
for _, c := range v.lines[newY].cells {
visibleLineWidth += c.width
}
if visibleLineWidth < newX {
+21 -1
View File
@@ -8,8 +8,9 @@ type Task interface {
Done()
Pause()
Continue()
// not exporting because we don't need to
// not exporting these because we don't need to
isBusy() bool
isBackground() bool
}
type TaskImpl struct {
@@ -17,6 +18,17 @@ type TaskImpl struct {
busy bool
onDone func()
withMutex func(func())
// Background tasks don't count towards the program being "busy" for the
// purpose of deciding whether a repo switch is safe (see
// TaskManager.hasBusyForegroundTaskExcept). Two kinds of work are tagged
// this way: the ongoing background routines (auto-fetch, files refresh,
// external-change detection) and the refreshes they trigger, whose model
// writes are already guarded against a concurrent repo switch by the repo
// generation; and view-buffer content rendering, which only paints a view
// and so is harmless to leave running across a switch. What stays
// foreground is lazygit driving a git operation and applying its results
// to the model — exactly the work a repo switch must not run underneath.
background bool
}
func (self *TaskImpl) Done() {
@@ -39,6 +51,10 @@ func (self *TaskImpl) isBusy() bool {
return self.busy
}
func (self *TaskImpl) isBackground() bool {
return self.background
}
type TaskStatus int
const (
@@ -73,6 +89,10 @@ func (self *FakeTask) isBusy() bool {
return self.status == TaskStatusBusy
}
func (self *FakeTask) isBackground() bool {
return false
}
func (self *FakeTask) Status() TaskStatus {
return self.status
}
+24 -2
View File
@@ -22,7 +22,7 @@ func newTaskManager() *TaskManager {
}
}
func (self *TaskManager) NewTask() *TaskImpl {
func (self *TaskManager) NewTask(background bool) *TaskImpl {
self.mutex.Lock()
defer self.mutex.Unlock()
@@ -30,12 +30,34 @@ func (self *TaskManager) NewTask() *TaskImpl {
taskId := self.nextId
onDone := func() { self.delete(taskId) }
task := &TaskImpl{id: taskId, busy: true, onDone: onDone, withMutex: self.withMutex}
task := &TaskImpl{id: taskId, busy: true, background: background, onDone: onDone, withMutex: self.withMutex}
self.tasks[taskId] = task
return task
}
// hasBusyForegroundTaskExcept reports whether any task other than `ignore` is
// currently busy and not a background task. It's used to decide whether a repo
// switch is safe: a foreground operation (or the refresh it triggers, or that
// refresh's follow-up callbacks) still in flight means the switch must wait, so
// it doesn't run against a repo that's about to be swapped out.
//
// `ignore` is the event currently being processed on the UI thread — the switch
// attempt itself — which is always busy and so must not count as a reason to
// refuse itself.
func (self *TaskManager) hasBusyForegroundTaskExcept(ignore Task) bool {
self.mutex.Lock()
defer self.mutex.Unlock()
for _, task := range self.tasks {
if task != ignore && task.isBusy() && !task.isBackground() {
return true
}
}
return false
}
func (self *TaskManager) addIdleListener(c chan struct{}) {
self.idleListeners = append(self.idleListeners, c)
}
+63
View File
@@ -0,0 +1,63 @@
package gocui
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestTaskManagerHasBusyForegroundTaskExcept(t *testing.T) {
t.Run("no tasks", func(t *testing.T) {
tm := newTaskManager()
assert.False(t, tm.hasBusyForegroundTaskExcept(nil))
})
t.Run("a busy foreground task counts", func(t *testing.T) {
tm := newTaskManager()
tm.NewTask(false)
assert.True(t, tm.hasBusyForegroundTaskExcept(nil))
})
t.Run("a busy background task does not count", func(t *testing.T) {
tm := newTaskManager()
tm.NewTask(true)
assert.False(t, tm.hasBusyForegroundTaskExcept(nil))
})
t.Run("a done foreground task does not count", func(t *testing.T) {
tm := newTaskManager()
task := tm.NewTask(false)
task.Done()
assert.False(t, tm.hasBusyForegroundTaskExcept(nil))
})
t.Run("a paused foreground task does not count", func(t *testing.T) {
tm := newTaskManager()
task := tm.NewTask(false)
task.Pause()
assert.False(t, tm.hasBusyForegroundTaskExcept(nil))
})
t.Run("the ignored task does not count", func(t *testing.T) {
tm := newTaskManager()
task := tm.NewTask(false)
assert.False(t, tm.hasBusyForegroundTaskExcept(task))
})
t.Run("another foreground task counts even when one is ignored", func(t *testing.T) {
tm := newTaskManager()
ignored := tm.NewTask(false)
tm.NewTask(false)
assert.True(t, tm.hasBusyForegroundTaskExcept(ignored))
})
t.Run("only a background task alongside the ignored current event", func(t *testing.T) {
// This is the repo-switch case: the switch is handled as the current
// event (ignored) while a background refresh is in flight; it must not
// be considered busy.
tm := newTaskManager()
current := tm.NewTask(false)
tm.NewTask(true)
assert.False(t, tm.hasBusyForegroundTaskExcept(current))
})
}
+18
View File
@@ -266,6 +266,22 @@ func (wrapper TcellResizeEventWrapper) toTcellEvent() tcell.Event {
return tcell.NewEventResize(wrapper.Width, wrapper.Height)
}
type TcellFocusEventWrapper struct {
Timestamp int64
Focused bool
}
func NewTcellFocusEventWrapper(event *tcell.EventFocus, timestamp int64) *TcellFocusEventWrapper {
return &TcellFocusEventWrapper{
Timestamp: timestamp,
Focused: event.Focused,
}
}
func (wrapper TcellFocusEventWrapper) toTcellEvent() tcell.Event {
return tcell.NewEventFocus(wrapper.Focused)
}
// pollEvent get tcell.Event and transform it into gocuiEvent
func (g *Gui) pollEvent() GocuiEvent {
var tev tcell.Event
@@ -277,6 +293,8 @@ func (g *Gui) pollEvent() GocuiEvent {
tev = (ev).toTcellEvent()
case ev := <-g.ReplayedEvents.MouseEvents:
tev = (ev).toTcellEvent()
case ev := <-g.ReplayedEvents.FocusEvents:
tev = (ev).toTcellEvent()
}
} else {
tev = <-Screen.EventQ()
+193 -101
View File
@@ -28,12 +28,12 @@ const (
// position.
type View struct {
name string
x0, y0, x1, y1 int // left top right bottom
ox, oy int // view offsets
cx, cy int // cursor position
rx, ry int // Read() offsets
wx, wy int // Write() offsets
lines [][]cell // All the data
x0, y0, x1, y1 int // left top right bottom
ox, oy int // view offsets
cx, cy int // cursor position
rx, ry int // Read() offsets
wx, wy int // Write() offsets
lines []lineType // All the data
outMode OutputMode
// The y position of the first line of a range selection.
// This is not relative to the view's origin: it is relative to the first line
@@ -50,6 +50,14 @@ type View struct {
// tained is true if the viewLines must be updated
tainted bool
// firstDirtyLine is the index of the lowest line in `lines` that has been
// written to or highlighted since viewLines was last refreshed, and whose
// cached wrapping (lineType.wrappedCells) may therefore be stale. Lines
// below it are unchanged and can reuse their cached wrapping instead of
// being re-wrapped, which keeps refreshViewLinesIfNeeded cheap while
// scrolling appends new lines to a long buffer.
firstDirtyLine int
// the last position that the mouse was hovering over; nil if the mouse is outside of
// this view, or not hovering over a cell
lastHoverPosition *pos
@@ -444,6 +452,38 @@ type SearchPosition struct {
type viewLine struct {
linesX, linesY int // coordinates relative to v.lines
line []cell
// Colors used to extend the bg past this wrapped segment's content.
// Derived at wrap time from the source line — see refreshViewLinesIfNeeded
// for the per-segment rule.
trailingFillAttributes *trailingFillAttributes
}
// lineType is one of v.lines: the cells of a source line, plus optional
// trailingFillAttributes recording the colors used to extend the bg
// past the line's content when the writer emitted '\x1b[K'.
type lineType struct {
cells cells
trailingFillAttributes *trailingFillAttributes
// wrappedCells caches the result of wrapping `cells` to `wrappedColumns`
// columns, so that unchanged lines don't have to be re-wrapped on every
// refreshViewLinesIfNeeded (which runs on every scroll event, via
// ViewLinesHeight). Wrapping measures every cell's width and allocates, so
// for a long buffer that dominates the cost of scrolling. The cache is used
// only for lines below View.firstDirtyLine whose wrappedColumns still
// matches the current width; nil means nothing is cached yet.
wrappedCells [][]cell
wrappedColumns int
}
// trailingFillAttributes describes the fg/bg colors that draw() should
// use for cells past the end of a wrapped segment's content. On a source
// line this records what the writer asked for via '\x1b[K' (and so opts
// the line in to trailing fill at all); the per-segment values on each
// viewLine are derived from it at wrap time.
type trailingFillAttributes struct {
fg, bg Attribute
}
type cell struct {
@@ -453,7 +493,7 @@ type cell struct {
hyperlink string
}
type lineType []cell
type cells []cell
func characterEquals(chr []byte, b byte) bool {
return len(chr) == 1 && chr[0] == b
@@ -464,7 +504,7 @@ func isCRLF(chr []byte) bool {
}
// String returns a string from a given cell slice.
func (l lineType) String() string {
func (l cells) String() string {
var str strings.Builder
for _, c := range l {
str.WriteString(c.chr)
@@ -738,20 +778,20 @@ func (v *View) makeWriteable(x, y int) {
}
v.lines = v.lines[:newLen]
} else {
v.lines = append(v.lines, nil)
v.lines = append(v.lines, lineType{})
}
}
// cell `x` need not be index-able (that's why `<`)
// append should be used by `lines[y]` user if he wants to write beyond `x`
for len(v.lines[y]) < x {
if cap(v.lines[y]) > len(v.lines[y]) {
newLen := cap(v.lines[y])
for len(v.lines[y].cells) < x {
if cap(v.lines[y].cells) > len(v.lines[y].cells) {
newLen := cap(v.lines[y].cells)
if newLen > x {
newLen = x
}
v.lines[y] = v.lines[y][:newLen]
v.lines[y].cells = v.lines[y].cells[:newLen]
} else {
v.lines[y] = append(v.lines[y], cell{})
v.lines[y].cells = append(v.lines[y].cells, cell{})
}
}
}
@@ -761,7 +801,7 @@ func (v *View) makeWriteable(x, y int) {
func (v *View) writeCells(cells []cell) {
var newLen int
// use maximum len available
line := v.lines[v.wy][:cap(v.lines[v.wy])]
line := v.lines[v.wy].cells[:cap(v.lines[v.wy].cells)]
maxCopy := len(line) - v.wx
if maxCopy < len(cells) {
copy(line[v.wx:], cells[:maxCopy])
@@ -770,11 +810,11 @@ func (v *View) writeCells(cells []cell) {
} else { // maxCopy >= len(cells)
copy(line[v.wx:], cells)
newLen = v.wx + len(cells)
if newLen < len(v.lines[v.wy]) {
newLen = len(v.lines[v.wy])
if newLen < len(v.lines[v.wy].cells) {
newLen = len(v.lines[v.wy].cells)
}
}
v.lines[v.wy] = line[:newLen]
v.lines[v.wy].cells = line[:newLen]
v.wx += len(cells)
}
@@ -793,6 +833,9 @@ func (v *View) Write(p []byte) (n int, err error) {
func (v *View) write(p []byte) {
v.tainted = true
// write only ever touches lines from v.wy onwards, so any cached wrapping
// below that stays valid.
v.firstDirtyLine = min(v.firstDirtyLine, v.wy)
v.clearHover()
// Fill with empty cells, if writing outside current view buffer
@@ -800,26 +843,19 @@ func (v *View) write(p []byte) {
finishLine := func() {
v.autoRenderHyperlinksInCurrentLine()
if v.wx >= len(v.lines[v.wy]) {
v.writeCells([]cell{{
chr: "",
width: 0,
fgColor: 0,
bgColor: 0,
}})
}
}
advanceToNextLine := func() {
v.wx = 0
v.wy++
if v.wy >= len(v.lines) {
v.lines = append(v.lines, nil)
v.lines = append(v.lines, lineType{})
}
}
if v.pendingNewline {
advanceToNextLine()
v.ei.notifyRowAdvance()
v.pendingNewline = false
}
@@ -841,17 +877,37 @@ func (v *View) write(p []byte) {
case characterEquals(chr, '\n') || isCRLF(chr):
finishLine()
advanceToNextLine()
v.ei.notifyRowAdvance()
case characterEquals(chr, '\r'):
finishLine()
v.wx = 0
v.ei.notifyColumnReset()
default:
truncateLine, cells := v.parseInput(chr, width, v.wx, v.wy)
if cd, ok := v.ei.instruction.(cursorDown); ok {
v.ei.instructionRead()
for range cd.n {
v.autoRenderHyperlinksInCurrentLine()
advanceToNextLine()
}
}
if cells == nil {
continue
}
v.writeCells(cells)
if truncateLine {
v.lines[v.wy] = v.lines[v.wy][:v.wx]
v.lines[v.wy].cells = v.lines[v.wy].cells[:v.wx]
}
// Soft-wrap tracking. truncateLine is true exactly when the
// cells are from \x1b[K filling to end of line — ConPTY
// doesn't advance the cursor for that, so we shouldn't count
// it toward wraps either.
if !truncateLine {
totalWidth := 0
for _, c := range cells {
totalWidth += c.width
}
v.ei.notifyCellsWritten(totalWidth, v.InnerWidth())
}
}
}
@@ -910,7 +966,7 @@ func (v *View) autoRenderHyperlinksInCurrentLine() {
return
}
line := v.lines[v.wy]
line := v.lines[v.wy].cells
start := 0
for {
linkStart := findLinkStart(line[start:])
@@ -927,7 +983,7 @@ func (v *View) autoRenderHyperlinksInCurrentLine() {
link.WriteString(line[linkEnd].chr)
}
for i := linkStart; i < linkEnd; i++ {
v.lines[v.wy][i].hyperlink = link.String()
v.lines[v.wy].cells[i].hyperlink = link.String()
}
start = linkEnd
}
@@ -955,16 +1011,27 @@ func (v *View) parseInput(ch []byte, width int, x int, _ int) (bool, []cell) {
} else {
repeatCount := 1
if _, ok := v.ei.instruction.(eraseInLineFromCursor); ok {
// fill rest of line
// Discard any old content past the cursor and record the
// fill colors so draw() paints the trailing area with them.
// This extends the bg to the right edge in both the
// content-fits and content-wraps cases — for the latter,
// the metadata is what reaches every wrapped segment past
// the last word.
v.ei.instructionRead()
cx := 0
for _, cell := range v.lines[v.wy][0:v.wx] {
cx += cell.width
truncateLine = true
v.lines[v.wy].trailingFillAttributes = &trailingFillAttributes{
fg: v.ei.curFgColor,
bg: v.ei.curBgColor,
}
repeatCount = v.InnerWidth() - cx
return truncateLine, []cell{}
} else if cf, ok := v.ei.instruction.(cursorForward); ok {
// emit `n` space cells under the parser-tracked SGR — used
// to materialize ConPTY's compressed runs of spaces (which
// it emits as ECH+CUF instead of literal whitespace).
v.ei.instructionRead()
repeatCount = cf.n
ch = []byte{' '}
width = 1
truncateLine = true
} else if isEscape {
// do not output anything
return truncateLine, nil
@@ -1010,8 +1077,8 @@ func (v *View) Read(p []byte) (n int, err error) {
v.readBuffer = nil
}
for v.ry < len(v.lines) {
for v.rx < len(v.lines[v.ry]) {
s := v.lines[v.ry][v.rx].chr
for v.rx < len(v.lines[v.ry].cells) {
s := v.lines[v.ry].cells[v.rx].chr
count := len(s)
copy(p[offset:], s)
v.rx++
@@ -1099,6 +1166,7 @@ func (v *View) FlushStaleCells() {
func (v *View) rewind() {
v.ei.reset()
v.ei.resetScreenCursor()
v.SetReadPos(0, 0)
v.SetWritePos(0, 0)
@@ -1175,9 +1243,9 @@ func (v *View) updateSearchPositions() {
}
// If a view line exists for this line index:
if v.lines[result.Y] != nil {
if v.lines[result.Y].cells != nil {
// search this view line for the search string
positions := searchPositionsForLine(v.lines[result.Y], result.Y)
positions := searchPositionsForLine(v.lines[result.Y].cells, result.Y)
if len(positions) > 0 {
// If we found any occurrences, add them
v.searcher.searchPositions = append(v.searcher.searchPositions, positions...)
@@ -1248,13 +1316,21 @@ func (v *View) draw() {
}
emptyCell := cell{chr: " ", width: 1, fgColor: ColorDefault, bgColor: ColorDefault}
var prevFgColor Attribute
for y, vline := range v.viewLines[start:] {
if y >= maxY {
break
}
// Decide the colors used for cells past the end of vline.line:
// the source line's trailingFillAttributes (set by '\x1b[K') if
// any, otherwise plain defaults.
trailingCell := emptyCell
if attrs := vline.trailingFillAttributes; attrs != nil {
trailingCell.fgColor = attrs.fg
trailingCell.bgColor = attrs.bg
}
// x tracks the current x position in the view, and cellIdx tracks the
// index of the cell. If we print a double-sized rune, we increment cellIdx
// by one but x by two.
@@ -1277,14 +1353,9 @@ func (v *View) draw() {
// if we're out of cells to write, we'll just print empty cells.
if cellIdx > len(vline.line)-1 {
c = emptyCell
c.fgColor = prevFgColor
c = trailingCell
} else {
c = vline.line[cellIdx]
// capturing previous foreground colour so that if we're using the reverse
// attribute we honour the final character's colour and don't awkwardly switch
// to a new background colour for the remainder of the line
prevFgColor = c.fgColor
}
fgColor := c.fgColor
@@ -1308,30 +1379,64 @@ func (v *View) draw() {
}
func (v *View) refreshViewLinesIfNeeded() {
if v.tainted {
maxX := v.InnerWidth()
lineIdx := 0
lines := v.lines
for i, line := range lines {
wrap := 0
if v.Wrap {
wrap = maxX
}
ls := lineWrap(line, wrap)
for j := range ls {
vline := viewLine{linesX: j, linesY: i, line: ls[j]}
if lineIdx > len(v.viewLines)-1 {
v.viewLines = append(v.viewLines, vline)
} else {
v.viewLines[lineIdx] = vline
}
lineIdx++
}
}
v.tainted = false
if !v.tainted {
return
}
maxX := v.InnerWidth()
wrap := 0
if v.Wrap {
wrap = maxX
}
lineIdx := 0
lines := v.lines
for i := range lines {
line := &lines[i]
// Reuse the previously wrapped result for lines that haven't changed
// since the last refresh (i.e. below firstDirtyLine) and were wrapped at
// the current width. Wrapping is expensive and this loop runs on every
// scroll event, so only the lines that were actually just read (or
// re-highlighted) should be wrapped afresh.
if line.wrappedCells == nil || line.wrappedColumns != wrap || i >= v.firstDirtyLine {
line.wrappedCells = lineWrap(line.cells, wrap)
line.wrappedColumns = wrap
}
ls := line.wrappedCells
for j := range ls {
// Per-segment trailing fill. When the source line opted in
// via '\x1b[K', the LAST wrapped segment uses those colors
// directly; earlier segments use the colors of their own
// last cell, so the trailing area matches the bg active
// where that segment ended rather than bleeding the
// '\x1b[K' bg back across color changes in the line.
var attrs *trailingFillAttributes
if line.trailingFillAttributes != nil {
if j == len(ls)-1 {
attrs = line.trailingFillAttributes
} else if len(ls[j]) > 0 {
last := ls[j][len(ls[j])-1]
attrs = &trailingFillAttributes{fg: last.fgColor, bg: last.bgColor}
}
}
vline := viewLine{
linesX: j, linesY: i, line: ls[j],
trailingFillAttributes: attrs,
}
if lineIdx > len(v.viewLines)-1 {
v.viewLines = append(v.viewLines, vline)
} else {
v.viewLines[lineIdx] = vline
}
lineIdx++
}
}
v.firstDirtyLine = len(lines)
v.tainted = false
}
// if autoscroll is enabled but we only have a single row of cells shown to the
@@ -1411,9 +1516,7 @@ func (v *View) BufferLines() []string {
lines := make([]string, len(v.lines))
for i, l := range v.lines {
str := lineType(l).String()
str = strings.ReplaceAll(str, "\x00", "")
lines[i] = str
lines[i] = l.cells.String()
}
return lines
}
@@ -1434,9 +1537,7 @@ func (v *View) ViewBufferLines() []string {
lines := make([]string, len(v.viewLines))
for i, l := range v.viewLines {
str := lineType(l.line).String()
str = strings.ReplaceAll(str, "\x00", "")
lines[i] = str
lines[i] = cells(l.line).String()
}
return lines
}
@@ -1458,12 +1559,12 @@ func (v *View) ViewLinesHeight() int {
// ViewBuffer returns a string with the contents of the view's buffer that is
// shown to the user.
func (v *View) ViewBuffer() string {
lines := make([][]cell, len(v.viewLines))
strs := make([]string, len(v.viewLines))
for i := range v.viewLines {
lines[i] = v.viewLines[i].line
strs[i] = cells(v.viewLines[i].line).String()
}
return linesToString(lines)
return strings.Join(strs, "\n")
}
// Line returns a string with the line of the view's internal buffer
@@ -1478,7 +1579,7 @@ func (v *View) Line(y int) (string, bool) {
return "", false
}
return lineType(v.lines[y]).String(), true
return v.lines[y].cells.String(), true
}
// Word returns a string with the word of the view's internal buffer
@@ -1489,11 +1590,11 @@ func (v *View) Word(x, y int) (string, bool) {
return "", false
}
if x < 0 || y < 0 || y >= len(v.lines) || x >= len(v.lines[y]) {
if x < 0 || y < 0 || y >= len(v.lines) || x >= len(v.lines[y].cells) {
return "", false
}
str := lineType(v.lines[y]).String()
str := v.lines[y].cells.String()
nl := strings.LastIndexFunc(str[:x], indexFunc)
if nl == -1 {
@@ -1523,9 +1624,8 @@ func (v *View) SetHighlight(y int, on bool) {
return
}
line := v.lines[y]
cells := make([]cell, 0)
for _, c := range line {
cells := make([]cell, 0, len(v.lines[y].cells))
for _, c := range v.lines[y].cells {
if on {
c.bgColor = v.SelBgColor
c.fgColor = v.SelFgColor
@@ -1536,7 +1636,8 @@ func (v *View) SetHighlight(y int, on bool) {
cells = append(cells, c)
}
v.tainted = true
v.lines[y] = cells
v.firstDirtyLine = min(v.firstDirtyLine, y)
v.lines[y].cells = cells
v.clearHover()
}
@@ -1602,17 +1703,10 @@ func lineWrap(line []cell, columns int) [][]cell {
return lines
}
func linesToString(lines [][]cell) string {
func linesToString(lines []lineType) string {
str := make([]string, len(lines))
for i := range lines {
rns := make([]rune, 0, len(lines[i]))
line := lineType(lines[i]).String()
for _, c := range line {
if c != '\x00' {
rns = append(rns, c)
}
}
str[i] = string(rns)
str[i] = lines[i].cells.String()
}
return strings.Join(str, "\n")
@@ -1682,9 +1776,7 @@ func (v *View) SelectedLines() []string {
}
func (v *View) lineContentAtIdx(idx int) string {
line := v.lines[idx]
str := lineType(line).String()
return strings.ReplaceAll(str, "\x00", "")
return v.lines[idx].cells.String()
}
func (v *View) SelectedPoint() (int, int) {
@@ -1788,11 +1880,11 @@ func (v *View) OverwriteLinesAndClearEverythingElse(lineCount int, y int, conten
v.overwriteLines(y, content)
for i := range y {
v.lines[i] = nil
v.lines[i] = lineType{}
}
for i := v.wy + 1; i < len(v.lines); i += 1 {
v.lines[i] = nil
v.lines[i] = lineType{}
}
}
@@ -1935,7 +2027,7 @@ func (v *View) scrollMargin() int {
// foreground color
func (v *View) ContainsColoredText(fgColor string, text string) bool {
for _, line := range v.lines {
if containsColoredTextInLine(fgColor, text, line) {
if containsColoredTextInLine(fgColor, text, line.cells) {
return true
}
}
+263 -12
View File
@@ -8,10 +8,28 @@ import (
"strings"
"testing"
"github.com/gdamore/tcell/v3"
"github.com/gdamore/tcell/v3/color"
"github.com/rivo/uniseg"
"github.com/stretchr/testify/assert"
)
// WithSimulationScreen swaps the package-level Screen for a tcell
// terminfo-backed mock terminal so tests can call view.draw() and
// inspect rendered cells via Screen.Get(). The previous Screen is
// restored on test cleanup.
func WithSimulationScreen(t *testing.T, width, height int) {
t.Helper()
saved := Screen
if err := (&Gui{}).tcellInitSimulation(width, height); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
Screen.Fini()
Screen = saved
})
}
func TestWriteString(t *testing.T) {
tests := []struct {
existingLines []string
@@ -26,17 +44,17 @@ func TestWriteString(t *testing.T) {
{
[]string{},
[]string{"1\n"},
[][]string{{"1", ""}},
[][]string{{"1"}},
},
{
[]string{},
[]string{"1\n", "2\n"},
[][]string{{"1", ""}, {"2", ""}},
[][]string{{"1"}, {"2"}},
},
{
[]string{"a"},
[]string{"1\n"},
[][]string{{"1", ""}},
[][]string{{"1"}},
},
{
[]string{"a\x00"},
@@ -56,12 +74,12 @@ func TestWriteString(t *testing.T) {
{
[]string{},
[]string{"1\r"},
[][]string{{"1", ""}},
[][]string{{"1"}},
},
{
[]string{"a"},
[]string{"1\r"},
[][]string{{"1", ""}},
[][]string{{"1"}},
},
{
[]string{"a\x00"},
@@ -83,14 +101,14 @@ func TestWriteString(t *testing.T) {
for _, test := range tests {
v := NewView("name", 0, 0, 10, 10, OutputNormal)
for _, l := range test.existingLines {
v.lines = append(v.lines, stringToCells(l))
v.lines = append(v.lines, lineType{cells: stringToCells(l)})
}
for _, s := range test.stringsToWrite {
v.writeString(s)
}
var resultingLines [][]string
for _, l := range v.lines {
resultingLines = append(resultingLines, cellsToStrings(l))
resultingLines = append(resultingLines, cellsToStrings(l.cells))
}
assert.Equal(t, test.expectedLines, resultingLines)
}
@@ -126,19 +144,19 @@ func TestAutoRenderingHyperlinks(t *testing.T) {
v.writeString("htt")
// No hyperlinks are generated for incomplete URLs
assert.Equal(t, "", v.lines[0][0].hyperlink)
assert.Equal(t, "", v.lines[0].cells[0].hyperlink)
// Writing more characters to the same line makes the link complete (even
// though we didn't see a newline yet)
v.writeString("ps://example.com")
assert.Equal(t, "https://example.com", v.lines[0][0].hyperlink)
assert.Equal(t, "https://example.com", v.lines[0].cells[0].hyperlink)
v.Clear()
// Valid but incomplete URL
v.writeString("https://exa")
assert.Equal(t, "https://exa", v.lines[0][0].hyperlink)
assert.Equal(t, "https://exa", v.lines[0].cells[0].hyperlink)
// Writing more characters to the same fixes the link
v.writeString("mple.com")
assert.Equal(t, "https://example.com", v.lines[0][0].hyperlink)
assert.Equal(t, "https://example.com", v.lines[0].cells[0].hyperlink)
}
func TestContainsColoredText(t *testing.T) {
@@ -211,11 +229,100 @@ func TestContainsColoredText(t *testing.T) {
}
for i, test := range tests {
v := &View{lines: test.lines}
lines := make([]lineType, len(test.lines))
for j, cells := range test.lines {
lines[j] = lineType{cells: cells}
}
v := &View{lines: lines}
assert.Equal(t, test.expected, v.ContainsColoredText(test.fgColorStr, test.text), "Test %d failed", i)
}
}
func TestWriteCursorPositionEscape(t *testing.T) {
// ConPTY presents its child's output as a screen buffer and uses cursor
// positioning escapes (CUP, `\x1b[<row>;<col>H`) to skip over blank rows
// rather than emitting empty LFs for them. The escape interpreter must
// synthesize the row advances those CUPs imply; otherwise non-blank rows
// that the child separated with blank lines end up adjacent in the view.
v := NewView("name", 0, 0, 20, 10, OutputNormal)
// "a", then "skip to row 3" (i.e. one blank row), then "b".
v.writeString("a\r\n\x1b[3;1Hb\r\n")
got := make([][]string, 0, len(v.lines))
for _, l := range v.lines {
got = append(got, cellsToStrings(l.cells))
}
assert.Equal(t, [][]string{{"a"}, {}, {"b"}}, got)
}
func TestWriteCursorPositionEscapeAcrossWrites(t *testing.T) {
// Mirrors the production flow: bufio.Scanner splits the pty output on
// LF and feeds the view one Write per line (each with a trailing \n
// appended). The parser's screen-row counter must keep ticking across
// writes; otherwise CUPs are evaluated against a stale row and
// overshoot, producing too many blank lines instead of the right
// number.
v := NewView("name", 0, 0, 30, 30, OutputNormal)
v.writeString("a\n")
v.writeString("b\n")
// ConPTY is on row 3 here; CUP to row 5 should skip exactly one row.
v.writeString("c\x1b[5;1Hd\n")
got := make([][]string, 0, len(v.lines))
for _, l := range v.lines {
got = append(got, cellsToStrings(l.cells))
}
assert.Equal(t, [][]string{
{"a"},
{"b"},
{"c"},
{},
{"d"},
}, got)
}
func TestWriteCursorForwardEscape(t *testing.T) {
// ConPTY compresses runs of default-colored spaces into ECH (\x1b[NX,
// "clear N cells, cursor stationary") + CUF (\x1b[NC, "cursor forward
// N") rather than emitting them literally. The interpreter has to
// materialize CUF as N visible spaces; otherwise the gap collapses
// and content that followed the indentation slides left.
v := NewView("name", 0, 0, 20, 10, OutputNormal)
// "a" + ECH 5 + CUF 5 + "b" — visually "a b".
v.writeString("a\x1b[5X\x1b[5Cb\n")
got := make([][]string, 0, len(v.lines))
for _, l := range v.lines {
got = append(got, cellsToStrings(l.cells))
}
assert.Equal(t, [][]string{{"a", " ", " ", " ", " ", " ", "b"}}, got)
}
func TestWriteCursorPositionEscapeWithSoftWraps(t *testing.T) {
// If a logical line is longer than ConPTY's terminal width, ConPTY
// soft-wraps it onto multiple physical rows in its screen, and any
// subsequent CUP is addressed against the post-wrap row count. To
// keep our screen-row counter accurate we have to count those wraps
// as we write the cells. InnerWidth here is 5; "abcdefghij" (10
// cells) wraps onto 2 rows, so ConPTY is on row 3 after the LF and a
// CUP to row 4 should skip exactly one row.
v := NewView("name", 0, 0, 6, 30, OutputNormal) // Width=7, InnerWidth=5
v.writeString("abcdefghij\n")
v.writeString("\x1b[4;1Hxyz\n")
got := make([][]string, 0, len(v.lines))
for _, l := range v.lines {
got = append(got, cellsToStrings(l.cells))
}
assert.Equal(t, [][]string{
{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"},
{},
{"x", "y", "z"},
}, got)
}
func stringToCells(s string) []cell {
var cells []cell
state := -1
@@ -413,3 +520,147 @@ func TestLineWrap(t *testing.T) {
})
}
}
// TestNewlineTerminatedLineClearsTrailingBg verifies that a '\n' resets
// any attributes (e.g. AttrReverse-driven background) past the line's
// content, so a reversed cell at the end doesn't bleed into the empty
// area to the right.
func TestNewlineTerminatedLineClearsTrailingBg(t *testing.T) {
WithSimulationScreen(t, 14, 5)
v := NewView("name", 0, 0, 11, 4, OutputNormal)
// \x1b[7m sets reverse; \x1b[31m sets fg=red. With reverse the cell
// renders with bg=red. The trailing area past "foo" must NOT extend
// the red bg because '\n' marks the line as cleanly terminated.
v.writeString("\x1b[7m\x1b[31mfoo\x1b[0m\n")
v.draw()
// First row: cells 1..3 are "foo" (render with red bg via reverse),
// cells 4..10 are trailing and should be plain default.
for x := 4; x <= 10; x++ {
_, style, _ := Screen.Get(x, 1)
assert.Equal(t, tcell.ColorDefault, style.GetForeground(),
"trailing cell at (%d, 1) should have default fg", x)
assert.False(t, style.HasReverse(),
"trailing cell at (%d, 1) should not have reverse attribute", x)
}
}
// TestUnterminatedReverseLineDoesNotExtend verifies that an unterminated
// line ending with an AttrReverse cell does NOT propagate the reversed
// background past the line's content — matching real terminal behavior
// (try `print '\x1b[7m\x1b[31mfoo'` in a shell). The trailing area
// is rendered as plain default.
func TestUnterminatedReverseLineDoesNotExtend(t *testing.T) {
WithSimulationScreen(t, 14, 5)
v := NewView("name", 0, 0, 11, 4, OutputNormal)
// Reverse + red fg, "foo", no termination. The trailing cells past
// "foo" should be plain default, NOT a continuation of the red bg.
v.writeString("\x1b[7m\x1b[31mfoo")
v.draw()
// Cells 4..10 are trailing and should be default with no reverse.
for x := 4; x <= 10; x++ {
_, style, _ := Screen.Get(x, 1)
assert.Equal(t, tcell.ColorDefault, style.GetForeground(),
"trailing cell at (%d, 1) should have default fg", x)
assert.False(t, style.HasReverse(),
"trailing cell at (%d, 1) should not have reverse attribute", x)
}
}
// TestShortFilledLineExtendsBgWithoutWrap verifies that '\x1b[K' fills
// the rest of the line with the current bg color for a line that's
// short enough to fit within the view's inner width.
func TestShortFilledLineExtendsBgWithoutWrap(t *testing.T) {
WithSimulationScreen(t, 14, 5)
v := NewView("name", 0, 0, 11, 4, OutputNormal)
// \x1b[41m sets bg=red. "hi" fits within InnerWidth=10; \x1b[K should
// fill the remaining 8 cells with red.
v.writeString("\x1b[41mhi\x1b[K\x1b[0m\n")
v.draw()
// All ten cells at (1..10, 1) should have red bg.
for x := 1; x <= 10; x++ {
_, style, _ := Screen.Get(x, 1)
assert.Equal(t, color.Maroon, style.GetBackground(),
"cell at (%d, 1) should have red bg", x)
}
}
// TestWrappedFilledLineExtendsBgToEdge verifies that when a line is
// filled to the edge with \x1b[K (the pattern used by `delta` for diff
// lines) but exceeds the view's inner width, every wrapped segment
// extends the fill background past its content to the right edge.
func TestWrappedFilledLineExtendsBgToEdge(t *testing.T) {
WithSimulationScreen(t, 14, 6)
// View dimensions: Width=12 (x0=0..x1=11), Height=6; InnerWidth=10,
// InnerHeight=4. Frame inset of 1 places content cells at screen
// (1..10, 1..4).
v := NewView("name", 0, 0, 11, 5, OutputNormal)
v.Wrap = true
// Content with spaces so word wrap ends each segment before the
// right edge: "aaa bbb ccc ddd eee" wraps at InnerWidth=10 to three
// segments — "aaa bbb" / "ccc ddd" / "eee". Each row's trailing area
// must pick up the red fill from \x1b[K.
v.writeString("\x1b[41m" + "aaa bbb ccc ddd eee" + "\x1b[0m\x1b[41m\x1b[K\x1b[0m\n")
v.draw()
// All three wrapped rows should have the red fill background across
// the full InnerWidth, including the trailing cells past each row's
// last word.
for y := 1; y <= 3; y++ {
for x := 1; x <= 10; x++ {
_, style, _ := Screen.Get(x, y)
assert.Equal(t, color.Maroon, style.GetBackground(),
"cell at (%d, %d) should have red bg", x, y)
}
}
}
// TestMulticolorWrappedFillUsesLastCellOfEachSegment demonstrates that
// when a wrapped line switches bg color part-way through and ends with
// \x1b[K, the trailing area on each wrapped row should match the bg
// that was active where that row's content ended — not the \x1b[K bg,
// which would bleed the color from the end of the logical line back
// into the earlier wrapped rows.
func TestMulticolorWrappedFillUsesLastCellOfEachSegment(t *testing.T) {
WithSimulationScreen(t, 14, 6)
// View dimensions: Width=12 (x0=0..x1=11), Height=6; InnerWidth=10,
// InnerHeight=4. Frame inset of 1 places content cells at screen
// (1..10, 1..4).
v := NewView("name", 0, 0, 11, 5, OutputNormal)
v.Wrap = true
// Content "aaa bbb ccc" is 11 cells; lineWrap breaks at the space
// between "bbb" and "ccc" (index 7) so segment 1 is "aaa bbb" (red,
// last cell red) and segment 2 is "ccc" (green, last cell green).
// \x1b[K records the green bg on the source line.
v.writeString("\x1b[41maaa bbb\x1b[42m ccc\x1b[K\x1b[0m\n")
v.draw()
// Row 1's content ends with a red cell at x=7, so trailing columns
// 8..10 should pick up red rather than the \x1b[K's green.
for x := 8; x <= 10; x++ {
_, style, _ := Screen.Get(x, 1)
assert.Equal(t, color.Maroon, style.GetBackground(),
"trailing cell at (%d, 1) should have red bg (matching segment's last cell)", x)
}
// Row 2's content ends with a green cell at x=3, so trailing
// columns 4..10 should pick up green (matching both the segment's
// last cell and the \x1b[K bg — these happen to agree here).
for x := 4; x <= 10; x++ {
_, style, _ := Screen.Get(x, 2)
assert.Equal(t, color.Green, style.GetBackground(),
"trailing cell at (%d, 2) should have green bg", x)
}
}
+84 -10
View File
@@ -3,6 +3,7 @@ package gui
import (
"fmt"
"runtime"
"sync/atomic"
"time"
"github.com/jesseduffield/lazygit/pkg/gocui"
@@ -13,17 +14,27 @@ import (
type BackgroundRoutineMgr struct {
gui *Gui
// if we've suspended the gui (e.g. because we've switched to a subprocess)
// we typically want to pause some things that are running like background
// file refreshes
pauseBackgroundRefreshes bool
// When this is greater than zero, the background routines (e.g. file refresh)
// skip their work. We pause them while the gui is suspended (e.g. for a
// subprocess) and while lazygit is itself driving a git operation that would
// otherwise be caught mid-flight (see the waiting-status helpers). It's a
// count rather than a bool because these pause scopes can overlap.
pauseRefreshesCount atomic.Int32
// a channel to trigger an immediate background fetch; we use this when switching repos
triggerFetch chan struct{}
}
func (self *BackgroundRoutineMgr) PauseBackgroundRefreshes(pause bool) {
self.pauseBackgroundRefreshes = pause
if pause {
self.pauseRefreshesCount.Add(1)
} else {
self.pauseRefreshesCount.Add(-1)
}
}
func (self *BackgroundRoutineMgr) backgroundRefreshesPaused() bool {
return self.pauseRefreshesCount.Load() > 0
}
func (self *BackgroundRoutineMgr) startBackgroundRoutines() {
@@ -51,6 +62,17 @@ func (self *BackgroundRoutineMgr) startBackgroundRoutines() {
}
}
if userConfig.Git.AutoDetectExternalChanges {
interval := userConfig.Refresher.ExternalChangeCheckInterval
if interval > 0 {
go utils.Safe(self.startBackgroundExternalChangeDetection)
} else {
self.gui.c.Log.Errorf(
"Value of config option 'refresher.externalChangeCheckInterval' (%d) is invalid, disabling external change detection",
interval)
}
}
if self.gui.Config.GetDebug() {
self.goEvery(time.Second*time.Duration(10), self.gui.stopChan, func(_ bool) error {
formatBytes := func(b uint64) string {
@@ -92,7 +114,7 @@ func (self *BackgroundRoutineMgr) startBackgroundFetch() {
if self.gui.UserConfig().Gui.ShowBottomLine || firstTimeOrRetriggered {
return self.gui.helpers.AppStatus.WithWaitingStatusImpl(self.gui.Tr.FetchingStatus, func(gocui.Task) error {
return self.backgroundFetch()
}, nil)
}, nil, true)
}
return self.backgroundFetch()
@@ -111,11 +133,60 @@ func (self *BackgroundRoutineMgr) startBackgroundFilesRefresh() {
userConfig := self.gui.UserConfig()
self.goEvery(userConfig.Refresher.RefreshIntervalDuration(), self.gui.stopChan, func(_ bool) error {
self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}})
self.gui.c.RefreshFromWorker(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}, Background: true})
return nil
})
}
func (self *BackgroundRoutineMgr) startBackgroundExternalChangeDetection() {
self.gui.waitForIntro.Wait()
// We don't seed the snapshot here. The startup refresh captures one on
// entry (like every refs-touching refresh), and until one has been
// captured RefsSnapshotChangedSince treats the empty baseline as
// "unchanged", so we never fire a spurious refresh before a baseline
// exists — no need to depend on the timing of that startup refresh.
userConfig := self.gui.UserConfig()
self.goEvery(
userConfig.Refresher.ExternalChangeCheckIntervalDuration(),
self.gui.stopChan,
func(_ bool) error {
self.checkForExternalChanges()
return nil
},
)
}
func (self *BackgroundRoutineMgr) checkForExternalChanges() {
current, err := self.gui.git.Status.RefsSnapshot()
if err != nil {
// Transient error (e.g. git process couldn't start). Don't update the
// stored snapshot; we'll retry next tick.
self.gui.c.Log.Warnf("RefsSnapshot failed: %v", err)
return
}
if !self.gui.helpers.Refresh.RefsSnapshotChangedSince(current) {
return
}
// goEvery checks the pause count before starting us, but a git operation
// may have begun (and paused refreshes) after that check, while we were
// reading the snapshot above. In that case the change we detected is the
// operation's own intermediate state, so back off: the operation will
// refresh and re-snapshot when it finishes, and if the change was really
// external we'll catch it on the next tick after the pause lifts. We don't
// update the stored snapshot, so nothing is swallowed.
if self.backgroundRefreshesPaused() {
return
}
// No need to update the stored snapshot here; Refresh does that.
self.gui.c.Log.Info("External ref change detected — refreshing")
self.gui.c.RefreshFromWorker(types.RefreshOptions{Background: true})
}
// returns a channel that can be used to trigger the callback immediately
func (self *BackgroundRoutineMgr) goEvery(interval time.Duration, stop chan struct{}, function func(bool) error) chan struct{} {
done := make(chan struct{})
@@ -124,10 +195,13 @@ func (self *BackgroundRoutineMgr) goEvery(interval time.Duration, stop chan stru
ticker := time.NewTicker(interval)
defer ticker.Stop()
doit := func(retriggered bool) {
if self.pauseBackgroundRefreshes {
if self.backgroundRefreshesPaused() {
return
}
self.gui.c.OnWorker(func(gocui.Task) error {
// OnWorkerBackground, not OnWorker: these routines and the refreshes
// they trigger must not count towards lazygit being busy, or they'd
// spuriously block a repo switch every time one happens to be running.
self.gui.c.OnWorkerBackground(func(gocui.Task) error {
_ = function(retriggered)
done <- struct{}{}
return nil
@@ -155,7 +229,7 @@ func (self *BackgroundRoutineMgr) goEvery(interval time.Duration, stop chan stru
func (self *BackgroundRoutineMgr) backgroundFetch() (err error) {
err = self.gui.git.Sync.FetchBackground()
return self.gui.helpers.BranchesHelper.PostFetchRefresh(err)
return self.gui.helpers.BranchesHelper.PostFetchRefresh(err, true)
}
func (self *BackgroundRoutineMgr) triggerImmediateFetch() {
+23 -7
View File
@@ -27,10 +27,20 @@ func (gui *Gui) LogAction(action string) {
return
}
gui.Views.Extras.Autoscroll = true
// LogAction and LogCommand are called both from the UI thread and from git
// worker goroutines, so bounce the writes onto the UI thread: they touch the
// view's autoscroll flag and the GuiLog slice, which the layout/draw code
// reads. Ordering between successive log calls is preserved by the FIFO the
// bounce enqueues onto. It's a background bounce because writing the command
// log is incidental display work that must not count towards lazygit being
// busy (otherwise it could block a repo switch).
gui.onUIThreadBackground(func() error {
gui.Views.Extras.Autoscroll = true
gui.GuiLog = append(gui.GuiLog, action)
fmt.Fprint(gui.Views.Extras, "\n"+style.FgYellow.Sprint(action))
gui.GuiLog = append(gui.GuiLog, action)
fmt.Fprint(gui.Views.Extras, "\n"+style.FgYellow.Sprint(action))
return nil
})
}
func (gui *Gui) LogCommand(cmdStr string, commandLine bool) {
@@ -38,17 +48,23 @@ func (gui *Gui) LogCommand(cmdStr string, commandLine bool) {
return
}
gui.Views.Extras.Autoscroll = true
textStyle := theme.DefaultTextColor
if !commandLine {
// if we're not dealing with a direct command that could be run on the command line,
// we style it differently to communicate that
textStyle = style.FgMagenta
}
gui.GuiLog = append(gui.GuiLog, cmdStr)
indentedCmdStr := " " + strings.ReplaceAll(cmdStr, "\n", "\n ")
fmt.Fprint(gui.Views.Extras, "\n"+textStyle.Sprint(indentedCmdStr))
// See the comment in LogAction: bounce onto the UI thread since we may be
// called from a git worker, in the background so it can't block a repo switch.
gui.onUIThreadBackground(func() error {
gui.Views.Extras.Autoscroll = true
gui.GuiLog = append(gui.GuiLog, cmdStr)
fmt.Fprint(gui.Views.Extras, "\n"+textStyle.Sprint(indentedCmdStr))
return nil
})
}
func (gui *Gui) printCommandLogHeader() {
+58 -38
View File
@@ -32,34 +32,76 @@ type ListRenderer struct {
getNonModelItems func() []*NonModelItem
// The remaining fields are private and shouldn't be initialized by clients
numNonModelItems int
viewIndicesByModelIndex []int
modelIndicesByViewIndex []int
columnPositions []int
columnPositions []int
}
func (self *ListRenderer) GetList() types.IList {
return self.list
}
func (self *ListRenderer) ModelIndexToViewIndex(modelIndex int) int {
modelIndex = lo.Clamp(modelIndex, 0, self.list.Len())
if self.viewIndicesByModelIndex != nil {
return self.viewIndicesByModelIndex[modelIndex]
func (self *ListRenderer) getNonModelItemList() []*NonModelItem {
if self.getNonModelItems == nil {
return nil
}
return self.getNonModelItems()
}
return modelIndex
func (self *ListRenderer) ModelIndexToViewIndex(modelIndex int) int {
return modelIndexToViewIndex(self.list.Len(), self.getNonModelItemList(), modelIndex)
}
func (self *ListRenderer) ViewIndexToModelIndex(viewIndex int) int {
viewIndex = lo.Clamp(viewIndex, 0, self.list.Len()+self.numNonModelItems)
if self.modelIndicesByViewIndex != nil {
return self.modelIndicesByViewIndex[viewIndex]
}
return viewIndexToModelIndex(self.list.Len(), self.getNonModelItemList(), viewIndex)
}
// modelToViewIndexConverter returns a model-to-view index conversion that
// reuses a single snapshot of the non-model items. Callers that convert many
// indices in a row (e.g. search, which converts every commit) should use this
// rather than calling ModelIndexToViewIndex per index, which would rebuild the
// non-model items each time.
func (self *ListRenderer) modelToViewIndexConverter() func(modelIndex int) int {
listLength := self.list.Len()
nonModelItems := self.getNonModelItemList()
return func(modelIndex int) int {
return modelIndexToViewIndex(listLength, nonModelItems, modelIndex)
}
}
// The view shows the model items with the non-model items (e.g. section
// headers) inserted at their model indices. The two conversions below are
// computed directly from the current list length and non-model items, so they
// don't depend on the list having been rendered, and they can never be stale
// with respect to a model that changed since the last render (which used to
// cause both wrong results and index-out-of-range panics).
//
// The non-model items are assumed to be ordered by their Index, which is how
// all producers build them; the i-th one therefore ends up at view index
// Index+i.
func modelIndexToViewIndex(listLength int, nonModelItems []*NonModelItem, modelIndex int) int {
modelIndex = lo.Clamp(modelIndex, 0, listLength)
// Each non-model item inserted at or before this model item pushes it down
// by one row in the view.
viewIndex := modelIndex
for _, item := range nonModelItems {
if item.Index <= modelIndex {
viewIndex++
}
}
return viewIndex
}
func viewIndexToModelIndex(listLength int, nonModelItems []*NonModelItem, viewIndex int) int {
viewIndex = lo.Clamp(viewIndex, 0, listLength+len(nonModelItems))
// Subtract the non-model items that appear before this view index.
modelIndex := viewIndex
for i, item := range nonModelItems {
if item.Index+i < viewIndex {
modelIndex--
}
}
return modelIndex
}
func (self *ListRenderer) ColumnPositions() []int {
return self.columnPositions
}
@@ -71,23 +113,18 @@ func (self *ListRenderer) renderLines(startIdx int, endIdx int) string {
if self.getColumnAlignments != nil {
columnAlignments = self.getColumnAlignments()
}
nonModelItems := []*NonModelItem{}
self.numNonModelItems = 0
if self.getNonModelItems != nil {
nonModelItems = self.getNonModelItems()
self.prepareConversionArrays(nonModelItems)
}
nonModelItems := self.getNonModelItemList()
startModelIdx := 0
if startIdx == -1 {
startIdx = 0
} else {
startModelIdx = self.ViewIndexToModelIndex(startIdx)
startModelIdx = viewIndexToModelIndex(self.list.Len(), nonModelItems, startIdx)
}
endModelIdx := self.list.Len()
if endIdx == -1 {
endIdx = endModelIdx + len(nonModelItems)
} else {
endModelIdx = self.ViewIndexToModelIndex(endIdx)
endModelIdx = viewIndexToModelIndex(self.list.Len(), nonModelItems, endIdx)
}
lines, columnPositions := utils.RenderDisplayStrings(
self.getDisplayStrings(startModelIdx, endModelIdx),
@@ -97,23 +134,6 @@ func (self *ListRenderer) renderLines(startIdx int, endIdx int) string {
return strings.Join(lines, "\n")
}
func (self *ListRenderer) prepareConversionArrays(nonModelItems []*NonModelItem) {
self.numNonModelItems = len(nonModelItems)
viewIndicesByModelIndex := lo.Range(self.list.Len() + 1)
modelIndicesByViewIndex := lo.Range(self.list.Len() + 1)
offset := 0
for _, item := range nonModelItems {
for i := item.Index; i <= self.list.Len(); i++ {
viewIndicesByModelIndex[i]++
}
modelIndicesByViewIndex = slices.Insert(
modelIndicesByViewIndex, item.Index+offset, modelIndicesByViewIndex[item.Index+offset])
offset++
}
self.viewIndicesByModelIndex = viewIndicesByModelIndex
self.modelIndicesByViewIndex = modelIndicesByViewIndex
}
func (self *ListRenderer) insertNonModelItems(
nonModelItems []*NonModelItem, endIdx int, startIdx int, lines []string, columnPositions []int,
) []string {
+24 -3
View File
@@ -254,9 +254,6 @@ func TestListRenderer_ModelIndexToViewIndex_and_back(t *testing.T) {
getNonModelItems: getNonModelItems,
}
// Need to render first so that it knows the non-model items
self.renderLines(-1, -1)
for i := range len(s.modelIndices) {
assert.Equal(t, s.expectedViewIndices[i], self.ModelIndexToViewIndex(s.modelIndices[i]))
}
@@ -267,3 +264,27 @@ func TestListRenderer_ModelIndexToViewIndex_and_back(t *testing.T) {
})
}
}
// The index conversions must not depend on the list having been rendered
// first. It used to be renderLines that populated the conversion arrays, so
// converting an index before the first render silently ignored the non-model
// items (and converting after the model changed used a stale snapshot).
func TestListRenderer_IndexConversionsAreRenderIndependent(t *testing.T) {
modelInts := lo.Map(lo.Range(3), func(i int, _ int) myint { return myint(i) })
self := &ListRenderer{
list: NewListViewModel(func() []myint { return modelInts }),
getDisplayStrings: func(startIdx int, endIdx int) [][]string {
return lo.Map(modelInts[startIdx:endIdx],
func(i myint, _ int) []string { return []string{fmt.Sprint(i)} })
},
// A section header sits at model index 1, so model item 1 is pushed down
// to view index 2, and view index 2 maps back to model item 1.
getNonModelItems: func() []*NonModelItem {
return []*NonModelItem{{Index: 1, Content: "--- header ---"}}
},
}
// Deliberately convert without rendering first.
assert.Equal(t, 2, self.ModelIndexToViewIndex(1))
assert.Equal(t, 1, self.ViewIndexToModelIndex(2))
}
+8 -5
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"log"
"strings"
"sync/atomic"
"time"
"github.com/jesseduffield/lazygit/pkg/commands/models"
@@ -142,7 +143,9 @@ type LocalCommitsViewModel struct {
// If this is true we limit the amount of commits we load, for the sake of keeping things fast.
// If the user attempts to scroll past the end of the list, we will load more commits.
limitCommits bool
// Atomic because a checkout or reset sets it from a worker goroutine while the
// commits refresh reads it on the UI thread to decide how many commits to load.
limitCommits atomic.Bool
// If this is true we'll use git log --all when fetching the commits.
showWholeGitGraph bool
@@ -151,9 +154,9 @@ type LocalCommitsViewModel struct {
func NewLocalCommitsViewModel(getModel func() []*models.Commit, c *ContextCommon) *LocalCommitsViewModel {
self := &LocalCommitsViewModel{
ListViewModel: NewListViewModel(getModel),
limitCommits: true,
showWholeGitGraph: c.UserConfig().Git.Log.ShowWholeGraph,
}
self.limitCommits.Store(true)
return self
}
@@ -221,15 +224,15 @@ func (self *LocalCommitsContext) RefForAdjustingLineNumberInDiff() string {
}
func (self *LocalCommitsContext) ModelSearchResults(searchStr string, caseSensitive bool) []gocui.SearchPosition {
return searchModelCommits(caseSensitive, self.GetCommits(), self.ColumnPositions(), self.ModelIndexToViewIndex, searchStr)
return searchModelCommits(caseSensitive, self.GetCommits(), self.ColumnPositions(), self.modelToViewIndexConverter(), searchStr)
}
func (self *LocalCommitsViewModel) SetLimitCommits(value bool) {
self.limitCommits = value
self.limitCommits.Store(value)
}
func (self *LocalCommitsViewModel) GetLimitCommits() bool {
return self.limitCommits
return self.limitCommits.Load()
}
func (self *LocalCommitsViewModel) SetShowWholeGitGraph(value bool) {
+5 -2
View File
@@ -60,8 +60,11 @@ func NewContextTree(c *ContextCommon) *ContextTree {
"main",
PATCH_BUILDING_MAIN_CONTEXT_KEY,
func() []int {
filename := commitFilesContext.GetSelectedPath()
includedLineIndices, err := c.Git().Patch.PatchBuilder.GetFileIncLineIndices(filename)
file := commitFilesContext.GetSelectedFile()
if file == nil {
return nil
}
includedLineIndices, err := c.Git().Patch.PatchBuilder.GetFileIncLineIndices(file.Path, file.PreviousPath)
if err != nil {
c.Log.Error(err)
return nil
+1 -1
View File
@@ -223,7 +223,7 @@ func (self *SubCommitsContext) RefForAdjustingLineNumberInDiff() string {
}
func (self *SubCommitsContext) ModelSearchResults(searchStr string, caseSensitive bool) []gocui.SearchPosition {
return searchModelCommits(caseSensitive, self.GetCommits(), self.ColumnPositions(), self.ModelIndexToViewIndex, searchStr)
return searchModelCommits(caseSensitive, self.GetCommits(), self.ColumnPositions(), self.modelToViewIndexConverter(), searchStr)
}
func (self *SubCommitsContext) IndexForGotoBottom() int {
-12
View File
@@ -207,18 +207,6 @@ func (gui *Gui) resetHelpersAndControllers() {
controllers.AttachControllers(context, searchControllerFactory.Create(context))
}
for _, context := range []controllers.CanViewWorktreeOptions{
gui.State.Contexts.LocalCommits,
gui.State.Contexts.ReflogCommits,
gui.State.Contexts.SubCommits,
gui.State.Contexts.Stash,
gui.State.Contexts.Branches,
gui.State.Contexts.RemoteBranches,
gui.State.Contexts.Tags,
} {
controllers.AttachControllers(context, controllers.NewWorktreeOptionsController(common, context))
}
// allow for navigating between side window contexts
for _, context := range []types.Context{
gui.State.Contexts.Status,
@@ -89,6 +89,12 @@ func (self *BasicCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
Description: self.c.Tr.MoveCommitsToNewBranch,
Tooltip: self.c.Tr.MoveCommitsToNewBranchTooltip,
},
{
Keys: opts.GetKeys(opts.Config.Universal.NewWorktree),
Handler: self.withItem(self.c.Helpers().Worktree.NewWorktreeMenuForCommit),
Description: self.c.Tr.NewWorktree,
OpensMenu: true,
},
{
Keys: opts.GetKeys(opts.Config.Commits.ViewResetOptions),
Handler: self.withItem(self.createResetMenu),

Some files were not shown because too many files have changed in this diff Show More