Compare commits

...
101 Commits
Author SHA1 Message Date
Stefan Haller 80223c7fbb Fix keybinding cheatsheets with regard to pipe characters in key or description (#5007)
The "Cycle pagers" command wasn't rendered correctly, because it's bound
to '|' by default, but this was taked as a table column delimiter. Fix
this by escaping the pipe character.

A similar thing could happen for the description or tooltip (and did in
fact, for a tooltip in the russion translation), so escape those too
just to be sure.
2025-11-01 09:56:57 +01:00
Stefan Haller ff0a16f809 Fix keybinding cheatsheets with regard to pipe characters in key or description
The "Cycle pagers" command wasn't rendered correctly, because it's bound to '|'
by default, but this was taked as a table column delimiter. Fix this by escaping
the pipe character.

A similar thing could happen for the description or tooltip (and did in fact,
for a tooltip in the russion translation), so escape those too just to be sure.
2025-11-01 09:54:32 +01:00
Stefan Haller 48c1db6fe4 Fix fixup's false filename detection in hunks containing dashed lines (#5004)
### PR Description

The existing diff parser incorrectly treated subsequent lines beginning
with `---` as filename headers while processing hunks. This caused
corruption when dashed lines appeared within diffs themselves.

Restrict filename detection to only occur between hunks.

This repository https://github.com/abobov/lazygit-fixup-issue
demonstrates the issue:

- clone repository
- run `git reset --soft HEAD~`
- try `<c-f>` Find base commit for fixup 
- `fatal: no such path ...` error
2025-11-01 09:42:45 +01:00
Anton Bobov 4c4929acad Fix fixup's false filename detection in hunks containing dashed lines
The existing diff parser incorrectly treated subsequent lines beginning
with "---" as filename headers while processing hunks.  This caused
corruption when dashed lines appeared within diffs themselves.

Restrict filename detection to only occur between hunks.
2025-11-01 09:40:04 +01:00
Stefan Haller f4920e8efa Use a PTY when using external diff command from git config (#4983)
### PR Description

I hit the same issue as
https://github.com/jesseduffield/lazygit/issues/3119 when setting
`useExtDiffGitConfig` to true and using `difftastic` side-by-side
comparison in my git config.

The same fix in https://github.com/jesseduffield/lazygit/pull/3120 needs
to be applied for the newer config option.
2025-11-01 09:35:45 +01:00
Brandon 263ad11b3b Use a PTY when using external diff command from git config
Same logic as https://github.com/jesseduffield/lazygit/pull/3120 but for new external diff option
2025-11-01 09:33:52 +01:00
Stefan Haller 6f0f1e7995 Update translations from Crowdin (#5005) 2025-10-31 19:58:37 +01:00
Stefan Haller 3967edd113 Update translations from Crowdin 2025-10-31 19:56:01 +01:00
Stefan Haller 32a701cb9c Use ignore directive to ignore test files not to be passes to gofumpt (#4936)
### Motivation
To replace the workaround introduced in
https://github.com/jesseduffield/lazygit/pull/4809 with `ignore`
directive to make the code simpler.

### Overview
This is a follow-up PR to
https://github.com/jesseduffield/lazygit/pull/4809.
I have added 4 changes due to updating gofumpt to remove `git ls-files`
workaround.

Please take a look at each commit messages for details.
2025-10-20 07:59:40 +02:00
kyu08 64bcc72e45 Specify return value where named return value is used
After [v0.9.0](https://github.com/mvdan/gofumpt/releases/tag/v0.9.0),
gofumpt prohibits "naked return" for the sake of clarity. This makes
more readable when "named return value" is used.
For more infomation for "prohibition of naked return":
https://github.com/mvdan/gofumpt/issues/285.
2025-10-20 07:56:56 +02:00
kyu08andStefan Haller beb05d4a61 Fix makeAtomic in branches_test
When replacing the naked return with a `return result`, the linter starts to
complain about "return copies lock value: sync/atomic.Int32 contains
sync/atomic.noCopy". I suspect this is also a problem when using a naked return,
and the linter just doesn't catch it in that case. Either way, it's better to
use a pointer to ensure that the atomic is not copied.

Co-authored-by: Stefan Haller <stefan@haller-berlin.de>
2025-10-20 07:56:56 +02:00
kyu08andStefan Haller 65c27dd8ce Remove workaround for "make format"
When a new enough gofumpt version is used (v0.9.0 or later), it suports the
`ignore` directive that we just added, so the workaround is no longer needed.

Co-authored-by: Stefan Haller <stefan@haller-berlin.de>
2025-10-20 07:56:56 +02:00
kyu08 76452a0bcc Add ignore directive in go.mod
This can be used by go tools such as gofumpt.
2025-10-20 07:56:56 +02:00
Stefan Haller 6782f04e8c Add no-ff merge option (#4966)
This will put whatever git's default merge variant is as the first menu
item, and add a second item which is the opposite (no-ff if the default
is ff, and vice versa). Which one is the default depends on whether a
fast-forward merge is possible, and whether users have set git's
`merge.ff` config or lazygit's `git.merging.args` config.

If users prefer to always have the same option first no matter whether
it's applicable, they can make ff always appear first by setting git's
`merge.ff` config to "true" or "only", or by setting lazygit's
`git.merging.args` config to "--ff" or "--ff-only"; if they want no-ff
to appear first, they can do that by setting git's `merge.ff` config to
"false", or by setting lazygit's `git.merging.args` config to "--no-ff".
Which of these they choose depends on whether they want the config to
also apply to other git clients including the cli, or only to lazygit.

Closes #4091.
2025-10-19 21:26:31 +02:00
Stefan Haller 62854026a3 Add no-ff merge option
This will put whatever git's default merge variant is as the first menu item,
and add a second item which is the opposite (no-ff if the default is ff, and
vice versa).

If users prefer to always have the same option first no matter whether it's
applicable, they can make ff always appear first by setting git's "merge.ff"
config to "true" or "only", or by setting lazygit's "git.merging.args" config to
"--ff" or "--ff-only"; if they want no-ff to appear first, they can do that by
setting git's "merge.ff" config to "false", or by setting lazygit's
"git.merging.args" config to "--no-ff". Which of these they choose depends on
whether they want the config to also apply to other git clients including the
cli, or only to lazygit.
2025-10-19 21:24:28 +02:00
Stefan Haller c88381b9ce Rename texts to be consistent with our conventions 2025-10-19 21:24:28 +02:00
Stefan Haller afbef40258 Reorder texts so that they are grouped together, and ordered in a sensible way 2025-10-19 21:24:28 +02:00
Stefan Haller 4e0194d8f7 Replace MergeOpts struct with MergeVariant enum
- Squash and FastForwardOnly are mutually exclusive, and instead of asserting
  this at runtime, model the API so that they can't be passed together.
- FastForwardOnly is unused, so remove it; however, we are going to need --ff
  and --no-ff in the next commit, so add those instead.
- Instead of putting the enum into the MergeOpts struct, replace the struct by
  the enum. We can reintroduce the struct when we add more arguments, but for
  now it's an unnecessary indirection.
2025-10-19 21:24:28 +02:00
Stefan Haller d334bae0a0 Add synchronize event to the hooks of "Check Required Labels" (#4974)
I found that PR status becomes green even if no label added when we use
`Update branch` on GitHub PR page.

So I added `synchronize` event to the hooks of `Check Required Labels`.
2025-10-19 17:29:17 +02:00
kyu08 76948f82c1 Add synchronize event to the hooks of "Check Required Labels" 2025-10-19 22:12:23 +09:00
Stefan Haller a22aced1bb Avoid auto-stashing when only submodules are out of date (#4969)
Stashing doesn't affect submodules, so if you have a working copy that
has out-of-date submodules but no other changes, and then you revert or
paste a commit (or invoke one of the many other lazygit commands that
auto-stash, e.g. undo), lazygit would previously try to stash changes
(which did nothing, but also didn't return an error), perform the
operation, and then pop the stash again. If no stashes existed before,
then this would only cause a confusing error popup ("error:
refs/stash@{0} is not a valid reference"), but if there were stashes,
this would try to pop the newest one of these, which is very undesirable
and confusing.
2025-10-16 12:08:48 +02:00
Stefan Haller 40cc008a5a Avoid auto-stashing when only submodules are out of date
Stashing doesn't affect submodules, so if you have a working copy that has
out-of-date submodules but no other changes, and then you revert or paste a
commit (or invoke one of the many other lazygit commands that auto-stash, e.g.
undo), lazygit would previously try to stash changes (which did nothing, but
also didn't return an error), perform the operation, and then pop the stash
again. If no stashes existed before, then this would only cause a confusing
error popup ("error: refs/stash@{0} is not a valid reference"), but if there
were stashes, this would try to pop the newest one of these, which is very
undesirable and confusing.
2025-10-16 12:00:31 +02:00
Stefan Haller 7282159f78 Show a better error message if the temp directory is not writeable (#4968)
This happened to a few users after updating macOS to version 15.7.1, and
apparently a reboot fixes it, so suggest this in the error message.

Closes #4924
2025-10-16 10:10:30 +02:00
Stefan Haller e02f073cd0 Show a better error message if the temp directory is not writeable
This happened to a few users after updating macOS to version 15.7.1, and
apparently a reboot fixes it, so suggest this in the error message.
2025-10-16 10:04:47 +02:00
Stefan Haller 853b54b069 Add config option gui.skipSwitchWorktreeOnCheckoutWarning (#4964)
Adds a config option that disables the confirmation for switching to a
worktree when trying to check out a branch that is checked out in that
other worktree.

Fixes #4938.
2025-10-14 14:24:36 +02:00
Stefan Haller 80f860beb7 Add config option gui.skipSwitchWorktreeOnCheckoutWarning 2025-10-14 14:19:54 +02:00
Stefan Haller d3fb22a673 Fix lazygit getting unresponsive when pasting commits that become empty (#4958)
Lazygit would become unresponsive when cherry-picking a commit that
becomes empty at the destination. There are two ways this can happen:
one where a cherry-picked commit simply becomes empty "by itself",
because the change is already present on the destination branch (this
was only a problem with git versions older than 2.45), and another where
the cherry-pick stops with conflicts, and the user resolves them such
that no changes are left, and then continues the cherry-pick. This would
still fail even with git 2.45 or later. The fix is the same for both
cases though.

Fixes #4763.
2025-10-14 12:30:15 +02:00
Stefan Haller 3ac9003298 Add tests
I can't do my usual "add the tests first, with EXPECTED/ACTUAL sections that
document the bug" method here, because the tests would hang without the bug
being fixed.

We need two different tests here: one where a cherry-picked commit simply
becomes empty "by itself", because the change is already present on the
destination branch (this was only a problem with git versions older than 2.45),
and the other where the cherry-pick stops with conflicts, and the user resolves
them such that no changes are left, and then continues the cherry-pick. This
would still fail even with git 2.45 or later. The fix is the same for both cases
though.

The tests show that the selection behavior after skipping an empty cherry-picked
commit is not ideal, but since that's only a minor cosmetic problem we don't
bother fixing it here.
2025-10-14 12:27:32 +02:00
Stefan Haller e858bad3f8 Fix lazygit getting unresponsive when pasting commits that become empty
Whenever git returns the error "The previous cherry-pick is now empty", we would
previously continue the rebase; this works for rebase because it behaves the
same as "git rebase --skip" in this case. That's not true for cherry-pick
though; if you continue a cherry-pick where the current commit is empty, it will
return the same error again, causing lazygit to be stuck in an endless loop.

Fix this by skipping instead of continuing; this shouldn't make a difference for
rebase, but works for cherry-pick.

Theoretically we could have a similar problem for revert (if you are trying to
revert a commit that has already been undone through some other means); this
should then be fixed in the same way with this change. However, the change is
not relevant for revert because git returns a different error in this case.
2025-10-14 12:27:32 +02:00
Stefan Haller 9adcc8ec79 Fix window arrangement when a popup or the search prompt is open (#4961)
When focusing the main view, going into full screen mode by pressing '+'
twice, and then opening the search prompt ('/') or a menu (e.g. '?' or
':'), the full screen display would switch to the focused side panel.

Fix this by always excluding popups from the window arrangement logic.
No popup should ever have any influence on how the views beneath it are
laid out.

Addresses [this detail
aspect](https://github.com/jesseduffield/lazygit/issues/1113#issuecomment-3206883422)
of #1113.
2025-10-14 12:27:16 +02:00
Stefan Haller 8eeb16c8da Fix window arrangement when a popup or the search prompt is open
When focusing the main view, going into full screen mode by pressing '+' twice,
and then opening the search prompt ('/') or a menu (e.g. '?' or ':'), the full
screen display would switch to the focused side panel.

Fix this by always excluding popups from the window arrangement logic. No popup
should ever have any influence on how the views beneath it are laid out.
2025-10-14 12:22:58 +02:00
Stefan Haller 442ede95c2 Offer to force-delete a worktree if it contains submodules (#4959)
For a worktree that contains submodules, trying to delete it would show
an error instead of offering to force-remove it.

Fixes #4125.
2025-10-14 12:22:38 +02:00
Stefan Haller 3415ed975b Offer to force-delete a worktree if it contains submodules 2025-10-14 12:19:49 +02:00
Stefan Haller 7568f5bf05 Add test demonstrating the problem
For a worktree that contains submodules, trying to delete it shows an error and
doesn't offer to force-remove it.
2025-10-14 12:19:49 +02:00
Stefan Haller 5812466145 Support multiple pagers (#4953)
Allow configuring an array of pagers, and cycling through them with the
`|` key (mnemonic: we are piping the output through something). I find
this useful for switching between delta (which I prefer most of the
time) and difftastic (which occasionally produces better output for
certain kinds of changes). It could also be used to switch between delta
in inline mode and in side-by-side mode.
2025-10-14 12:19:32 +02:00
Stefan Haller 32bf6d685c Add config migration of paging section to pagers array 2025-10-14 12:17:13 +02:00
Stefan Haller 823b2e9aab Update cheatsheets 2025-10-14 12:17:13 +02:00
Stefan Haller e44d6ec330 Replace paging config with an array of pagers 2025-10-14 12:17:13 +02:00
Stefan Haller 765c9eb85c Add PagerConfig
This is an object that is owned by Gui, is accessible through GuiCommon.State(),
and also passed down to GitCommand, where it is mostly needed. Right now it
simply wraps access to the Git.Paging config, which isn't very exciting, but
we'll extend it in the next commit to handle a slice of pagers (and maintain the
currently selected pager index), and doing this refactoring up front allows us
to make that change without having to touch clients.
2025-10-14 12:17:13 +02:00
Stefan Haller ed05470732 Cleanup: remove unused method ConfiguredPager
This could have been removed in commit 9657b4346f.
2025-10-14 12:17:13 +02:00
Stefan Haller 1c87776470 When pasting multi-line text into a prompt, don't treat the line feeds as "confirm" (#4955)
This is likely to do bad things; for example, if the prompt is the shell
command prompt, then we would run into what looks like a deadlock bug in
tcell. In other cases, the characters in the following lines might be
treated as random keybindings after the prompt is confirmed; this is
very similar to #4234.

In this case, it seems the best we can do is to simply swallow the line
feeds. The entire pasted text will then appear as a single long line in
the prompt, and hopefully the user knows that they can use ctrl-u to
delete it again. If not, they will probably just hit esc to close the
prompt.

Fixes #4954.
2025-10-14 12:15:44 +02:00
Stefan Haller fbaea583a5 When pasting multi-line text into a prompt, don't treat the line feeds as "confirm"
This is likely to do bad things; for example, if the prompt is the shell command
prompt, then we would run into what looks like a deadlock bug in tcell. In other
cases, the characters in the following lines might be treated as random commands
after the prompt is confirmed.
2025-10-14 12:10:51 +02:00
Stefan Haller 6b5b493237 Fix support for Git copy status when status.renames=copies (#4892)
Fixes: #4890

- Fix file status parsing to handle Git copy operations when
`status.renames=copies` is configured
- Extend status parser to recognize both "R" (rename) and "C" (copy)
prefixes
- Add comprehensive test coverage for copy status codes

The same issue from https://github.com/jesseduffield/lazygit/issues/4890
now is fixed

<img width="602" height="453" alt="image"
src="https://github.com/user-attachments/assets/c5b0dc4b-8f77-4867-b601-65faa91d6607"
/>
2025-10-14 12:07:04 +02:00
Karen Grigoryan e6f3313d9a Fix support for Git copy status when status.renames=copies
Fixes #4890

When Git is configured with status.renames=copies, it can produce
status codes starting with "C" (copy) in addition to "R" (rename).
The file loader was only checking for "R" prefixes, causing copy
operations to be parsed incorrectly and breaking file staging.

This fix extends the status parser to handle both "R" and "C"
prefixes, ensuring proper support for Git's copy detection feature.

Fixes file staging issues when using status.renames=copies configuration.
2025-10-14 11:57:12 +02:00
Jesse Duffield 1fd771f818 Update README.md
Updated image source for Warp logo in README.
2025-10-11 15:50:08 +11:00
Stefan Haller f7a4092a29 Wrap long lines in comments in Config.md (#4951)
Since these are displayed in a fenced code block on github, they aren't
auto-wrapped on display, so users have to scroll horizontally to read
longer paragraphs.

Addresses
https://github.com/jesseduffield/lazygit/issues/800#issuecomment-3209372957.
2025-10-10 21:27:56 +02:00
Stefan Haller db9fb216fd Wrap long comment lines in Config.md 2025-10-10 21:25:02 +02:00
Stefan Haller c9eef8079e Add missing Portuguese language 2025-10-10 21:25:02 +02:00
Stefan Haller 396f87b46b Unwrap paragraphs in user_config comments
We want to switch to have paragraphs consistently on one line, and auto-wrap
them automatically when generating Config.md.

The changes to Config.md in this commit are temporary.
2025-10-10 21:25:02 +02:00
Stefan Haller 30169bd258 Fix dropping submodule changes from a commit (#4937)
Our logic to decide if a file needs to be checked out from the previous
commit or deleted because it didn't exist in the previous commit didn't
work for submodules. We were using `git cat-file -e` to ask whether the
file existed, but this returns an error for submodules, so we were
always deleting those instead of reverting them back to their previous
state.

Switch to using `git ls-tree -- file` instead, which works for both
files and submodules.

Fixes #4932.
2025-10-10 12:58:06 +02:00
Stefan Haller c1e52fc807 Fix dropping submodule changes from a commit
Our logic to decide if a file needs to be checked out from the previous commit
or deleted because it didn't exist in the previous commit didn't work for
submodules. We were using `git cat-file -e` to ask whether the file existed, but
this returns an error for submodules, so we were always deleting those instead
of reverting them back to their previous state.

Switch to using `git ls-tree -- file` instead, which works for both files and
submodules.
2025-10-10 12:55:58 +02:00
Stefan Haller 0904bf9969 Add test demonstrating the problem
When dropping changes to the submodule, we expect it to get rolled back to the
previous version; however, it is removed entirely instead.
2025-10-10 12:55:58 +02:00
Stefan Haller 21483b259c Rename AddFileInWorktree to AddFileInWorktreeOrSubmodule
It works for submodules too.

Also, pass file name and file content explicitly; the existing tests don't care
about these, but when writing tests that do, it makes them easier to understand.
2025-10-10 12:55:58 +02:00
Stefan Haller 8e00bbd939 Show "Log (x of y)" in the title bar when there is more than one branch log command (#4943)
Addresses #4939.
2025-10-10 12:51:54 +02:00
Stefan Haller 5d02cba721 Show "Log (x of y)" in the title bar when there is more than one branch log command 2025-10-10 12:49:42 +02:00
Stefan Haller bf56a61b71 Change condition that determines whether we are showing the dashboard
Doesn't make a difference currently, since the title is either StatusTitle when
the dashboard is showing, or LogTitle when one of the branch logs is showing.
This is going to change in the next commit, though.
2025-10-10 12:49:42 +02:00
Stefan Haller 220cde1376 Fix potential crash when reloading the config after removing a branch log command
When cycling to the last branch log command, and then editing the config to
remove one or more log commands, lazygit would crash with an out of bounds panic
when returning to it. Fix this by resetting the log index to 0 when it is out of
bounds. (I think resetting to 0 is better than clamping, although it doesn't
matter much.)
2025-10-10 12:49:42 +02:00
Stefan Haller 19f88290a2 Change allBranchesLogCmdIndex to an int
I know that uint was chosen to document that it can't be negative (not sure why
the "8" was chosen, though). That's pointless in languages that don't enforce
this though: you can subtract 1 from uint8(0) and you'll get something that
doesn't make sense; that's not any better than getting -1. I'm not a fan of
using unsigned types in general (at least in languages like go or C++), and they
usually just make the code more cumbersome without real benefits.
2025-10-10 12:49:42 +02:00
Stefan Haller 47ca08240b Don't depend on en_US locale to be installed (#4949)
For some of the git commands that lazygit uses, it needs to force the
language to English because it parses git's output and needs to react to
certain things. It used to do this by setting the locale to
`en_US.UTF-8`; some users reported that this locale isn't available on
their systems. Use the "C" locale instead, which achieves the same
result and is guaranteed to be available everywhere.

Fixes #4731.
2025-10-09 13:19:58 +02:00
Stefan Haller 99a83efc34 Set LC_MESSAGES too
I'm not sure this is necessary, but it doesn't hurt.
2025-10-09 13:10:58 +02:00
Stefan Haller b755f52754 Use C locale instead of en_US.UTF-8 to force English language
Some users reported that en_US.UTF-8 is not available on their systems, leading
to warnings in the command log. "C" also forces the language to English, and is
guaranteed to be available everywhere.
2025-10-09 13:10:02 +02:00
Stefan Haller c31b604c9d Document a workaround for using custom pagers on Windows (#4941)
Custom pagers are not supported on Windows, because using a pager in git
requires the git command to run in a PTY, but the PTY package we are
using doesn't support Windows.

However, there's a workaround to use a custom pager by using a
Powershell script configured as an external diff command. Document this
in the `Custom_Pagers.md` document, and export a `LAZYGIT_COLUMNS`
environment variable to allow such scripts to work better.
2025-10-09 10:06:44 +02:00
stk 2abd76eaa3 Add documentation for pager workaround on Windows 2025-10-09 10:04:29 +02:00
stk 43811cdad8 Export a LAZYGIT_COLUMNS variable to "pty" tasks on Windows
This makes it possible to pass it to an external diff command that is
used like a pager. An example for this can be seen in the added
documentation in the next commit.
2025-10-09 10:04:29 +02:00
Stefan Haller 4b59f9855e When entering a commit in path filtering mode, select the filtered path (#4942)
This can be useful if you want to do some operation on the filtered
file, e.g. check it out using `c`, or enter the file to make a custom
patch from it.

Addresses #4935.
2025-10-09 10:00:38 +02:00
Stefan Haller 7fe73c1ee2 When entering a commit in path filtering mode, select the filtered path 2025-10-09 09:58:15 +02:00
Stefan Haller 5811f2945c Extract a InternalTreePathForFilePath helper function 2025-10-09 09:58:10 +02:00
Stefan Haller fb00584f4d Update diff of conflicted file in the main view after conflicts have been resolved (#4945)
When lazygit detects that all conflicts have been resolved, it puts up a
popup asking whether the user wants to continue the merge or rebase.
However, it didn't rerender the main view of the conflicted file, so it
would still show the conflict markers, which is confusing. Add some
logic that updates the main view in this situation, behind the popup.
2025-10-09 09:04:32 +02:00
Stefan Haller 5fad3ab047 Refresh the main view for the current side panel when a popup is showing
The logic in postRefreshUpdate would only rerender the main view if the context
being updated is the current view. This is not the case when a popup is showing;
but we still want to render the main view in that case, behind the popup. This
happens for example when we refresh the Files scope, we determine that all
conflicts have been resolved and show a popup asking to continue the merge or
rebase, but the postRefreshUpdate of the Files context only happens when the
popup is already showing, so we would still see the conflict markers behind the
popup, which is rather confusing.
2025-10-09 09:02:03 +02:00
Stefan Haller 3f30008224 Add merge menu with conflict resolver (#4889)
Implements https://github.com/jesseduffield/lazygit/issues/2026. I also
tried to address issues mentioned in the
https://github.com/jesseduffield/lazygit/pull/3477 PR.

Previously, pressing `M` opened an external merge tool. Now it opens a
merge options menu that allows selecting all conflicts in chosen files
as **ours** (HEAD), **theirs** (incoming), or **union** (both), while
still providing access to the external merge tool.

This uses [git-merge-file](https://git-scm.com/docs/git-merge-file) for
a 3-way merge with the `--ours`, `--theirs`, and `--union` flags. This
approach avoids the issue mentioned in
https://github.com/jesseduffield/lazygit/discussions/1608#discussioncomment-13002595,
and correctly applies the chosen conflict resolutions while preserving
changes from other branches. The command is executed with `--object-id`,
which requires object IDs obtained via `rev-parse`, instead of relying
on the standard version that works with full saved files.
2025-10-09 08:51:04 +02:00
Krystof Gartner 703f053a7e Add merge options menu
Replace merge-tool with merge options menu that allows resolving all
conflicts for selected files as ours, theirs, or union, while still
providing access to the merge tool.
2025-10-09 08:45:58 +02:00
Stefan Haller 1f002af06b fix(nix): use nixos-unstable (#4948)
Fixed jesseduffield/lazygit#4947
2025-10-09 08:43:26 +02:00
doprz 610ac68635 fix(nix): use nixos-unstable
Fixed jesseduffield/lazygit#4947
2025-10-08 11:39:07 -05:00
Stefan Haller 60a7d6ac57 Fix normalisedSelectedNodes function (#4920)
The `normalisedSelectedNodes` function in `files_controller.go` had a
bug where it didn't work correctly for the root item (`/`). This PR
fixes that.

We don't have any tests for this bug. Apparently, the functions that use
this (staging and discarding files) also work correctly when they work
on a directory and then on the contained file again, that's why nobody
has noticed yet. I briefly looked into adding unit tests for the
function, but it was more work than I was willing to put into this right
now.
2025-10-08 15:12:03 +02:00
Stefan Haller 302b621b68 Fix: make isDescendentOfSelectedNodes work for the root item
The root item's path is ".", and the path of a file at top level is "./file".
When using GetPath, this gives us "." and "file", respectively, and
isDescendentOfSelectedNodes would return false for these.

Working with the internal paths (i.e. without stripping the leading "./") fixes
this.
2025-10-08 15:09:35 +02:00
Stefan Haller d0c6e27fee Cleanup: move variable assignment out of the loop
It never changes inside this function, so there's no need to recompute it with
every loop iteration.
2025-10-08 15:09:35 +02:00
Stefan Haller 6478bf456b feat(nix): add comprehensive Nix flake (#4826)
## PR Description

This PR adds Nix flake support to provide a reproducible development
environment and build process for `lazygit` contributors, devs, and
users.

Features:
- Nix Flake Configuration: Complete flake setup supporting multiple
architectures
- Development Shell: Pre-configured environment with Go toolchain,
linting tools, and development utilities
- Multiple Entry Points: Support for `nix develop`, `nix run`, and `nix
build`

Closes #3474.
2025-10-08 15:08:44 +02:00
doprzandEveeifyeve 6b8853c027 feat(nix): add comprehensive nix flake
Co-authored-by: Eveeifyeve
2025-10-08 15:05:54 +02:00
Stefan Haller 97762f484f Update Merge conflicts menu keybinding to use p instead of k (#4934) 2025-10-05 10:26:33 +02:00
Marco Zingoni b32b55201e Update resolve conflict using p instead of k 2025-10-05 10:23:03 +02:00
Stefan Haller f45e7ca705 Update go to 1.25 (#4844)
### PR Description
This PR includes 2 changes:
1. Bump golangci-lint version to `v2.4.0` from  `v.2.2.1`
1. Bump Go version to `1.25.0`:
	- Bump Go version to `1.25` in `go.mod`
	- Bump Go version to  `1.25` in `ci.yml`
	- Bump base image to `golang:1.25`  in `Dockerfile`

### Reference
- [Go 1.25 release note](https://tip.golang.org/doc/go1.25)
2025-10-05 10:21:49 +02:00
kyu08 93b8d70209 Update go to 1.25 2025-10-05 10:17:03 +02:00
kyu08 47b1ededf3 Bump golangci-lint to v.2.4.0 from v2.2.1 2025-10-05 10:17:03 +02:00
Stefan Haller 7544de03c0 Give better visual feedback when checking out the previous branch (#4929)
I find the command "Checkout previous branch" quite useful, and I use it
a lot. However, I'm unhappy with the visual feedback, so this PR
improves that a bit.

Previously, the feedback you got when pressing "-" was just a "Checking
out..." status in the bottom line. This was both easy to miss if you are
used to looking for an inline status in the branches panel, and it
didn't provide information about which branch was being checked out,
which can be annoying in very large repos where checking out takes a
while, and you only see at the end if you are now on the right branch.

Improve this by trying to figure out which branch was the previously
checked out one, and then checking it out normally so that you get an
inline status next to it (as if you had pressed space on it). There are
cases where this fails, e.g. when the previously checked out ref was a
detached head, in which case we fall back to the previous behavior.
2025-10-05 10:04:06 +02:00
Stefan Haller e585accd37 Give better feedback when checking out the previous branch
Previously, the feedback you got when pressing "-" was just a "Checking out..."
status in the bottom line. This was both easy to miss if you are used to looking
for an inline status in the branches panel, and it didn't provide information
about which branch was being checked out, which can be annoying in very large
repos where checking out takes a while, and you only see at the end if you are
now on the right branch.

Improve this by trying to figure out which branch was the previously checked out
one, and then checking it out normally so that you get an inline status next to
it (as if you had pressed space on it). There are cases where this fails, e.g.
when the previously checked out ref was a detached head, in which case we fall
back to the previous behavior.
2025-10-02 18:28:17 +02:00
Stefan Haller ba0e2d12da README.md: Update Sponsors (#4897)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action
2025-10-02 17:32:53 +02:00
github-actions[bot] 385f796c90 README.md: Update Sponsors 2025-10-02 15:30:45 +00:00
Stefan Haller 05b6588e39 Don't break line after footnote symbol in commit messages (#4912)
Addresses #4900.
2025-10-02 17:30:32 +02:00
Stefan Haller ef92e30315 Bump gocui
The main change here is https://github.com/jesseduffield/gocui/pull/85, which
avoids breaking lines after footnote symbols in commit messages (e.g. [1]).

[1]: https://www.example.com/this-is-a-really-long-url-that-lazy-git-automatically-wraps-and-is-an-issue
2025-10-02 17:28:13 +02:00
Stefan Haller cb2b5c3738 Hide the cursor when the password prompt is showing (#4878)
The cursor is a bit of a security concern, because it reveals the length
(and typing pattern) of the password.

Resolves #4877.
2025-09-20 11:14:05 +02:00
Stefan Haller 851afe94f5 Hide the cursor when the password prompt is showing
The cursor is a bit of a security concern, because it reveals the length (and
typing pattern) of the password.
2025-09-20 11:11:46 +02:00
Stefan Haller 6941b81992 Don't log the git rev-list command that we use for IsBranchMerged (#4896)
When you delete a branch, we call this function to determine whether we
need to ask for confirmation of not. We don't want to log this, because
it's not a command that a user would normally use as part of this
operation.
2025-09-20 11:11:00 +02:00
Stefan Haller 2b152dcf5e Don't log the git rev-list command that we use for IsBranchMerged
When you delete a branch, we call this function to determine whether we need to
ask for confirmation of not. We don't want to log this, because it's not a
command that a user would normally use as part of this operation.
2025-09-20 11:07:43 +02:00
Stefan Haller bbef570225 Make running with --debug and running integration tests work when built with go 1.25 (#4910)
The go-deadlock package needed to be updated to be compatible with go
1.25.
2025-09-20 11:07:18 +02:00
Stefan Haller f689b32c33 Bump the go-deadlock dependency to v0.3.6
This is needed for compatibility with go 1.25.
2025-09-20 10:51:37 +02:00
Stefan Haller d56a9cf73e Fix MoveCommitsToNewBranch description typo (#4867) 2025-09-18 12:29:23 +02:00
Torben Koch 308a4d5c93 fix command description typo 2025-09-18 12:24:25 +02:00
Stefan Haller 3de12b7fde Fix staging when using the new useExternalDiffGitConfig config (#4895)
This new config was introduced in 0.55, but it made it impossible to
enter the staging view or custom patch building view.

Fixes
https://github.com/jesseduffield/lazygit/pull/4832#issuecomment-3289371491.
2025-09-17 20:39:08 +02:00
Stefan Haller c35e3776dd Fix staging when using the new useExternalDiffGitConfig config
This new config was introduced in 0.55, but it didn't work in the staging view
or custom patch building view.
2025-09-17 20:37:14 +02:00
Stefan Haller b4a38a368a Don't hide keybindings that match the confirmMenu key in the keybindings menu (#4880)
In all other menus besides the keybindings menu it makes sense to hide
keybindings that match the confirmMenu binding. This is important to
make it clear which action will be triggered when you press the key.

In the keybindings menu this is different; the main purpose of that menu
is not to allow triggering commands by their key while the menu is open,
but to serve as a reference for what the keybindings are when it is not
open. Because of this, it is more important to show all bindings in this
menu, even if they conflict with the confirmMenu key.

This fixes a regression introduced in b3a3410a1a.

Fixes #4879.
2025-09-15 17:02:50 +02:00
Stefan Haller 26096d9dd9 Don't hide keybindings that match the confirmMenu key in the keybindings menu
In all other menus besides the keybindings menu it makes sense to hide
keybindings that match the confirmMenu binding. This is important to make it
clear which action will be triggered when you press the key.

In the keybindings menu this is different; the main purpose of that menu is not
to allow triggering commands by their key while the menu is open, but to serve
as a reference for what the keybindings are when it is not open. Because of
this, it is more important to show all bindings in this menu, even if they
conflict with the confirmMenu key.

This fixes a regression introduced in b3a3410a1a.
2025-09-07 12:43:54 +02:00
231 changed files with 6547 additions and 17911 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ name: Check Required Labels
on:
pull_request:
types: [opened, labeled, unlabeled]
types: [opened, labeled, unlabeled, synchronize]
jobs:
check-required-label:
+8 -8
View File
@@ -1,7 +1,7 @@
name: Continuous Integration
env:
GO_VERSION: 1.24
GO_VERSION: 1.25
on:
push:
@@ -32,7 +32,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.24.x
go-version: 1.25.x
- name: Test code
# we're passing -short so that we skip the integration tests, which will be run in parallel below
run: |
@@ -87,7 +87,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.24.x
go-version: 1.25.x
- name: Print git version
run: git --version
- name: Test code
@@ -113,7 +113,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.24.x
go-version: 1.25.x
- name: Build linux binary
run: |
GOOS=linux go build
@@ -140,7 +140,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.24.x
go-version: 1.25.x
- name: Check Vendor Directory
# ensure our vendor directory matches up with our go modules
run: |
@@ -166,12 +166,12 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.24.x
go-version: 1.25.x
- name: Lint
uses: golangci/golangci-lint-action@v8
with:
# If you change this, make sure to also update scripts/golangci-lint-shim.sh
version: v2.2.1
version: v2.4.0
- name: errors
run: golangci-lint run
if: ${{ failure() }}
@@ -187,7 +187,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.24.x
go-version: 1.25.x
- name: Download all coverage artifacts
uses: actions/download-artifact@v4
+1 -1
View File
@@ -139,7 +139,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.24.x
go-version: 1.25.x
- name: Run goreleaser
uses: goreleaser/goreleaser-action@v6
+6
View File
@@ -28,3 +28,9 @@ __debug_bin*
demo/output/*
coverage.out
# Nix
result
result-*
.direnv
.envrc
+1 -1
View File
@@ -1,6 +1,6 @@
version: "2"
run:
go: "1.24"
go: "1.25"
linters:
enable:
- copyloopvar
+25
View File
@@ -60,6 +60,31 @@ To run lazygit from within the integrated terminal just go `go run main.go`
This allows you to contribute to Lazygit without needing to install anything on your local machine. The Codespace has all the necessary tools and extensions pre-installed.
## Using Nix for development
If you use Nix, you can leverage the included flake to set up a complete development environment with all necessary dependencies:
```sh
nix develop
```
This will drop you into a development shell that includes:
* Latest Go toolchain
* golangci-lint for code linting
* git and make
You can also build and run lazygit using nix:
```sh
# Build lazygit
nix build
# Run lazygit directly
nix run
```
The nix flake supports multiple architectures (x86_64-linux, aarch64-linux, x86_64-darwin, aarch64-darwin) and provides a consistent development environment across different systems.
## Code of conduct
Please note by participating in this project, you agree to abide by the [code of conduct].
+1 -1
View File
@@ -2,7 +2,7 @@
# docker build -t lazygit .
# docker run -it lazygit:latest /bin/sh
FROM golang:1.24 as build
FROM golang:1.25 as build
WORKDIR /go/src/github.com/jesseduffield/lazygit/
COPY go.mod go.sum ./
RUN go mod download
+1 -3
View File
@@ -34,11 +34,9 @@ test: unit-test integration-test-all
generate:
go generate ./...
# If you execute `gofumpt -l -w .`, it will format all Go files in the current directory, including `test/_results/*` files.
# We pass only Git-tracked Go files to gofumpt because we don't want to format the test results or get errors from it.
.PHONY: format
format:
git ls-files '*.go' ':!vendor' | xargs gofumpt -l -w
gofumpt -l -w .
.PHONY: lint
lint:
+52 -7
View File
File diff suppressed because one or more lines are too long
+12
View File
@@ -0,0 +1,12 @@
(import (
let
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
nodeName = lock.nodes.root.inputs.flake-compat;
in
fetchTarball {
url =
lock.nodes.${nodeName}.locked.url
or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.${nodeName}.locked.rev}.tar.gz";
sha256 = lock.nodes.${nodeName}.locked.narHash;
}
) { src = ./.; }).defaultNix
+134 -72
View File
@@ -64,12 +64,15 @@ gui:
# One of: 'margin' (default) | 'jump'
scrollOffBehavior: margin
# The number of spaces per tab; used for everything that's shown in the main view, but probably mostly relevant for diffs.
# Note that when using a pager, the pager has its own tab width setting, so you need to pass it separately in the pager command.
# The number of spaces per tab; used for everything that's shown in the main
# view, but probably mostly relevant for diffs.
# Note that when using a pager, the pager has its own tab width setting, so you
# need to pass it separately in the pager command.
tabWidth: 4
# 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.
# 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.
mouseEvents: true
# If true, do not show a warning when amending a commit.
@@ -81,45 +84,61 @@ gui:
# If true, do not show warning when applying/popping the stash
skipStashWarning: false
# If true, do not show a warning when attempting to commit without any staged files; instead stage all unstaged files.
# If true, do not show a warning when attempting to commit without any staged
# files; instead stage all unstaged files.
skipNoStagedFilesWarning: false
# If true, do not show a warning when rewording a commit via an external editor
skipRewordInEditorWarning: false
# Fraction of the total screen width to use for the left side section. You may want to pick a small number (e.g. 0.2) if you're using a narrow screen, so that you can see more of the main section.
# If true, switch to a different worktree without confirmation when checking out
# a branch that is checked out in that worktree
skipSwitchWorktreeOnCheckoutWarning: false
# Fraction of the total screen width to use for the left side section. You may
# want to pick a small number (e.g. 0.2) if you're using a narrow screen, so
# that you can see more of the main section.
# Number from 0 to 1.0.
sidePanelWidth: 0.3333
# If true, increase the height of the focused side window; creating an accordion effect.
# If true, increase the height of the focused side window; creating an accordion
# effect.
expandFocusedSidePanel: false
# 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.
# twice as tall as the other panels. Only relevant if `expandFocusedSidePanel`
# is true.
expandedSidePanelWeight: 2
# 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.
# 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
# - 'vertical': split the window vertically
# - 'flexible': (default) split the window horizontally if the window is wide enough, otherwise split vertically
# - 'flexible': (default) split the window horizontally if the window is wide
# enough, otherwise split vertically
mainPanelSplitMode: flexible
# How the window is split when in half screen mode (i.e. after hitting '+' once).
# How the window is split when in half screen mode (i.e. after hitting '+'
# once).
# Possible values:
# - 'left': split the window horizontally (side panel on the left, main view on the right)
# - 'left': split the window horizontally (side panel on the left, main view on
# the right)
# - 'top': split the window vertically (side panel on top, main view below)
enlargedSideViewLocation: left
# If true, wrap lines in the staging view to the width of the view. This
# makes it much easier to work with diffs that have long lines, e.g.
# paragraphs of markdown text.
# If true, wrap lines in the staging view to the width of the view. This makes
# it much easier to work with diffs that have long lines, e.g. paragraphs of
# markdown text.
wrapLinesInStagingView: true
# If true, hunk selection mode will be enabled by default when entering the staging view.
# If true, hunk selection mode will be enabled by default when entering the
# staging view.
useHunkModeInStagingView: true
# One of 'auto' (default) | 'en' | 'zh-CN' | 'zh-TW' | 'pl' | 'nl' | 'ja' | 'ko' | 'ru'
# One of 'auto' (default) | 'en' | 'zh-CN' | 'zh-TW' | 'pl' | 'nl' | 'ja' | 'ko'
# | 'ru' | 'pt'
language: auto
# Format used when displaying time e.g. commit time.
@@ -192,11 +211,15 @@ gui:
# If true, show the '5 of 20' footer at the bottom of list views
showListFooter: true
# If true, display the files in the file views as a tree. If false, display the files as a flat list.
# This can be toggled from within Lazygit with the '`' key, but that will not change the default.
# If true, display the files in the file views as a tree. If false, display the
# files as a flat list.
# This can be toggled from within Lazygit with the '`' key, but that will not
# change the default.
showFileTree: true
# If true, add a "/" root item in the file tree representing the root of the repository. It is only added when necessary, i.e. when there is more than one item at top level.
# If true, add a "/" root item in the file tree representing the root of the
# repository. It is only added when necessary, i.e. when there is more than one
# item at top level.
showRootItemInFileTree: true
# If true, show the number of lines changed per file in the Files view
@@ -208,7 +231,9 @@ gui:
# If true, show the command log
showCommandLog: true
# If true, show the bottom line that contains keybinding info and useful buttons. If false, this line will be hidden except to display a loader for an in-progress action.
# If true, show the bottom line that contains keybinding info and useful
# buttons. If false, this line will be hidden except to display a loader for an
# in-progress action.
showBottomLine: true
# If true, show jump-to-window keybindings in window titles.
@@ -219,10 +244,12 @@ gui:
# If empty, do not show icons.
nerdFontsVersion: ""
# If true (default), file icons are shown in the file views. Only relevant if NerdFontsVersion is not empty.
# If true (default), file icons are shown in the file views. Only relevant if
# NerdFontsVersion is not empty.
showFileIcons: true
# Length of author name in (non-expanded) commits view. 2 means show initials only.
# Length of author name in (non-expanded) commits view. 2 means show initials
# only.
commitAuthorShortLength: 2
# Length of author name in expanded commits view. 2 means show initials only.
@@ -243,10 +270,12 @@ gui:
# Whether to split the main window when viewing file changes.
# One of: 'auto' | 'always'
# If 'auto', only split the main window when a file has both staged and unstaged changes
# If 'auto', only split the main window when a file has both staged and unstaged
# changes
splitDiff: auto
# Default size for focused window. Can be changed from within Lazygit with '+' and '_' (but this won't change the default).
# Default size for focused window. Can be changed from within Lazygit with '+'
# and '_' (but this won't change the default).
# One of: 'normal' (default) | 'half' | 'full'
screenMode: normal
@@ -254,7 +283,8 @@ gui:
# One of 'rounded' (default) | 'single' | 'double' | 'hidden' | 'bold'
border: rounded
# If true, show a seriously epic explosion animation when nuking the working tree.
# If true, show a seriously epic explosion animation when nuking the working
# tree.
animateExplosion: true
# Whether to stack UI components on top of each other.
@@ -287,27 +317,36 @@ gui:
# If true, jump to the Files panel after applying a stash
switchToFilesAfterStashApply: true
# If true, when using the panel jump keys (default 1 through 5) and target panel is already active, go to next tab instead
# If true, when using the panel jump keys (default 1 through 5) and target panel
# is already active, go to next tab instead
switchTabsWithPanelJumpKeys: false
# Config relating to git
git:
# Array of pagers. Each entry has the following format:
#
# # 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"
#
# # e.g.
# # diff-so-fancy
# # delta --dark --paging=never
# # ydiff -p cat -s --wrap --width={{columnWidth}}
# pager: ""
#
# # e.g. 'difft --color=always'
# externalDiffCommand: ""
#
# # If true, Lazygit will use git's `diff.external` config for paging.
# # The advantage over `externalDiffCommand` is that this can be
# # configured per file type in .gitattributes; see
# # https://git-scm.com/docs/gitattributes#_defining_an_external_diff_driver.
# useExternalDiffGitConfig: false
#
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Custom_Pagers.md
paging:
# 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
# e.g.
# diff-so-fancy
# delta --dark --paging=never
# ydiff -p cat -s --wrap --width={{columnWidth}}
pager: ""
# e.g. 'difft --color=always'
externalDiffCommand: ""
# If true, Lazygit will use git's `diff.external` config for paging. The advantage over `externalDiffCommand` is that this can be configured per file type in .gitattributes; see https://git-scm.com/docs/gitattributes#_defining_an_external_diff_driver.
useExternalDiffGitConfig: false
# for more information.
pagers: []
# Config relating to committing
commit:
@@ -322,22 +361,26 @@ git:
# Config relating to merging
merging:
# If true, run merges in a subprocess so that if a commit message is required, Lazygit will not hang
# If true, run merges in a subprocess so that if a commit message is required,
# Lazygit will not hang
# Only applicable to unix users.
manualCommit: false
# Extra args passed to `git merge`, e.g. --no-ff
args: ""
# The commit message to use for a squash merge commit. Can contain "{{selectedRef}}" and "{{currentBranch}}" placeholders.
# The commit message to use for a squash merge commit. Can contain
# "{{selectedRef}}" and "{{currentBranch}}" placeholders.
squashMergeMessage: Squash merge {{selectedRef}} into {{currentBranch}}
# list of branches that are considered 'main' branches, used when displaying commits
# list of branches that are considered 'main' branches, used when displaying
# commits
mainBranches:
- master
- main
# Prefix to use when skipping hooks. E.g. if set to 'WIP', then pre-commit hooks will be skipped when the commit message starts with 'WIP'
# Prefix to use when skipping hooks. E.g. if set to 'WIP', then pre-commit hooks
# will be skipped when the commit message starts with 'WIP'
skipHookPrefix: WIP
# If true, periodically fetch from remote
@@ -346,7 +389,10 @@ git:
# If true, periodically refresh files and submodules
autoRefresh: 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 (as opposed to diverged).
# If not "none", lazygit will automatically fast-forward local branches to match
# their upstream after fetching. Applies to branches that are not the currently
# checked out branch, and only to those that are strictly behind their upstream
# (as opposed to diverged).
# Possible values: 'none' | 'onlyMainBranches' | 'allBranches'
autoForwardBranches: onlyMainBranches
@@ -354,25 +400,29 @@ git:
fetchAll: true
# If true, lazygit will automatically stage files that used to have merge
# conflicts but no longer do; and it will also ask you if you want to
# continue a merge or rebase if you've resolved all conflicts. If false, it
# won't do either of these things.
# conflicts but no longer do; and it will also ask you if you want to continue a
# merge or rebase if you've resolved all conflicts. If false, it won't do either
# of these things.
autoStageResolvedConflicts: true
# Command used when displaying the current branch git log in the main window
branchLogCmd: git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --
# Commands used to display git log of all branches in the main window, they will be cycled in order of appearance (array of strings)
# Commands used to display git log of all branches in the main window, they will
# be cycled in order of appearance (array of strings)
allBranchesLogCmds:
- git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium
# If true, git diffs are rendered with the `--ignore-all-space` flag, which ignores whitespace changes. Can be toggled from within Lazygit with `<c-w>`.
# If true, git diffs are rendered with the `--ignore-all-space` flag, which
# ignores whitespace changes. Can be toggled from within Lazygit with `<c-w>`.
ignoreWhitespaceInDiffView: false
# The number of lines of context to show around each diff hunk. Can be changed from within Lazygit with the `{` and `}` keys.
# The number of lines of context to show around each diff hunk. Can be changed
# from within Lazygit with the `{` and `}` keys.
diffContextSize: 3
# The threshold for considering a file to be renamed, in percent. Can be changed from within Lazygit with the `(` and `)` keys.
# The threshold for considering a file to be renamed, in percent. Can be changed
# from within Lazygit with the `(` and `)` keys.
renameSimilarityThreshold: 50
# If true, do not spawn a separate process when using GPG
@@ -400,30 +450,35 @@ git:
# 'topo-order' makes it easier to read the git log graph, but commits may not
# appear chronologically. See https://git-scm.com/docs/
#
# Can be changed from within Lazygit with `Log menu -> Commit sort order` (`<c-l>` in the commits window by default).
# Can be changed from within Lazygit with `Log menu -> Commit sort order`
# (`<c-l>` in the commits window by default).
order: topo-order
# This determines whether the git graph is rendered in the commits panel
# One of 'always' | 'never' | 'when-maximised'
#
# Can be toggled from within lazygit with `Log menu -> Show git graph` (`<c-l>` in the commits window by default).
# Can be toggled from within lazygit with `Log menu -> Show git graph` (`<c-l>`
# in the commits window by default).
showGraph: always
# displays the whole git graph by default in the commits view (equivalent to passing the `--all` argument to `git log`)
# displays the whole git graph by default in the commits view (equivalent to
# passing the `--all` argument to `git log`)
showWholeGraph: false
# How branches are sorted in the local branches view.
# One of: 'date' (default) | 'recency' | 'alphabetical'
# Can be changed from within Lazygit with the Sort Order menu (`s`) in the branches panel.
# Can be changed from within Lazygit with the Sort Order menu (`s`) in the
# branches panel.
localBranchSortOrder: date
# How branches are sorted in the remote branches view.
# One of: 'date' (default) | 'alphabetical'
# Can be changed from within Lazygit with the Sort Order menu (`s`) in the remote branches panel.
# Can be changed from within Lazygit with the Sort Order menu (`s`) in the
# remote branches panel.
remoteBranchSortOrder: date
# When copying commit hashes to the clipboard, truncate them to this
# length. Set to 40 to disable truncation.
# When copying commit hashes to the clipboard, truncate them to this length. Set
# to 40 to disable truncation.
truncateCopiedCommitHashesTo: 12
# Periodic update checks
@@ -447,10 +502,12 @@ refresher:
# If true, show a confirmation popup before quitting Lazygit
confirmOnQuit: false
# If true, exit Lazygit when the user presses escape in a context where there is nothing to cancel/close
# If true, exit Lazygit when the user presses escape in a context where there is
# nothing to cancel/close
quitOnTopLevelReturn: false
# Config relating to things outside of Lazygit like how files are opened, copying to clipboard, etc
# Config relating to things outside of Lazygit like how files are opened,
# copying to clipboard, etc
os:
# Command for editing a file. Should contain "{{filename}}".
edit: ""
@@ -459,8 +516,8 @@ os:
# "{{filename}}", and may optionally contain "{{line}}".
editAtLine: ""
# Same as EditAtLine, except that the command needs to wait until the
# window is closed.
# Same as EditAtLine, except that the command needs to wait until the window is
# closed.
editAtLineAndWait: ""
# Whether lazygit suspends until an edit process returns
@@ -469,12 +526,12 @@ os:
# For opening a directory in an editor
openDirInEditor: ""
# A built-in preset that sets all of the above settings. Supported presets
# are defined in the getPreset function in editor_presets.go.
# A built-in preset that sets all of the above settings. Supported presets are
# defined in the getPreset function in editor_presets.go.
editPreset: ""
# Command for opening a file, as if the file is double-clicked. Should
# contain "{{filename}}", but doesn't support "{{line}}".
# Command for opening a file, as if the file is double-clicked. Should contain
# "{{filename}}", but doesn't support "{{line}}".
open: ""
# Command for opening a link. Should contain "{{link}}".
@@ -488,7 +545,9 @@ os:
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-command-for-copying-to-and-pasting-from-clipboard
readFromClipboardCmd: ""
# A shell startup file containing shell aliases or shell functions. This will be sourced before running any shell commands, so that shell functions are available in the `:` command prompt or even in custom commands.
# A shell startup file containing shell aliases or shell functions. This will be
# sourced before running any shell commands, so that shell functions are
# available in the `:` command prompt or even in custom commands.
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#using-aliases-or-functions-in-shell-commands
shellFunctionsFile: ""
@@ -503,13 +562,15 @@ customCommands: []
services: {}
# What to do when opening Lazygit outside of a git repo.
# - 'prompt': (default) ask whether to initialize a new repo or open in the most recent repo
# - 'prompt': (default) ask whether to initialize a new repo or open in the most
# recent repo
# - 'create': initialize a new repo
# - 'skip': open most recent repo
# - 'quit': exit Lazygit
notARepository: prompt
# If true, display a confirmation when subprocess terminates. This allows you to view the output of the subprocess before returning to Lazygit.
# If true, display a confirmation when subprocess terminates. This allows you to
# view the output of the subprocess before returning to Lazygit.
promptToReturnFromSubprocess: true
# Keybindings
@@ -585,6 +646,7 @@ keybinding:
prevTab: '['
nextScreenMode: +
prevScreenMode: _
cyclePagers: '|'
undo: z
redo: Z
filteringMenu: <c-s>
@@ -619,7 +681,7 @@ keybinding:
viewResetOptions: D
fetch: f
toggleTreeView: '`'
openMergeTool: M
openMergeOptions: M
openStatusFilter: <c-b>
copyFileInfoToClipboard: "y"
collapseAll: '-'
+50 -20
View File
@@ -2,25 +2,29 @@
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.
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.
## Default:
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:
```yaml
git:
paging:
colorArg: always
pagers:
- pager: delta --dark --paging=never
- pager: ydiff -p cat -s --wrap --width={{columnWidth}}
colorArg: never
- externalDiffCommand: difft --color=always
```
the `colorArg` key is for whether you want the `--color=always` arg in your `git diff` command. Some pagers want it set to `always`, others want it set to `never`.
The `colorArg` key is for whether you want the `--color=always` arg in your `git diff` command. Some pagers want it set to `always`, others want it set to `never`. The default is `always`, since that's what most pagers need.
## Delta:
```yaml
git:
paging:
colorArg: always
pager: delta --dark --paging=never
pagers:
- pager: delta --dark --paging=never
```
![](https://i.imgur.com/QJpQkF3.png)
@@ -31,9 +35,8 @@ A cool feature of delta is --hyperlinks, which renders clickable links for the l
```yaml
git:
paging:
colorArg: always
pager: diff-so-fancy
pagers:
- pager: diff-so-fancy
```
![](https://i.imgur.com/rjH1TpT.png)
@@ -44,9 +47,9 @@ git:
gui:
sidePanelWidth: 0.2 # gives you more space to show things side-by-side
git:
paging:
colorArg: never
pager: ydiff -p cat -s --wrap --width={{columnWidth}}
pagers:
- colorArg: never
pager: ydiff -p cat -s --wrap --width={{columnWidth}}
```
![](https://i.imgur.com/vaa8z0H.png)
@@ -61,8 +64,8 @@ These can be used in lazygit by using the `externalDiffCommand` config; in the c
```yaml
git:
paging:
externalDiffCommand: difft --color=always
pagers:
- externalDiffCommand: difft --color=always
```
The `colorArg` and `pager` options are not used in this case.
@@ -71,16 +74,43 @@ You can add whatever extra arguments you prefer for your difftool; for instance
```yaml
git:
paging:
externalDiffCommand: difft --color=always --display=inline --syntax-highlight=off
pagers:
- externalDiffCommand: difft --color=always --display=inline --syntax-highlight=off
```
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
git:
paging:
useExternalDiffGitConfig: true
pagers:
- useExternalDiffGitConfig: true
```
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.)
+3 -2
View File
@@ -24,6 +24,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` 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 |
| `` <esc> `` | Cancel | |
| `` ? `` | Open keybindings menu | |
| `` <c-s> `` | View filter options | View options for filtering the commit log, so that only commits matching the filter are shown. |
@@ -153,7 +154,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` D `` | Reset | View reset options for working tree (e.g. nuking the working tree). |
| `` ` `` | Toggle file tree view | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.<br><br>The default can be changed in the config file with the key 'gui.showFileTree'. |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` M `` | Open external merge tool | Run `git mergetool`. |
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
| `` 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 |
@@ -210,7 +211,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` z `` | Undo | Undo last merge conflict resolution. |
| `` e `` | Edit file | Open file in external editor. |
| `` o `` | Open file | Open file in default application. |
| `` M `` | Open external merge tool | Run `git mergetool`. |
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
| `` <esc> `` | Return to files panel | |
## Main panel (normal)
+3 -2
View File
@@ -24,6 +24,7 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味
| `` R `` | 更新 | Gitの状態を更新します(`git status`、`git branch`などをバックグラウンドで実行してパネルの内容を更新します)。これは`git fetch`を実行しません。 |
| `` + `` | 次の画面モード(通常/半分/全画面) | |
| `` _ `` | 前の画面モード | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers |
| `` <esc> `` | キャンセル | |
| `` ? `` | キーバインディングメニューを開く | |
| `` <c-s> `` | フィルターオプションを表示 | コミットログのフィルタリングオプションを表示し、フィルタに一致するコミットのみを表示します。 |
@@ -235,7 +236,7 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味
| `` D `` | リセット | 作業ツリーのリセットオプション(例:作業ツリーの完全破棄)を表示します。 |
| `` ` `` | ファイルツリービューを切り替え | 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'. |
| `` <c-t> `` | 外部差分ツールを開く(git difftool) | |
| `` M `` | 外部マージツールを開く | `git mergetool`を実行します。 |
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
| `` f `` | フェッチ | リモートから変更をフェッチします。 |
| `` - `` | すべてのファイルを折りたたむ | ファイルツリー内のすべてのディレクトリを折りたたみます |
| `` = `` | すべてのファイルを展開 | ファイルツリー内のすべてのディレクトリを展開します |
@@ -292,7 +293,7 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味
| `` z `` | 元に戻す | 最後のマージコンフリクト解決を元に戻します。 |
| `` e `` | ファイルを編集 | 外部エディタでファイルを開きます。 |
| `` o `` | ファイルを開く | デフォルトのアプリケーションでファイルを開きます。 |
| `` M `` | 外部マージツールを開く | `git mergetool`を実行します。 |
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
| `` <esc> `` | ファイルパネルに戻る | |
## メインパネル(通常)
+5 -4
View File
@@ -24,6 +24,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` 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 |
| `` <esc> `` | 취소 | |
| `` ? `` | 매뉴 열기 | |
| `` <c-s> `` | View filter-by-path options | View options for filtering the commit log, so that only commits matching the filter are shown. |
@@ -152,7 +153,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` z `` | 되돌리기 | Undo last merge conflict resolution. |
| `` e `` | 파일 편집 | Open file in external editor. |
| `` o `` | 파일 닫기 | Open file in default application. |
| `` M `` | Git mergetool를 열기 | Run `git mergetool`. |
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
| `` <esc> `` | 파일 목록으로 돌아가기 | |
## 메인 패널 (Normal)
@@ -196,7 +197,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` <esc> `` | 파일 목록으로 돌아가기 | |
| `` <tab> `` | 패널 전환 | Switch to other view (staged/unstaged changes). |
| `` E `` | Edit hunk | Edit selected hunk in external editor. |
| `` c `` | 커밋 변경내용 | Commit staged changes. |
| `` c `` | 커밋 변경내용 | 스테이징된 변경 사항 커밋. |
| `` w `` | Commit changes without pre-commit hook | |
| `` C `` | Git 편집기를 사용하여 변경 내용을 커밋합니다. | |
| `` <c-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> |
@@ -378,7 +379,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` <space> `` | Staged 전환 | Toggle staged for selected file. |
| `` <c-b> `` | 파일을 필터하기 (Staged/unstaged) | |
| `` y `` | 클립보드에 복사 | |
| `` c `` | 커밋 변경내용 | Commit staged changes. |
| `` c `` | 커밋 변경내용 | 스테이징된 변경 사항 커밋. |
| `` w `` | Commit changes without pre-commit hook | |
| `` A `` | 마지맛 커밋 수정 | |
| `` C `` | Git 편집기를 사용하여 변경 내용을 커밋합니다. | |
@@ -396,7 +397,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` D `` | 초기화 | View reset options for working tree (e.g. nuking the working tree). |
| `` ` `` | 파일 트리뷰로 전환 | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.<br><br>The default can be changed in the config file with the key 'gui.showFileTree'. |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` M `` | Git mergetool를 열기 | Run `git mergetool`. |
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
| `` 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 |
+3 -2
View File
@@ -24,6 +24,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` 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 |
| `` <esc> `` | Annuleren | |
| `` ? `` | Open menu | |
| `` <c-s> `` | Bekijk scoping opties | View options for filtering the commit log, so that only commits matching the filter are shown. |
@@ -78,7 +79,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` D `` | Reset | 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'. |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` M `` | Open external merge tool | Run `git mergetool`. |
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
| `` 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 |
@@ -218,7 +219,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` 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 `` | Open external merge tool | Run `git mergetool`. |
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
| `` <esc> `` | Ga terug naar het bestanden paneel | |
## Normaal
+3 -2
View File
@@ -24,6 +24,7 @@ _Legenda: `<c-b>` oznacza ctrl+b, `<a-b>` oznacza alt+b, `B` oznacza shift+b_
| `` 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 |
| `` <esc> `` | Anuluj | |
| `` ? `` | Otwórz menu przypisań klawiszy | |
| `` <c-s> `` | Pokaż opcje filtrowania | Pokaż opcje filtrowania dziennika commitów, tak aby pokazywane były tylko commity pasujące do filtra. |
@@ -193,7 +194,7 @@ _Legenda: `<c-b>` oznacza ctrl+b, `<a-b>` oznacza alt+b, `B` oznacza shift+b_
| `` z `` | Cofnij | Cofnij ostatnie rozwiązanie konfliktu scalania. |
| `` e `` | Edytuj plik | Otwórz plik w zewnętrznym edytorze. |
| `` o `` | Otwórz plik | Otwórz plik w domyślnej aplikacji. |
| `` M `` | Otwórz zewnętrzne narzędzie scalania | Uruchom `git mergetool`. |
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
| `` <esc> `` | Wróć do panelu plików | |
## Panel główny (zatwierdzanie)
@@ -252,7 +253,7 @@ _Legenda: `<c-b>` oznacza ctrl+b, `<a-b>` oznacza alt+b, `B` oznacza shift+b_
| `` D `` | Reset | Wyświetl opcje resetu dla drzewa roboczego (np. zniszczenie drzewa roboczego). |
| `` ` `` | Przełącz widok drzewa plików | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.<br><br>The default can be changed in the config file with the key 'gui.showFileTree'. |
| `` <c-t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
| `` M `` | Otwórz zewnętrzne narzędzie scalania | Uruchom `git mergetool`. |
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
| `` f `` | Pobierz | Pobierz zmiany ze zdalnego serwera. |
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
+3 -2
View File
@@ -24,6 +24,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` 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`. |
| `` + `` | Next screen mode (normal/half/fullscreen) | |
| `` _ `` | Prev screen mode | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers |
| `` <esc> `` | Cancelar | |
| `` ? `` | Open keybindings menu | |
| `` <c-s> `` | View filter options | View options for filtering the commit log, so that only commits matching the filter are shown. |
@@ -78,7 +79,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` D `` | Restaurar | Opções de redefinição de exibição para árvore de trabalho (por exemplo, nukando a árvore de trabalho). |
| `` ` `` | Alternar exibição de árvore de arquivo | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.<br><br>The default can be changed in the config file with the key 'gui.showFileTree'. |
| `` <c-t> `` | Abrir ferramenta de diff externa (git difftool) | |
| `` M `` | Abrir ferramenta de merge externa | Execute `git mergetool`. |
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
| `` f `` | Buscar | Buscar alterações do controle remoto. |
| `` - `` | Recolher todos os arquivos | Recolher todos os diretórios na árvore de arquivos |
| `` = `` | Expandir todos os arquivos | Expandir todos os diretórios na árvore do arquivo |
@@ -278,7 +279,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` z `` | Desfazer | Desfazer resolução de conflitos de última mesclagem. |
| `` e `` | Editar arquivo | Abrir arquivo no editor externo. |
| `` o `` | Abrir arquivo | Abrir arquivo no aplicativo padrão. |
| `` M `` | Abrir ferramenta de merge externa | Execute `git mergetool`. |
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
| `` <esc> `` | Retornar ao painel de arquivos | |
## Painel principal (patch build)
+6 -5
View File
@@ -24,6 +24,7 @@ _Связки клавиш_
| `` 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 |
| `` <esc> `` | Отменить | |
| `` ? `` | Открыть меню | |
| `` <c-s> `` | Просмотреть параметры фильтрации по пути | View options for filtering the commit log, so that only commits matching the filter are shown. |
@@ -122,7 +123,7 @@ _Связки клавиш_
| `` z `` | Отменить | Undo last merge conflict resolution. |
| `` e `` | Редактировать файл | Open file in external editor. |
| `` o `` | Открыть файл | Open file in default application. |
| `` M `` | Открыть внешний инструмент слияния (git mergetool) | Run `git mergetool`. |
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
| `` <esc> `` | Вернуться к панели файлов | |
## Главная панель (сборка патчей)
@@ -152,7 +153,7 @@ _Связки клавиш_
| `` 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). |
| `` 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. |
| `` <c-r> `` | Сбросить отобранную (скопированную | cherry-picked) выборку коммитов | |
| `` <c-r> `` | Сбросить отобранную (скопированную \| cherry-picked) выборку коммитов | |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
@@ -165,7 +166,7 @@ _Связки клавиш_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Скопировать hash коммита в буфер обмена | |
| `` <c-r> `` | Сбросить отобранную (скопированную | cherry-picked) выборку коммитов | |
| `` <c-r> `` | Сбросить отобранную (скопированную \| cherry-picked) выборку коммитов | |
| `` b `` | Просмотреть параметры бинарного поиска | |
| `` s `` | Объединить коммиты (Squash) | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
| `` f `` | Объединить несколько коммитов в один отбросив сообщение коммита (Fixup) | Meld the selected commit into the commit below it. Similar to squash, but the selected commit's message will be discarded. |
@@ -258,7 +259,7 @@ _Связки клавиш_
| `` 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). |
| `` 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. |
| `` <c-r> `` | Сбросить отобранную (скопированную | cherry-picked) выборку коммитов | |
| `` <c-r> `` | Сбросить отобранную (скопированную \| cherry-picked) выборку коммитов | |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
@@ -390,7 +391,7 @@ _Связки клавиш_
| `` D `` | Reset | View reset options for working tree (e.g. nuking the working tree). |
| `` ` `` | Переключить вид дерева файлов | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.<br><br>The default can be changed in the config file with the key 'gui.showFileTree'. |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` M `` | Открыть внешний инструмент слияния (git mergetool) | Run `git mergetool`. |
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
| `` f `` | Получить изменения | Fetch changes from remote. |
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
+3 -2
View File
@@ -24,6 +24,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` R `` | 刷新 | 刷新git状态(即在后台上运行`git status`,`git branch`等命令以更新面板内容) 不会运行`git fetch` |
| `` + `` | 下一屏模式(正常/半屏/全屏) | |
| `` _ `` | 上一屏模式 | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers |
| `` <esc> `` | 取消 | |
| `` ? `` | 打开菜单 | |
| `` <c-s> `` | 查看按路径过滤选项 | 查看用于过滤提交日志的选项,以便仅显示与过滤器匹配的提交。 |
@@ -216,7 +217,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` D `` | 重置 | 查看工作树的重置选项(例如:清除工作树)。 |
| `` ` `` | 切换文件树视图 | 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'. |
| `` <c-t> `` | 使用外部差异比较工具(git difftool) | |
| `` M `` | 打开外部合并工具(git mergetool) | 执行 `git mergetool`. |
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
| `` f `` | 抓取 | 从远程获取变更 |
| `` - `` | 折叠全部文件 | 折叠文件树中的全部目录 |
| `` = `` | 展开全部文件 | 展开文件树中的全部目录 |
@@ -305,7 +306,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` z `` | 撤销 | 撤消上次合并冲突解决 |
| `` e `` | 编辑文件 | 使用外部编辑器打开文件 |
| `` o `` | 打开文件 | 使用默认程序打开该文件 |
| `` M `` | 打开外部合并工具(git mergetool) | 执行 `git mergetool`. |
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
| `` <esc> `` | 返回文件面板 | |
## 正在暂存
+3 -2
View File
@@ -24,6 +24,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B
| `` 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 |
| `` <esc> `` | 取消 | |
| `` ? `` | 開啟選單 | |
| `` <c-s> `` | 檢視篩選路徑選項 | View options for filtering the commit log, so that only commits matching the filter are shown. |
@@ -97,7 +98,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B
| `` z `` | 復原 | Undo last merge conflict resolution. |
| `` e `` | 編輯檔案 | 使用外部編輯器開啟 |
| `` o `` | 開啟檔案 | 使用預設軟體開啟 |
| `` M `` | 開啟外部合併工具 | 執行 `git mergetool`。 |
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
| `` <esc> `` | 返回檔案面板 | |
## 主面板(預存)
@@ -347,7 +348,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B
| `` D `` | 重設 | View reset options for working tree (e.g. nuking the working tree). |
| `` ` `` | 顯示檔案樹狀視圖 | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.<br><br>The default can be changed in the config file with the key 'gui.showFileTree'. |
| `` <c-t> `` | 開啟外部差異工具 (git difftool) | |
| `` M `` | 開啟外部合併工具 | 執行 `git mergetool`。 |
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
| `` f `` | 擷取 | 同步遠端異動 |
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
Generated
+127
View File
@@ -0,0 +1,127 @@
{
"nodes": {
"flake-compat": {
"locked": {
"lastModified": 1733328505,
"narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
"revCount": 69,
"type": "tarball",
"url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1759362264,
"narHash": "sha256-wfG0S7pltlYyZTM+qqlhJ7GMw2fTF4mLKCIVhLii/4M=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "758cf7296bee11f1706a574c77d072b8a7baa881",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1759831965,
"narHash": "sha256-vgPm2xjOmKdZ0xKA6yLXPJpjOtQPHfaZDRtH+47XEBo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c9b6fb798541223bbb396d287d16f43520250518",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1754788789,
"narHash": "sha256-x2rJ+Ovzq0sCMpgfgGaaqgBSwY+LST+WbZ6TytnT9Rk=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "a73b9c743612e4244d865a2fdee11865283c04e6",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1754340878,
"narHash": "sha256-lgmUyVQL9tSnvvIvBp7x1euhkkCho7n3TMzgjdvgPoU=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "cab778239e705082fe97bb4990e0d24c50924c04",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-compat": "flake-compat",
"flake-parts": "flake-parts",
"nixpkgs": "nixpkgs",
"systems": "systems",
"treefmt-nix": "treefmt-nix"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1758728421,
"narHash": "sha256-ySNJ008muQAds2JemiyrWYbwbG+V7S5wg3ZVKGHSFu8=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "5eda4ee8121f97b218f7cc73f5172098d458f1d1",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
+125
View File
@@ -0,0 +1,125 @@
{
description = "A simple terminal UI for git commands";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
systems.url = "github:nix-systems/default";
flake-parts.url = "github:hercules-ci/flake-parts";
flake-compat.url = "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz";
treefmt-nix.url = "github:numtide/treefmt-nix";
};
outputs =
inputs@{ flake-parts, systems, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
systems = import systems;
imports = [
inputs.treefmt-nix.flakeModule
];
perSystem =
{
pkgs,
system,
...
}:
let
goMod = builtins.readFile ./go.mod;
versionMatch = builtins.match ".*go[[:space:]]([0-9]+\\.[0-9]+)(\\.[0-9]+)?.*" goMod;
goVersion =
if versionMatch != null then
builtins.head versionMatch
else
throw "Could not extract Go version from go.mod";
goOverlay = final: prev: {
go = prev."go_${builtins.replaceStrings [ "." ] [ "_" ] goVersion}";
};
lazygit = pkgs.buildGoModule rec {
pname = "lazygit";
version = "dev";
gitCommit = inputs.self.rev or inputs.self.dirtyRev or "dev";
src = ./.;
vendorHash = null;
# Disable integration tests that require specific environment
doCheck = false;
nativeBuildInputs = with pkgs; [
git
makeWrapper
];
buildInputs = [ pkgs.git ];
ldflags = [
"-s"
"-w"
"-X main.commit=${gitCommit}"
"-X main.version=${version}"
"-X main.buildSource=nix"
];
postInstall = ''
wrapProgram $out/bin/lazygit \
--prefix PATH : ${pkgs.lib.makeBinPath [ pkgs.git ]}
'';
meta = {
description = "A simple terminal UI for git commands";
homepage = "https://github.com/jesseduffield/lazygit";
license = pkgs.lib.licenses.mit;
maintainers = [ "jesseduffield" ];
platforms = pkgs.lib.platforms.unix;
mainProgram = "lazygit";
};
};
in
{
_module.args.pkgs = import inputs.nixpkgs {
inherit system;
overlays = [ goOverlay ];
config = { };
};
packages = {
default = lazygit;
inherit lazygit;
};
devShells.default = pkgs.mkShell {
name = "lazygit-dev";
buildInputs = with pkgs; [
# Go toolchain
go
gotools
# Development tools
git
gnumake
];
# Environment variables for development
CGO_ENABLED = "0";
};
treefmt = {
programs.nixfmt.enable = pkgs.lib.meta.availableOn pkgs.stdenv.buildPlatform pkgs.nixfmt-rfc-style.compiler;
programs.nixfmt.package = pkgs.nixfmt-rfc-style;
programs.gofmt.enable = true;
};
checks.build = lazygit;
};
flake = {
overlays.default = final: prev: {
lazygit = inputs.self.packages.${final.system}.lazygit;
};
};
};
}
+15 -12
View File
@@ -1,6 +1,9 @@
module github.com/jesseduffield/lazygit
go 1.24.0
go 1.25.0
// This is necessary to ignore test files when executing gofumpt.
ignore ./test
require (
dario.cat/mergo v1.0.1
@@ -9,26 +12,26 @@ require (
github.com/aybabtme/humanlog v0.4.1
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
github.com/creack/pty v1.1.11
github.com/gdamore/tcell/v2 v2.8.1
github.com/gdamore/tcell/v2 v2.9.0
github.com/go-errors/errors v1.5.1
github.com/gookit/color v1.4.2
github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd
github.com/jesseduffield/gocui v0.3.1-0.20250711082438-4aa4fd0b4d22
github.com/jesseduffield/gocui v0.3.1-0.20251002151855-67e0e55ff42a
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/karimkhaleel/jsonschema v0.0.0-20231001195015-d933f0d94ea3
github.com/kyokomi/emoji/v2 v2.2.8
github.com/lucasb-eyer/go-colorful v1.2.0
github.com/mattn/go-runewidth v0.0.16
github.com/lucasb-eyer/go-colorful v1.3.0
github.com/mattn/go-runewidth v0.0.19
github.com/mgutz/str v1.2.0
github.com/mitchellh/go-ps v1.0.0
github.com/sahilm/fuzzy v0.1.0
github.com/samber/lo v1.31.0
github.com/sanity-io/litter v1.5.2
github.com/sasha-s/go-deadlock v0.3.5
github.com/sasha-s/go-deadlock v0.3.6
github.com/sirupsen/logrus v1.9.3
github.com/spf13/afero v1.9.5
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
@@ -36,8 +39,8 @@ require (
github.com/stretchr/testify v1.10.0
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
golang.org/x/sync v0.16.0
golang.org/x/sys v0.34.0
golang.org/x/sync v0.17.0
golang.org/x/sys v0.36.0
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -47,6 +50,7 @@ require (
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
@@ -68,18 +72,17 @@ require (
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/onsi/ginkgo v1.10.3 // indirect
github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect
github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/term v0.33.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/term v0.35.0 // indirect
golang.org/x/text v0.29.0 // indirect
gopkg.in/fsnotify.v1 v1.4.7 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
+22 -19
View File
@@ -65,6 +65,8 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do=
@@ -96,8 +98,8 @@ github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
github.com/gdamore/tcell/v2 v2.8.0/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw=
github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU=
github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw=
github.com/gdamore/tcell/v2 v2.9.0 h1:N6t+eqK7/xwtRPwxzs1PXeRWnm0H9l02CrgJ7DLn1ys=
github.com/gdamore/tcell/v2 v2.9.0/go.mod h1:8/ZoqM9rxzYphT9tH/9LnunhV9oPBqwS8WHGYm5nrmo=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
@@ -194,8 +196,8 @@ github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c h1:tC2Paiis
github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c/go.mod h1:F2fEBk0ddf6ixrBrJjY7phfQ3hL9rXG0uSjvwYe50bE=
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd h1:ViKj6qth8FgcIWizn9KiACWwPemWSymx62OPN0tHT+Q=
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd/go.mod h1:lRhCiBr6XjQrvcQVa+UYsy/99d3wMXn/a0nSQlhnhlA=
github.com/jesseduffield/gocui v0.3.1-0.20250711082438-4aa4fd0b4d22 h1:vhMwEsLlMtuKKo9/z3Qcggycgad8oV7+siwOZEnJDOs=
github.com/jesseduffield/gocui v0.3.1-0.20250711082438-4aa4fd0b4d22/go.mod h1:sLIyZ2J42R6idGdtemZzsiR3xY5EF0KsvYEGh3dQv3s=
github.com/jesseduffield/gocui v0.3.1-0.20251002151855-67e0e55ff42a h1:z3NQvFXAWGT4B/MwQBZc+1ej8WJ/Nv35xngQRvwzPuI=
github.com/jesseduffield/gocui v0.3.1-0.20251002151855-67e0e55ff42a/go.mod h1:sLIyZ2J42R6idGdtemZzsiR3xY5EF0KsvYEGh3dQv3s=
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY=
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5/go.mod h1:qxN4mHOAyeIDLP7IK7defgPClM/z1Kze8VVQiaEjzsQ=
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e h1:uw/oo+kg7t/oeMs6sqlAwr85ND/9cpO3up3VxphxY0U=
@@ -224,8 +226,9 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/kyokomi/emoji/v2 v2.2.8 h1:jcofPxjHWEkJtkIbcLHvZhxKgCPl6C7MyjTrD4KDqUE=
github.com/kyokomi/emoji/v2 v2.2.8/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
@@ -237,8 +240,9 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mgutz/str v1.2.0 h1:4IzWSdIz9qPQWLfKZ0rJcV0jcUDpxvP4JVZ4GXQyvSw=
github.com/mgutz/str v1.2.0/go.mod h1:w1v0ofgLaJdoD0HpQ3fycxKD1WtxpjSo151pK/31q6w=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
@@ -247,8 +251,8 @@ github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY=
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw=
github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
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/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -260,7 +264,6 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
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.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
@@ -271,8 +274,8 @@ github.com/samber/lo v1.31.0 h1:Sfa+/064Tdo4SvlohQUQzBhgSer9v/coGvKQI/XLWAM=
github.com/samber/lo v1.31.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8=
github.com/sanity-io/litter v1.5.2 h1:AnC8s9BMORWH5a4atZ4D6FPVvKGzHcnc5/IVTa87myw=
github.com/sanity-io/litter v1.5.2/go.mod h1:5Z71SvaYy5kcGtyglXOC9rrUi3c1E8CamFWjQsazTh0=
github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU=
github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U=
github.com/sasha-s/go-deadlock v0.3.6 h1:TR7sfOnZ7x00tWPfD397Peodt57KzMDo+9Ae9rMiUmw=
github.com/sasha-s/go-deadlock v0.3.6/go.mod h1:CUqNyyvMxTyjFqDT7MRg9mb4Dv/btmGTqSR+rky/UXo=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
@@ -434,8 +437,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20170407050850-f3918c30c5c2/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -488,8 +491,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
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=
@@ -499,8 +502,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -515,8 +518,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+6 -1
View File
@@ -125,8 +125,13 @@ func Start(buildInfo *BuildInfo, integrationTest integrationTypes.IntegrationTes
os.Exit(0)
}
tempDir, err := os.MkdirTemp(getTempDirBase(), "lazygit-*")
tempDirBase := getTempDirBase()
tempDir, err := os.MkdirTemp(tempDirBase, "lazygit-*")
if err != nil {
if os.IsPermission(err) {
log.Fatalf("Your temp directory (%s) is not writeable. Try if rebooting your machine fixes this.", tempDirBase)
}
log.Fatal(err.Error())
}
defer os.RemoveAll(tempDir)
+5
View File
@@ -225,6 +225,11 @@ func formatBinding(binding *types.Binding) string {
// Replace newlines with <br> tags for proper markdown table formatting
tooltip := strings.ReplaceAll(binding.Tooltip, "\n", "<br>")
// Escape pipe characters to avoid breaking the table format
action = strings.ReplaceAll(action, `|`, `\|`)
description = strings.ReplaceAll(description, `|`, `\|`)
tooltip = strings.ReplaceAll(tooltip, `|`, `\|`)
// Use backticks for keyboard keys. Two backticks are needed with an inner space
// to escape a key that is itself a backtick.
return fmt.Sprintf("| `` %s `` | %s | %s |\n", action, description, tooltip)
+5 -1
View File
@@ -12,6 +12,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -59,6 +60,7 @@ func NewGitCommand(
version *git_commands.GitVersion,
osCommand *oscommands.OSCommand,
gitConfig git_config.IGitConfig,
pagerConfig *config.PagerConfig,
) (*GitCommand, error) {
repoPaths, err := git_commands.GetRepoPaths(osCommand.Cmd, version)
if err != nil {
@@ -88,6 +90,7 @@ func NewGitCommand(
gitConfig,
repoPaths,
repository,
pagerConfig,
), nil
}
@@ -98,6 +101,7 @@ func NewGitCommandAux(
gitConfig git_config.IGitConfig,
repoPaths *git_commands.RepoPaths,
repo *gogit.Repository,
pagerConfig *config.PagerConfig,
) *GitCommand {
cmd := NewGitCmdObjBuilder(cmn.Log, osCommand.Cmd)
@@ -108,7 +112,7 @@ func NewGitCommandAux(
// common ones are: cmn, osCommand, dotGitDir, configCommands
configCommands := git_commands.NewConfigCommands(cmn, gitConfig, repo)
gitCommon := git_commands.NewGitCommon(cmn, version, cmd, osCommand, repoPaths, repo, configCommands)
gitCommon := git_commands.NewGitCommon(cmn, version, cmd, osCommand, repoPaths, repo, configCommands, pagerConfig)
fileLoader := git_commands.NewFileLoader(gitCommon, cmd, configCommands)
statusCommands := git_commands.NewStatusCommands(gitCommon)
+64 -13
View File
@@ -13,7 +13,7 @@ import (
type BranchCommands struct {
*GitCommon
allBranchesLogCmdIndex uint8 // keeps track of current all branches log command
allBranchesLogCmdIndex int // keeps track of current all branches log command
}
func NewBranchCommands(gitCommon *GitCommon) *BranchCommands {
@@ -116,6 +116,22 @@ func (self *BranchCommands) CurrentBranchName() (string, error) {
return strings.TrimSpace(output), nil
}
// Gets the full ref name of the previously checked out branch. Can return an empty string (but no
// error) e.g. when the previously checked out thing was a detached head.
func (self *BranchCommands) PreviousRef() (string, error) {
cmdArgs := NewGitCmd("rev-parse").
Arg("--symbolic-full-name").
Arg("@{-1}").
ToArgv()
output, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
if err != nil {
return "", err
}
return strings.TrimSpace(output), nil
}
// LocalDelete delete branch locally
func (self *BranchCommands) LocalDelete(branches []string, force bool) error {
cmdArgs := NewGitCmd("branch").
@@ -234,26 +250,51 @@ func (self *BranchCommands) Rename(oldName string, newName string) error {
return self.cmd.New(cmdArgs).Run()
}
type MergeOpts struct {
FastForwardOnly bool
Squash bool
}
type MergeVariant int
const (
MERGE_VARIANT_REGULAR MergeVariant = iota
MERGE_VARIANT_FAST_FORWARD
MERGE_VARIANT_NON_FAST_FORWARD
MERGE_VARIANT_SQUASH
)
func (self *BranchCommands) Merge(branchName string, variant MergeVariant) error {
extraArgs := func() []string {
switch variant {
case MERGE_VARIANT_REGULAR:
return []string{}
case MERGE_VARIANT_FAST_FORWARD:
return []string{"--ff"}
case MERGE_VARIANT_NON_FAST_FORWARD:
return []string{"--no-ff"}
case MERGE_VARIANT_SQUASH:
return []string{"--squash", "--ff"}
}
panic("shouldn't get here")
}()
func (self *BranchCommands) Merge(branchName string, opts MergeOpts) error {
if opts.Squash && opts.FastForwardOnly {
panic("Squash and FastForwardOnly can't both be true")
}
cmdArgs := NewGitCmd("merge").
Arg("--no-edit").
Arg(strings.Fields(self.UserConfig().Git.Merging.Args)...).
ArgIf(opts.FastForwardOnly, "--ff-only").
ArgIf(opts.Squash, "--squash", "--ff").
Arg(extraArgs...).
Arg(branchName).
ToArgv()
return self.cmd.New(cmdArgs).Run()
}
// Returns whether refName can be fast-forward merged into the current branch
func (self *BranchCommands) CanDoFastForwardMerge(refName string) bool {
cmdArgs := NewGitCmd("merge-base").
Arg("--is-ancestor").
Arg("HEAD", refName).
ToArgv()
err := self.cmd.New(cmdArgs).DontLog().Run()
return err == nil
}
// Only choose between non-empty, non-identical commands
func (self *BranchCommands) allBranchesLogCandidates() []string {
return lo.Uniq(lo.WithoutEmpty(self.UserConfig().Git.AllBranchesLogCmds))
@@ -262,6 +303,10 @@ func (self *BranchCommands) allBranchesLogCandidates() []string {
func (self *BranchCommands) AllBranchesLogCmdObj() *oscommands.CmdObj {
candidates := self.allBranchesLogCandidates()
if self.allBranchesLogCmdIndex >= len(candidates) {
self.allBranchesLogCmdIndex = 0
}
i := self.allBranchesLogCmdIndex
return self.cmd.New(str.ToArgv(candidates[i])).DontLog()
}
@@ -269,7 +314,13 @@ func (self *BranchCommands) AllBranchesLogCmdObj() *oscommands.CmdObj {
func (self *BranchCommands) RotateAllBranchesLogIdx() {
n := len(self.allBranchesLogCandidates())
i := self.allBranchesLogCmdIndex
self.allBranchesLogCmdIndex = uint8((int(i) + 1) % n)
self.allBranchesLogCmdIndex = (i + 1) % n
}
func (self *BranchCommands) GetAllBranchesLogIdxAndCount() (int, int) {
n := len(self.allBranchesLogCandidates())
i := self.allBranchesLogCmdIndex
return i, n
}
func (self *BranchCommands) IsBranchMerged(branch *models.Branch, mainBranches *MainBranches) (bool, error) {
@@ -288,7 +339,7 @@ func (self *BranchCommands) IsBranchMerged(branch *models.Branch, mainBranches *
Arg("--").
ToArgv()
stdout, _, err := self.cmd.New(cmdArgs).RunWithOutputs()
stdout, _, err := self.cmd.New(cmdArgs).DontLog().RunWithOutputs()
if err != nil {
return false, err
}
+22 -8
View File
@@ -122,14 +122,14 @@ func TestBranchMerge(t *testing.T) {
scenarios := []struct {
testName string
userConfig *config.UserConfig
opts MergeOpts
variant MergeVariant
branchName string
expected []string
}{
{
testName: "basic",
userConfig: &config.UserConfig{},
opts: MergeOpts{},
variant: MERGE_VARIANT_REGULAR,
branchName: "mybranch",
expected: []string{"merge", "--no-edit", "mybranch"},
},
@@ -142,7 +142,7 @@ func TestBranchMerge(t *testing.T) {
},
},
},
opts: MergeOpts{},
variant: MERGE_VARIANT_REGULAR,
branchName: "mybranch",
expected: []string{"merge", "--no-edit", "--merging-args", "mybranch"},
},
@@ -155,16 +155,30 @@ func TestBranchMerge(t *testing.T) {
},
},
},
opts: MergeOpts{},
variant: MERGE_VARIANT_REGULAR,
branchName: "mybranch",
expected: []string{"merge", "--no-edit", "--arg1", "--arg2", "mybranch"},
},
{
testName: "fast forward only",
testName: "fast-forward merge",
userConfig: &config.UserConfig{},
opts: MergeOpts{FastForwardOnly: true},
variant: MERGE_VARIANT_FAST_FORWARD,
branchName: "mybranch",
expected: []string{"merge", "--no-edit", "--ff-only", "mybranch"},
expected: []string{"merge", "--no-edit", "--ff", "mybranch"},
},
{
testName: "non-fast-forward merge",
userConfig: &config.UserConfig{},
variant: MERGE_VARIANT_NON_FAST_FORWARD,
branchName: "mybranch",
expected: []string{"merge", "--no-edit", "--no-ff", "mybranch"},
},
{
testName: "squash merge",
userConfig: &config.UserConfig{},
variant: MERGE_VARIANT_SQUASH,
branchName: "mybranch",
expected: []string{"merge", "--no-edit", "--squash", "--ff", "mybranch"},
},
}
@@ -174,7 +188,7 @@ func TestBranchMerge(t *testing.T) {
ExpectGitArgs(s.expected, "", nil)
instance := buildBranchCommands(commonDeps{runner: runner, userConfig: s.userConfig})
assert.NoError(t, instance.Merge(s.branchName, s.opts))
assert.NoError(t, instance.Merge(s.branchName, s.variant))
runner.CheckForMissingCalls()
})
}
+3 -3
View File
@@ -256,14 +256,14 @@ func (self *CommitCommands) AmendHeadCmdObj() *oscommands.CmdObj {
func (self *CommitCommands) ShowCmdObj(hash string, filterPaths []string) *oscommands.CmdObj {
contextSize := self.UserConfig().Git.DiffContextSize
extDiffCmd := self.UserConfig().Git.Paging.ExternalDiffCommand
useExtDiffGitConfig := self.UserConfig().Git.Paging.UseExternalDiffGitConfig
extDiffCmd := self.pagerConfig.GetExternalDiffCommand()
useExtDiffGitConfig := self.pagerConfig.GetUseExternalDiffGitConfig()
cmdArgs := NewGitCmd("show").
Config("diff.noprefix=false").
ConfigIf(extDiffCmd != "", "diff.external="+extDiffCmd).
ArgIfElse(extDiffCmd != "" || useExtDiffGitConfig, "--ext-diff", "--no-ext-diff").
Arg("--submodule").
Arg("--color="+self.UserConfig().Git.Paging.ColorArg).
Arg("--color="+self.pagerConfig.GetColorArg()).
Arg(fmt.Sprintf("--unified=%d", contextSize)).
Arg("--stat").
Arg("--decorate").
+11 -11
View File
@@ -255,8 +255,7 @@ func TestCommitShowCmdObj(t *testing.T) {
contextSize uint64
similarityThreshold int
ignoreWhitespace bool
extDiffCmd string
useExtDiffGitConfig bool
pagerConfig *config.PagingConfig
expected []string
}
@@ -267,7 +266,7 @@ func TestCommitShowCmdObj(t *testing.T) {
contextSize: 3,
similarityThreshold: 50,
ignoreWhitespace: false,
extDiffCmd: "",
pagerConfig: nil,
expected: []string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "show", "--no-ext-diff", "--submodule", "--color=always", "--unified=3", "--stat", "--decorate", "-p", "1234567890", "--find-renames=50%", "--"},
},
{
@@ -276,7 +275,7 @@ func TestCommitShowCmdObj(t *testing.T) {
contextSize: 3,
similarityThreshold: 50,
ignoreWhitespace: false,
extDiffCmd: "",
pagerConfig: nil,
expected: []string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "show", "--no-ext-diff", "--submodule", "--color=always", "--unified=3", "--stat", "--decorate", "-p", "1234567890", "--find-renames=50%", "--", "file.txt"},
},
{
@@ -285,7 +284,7 @@ func TestCommitShowCmdObj(t *testing.T) {
contextSize: 77,
similarityThreshold: 50,
ignoreWhitespace: false,
extDiffCmd: "",
pagerConfig: nil,
expected: []string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "show", "--no-ext-diff", "--submodule", "--color=always", "--unified=77", "--stat", "--decorate", "-p", "1234567890", "--find-renames=50%", "--"},
},
{
@@ -294,7 +293,7 @@ func TestCommitShowCmdObj(t *testing.T) {
contextSize: 3,
similarityThreshold: 33,
ignoreWhitespace: false,
extDiffCmd: "",
pagerConfig: nil,
expected: []string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "show", "--no-ext-diff", "--submodule", "--color=always", "--unified=3", "--stat", "--decorate", "-p", "1234567890", "--find-renames=33%", "--"},
},
{
@@ -303,7 +302,7 @@ func TestCommitShowCmdObj(t *testing.T) {
contextSize: 77,
similarityThreshold: 50,
ignoreWhitespace: true,
extDiffCmd: "",
pagerConfig: nil,
expected: []string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "show", "--no-ext-diff", "--submodule", "--color=always", "--unified=77", "--stat", "--decorate", "-p", "1234567890", "--ignore-all-space", "--find-renames=50%", "--"},
},
{
@@ -312,7 +311,7 @@ func TestCommitShowCmdObj(t *testing.T) {
contextSize: 3,
similarityThreshold: 50,
ignoreWhitespace: false,
extDiffCmd: "difft --color=always",
pagerConfig: &config.PagingConfig{ExternalDiffCommand: "difft --color=always"},
expected: []string{"-C", "/path/to/worktree", "-c", "diff.external=difft --color=always", "-c", "diff.noprefix=false", "show", "--ext-diff", "--submodule", "--color=always", "--unified=3", "--stat", "--decorate", "-p", "1234567890", "--find-renames=50%", "--"},
},
{
@@ -321,7 +320,7 @@ func TestCommitShowCmdObj(t *testing.T) {
contextSize: 3,
similarityThreshold: 50,
ignoreWhitespace: false,
useExtDiffGitConfig: true,
pagerConfig: &config.PagingConfig{UseExternalDiffGitConfig: true},
expected: []string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "show", "--ext-diff", "--submodule", "--color=always", "--unified=3", "--stat", "--decorate", "-p", "1234567890", "--find-renames=50%", "--"},
},
}
@@ -329,11 +328,12 @@ func TestCommitShowCmdObj(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
userConfig := config.GetDefaultConfig()
userConfig.Git.Paging.ExternalDiffCommand = s.extDiffCmd
if s.pagerConfig != nil {
userConfig.Git.Pagers = []config.PagingConfig{*s.pagerConfig}
}
userConfig.Git.IgnoreWhitespaceInDiffView = s.ignoreWhitespace
userConfig.Git.DiffContextSize = s.contextSize
userConfig.Git.RenameSimilarityThreshold = s.similarityThreshold
userConfig.Git.Paging.UseExternalDiffGitConfig = s.useExtDiffGitConfig
runner := oscommands.NewFakeRunner(t).ExpectGitArgs(s.expected, "", nil)
repoPaths := RepoPaths{
+17 -13
View File
@@ -4,16 +4,18 @@ import (
gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/config"
)
type GitCommon struct {
*common.Common
version *GitVersion
cmd oscommands.ICmdObjBuilder
os *oscommands.OSCommand
repoPaths *RepoPaths
repo *gogit.Repository
config *ConfigCommands
version *GitVersion
cmd oscommands.ICmdObjBuilder
os *oscommands.OSCommand
repoPaths *RepoPaths
repo *gogit.Repository
config *ConfigCommands
pagerConfig *config.PagerConfig
}
func NewGitCommon(
@@ -24,14 +26,16 @@ func NewGitCommon(
repoPaths *RepoPaths,
repo *gogit.Repository,
config *ConfigCommands,
pagerConfig *config.PagerConfig,
) *GitCommon {
return &GitCommon{
Common: cmn,
version: version,
cmd: cmd,
os: osCommand,
repoPaths: repoPaths,
repo: repo,
config: config,
Common: cmn,
version: version,
cmd: cmd,
os: osCommand,
repoPaths: repoPaths,
repo: repo,
config: config,
pagerConfig: pagerConfig,
}
}
+4 -25
View File
@@ -1,15 +1,10 @@
package git_commands
import (
"os"
"strconv"
"strings"
gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/go-git/v5/config"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type ConfigCommands struct {
@@ -31,26 +26,6 @@ func NewConfigCommands(
}
}
func (self *ConfigCommands) ConfiguredPager() string {
if os.Getenv("GIT_PAGER") != "" {
return os.Getenv("GIT_PAGER")
}
if os.Getenv("PAGER") != "" {
return os.Getenv("PAGER")
}
output := self.gitConfig.Get("core.pager")
return strings.Split(output, "\n")[0]
}
func (self *ConfigCommands) GetPager(width int) string {
templateValues := map[string]string{
"columnWidth": strconv.Itoa(width/2 - 6),
}
pagerTemplate := string(self.UserConfig().Git.Paging.Pager)
return utils.ResolvePlaceholderString(pagerTemplate, templateValues)
}
type GpgConfigKey string
const (
@@ -122,6 +97,10 @@ func (self *ConfigCommands) GetRebaseUpdateRefs() bool {
return self.gitConfig.GetBool("rebase.updateRefs")
}
func (self *ConfigCommands) GetMergeFF() string {
return self.gitConfig.Get("merge.ff")
}
func (self *ConfigCommands) DropConfigCache() {
self.gitConfig.DropCache()
}
+4
View File
@@ -61,6 +61,10 @@ func buildGitCommon(deps commonDeps) *GitCommon {
gitCommon.Common.SetUserConfig(config.GetDefaultConfig())
}
gitCommon.pagerConfig = config.NewPagerConfig(func() *config.UserConfig {
return gitCommon.Common.UserConfig()
})
gitCommon.version = deps.gitVersion
if gitCommon.version == nil {
gitCommon.version = &GitVersion{2, 0, 0, ""}
+3 -3
View File
@@ -19,9 +19,9 @@ func NewDiffCommands(gitCommon *GitCommon) *DiffCommands {
// This is for generating diffs to be shown in the UI (e.g. rendering a range
// diff to the main view). It uses a custom pager if one is configured.
func (self *DiffCommands) DiffCmdObj(diffArgs []string) *oscommands.CmdObj {
extDiffCmd := self.UserConfig().Git.Paging.ExternalDiffCommand
extDiffCmd := self.pagerConfig.GetExternalDiffCommand()
useExtDiff := extDiffCmd != ""
useExtDiffGitConfig := self.UserConfig().Git.Paging.UseExternalDiffGitConfig
useExtDiffGitConfig := self.pagerConfig.GetUseExternalDiffGitConfig()
ignoreWhitespace := self.UserConfig().Git.IgnoreWhitespaceInDiffView
return self.cmd.New(
@@ -30,7 +30,7 @@ func (self *DiffCommands) DiffCmdObj(diffArgs []string) *oscommands.CmdObj {
ConfigIf(useExtDiff, "diff.external="+extDiffCmd).
ArgIfElse(useExtDiff || useExtDiffGitConfig, "--ext-diff", "--no-ext-diff").
Arg("--submodule").
Arg(fmt.Sprintf("--color=%s", self.UserConfig().Git.Paging.ColorArg)).
Arg(fmt.Sprintf("--color=%s", self.pagerConfig.GetColorArg())).
ArgIf(ignoreWhitespace, "--ignore-all-space").
Arg(fmt.Sprintf("--unified=%d", self.UserConfig().Git.DiffContextSize)).
Arg(diffArgs...).
+2 -2
View File
@@ -201,8 +201,8 @@ func (self *FileLoader) gitStatus(opts GitStatusOptions) ([]FileStatus, error) {
PreviousPath: "",
}
if strings.HasPrefix(status.Change, "R") {
// if a line starts with 'R' then the next line is the original file.
if strings.HasPrefix(status.Change, "R") || strings.HasPrefix(status.Change, "C") {
// if a line starts with 'R' (rename) or 'C' (copy) then the next line is the original file.
status.PreviousPath = splitLines[i+1]
status.StatusString = fmt.Sprintf("%s %s -> %s", status.Change, status.PreviousPath, status.Path)
i++
@@ -192,6 +192,43 @@ func TestFileGetStatusFiles(t *testing.T) {
},
},
},
{
testName: "Copied files",
similarityThreshold: 50,
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z", "--find-renames=50%"},
"C copy1.txt\x00original.txt\x00CM copy2.txt\x00original.txt",
nil,
),
expectedFiles: []*models.File{
{
Path: "copy1.txt",
PreviousPath: "original.txt",
HasStagedChanges: true,
HasUnstagedChanges: false,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "C original.txt -> copy1.txt",
ShortStatus: "C ",
},
{
Path: "copy2.txt",
PreviousPath: "original.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "CM original.txt -> copy2.txt",
ShortStatus: "CM",
},
},
},
}
for _, s := range scenarios {
+17 -8
View File
@@ -244,8 +244,9 @@ func (self *RebaseCommands) PrepareInteractiveRebaseCommand(opts PrepareInteract
cmdObj.AddEnvVars(
"DEBUG="+debug,
"LANG=en_US.UTF-8", // Force using EN as language
"LC_ALL=en_US.UTF-8", // Force using EN as language
"LANG=C", // Force using English language
"LC_ALL=C", // Force using English language
"LC_MESSAGES=C", // Force using English language
"GIT_SEQUENCE_EDITOR="+gitSequenceEditor,
)
@@ -277,8 +278,9 @@ func (self *RebaseCommands) GitRebaseEditTodo(todosFileContent []byte) error {
cmdObj.AddEnvVars(
"DEBUG="+debug,
"LANG=en_US.UTF-8", // Force using EN as language
"LC_ALL=en_US.UTF-8", // Force using EN as language
"LANG=C", // Force using English language
"LC_ALL=C", // Force using English language
"LC_MESSAGES=C", // Force using English language
"GIT_EDITOR="+ex,
"GIT_SEQUENCE_EDITOR="+ex,
)
@@ -519,10 +521,17 @@ func (self *RebaseCommands) DiscardOldFileChanges(commits []*models.Commit, comm
}
for _, filePath := range filePaths {
// check if file exists in previous commit (this command returns an error if the file doesn't exist)
cmdArgs := NewGitCmd("cat-file").Arg("-e", "HEAD^:"+filePath).ToArgv()
if err := self.cmd.New(cmdArgs).Run(); err != nil {
doesFileExistInPreviousCommit := false
if commitIndex < len(commits)-1 {
// check if file exists in previous commit (this command returns an empty string if the file doesn't exist)
cmdArgs := NewGitCmd("ls-tree").Arg("--name-only", "HEAD^", "--", filePath).ToArgv()
output, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
if err != nil {
return err
}
doesFileExistInPreviousCommit = strings.TrimRight(output, "\n") == filePath
}
if !doesFileExistInPreviousCommit {
if err := self.os.Remove(filePath); err != nil {
return err
}
+1 -1
View File
@@ -139,7 +139,7 @@ func TestRebaseDiscardOldFileChanges(t *testing.T) {
fileName: []string{"test999.txt"},
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"rebase", "--interactive", "--autostash", "--keep-empty", "--no-autosquash", "--rebase-merges", "abcdef"}, "", nil).
ExpectGitArgs([]string{"cat-file", "-e", "HEAD^:test999.txt"}, "", nil).
ExpectGitArgs([]string{"ls-tree", "--name-only", "HEAD^", "--", "test999.txt"}, "test999.txt\n", nil).
ExpectGitArgs([]string{"checkout", "HEAD^", "--", "test999.txt"}, "", nil).
ExpectGitArgs([]string{"commit", "--amend", "--no-edit", "--allow-empty", "--allow-empty-message"}, "", nil).
ExpectGitArgs([]string{"rebase", "--continue"}, "", nil),
+3 -3
View File
@@ -81,8 +81,8 @@ func (self *StashCommands) Hash(index int) (string, error) {
}
func (self *StashCommands) ShowStashEntryCmdObj(index int) *oscommands.CmdObj {
extDiffCmd := self.UserConfig().Git.Paging.ExternalDiffCommand
useExtDiffGitConfig := self.UserConfig().Git.Paging.UseExternalDiffGitConfig
extDiffCmd := self.pagerConfig.GetExternalDiffCommand()
useExtDiffGitConfig := self.pagerConfig.GetUseExternalDiffGitConfig()
// "-u" is the same as "--include-untracked", but the latter fails in older git versions for some reason
cmdArgs := NewGitCmd("stash").Arg("show").
@@ -91,7 +91,7 @@ func (self *StashCommands) ShowStashEntryCmdObj(index int) *oscommands.CmdObj {
Arg("-u").
ConfigIf(extDiffCmd != "", "diff.external="+extDiffCmd).
ArgIfElse(extDiffCmd != "" || useExtDiffGitConfig, "--ext-diff", "--no-ext-diff").
Arg(fmt.Sprintf("--color=%s", self.UserConfig().Git.Paging.ColorArg)).
Arg(fmt.Sprintf("--color=%s", self.pagerConfig.GetColorArg())).
Arg(fmt.Sprintf("--unified=%d", self.UserConfig().Git.DiffContextSize)).
ArgIf(self.UserConfig().Git.IgnoreWhitespaceInDiffView, "--ignore-all-space").
Arg(fmt.Sprintf("--find-renames=%d%%", self.UserConfig().Git.RenameSimilarityThreshold)).
+6 -6
View File
@@ -103,8 +103,7 @@ func TestStashStashEntryCmdObj(t *testing.T) {
contextSize uint64
similarityThreshold int
ignoreWhitespace bool
extDiffCmd string
useExtDiffGitConfig bool
pagerConfig *config.PagingConfig
expected []string
}
@@ -139,7 +138,7 @@ func TestStashStashEntryCmdObj(t *testing.T) {
contextSize: 3,
similarityThreshold: 50,
ignoreWhitespace: false,
extDiffCmd: "difft --color=always",
pagerConfig: &config.PagingConfig{ExternalDiffCommand: "difft --color=always"},
expected: []string{"git", "-C", "/path/to/worktree", "-c", "diff.external=difft --color=always", "stash", "show", "-p", "--stat", "-u", "--ext-diff", "--color=always", "--unified=3", "--find-renames=50%", "refs/stash@{5}"},
},
{
@@ -148,7 +147,7 @@ func TestStashStashEntryCmdObj(t *testing.T) {
contextSize: 3,
similarityThreshold: 50,
ignoreWhitespace: false,
useExtDiffGitConfig: true,
pagerConfig: &config.PagingConfig{UseExternalDiffGitConfig: true},
expected: []string{"git", "-C", "/path/to/worktree", "stash", "show", "-p", "--stat", "-u", "--ext-diff", "--color=always", "--unified=3", "--find-renames=50%", "refs/stash@{5}"},
},
{
@@ -167,8 +166,9 @@ func TestStashStashEntryCmdObj(t *testing.T) {
userConfig.Git.IgnoreWhitespaceInDiffView = s.ignoreWhitespace
userConfig.Git.DiffContextSize = s.contextSize
userConfig.Git.RenameSimilarityThreshold = s.similarityThreshold
userConfig.Git.Paging.ExternalDiffCommand = s.extDiffCmd
userConfig.Git.Paging.UseExternalDiffGitConfig = s.useExtDiffGitConfig
if s.pagerConfig != nil {
userConfig.Git.Pagers = []config.PagingConfig{*s.pagerConfig}
}
repoPaths := RepoPaths{
worktreePath: "/path/to/worktree",
}
+50 -6
View File
@@ -5,6 +5,7 @@ import (
"os"
"path/filepath"
"regexp"
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/models"
@@ -257,7 +258,7 @@ func (self *WorkingTreeCommands) WorktreeFileDiff(file *models.File, plain bool,
}
func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain bool, cached bool) *oscommands.CmdObj {
colorArg := self.UserConfig().Git.Paging.ColorArg
colorArg := self.pagerConfig.GetColorArg()
if plain {
colorArg = "never"
}
@@ -265,9 +266,9 @@ func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain
contextSize := self.UserConfig().Git.DiffContextSize
prevPath := node.GetPreviousPath()
noIndex := !node.GetIsTracked() && !node.GetHasStagedChanges() && !cached && node.GetIsFile()
extDiffCmd := self.UserConfig().Git.Paging.ExternalDiffCommand
extDiffCmd := self.pagerConfig.GetExternalDiffCommand()
useExtDiff := extDiffCmd != "" && !plain
useExtDiffGitConfig := self.UserConfig().Git.Paging.UseExternalDiffGitConfig
useExtDiffGitConfig := self.pagerConfig.GetUseExternalDiffGitConfig() && !plain
cmdArgs := NewGitCmd("diff").
ConfigIf(useExtDiff, "diff.external="+extDiffCmd).
@@ -298,14 +299,14 @@ func (self *WorkingTreeCommands) ShowFileDiff(from string, to string, reverse bo
func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reverse bool, fileName string, plain bool) *oscommands.CmdObj {
contextSize := self.UserConfig().Git.DiffContextSize
colorArg := self.UserConfig().Git.Paging.ColorArg
colorArg := self.pagerConfig.GetColorArg()
if plain {
colorArg = "never"
}
extDiffCmd := self.UserConfig().Git.Paging.ExternalDiffCommand
extDiffCmd := self.pagerConfig.GetExternalDiffCommand()
useExtDiff := extDiffCmd != "" && !plain
useExtDiffGitConfig := self.UserConfig().Git.Paging.UseExternalDiffGitConfig
useExtDiffGitConfig := self.pagerConfig.GetUseExternalDiffGitConfig() && !plain
cmdArgs := NewGitCmd("diff").
Config("diff.noprefix=false").
@@ -407,3 +408,46 @@ func (self *WorkingTreeCommands) ResetMixed(ref string) error {
return self.cmd.New(cmdArgs).Run()
}
func (self *WorkingTreeCommands) ShowFileAtStage(path string, stage int) (string, error) {
cmdArgs := NewGitCmd("show").
Arg(fmt.Sprintf(":%d:%s", stage, path)).
ToArgv()
return self.cmd.New(cmdArgs).RunWithOutput()
}
func (self *WorkingTreeCommands) ObjectIDAtStage(path string, stage int) (string, error) {
cmdArgs := NewGitCmd("rev-parse").
Arg(fmt.Sprintf(":%d:%s", stage, path)).
ToArgv()
output, err := self.cmd.New(cmdArgs).RunWithOutput()
if err != nil {
return "", err
}
return strings.TrimSpace(output), nil
}
func (self *WorkingTreeCommands) MergeFileForFiles(strategy string, oursFilepath string, baseFilepath string, theirsFilepath string) (string, error) {
cmdArgs := NewGitCmd("merge-file").
Arg(strategy).
Arg("--stdout").
Arg(oursFilepath, baseFilepath, theirsFilepath).
ToArgv()
return self.cmd.New(cmdArgs).RunWithOutput()
}
// OIDs mode (Git 2.43+)
func (self *WorkingTreeCommands) MergeFileForObjectIDs(strategy string, oursID string, baseID string, theirsID string) (string, error) {
cmdArgs := NewGitCmd("merge-file").
Arg(strategy).
Arg("--stdout").
Arg("--object-id").
Arg(oursID, baseID, theirsID).
ToArgv()
return self.cmd.New(cmdArgs).RunWithOutput()
}
+1 -1
View File
@@ -330,7 +330,7 @@ func (self *cmdObjRunner) runAndDetectCredentialRequest(
promptUserForCredential func(CredentialType) <-chan string,
) error {
// setting the output to english so we can parse it for a username/password request
cmdObj.AddEnvVars("LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8")
cmdObj.AddEnvVars("LANG=C", "LC_ALL=C", "LC_MESSAGES=C")
return self.runAndStreamAux(cmdObj, func(handler *cmdHandler, cmdWriter io.Writer) {
tr := io.TeeReader(handler.stdoutPipe, cmdWriter)
+16 -16
View File
@@ -35,16 +35,16 @@ import (
// destination file exists, all it's contents will be replaced by the contents
// of the source file. The file mode will be copied from the source and
// the copied data is synced/flushed to stable storage.
func CopyFile(src, dst string) (err error) {
func CopyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return //nolint: nakedret
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return //nolint: nakedret
return err
}
defer func() {
if e := out.Close(); e != nil {
@@ -54,30 +54,30 @@ func CopyFile(src, dst string) (err error) {
_, err = io.Copy(out, in)
if err != nil {
return //nolint: nakedret
return err
}
err = out.Sync()
if err != nil {
return //nolint: nakedret
return err
}
si, err := os.Stat(src)
if err != nil {
return //nolint: nakedret
return err
}
err = os.Chmod(dst, si.Mode())
if err != nil {
return //nolint: nakedret
return err
}
return //nolint: nakedret
return err
}
// CopyDir recursively copies a directory tree, attempting to preserve permissions.
// Source directory must exist. If destination already exists we'll clobber it.
// Symlinks are ignored and skipped.
func CopyDir(src string, dst string) (err error) {
func CopyDir(src string, dst string) error {
src = filepath.Clean(src)
dst = filepath.Clean(dst)
@@ -91,7 +91,7 @@ func CopyDir(src string, dst string) (err error) {
_, err = os.Stat(dst)
if err != nil && !os.IsNotExist(err) {
return //nolint: nakedret
return err
}
if err == nil {
// it exists so let's remove it
@@ -102,12 +102,12 @@ func CopyDir(src string, dst string) (err error) {
err = os.MkdirAll(dst, si.Mode())
if err != nil {
return //nolint: nakedret
return err
}
entries, err := os.ReadDir(src)
if err != nil {
return //nolint: nakedret
return err
}
for _, entry := range entries {
@@ -117,13 +117,13 @@ func CopyDir(src string, dst string) (err error) {
if entry.IsDir() {
err = CopyDir(srcPath, dstPath)
if err != nil {
return //nolint: nakedret
return err
}
} else {
var info os.FileInfo
info, err = entry.Info()
if err != nil {
return //nolint: nakedret
return err
}
// Skip symlinks.
@@ -133,10 +133,10 @@ func CopyDir(src string, dst string) (err error) {
err = CopyFile(srcPath, dstPath)
if err != nil {
return //nolint: nakedret
return err
}
}
}
return //nolint: nakedret
return err
}
+37
View File
@@ -272,6 +272,7 @@ func computeMigratedConfig(path string, content []byte, changes *ChangesSet) ([]
{[]string{"gui", "skipUnstageLineWarning"}, "skipDiscardChangeWarning"},
{[]string{"keybinding", "universal", "executeCustomCommand"}, "executeShellCommand"},
{[]string{"gui", "windowSize"}, "screenMode"},
{[]string{"keybinding", "files", "openMergeTool"}, "openMergeOptions"},
}
for _, pathToReplace := range pathsToReplace {
@@ -309,6 +310,11 @@ func computeMigratedConfig(path string, content []byte, changes *ChangesSet) ([]
return nil, false, fmt.Errorf("Couldn't migrate config file at `%s`: %w", path, err)
}
err = migratePagers(&rootNode, changes)
if err != nil {
return nil, false, fmt.Errorf("Couldn't migrate config file at `%s`: %w", path, err)
}
// Add more migrations here...
if reflect.DeepEqual(rootNode, originalCopy) {
@@ -468,6 +474,37 @@ func migrateAllBranchesLogCmd(rootNode *yaml.Node, changes *ChangesSet) error {
})
}
func migratePagers(rootNode *yaml.Node, changes *ChangesSet) error {
return yaml_utils.TransformNode(rootNode, []string{"git"}, func(gitNode *yaml.Node) error {
pagingKeyNode, pagingValueNode := yaml_utils.LookupKey(gitNode, "paging")
if pagingKeyNode == nil || pagingValueNode.Kind != yaml.MappingNode {
// If there's no "paging" section (or it's not an object), there's nothing to do
return nil
}
pagersKeyNode, _ := yaml_utils.LookupKey(gitNode, "pagers")
if pagersKeyNode != nil {
// Conversely, if there *is* already a "pagers" array, we also have nothing to do.
// This covers the case where the user keeps both the "paging" section and the "pagers"
// array for the sake of easier testing of old versions.
return nil
}
pagingKeyNode.Value = "pagers"
pagingContentCopy := pagingValueNode.Content
pagingValueNode.Kind = yaml.SequenceNode
pagingValueNode.Tag = "!!seq"
pagingValueNode.Content = []*yaml.Node{{
Kind: yaml.MappingNode,
Content: pagingContentCopy,
}}
changes.Add("Moved git.paging object to git.pagers array")
return nil
})
}
func (c *AppConfig) GetDebug() bool {
return c.debug
}
+96 -1
View File
@@ -897,7 +897,7 @@ keybinding:
toggleStagedAll: a
viewResetOptions: D
fetch: f
openMergeTool: M
openMergeOptions: M
openStatusFilter: <c-b>
copyFileInfoToClipboard: "y"
collapseAll: '-'
@@ -1089,3 +1089,98 @@ func TestAllBranchesLogCmdMigrations(t *testing.T) {
})
}
}
func TestPagerMigration(t *testing.T) {
scenarios := []struct {
name string
input string
expected string
expectedDidChange bool
expectedChanges []string
}{
{
name: "Incomplete Configuration Passes uneventfully",
input: "git:",
expectedDidChange: false,
expectedChanges: []string{},
},
{
name: "No paging section",
input: `git:
autoFetch: true
`,
expected: `git:
autoFetch: true
`,
expectedDidChange: false,
expectedChanges: []string{},
},
{
name: "Both paging and pagers exist",
input: `git:
paging:
pager: delta --dark --paging=never
pagers:
- diff: diff-so-fancy
`,
expected: `git:
paging:
pager: delta --dark --paging=never
pagers:
- diff: diff-so-fancy
`,
expectedDidChange: false,
expectedChanges: []string{},
},
{
name: "paging is not an object",
input: `git:
paging: 5
`,
expected: `git:
paging: 5
`,
expectedDidChange: false,
expectedChanges: []string{},
},
{
name: "paging is moved to pagers array (keeping the order)",
input: `git:
paging:
pager: delta --dark --paging=never
autoFetch: true
`,
expected: `git:
pagers:
- pager: delta --dark --paging=never
autoFetch: true
`,
expectedDidChange: true,
expectedChanges: []string{"Moved git.paging object to git.pagers array"},
},
{
name: "paging is moved to pagers array even if empty",
input: `git:
paging: {}
`,
expected: `git:
pagers: [{}]
`,
expectedDidChange: true,
expectedChanges: []string{"Moved git.paging object to git.pagers array"},
},
}
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())
})
}
}
+82
View File
@@ -0,0 +1,82 @@
package config
import (
"strconv"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type PagerConfig struct {
getUserConfig func() *UserConfig
pagerIndex int
}
func NewPagerConfig(getUserConfig func() *UserConfig) *PagerConfig {
return &PagerConfig{getUserConfig: getUserConfig}
}
func (self *PagerConfig) currentPagerConfig() *PagingConfig {
pagers := self.getUserConfig().Git.Pagers
if len(pagers) == 0 {
return nil
}
// Guard against the pager index being out of range, which can happen if the user
// has removed pagers from their config file while lazygit is running.
if self.pagerIndex >= len(pagers) {
self.pagerIndex = 0
}
return &pagers[self.pagerIndex]
}
func (self *PagerConfig) GetPagerCommand(width int) string {
currentPagerConfig := self.currentPagerConfig()
if currentPagerConfig == nil {
return ""
}
templateValues := map[string]string{
"columnWidth": strconv.Itoa(width/2 - 6),
}
pagerTemplate := string(currentPagerConfig.Pager)
return utils.ResolvePlaceholderString(pagerTemplate, templateValues)
}
func (self *PagerConfig) GetColorArg() string {
currentPagerConfig := self.currentPagerConfig()
if currentPagerConfig == nil {
return "always"
}
colorArg := currentPagerConfig.ColorArg
if colorArg == "" {
return "always"
}
return colorArg
}
func (self *PagerConfig) GetExternalDiffCommand() string {
currentPagerConfig := self.currentPagerConfig()
if currentPagerConfig == nil {
return ""
}
return currentPagerConfig.ExternalDiffCommand
}
func (self *PagerConfig) GetUseExternalDiffGitConfig() bool {
currentPagerConfig := self.currentPagerConfig()
if currentPagerConfig == nil {
return false
}
return currentPagerConfig.UseExternalDiffGitConfig
}
func (self *PagerConfig) CyclePagers() {
self.pagerIndex = (self.pagerIndex + 1) % len(self.getUserConfig().Git.Pagers)
}
func (self *PagerConfig) CurrentPagerIndex() (int, int) {
return self.pagerIndex, len(self.getUserConfig().Git.Pagers)
}
+68 -57
View File
@@ -84,13 +84,14 @@ type GuiConfig struct {
SkipNoStagedFilesWarning bool `yaml:"skipNoStagedFilesWarning"`
// If true, do not show a warning when rewording a commit via an external editor
SkipRewordInEditorWarning bool `yaml:"skipRewordInEditorWarning"`
// If true, switch to a different worktree without confirmation when checking out a branch that is checked out in that worktree
SkipSwitchWorktreeOnCheckoutWarning bool `yaml:"skipSwitchWorktreeOnCheckoutWarning"`
// Fraction of the total screen width to use for the left side section. You may want to pick a small number (e.g. 0.2) if you're using a narrow screen, so that you can see more of the main section.
// Number from 0 to 1.0.
SidePanelWidth float64 `yaml:"sidePanelWidth" jsonschema:"maximum=1,minimum=0"`
// If true, increase the height of the focused side window; creating an accordion effect.
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.
// 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"`
// 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:
@@ -103,13 +104,11 @@ type GuiConfig struct {
// - 'left': split the window horizontally (side panel on the left, main view on the right)
// - 'top': split the window vertically (side panel on top, main view below)
EnlargedSideViewLocation string `yaml:"enlargedSideViewLocation"`
// If true, wrap lines in the staging view to the width of the view. This
// makes it much easier to work with diffs that have long lines, e.g.
// paragraphs of markdown text.
// If true, wrap lines in the staging view to the width of the view. This makes it much easier to work with diffs that have long lines, e.g. paragraphs of markdown text.
WrapLinesInStagingView bool `yaml:"wrapLinesInStagingView"`
// If true, hunk selection mode will be enabled by default when entering the staging view.
UseHunkModeInStagingView bool `yaml:"useHunkModeInStagingView"`
// One of 'auto' (default) | 'en' | 'zh-CN' | 'zh-TW' | 'pl' | 'nl' | 'ja' | 'ko' | 'ru'
// One of 'auto' (default) | 'en' | 'zh-CN' | 'zh-TW' | 'pl' | 'nl' | 'ja' | 'ko' | 'ru' | 'pt'
Language string `yaml:"language" jsonschema:"enum=auto,enum=en,enum=zh-TW,enum=zh-CN,enum=pl,enum=nl,enum=ja,enum=ko,enum=ru"`
// Format used when displaying time e.g. commit time.
// Uses Go's time format syntax: https://pkg.go.dev/time#Time.Format
@@ -236,8 +235,30 @@ type SpinnerConfig struct {
}
type GitConfig struct {
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Custom_Pagers.md
Paging PagingConfig `yaml:"paging"`
// Array of pagers. Each entry has the following format:
// [dev] The following documentation is duplicated from the PagingConfig struct below.
//
// # 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"
//
// # e.g.
// # diff-so-fancy
// # delta --dark --paging=never
// # ydiff -p cat -s --wrap --width={{columnWidth}}
// pager: ""
//
// # e.g. 'difft --color=always'
// externalDiffCommand: ""
//
// # If true, Lazygit will use git's `diff.external` config for paging.
// # The advantage over `externalDiffCommand` is that this can be
// # configured per file type in .gitattributes; see
// # https://git-scm.com/docs/gitattributes#_defining_an_external_diff_driver.
// useExternalDiffGitConfig: false
//
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Custom_Pagers.md for more information.
Pagers []PagingConfig `yaml:"pagers"`
// Config relating to committing
Commit CommitConfig `yaml:"commit"`
// Config relating to merging
@@ -255,10 +276,7 @@ type GitConfig struct {
AutoForwardBranches string `yaml:"autoForwardBranches" jsonschema:"enum=none,enum=onlyMainBranches,enum=allBranches"`
// If true, pass the --all arg to git fetch
FetchAll bool `yaml:"fetchAll"`
// If true, lazygit will automatically stage files that used to have merge
// conflicts but no longer do; and it will also ask you if you want to
// continue a merge or rebase if you've resolved all conflicts. If false, it
// won't do either of these things.
// If true, lazygit will automatically stage files that used to have merge conflicts but no longer do; and it will also ask you if you want to continue a merge or rebase if you've resolved all conflicts. If false, it won't do either of these things.
AutoStageResolvedConflicts bool `yaml:"autoStageResolvedConflicts"`
// Command used when displaying the current branch git log in the main window
BranchLogCmd string `yaml:"branchLogCmd"`
@@ -293,8 +311,7 @@ type GitConfig struct {
// One of: 'date' (default) | 'alphabetical'
// Can be changed from within Lazygit with the Sort Order menu (`s`) in the remote branches panel.
RemoteBranchSortOrder string `yaml:"remoteBranchSortOrder" jsonschema:"enum=date,enum=alphabetical"`
// When copying commit hashes to the clipboard, truncate them to this
// length. Set to 40 to disable truncation.
// When copying commit hashes to the clipboard, truncate them to this length. Set to 40 to disable truncation.
TruncateCopiedCommitHashesTo int `yaml:"truncateCopiedCommitHashesTo"`
}
@@ -308,6 +325,7 @@ 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 {
// 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"`
@@ -343,8 +361,7 @@ type MergingConfig struct {
type LogConfig struct {
// One of: 'date-order' | 'author-date-order' | 'topo-order' | 'default'
// 'topo-order' makes it easier to read the git log graph, but commits may not
// appear chronologically. See https://git-scm.com/docs/
// 'topo-order' makes it easier to read the git log graph, but commits may not appear chronologically. See https://git-scm.com/docs/
//
// Can be changed from within Lazygit with `Log menu -> Commit sort order` (`<c-l>` in the commits window by default).
Order string `yaml:"order" jsonschema:"enum=date-order,enum=author-date-order,enum=topo-order,enum=default"`
@@ -449,6 +466,7 @@ type KeybindingUniversalConfig struct {
PrevTab string `yaml:"prevTab"`
NextScreenMode string `yaml:"nextScreenMode"`
PrevScreenMode string `yaml:"prevScreenMode"`
CyclePagers string `yaml:"cyclePagers"`
Undo string `yaml:"undo"`
Redo string `yaml:"redo"`
FilteringMenu string `yaml:"filteringMenu"`
@@ -487,7 +505,7 @@ type KeybindingFilesConfig struct {
ViewResetOptions string `yaml:"viewResetOptions"`
Fetch string `yaml:"fetch"`
ToggleTreeView string `yaml:"toggleTreeView"`
OpenMergeTool string `yaml:"openMergeTool"`
OpenMergeOptions string `yaml:"openMergeOptions"`
OpenStatusFilter string `yaml:"openStatusFilter"`
CopyFileInfoToClipboard string `yaml:"copyFileInfoToClipboard"`
CollapseAll string `yaml:"collapseAll"`
@@ -582,12 +600,10 @@ type OSConfig struct {
// Command for editing a file. Should contain "{{filename}}".
Edit string `yaml:"edit,omitempty"`
// Command for editing a file at a given line number. Should contain
// "{{filename}}", and may optionally contain "{{line}}".
// Command for editing a file at a given line number. Should contain "{{filename}}", and may optionally contain "{{line}}".
EditAtLine string `yaml:"editAtLine,omitempty"`
// Same as EditAtLine, except that the command needs to wait until the
// window is closed.
// Same as EditAtLine, except that the command needs to wait until the window is closed.
EditAtLineAndWait string `yaml:"editAtLineAndWait,omitempty"`
// Whether lazygit suspends until an edit process returns
@@ -598,12 +614,10 @@ type OSConfig struct {
// For opening a directory in an editor
OpenDirInEditor string `yaml:"openDirInEditor,omitempty"`
// A built-in preset that sets all of the above settings. Supported presets
// are defined in the getPreset function in editor_presets.go.
// A built-in preset that sets all of the above settings. Supported presets are defined in the getPreset function in editor_presets.go.
EditPreset string `yaml:"editPreset,omitempty" jsonschema:"example=vim,example=nvim,example=emacs,example=nano,example=vscode,example=sublime,example=kakoune,example=helix,example=xcode,example=zed,example=acme"`
// Command for opening a file, as if the file is double-clicked. Should
// contain "{{filename}}", but doesn't support "{{line}}".
// Command for opening a file, as if the file is double-clicked. Should contain "{{filename}}", but doesn't support "{{line}}".
Open string `yaml:"open,omitempty"`
// Command for opening a link. Should contain "{{link}}".
@@ -760,32 +774,33 @@ func GetDefaultConfig() *UserConfig {
UnstagedChangesColor: []string{"red"},
DefaultFgColor: []string{"default"},
},
CommitLength: CommitLengthConfig{Show: true},
SkipNoStagedFilesWarning: false,
ShowListFooter: true,
ShowCommandLog: true,
ShowBottomLine: true,
ShowPanelJumps: true,
ShowFileTree: true,
ShowRootItemInFileTree: true,
ShowNumstatInFilesView: false,
ShowRandomTip: true,
ShowIcons: false,
NerdFontsVersion: "",
ShowFileIcons: true,
CommitAuthorShortLength: 2,
CommitAuthorLongLength: 17,
CommitHashLength: 8,
ShowBranchCommitHash: false,
ShowDivergenceFromBaseBranch: "none",
CommandLogSize: 8,
SplitDiff: "auto",
SkipRewordInEditorWarning: false,
ScreenMode: "normal",
Border: "rounded",
AnimateExplosion: true,
PortraitMode: "auto",
FilterMode: "substring",
CommitLength: CommitLengthConfig{Show: true},
SkipNoStagedFilesWarning: false,
ShowListFooter: true,
ShowCommandLog: true,
ShowBottomLine: true,
ShowPanelJumps: true,
ShowFileTree: true,
ShowRootItemInFileTree: true,
ShowNumstatInFilesView: false,
ShowRandomTip: true,
ShowIcons: false,
NerdFontsVersion: "",
ShowFileIcons: true,
CommitAuthorShortLength: 2,
CommitAuthorLongLength: 17,
CommitHashLength: 8,
ShowBranchCommitHash: false,
ShowDivergenceFromBaseBranch: "none",
CommandLogSize: 8,
SplitDiff: "auto",
SkipRewordInEditorWarning: false,
SkipSwitchWorktreeOnCheckoutWarning: false,
ScreenMode: "normal",
Border: "rounded",
AnimateExplosion: true,
PortraitMode: "auto",
FilterMode: "substring",
Spinner: SpinnerConfig{
Frames: []string{"|", "/", "-", "\\"},
Rate: 50,
@@ -796,11 +811,6 @@ func GetDefaultConfig() *UserConfig {
SwitchTabsWithPanelJumpKeys: false,
},
Git: GitConfig{
Paging: PagingConfig{
ColorArg: "always",
Pager: "",
ExternalDiffCommand: "",
},
Commit: CommitConfig{
SignOff: false,
AutoWrapCommitMessage: true,
@@ -916,6 +926,7 @@ func GetDefaultConfig() *UserConfig {
PrevTab: "[",
NextScreenMode: "+",
PrevScreenMode: "_",
CyclePagers: "|",
Undo: "z",
Redo: "Z",
FilteringMenu: "<c-s>",
@@ -950,7 +961,7 @@ func GetDefaultConfig() *UserConfig {
ViewResetOptions: "D",
Fetch: "f",
ToggleTreeView: "`",
OpenMergeTool: "M",
OpenMergeOptions: "M",
OpenStatusFilter: "<c-b>",
ConfirmDiscard: "x",
CopyFileInfoToClipboard: "y",
+2 -2
View File
@@ -124,8 +124,8 @@ func (gui *Gui) getRandomTip() string {
formattedKey(config.Universal.Remove),
),
fmt.Sprintf(
"If you need to pull out the big guns to resolve merge conflicts, you can press '%s' in the files panel to open 'git mergetool'",
formattedKey(config.Files.OpenMergeTool),
"If you need to pull out the big guns to resolve merge conflicts, you can press '%s' in the files panel to open merge options",
formattedKey(config.Files.OpenMergeOptions),
),
fmt.Sprintf(
"To revert a commit, press '%s' on that commit",
+1 -1
View File
@@ -196,7 +196,7 @@ func (self *ContextMgr) Activate(c types.Context, opts types.OnFocusOpts) {
v.Visible = true
self.gui.c.GocuiGui().Cursor = v.Editable
self.gui.c.GocuiGui().Cursor = v.Editable && v.Mask == 0
c.HandleFocus(opts)
}
+1 -1
View File
@@ -98,7 +98,7 @@ func (gui *Gui) resetHelpersAndControllers() {
Bisect: bisectHelper,
Suggestions: suggestionsHelper,
Files: helpers.NewFilesHelper(helperCommon),
WorkingTree: helpers.NewWorkingTreeHelper(helperCommon, refsHelper, commitsHelper, gpgHelper),
WorkingTree: helpers.NewWorkingTreeHelper(helperCommon, refsHelper, commitsHelper, gpgHelper, rebaseHelper),
Tags: helpers.NewTagsHelper(helperCommon, commitsHelper, gpgHelper),
BranchesHelper: helpers.NewBranchesHelper(helperCommon, worktreeHelper),
GPG: helpers.NewGpgHelper(helperCommon),
+2 -4
View File
@@ -420,15 +420,13 @@ func (self *BranchesController) promptToCheckoutWorktree(worktree *models.Worktr
"worktreeName": worktree.Name,
})
self.c.Confirm(types.ConfirmOpts{
return self.c.ConfirmIf(!self.c.UserConfig().Gui.SkipSwitchWorktreeOnCheckoutWarning, types.ConfirmOpts{
Title: self.c.Tr.SwitchToWorktree,
Prompt: prompt,
HandleConfirm: func() error {
return self.c.Helpers().Worktree.Switch(worktree, context.LOCAL_BRANCHES_CONTEXT_KEY)
},
})
return nil
}
func (self *BranchesController) handleCreatePullRequest(selectedBranch *models.Branch) error {
@@ -490,7 +488,7 @@ func (self *BranchesController) forceCheckout() error {
func (self *BranchesController) checkoutPreviousBranch() error {
self.c.LogAction(self.c.Tr.Actions.CheckoutBranch)
return self.c.Helpers().Refs.CheckoutRef("-", types.CheckoutRefOptions{})
return self.c.Helpers().Refs.CheckoutPreviousRef()
}
func (self *BranchesController) checkoutByName() error {
@@ -173,7 +173,7 @@ func (self *CustomPatchOptionsMenuAction) handleMovePatchIntoWorkingTree() error
self.returnFocusFromPatchExplorerIfNecessary()
mustStash := self.c.Helpers().WorkingTree.IsWorkingTreeDirty()
mustStash := self.c.Helpers().WorkingTree.IsWorkingTreeDirtyExceptSubmodules()
return self.c.ConfirmIf(mustStash, types.ConfirmOpts{
Title: self.c.Tr.MustStashTitle,
Prompt: self.c.Tr.MustStashWarning,
+76 -12
View File
@@ -178,10 +178,13 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types
Description: self.c.Tr.OpenDiffTool,
},
{
Key: opts.GetKey(opts.Config.Files.OpenMergeTool),
Handler: self.c.Helpers().WorkingTree.OpenMergeTool,
Description: self.c.Tr.OpenMergeTool,
Tooltip: self.c.Tr.OpenMergeToolTooltip,
Key: opts.GetKey(opts.Config.Files.OpenMergeOptions),
Handler: self.withItems(self.openMergeConflictMenu),
Description: self.c.Tr.ViewMergeConflictOptions,
Tooltip: self.c.Tr.ViewMergeConflictOptionsTooltip,
GetDisabledReason: self.require(self.itemsSelected(self.canOpenMergeConflictMenu)),
OpensMenu: true,
DisplayOnScreen: true,
},
{
Key: opts.GetKey(opts.Config.Files.Fetch),
@@ -573,7 +576,7 @@ func (self *FilesController) handleNonInlineConflict(file *models.File) error {
OnPress: func() error {
return handle(self.c.Git().WorkingTree.StageFile, self.c.Tr.Actions.ResolveConflictByKeepingFile)
},
Key: 'k',
Key: 'p',
}
deleteItem := &types.MenuItem{
Label: self.c.Tr.MergeConflictDeleteFile,
@@ -970,7 +973,7 @@ func (self *FilesController) createStashMenu() error {
{
Label: self.c.Tr.StashAllChanges,
OnPress: func() error {
if !self.c.Helpers().WorkingTree.IsWorkingTreeDirty() {
if !self.c.Helpers().WorkingTree.IsWorkingTreeDirtyExceptSubmodules() {
return errors.New(self.c.Tr.NoFilesToStash)
}
return self.handleStashSave(self.c.Git().Stash.Push, self.c.Tr.Actions.StashAllChanges)
@@ -980,7 +983,7 @@ func (self *FilesController) createStashMenu() error {
{
Label: self.c.Tr.StashAllChangesKeepIndex,
OnPress: func() error {
if !self.c.Helpers().WorkingTree.IsWorkingTreeDirty() {
if !self.c.Helpers().WorkingTree.IsWorkingTreeDirtyExceptSubmodules() {
return errors.New(self.c.Tr.NoFilesToStash)
}
// if there are no staged files it behaves the same as Stash.Save
@@ -999,7 +1002,7 @@ func (self *FilesController) createStashMenu() error {
Label: self.c.Tr.StashStagedChanges,
OnPress: func() error {
// there must be something in staging otherwise the current implementation mucks the stash up
if !self.c.Helpers().WorkingTree.AnyStagedFiles() {
if !self.c.Helpers().WorkingTree.AnyStagedFilesExceptSubmodules() {
return errors.New(self.c.Tr.NoTrackedStagedFilesStash)
}
return self.handleStashSave(self.c.Git().Stash.SaveStagedChanges, self.c.Tr.Actions.StashStagedChanges)
@@ -1009,10 +1012,10 @@ func (self *FilesController) createStashMenu() error {
{
Label: self.c.Tr.StashUnstagedChanges,
OnPress: func() error {
if !self.c.Helpers().WorkingTree.IsWorkingTreeDirty() {
if !self.c.Helpers().WorkingTree.IsWorkingTreeDirtyExceptSubmodules() {
return errors.New(self.c.Tr.NoFilesToStash)
}
if self.c.Helpers().WorkingTree.AnyStagedFiles() {
if self.c.Helpers().WorkingTree.AnyStagedFilesExceptSubmodules() {
return self.handleStashSave(self.c.Git().Stash.StashUnstagedChanges, self.c.Tr.Actions.StashUnstagedChanges)
}
// ordinary stash
@@ -1024,6 +1027,34 @@ func (self *FilesController) createStashMenu() error {
})
}
func (self *FilesController) openMergeConflictMenu(nodes []*filetree.FileNode) error {
normalizedNodes := flattenSelectedNodesToFiles(nodes)
fileNodesWithConflicts := lo.Filter(normalizedNodes, func(node *filetree.FileNode, _ int) bool {
return node.File != nil && node.File.HasInlineMergeConflicts
})
filepaths := lo.Map(fileNodesWithConflicts, func(node *filetree.FileNode, _ int) string {
return node.GetPath()
})
return self.c.Helpers().WorkingTree.CreateMergeConflictMenu(filepaths)
}
func (self *FilesController) canOpenMergeConflictMenu(nodes []*filetree.FileNode) *types.DisabledReason {
normalizedNodes := flattenSelectedNodesToFiles(nodes)
hasFileNodesWithConflicts := lo.SomeBy(normalizedNodes, func(node *filetree.FileNode) bool {
return node.File != nil && node.File.HasInlineMergeConflicts
})
if !hasFileNodesWithConflicts {
return &types.DisabledReason{Text: self.c.Tr.NoFilesWithMergeConflicts}
}
return nil
}
func (self *FilesController) openCopyMenu() error {
node := self.context().GetSelected()
@@ -1221,13 +1252,14 @@ func normalisedSelectedNodes(selectedNodes []*filetree.FileNode) []*filetree.Fil
}
func isDescendentOfSelectedNodes(node *filetree.FileNode, selectedNodes []*filetree.FileNode) bool {
nodePath := node.GetInternalPath()
for _, selectedNode := range selectedNodes {
if selectedNode.IsFile() {
continue
}
selectedNodePath := selectedNode.GetPath()
nodePath := node.GetPath()
selectedNodePath := selectedNode.GetInternalPath()
if strings.HasPrefix(nodePath, selectedNodePath+"/") {
return true
@@ -1236,6 +1268,38 @@ func isDescendentOfSelectedNodes(node *filetree.FileNode, selectedNodes []*filet
return false
}
// BFS algorithm for expanding directories into their children,
// and for collecting the unique file nodes
func flattenSelectedNodesToFiles(selectedNodes []*filetree.FileNode) []*filetree.FileNode {
queue := append(make([]*filetree.FileNode, 0, len(selectedNodes)), selectedNodes...)
visited := set.New[string]()
var files []*filetree.FileNode
for len(queue) > 0 {
// pop node from queue
node := queue[0]
queue = queue[1:]
nodeID := node.ID()
if visited.Includes(nodeID) {
continue
}
visited.Add(nodeID)
if node.File != nil {
// unique file node -> collect it
files = append(files, node)
continue
}
// directory node -> enqueue children
for _, ch := range node.Children {
queue = append(queue, &filetree.FileNode{Node: ch})
}
}
return files
}
func someNodesHaveUnstagedChanges(nodes []*filetree.FileNode) bool {
return lo.SomeBy(nodes, (*filetree.FileNode).GetHasUnstagedChanges)
}
+30
View File
@@ -1,6 +1,8 @@
package controllers
import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
@@ -58,6 +60,13 @@ func (self *GlobalController) GetKeybindings(opts types.KeybindingsOpts) []*type
Handler: opts.Guards.NoPopupPanel(self.prevScreenMode),
Description: self.c.Tr.PrevScreenMode,
},
{
Key: opts.GetKey(opts.Config.Universal.CyclePagers),
Handler: opts.Guards.NoPopupPanel(self.cyclePagers),
GetDisabledReason: self.canCyclePagers,
Description: self.c.Tr.CyclePagers,
Tooltip: self.c.Tr.CyclePagersTooltip,
},
{
Key: opts.GetKey(opts.Config.Universal.Return),
Modifier: gocui.ModNone,
@@ -171,6 +180,27 @@ func (self *GlobalController) prevScreenMode() error {
return (&ScreenModeActions{c: self.c}).Prev()
}
func (self *GlobalController) cyclePagers() error {
self.c.State().GetPagerConfig().CyclePagers()
if self.c.Context().CurrentSide().GetKey() == self.c.Context().Current().GetKey() {
self.c.Context().CurrentSide().HandleFocus(types.OnFocusOpts{})
}
current, total := self.c.State().GetPagerConfig().CurrentPagerIndex()
self.c.Toast(fmt.Sprintf("Selected pager %d of %d", current+1, total))
return nil
}
func (self *GlobalController) canCyclePagers() *types.DisabledReason {
_, total := self.c.State().GetPagerConfig().CurrentPagerIndex()
if total <= 1 {
return &types.DisabledReason{
Text: self.c.Tr.CyclePagersDisabledReason,
}
}
return nil
}
func (self *GlobalController) createOptionsMenu() error {
return (&OptionsMenuAction{c: self.c}).Call()
}
@@ -77,7 +77,7 @@ func (self *CherryPickHelper) Paste() error {
}),
HandleConfirm: func() error {
return self.c.WithWaitingStatusSync(self.c.Tr.CherryPickingStatus, func() error {
mustStash := IsWorkingTreeDirty(self.c.Model().Files)
mustStash := IsWorkingTreeDirtyExceptSubmodules(self.c.Model().Files, self.c.Model().Submodules)
self.c.LogAction(self.c.Tr.Actions.CherryPick)
@@ -26,6 +26,14 @@ func NewConfirmationHelper(c *HelperCommon) *ConfirmationHelper {
func (self *ConfirmationHelper) wrappedConfirmationFunction(cancel goContext.CancelFunc, function func() error) func() error {
return func() error {
if self.c.GocuiGui().IsPasting {
// The user is pasting multi-line text into a prompt; we don't want to handle the
// line feeds as "confirm" keybindings. Simply ignoring them is the best we can do; this
// will cause the entire pasted text to appear as a single line in the prompt. Hopefully
// the user knows that ctrl-u allows them to delete it again...
return nil
}
cancel()
self.c.Context().Pop()
+1 -1
View File
@@ -194,7 +194,7 @@ func parseDiff(diff string) ([]*hunk, []*hunk) {
if strings.HasPrefix(line, "diff --git") {
finishHunk()
currentHunk = nil
} else if strings.HasPrefix(line, "--- ") {
} else if currentHunk == nil && strings.HasPrefix(line, "--- ") {
// For some reason, the line ends with a tab character if the file
// name contains spaces
filename = strings.TrimRight(line[6:], "\t")
@@ -78,6 +78,26 @@ index 9ce8efb33..fb5e469e7 100644
},
},
},
{
name: "hunk with dashed lines",
diff: `
diff --git a/file1.txt b/file1.txt
index 9ce8efb33..fb5e469e7 100644
--- a/file1.txt
+++ b/file1.txt
@@ -3,1 +3,1 @@
--- xxx
+-- yyy
`,
expectedDeletedLineHunks: []*hunk{
{
filename: "file1.txt",
startLineIdx: 3,
numLines: 1,
},
},
expectedAddedLineHunks: []*hunk{},
},
{
name: "several hunks in different files",
diff: `
@@ -157,7 +157,7 @@ func (self *MergeAndRebaseHelper) CheckMergeOrRebaseWithRefreshOptions(result er
} else if strings.Contains(result.Error(), "No changes - did you forget to use") {
return self.genericMergeCommand(REBASE_OPTION_SKIP)
} else if strings.Contains(result.Error(), "The previous cherry-pick is now empty") {
return self.genericMergeCommand(REBASE_OPTION_CONTINUE)
return self.genericMergeCommand(REBASE_OPTION_SKIP)
} else if strings.Contains(result.Error(), "No rebase in progress?") {
// assume in this case that we're already done
return nil
@@ -381,38 +381,103 @@ func (self *MergeAndRebaseHelper) MergeRefIntoCheckedOutBranch(refName string) e
return errors.New(self.c.Tr.CantMergeBranchIntoItself)
}
wantFastForward, wantNonFastForward := self.fastForwardMergeUserPreference()
canFastForward := self.c.Git().Branch.CanDoFastForwardMerge(refName)
var firstRegularMergeItem *types.MenuItem
var secondRegularMergeItem *types.MenuItem
var fastForwardMergeItem *types.MenuItem
if !wantNonFastForward && (wantFastForward || canFastForward) {
firstRegularMergeItem = &types.MenuItem{
Label: self.c.Tr.RegularMergeFastForward,
OnPress: self.RegularMerge(refName, git_commands.MERGE_VARIANT_REGULAR),
Key: 'm',
Tooltip: utils.ResolvePlaceholderString(
self.c.Tr.RegularMergeFastForwardTooltip,
map[string]string{
"checkedOutBranch": checkedOutBranchName,
"selectedBranch": refName,
},
),
}
fastForwardMergeItem = firstRegularMergeItem
secondRegularMergeItem = &types.MenuItem{
Label: self.c.Tr.RegularMergeNonFastForward,
OnPress: self.RegularMerge(refName, git_commands.MERGE_VARIANT_NON_FAST_FORWARD),
Key: 'n',
Tooltip: utils.ResolvePlaceholderString(
self.c.Tr.RegularMergeNonFastForwardTooltip,
map[string]string{
"checkedOutBranch": checkedOutBranchName,
"selectedBranch": refName,
},
),
}
} else {
firstRegularMergeItem = &types.MenuItem{
Label: self.c.Tr.RegularMergeNonFastForward,
OnPress: self.RegularMerge(refName, git_commands.MERGE_VARIANT_REGULAR),
Key: 'm',
Tooltip: utils.ResolvePlaceholderString(
self.c.Tr.RegularMergeNonFastForwardTooltip,
map[string]string{
"checkedOutBranch": checkedOutBranchName,
"selectedBranch": refName,
},
),
}
secondRegularMergeItem = &types.MenuItem{
Label: self.c.Tr.RegularMergeFastForward,
OnPress: self.RegularMerge(refName, git_commands.MERGE_VARIANT_FAST_FORWARD),
Key: 'f',
Tooltip: utils.ResolvePlaceholderString(
self.c.Tr.RegularMergeFastForwardTooltip,
map[string]string{
"checkedOutBranch": checkedOutBranchName,
"selectedBranch": refName,
},
),
}
fastForwardMergeItem = secondRegularMergeItem
}
if !canFastForward {
fastForwardMergeItem.DisabledReason = &types.DisabledReason{
Text: utils.ResolvePlaceholderString(
self.c.Tr.CannotFastForwardMerge,
map[string]string{
"checkedOutBranch": checkedOutBranchName,
"selectedBranch": refName,
},
),
}
}
return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.Merge,
Items: []*types.MenuItem{
firstRegularMergeItem,
secondRegularMergeItem,
{
Label: self.c.Tr.RegularMerge,
OnPress: self.RegularMerge(refName),
Key: 'm',
Tooltip: utils.ResolvePlaceholderString(
self.c.Tr.RegularMergeTooltip,
map[string]string{
"checkedOutBranch": checkedOutBranchName,
"selectedBranch": refName,
},
),
},
{
Label: self.c.Tr.SquashMergeUncommittedTitle,
Label: self.c.Tr.SquashMergeUncommitted,
OnPress: self.SquashMergeUncommitted(refName),
Key: 's',
Tooltip: utils.ResolvePlaceholderString(
self.c.Tr.SquashMergeUncommitted,
self.c.Tr.SquashMergeUncommittedTooltip,
map[string]string{
"selectedBranch": refName,
},
),
},
{
Label: self.c.Tr.SquashMergeCommittedTitle,
Label: self.c.Tr.SquashMergeCommitted,
OnPress: self.SquashMergeCommitted(refName, checkedOutBranchName),
Key: 'S',
Tooltip: utils.ResolvePlaceholderString(
self.c.Tr.SquashMergeCommitted,
self.c.Tr.SquashMergeCommittedTooltip,
map[string]string{
"checkedOutBranch": checkedOutBranchName,
"selectedBranch": refName,
@@ -423,10 +488,10 @@ func (self *MergeAndRebaseHelper) MergeRefIntoCheckedOutBranch(refName string) e
})
}
func (self *MergeAndRebaseHelper) RegularMerge(refName string) func() error {
func (self *MergeAndRebaseHelper) RegularMerge(refName string, variant git_commands.MergeVariant) func() error {
return func() error {
self.c.LogAction(self.c.Tr.Actions.Merge)
err := self.c.Git().Branch.Merge(refName, git_commands.MergeOpts{})
err := self.c.Git().Branch.Merge(refName, variant)
return self.CheckMergeOrRebase(err)
}
}
@@ -434,7 +499,7 @@ func (self *MergeAndRebaseHelper) RegularMerge(refName string) func() error {
func (self *MergeAndRebaseHelper) SquashMergeUncommitted(refName string) func() error {
return func() error {
self.c.LogAction(self.c.Tr.Actions.SquashMerge)
err := self.c.Git().Branch.Merge(refName, git_commands.MergeOpts{Squash: true})
err := self.c.Git().Branch.Merge(refName, git_commands.MERGE_VARIANT_SQUASH)
return self.CheckMergeOrRebase(err)
}
}
@@ -442,7 +507,7 @@ func (self *MergeAndRebaseHelper) SquashMergeUncommitted(refName string) func()
func (self *MergeAndRebaseHelper) SquashMergeCommitted(refName, checkedOutBranchName string) func() error {
return func() error {
self.c.LogAction(self.c.Tr.Actions.SquashMerge)
err := self.c.Git().Branch.Merge(refName, git_commands.MergeOpts{Squash: true})
err := self.c.Git().Branch.Merge(refName, git_commands.MERGE_VARIANT_SQUASH)
if err = self.CheckMergeOrRebase(err); err != nil {
return err
}
@@ -459,6 +524,31 @@ func (self *MergeAndRebaseHelper) SquashMergeCommitted(refName, checkedOutBranch
}
}
// Returns wantsFastForward, wantsNonFastForward. These will never both be true, but they can both be false.
func (self *MergeAndRebaseHelper) fastForwardMergeUserPreference() (bool, bool) {
// Check user config first, because it takes precedence over git config
mergingArgs := self.c.UserConfig().Git.Merging.Args
if strings.Contains(mergingArgs, "--ff") { // also covers "--ff-only"
return true, false
}
if strings.Contains(mergingArgs, "--no-ff") {
return false, true
}
// Then check git config
mergeFfConfig := self.c.Git().Config.GetMergeFF()
if mergeFfConfig == "true" || mergeFfConfig == "only" {
return true, false
}
if mergeFfConfig == "false" {
return false, true
}
return false, false
}
func (self *MergeAndRebaseHelper) ResetMarkedBaseCommit() error {
self.c.Modes().MarkedBaseCommit.Reset()
self.c.PostRefreshUpdate(self.c.Contexts().LocalCommits)
+12 -3
View File
@@ -163,6 +163,15 @@ func (self *RefsHelper) CheckoutRemoteBranch(fullBranchName string, localBranchN
})
}
func (self *RefsHelper) CheckoutPreviousRef() error {
previousRef, err := self.c.Git().Branch.PreviousRef()
if err == nil && strings.HasPrefix(previousRef, "refs/heads/") {
return self.CheckoutRef(strings.TrimPrefix(previousRef, "refs/heads/"), types.CheckoutRefOptions{})
}
return self.CheckoutRef("-", types.CheckoutRefOptions{})
}
func (self *RefsHelper) GetCheckedOutRef() *models.Branch {
if len(self.c.Model().Branches) == 0 {
return nil
@@ -249,7 +258,7 @@ func (self *RefsHelper) CreateGitResetMenu(name string, ref string) error {
style.FgRed.Sprintf("reset --%s %s", row.strength, name),
},
OnPress: func() error {
return self.c.ConfirmIf(row.strength == "hard" && IsWorkingTreeDirty(self.c.Model().Files),
return self.c.ConfirmIf(row.strength == "hard" && IsWorkingTreeDirtyExceptSubmodules(self.c.Model().Files, self.c.Model().Submodules),
types.ConfirmOpts{
Title: self.c.Tr.Actions.HardReset,
Prompt: self.c.Tr.ResetHardConfirmation,
@@ -475,7 +484,7 @@ func (self *RefsHelper) moveCommitsToNewBranchStackedOnCurrentBranch(newBranchNa
return err
}
mustStash := IsWorkingTreeDirty(self.c.Model().Files)
mustStash := IsWorkingTreeDirtyExceptSubmodules(self.c.Model().Files, self.c.Model().Submodules)
if mustStash {
if err := self.c.Git().Stash.Push(fmt.Sprintf(self.c.Tr.AutoStashForNewBranch, newBranchName)); err != nil {
return err
@@ -508,7 +517,7 @@ func (self *RefsHelper) moveCommitsToNewBranchOffOfMainBranch(newBranchName stri
return commit.Status == models.StatusUnpushed
})
mustStash := IsWorkingTreeDirty(self.c.Model().Files)
mustStash := IsWorkingTreeDirtyExceptSubmodules(self.c.Model().Files, self.c.Model().Submodules)
if mustStash {
if err := self.c.Git().Stash.Push(fmt.Sprintf(self.c.Tr.AutoStashForNewBranch, newBranchName)); err != nil {
return err
@@ -43,10 +43,9 @@ type WindowArrangementArgs struct {
Height int
// User config
UserConfig *config.UserConfig
// Name of the currently focused window
// Name of the currently focused window. (It's actually the current static window, meaning
// popups are ignored)
CurrentWindow string
// Name of the current static window (meaning popups are ignored)
CurrentStaticWindow string
// Name of the current side window (i.e. the current window in the left
// section of the UI)
CurrentSideWindow string
@@ -86,21 +85,20 @@ func (self *WindowArrangementHelper) GetWindowDimensions(informationStr string,
}
args := WindowArrangementArgs{
Width: width,
Height: height,
UserConfig: self.c.UserConfig(),
CurrentWindow: self.windowHelper.CurrentWindow(),
CurrentSideWindow: self.c.Context().CurrentSide().GetWindowName(),
CurrentStaticWindow: self.c.Context().CurrentStatic().GetWindowName(),
SplitMainPanel: repoState.GetSplitMainPanel(),
ScreenMode: repoState.GetScreenMode(),
AppStatus: appStatus,
InformationStr: informationStr,
ShowExtrasWindow: self.c.State().GetShowExtrasWindow(),
InDemo: self.c.InDemo(),
IsAnyModeActive: self.modeHelper.IsAnyModeActive(),
InSearchPrompt: repoState.InSearchPrompt(),
SearchPrefix: searchPrefix,
Width: width,
Height: height,
UserConfig: self.c.UserConfig(),
CurrentWindow: self.c.Context().CurrentStatic().GetWindowName(),
CurrentSideWindow: self.c.Context().CurrentSide().GetWindowName(),
SplitMainPanel: repoState.GetSplitMainPanel(),
ScreenMode: repoState.GetScreenMode(),
AppStatus: appStatus,
InformationStr: informationStr,
ShowExtrasWindow: self.c.State().GetShowExtrasWindow(),
InDemo: self.c.InDemo(),
IsAnyModeActive: self.modeHelper.IsAnyModeActive(),
InSearchPrompt: repoState.InSearchPrompt(),
SearchPrefix: searchPrefix,
}
return GetWindowDimensions(args)
@@ -393,7 +391,7 @@ func splitMainPanelSideBySide(args WindowArrangementArgs) bool {
func getExtrasWindowSize(args WindowArrangementArgs) int {
var baseSize int
// The 'extras' window contains the command log context
if args.CurrentStaticWindow == "extras" {
if args.CurrentWindow == "extras" {
baseSize = 1000 // my way of saying 'fill the available space'
} else if args.Height < 40 {
baseSize = 1
@@ -19,21 +19,20 @@ import (
func TestGetWindowDimensions(t *testing.T) {
getDefaultArgs := func() WindowArrangementArgs {
return WindowArrangementArgs{
Width: 75,
Height: 30,
UserConfig: config.GetDefaultConfig(),
CurrentWindow: "files",
CurrentSideWindow: "files",
CurrentStaticWindow: "files",
SplitMainPanel: false,
ScreenMode: types.SCREEN_NORMAL,
AppStatus: "",
InformationStr: "information",
ShowExtrasWindow: false,
InDemo: false,
IsAnyModeActive: false,
InSearchPrompt: false,
SearchPrefix: "",
Width: 75,
Height: 30,
UserConfig: config.GetDefaultConfig(),
CurrentWindow: "files",
CurrentSideWindow: "files",
SplitMainPanel: false,
ScreenMode: types.SCREEN_NORMAL,
AppStatus: "",
InformationStr: "information",
ShowExtrasWindow: false,
InDemo: false,
IsAnyModeActive: false,
InSearchPrompt: false,
SearchPrefix: "",
}
}
@@ -3,21 +3,24 @@ package helpers
import (
"errors"
"fmt"
"os"
"regexp"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/samber/lo"
)
type WorkingTreeHelper struct {
c *HelperCommon
refHelper *RefsHelper
commitsHelper *CommitsHelper
gpgHelper *GpgHelper
c *HelperCommon
refHelper *RefsHelper
commitsHelper *CommitsHelper
gpgHelper *GpgHelper
mergeAndRebaseHelper *MergeAndRebaseHelper
}
func NewWorkingTreeHelper(
@@ -25,12 +28,14 @@ func NewWorkingTreeHelper(
refHelper *RefsHelper,
commitsHelper *CommitsHelper,
gpgHelper *GpgHelper,
mergeAndRebaseHelper *MergeAndRebaseHelper,
) *WorkingTreeHelper {
return &WorkingTreeHelper{
c: c,
refHelper: refHelper,
commitsHelper: commitsHelper,
gpgHelper: gpgHelper,
c: c,
refHelper: refHelper,
commitsHelper: commitsHelper,
gpgHelper: gpgHelper,
mergeAndRebaseHelper: mergeAndRebaseHelper,
}
}
@@ -42,6 +47,14 @@ func AnyStagedFiles(files []*models.File) bool {
return lo.SomeBy(files, func(f *models.File) bool { return f.HasStagedChanges })
}
func (self *WorkingTreeHelper) AnyStagedFilesExceptSubmodules() bool {
return AnyStagedFilesExceptSubmodules(self.c.Model().Files, self.c.Model().Submodules)
}
func AnyStagedFilesExceptSubmodules(files []*models.File, submoduleConfigs []*models.SubmoduleConfig) bool {
return lo.SomeBy(files, func(f *models.File) bool { return f.HasStagedChanges && !f.IsSubmodule(submoduleConfigs) })
}
func (self *WorkingTreeHelper) AnyTrackedFiles() bool {
return AnyTrackedFiles(self.c.Model().Files)
}
@@ -50,12 +63,20 @@ func AnyTrackedFiles(files []*models.File) bool {
return lo.SomeBy(files, func(f *models.File) bool { return f.Tracked })
}
func (self *WorkingTreeHelper) IsWorkingTreeDirty() bool {
return IsWorkingTreeDirty(self.c.Model().Files)
func (self *WorkingTreeHelper) AnyTrackedFilesExceptSubmodules() bool {
return AnyTrackedFilesExceptSubmodules(self.c.Model().Files, self.c.Model().Submodules)
}
func IsWorkingTreeDirty(files []*models.File) bool {
return AnyStagedFiles(files) || AnyTrackedFiles(files)
func AnyTrackedFilesExceptSubmodules(files []*models.File, submoduleConfigs []*models.SubmoduleConfig) bool {
return lo.SomeBy(files, func(f *models.File) bool { return f.Tracked && !f.IsSubmodule(submoduleConfigs) })
}
func (self *WorkingTreeHelper) IsWorkingTreeDirtyExceptSubmodules() bool {
return IsWorkingTreeDirtyExceptSubmodules(self.c.Model().Files, self.c.Model().Submodules)
}
func IsWorkingTreeDirtyExceptSubmodules(files []*models.File, submoduleConfigs []*models.SubmoduleConfig) bool {
return AnyStagedFilesExceptSubmodules(files, submoduleConfigs) || AnyTrackedFilesExceptSubmodules(files, submoduleConfigs)
}
func (self *WorkingTreeHelper) FileForSubmodule(submodule *models.SubmoduleConfig) *models.File {
@@ -247,3 +268,135 @@ func (self *WorkingTreeHelper) commitPrefixConfigsForRepo() []config.CommitPrefi
return self.c.UserConfig().Git.CommitPrefix
}
func (self *WorkingTreeHelper) mergeFile(filepath string, strategy string) (string, error) {
if self.c.Git().Version.IsOlderThan(2, 43, 0) {
return self.mergeFileWithTempFiles(filepath, strategy)
}
return self.mergeFileWithObjectIDs(filepath, strategy)
}
func (self *WorkingTreeHelper) mergeFileWithTempFiles(filepath string, strategy string) (string, error) {
showToTempFile := func(stage int, label string) (string, error) {
output, err := self.c.Git().WorkingTree.ShowFileAtStage(filepath, stage)
if err != nil {
return "", err
}
f, err := os.CreateTemp(self.c.GetConfig().GetTempDir(), "mergefile-"+label+"-*")
if err != nil {
return "", err
}
defer f.Close()
if _, err := f.Write([]byte(output)); err != nil {
return "", err
}
return f.Name(), nil
}
baseFilepath, err := showToTempFile(1, "base")
if err != nil {
return "", err
}
defer os.Remove(baseFilepath)
oursFilepath, err := showToTempFile(2, "ours")
if err != nil {
return "", err
}
defer os.Remove(oursFilepath)
theirsFilepath, err := showToTempFile(3, "theirs")
if err != nil {
return "", err
}
defer os.Remove(theirsFilepath)
return self.c.Git().WorkingTree.MergeFileForFiles(strategy, oursFilepath, baseFilepath, theirsFilepath)
}
func (self *WorkingTreeHelper) mergeFileWithObjectIDs(filepath, strategy string) (string, error) {
baseID, err := self.c.Git().WorkingTree.ObjectIDAtStage(filepath, 1)
if err != nil {
return "", err
}
oursID, err := self.c.Git().WorkingTree.ObjectIDAtStage(filepath, 2)
if err != nil {
return "", err
}
theirsID, err := self.c.Git().WorkingTree.ObjectIDAtStage(filepath, 3)
if err != nil {
return "", err
}
return self.c.Git().WorkingTree.MergeFileForObjectIDs(strategy, oursID, baseID, theirsID)
}
func (self *WorkingTreeHelper) CreateMergeConflictMenu(selectedFilepaths []string) error {
onMergeStrategySelected := func(strategy string) error {
for _, filepath := range selectedFilepaths {
output, err := self.mergeFile(filepath, strategy)
if err != nil {
return err
}
if err = os.WriteFile(filepath, []byte(output), 0o644); err != nil {
return err
}
}
err := self.c.Git().WorkingTree.StageFiles(selectedFilepaths, nil)
self.c.Refresh(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{types.FILES}})
return err
}
cmdColor := style.FgBlue
return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.MergeConflictOptionsTitle,
Items: []*types.MenuItem{
{
LabelColumns: []string{
self.c.Tr.UseCurrentChanges,
cmdColor.Sprint("git merge-file --ours"),
},
OnPress: func() error {
return onMergeStrategySelected("--ours")
},
Key: 'c',
},
{
LabelColumns: []string{
self.c.Tr.UseIncomingChanges,
cmdColor.Sprint("git merge-file --theirs"),
},
OnPress: func() error {
return onMergeStrategySelected("--theirs")
},
Key: 'i',
},
{
LabelColumns: []string{
self.c.Tr.UseBothChanges,
cmdColor.Sprint("git merge-file --union"),
},
OnPress: func() error {
return onMergeStrategySelected("--union")
},
Key: 'b',
},
{
LabelColumns: []string{
self.c.Tr.OpenMergeTool,
cmdColor.Sprint("git mergetool"),
},
OnPress: self.OpenMergeTool,
Key: 'm',
},
},
})
}
@@ -189,7 +189,8 @@ func (self *WorktreeHelper) Remove(worktree *models.Worktree, force bool) error
self.c.LogAction(self.c.Tr.RemoveWorktree)
if err := self.c.Git().Worktree.Delete(worktree.Path, force); err != nil {
errMessage := err.Error()
if !strings.Contains(errMessage, "--force") {
if !strings.Contains(errMessage, "--force") &&
!strings.Contains(errMessage, "fatal: working trees containing submodules cannot be moved or removed") {
return err
}
@@ -856,7 +856,7 @@ func (self *LocalCommitsController) revert(commits []*models.Commit, start, end
HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.RevertCommit)
return self.c.WithWaitingStatusSync(self.c.Tr.RevertingStatus, func() error {
mustStash := helpers.IsWorkingTreeDirty(self.c.Model().Files)
mustStash := helpers.IsWorkingTreeDirtyExceptSubmodules(self.c.Model().Files, self.c.Model().Submodules)
if mustStash {
if err := self.c.Git().Stash.Push(self.c.Tr.AutoStashForReverting); err != nil {
@@ -112,10 +112,11 @@ func (self *MergeConflictsController) GetKeybindings(opts types.KeybindingsOpts)
Tag: "navigation",
},
{
Key: opts.GetKey(opts.Config.Files.OpenMergeTool),
Handler: self.c.Helpers().WorkingTree.OpenMergeTool,
Description: self.c.Tr.OpenMergeTool,
Tooltip: self.c.Tr.OpenMergeToolTooltip,
Key: opts.GetKey(opts.Config.Files.OpenMergeOptions),
Handler: self.openMergeConflictMenu,
Description: self.c.Tr.ViewMergeConflictOptions,
Tooltip: self.c.Tr.ViewMergeConflictOptionsTooltip,
OpensMenu: true,
DisplayOnScreen: true,
},
{
@@ -320,6 +321,11 @@ func (self *MergeConflictsController) onLastConflictResolved() {
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
}
func (self *MergeConflictsController) openMergeConflictMenu() error {
filepath := self.context().GetState().GetPath()
return self.c.Helpers().WorkingTree.CreateMergeConflictMenu([]string{filepath})
}
func (self *MergeConflictsController) withRenderAndFocus(f func() error) func() error {
return self.withLock(func() error {
if err := f(); err != nil {
@@ -51,6 +51,7 @@ func (self *OptionsMenuAction) Call() error {
HideCancel: true,
ColumnAlignment: []utils.Alignment{utils.AlignRight, utils.AlignLeft},
AllowFilteringKeybindings: true,
KeepConfirmKeybindings: true,
})
}
+6 -2
View File
@@ -181,10 +181,14 @@ func (self *StatusController) showAllBranchLogs() {
cmdObj := self.c.Git().Branch.AllBranchesLogCmdObj()
task := types.NewRunPtyTask(cmdObj.GetCmd())
title := self.c.Tr.LogTitle
if i, n := self.c.Git().Branch.GetAllBranchesLogIdxAndCount(); n > 1 {
title = fmt.Sprintf(self.c.Tr.LogXOfYTitle, i+1, n)
}
self.c.RenderToMainViews(types.RefreshMainOpts{
Pair: self.c.MainViewPairs().Normal,
Main: &types.ViewUpdateOpts{
Title: self.c.Tr.LogTitle,
Title: title,
Task: task,
},
})
@@ -196,7 +200,7 @@ func (self *StatusController) switchToOrRotateAllBranchesLogs() {
// A bit of a hack to ensure we only rotate to the next branch log command
// if we currently are looking at a branch log. Otherwise, we should just show
// the current index (if we are coming from the dashboard).
if self.c.Views().Main.Title == self.c.Tr.LogTitle {
if self.c.Views().Main.Title != self.c.Tr.StatusTitle {
self.c.Git().Branch.RotateAllBranchesLogIdx()
}
self.showAllBranchLogs()
@@ -1,6 +1,8 @@
package controllers
import (
"path/filepath"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
@@ -90,6 +92,15 @@ func (self *SwitchToDiffFilesController) enter() error {
Scope: []types.RefreshableView{types.COMMIT_FILES},
})
if filterPath := self.c.Modes().Filtering.GetPath(); filterPath != "" {
path, err := filepath.Rel(self.c.Git().RepoPaths.RepoPath(), filterPath)
if err != nil {
path = filterPath
}
commitFilesContext.CommitFileTreeViewModel.SelectPath(
filepath.ToSlash(path), self.c.UserConfig().Gui.ShowRootItemInFileTree)
}
self.c.Context().Push(commitFilesContext, types.OnFocusOpts{})
return nil
}
+1 -1
View File
@@ -257,7 +257,7 @@ func (self *UndoController) hardResetWithAutoStash(commitHash string, options ha
}
// if we have any modified tracked files we need to auto-stash
dirtyWorkingTree := self.c.Helpers().WorkingTree.IsWorkingTreeDirty()
dirtyWorkingTree := self.c.Helpers().WorkingTree.IsWorkingTreeDirtyExceptSubmodules()
if dirtyWorkingTree {
return self.c.WithWaitingStatus(options.WaitingStatus, func(gocui.Task) error {
if err := self.c.Git().Stash.Push(fmt.Sprintf(self.c.Tr.AutoStashForUndo, utils.ShortHash(commitHash))); err != nil {
@@ -100,7 +100,7 @@ func (self *FilesController) createResetMenu() error {
Tooltip: self.c.Tr.DiscardStagedChangesDescription,
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.RemoveStagedFiles)
if !self.c.Helpers().WorkingTree.IsWorkingTreeDirty() {
if !self.c.Helpers().WorkingTree.IsWorkingTreeDirtyExceptSubmodules() {
return errors.New(self.c.Tr.NoTrackedStagedFilesStash)
}
if err := self.c.Git().Stash.SaveStagedChanges("[lazygit] tmp stash"); err != nil {
@@ -159,7 +159,7 @@ func (self *FilesController) createResetMenu() error {
red.Sprint("git reset --hard HEAD"),
},
OnPress: func() error {
return self.c.ConfirmIf(helpers.IsWorkingTreeDirty(self.c.Model().Files),
return self.c.ConfirmIf(helpers.IsWorkingTreeDirtyExceptSubmodules(self.c.Model().Files, self.c.Model().Submodules),
types.ConfirmOpts{
Title: self.c.Tr.Actions.HardReset,
Prompt: self.c.Tr.ResetHardConfirmation,
+6 -2
View File
@@ -162,9 +162,13 @@ func join(strs []string) string {
}
func SplitFileTreePath(path string, showRootItem bool) []string {
return split(InternalTreePathForFilePath(path, showRootItem))
}
func InternalTreePathForFilePath(path string, showRootItem bool) string {
if showRootItem {
return split("./" + path)
return "./" + path
}
return split(path)
return path
}
@@ -191,3 +191,15 @@ func (self *CommitFileTreeViewModel) ExpandAll() {
self.SetSelectedLineIdx(index)
}
}
// Try to select the given path if present. If it doesn't exist, or one of the parent directories is
// collapsed, do nothing.
// Note that filepath is an actual file path, not an internal tree path as with e.g.
// ToggleCollapsed. It must be a relative path (relative to the repo root), and it must contain
// forward slashes rather than backslashes even on Windows.
func (self *CommitFileTreeViewModel) SelectPath(filepath string, showRootItem bool) {
index, found := self.GetIndexForPath(InternalTreePathForFilePath(filepath, showRootItem))
if found {
self.SetSelection(index)
}
}
+12 -3
View File
@@ -69,6 +69,8 @@ type Gui struct {
// this is the state of the GUI for the current repo
State *GuiRepoState
pagerConfig *config.PagerConfig
CustomCommandsClient *custom_commands.Client
// this is a mapping of repos to gui states, so that we can restore the original
@@ -169,6 +171,10 @@ func (self *StateAccessor) GetRepoState() types.IRepoStateAccessor {
return self.gui.State
}
func (self *StateAccessor) GetPagerConfig() *config.PagerConfig {
return self.gui.pagerConfig
}
func (self *StateAccessor) GetIsRefreshingFiles() bool {
return self.gui.IsRefreshingFiles
}
@@ -307,6 +313,7 @@ func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, contextKey types.Context
gui.gitVersion,
gui.os,
git_config.NewStdCachedGitConfig(gui.Log),
gui.pagerConfig,
)
if err != nil {
return err
@@ -653,7 +660,7 @@ func (gui *Gui) Contexts() *context.ContextTree {
// NewGui builds a new gui handler
func NewGui(
cmn *common.Common,
config config.AppConfigurer,
configurer config.AppConfigurer,
gitVersion *git_commands.GitVersion,
updater *updates.Updater,
showRecentRepos bool,
@@ -663,7 +670,7 @@ func NewGui(
gui := &Gui{
Common: cmn,
gitVersion: gitVersion,
Config: config,
Config: configurer,
Updater: updater,
statusManager: status.NewStatusManager(),
viewBufferManagerMap: map[string]*tasks.ViewBufferManager{},
@@ -713,7 +720,7 @@ func NewGui(
credentialsHelper.PromptUserForCredential,
)
osCommand := oscommands.NewOSCommand(cmn, config, oscommands.GetPlatform(), guiIO)
osCommand := oscommands.NewOSCommand(cmn, configurer, oscommands.GetPlatform(), guiIO)
gui.os = osCommand
@@ -724,6 +731,8 @@ func NewGui(
gui.BackgroundRoutineMgr = &BackgroundRoutineMgr{gui: gui}
gui.stateAccessor = &StateAccessor{gui: gui}
gui.pagerConfig = config.NewPagerConfig(func() *config.UserConfig { return gui.UserConfig() })
return gui, nil
}
+1 -1
View File
@@ -35,7 +35,7 @@ func (gui *Gui) createMenu(opts types.CreateMenuOptions) error {
maxColumnSize = max(maxColumnSize, len(item.LabelColumns))
// Remove all item keybindings that are the same as the confirm binding
if item.Key == confirmKey {
if item.Key == confirmKey && !opts.KeepConfirmKeybindings {
item.Key = nil
}
}
+7 -6
View File
@@ -16,9 +16,10 @@ import (
"github.com/xo/terminfo"
)
func makeAtomic(v int32) (result atomic.Int32) {
func makeAtomic(v int32) *atomic.Int32 {
var result atomic.Int32
result.Store(v)
return //nolint: nakedret
return &result
}
func Test_getBranchDisplayStrings(t *testing.T) {
@@ -109,7 +110,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
branch: &models.Branch{
Name: "branch_name",
Recency: "1m",
BehindBaseBranch: makeAtomic(2),
BehindBaseBranch: *makeAtomic(2),
},
itemOperation: types.ItemOperationNone,
fullDescription: false,
@@ -126,7 +127,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
UpstreamRemote: "origin",
AheadForPull: "0",
BehindForPull: "0",
BehindBaseBranch: makeAtomic(2),
BehindBaseBranch: *makeAtomic(2),
},
itemOperation: types.ItemOperationNone,
fullDescription: false,
@@ -143,7 +144,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
UpstreamRemote: "origin",
AheadForPull: "3",
BehindForPull: "5",
BehindBaseBranch: makeAtomic(2),
BehindBaseBranch: *makeAtomic(2),
},
itemOperation: types.ItemOperationNone,
fullDescription: false,
@@ -247,7 +248,7 @@ func Test_getBranchDisplayStrings(t *testing.T) {
UpstreamRemote: "origin",
AheadForPull: "3",
BehindForPull: "5",
BehindBaseBranch: makeAtomic(4),
BehindBaseBranch: *makeAtomic(4),
},
itemOperation: types.ItemOperationNone,
fullDescription: false,
+6 -5
View File
@@ -45,11 +45,12 @@ func (gui *Gui) onResize() error {
// command.
func (gui *Gui) newPtyTask(view *gocui.View, cmd *exec.Cmd, prefix string) error {
width := view.InnerWidth()
pager := gui.git.Config.GetPager(width)
externalDiffCommand := gui.Config.GetUserConfig().Git.Paging.ExternalDiffCommand
pager := gui.stateAccessor.GetPagerConfig().GetPagerCommand(width)
externalDiffCommand := gui.stateAccessor.GetPagerConfig().GetExternalDiffCommand()
useExtDiffGitConfig := gui.stateAccessor.GetPagerConfig().GetUseExternalDiffGitConfig()
if pager == "" && externalDiffCommand == "" {
// if we're not using a custom pager we don't need to use a pty
if pager == "" && externalDiffCommand == "" && !useExtDiffGitConfig {
// If we're not using a custom pager nor external diff command, then we don't need to use a pty
return gui.newCmdTask(view, cmd, prefix)
}
@@ -58,7 +59,7 @@ func (gui *Gui) newPtyTask(view *gocui.View, cmd *exec.Cmd, prefix string) error
// Need to get the width and the pager again because the layout might have
// changed the size of the view
width = view.InnerWidth()
pager = gui.git.Config.GetPager(width)
pager := gui.stateAccessor.GetPagerConfig().GetPagerCommand(width)
cmdStr := strings.Join(cmd.Args, " ")
+2
View File
@@ -1,6 +1,7 @@
package gui
import (
"fmt"
"os/exec"
"github.com/jesseduffield/gocui"
@@ -11,5 +12,6 @@ func (gui *Gui) onResize() error {
}
func (gui *Gui) newPtyTask(view *gocui.View, cmd *exec.Cmd, prefix string) error {
cmd.Env = append(cmd.Env, fmt.Sprintf("LAZYGIT_COLUMNS=%d", view.InnerWidth()))
return gui.newCmdTask(view, cmd, prefix)
}
+2
View File
@@ -156,6 +156,7 @@ type CreateMenuOptions struct {
HideCancel bool
ColumnAlignment []utils.Alignment
AllowFilteringKeybindings bool
KeepConfirmKeybindings bool // if true, the keybindings that match the confirm binding will not be removed from menu items
}
type CreatePopupPanelOpts struct {
@@ -355,6 +356,7 @@ type HasUrn interface {
type IStateAccessor interface {
GetRepoPathStack() *utils.StringStack
GetRepoState() IRepoStateAccessor
GetPagerConfig() *config.PagerConfig
// tells us whether we're currently updating lazygit
GetUpdating() bool
SetUpdating(bool)
+5
View File
@@ -157,6 +157,11 @@ func (gui *Gui) postRefreshUpdate(c types.Context) {
sidePanelContext.HandleRenderToMain()
}
}
} else if c.GetKey() == gui.State.ContextMgr.CurrentStatic().GetKey() {
// If our view is not the current one, but it is the current static context, then this
// can only mean that a popup is showing. In that case we want to refresh the main view
// behind the popup.
c.HandleRenderToMain()
}
}
}
+42 -16
View File
@@ -23,13 +23,9 @@ type TranslationSet struct {
StagedChanges string
StagingTitle string
MergingTitle string
SquashMergeUncommittedTitle string
SquashMergeCommittedTitle string
SquashMergeUncommitted string
SquashMergeCommitted string
RegularMergeTooltip string
NormalTitle string
LogTitle string
LogXOfYTitle string
CommitSummary string
CredentialsUsername string
CredentialsPassword string
@@ -63,7 +59,6 @@ type TranslationSet struct {
ToggleTreeViewTooltip string
OpenDiffTool string
OpenMergeTool string
OpenMergeToolTooltip string
Refresh string
RefreshTooltip string
Push string
@@ -268,8 +263,16 @@ type TranslationSet struct {
RefreshFiles string
FocusMainView string
Merge string
RegularMerge string
MergeBranchTooltip string
RegularMergeFastForward string
RegularMergeFastForwardTooltip string
CannotFastForwardMerge string
RegularMergeNonFastForward string
RegularMergeNonFastForwardTooltip string
SquashMergeUncommitted string
SquashMergeUncommittedTooltip string
SquashMergeCommitted string
SquashMergeCommittedTooltip string
ConfirmQuit string
SwitchRepo string
AllBranchesLogGraph string
@@ -585,6 +588,9 @@ type TranslationSet struct {
ViewResetToUpstreamOptions string
NextScreenMode string
PrevScreenMode string
CyclePagers string
CyclePagersTooltip string
CyclePagersDisabledReason string
StartSearch string
StartFilter string
Keybindings string
@@ -898,6 +904,13 @@ type TranslationSet struct {
BreakingChangesTitle string
BreakingChangesMessage string
BreakingChangesByVersion map[string]string
ViewMergeConflictOptions string
ViewMergeConflictOptionsTooltip string
NoFilesWithMergeConflicts string
MergeConflictOptionsTitle string
UseCurrentChanges string
UseIncomingChanges string
UseBothChanges string
}
type Bisect struct {
@@ -1098,12 +1111,11 @@ func EnglishTranslationSet() *TranslationSet {
EasterEgg: "Easter egg",
UnstagedChanges: "Unstaged changes",
StagedChanges: "Staged changes",
SquashMergeUncommittedTitle: "Squash merge and leave uncommitted",
SquashMergeCommittedTitle: "Squash merge and commit",
StagingTitle: "Main panel (staging)",
MergingTitle: "Main panel (merging)",
NormalTitle: "Main panel (normal)",
LogTitle: "Log",
LogXOfYTitle: "Log (%d of %d)",
CommitSummary: "Commit summary",
CredentialsUsername: "Username",
CredentialsPassword: "Password",
@@ -1136,7 +1148,6 @@ func EnglishTranslationSet() *TranslationSet {
ToggleTreeViewTooltip: "Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.\n\nThe default can be changed in the config file with the key 'gui.showFileTree'.",
OpenDiffTool: "Open external diff tool (git difftool)",
OpenMergeTool: "Open external merge tool",
OpenMergeToolTooltip: "Run `git mergetool`.",
Refresh: "Refresh",
RefreshTooltip: "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`.",
Push: "Push",
@@ -1223,7 +1234,7 @@ func EnglishTranslationSet() *TranslationSet {
NewBranchFromStashTooltip: "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.",
MoveCommitsToNewBranch: "Move commits to new branch",
MoveCommitsToNewBranchTooltip: "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.\n\nNote 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).",
MoveCommitsToNewBranchFromMainPrompt: "This will take all unpushed commits and move them to a new branch (off of {{.baseBranchName}}). It will then hard-reset the current branch its the upstream branch. Do you want to continue?",
MoveCommitsToNewBranchFromMainPrompt: "This will take all unpushed commits and move them to a new branch (off of {{.baseBranchName}}). It will then hard-reset the current branch to its upstream branch. Do you want to continue?",
MoveCommitsToNewBranchMenuPrompt: "This will take all unpushed commits and move them to a new branch. This new branch can either be created from the main branch ({{.baseBranchName}}) or stacked on top of the current branch. Which of these would you like to do?",
MoveCommitsToNewBranchFromBaseItem: "New branch from base branch (%s)",
MoveCommitsToNewBranchStackedItem: "New branch stacked on current branch (%s)",
@@ -1342,8 +1353,16 @@ func EnglishTranslationSet() *TranslationSet {
RefreshFiles: `Refresh files`,
FocusMainView: "Focus main view",
Merge: `Merge`,
RegularMerge: "Regular merge",
MergeBranchTooltip: "View options for merging the selected item into the current branch (regular merge, squash merge)",
RegularMergeFastForward: "Regular merge (fast-forward)",
RegularMergeFastForwardTooltip: "Fast-forward '{{.checkedOutBranch}}' to '{{.selectedBranch}}' without creating a merge commit.",
CannotFastForwardMerge: "Cannot fast-forward '{{.checkedOutBranch}}' to '{{.selectedBranch}}'",
RegularMergeNonFastForward: "Regular merge (with merge commit)",
RegularMergeNonFastForwardTooltip: "Merge '{{.selectedBranch}}' into '{{.checkedOutBranch}}', creating a merge commit.",
SquashMergeUncommitted: "Squash merge and leave uncommitted",
SquashMergeUncommittedTooltip: "Squash merge '{{.selectedBranch}}' into the working tree.",
SquashMergeCommitted: "Squash merge and commit",
SquashMergeCommittedTooltip: "Squash merge '{{.selectedBranch}}' into '{{.checkedOutBranch}}' as a single commit.",
ConfirmQuit: `Are you sure you want to quit?`,
SwitchRepo: `Switch to a recent repo`,
AllBranchesLogGraph: `Show/cycle all branch logs`,
@@ -1428,9 +1447,6 @@ func EnglishTranslationSet() *TranslationSet {
InteractiveRebaseTooltip: "Begin an interactive rebase with a break at the start, so you can update the TODO commits before continuing.",
RebaseOntoBaseBranchTooltip: "Rebase the checked out branch onto its base branch (i.e. the closest main branch).",
MustSelectTodoCommits: "When rebasing, this action only works on a selection of TODO commits.",
SquashMergeUncommitted: "Squash merge '{{.selectedBranch}}' into the working tree.",
SquashMergeCommitted: "Squash merge '{{.selectedBranch}}' into '{{.checkedOutBranch}}' as a single commit.",
RegularMergeTooltip: "Merge '{{.selectedBranch}}' into '{{.checkedOutBranch}}'.",
FwdNoUpstream: "Cannot fast-forward a branch with no upstream",
FwdNoLocalUpstream: "Cannot fast-forward a branch whose remote is not registered locally",
FwdCommitsToPush: "Cannot fast-forward a branch with commits to push",
@@ -1671,6 +1687,9 @@ func EnglishTranslationSet() *TranslationSet {
ViewResetToUpstreamOptions: "View upstream reset options",
NextScreenMode: "Next screen mode (normal/half/fullscreen)",
PrevScreenMode: "Prev screen mode",
CyclePagers: "Cycle pagers",
CyclePagersTooltip: "Choose the next pager in the list of configured pagers",
CyclePagersDisabledReason: "No other pagers configured",
StartSearch: "Search the current view by text",
StartFilter: "Filter the current view by text",
KeybindingsLegend: "Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b",
@@ -1920,7 +1939,7 @@ func EnglishTranslationSet() *TranslationSet {
RemoveWorktree: "Remove worktree",
RemoveWorktreeTitle: "Remove worktree",
RemoveWorktreePrompt: "Are you sure you want to remove worktree '{{.worktreeName}}'?",
ForceRemoveWorktreePrompt: "'{{.worktreeName}}' contains modified or untracked files (to be honest, it could contain both). Are you sure you want to remove it?",
ForceRemoveWorktreePrompt: "'{{.worktreeName}}' contains modified or untracked files, or submodules (or all of these). Are you sure you want to remove it?",
RemovingWorktree: "Deleting worktree",
DetachWorktree: "Detach worktree",
DetachingWorktree: "Detaching worktree",
@@ -1970,6 +1989,13 @@ func EnglishTranslationSet() *TranslationSet {
CustomCommands: "Custom commands",
NoApplicableCommandsInThisContext: "(No applicable commands in this context)",
SelectCommitsOfCurrentBranch: "Select commits of current branch",
ViewMergeConflictOptions: "View merge conflict options",
ViewMergeConflictOptionsTooltip: "View options for resolving merge conflicts.",
NoFilesWithMergeConflicts: "There are no files with merge conflicts.",
MergeConflictOptionsTitle: "Resolve merge conflicts",
UseCurrentChanges: "Use current changes",
UseIncomingChanges: "Use incoming changes",
UseBothChanges: "Use both",
Actions: Actions{
// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm)
-8
View File
@@ -11,11 +11,6 @@
"StagedChanges": "ステージされた変更",
"StagingTitle": "メインパネル(ステージング)",
"MergingTitle": "メインパネル(マージ中)",
"SquashMergeUncommittedTitle": "スカッシュマージして未コミットのままにする",
"SquashMergeCommittedTitle": "スカッシュマージしてコミットする",
"SquashMergeUncommitted": "'{{.selectedBranch}}'をワーキングツリーにスカッシュマージします。",
"SquashMergeCommitted": "'{{.selectedBranch}}'を'{{.checkedOutBranch}}'に単一のコミットとしてスカッシュマージします。",
"RegularMergeTooltip": "'{{.selectedBranch}}'を'{{.checkedOutBranch}}'にマージします。",
"NormalTitle": "メインパネル(通常)",
"LogTitle": "ログ",
"CommitSummary": "コミット概要",
@@ -50,7 +45,6 @@
"ToggleTreeView": "ファイルツリービューを切り替え",
"OpenDiffTool": "外部差分ツールを開く(git difftool)",
"OpenMergeTool": "外部マージツールを開く",
"OpenMergeToolTooltip": "`git mergetool`を実行します。",
"Refresh": "更新",
"RefreshTooltip": "Gitの状態を更新します(`git status`、`git branch`などをバックグラウンドで実行してパネルの内容を更新します)。これは`git fetch`を実行しません。",
"Push": "プッシュ",
@@ -247,7 +241,6 @@
"RefreshFiles": "ファイルを更新",
"FocusMainView": "メインビューにフォーカス",
"Merge": "マージ",
"RegularMerge": "通常のマージ",
"MergeBranchTooltip": "選択した項目を現在のブランチにマージするためのオプションを表示します(通常のマージ、スカッシュマージ)",
"ConfirmQuit": "本当に終了してよろしいですか?",
"SwitchRepo": "最近のリポジトリをチェックアウト",
@@ -781,7 +774,6 @@
"WorktreesTitle": "ワークツリー",
"WorktreeTitle": "ワークツリー",
"RemoveWorktreePrompt": "ワークツリー '{{.worktreeName}}' を削除してよろしいですか?",
"ForceRemoveWorktreePrompt": "'{{.worktreeName}}' には変更されたファイルまたは追跡されていないファイルが含まれています(正直なところ、両方含まれている可能性があります)。削除してよろしいですか?",
"RemovingWorktree": "ワークツリーを削除中",
"AddingWorktree": "ワークツリーを追加中",
"CantDeleteCurrentWorktree": "現在のワークツリーは削除できません!",
+10 -2
View File
@@ -9,7 +9,6 @@
"StagedChanges": "Staged된 변경 내용",
"StagingTitle": "메인 패널 (Staging)",
"MergingTitle": "메인 패널 (Merging)",
"SquashMergeCommittedTitle": "스쿼시 병합 및 커밋",
"NormalTitle": "메인 패널 (Normal)",
"LogTitle": "로그",
"CommitSummary": "커밋 메시지",
@@ -17,13 +16,19 @@
"CredentialsPassword": "패스워드",
"CredentialsPassphrase": "SSH키의 passphrase 입력",
"CredentialsPIN": "SSH키\u001d의 PIN\u001d을 입력",
"CredentialsToken": "SSH 키를 위한 토큰 입력",
"PassUnameWrong": "패스워드, passphrase 또는 사용자 이름이 잘못되었습니다.",
"Commit": "커밋 변경내용",
"CommitTooltip": "스테이징된 변경 사항 커밋.",
"AmendLastCommit": "마지맛 커밋 수정",
"AmendLastCommitTitle": "마지막 커밋 수정",
"SureToAmend": "마지막 커밋을 수정하시겠습니까? 그런 다음 커밋 패널에서 커밋 메시지를 변경할 수 있습니다.",
"NoCommitToAmend": "Amend 가능한 커밋이 없습니다.",
"CommitChangesWithEditor": "Git 편집기를 사용하여 변경 내용을 커밋합니다.",
"NoBaseCommitsFound": "기본 커밋을 찾을 수 없습니다",
"MultipleBaseCommitsFoundStaged": "여러 개의 기본 커밋을 찾았습니다. (한 번에 더 적은 변경 사항을 스테이징해보세요)",
"MultipleBaseCommitsFoundUnstaged": "여러 개의 기본 커밋을 찾았습니다. (변경 사항 중 일부를 스테이징해보세요)",
"BaseCommitIsAlreadyOnMainBranch": "이 변경 사항의 기본 커밋은 이미 메인 브랜치에 있습니다",
"StatusTitle": "상태",
"GlobalTitle": "글로벌 키 바인딩",
"Execute": "실행",
@@ -46,7 +51,7 @@
"AllFilesDiffCopiedToast": "모든 파일의 변경점을 클립보드에 복사했습니다.",
"FilterStagedFiles": "Staged된 파일만 표시",
"FilterUnstagedFiles": "Stage되지 않은 파일만 표시",
"MergeConflictsTitle": "병합 충돌 내용",
"MergeConflictsTitle": "병합 충돌",
"Checkout": "체크아웃",
"NoChangedFiles": "변경된 파일이 없습니다.",
"SoftReset": "소프트 리셋",
@@ -348,6 +353,9 @@
"Push": "푸시",
"Pull": "업데이트(Pull)",
"OpenFile": "파일 열기",
"CopyToClipboard": "클립보드에 복사",
"CopySelectedTextToClipboard": "선택한 텍스트를 클립보드에 복사",
"DeleteRemoteBranch": "원격 브랜치 삭제",
"RemoveSubmodule": "서브모듈 삭제",
"ResetSubmodule": "서브모듈 Reset",
"AddSubmodule": "서브모듈 추가",
-2
View File
@@ -41,7 +41,6 @@
"ToggleTreeView": "Przełącz widok drzewa plików",
"OpenDiffTool": "Otwórz zewnętrzne narzędzie różnic (git difftool)",
"OpenMergeTool": "Otwórz zewnętrzne narzędzie scalania",
"OpenMergeToolTooltip": "Uruchom `git mergetool`.",
"Refresh": "Odśwież",
"RefreshTooltip": "Odśwież stan git (tj. uruchom `git status`, `git branch`, itp. w tle, aby zaktualizować zawartość paneli). To nie uruchamia `git fetch`.",
"Push": "Wypchnij",
@@ -634,7 +633,6 @@
"WorktreesTitle": "Drzewa pracy",
"WorktreeTitle": "Drzewo pracy",
"RemoveWorktreePrompt": "Czy na pewno chcesz usunąć drzewo pracy '{{.worktreeName}}'?",
"ForceRemoveWorktreePrompt": "'{{.worktreeName}}' zawiera zmodyfikowane lub nieśledzone pliki (szczerze mówiąc, może zawierać oba). Czy na pewno chcesz to usunąć?",
"RemovingWorktree": "Usuwanie drzewa pracy",
"AddingWorktree": "Dodawanie drzewa pracy",
"CantDeleteCurrentWorktree": "Nie możesz usunąć bieżącego drzewa pracy!",
+1 -7
View File
@@ -11,11 +11,6 @@
"StagedChanges": "Alterações preparadas",
"StagingTitle": "Painel Principal (preparação)",
"MergingTitle": "Painel principal (mesclagem)",
"SquashMergeUncommittedTitle": "Mesclar Squash e sair sem commit",
"SquashMergeCommittedTitle": "Mesclar Squash e commit",
"SquashMergeUncommitted": "Mesclar Squash '{{.selectedBranch}}' na árvore de trabalho",
"SquashMergeCommitted": "Mesclar Squash '{{.selectedBranch}}' em '{{.checkedOutBranch}}' como um único commit.",
"RegularMergeTooltip": "Mesclar '{{.selectedBranch}}' em '{{.checkedOutBranch}}'.",
"NormalTitle": "Painel Principal (Normal)",
"LogTitle": "Log",
"CommitSummary": "Sumário do commit",
@@ -50,7 +45,6 @@
"ToggleTreeView": "Alternar exibição de árvore de arquivo",
"OpenDiffTool": "Abrir ferramenta de diff externa (git difftool)",
"OpenMergeTool": "Abrir ferramenta de merge externa",
"OpenMergeToolTooltip": "Execute `git mergetool`.",
"Refresh": "Atualizar",
"RefreshTooltip": "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`.",
"Push": "Empurre (Push)",
@@ -239,7 +233,6 @@
"ExcludeFile": "Adicionar ao .git/info/exclui",
"RefreshFiles": "Atualizar arquivos",
"Merge": "Mesclar",
"RegularMerge": "Mesclagem regular",
"MergeBranchTooltip": "Ver opções para mesclar o item selecionado no branch atual (mesclar regularmente, mesclar squash)",
"ConfirmQuit": "Tem a certeza de que pretende sair?",
"SwitchRepo": "Mudar para um repositório recente",
@@ -296,6 +289,7 @@
"InformationTitle": "Informações",
"SecondaryTitle": "Secundário",
"ReflogCommitsTitle": "Reflog",
"ConflictsResolved": "Todos os conflitos de merge resolvidos. Continuar?",
"Continue": "Continuar",
"UnstagedFilesAfterConflictsResolved": "Os arquivos foram modificados desde que os conflitos foram resolvidos. Auto-encapsulá-los e continuar?",
"RebasingTitle": "Rebase '{{.checkedOutBranch}}'",
-8
View File
@@ -11,11 +11,6 @@
"StagedChanges": "已暂存变更",
"StagingTitle": "正在暂存",
"MergingTitle": "正在合并",
"SquashMergeUncommittedTitle": "压缩合并并保持未提交状态",
"SquashMergeCommittedTitle": "压缩合并,然后提交",
"SquashMergeUncommitted": "将分支 ‘{{.selectedBranch}}’ 压缩合并到工作树中",
"SquashMergeCommitted": "将分支 '{{.selectedBranch}}' 压缩合并为单个提交,到 '{{.checkedOutBranch}}' 分支中。",
"RegularMergeTooltip": "将分支 '{{.selectedBranch}}' 合并到 '{{.checkedOutBranch}}'",
"NormalTitle": "正常",
"LogTitle": "日志",
"CommitSummary": "提交信息",
@@ -50,7 +45,6 @@
"ToggleTreeView": "切换文件树视图",
"OpenDiffTool": "使用外部差异比较工具(git difftool)",
"OpenMergeTool": "打开外部合并工具(git mergetool)",
"OpenMergeToolTooltip": "执行 `git mergetool`.",
"Refresh": "刷新",
"RefreshTooltip": "刷新git状态(即在后台上运行`git status`,`git branch`等命令以更新面板内容) 不会运行`git fetch`",
"Push": "推送",
@@ -242,7 +236,6 @@
"ExcludeFile": "添加到 .git/info/exclude",
"RefreshFiles": "刷新文件",
"Merge": "合并到当前检出的分支",
"RegularMerge": "常规合并",
"MergeBranchTooltip": "Merge selected branch into currently checked out branch.",
"ConfirmQuit": "您确定要退出吗?",
"SwitchRepo": "切换到最近的仓库",
@@ -757,7 +750,6 @@
"WorktreesTitle": "工作区",
"WorktreeTitle": "工作区",
"RemoveWorktreePrompt": "您确定要删除工作树 {{.worktreeName}}' ?",
"ForceRemoveWorktreePrompt": "'{{.worktreeName}}' 包含已修改或未跟踪的文件(说实话,它可能包含两者)。您确定要删除它吗?",
"RemovingWorktree": "正在删除工作树",
"AddingWorktree": "添加工作区",
"CantDeleteCurrentWorktree": "您不能删除当前的工作树!",
-5
View File
@@ -11,8 +11,6 @@
"StagedChanges": "已預存變更",
"StagingTitle": "主面板(預存)",
"MergingTitle": "主面板(合併)",
"SquashMergeUncommittedTitle": "壓縮合併但不提交",
"SquashMergeCommittedTitle": "壓縮合併並提交",
"NormalTitle": "主面板(一般)",
"LogTitle": "版本記錄",
"CommitSummary": "提交摘要",
@@ -36,7 +34,6 @@
"ToggleTreeView": "顯示檔案樹狀視圖",
"OpenDiffTool": "開啟外部差異工具 (git difftool)",
"OpenMergeTool": "開啟外部合併工具",
"OpenMergeToolTooltip": "執行 `git mergetool`。",
"Refresh": "重新整理",
"Push": "推送",
"Pull": "拉取",
@@ -164,7 +161,6 @@
"ExcludeFile": "添加到 .git/info/exclude",
"RefreshFiles": "重新整理檔案",
"Merge": "合併到當前檢出的分支",
"RegularMerge": "一般合併",
"ConfirmQuit": "是否結束?",
"SwitchRepo": "切換到最近使用的版本庫",
"UnsupportedGitService": "不支援的 git 服務",
@@ -539,7 +535,6 @@
"WorktreesTitle": "工作目錄",
"WorktreeTitle": "工作目錄",
"RemoveWorktreePrompt": "是否刪除 {{.worktreeName}} 工作目錄?",
"ForceRemoveWorktreePrompt": "'{{.worktreeName}}' 包括已更動或未追蹤的檔案。是否繼續刪除工作目錄?",
"RemovingWorktree": "正在刪除工作目錄",
"AddingWorktree": "正在建立工作目錄",
"CantDeleteCurrentWorktree": "無法刪除當前工作目錄!",
+17 -3
View File
@@ -170,6 +170,10 @@ func (self *Shell) Commit(message string) *Shell {
return self.RunCommand([]string{"git", "commit", "-m", message})
}
func (self *Shell) CommitInWorktreeOrSubmodule(worktreePath string, message string) *Shell {
return self.RunCommand([]string{"git", "-C", worktreePath, "commit", "-m", message})
}
func (self *Shell) EmptyCommit(message string) *Shell {
return self.RunCommand([]string{"git", "commit", "--allow-empty", "-m", message})
}
@@ -428,11 +432,21 @@ func (self *Shell) AddWorktreeCheckout(base string, path string) *Shell {
})
}
func (self *Shell) AddFileInWorktree(worktreePath string) *Shell {
self.CreateFile(filepath.Join(worktreePath, "content"), "content")
func (self *Shell) AddFileInWorktreeOrSubmodule(worktreePath string, filePath string, content string) *Shell {
self.CreateFile(filepath.Join(worktreePath, filePath), content)
self.RunCommand([]string{
"git", "-C", worktreePath, "add", "content",
"git", "-C", worktreePath, "add", filePath,
})
return self
}
func (self *Shell) UpdateFileInWorktreeOrSubmodule(worktreePath string, filePath string, content string) *Shell {
self.UpdateFile(filepath.Join(worktreePath, filePath), content)
self.RunCommand([]string{
"git", "-C", worktreePath, "add", filePath,
})
return self
@@ -0,0 +1,68 @@
package branch
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var MergeFastForward = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Merge a branch into another using fast-forward merge",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
config.GetUserConfig().Git.LocalBranchSortOrder = "alphabetical"
},
SetupRepo: func(shell *Shell) {
shell.NewBranch("original-branch").
EmptyCommit("one").
NewBranch("branch1").
EmptyCommit("branch1").
Checkout("original-branch").
NewBranchFrom("branch2", "original-branch").
EmptyCommit("branch2").
Checkout("original-branch")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Focus().
Lines(
Contains("original-branch").IsSelected(),
Contains("branch1"),
Contains("branch2"),
).
SelectNextItem().
Press(keys.Branches.MergeIntoCurrentBranch)
t.ExpectPopup().Menu().
Title(Equals("Merge")).
TopLines(
Contains("Regular merge (fast-forward)"),
Contains("Regular merge (with merge commit)"),
).
Select(Contains("Regular merge (fast-forward)")).
Confirm()
t.Views().Commits().
Lines(
Contains("branch1").IsSelected(),
Contains("one"),
)
// Check that branch2 can't be merged using fast-forward
t.Views().Branches().
Focus().
NavigateToLine(Contains("branch2")).
Press(keys.Branches.MergeIntoCurrentBranch)
t.ExpectPopup().Menu().
Title(Equals("Merge")).
TopLines(
Contains("Regular merge (with merge commit)"),
Contains("Regular merge (fast-forward)"),
).
Select(Contains("Regular merge (fast-forward)")).
Confirm()
t.ExpectToast(Contains("Cannot fast-forward 'original-branch' to 'branch2'"))
},
})
@@ -0,0 +1,76 @@
package branch
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var MergeNonFastForward = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Merge a branch into another using non-fast-forward merge",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
config.GetUserConfig().Git.LocalBranchSortOrder = "alphabetical"
},
SetupRepo: func(shell *Shell) {
shell.NewBranch("original-branch").
EmptyCommit("one").
NewBranch("branch1").
EmptyCommit("branch1").
Checkout("original-branch").
NewBranchFrom("branch2", "original-branch").
EmptyCommit("branch2").
Checkout("original-branch")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Focus().
Lines(
Contains("original-branch").IsSelected(),
Contains("branch1"),
Contains("branch2"),
).
SelectNextItem().
Press(keys.Branches.MergeIntoCurrentBranch)
t.ExpectPopup().Menu().
Title(Equals("Merge")).
TopLines(
Contains("Regular merge (fast-forward)"),
Contains("Regular merge (with merge commit)"),
).
Select(Contains("Regular merge (with merge commit)")).
Confirm()
t.Views().Commits().
Lines(
Contains("⏣─╮ Merge branch 'branch1' into original-branch").IsSelected(),
Contains("│ ◯ * branch1"),
Contains("◯─╯ one"),
)
// Check that branch2 shows the non-fast-forward option first
t.Views().Branches().
Focus().
NavigateToLine(Contains("branch2")).
Press(keys.Branches.MergeIntoCurrentBranch)
t.ExpectPopup().Menu().
Title(Equals("Merge")).
TopLines(
Contains("Regular merge (with merge commit)"),
Contains("Regular merge (fast-forward)"),
).
Select(Contains("Regular merge (with merge commit)")).
Confirm()
t.Views().Commits().
Lines(
Contains("⏣─╮ Merge branch 'branch2' into original-branch").IsSelected(),
Contains("│ ◯ * branch2"),
Contains("⏣─│─╮ Merge branch 'branch1' into original-branch"),
Contains("│ │ ◯ * branch1"),
Contains("◯─┴─╯ one"),
)
},
})
@@ -0,0 +1,99 @@
package cherry_pick
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var CherryPickCommitThatBecomesEmpty = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Cherry-pick a commit that becomes empty at the destination",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.
EmptyCommit("base").
CreateFileAndAdd("file1", "change 1\n").
CreateFileAndAdd("file2", "change 2\n").
Commit("two changes in one commit").
NewBranchFrom("branch", "HEAD^").
CreateFileAndAdd("file1", "change 1\n").
Commit("single change").
CreateFileAndAdd("file3", "change 3\n").
Commit("unrelated change").
Checkout("master")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Focus().
Lines(
Contains("master").IsSelected(),
Contains("branch"),
).
SelectNextItem().
PressEnter()
t.Views().SubCommits().
IsFocused().
Lines(
Contains("unrelated change").IsSelected(),
Contains("single change"),
Contains("base"),
).
Press(keys.Universal.RangeSelectDown).
Press(keys.Commits.CherryPickCopy).
Tap(func() {
t.Views().Information().Content(Contains("2 commits copied"))
})
t.Views().Commits().
Focus().
Lines(
Contains("two changes in one commit").IsSelected(),
Contains("base"),
).
Press(keys.Commits.PasteCommits).
Tap(func() {
t.ExpectPopup().Alert().
Title(Equals("Cherry-pick")).
Content(Contains("Are you sure you want to cherry-pick the 2 copied commit(s) onto this branch?")).
Confirm()
})
if t.Git().Version().IsAtLeast(2, 45, 0) {
t.Views().Commits().
Lines(
Contains("unrelated change"),
Contains("single change"),
Contains("two changes in one commit").IsSelected(),
Contains("base"),
).
SelectPreviousItem()
// Cherry-picked commit is empty
t.Views().Main().Content(DoesNotContain("diff --git"))
} else {
t.Views().Commits().
// We have a bug with how the selection is updated in this case; normally you would
// expect the "two changes in one commit" commit to be selected because it was
// selected before pasting, and we try to maintain that selection. This is broken
// for two reasons:
// 1. We increment the selected line index after pasting by the number of pasted
// commits; this is wrong because we skipped the commit that became empty. So
// according to this bug, the "base" commit should be selected.
// 2. We only update the selected line index after pasting if the currently selected
// commit is not a rebase TODO commit, on the assumption that if it is, we are in a
// rebase and the cherry-picked commits end up below the selection. In this case,
// however, we still think we are cherry-picking because the final refresh after the
// CheckMergeOrRebase in CherryPickHelper.Paste is async and hasn't completed yet;
// so the "unrelated change" still has a "pick" action.
//
// Since this only happens for older git versions, we don't bother fixing it.
Lines(
Contains("unrelated change").IsSelected(),
Contains("two changes in one commit"),
Contains("base"),
)
}
},
})
@@ -0,0 +1,92 @@
package cherry_pick
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
"github.com/jesseduffield/lazygit/pkg/integration/tests/shared"
)
var CherryPickConflictsEmptyCommitAfterResolving = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Cherry pick commits with conflicts, resolve them so that the commit becomes empty",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
config.GetUserConfig().Git.LocalBranchSortOrder = "recency"
},
SetupRepo: func(shell *Shell) {
shared.MergeConflictsSetup(shell)
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Focus().
Lines(
Contains("first-change-branch"),
Contains("second-change-branch"),
Contains("original-branch"),
).
SelectNextItem().
PressEnter()
t.Views().SubCommits().
IsFocused().
TopLines(
Contains("second-change-branch unrelated change"),
Contains("second change"),
).
Press(keys.Universal.RangeSelectDown).
Press(keys.Commits.CherryPickCopy)
t.Views().Information().Content(Contains("2 commits copied"))
t.Views().Commits().
Focus().
TopLines(
Contains("first change").IsSelected(),
).
Press(keys.Commits.PasteCommits)
t.ExpectPopup().Alert().
Title(Equals("Cherry-pick")).
Content(Contains("Are you sure you want to cherry-pick the 2 copied commit(s) onto this branch?")).
Confirm()
t.Common().AcknowledgeConflicts()
t.Views().Files().
IsFocused().
SelectedLine(Contains("file")).
Press(keys.Universal.Remove)
t.ExpectPopup().Menu().
Title(Equals("Discard changes")).
Select(Contains("Discard all changes")).
Confirm()
t.Common().ContinueOnConflictsResolved("cherry-pick")
t.Views().Files().IsEmpty()
t.Views().Commits().
Focus().
TopLines(
// We have a bug with how the selection is updated in this case; normally you would
// expect the "first change" commit to be selected because it was selected before
// pasting, and we try to maintain that selection. This is broken for two reasons:
// 1. We increment the selected line index after pasting by the number of pasted
// commits; this is wrong because we skipped the commit that became empty. So
// according to this bug, the "original" commit should be selected.
// 2. We only update the selected line index after pasting if the currently selected
// commit is not a rebase TODO commit, on the assumption that if it is, we are in a
// rebase and the cherry-picked commits end up below the selection. In this case,
// however, we still think we are cherry-picking because the final refresh after the
// CheckMergeOrRebase in CherryPickHelper.Paste is async and hasn't completed yet;
// so the "second-change-branch unrelated change" still has a "pick" action.
//
// We don't bother fixing it for now because it's a pretty niche case, and the
// nature of the problem is only cosmetic.
Contains("second-change-branch unrelated change").IsSelected(),
Contains("first change"),
Contains("original"),
)
},
})
@@ -0,0 +1,54 @@
package commit
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var DiscardSubmoduleChanges = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Discarding changes to a submodule from an old commit.",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.EmptyCommit("Initial commit")
shell.CloneIntoSubmodule("submodule", "submodule")
shell.Commit("Add submodule")
shell.AddFileInWorktreeOrSubmodule("submodule", "file", "content")
shell.CommitInWorktreeOrSubmodule("submodule", "add file in submodule")
shell.GitAdd("submodule")
shell.Commit("Update submodule")
shell.UpdateFileInWorktreeOrSubmodule("submodule", "file", "changed content")
shell.CommitInWorktreeOrSubmodule("submodule", "change file in submodule")
shell.GitAdd("submodule")
shell.Commit("Update submodule again")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("Update submodule again").IsSelected(),
Contains("Update submodule"),
Contains("Add submodule"),
Contains("Initial commit"),
).
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Equals("M submodule").IsSelected(),
).
Press(keys.Universal.Remove)
t.ExpectPopup().Confirmation().
Title(Equals("Discard file changes")).
Content(Contains("Are you sure you want to remove changes to the selected file(s) from this commit?")).
Confirm()
t.Shell().RunCommand([]string{"git", "submodule", "update"})
t.FileSystem().FileContent("submodule/file", Equals("content"))
},
})
@@ -0,0 +1,77 @@
package conflicts
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
"github.com/jesseduffield/lazygit/pkg/integration/tests/shared"
)
func testDataBoth() (original, current, incoming, final string) {
original = `
1
2
3
4
5
6
`
current = `
1a
2
3
4
5a
6
`
incoming = `
1
2
3b
4
5b
6
`
final = `
1a
2
3b
4
5a
5b
6
`
return original, current, incoming, final
}
var MergeFileBoth = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Conflicting file can be resolved to 'union' (both changes) version via merge-file",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
original, current, incoming, _ := testDataBoth()
shared.CreateMergeConflictFileForMergeFileTests(shell, original, current, incoming)
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
_, _, _, expected := testDataBoth()
t.Views().Files().
IsFocused().
Lines(
Contains("file").IsSelected(),
)
t.GlobalPress(keys.Files.OpenMergeOptions)
t.ExpectPopup().Menu().
Title(Equals("Resolve merge conflicts")).
Select(Contains("Use both")). // merge-file --union
Confirm()
t.Common().ContinueOnConflictsResolved("merge")
t.Views().Files().IsEmpty()
t.FileSystem().FileContent("file", Equals(expected))
},
})
@@ -0,0 +1,76 @@
package conflicts
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
"github.com/jesseduffield/lazygit/pkg/integration/tests/shared"
)
func testDataCurrent() (original, current, incoming, final string) {
original = `
1
2
3
4
5
6
`
current = `
1
2
3
4
5a
6
`
incoming = `
1b
2
3
4
5b
6
`
final = `
1b
2
3
4
5a
6
`
return original, current, incoming, final
}
var MergeFileCurrent = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Conflicting file can be resolved to 'ours' (current changes) version via merge-file",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
original, current, incoming, _ := testDataCurrent()
shared.CreateMergeConflictFileForMergeFileTests(shell, original, current, incoming)
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
_, _, _, expected := testDataCurrent()
t.Views().Files().
IsFocused().
Lines(
Contains("file").IsSelected(),
)
t.GlobalPress(keys.Files.OpenMergeOptions)
t.ExpectPopup().Menu().
Title(Equals("Resolve merge conflicts")).
Select(Contains("Use current changes")). // merge-file --ours
Confirm()
t.Common().ContinueOnConflictsResolved("merge")
t.Views().Files().IsEmpty()
t.FileSystem().FileContent("file", Equals(expected))
},
})
@@ -0,0 +1,76 @@
package conflicts
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
"github.com/jesseduffield/lazygit/pkg/integration/tests/shared"
)
func testDataIncoming() (original, current, incoming, final string) {
original = `
1
2
3
4
5
6
`
current = `
1a
2
3
4
5a
6
`
incoming = `
1
2
3
4
5b
6
`
final = `
1a
2
3
4
5b
6
`
return original, current, incoming, final
}
var MergeFileIncoming = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Conflicting file can be resolved to 'theirs' (incoming changes) version via merge-file",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
original, current, incoming, _ := testDataIncoming()
shared.CreateMergeConflictFileForMergeFileTests(shell, original, current, incoming)
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
_, _, _, expected := testDataIncoming()
t.Views().Files().
IsFocused().
Lines(
Contains("file").IsSelected(),
)
t.GlobalPress(keys.Files.OpenMergeOptions)
t.ExpectPopup().Menu().
Title(Equals("Resolve merge conflicts")).
Select(Contains("Use incoming changes")). // merge-file --theirs
Confirm()
t.Common().ContinueOnConflictsResolved("merge")
t.Views().Files().IsEmpty()
t.FileSystem().FileContent("file", Equals(expected))
},
})
@@ -0,0 +1,47 @@
package filter_by_path
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var SelectFilteredFileWhenEnteringCommit = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Filter commits by file path, then enter a commit and ensure the file is selected",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("file1", "")
shell.CreateFileAndAdd("dir/file2", "")
shell.Commit("add files")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.GlobalPress(keys.Universal.FilteringMenu)
t.ExpectPopup().Menu().
Title(Equals("Filtering")).
Select(Contains("Enter path to filter by")).
Confirm()
t.ExpectPopup().Prompt().
Title(Equals("Enter path:")).
Type("dir/file2").
Confirm()
t.Views().Commits().
Focus().
Lines(
Contains("add files").IsSelected(),
).
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Equals("▼ /"),
Equals(" ▼ dir"),
Equals(" A file2").IsSelected(),
Equals(" A file1"),
)
},
})
@@ -0,0 +1,47 @@
package filter_by_path
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var SelectFilteredFileWhenEnteringCommitNoRootItem = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Filter commits by file path, then enter a commit and ensure the file is selected (with the show root item config off)",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
config.GetUserConfig().Gui.ShowRootItemInFileTree = false
},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("file1", "")
shell.CreateFileAndAdd("dir/file2", "")
shell.Commit("add files")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.GlobalPress(keys.Universal.FilteringMenu)
t.ExpectPopup().Menu().
Title(Equals("Filtering")).
Select(Contains("Enter path to filter by")).
Confirm()
t.ExpectPopup().Prompt().
Title(Equals("Enter path:")).
Type("dir/file2").
Confirm()
t.Views().Commits().
Focus().
Lines(
Contains("add files").IsSelected(),
).
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Equals("▼ dir"),
Equals(" A file2").IsSelected(),
Equals("A file1"),
)
},
})

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