Compare commits

..
Author SHA1 Message Date
Jesse Duffield da32b59e11 Fix lazygit's UI becoming unresponsive when a background fetch asks for a passphrase (#4586)
Fix a regression introduced with #4525.

This fixes the problem that background fetching makes lazygit hang when
the fetch request needs to prompt for a passphrase. For Mac users who
use the keychain to store their ssh passphrases, this can happen when
lazygit is running while the machine goes to sleep, because macOS looks
the keychain in that case.
2025-05-23 20:33:22 +10:00
Stefan Haller 2a8ef71e8f Use PTY also with credentialStrategy=FAIL
This is a regression introduced with a199ed1396c; it is important to use a PTY
even with credentialStrategy=FAIL, otherwise the fetch command will spew the
credentials request into the UI and then hang.

This fixes the problem that background fetching makes lazygit hang when the
fetch request needs to prompt for a passphrase. For Mac users who use the
keychain to store their ssh passphrases, this can happen when lazygit is running
while the machine goes to sleep, because macOS looks the keychain in that case.
2025-05-23 11:34:57 +02:00
Stefan Haller 6705428a8d Fix crash when clicking in the status view (#4567)
- **PR Description**

The status view is not supposed to be focusable right now. (This might
change soon, but for now it isn't.) Pressing '0' on it does nothing.

However, clicking on it would still focus it, because the click handler
in MainViewController assumed that when the clicked view doesn't have
the focus, then its "other" view must have, and we just want to toggle
the focus between the two (like when pressing tab). It didn't take the
possibility into account that the current side panel isn't focusable at
all; if it was, then its SwitchToFocusedMainViewController would have
handled the click.

To fix this, check if the "other" view has the focus before handling the
click, and do nothing otherwise.

This also fixes clicking in the main views of the Worktrees or
Submodules tabs, or any other tabs whose main views are not focusable.

Fixes #4566.
2025-05-22 14:41:04 +02:00
Stefan Haller bbd17abc43 Add ContextMgr.NextInStack and use it to access side panel of focused main view
This way we don't have to abuse the parent context mechanism, which isn't meant
for this purpose.
2025-05-22 14:38:40 +02:00
Stefan Haller 12ed50464b Cleanup: pass target context to focusMainView directly
It's a bit silly to pass a window name and then call a function to get the
corresponding context, when we can simply pass the context directly.
2025-05-22 14:38:40 +02:00
Stefan Haller 57991c1da8 Fix crash when clicking in the status view
The click handler of MainViewController was registered as a global handler, so
it was used when a side panel was focused that doesn't have a
SwitchToFocusedMainViewController attached (e.g. Status, Worktrees, or
Submodules). This handler would then push the main view context, but with the
code that is meant only for toggling between the main view pair contexts, i.e.
with taking over the parentContext from the otherContext, which doesn't have one
at that point. This would later lead to a crash in onClick because the
parentContext was nil.

Fix this by splitting the click handler in two, one for when it already has the
focus, and one for toggling from the other view, and make these focus specific.
2025-05-22 14:38:40 +02:00
Stefan Haller eecb59dd8a Fix home and end keys in prompts (#4554)
- **PR Description**

In #4404 we added home/end support to confirmation popups. This broke
handling of the home/end keys in prompts, where they used to work as
alternatives to ctrl-a and ctrl-e.

Here's one way (maybe not the best) to fix this.

A better fix might have been to have separate views for confirmations
and prompts, so that we can have different keybindings for each. That's
a bit more work though.

Fixes #4553.
2025-05-22 08:50:45 +02:00
Stefan Haller b8fcaf01b8 Make home and end keys work in prompts 2025-05-22 08:48:38 +02:00
Stefan Haller c80b0e1910 Bump gocui 2025-05-22 08:48:14 +02:00
Stefan Haller f250dfb8de Fix main view occasionally scrolling to the top on its own when focused (#4573)
Ok, this is a long one. (It took me all weekend to figure out.)

We seem to have a race condition between re-rendering the main view and
the [layout
code](https://github.com/jesseduffield/lazygit/blob/a0ec22c251c67b40f2df9a61226f3bd514789e15/pkg/gui/layout.go#L75)
that checks whether a view has become smaller and therefore needs to
scroll up. When rerendering the main view, we are careful not to
invalidate the ViewLines array, so the code first calls Reset on the
view, and then starts writing lines to the view until we have written
enough for its old scroll position, and only then do we trigger a
redraw. This is all well and good, but theoretically it could happen
that the above-mentioned layout code runs shortly after the task has
started writing lines to the view (say, after the first one has been
written), and then it would see that the view's height is only 1, and
scroll it all the way to the top.

I have never seen this happen, so it seems that we are being lucky and
the race condition is only a theoretical one.

However: we had a very silly and embarrassing bug in the
focused-main-view code that triggers the race condition occasionally.
The bug is that when the main view is focused, we would refresh it
multiple times in quick succession, once for every side panel that is
being refreshed (instead of just once for the side panel that it belongs
to). So the first task would call Reset, start writing lines to the
view, and then the second task would come along, kill the first, call
Reset again, and start writing lines again, and so on. Apparently this
made it more likely for the layout code to run concurrently with this,
and see the view at a moment where it only has one or two lines. I have
seen it scroll to the top on its own a few times, which is very annoying
when you are in the middle of reviewing a longer commit, for instance.

We can fix this by refreshing the main view only for the side panel that
it belongs to, which is what this PR does. I have let lazygit run over
night with a `refresher.refreshInterval` value of 3, and it hadn't
scrolled to the top when I came to look in the morning, which makes me
pretty confident that we're good now.

It would still be nice if we could fix the race condition for real too,
but it's less urgent now, and it also doesn't seem trivial. I guess
instead of writing lines directly to the view we would have to buffer
them first, and only write them to the view when the original scroll
position is reached (with some synchronization, e.g. with a OnUIThread).
There are other complications that make this tricky though, and I have
no plans right now to tackle this.
2025-05-22 08:17:19 +02:00
Stefan Haller f3466e2525 Render the main view only for the side context that it belongs to
Previously we would re-render the focused main view several times during a
refresh, once for every side panel. While this should usually not be noticeable
for users because we are careful to avoid flicker when refreshing the main view,
this would sometimes lead to the main view scrolling up to the top by itself;
see PR description for more details.
2025-05-22 08:15:25 +02:00
Stefan Haller 7a24c5628a Print migration changes to the console when migrating config file (#4548)
- **PR Description**

This might be useful to see in general (users will normally only see it
after they quit lazygit again, but still). But it is especially useful
when writing back the config file fails; some users have their config
file in a read-only location, so we had reports of lazygit no longer
starting up when migration was necessary. #4210 was supposed to improve
this a bit, but it didn't tell users what changes need to be made to the
config file. Now we tell them, and users can then make these changes
manually if they want.

We do this only at startup, when the GUI hasn't started yet. This is
probably good enough, because it is much less likely that writing back a
migrated repo-local config fails because it is not writeable.

Example output:
```
The user config file /Users/stk/Library/Application Support/lazygit/config.yml must be migrated. Attempting to do this automatically.
The following changes were made:

- Renamed 'gui.windowSize' to 'screenMode'
- Changed 'null' to '<disabled>' for keybinding 'keybinding.universal.confirmInEditor'
- Changed 'stream: true' to 'output: log' in custom command

Config file saved successfully to /Users/stk/Library/Application Support/lazygit/config.yml
```

The branch also contains a lot of code cleanups.

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [x] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [ ] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [ ] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
* [ ] Docs have been updated if necessary
* [x] You've read through your own file changes for silly mistakes etc
2025-05-21 08:54:55 +02:00
Stefan Haller cabcd54508 Include migration changes in the error message if we can't log them
This is for the unlikely case that a repo-local config file can't be written
back after migration; in this case we can't log the migration changes to the
console, so include them in the error popup instead.
2025-05-21 08:51:24 +02:00
Stefan Haller a4f43cb275 Log a list of migration changes to the console
This might be useful to see in general (users will normally only see it after
they quit lazygit again, but still). But it is especially useful when writing
back the config file fails for some reason, because users can then make these
changes manually if they want.

We do this only at startup, when the GUI hasn't started yet. This is probably
good enough, because it is much less likely that writing back a migrated
repo-local config fails because it is not writeable.
2025-05-21 08:51:24 +02:00
Stefan Haller caa8c921e6 Make RenameYamlKey return a bool 2025-05-21 08:51:24 +02:00
Stefan Haller 48305c18f7 Cleanup: remove redundant if statement 2025-05-21 08:51:24 +02:00
Stefan Haller ffda51014d Print migration hints only when GUI hasn't started yet
Most migrations happen at startup when loading the global config file, at a time
where the GUI hasn't been initialized yet. We can safely print to the console at
that point. However, it is also possible that repo-local config files need to be
migrated, and this happens when the GUI has already started, at which point we
had better not print anything to stdout; this totally messes up the UI.

In this commit we simply suppress the logging when the GUI is running already.
This is probably good enough, because the logging is mostly useful in the case
that writing back the migrated config file fails, so that users understand
better why lazygit doesn't start up; and this is very unlikely to happen for
repo-local config files, because why would users make them read-only.
2025-05-21 08:51:24 +02:00
Stefan Haller 61822b73f0 Add tests for migrating null keybindings to <disabled> 2025-05-21 08:51:24 +02:00
Stefan Haller df805f3a1a Add tests for migration of renamed keys 2025-05-21 08:51:24 +02:00
Stefan Haller 0249d4c8ab Cleanup: return didChange bool from computeMigratedConfig
It's a bit silly to find out by string comparison whether computeMigratedConfig
did something, when it knows this already and can just return the information.

This doesn't make a huge difference to the production code; the string
comparison isn't very expensive, so this isn't a big deal. However, it makes the
tests clearer; we don't have to bother specifying an expected output string if
the didChange flag is false, and in particular we can get rid of the ugly "This
test intentionally uses non-standard indentation" bit in one of the tests.
2025-05-21 08:51:24 +02:00
Stefan Haller 4f959da9f8 Cleanup: fix formatting of test cases 2025-05-21 08:51:24 +02:00
Stefan Haller da5789ba5f Cleanup: flip conditions for less indentation 2025-05-21 08:51:24 +02:00
Stefan Haller bf13e0bc6a Cleanup: use assert.NoError 2025-05-21 08:51:24 +02:00
Stefan Haller 5686e63af3 Bump generics dependency 2025-05-21 08:51:24 +02:00
Stefan Haller 1fbfefa625 Fix possible crash with auto-forwarding branches (#4565)
- **PR Description**

Fix crash in AutoForwardBranches when the branches slice is empty.

I'm not aware of any real scenario where this can happen, but we have
seen one stack trace where it crashed with an out-of-bounds error in the
range expression below, so there must be a way. And it seems better to
guard against it anyway, rather than assuming it can't happen.

(Hopefully) fixes #4564.
2025-05-21 08:24:58 +02:00
Stefan Haller 01b63a25ba Fix possible crash with auto-forwarding branches
I'm not aware of any real scenario where this can happen, but we have seen one
stack trace where it crashed with an out-of-bounds error in the range expression
below, so there must be a way. And it seems better to guard against it anyway,
rather than assuming it can't happen.
2025-05-21 08:21:38 +02:00
Stefan Haller 43efdded4a Split behavior of rendering allBranchesLogCmd and switching to next cmd (#4574)
- **PR Description**

This is a recreation of
https://github.com/jesseduffield/lazygit/pull/4556.

Compared to that PR description, this has actually solved the problem
that repeated attempts to `1` `a` when `gui.statusPanelView: dashboard`.
Now we only rotate logs on the 2nd `a` press. While this does differ
from current behavior, I believe it is better, and I have more details
in my commit description.

Fixes https://github.com/jesseduffield/lazygit/issues/4469
2025-05-21 08:20:23 +02:00
Chris McDonnell e67bc45d48 Split behavior of rendering allBranchesLogCmd and switching to next cmd
This now allows for leaving the status panel and returning back to the
same log command. Previously any return to the status panel would result
in the next command in the list being shown. Now, you need to press `a`,
with a log command being rendered, to rotate to the next
allBranchesLogCmd.
2025-05-21 08:17:09 +02:00
Stefan Haller a0ec22c251 Clear preserved commit message when entering CommitEditorPanel (#4558)
- **PR Description**

Fixes the problem described in #4557:

I often find myself initiating a commit message with `c`, but then
halfway through typing the message, realize that is is likely to be more
substantial than I thought. I press `<esc>` and then `C` to launch my
editor where I can edit a larger commit body more effectively. I finish
my commit message, close my editor, return to LazyGit and all is well.

Unfortunately, the next time I press `c`, creating a totally new commit,
my partially completed message is still present. It no longer is
relevant, so I have to delete the few words I typed before.
2025-05-15 21:46:19 +02:00
Chris McDonnell 3bd8a923ee Clear preserved commit message when entering CommitEditorPanel 2025-05-15 18:37:56 +02:00
Chris McDonnell 47f1d847f1 Rename OnCommitSuccess to ClearPreservedCommitMessage
In one case it was actually called *before* making a commit (when switching from
the commit message panel to committing in the editor). And clearing the
preserved message is the only thing it does, so name it after what it does
rather than when it's called.
2025-05-15 18:37:56 +02:00
Chris McDonnell 04bacbf9ce Remove the if statement from OnCommitSuccess
Previously it would only clear the message if the panel had been opened with
preserveMessage=true; we don't need this check, because callers know how they
opened the panel, and whether they want to clear the message.
2025-05-15 18:35:55 +02:00
Chris McDonnell 26e58b8def Remove the call to OnCommitSuccess from tag creation
This is no change in behavior because OnCommitSuccess only clears the message
when the commit message panel was opened with preserveMessage=true, which it
isn't in the case of creating a tag.

And this is in fact the desired behavior, because we don't want creating a tag
to interfere with preserving commit messages in any way.
2025-05-15 18:33:40 +02:00
Stefan Haller 5d30409f33 Add an alternate keybinding (default <c-s>) for ConfirmInEditor (#4532)
- **PR Description**

The default binding for ConfirmInEditor is <a-enter>, which has two
problems:
- some terminal emulators don't support it, including the default
terminal on Mac (Terminal.app)
- on Windows it is bound to toggling full-screen

Ideally we would use `<c-enter>` instead (and Command-Enter on Mac), but
neither is possible without https://github.com/gdamore/tcell/issues/671,
so for the time being add an alternate keybinding which works
everywhere.

Show both bindings in the footer of the commit description panel if they
are both non-null. While we're at it, fix the footer for the case where
either or both of the keybindings are set to `<disabled>`.

And finally, change "commit" to "submit" in that footer; we use the same
panel also for creating tags, in which case "commit" is not quite right.
2025-05-11 14:01:27 +02:00
Stefan Haller 450239d5c8 Add an alternate keybinding (default <c-s>) for ConfirmInEditor
The default binding for ConfirmInEditor is <a-enter>, which has two problems:
- some terminal emulators don't support it, including the default terminal on
  Mac (Terminal.app)
- on Windows it is bound to toggling full-screen

Ideally we would use <c-enter> instead (and Command-Enter on Mac), but neither
is possible without https://github.com/gdamore/tcell/issues/671, so for the time
being add an alternate keybinding which works everywhere.

Show both bindings in the footer of the commit description panel if they are
both non-null. While we're at it, fix the footer for the case where either or
both of the keybindings are set to <disabled>.

And finally, change "commit" to "submit" in that footer; we use the same panel
also for creating tags, in which case "commit" is not quite right.
2025-05-11 13:59:22 +02:00
Stefan Haller c3081ef02e Make '>' first jump to the beginning of the branch, and only then to the first commit (#4544)
- **PR Description**

In longer branches there's often the need to jump to the beginning of
the branch, e.g. in order to re-review all commits from the beginning.
There's no easy way to do this in lazygit.

In this PR I overload the "go to bottom" key (`>`) to jump to the first
commit of the current branch if the selection is above it, and only then
jump to the very bottom. I like that we don't need to introduce a new
key binding for this.
2025-05-11 13:59:02 +02:00
Stefan Haller 636b94cf22 Make '>' first jump to the beginning of the branch, and only then to the first commit 2025-05-11 13:55:43 +02:00
Stefan Haller 1a880f916c Remove unused keybinding handler
It's never called, the binding ListController.HandleGotoBottom wins.

The functionality of loading more commits is implemented by GetOnFocus, and this
way it works not only for '>', but also for other navigation keys like page
down.
2025-05-11 13:55:43 +02:00
Stefan Haller a27db87fb6 Add custom patch command "Move patch into new commit before the original commit" (#4552)
- **PR Description**

This is often useful to extract preparatory refactoring commits from a
bigger one. It works best when selecting only entire hunks or even
entire files; if partial hunks are in the patch, you are likely to get
conflicts.

Along the way, fix rewording merge commits. (Not because I find this
super important, but just because I came across the code while working
on this.)

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [x] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [x] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [ ] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
* [ ] Docs have been updated if necessary
* [x] You've read through your own file changes for silly mistakes etc
2025-05-11 13:55:19 +02:00
Stefan Haller f6d13330dd Add custom patch command "Move patch into new commit before the original commit"
This is often useful to extract preparatory refactoring commits from a bigger
one.
2025-05-10 18:57:28 +02:00
Stefan Haller 5321101276 Use 'break' instead of 'edit' for BeginInteractiveRebaseForCommit with merge commit
BeginInteractiveRebaseForCommit is used for all the patch commands, and for
rewording. It works by setting the commit we want to stop at to 'edit'; this
doesn't work for merge commits. This wasn't a problem for the patch commands so
far, because you typically don't use custom patches with merge commits (although
we don't prevent this; maybe we should?).

However, it was a problem when you tried to reword a merge commit; this
previously failed with an error, as the test added in the previous commit
demonstrated.

Also, we want to add a new patch command that has to stop *before* the selected
commit (pull patch to new commit before the original one), and this wouldn't
work for the first commit in a feature branch, because it would have to set the
last commit before that to 'edit', which isn't possible if that's a merge (which
is likely).

To fix all this, use a 'break' before the selected commit if the commit is a
merge. It is important that we only do it in that case and not always, otherwise
we would break the new regression tests that were added a few commits ago.
2025-05-10 18:44:31 +02:00
Stefan Haller 50b2aa5843 Add test for rewording a merge commit
This currently fails with an error.
2025-05-10 18:19:35 +02:00
Stefan Haller b02441cdca Regression test for moving custom patch to new commit from last commit of a stacked branch
I almost broke this during the development of this branch, so add a test to
guard against that. The point here is that the stack remains intact, i.e. the
newly created commit is the last commit of the lower branch, and thus shows the
"*".
2025-05-10 18:19:35 +02:00
Stefan Haller 0c0f95168c Regression test for renaming the last commit of a stacked branch
I almost broke this during the development of this branch, so add a test to
guard against that. The point here is that the stack remains intact, i.e. the
renamed commit is the head of the lower branch, and thus shows the "*".
2025-05-10 18:19:09 +02:00
Stefan Haller 200b34ff28 Shorten commit hash in custom patch menu 2025-05-10 18:15:42 +02:00
Stefan Haller 144565ee03 Cleanup: remove unnecessary setup steps 2025-05-10 18:15:42 +02:00
Stefan Haller ef1da6f704 reword documentation for git.autoForwardBranches (#4545)
- **PR Description**

The wording "lazygit will automatically forward branches to their
upstream after fetching" could be interpreted to mean that lazygit
automatically pushes branches, rather than operating locally. Edited to
make the behavior more clear.

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [ ] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [ ] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [ ] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
* [x] Docs have been updated if necessary
* [x] You've read through your own file changes for silly mistakes etc

<!--
Be sure to name your PR with an imperative e.g. 'Add worktrees view'
see https://github.com/jesseduffield/lazygit/releases/tag/v0.40.0 for
examples
-->
2025-05-09 08:48:02 +02:00
sean simonsen 4e497eef8a reword documentation for git.autoForwardBranches 2025-05-08 16:37:26 -04:00
Stefan Haller e6bd9d0ae6 Migrate deprecated AllBranchesLogCmd to AllBranchesLogCmds (#4345)
- **PR Description**

Fixes https://github.com/jesseduffield/lazygit/issues/3961

Their issue where the default `allBranchesLogCmd` default remains
present is because we just do a `lo.Uniq(lo.WithoutEmpty())` on the
combined list of `allBranchesLogCmd` and `allBranchesLogCmds`.

At the point of this code, it is not possible to tell whether the value
present in `allBranchesLogCmd` is user-provided or not. We have already
merged the config with the default config, so the user not setting
anything, and the user explicitly setting "Yes, I want the default", are
indistinguishable.

Based on that bug report, I'm assuming that users that have not set
anything for `allBranchesLogCmd`, but _have_ set something for
`allBranchesLogCmds`, just want the list they have specified in the
plural version. Some users have likely figured out they can explicitly
set `allBranchesLogCmd: ""` to get this behavior, but most would not.

To achieve this desired behavior, I figure it is easiest to just migrate
all user config to `allBranchesLogCmds`. If they have explicitly set a
non-empty value in `allBranchesLogCmd`, it will be pulled over. If they
set an empty string, it will be excluded.

- **Please check if the PR fulfills these requirements**

* [X] Cheatsheets are up-to-date (run `go generate ./...`)
* [X] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [X] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [ ] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [ ] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
* [X] Docs have been updated if necessary
* [X] You've read through your own file changes for silly mistakes etc
2025-05-07 10:12:36 +02:00
Chris McDonnell 1028f8efb8 Migrate deprecated AllBranchesLogCmd to AllBranchesLogCmds
This solves https://github.com/jesseduffield/lazygit/issues/3961 because
we no longer have a combination of the default and the user defined
list. We just have the user defined list.
2025-05-06 23:33:17 -04:00
Stefan Haller c16c9f982f Clean up utils package (#4538)
- **PR Description**

The utils package is a bit of a heterogeneous bag of miscellaneous
things at different abstraction levels right now; ideally it should only
contain low-level utilities similar to the helpers in utils/slice.go.
Further cleanup is possible here, e.g. something like rebase_todo.go
shouldn't be in this utils package. This PR doesn't address that,
however.

The goal of this PR is just to make it possible to import utils from any
other package. Previously it wasn't possible to import it from config,
because some of the stuff in utils depended on the config package. So
here we move only those things to better places. See the individual
commit messages for details.
2025-05-06 13:07:30 +02:00
Stefan Haller 252dda5220 Move Loader to presentation package
It is very gui specific and shouldn't be in a low-level utils package.
2025-05-06 09:52:53 +02:00
Stefan Haller a6a68778ea Move NewDummyCommon to pkg/common
It's too special for a utils package, and it als makes sense to put it right
next to the thing that it is a dummy for.
2025-05-06 09:43:26 +02:00
Stefan Haller 223978eb82 Clean up the configuration of where a custom command's output goes (#4525)
- **PR Description**

Previously, custom commands had a `stream` field that was overloaded
with two meanings: 1) it made the command's output appear in the log
view, and 2) it used a pty for running the command. It makes sense to be
able to configure these independently, so add a separate `pty` field
(although that's probably rarely needed in practice).

Also, the `stream` and `showOutput` fields were conflicting; they could
be used together, but when setting them both to true, the popup would
always show "empty output", so this doesn't make sense. Combine them
both into a single `output` property with the possible values "none",
"log", or "popup".

We still have some more redundancy here, for example pty is only used
when output is set to "log", and neither output nor pty are used when
subprocess is true. But I stopped there, because I think this is already
an improvement over the previous state.

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [x] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [ ] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [x] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
* [x] Docs have been updated if necessary
* [x] You've read through your own file changes for silly mistakes etc
2025-05-05 17:49:21 +02:00
Stefan Haller e8d365f1aa Add breaking change notice about 'stream: true' being converted to not use a pty any more 2025-05-01 16:09:10 +02:00
Stefan Haller a9f9dee30d Combine customCommand's subprocess, stream, and showOutput fields into a single output enum 2025-05-01 15:46:06 +02:00
Stefan HallerandChris McDonnell 5f4be3bfb7 Add yaml_utils.RemoveKey
Co-authored-by: Chris McDonnell <c.a.mcdonn@gmail.com>
2025-05-01 15:21:37 +02:00
Stefan Haller 02611dad7c Remove unused function UpdateYamlValue 2025-05-01 15:21:37 +02:00
Stefan Haller a199ed1396 Add separate UsePty flag for CmdObj
This decouples StreamOutput from whether a PTY is used. In most cases we just
want to see the output in the log window, but don't have to use a PTY, e.g. for
the bisect commands.

This has the implication that custom commands that are using "stream: true" no
longer use a PTY. In most cases that's probably a good thing, but we're going to
add a separate pty config for those who really wanted this.
2025-05-01 15:21:37 +02:00
Stefan Haller 8cf617b683 Rename getCmdHandler to getCmdHandlerPty, and add getCmdHandlerNonPty
getCmdHandlerNonPty is defined for all platforms.
2025-05-01 15:21:37 +02:00
Stefan Haller a400ef0079 Remove ICmdObj interface
It is only implemented by *CmdObj, so use that directly in client code.
2025-05-01 15:21:37 +02:00
Stefan Haller 4e3d09e9d8 Validate custom commands in sub menus 2025-05-01 15:21:27 +02:00
Stefan Haller e295477951 Add test demonstrating missing validation for custom commands in sub menus
We only validate the commands at top level right now.
2025-04-30 17:41:49 +02:00
Stefan Haller 7137196788 Add comments in tests to explain what they test
Looking at these again, I needed a moment to remember what they do, so make this
more obvious to help future readers.
2025-04-30 17:31:58 +02:00
Stefan Haller 66caa25dcd Reduce memory consumption of graph (pipe sets) (#4498)
- **PR Description**

As a followup to #2533, reduce the memory consumption some more by
optimizing the storage of the pipe sets used for the commit graph.

Some coarse measurements (taken by opening the respective repo, typing
`4 >`, and waiting for all commits to be loaded, and then reading the
Memory column in Activity Monitor):

|               | master  | this PR |
| ------------- | ------- | ------- |
| git           | 2.5 GB  | 1.0 GB  |
| our work repo | 2.3 GB  | 1.3 GB  |
| linux kernel  | 94.8 GB | 38.0 GB |

It's still not really usable for the linux kernel, but for all other
repos that I come across in my daily use of lazygit, it works quite well
now.
2025-04-29 14:59:42 +02:00
Stefan Haller 6ca627d9d7 Store fromPos/toPos as 16-bit ints, and reorder fields for better packing
Hopefully, graphs will never get wider than 32768 characters. (They would get
kind of hard to navigate if they did...)

This reduces the size of the Pipe struct from 48 to 32 bytes, which makes a
significant difference when there are many millions of instances.
2025-04-29 14:57:15 +02:00
Stefan Haller e63abf89db Store TextStyle in Pipe struct as pointer
The instances are held by the AuthorStyle cache.
2025-04-29 14:57:15 +02:00
Stefan Haller 28aa26f30a Store Pipe objects by value in slice of Pipes
This saves some memory at the cost of a slight performance increase (I suppose
reallocting the slice when adding new Pipes is slightly more expensive now).

Performance of the BenchmarkRenderCommitGraph benchmark is 130μs before, 175μs
after. I'm guessing this is still acceptable.
2025-04-29 14:57:15 +02:00
Stefan Haller 18e5b0a650 Simplify equalHashes
Now that all hashes that we deal with are stored in the same pool, we can simply
compare their addresses.
2025-04-29 14:57:15 +02:00
Stefan Haller 13c21365c0 Store fromHash/toHash in Pipe struct as pointers
Now that commit hashes are stored in a pool and referenced by pointer by the
commits, we can use those same pointers in the pipes.
2025-04-29 14:57:15 +02:00
Stefan Haller 8d834e2eab Pack the models.Commit struct a little tighter
Change the base type of some of our enums from int to uint8, and reorder fields
for better packing. This reduces the size of models.Commit from 152 to 132 bytes
on my machine.

This doesn't improve overall memory usage significantly, but why not save a
little bit of memory if it's easy.
2025-04-29 14:57:15 +02:00
Stefan Haller 722cc85508 Store Commit.Parents in the pool too
We need to pass %P instead of %p in the format string of the git log command, so
that the parent hashes have the full length and can be shared with the real
hashes.
2025-04-29 14:57:15 +02:00
Stefan Haller 0f1f455edb Make Commit.Parents a getter for an unexported parents field
This is exactly the same as what we did for Hash earlier. And for the same
reason: we want to turn the parents field into a slice of pointers.
2025-04-29 14:57:15 +02:00
Stefan Haller e27bc15bbd Store Commit.Hash by pointer (kept in a pool of hashes)
This in itself is not an improvement, because hashes are unique (they are shared
between real commits and rebase todos, but there are so few of those that it
doesn't matter). However, it becomes an improvement once we also store parent
hashes in the same pool; but the real motivation for this change is to also
reuse the hash pointers in Pipe objects later in the branch. This will be a big
win because in a merge-heavy git repo there are many more Pipe instances than
commits.
2025-04-29 14:57:15 +02:00
Stefan Haller 1037371a44 Make Commit.Hash a getter for an unexported hash field
This is in preparation for turning the hash into pointer to a string.
2025-04-29 14:57:15 +02:00
Stefan Haller 97aa7a04e6 Rewrite generateCommits to avoid write access to commit.Parents
We want to unexport Parents in a later commit.
2025-04-29 14:57:15 +02:00
Stefan Haller 5844ec6eb2 Cleanup: use IsMerge instead of counting Parents 2025-04-29 14:57:15 +02:00
Stefan Haller cb0c8f39bf Simplify code and fix comment
The "// merge commit" comment was plain wrong, this is any commit that has a
parent, merge or not. The "else if" condition was unnecessary, a plain "else"
would have been enough. But the code in the two blocks was almost identical, so
extract the one thing that was different and unify it.

And while we're at it, use IsFirstCommit() instead of counting parents.
2025-04-29 14:57:15 +02:00
Stefan Haller 5b109b2dd6 Fix confusing variable name
These are not the expected commits.
2025-04-29 14:57:15 +02:00
Stefan Haller 9d202cf9ea Remove unused function 2025-04-29 14:57:15 +02:00
Stefan Haller 4cfa6e0c98 Modernize benchmarks
See https://go.dev/blog/testing-b-loop
2025-04-29 14:57:15 +02:00
Stefan Haller 14eb4c29ca Bump git-todo-parser 2025-04-29 14:57:15 +02:00
Stefan Haller d87bdaf94d fix wording of random tip (#4488)
remove a superfluous word
2025-04-29 14:17:22 +02:00
dawe 80e2e76f60 fix wording of random tip
remove a superfluous word
2025-04-29 14:13:55 +02:00
Stefan Haller 326e7e8716 Strip the '+' and '-' characters when copying parts of a diff to the clipboard (#4519)
- **PR Description**

This makes it easier to copy diff hunks and paste them into code. We
only strip the prefixes if the copied lines are either all '+' or all
'-' (possibly including context lines), otherwise we keep them. We also
keep them when parts of a hunk header is included in the selection; this
is useful for copying a diff hunk and pasting it into a github comment,
for example.

A not-quite-correct edge case is when you select the '--- a/file.txt'
line of a diff header on its own; in this case we copy it as '--
a/file.txt' (same for the '+++' line). This is probably uncommon enough
that it's not worth fixing (it's not trivial to fix because we don't
know that we're in a header).

Fixes #3859.
Fixes #4511.
2025-04-29 11:35:52 +02:00
Stefan Haller 159bbb0825 Strip the '+' and '-' characters when copying parts of a diff to the clipboard
This makes it easier to copy diff hunks and paste them into code. We only strip
the prefixes if the copied lines are either all '+' or all '-' (possibly
including context lines), otherwise we keep them. We also keep them when parts
of a hunk header is included in the selection; this is useful for copying a diff
hunk and pasting it into a github comment, for example.

A not-quite-correct edge case is when you select the '--- a/file.txt' line of a
diff header on its own; in this case we copy it as '-- a/file.txt' (same for the
'+++' line). This is probably uncommon enough that it's not worth fixing (it's
not trivial to fix because we don't know that we're in a header).
2025-04-29 11:33:47 +02:00
Stefan Haller ac9b830bf1 Remove double space between rebase todo and author columns (#4520)
- **PR Description**

In the commits list of an interactive rebase, the gap between the rebase
todo column and the author column was two instead of one. Remove the
extra space to make this gap the same as for all other columns.
2025-04-29 11:32:36 +02:00
Stefan Haller 6402c830d5 Remove space after rebase todo
This is very old; I can only guess that this was added at a time where today's
list column handling wasn't in place yet, so the space was needed to separate
columns. This now causes a gap of two spaces between the rebase todo column and
the author column, which I'm sure wasn't intended. Funny that I never noticed.
2025-04-29 11:30:37 +02:00
Stefan Haller 3ff1be0788 Disallow creating custom patches when the diff context size is 0 (#4522)
- **PR Description**

This is very similar to what we are doing for staging or discarding
hunks in the Files panel (see #4235). Git doesn't allow applying patches
with a zero context size (unless you use the --unidiff-zero option,
which is discouraged).

Fixes #4521.
2025-04-29 11:30:11 +02:00
Stefan Haller 0496e3af50 Disallow creating custom patches when the diff context size is 0
This is very similar to what we are doing for staging or discarding hunks in the
Files panel. Git doesn't allow applying patches with a zero context size (unless
you use the --unidiff-zero option, which is discouraged).
2025-04-29 10:21:18 +02:00
Stefan Haller bb64e3ccb6 Fix nvim-remote commands for fish shell (#4506)
Fish shell does not support "&&" and "||" operators like
POSIX-compatible shells. Instead, it uses a different syntax structure
based on begin/end and if/else.

This caused existing lazygit nvim-remote integration templates to break
when fish was the user's default shell.

This commit adds explicit fish shell detection using the FISH_VERSION
environment variable, and provides fish-compatible templates that
correctly handle launching Neovim or sending remote commands via $NVIM.

Fixes behavior where edits would not open in a new Neovim tab or line
navigation would fail when $NVIM was set.

Ensures smoother editing experience for users running fish shell
(supported since Nov 2012 with FISH_VERSION).
2025-04-27 20:14:40 +02:00
SavingFrame 4d0eaea6fc Fix nvim-remote commands for fish shell
Fish shell does not support "&&" and "||" operators like
POSIX-compatible shells. Instead, it uses a different syntax structure
based on begin/end and if/else.

This caused existing lazygit nvim-remote integration templates to break
when fish was the user's default shell.

This commit adds explicit fish shell detection using the FISH_VERSION
environment variable, and provides fish-compatible templates that
correctly handle launching Neovim or sending remote commands via $NVIM.

Fixes behavior where edits would not open in a new Neovim tab or line
navigation would fail when $NVIM was set.

Ensures smoother editing experience for users running fish shell
(supported since Nov 2012 with FISH_VERSION).
2025-04-27 20:12:11 +02:00
Stefan Haller 8bbfd4b9b9 Fix race condition when starting several command tasks in quick succession (#4518)
- **PR Description**

Fixes #4507, see there for an elaborate description of the problem.
2025-04-27 10:53:34 +02:00
Stefan Haller 3302a090f9 Fix race condition when starting several command tasks in quick succession
See https://github.com/jesseduffield/lazygit/issues/4507 for an elaborate
description.
2025-04-27 10:48:43 +02:00
Jesse Duffield 829aa3c6af Allow closing issues via github actions (#4515)
- **PR Description**

As discussed, this'll allow @ChrisMcD1 to clean up any issues that have
already been resolved.

Basically if you want to close an issue just leave a comment saying
'/close'.

Tested this over in my [OK?](https://github.com/jesseduffield/OK) and
works like a charm.

- **Please check if the PR fulfills these requirements**

* [ ] Cheatsheets are up-to-date (run `go generate ./...`)
* [ ] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [ ] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [ ] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [ ] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
* [ ] Docs have been updated if necessary
* [ ] You've read through your own file changes for silly mistakes etc

<!--
Be sure to name your PR with an imperative e.g. 'Add worktrees view'
see https://github.com/jesseduffield/lazygit/releases/tag/v0.40.0 for
examples
-->
2025-04-26 14:46:16 +10:00
Jesse Duffield b353d2a55a Allow closing issues via github actions
This provides some basic admin permissions without needing to go all the
way up to a collaborator.
2025-04-26 14:42:21 +10:00
Jesse Duffield 2b43f5b592 Add Debian installation instructions alongside Ubuntu (#4501)
**PR Description**

This PR updates the README to include installation instructions for
Debian.

* Renames the "Ubuntu" section to "Debian and Ubuntu"
* Adds the `sudo apt install lazygit` for Debian Trixie, Sid, and Ubuntu
25.10 and later
* Keeps the manual installation method for Debian Bookworm, Ubuntu 25.04
and earlier

This helps Debian and Ubuntu users install lazygit using either apt or
manual download, depending on the distribution version.

**Affected files**

* Documentation
* `README.md`: Renames the "Ubuntu" section to "Debian and Ubuntu" and
adds Debian installation instructions

**Requirements fulfilled by this PR**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [x] Docs have been updated if necessary
* [x] You've read through your own file changes for silly mistakes etc
2025-04-23 19:51:02 +10:00
Jongmin Kim 9e90d5ee62 Add Debian installation instructions alongside Ubuntu
Signed-off-by: Jongmin Kim <jmkim@debian.org>
2025-04-23 19:47:52 +10:00
Stefan Haller 7c2063c7a9 Allow changing diff context size and rename threshold when main view is focused (#4505)
- **PR Description**

Allow pressing `{`, `}`, `(`, and `)` to change the diff context size
and rename threshold when the main view is focused. This was forgotten
in #4429.
2025-04-23 11:26:21 +02:00
Stefan Haller da24deb881 Allow changing diff context size and rename threshold when main view is focused 2025-04-23 08:38:46 +02:00
Stefan Haller aa68885215 Add new command "Move commits to new branch" (#3876)
- **PR Description**

Add a new command "Move commits to new branch" (bound to `N` by
default), which is useful if you have just started some new work, you
already made some commits, and then you realize that you forgot to
create a new branch first, accidentally making those commits on main or
whichever other feature branch you happened to be on.

If you made those commits on main, you are prompted for a branch name
for the new branch, and then it creates the new branch right there and
hard-resets main to where it was. If you made them on another feature
branch though, you are first given the choice whether you want to create
the new branch from main, or keep it stacked on that other feature
branch. Then it prompts you for the name and proceeds as above.

Inspired by Magit's
[magit-branch-spinoff](https://magit.vc/manual/magit/Branch-Commands.html#index-b-s)
command.

The conditions under which the command is available are rather
restrictive: the current branch must have an upstream, it must not be
behind its upstream, but it must be ahead of it (otherwise there
wouldn't be any commits to move, and you might as well just create a new
branch normally).
2025-04-21 18:14:54 +02:00
Stefan Haller f65166ae91 Update config docs and schema 2025-04-21 18:12:50 +02:00
Stefan Haller 30868eead8 Add new command "Move commits to new branch" 2025-04-21 18:12:50 +02:00
Stefan Haller 4bf11eae4b Add free-standing function IsWorkingTreeDirty
The long story: I want to call this function from RefsHelper; however, I can't
make WorkingTreeHelper a field of RefsHelper because RefsHelper is already a
field in WorkingTreeHelper, so that would be a circular dependency.

The shorter story: there's really little reason to have to instantiate a helper
object in order to call a simple function like this. Long term I would like to
get to a state where a lot more of these helper functions are free-standing, and
you pass in the data they need.

While at it, simplify the implementation of AnyStagedFiles and AnyTrackedFiles
to one-liners.
2025-04-21 18:12:50 +02:00
Stefan Haller 9d88d6a44e Remove MergeAndRebaseHelper's dependency on RefsHelper
We want to make MergeAndRebaseHelper a dependency of RefsHelper instead.
2025-04-21 18:12:50 +02:00
Stefan Haller 483195110a Use Model().Branches[0] instead of refsHelper.GetCheckedOutRef() in MergeAndRebaseHelper
It's the same, really, except that GetCheckedOutRef() does a check if any
branches exist and returns nil if not. Since we are accessing the returned
branch unconditionally without checking for nil, it seems this check is not
needed here. (The functions we are touching here are called from handlers that
are guarded with itemSelected or singleItemSelected, so we know that at least
one branch exists.)

The goal is to get rid of the dependency to refsHelper.
2025-04-21 18:12:50 +02:00
Stefan Haller ecc70f4764 Cleanup: remove unnecessary indirection 2025-04-21 18:12:50 +02:00
Stefan Haller bd1e34b39d Auto-forward main branches after fetching (#4493)
- **PR Description**

Add a new user config for auto-forwarding branches after fetching; by
default this is set to "onlyMainBranches", but it can be set to
"allBranches" to extend it to feature branches too, or to "none" to
disable it.

This is used both when fetching manually by pressing `f` in the files
panel, and for automatic background fetching.
2025-04-21 18:08:36 +02:00
Stefan Haller 7495854243 Add a breaking changes hint for the new auto-forward behavior 2025-04-21 18:06:09 +02:00
Stefan Haller eaaa937315 Add config for auto-forwarding branches after fetching 2025-04-21 18:06:09 +02:00
Stefan Haller 2174762315 Inline fetchAux into call site
It's only called from this one place, and the call is a one-liner, so it makes
more sense to inline the code there.
2025-04-21 18:06:09 +02:00
Stefan Haller 3577808a14 Remove double Refresh
fetchAux already calls Refresh (and with a more targeted scope too), no need to
call it again here.
2025-04-21 18:06:09 +02:00
Stefan Haller e4362eea60 Focus the main view (#4429)
- **PR Description**

This adds a new `0` keybinding to the side panels that focuses whatever
main view is currently displayed, with the goal of making it easier to
scroll the main view (using the normal navigation keys `,` `.` `<` `>`),
and being able to search the main view using `/`.

Alternatively to pressing `0` you can also click the main view to focus
it. Note that previously it was possible to go directly to the staging
panel by clicking in the main view when a file was selected; this now
takes a double click, because the first click just focuses the main
view, but you can go to staging from there by clicking again.

I'm reasonably happy with the overall behavior, but it takes some
getting used to, so we'll want to test this for a while to see if it
doesn't make the focus handling too confusing.

Fixes #3988.
2025-04-21 18:05:38 +02:00
Stefan Haller 4e21a096b9 Re-render focused main view on refresh if needed 2025-04-21 18:03:24 +02:00
Stefan Haller 98e4cb733f Allow clicking in the respective other main view to switch focus to it 2025-04-21 18:03:24 +02:00
Stefan Haller fbb8b2e17e Allow clicking in focused main view to go to staging
Only works if a file is selected.
2025-04-21 18:03:24 +02:00
Stefan Haller acfa024915 Allow clicking in main view to focus it 2025-04-21 18:03:24 +02:00
Stefan Haller 338064ac2c Read all lines from task when starting to search 2025-04-21 18:03:24 +02:00
Stefan Haller ec51efc82c Make the main view searchable 2025-04-21 18:03:24 +02:00
Stefan Haller 2b399a3c36 Read all lines from task when jumping to bottom 2025-04-21 18:03:19 +02:00
Stefan Haller 60887eddd0 Use readLines channel only for command tasks
And only while the task is running.

This avoids accumulating lots of blocked goroutines when scrolling a view down
more than 1024 times (the capacity of the readLines channel).
2025-04-21 18:03:19 +02:00
Stefan Haller 7b17f33e9e Add navigation keybindings to focused main view 2025-04-21 18:03:19 +02:00
Stefan Haller fa35f661d9 Update keybindings, config docs, and schema 2025-04-21 18:03:19 +02:00
Stefan Haller 1a93b2324b Allow focussing the main view
In this commit this is only possible by pressing '0' in a side panel; we'll add
mouse clicking later in the branch.

Also, you can't really do anything in the focused view except press escape to
leave it again. We'll add some more functionality in a following commit.
2025-04-21 18:03:19 +02:00
Stefan Haller b7b7c65999 Add class MainContext
In this state of the code it isn't worth much because it's not any more than a
SimpleContext, but we'll add things to it later in the branch.
2025-04-21 18:03:18 +02:00
Stefan Haller 2a2705dcc3 Always render diffs to the main/secondary context pair, even for files
Previously we would render the diff for a directory to the main/secondary pair,
but a diff for a file to the staging/stagingSecondary pair. (And similar for
commit files: main/secondary for directories, but
patchBuilding/patchBuildingSecondary for files.)

I always found this confusing and couldn't really understand why we are doing
this; but now it gets in my way because I want to attach a controller to
main/secondary so that they can be focused. So change it to always use the main
context pair for everything we render from a side panel.
2025-04-21 18:03:18 +02:00
Stefan Haller 7b96615792 Add IGuiCommon.GetViewBufferManagerForView
So that we don't have to pass the map to controllers.
2025-04-21 18:03:18 +02:00
Stefan Haller b97dd6bc3f Remove utils.Clamp, use lo.Clamp instead 2025-04-21 18:03:18 +02:00
Stefan Haller 6c0ee7df2b Bump gocui 2025-04-21 18:03:11 +02:00
Stefan Haller 548d486aee Section headers for rebase todos and actual commits (#4463)
- **PR Description**

During interactive rebase, or when stopping with conflicts in a
cherry-pick or revert, show section headers for the todo items and the
actual commits. This makes it much clearer where the current head commit
is. Get rid of the "<-- YOU ARE HERE" marker that we would previously
show; it is no longer needed. (We still show the "<-- CONFLICT" marker
though.)

For example:
```
╭─[4]─Commits - Reflog───────────────────────────╮
│--- Pending rebase todos ---                    │
│6562f903 pick  CI commit 03                     │
│--- Commits ---                                 │
│56a04448       CI ◯ commit 02                   │
│6d0c0e41       CI ◯ commit 01                   │
```
2025-04-20 16:09:40 +02:00
Stefan Haller 74054c9d70 Remove the "YOU ARE HERE" marker
Now that we have sections, it is no longer needed.

Keep the "<-- CONFLICTS" marker though.
2025-04-20 16:06:15 +02:00
Stefan Haller 6cbc1e0ace Add section headers for rebase todos, cherry-picks, reverts, and actual commits 2025-04-20 16:06:15 +02:00
Stefan Haller 98c19feccf Add new commit status StatusCherryPickingOrReverting
This is needed because we want to show different section headers for rebase
todos and cherry-pick/revert todos.
2025-04-20 16:06:15 +02:00
Stefan Haller e90a3dc666 Fix view selection running out of sync with list selection when view isn't focused
When rerendering a view at the end of a refresh, we call HandleFocus only if the
view has the focus. This is so that we rerender the main view for the new
selection.

What was missing here is to update the view selection from the list selection if
the view doesn't have the focus, so that the selection is painted properly.
Normally this is not relevant because you don't see the selection if another
side panel has the focus; however, you do see it as an inactive selection when
e.g. a popup is shown, in which case it does matter.

This will become more important when we introduce section headers for commits,
because in that case the view selection needs to change when the working copy
state changes from normal to rebasing or vice versa, even if the list selection
stays the same.

The changed test submodule/reset.go shows how this was wrong before: when
entering the submodule again after resetting, there is a refresh which keeps the
same branch selected as before (master); however, since the branches panel is
not focused, the view didn't notice and kept thinking that the detached head is
selected (which it isn't, you can tell by running the test in sandbox mode and
focusing the branches panel at the end: you'll see that master is selected). So
the change in this commit fixes that.
2025-04-20 16:06:15 +02:00
Stefan Haller 0dab37a631 Allow reverting a range of commits (#4444)
- **PR Description**

This is part four of a four part series of PRs that improve the
cherry-pick and revert experience.

With this PR we support reverting a range selection of commits.

Fixes #3272
2025-04-20 16:05:27 +02:00
Stefan Haller 945b023eb5 Support range selection for reverting commits 2025-04-20 16:03:06 +02:00
Stefan Haller 1c91999f61 Inline afterRevertCommit
It's only called from one place now, so there's no reason not to inline it.
2025-04-20 16:03:06 +02:00
Stefan Haller f14a3cdd46 Remove the "Select parent commit" prompt when reverting a merge commit
In pretty much 100% of the cases, you want to use -m1, so spare users the
complexity of a confusing prompt.

See
https://public-inbox.org/git/e60a8b1a-98c8-4ac7-b966-ff9635bb781d@haller-berlin.de/
for some discussion.
2025-04-20 16:03:06 +02:00
Stefan Haller c13c6356e3 Use "git cherry-pick" for implementing copy/paste of commits (#4443)
- **PR Description**

This is part three of a four part series of PRs that improve the
cherry-pick and revert experience.

With this PR we reimplement copy/paste of commits to use `git
cherry-pick` under the hood. We do this because
- it's closer to what you would do on the command line
- it simplifies the code a bit
- it allows us to support cherry-picking merge commits.

Fixes #1374
Fixes #3317
2025-04-20 16:02:51 +02:00
Stefan Haller 108054efc6 Allow cherry-picking merge commits
Now that we use git cherry-pick to implement it, there's no reason not to.
2025-04-20 15:59:48 +02:00
Stefan Haller 27825eba9e Remove unused cherry-picking code in daemon 2025-04-20 15:59:48 +02:00
Stefan Haller 4b35434eba Use "git cherry-pick" for implementing copy/paste of commits
We do this because
- it's closer to what you would do on the command line
- it simplifies the code a bit
- it will allow us to support cherry-picking merge commits.
2025-04-20 15:59:48 +02:00
Stefan Haller 05b3ae9524 Reference original commits in CherryPicking mode instead of synthesizing new ones
Previously we would create new Commit objects to store in the CherryPicking mode
which only contained a name and hash, all other fields were unset. I'm not sure
why we did this; it's easier to just reference the original commits. Later on
this branch we will need this because we need to determine whether a copied
commit was a merge commit (by looking at its Parents field).
2025-04-20 15:59:48 +02:00
Stefan Haller b7d01d67a6 Show todo items for pending cherry-picks and reverts (#4442)
- **PR Description**

This is part two of a four part series of PRs that improve the
cherry-pick and revert experience.

With this PR we include pending cherry-picks and reverts in the commit
list (like rebase todos) when a cherry-pick or revert stops with
conflicts; also, we show the conflicting item in the list like we do
with conflicting rebase todos.

As with the previous PR, this is not really very useful yet because you
can't revert a range of commits, and we don't use git cherry-pick for
copy/paste. Both of these will change in later PRs in this series, so
again this is preparation for that.
2025-04-20 15:58:54 +02:00
Stefan Haller b350876f84 Disallow any changes to commits or todos when cherry-picking or reverting
We treat the .git/sequencer/todo file as read-only. Technically it seems it
would be possible to treat it as modifiable in the same way as
.git/rebase-merge/git-rebase-todo, effectively turning a cherry-pick or revert
that stops at a conflict into an interactive rebase; however, git itself doesn't
allow this (there is no "git cherry-pick --edit-todo"), so it seems safer not to
rely on it.

Theoretically it would be possible to allow modifying the rebase todos when a
cherry-pick or revert conflicts in the middle of a rebase. However, it would
introduce a bit of complexity to support this, as we would have to be able to
distinguish between rebasing todos and cherry-picking/reverting todos, which we
currently can't; it could also be a bit error-prone as far as edge cases are
concerned. And it's really a pretty uncommon situation, so it doesn't seem worth
it, and we just forbid all modifications to todos whenever we are cherry-picking
or reverting.
2025-04-20 15:55:44 +02:00
Stefan Haller 5ba8d42c80 Fix the bug described in the previous commit
What happens here is that when stopping on an "edit" todo entry, we rely on the
assumption that if the .git/rebase-merge/amend file exists, the command was
successful, and if it doesn't, there was a conflict. The problem is that when
you stop on an edit command, and then run a multi-commit cherry-pick or rebase,
this will delete the amend file. You may or may not consider this a bug in git;
to work around it, we also check the existence of the rebase-merge/message file,
which will be deleted as well by the cherry-pick or revert.
2025-04-20 15:55:44 +02:00
Stefan Haller 9b88052a4e Add test demonstrating problem with revert (or cherry-pick) during a rebase
This problem can't happen inside of lazygit itself right now, but this will
change in the future. It will only happen when you stopped in an interactive
rebase on an "edit" entry, and then you perform a revert or cherry-pick
consisting of more than one commit; in this situation lazygit will show a
conflict although there is none.

This is not possible with lazygit yet, as we don't support range-select for
reverting, and we don't use `git cherry-pick` for cherry-picking. Both will
change in the future, so it's good to fix this bug.
2025-04-20 15:55:44 +02:00
Stefan Haller 6b6d881624 Add test to check that an "edit" entry correctly shows a conflict
This works correctly, we are only adding this as a regression test to verify
that the change later in this branch doesn't break it.
2025-04-20 15:55:44 +02:00
Stefan Haller bb4d03db1f Show todos (and conflicting commit) for cherry-pick and revert 2025-04-20 15:55:44 +02:00
Stefan Haller 687bae48ed Simplify the MergeRebasingCommits call in GetCommits (slightly)
MergeRebasingCommits already merges the rebasing commits into the commits slice
that is passed in, so it doesn't make sense to append the result to commits
again. It isn't a problem, but only because commits is always empty.
2025-04-20 15:55:44 +02:00
Stefan Haller ff465e2581 Show original todo action instead of "conflict", and show <-- CONFLICT instead
It is useful to see if the conflicted commit was a "pick" or an "edit". What's
more, we're about to add support for showing cherry-picks and reverts, and
seeing that a conflicted commit was a revert is important because its diff is
backwards compared to the diff of the conflicting files in the Files panel.
2025-04-20 15:55:44 +02:00
Stefan Haller 9c8f987934 Use commit.IsTODO instead of comparing Status against models.StatusRebasing
This is equivalent in the current state of the code, but it will no longer be
after the next commit, because we will introduce a new status value
StatusConflicted. And in a later PR we might add yet another value
StatusCherryPicking to distinguish rebase todos from cherry-pick todos; using
commit.IsTODO is a safer way to check whether a commit is any of these.
2025-04-20 15:55:44 +02:00
Stefan Haller d84ee606e8 Remove unused enum entry StatusSelected 2025-04-20 15:55:44 +02:00
Stefan Haller 3dc045b69c Continue/abort a conflicted cherry-pick or revert (#4441)
- **PR Description**

This is part one of a four part series of PRs that improve the
cherry-pick and revert experience.

With this first PR we make it possible to continue or abort a
cherry-pick or revert operation, in the same way you can continue or
abort a rebase or merge. Currently this is only relevant for revert,
because lazygit doesn't use git cherry-pick for copying/pasting commits.
This will change in a later PR in this series though, so here we are
already preparing for that.

Fixes #1807
2025-04-20 15:55:11 +02:00
Stefan Haller 90db796f42 Add DisabledReason for rebase options when not rebasing or merging 2025-04-20 15:53:17 +02:00
Stefan Haller 3e5024480d Check for conflicts after reverting a commit
This way we get the usual menu for viewing the conflicts or aborting, like for
rebases.
2025-04-20 15:53:17 +02:00
Stefan Haller 4b3262ab3e Add test for reverting a commit that conflicts
This works already (except that the "you are here" marker is not right yet, but
that's an issue for another PR), just add a test that verifies it.
2025-04-20 15:53:17 +02:00
Stefan Haller 362678e2ef Mention which command is continued in PromptToContinueRebase
When you are in the middle of a rebase, and you cherry-pick a commit which
conflicts, it helps to be clear on whether you are prompted to continue the
cherry-pick or the rebase.
2025-04-20 15:53:17 +02:00
Stefan Haller 542525743c Make WorkingTreeState a struct, and add cherry-picking and reverting states 2025-04-20 15:53:17 +02:00
Stefan Haller 8af8f7754b Move types/enums/enums.go to working_tree_state.go
It looks like enums.go was supposed to be file that collects a bunch of enums,
but actually there's only one in there, and since it has methods, it deserves to
be in a file of its own, named after the type.
2025-04-20 15:53:17 +02:00
Stefan Haller e1eb95b2b3 Add test for a special situation in rebases involving empty commits
The situation is that you perform a rebase, and one of the commits becomes empty
during the rebase, in a way that couldn't be predicted before the rebase
started. To explain: git rebase has some logic where it immediately discards
commits if it can tell that they already exist in the branch being rebased onto;
it does this by looking at the patch ids of the commits to work out which
commits already exist upstream. But for those commits that become empty even
though there isn't a corresponding commit upstream, git stops with an error, and
lazygit detects this (in CheckMergeOrRebaseWithRefreshOptions) and automatically
continues the rebase.

This all works fine; I'm adding this test because I almost broke this during
development of this branch, so I'm adding it to guard against regressions.
2025-04-20 15:53:17 +02:00
Stefan Haller b210b4363d Centralize logic regarding WorkingTreeState close to its definition 2025-04-20 15:53:17 +02:00
Stefan Haller cd36e95a82 Rename RebaseMode to WorkingTreeState
We're about to add more possible values (reverting and cherry-picking), so
working tree state seems like a more suitable name.
2025-04-20 15:53:17 +02:00
Stefan Haller 1a73697546 Simplify the RebaseMode enum
- Remove REBASE_MODE_NORMAL. It is not the "normal" mode anyway, rather a legacy
mode; we have removed support for it in eb0f7e3d02, so there's no point in
representing it in the enum.
- Remove distinction between REBASE_MODE_REBASING and REBASE_MODE_INTERACTIVE;
these are the same now.
- Rename StatusCommands.IsInInteractiveRebase to IsInRebase.
- Remove StatusCommands.RebaseMode; use StatusCommands.IsInRebase instead.
2025-04-20 15:53:17 +02:00
Stefan Haller 37f835244d Use WorkingTreeState instead of RebaseMode in CommitLoader
We want to get rid of RebaseMode, and using the more general WorkingTreeState
will later allow us to also show cherry-pick or revert todos.
2025-04-20 15:53:17 +02:00
Stefan Haller 94fc4d7eb4 Remove rebaseMode field from TestGetCommits scenario
All test cases set it to enums.REBASE_MODE_NONE, so we can simplify things a
little bit by hard-coding that. This makes the changes in the following commits
a little easier.
2025-04-20 15:53:17 +02:00
Stefan Haller 682e54222c Don't wait for debugger in daemon mode
When debugging an integration test that involves some behind-the-scenes
rebasing, the daemon lazygit would also wait for a debugger to attach. This is
very confusing because the test seems to hang; once you figured out what's going
on, it's inconvenient because you need to attach a debugger to the daemon every
time you debug the test.

Now, it would sometimes be useful to be able to debug the daemon itself (whether
inside an integration test, or during normal usage), and I have often wished to
be able to do that. We might introduce an additional env var (and command-line
option?) to enable this; but that's out of scope here.
2025-04-20 15:53:17 +02:00
Stefan Haller 89ba3f2b1a Fix truncation of branches when scrolling branches panel to the left (#4483)
- **PR Description**

Truncation of long branch names didn't take the horizontal scroll
position into account; when scrolling the panel to the left using `H`,
the long names were truncated way before the right edge of the view.
2025-04-20 15:52:20 +02:00
Stefan Haller fa9d75835c Rerender views if necessary when scrolling horizontally
If the context says it wants to rerender when its width changes, we must also
rerender when the horizontal scroll position changes.
2025-04-20 15:50:38 +02:00
Stefan Haller 9e3e1a7b3a Fix truncating branches to the right edge of the view when scrolled to the left
When scrolling the view to the left, the available width becomes the width of
the view plus the scroll position.
2025-04-20 15:50:38 +02:00
Jesse Duffield 0236682882 README.md: Update Sponsors (#4281)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action
2025-04-20 18:24:21 +10:00
github-actions[bot] 058699cb71 README.md: Update Sponsors 2025-04-14 08:28:01 +00:00
Jesse Duffield 5f809809dd Skip date check when release worfklow is manually invoked (#4484)
- **PR Description**

I just tried creating a release manually and hit an error because it
wasn't the first saturday of the month.

- **Please check if the PR fulfills these requirements**

* [ ] Cheatsheets are up-to-date (run `go generate ./...`)
* [ ] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [ ] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [ ] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [ ] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
* [ ] Docs have been updated if necessary
* [ ] You've read through your own file changes for silly mistakes etc

<!--
Be sure to name your PR with an imperative e.g. 'Add worktrees view'
see https://github.com/jesseduffield/lazygit/releases/tag/v0.40.0 for
examples
-->
2025-04-14 18:27:49 +10:00
Jesse Duffield f7eb9113f3 Skip date check when release worfklow is manually invoked 2025-04-14 18:24:45 +10:00
Jesse Duffield d68c116387 Escape special characters in filenames when git-ignoring files (#4475)
- **PR Description**

Some characters in file names need to be escaped so that they work
correctly in .gitignore files. These include `#` and `!` (only at the
beginning of a filename), as well as `[`, `]`, and `*` anywhere in the
name.

Fixes #4075
Fixes #4445

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [x] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [ ] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [ ] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
* [ ] Docs have been updated if necessary
* [x] You've read through your own file changes for silly mistakes etc
2025-04-13 18:31:23 +10:00
Stefan Haller b0ab6529c1 Escape special characters when git-ignoring files 2025-04-13 18:25:24 +10:00
Stefan Haller 41f89d86f0 Add test that shows problems with git-ignoring files with special characters
For #, !, [, and ], the problem is that it doesn't ignore the file because the
special characters need to be quoted. For *, the problem is that it ignores too
much (it also hides the abc_def file because the * is treated as a glob).
2025-04-13 18:25:24 +10:00
Jesse Duffield d70a405169 Fix crash with drag selecting and staging (#4480)
- **PR Description**

Fix a crash in the staging view when clicking below the end of the diff,
dragging upwards into the diff to select a range, and pressing space to
stage.

Fixes #4479.

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [ ] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [ ] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [ ] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
* [ ] Docs have been updated if necessary
* [x] You've read through your own file changes for silly mistakes etc
2025-04-13 18:25:09 +10:00
Stefan Haller 825e5c23c1 Fix crash when dragging from below the end of the diff upwards and then staging the range
We need to clamp the range start index of a selection range in the same way as
we clamp the selection index.
2025-04-11 10:14:46 +02:00
Stefan Haller a3ae1c8e4d Refactor: extract function clampLineIdx
And reimplement using lo.Clamp instead of comparing manually.
2025-04-11 10:14:46 +02:00
Stefan Haller 61636d820c Update translation files from Crowdin (#4473)
- **PR Description**

Update translation files from Crowdin. Some texts had to be deleted
because they are no longer accurate (e.g. the minimum git version
error), these need to be retranslated.
2025-04-10 11:42:41 +02:00
Stefan Haller 204f6c7edb Update translation files from crowdin 2025-04-10 11:40:55 +02:00
Stefan Haller 1106b67f20 fix: update vscode color to logo color (#4459)
The vscode folder color appears to be using the visual studio purple
color which is different from the vscode blue color. This updates it
using a color picker on the inner part of the logo.
2025-04-10 09:58:08 +02:00
Peter Cardenas 93899b82fa fix: update vscode color to logo color 2025-04-10 09:56:19 +02:00
Stefan Haller 12fa50a61c Add a command to select all commits of the current branch (#4448)
- **PR Description**

This is useful for seeing the combined diff of all commits of a branch.

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [x] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [x] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [ ] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
* [ ] Docs have been updated if necessary
* [x] You've read through your own file changes for silly mistakes etc
2025-04-10 09:05:55 +02:00
Stefan Haller 80b5e21bd5 Add a command to select all commits of the current branch
This is useful for seeing the combined diff of all commits of a branch.
2025-04-10 09:02:09 +02:00
Stefan Haller 7bccf848af Show "(hooks disabled)" in title bar of commit message editor (#4467)
- **PR Description**

It is shown either when committing with `w`, or when typing the skipHooks prefix
if there is one. This should hopefully make it clearer when the hooks are run
and when they are not.

```
╭─Commit summary───────────────────(hooks disabled)─ 7 ─────╮
│WIP foo                                                    │
╰───────────────────────────────────────────────────────────╯
```

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [ ] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [x] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [ ] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
* [ ] Docs have been updated if necessary
* [x] You've read through your own file changes for silly mistakes etc
2025-04-10 08:59:55 +02:00
Stefan Haller b3bffbec4a Show "hooks disabled" in title bar of commit message editor
It is shown either when committing with `w`, or when typing the skipHooks prefix
if there is one. This should hopefully make it clearer when the hooks are run
and when they are not.
2025-04-10 08:57:32 +02:00
Stefan Haller 2ee80d7150 Cleanup: don't render the commit length when typing in the description
Only the commit message has an effect on this value, the description doesn't.
2025-04-10 08:55:13 +02:00
Stefan Haller a24189ba63 Prefill the commit subject with the skipHook prefix when pressing w (#4456)
We removed prefilling the skipHook prefix in b102646b20 (#4374) with
the intention of making it clearer that using the prefix in normal
commits and typing `w` to skip hooks are now two independent features.

It turns out that some people liked it with prefilling the prefix and
perceive it as a regression, so put it back in.

But only if we don't have a preserved message; this is an important use
case, when you try to make a normal commit, the hook fails, and then you
want to make the same commit with skipping the hook, but with the same
message that you already typed.
2025-04-10 08:54:46 +02:00
Stefan Haller ced70903ec Prefill the commit subject with the skipHook prefix when pressing w
We removed prefilling the skipHook prefix in b102646b20 with the intention of
making it clearer that using the prefix in normal commits and typing `w` to skip
hooks are now two independent features.

It turns out that some people liked it with prefilling the prefix and perceive
it as a regression, so put it back in.

But only if we don't have a preserved message; this is an important use case,
when you try to make a normal commit, the hook fails, and then you want to make
the same commit with skipping the hook, but with the same message that you
already typed.
2025-04-10 08:53:11 +02:00
Stefan Haller 391654984b Bump go-git, and get rid of github.com/imdario/mergo (#4460)
- **PR Description**

This updates our go-git fork to the newest version (based on 5.14.1),
and then updates the mergo dependency to use dario.cat/mergo instead of
github.com/imdario/mergo.
2025-04-09 11:26:30 +02:00
Stefan Haller b78aa54439 Use dario.cat/mergo instead of github.com/imdario/mergo
It's the new canonical location as of 1.0.0, the old one is deprecated and stuck
on 0.3.something.
2025-04-09 11:23:55 +02:00
Stefan Haller 4cf49ff449 Bump go-git 2025-04-09 11:23:55 +02:00
Stefan Haller da0105c16b Add runCommand function to Go template syntax + add support for templates in git branchPrefix setting (#4438)
This makes it possible to use date and time in initial values like this:

```yaml
initialValue: 'ruudk/{{ runCommand "date +\"%Y/%-m\"" }}/'
```
![Screenshot 2025-04-02 at 19 41
40@2x](https://github.com/user-attachments/assets/7f5e79c6-c8c9-4047-83da-ee61240ded27)

I want to use this to configure my BranchPrefix like this:

```yaml
git:
  branchPrefix: 'ruudk/{{ runCommand "date +\"%Y/%-m\"" }}/'
```
![Screenshot 2025-04-02 at 19 43
06@2x](https://github.com/user-attachments/assets/c0c517d5-8bc3-4e83-944d-2bf591ac1521)
2025-04-09 11:11:07 +02:00
Ruud Kamphuis 12820481e6 Add runCommand function to Go template syntax
This makes it possible to use date and time in initial values like this:

```yaml
initialValue: 'ruudk/{{ runCommand "date +\"%Y/%-m\"" }}/'
```

I want to use this to configure my BranchPrefix like this:

```yaml
git:
  branchPrefix: 'ruudk/{{ runCommand "date +\"%Y/%-m\"" }}/'
```
2025-04-09 10:44:36 +02:00
Ruud Kamphuis c7feba9bb1 Move test from commit to branch 2025-04-09 10:44:36 +02:00
Stefan Haller e127a928fe Bump the minimum required git version to 2.22 (#4439)
- **PR Description**

For my current work on better cherry-picking/reverting, git versions
older than 2.22 cause considerable headache. Apparently they have issues
with `git cherry-pick --continue` and `git cherry-pick --skip` which I
would prefer not to have to work around.

The last time [we
discussed](https://github.com/jesseduffield/lazygit/issues/2457#issuecomment-1436767849)
the question of bumping the minimum version from 2.20 to 2.22, we
decided not to because 2.22 is 6 months newer. That was two years ago
though, so now it should be fine, I guess.
2025-04-09 10:43:31 +02:00
Stefan Haller bf2d175865 Print which git version we are using for running integration tests
I usually use something like

   PATH=~/git-versions/2.22.0/bin:$PATH ./scripts/run_integration_tests.sh

for running integration tests with an older version. This has the problem that
when you specify a version that you don't have locally, it will silently use the
current version. Guard against that by printing the version it is using.
2025-04-09 10:40:52 +02:00
Stefan Haller 82e1caa166 Remove conditional code related to git earlier than 2.22 2025-04-09 10:40:52 +02:00
Stefan Haller 765168b9d7 Remove canUsePushTrack parameter of obtainBranches function
It was only needed for git versions older than 2.22.
2025-04-09 10:40:52 +02:00
Stefan Haller ad813503fb Bump minimum required git version to 2.22
Versions older than 2.22 have issues with "git cherry-pick --continue" and
"git cherry-pick --skip" that are difficult to work around.
2025-04-09 10:40:52 +02:00
Stefan Haller 9469037cdd Resolve non-inline merge conflicts (#4431)
- **PR Description**

We offer an inline merge conflict editor for text conflicts (i.e. where
both sides modify the same section of code). However, there are other
types of conflicts that can't be resolved this way, for example when one
side modifies a file and the other side deletes it. For these cases we
would previously only show `* Unmerged path` in the main view, which
isn't helpful. (Also, for some of these we would split the main view and
show this text both in the unstaged changes and staged changes views,
which is a bit embarrassing.) But more importantly, it was very unclear
how to resolve such a conflict. The only option we had was to discard
the file, which would basically pick "ours" and discard "theirs"; but
there was no way to do the opposite.

This PR improves the situation in two ways:
- it shows elaborate help texts in the main view explaining the
situation (which, in some obscure cases, can be extremely non-obvious,
and a `git status` on the command line doesn't help at all either). For
`UD` and `DU` conflicts we also show the diff of the side that didn't
delete the file; this is usually essential for resolving the conflict
properly, because it's likely that this diff needs to be applied
manually somewhere else.
- when pressing enter, we show a dialog with options to keep the
modified file or to delete it.


![image](https://github.com/user-attachments/assets/568cdaa9-0945-4111-aa99-4bbbf465bf8b)

A note about terminology: a common way to describe the two sides of a
merge is "ours" and "theirs". I dislike these terms, because while they
work well for merges, they are backwards [for
rebases](https://git-scm.com/docs/git-checkout#Documentation/git-checkout.txt---ours).
I chose to avoid them in this PR, and to use the terms "current" and
"incoming" instead (like in the conflict code lenses in VS Code), which
I think work much better in general; however, they might not be easy to
understand when they occur in the middle of a sentence, so maybe we
should put them in quotes there.

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [x] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [x] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [ ] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
* [ ] Docs have been updated if necessary
* [x] You've read through your own file changes for silly mistakes etc
2025-04-09 10:29:47 +02:00
Stefan Haller ebb576feac Provide conflict resolution dialogs for non-textual conflicts 2025-04-09 10:27:57 +02:00
Stefan Haller efcd71b296 Allow chaining of FileSystem methods 2025-04-09 10:27:57 +02:00
Stefan Haller 2e1be45957 Better main view display for conflicing files
For the less common conflict types DD, AU, UA, DU, and UD, we would previously
only show "* Unmerged path" in the main view, which isn't helpful. Also, for
some of these we would split the main view and show this text both in the
unstaged changes and staged changes views, which is a bit embarrassing.

Improve this by offering more explanation about what's going on, and what the
most likely way to resolve the situation is for each case.
2025-04-09 10:27:57 +02:00
Stefan Haller a09ca592fa Fix display of renamed files in tree view (#4452)
- **PR Description**

Fix a regression (introduced with the root item PR, #4346) that caused
renamed files to be displayed with their full path in tree view.

While fixing this I noticed that the display of moved files is a bit
confusing; for example, you can't distinguish a file being moved from
the root level into a directory from one that was renamed inside the
directory; see commit message of the first commit for more. I'm not
doing anything about this right now, just fix the regression for now.

Labeled as "ignore-for-release" because it fixes a regression in code
that wasn't released yet.

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [x] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [ ] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [ ] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
* [ ] Docs have been updated if necessary
* [x] You've read through your own file changes for silly mistakes etc
2025-04-08 16:19:19 +02:00
Stefan Haller b09251e27b Merge branch 'master' into fix-display-of-renamed-files-in-tree-view 2025-04-08 16:17:33 +02:00
Stefan Haller 6fb7942500 Fix wrong main view content after pressing e in a stack of branches (#4450)
- **PR Description**

When pressing `e` (or `i`) in a stack of branches, we take care of
maintaining the selected commit (as of #2954 for `e`, and #3247 for
`i`), to account for the selection index changing because of added
update-ref todos. This is great, however we missed that the main view
diff now shows the wrong commit, which is very confusing. I'm surprised
that this hasn't been noticed yet.

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [x] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [ ] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [ ] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
* [ ] Docs have been updated if necessary
* [x] You've read through your own file changes for silly mistakes etc
2025-04-08 16:17:14 +02:00
Stefan Haller 59af407eb1 Fix the bug described in the previous commit 2025-04-08 16:14:31 +02:00
Stefan Haller f20b7ea593 Add test demonstrating problem with main view display when pressing e in a stack of branches
We keep the same commit selected (even though its index changed because of the
added update-ref todo), which is nice; however, the main view shows the diff of
the wrong commit, which is very confusing. I'm suprised that this hasn't been
noticed yet.

The reason why this happens is that we first do the refresh, which includes
re-rendering the main view diff (with the same commit index as before, so the
wrong one), and then we restore the correct commit index but don't render the
main view again.
2025-04-08 16:14:31 +02:00
Stefan Haller 14187c9d15 More navigation keybindings for confirmation panel (#4404)
- **PR Description**

Add bindings for `,` (page up), `.` (page down), `<` or `<home>` (top),
and `>` or `<end>` (bottom), for scrolling long text in confirmation
panels. This is useful for example for git hooks that output a lot of
error text on failure, where the most interesting bit of information is
probably at the end.

I chose not to bind `<pgUp>` and `<pgDown>`, since they are normally
used for scrolling the main view. Which is not a thing when a
confirmation is shown or the Extras panel is focused, so we *could* use
them in these cases, but I thought it might be confusing when they are
used for different things in different contexts.

While we're at it, add the same navigation bindings also to the Extras
panel (i.e. the command log when it has the focus).

Fixes #4372.

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [ ] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [ ] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [ ] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
* [ ] Docs have been updated if necessary
* [x] You've read through your own file changes for silly mistakes etc
2025-04-08 16:14:17 +02:00
Stefan Haller 8a4506066a Add pageUp/pageDown/top/bottom keybindings to focused command log panel 2025-04-08 16:11:15 +02:00
Stefan Haller 2b783d1bc6 Add pageUp/pageDown/top/bottom keybindings to confirmation panel 2025-04-08 16:11:15 +02:00
Stefan Haller ed0cc8b70b Some code cleanup (#4449)
- **PR Description**

Two cleanup commits.
2025-04-08 16:11:00 +02:00
Stefan Haller 8ba57b6bd0 Cleanup: remove unused interfaces for helpers
I can only guess here: maybe they were added to more clearly document the public
interface of the classes? If so, I don't think that works. Developers who are
not familiar with the convention will just add a new public method to the class
without updating the interface.
2025-04-08 16:08:25 +02:00
Stefan Haller b3215a750c Cleanup: get rid of the variadic parameter of ContextMgr.Push
Apparently this was an attempt at working around go's lack of default arguments,
but it's very unidiomatic and a bit confusing. Make it a normal parameter
instead, so all clients have to pass it explicitly.
2025-04-08 16:08:25 +02:00
Stefan Haller 9c98fd809c Fix hyperlinks in last line of confirmation popups (#4454)
- **PR Description**

This fixes the bug that hyperlinks were not clickable if they occurred
in the last line of a confirmation panel.

While we're at it, make them clickable in tooltips too, which is useful
if they contain documentation links (e.g. the tooltip of the `<c-f>`
binding in the Files panel).
2025-04-08 16:08:01 +02:00
Stefan Haller 6f31f94266 Enable AutoRenderHyperLinks for the tooltip view
Sometimes tooltips contain documentation links, it's useful to be able to
single-click them.
2025-04-08 16:05:34 +02:00
Stefan Haller 7322cf1a9e Bump gocui
This includes a fix to render hyperlinks in the last line of a view if it
doesn't end with a line feed; see
https://github.com/jesseduffield/gocui/pull/77.
2025-04-08 16:05:34 +02:00
Stefan Haller 7ba76f47b4 Update kidpix link in README to active url (#4425)
- **PR Description**

I chose a different link than you did Jesse, since this one had a few
more pixels. It's also incredible how _dominant_ Keith Kelly is for
videos of Kid Pix! What compels a man to make a playlist with 16 videos
of various explosion screens!

![image](https://github.com/user-attachments/assets/a7c0acf6-e5e3-472d-86ce-9c983f5ea6d1)



Fixes https://github.com/jesseduffield/lazygit/issues/4408

- **Please check if the PR fulfills these requirements**

* [ ] Cheatsheets are up-to-date (run `go generate ./...`)
* [ ] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [ ] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [ ] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [ ] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
* [X] Docs have been updated if necessary
* [X] You've read through your own file changes for silly mistakes etc

<!--
Be sure to name your PR with an imperative e.g. 'Add worktrees view'
see https://github.com/jesseduffield/lazygit/releases/tag/v0.40.0 for
examples
-->
2025-04-08 13:53:58 +02:00
Chris McDonnell 8d47928c94 Update kidpix link in README to active url 2025-04-08 13:51:22 +02:00
Stefan Haller f7787fc819 Better support for shell aliases (#4385)
- **PR Description**

In version 0.45.0 we started to use an interactive shell for running
shell commands (see #4159). The idea was that this allows users to use
their aliases and shell functions in lazygit without having to do any
additional configuration.

Unfortunately, this hasn't worked out well. For some users this resulted
in lazygit hanging in the background upon trying to return from the
shell command; we tried various fixes for this (see #4126, #4159, and
#4350), but some users still have this problem (e.g. #4320).

Also, starting an interactive shell can be a lot slower than starting a
non-interactive one, depending on how much happens in the `.bashrc` or
`.zshrc` file. For example, on my machine calling `zsh -ic true` takes
600ms, whereas `zsh -c true` takes less than 2ms. This is too high of a
price to pay for using shell aliases, especially when _all_ users have
to pay it, even those who don't care about using their aliases in
lazygit.

This PR reverts all commits related to interactive shells, and instead
introduces a different approach: we let users specify a shell aliases
file that will be sourced before running a command. The downside is that
it doesn't work transparently out of the box, but requires
configuration, and it may also require that users restructure their
shell startup file(s) if they currently only have a single big one. The
advantage is that only users who actually want to use aliases or
functions are affected, and that we can now use this mechanism not only
for shell commands, but also for custom commands and for calling the
editor (some users have asked for this in the past).
2025-04-07 14:38:47 +02:00
Stefan Haller d13306ac64 Add a breaking changes entry for using shell aliases 2025-04-07 14:35:40 +02:00
Stefan Haller dc48cf963a Add config os.shellFunctionsFile 2025-04-07 14:35:40 +02:00
Stefan Haller 41e9335ea8 Reorder configs in OSConfig
There is a section at the end with deprecated settings, and a comment saying
"The following configs are all deprecated". The clipboard-related settings were
accidentally added to that section; they are not deprecated, so move them up to
before that section.
2025-04-07 14:35:40 +02:00
Stefan Haller 4e5e21f946 Revert commits related to using an interactive shell for running shell commands
This reverts commits f28b6f439d, dbd407c01d, 5fac40c129, and 5a3049485c.
2025-04-07 14:35:40 +02:00
Stefan Haller 959f932ddd Fix discarding submodule changes in nested folders (#4317)
- **PR Description**

This PR addresses https://github.com/jesseduffield/lazygit/issues/3951.

The current rules for discarding submodule changes is that no other
changed item must be also selected. There are some bugs with the current
implementation when submodules are in folders. The filed issue goes into
more detail.

As part of this PR, I also tentatively changed the disabled message
("Range select not supported for submodules" -> "Multiselection not
supported for submodules"). The former was not quite accurate because
you could select a single line (folder) but the reset action still needs
to be disabled (folder contains submodule change and some other change).
Not sure if there is some better phrasing.
2025-04-07 09:49:38 +02:00
Brandon f75c0af6e0 Update disabled message 2025-04-07 09:42:19 +02:00
Brandon 2db0a024bc Add integration test 2025-04-07 09:42:19 +02:00
Brandon 1fa9ea7f04 Fix discarding submodule changes in nested folders
The current rules for discarding submodule changes is that no other changed item
must be also selected. There are some bugs with the current implementation when
submodules are in folders.

For example, selecting and discarding a folder with only a nested submodule
change will currently do nothing. The submodule changes should be discarded. The
folder only contains submodule changes so it should be no different than
pressing discard on the submodule entry itself.

Also, I noticed range selecting both the folder and the submodule and then
pressing discard would be incorrectly disallowed.
2025-04-07 09:42:19 +02:00
Brandon 106b7018dd Cleanup: rename predicate params (test -> predicate) 2025-04-07 09:39:42 +02:00
Brandon c488e22abc Bump generics dependency 2025-04-06 22:18:20 -07:00
Stefan Haller b0b8ef9cf6 Fix display of renamed files
This broke with #4346 (Add root node in file tree).
2025-04-02 18:15:44 +02:00
Stefan Haller 267ef70de6 Add test showing problem with the display of renamed files
Renaming a file inside the same directory shows it with its full path in the
tree view, which isn't what we want. We'll fix this in the next commit.

Also adding a few other test cases for moving files; they show that the display
of moved files in tree view isn't ideal. For example, moving file1 from top
level into dir shows it as "R file1 → file1", which isn't distinguishable from
renaming file1 inside dir. I suppose what we would like to have here is
"R ../file1 → file1" or something, but I'll leave that for the future; here I
only want to fix the regression that was introduced with the root item PR.
2025-04-02 18:15:27 +02:00
Stefan Haller facc73a88b Allow range drop stashes (#4451)
- **PR Description**

This pr implement dropping multiple stashes feature.

In #3196, dropping multiple stashes feature is listed as possible but
less needed. In practice, I tend to use `apply` instead of `drop` in
order to retrieve some temporary changes in the near future, but the
consequence of this is that at the end of the work week I often have to
go back and manually clean up stashes that I no longer need or that I
simply forgot to drop.

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [x] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [ ] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [ ] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
* [ ] Docs have been updated if necessary
* [x] You've read through your own file changes for silly mistakes etc
2025-04-02 11:37:39 +02:00
gaogao-qwq 3a03aebd89 feat: Implement range stash drop
Signed-off-by: gaogao-qwq <gaogaoqwq@gmail.com>
2025-04-02 17:34:35 +08:00
Stefan Haller cd2286276c Fix flaky integration test (#4432)
- **PR Description**

The integration test `stash/rename.go` occasionally fails for me
locally. This PR fixes it.

In 8b8343b8a9 we made a change to run newPtyTask from AfterLayout; this
is needed so that the PTY gets the new, updated view size. However, this
created a race condition for integration tests that select a line in a
list view and then expect the main view to have certain content;
sometimes that content gets rendered too late.

I'm surprised that this didn't cause more tests to fail; right now I
only know of one test that occasionally fails because of this, which is
`stash/rename.go`.

Fix this by moving the AfterLayout to inside newPtyTask, and do it only
when we are actually using a PTY (we don't when no pager is configured,
which is the case for integration tests).

The diff is best viewed with "ignore whitespace" turned on.
2025-03-31 10:50:39 +02:00
Stefan Haller 10f29bc6b4 Fix race with PTYs in integration tests
In 8b8343b8a9 we made a change to run newPtyTask from AfterLayout; this is
needed so that the PTY gets the new, updated view size. However, this created a
race condition for integration tests that select a line in a list view and then
expect the main view to have certain content; sometimes that content gets
rendered too late.

I'm surprised that this didn't cause more tests to fail; right now I only know
of one test that occasionally fails because of this, which is stash/rename.go.

Fix this by moving the AfterLayout to inside newPtyTask, and do it only when we
are actually using a PTY (we don't when no pager is configured, which is the
case for integration tests).

The diff is best viewed with "ignore whitespace" turned on.
2025-03-28 11:16:04 +01:00
Stefan Haller d2d13449e4 Cleanup: remove pointless if statement 2025-03-28 11:06:35 +01:00
Stefan Haller c55062fe86 Corrected interactive rebase keybind example in README.md (#4426)
- **PR Description**
* The keybind for moving a commit up in the interactive rebase example
of the README file is incorrect.
I've changed it to the keybind present in current versions of Lazygit
2025-03-26 09:10:01 +01:00
Nitin KM 55445dca8d Corrected interactive rebase keybinds example in README.md 2025-03-26 08:40:14 +05:30
Stefan Haller 77651cac35 Add "Absolute path" item to the file view's copy menu (#4410)
- **PR Description**

Make it possible to copy the absolute path of a file to the clipboard.

Addresses #4409.
2025-03-25 10:03:41 +01:00
Stefan Haller a8f6b04ff3 Add "Absolute path" item to the file view's copy menu
Rename the existing item to "Relative path".
2025-03-25 10:00:56 +01:00
Stefan Haller 200e490398 Fix checking out a file from a range selection of commits (#4423)
- **PR Description**

When selecting a range of commits by selecting the top one and then
pressing shift-down to create a range, and then pressing enter and
checking out one of the files by pressing `c`, you would get the file
checked out with its state at the bottom end of the range, which is not
what you want; it is expected to check out the file at the state that
the diff shows it changes to. (And if the file was only created in the
middle of that range, trying to check it out would result in an error.)
Fix this by paying attention to a "from-to" range selection, and
checking out the file at the "to" state.

Related to #4420.
2025-03-25 09:58:34 +01:00
Stefan Haller d2059df543 Fix the bug mentioned in the previous commit 2025-03-25 09:56:01 +01:00
Stefan Haller 61c56c7822 Add test for checking out a file from a range selection of commits
The test shows a misbehavior: even though the diff shows "-one" and "+three",
meaning that "three" is the state we want to check out, we get "one". The reason
is that the checkout file command doesn't pay attention to range selections, it
only looks at the "moving end" of the range. Had we created the range by
selecting "two" and then pressed shift-up to "three", we would have gotten the
expected result.
2025-03-25 09:56:01 +01:00
Stefan Haller ac7d8ad025 Add test for checking out a file from a commit
This works, we just didn't have a test for it.
2025-03-25 09:56:01 +01:00
Stefan Haller 5038fa2d51 Let users define custom icons and color for files on the config file (#4395)
- **PR Description**

_Yet another icon-related PR_

- Implementation on config file for user defined file icons and colors. 
- Fix for some file icon mappings.

I use [eza](https://github.com/eza-community/eza),
[nvim-wewb-devicons](https://github.com/nvim-tree/nvim-web-devicons) and
lazygit; the first ones already let user to override icons and colors
via a config file, so with this change I can customize based on
filenames and file extensions.

| Before | After  |
|--------|--------|
|
![image](https://github.com/user-attachments/assets/bc747d99-25c1-4b74-868e-a31178d7d22e)
|
![image](https://github.com/user-attachments/assets/9a43eda9-d167-4602-a891-8c7c3e530289)
|

```yaml
gui:
  customIcons:
    filenames:
      "CONTRIBUTING.md": { icon: "\uede2", color: "#FEDDEF" }
      "HACKING.md": { icon: "\uede2", color: "#FEDDEF" }
    extensions:
      ".cat":
        icon: "\U000f011b"
        color: "#BC4009"
      ".dog":
        icon: "\U000f0a43"
        color: "#B6977E"
```
2025-03-25 09:55:18 +01:00
hasecilu 0f37f62770 Fix double '#' on hexadecimal colors 2025-03-25 09:48:12 +01:00
haseciluandStefan Haller 8ec37f80b7 Let users to define custom icons and color for files on the config file
Co-authored-by: Stefan Haller <stefan@haller-berlin.de>
2025-03-25 09:48:12 +01:00
Stefan Haller 1eb00d8d14 Add newlines for correct markdown
See
https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md#md031---fenced-code-blocks-should-be-surrounded-by-blank-lines
2025-03-25 09:48:12 +01:00
Stefan Haller 7d26a751bd Enhance support for GPG signed tags (#4394)
- **PR Description**

When using gpg signing on tags via the git config `tag.gpgSign=true`, it
is not possible to do a lightweight tag. You must do an annotated tag
(Which is allowed to have an empty description).

This PR augments the existing `WithGpgHandling` helper to allow it to
specifically target commit signing and tag signing. It then uses that
handler, and creates an annotated tag, when the git config demands tag
GPG signing. By default it will launch the tag signing in a new
subprocess. This does require that users click an extra `<enter>` after
creating a signed tag, but this is currently the behavior for signed
commits, so I don't feel too bad about that. If they want, they can use
the LazyGit configuration option of `git.overrideGpg: true` to promise
to LazyGit that they do not need a GPG sub-process (because they
reliably already have gpg-agent or similar running in the background).
This again matches the current behavior for GPG signed commits.

This has no integration test because we don't have the machinery set up
in place to set up a GPG key inside of our integration test framework.

Fixes https://github.com/jesseduffield/lazygit/issues/2955
2025-03-24 08:43:41 +01:00
Stefan Haller c765da10f5 Allow passing refresh scope to WithGpgHandling 2025-03-24 08:41:58 +01:00
Chris McDonnell c06d4e7b18 fix: Make tag operation use GPG helper to run signing in sub-process 2025-03-24 08:41:58 +01:00
Chris McDonnell f779a5878d refactor: Express WithGpgHelper with a config key parameter 2025-03-24 08:41:58 +01:00
Chris McDonnell 6fb3b7430c refactor: Make commit.gpgSign match official capitalization
The actual usage is case insensitive, so this doesn't actually matter.
But if fills my heart with joy.

The test is case sensitive, but the actual response to `git config
commit.gpgSign` is equivalent to `git config commit.gppsign`
2025-03-24 08:41:58 +01:00
Chris McDonnell 52da806c57 refactor: Rename UsingGpg to make room for Gpg Tag logic 2025-03-24 08:41:58 +01:00
Stefan Haller 67b0db0bd8 Filter out deprecated user config fields from generated Config.md (#4416)
- **PR Description**

This removes generated Config.md entries that are contain the word:
`deprecated` (case insensitive) anywhere in their header comment.

I think adding in deprecated configs into the reference config clutters
it at best, and encourages users to use those configs at worst. They are
still going to be validated by the schema, but this PR removes them from
the generated config.

I also added in the missing `deprecated` yaml tags in our user config in
anticipation of https://github.com/invopop/jsonschema/pull/79 getting
merged.
2025-03-23 12:47:32 +01:00
Karim Khaleel a64c32d431 Add in missing "deprecated" yaml tags 2025-03-23 12:45:34 +01:00
Karim Khaleel b5d4bdbd08 Remove deprecated configs from auto generated config.md 2025-03-23 12:44:24 +01:00
Stefan Haller d8e2819e2a Include empty arrays and maps in config docs (#4413)
- **PR Description**

Include empty arrays and maps in the generated Config.md

It's not an ideal solution because there's no indication of what kind of objects
you can add to those maps or arrays, but at least they show up at all (with a
comment containing a link to more information), and that's already an improvement.
2025-03-23 12:40:50 +01:00
Stefan Haller c3b099398b Include empty arrays and maps in the generated Config.md
It's not an ideal solution because there's no indication of what kind of objects
you can add to those maps or arrays, but at least they show up at all (with a
comment containing a link to more information), and that's already an
improvement.
2025-03-22 12:35:24 +01:00
Stefan Haller 0c9154ca9d Add comments with more information
This will be useful in the next commit when these start to show up in the
generated Config.md.
2025-03-22 12:35:24 +01:00
Stefan Haller a51bf12661 Commit without pre-commit hooks action is independent on prefix (#4374)
As discussed in #1984, this PR modifies the code such that the `w`
command always commits without pre-commit hooks. The `skipHookPrefix` is
still considered when starting the commit messaged with the prefix in
the regular commit command using `c`.

Skipping pre-commits also works when switching to the editor and adding
a co author from the the `w` command
2025-03-22 11:08:50 +01:00
Stefan Haller 728f0d9dfa Remove text that is now unused 2025-03-22 11:06:15 +01:00
Korbinian Schweiger b102646b20 Commit without pre-commit hooks is independent on prefix
Add verify flag

Add and update integration tests

Rename verify to forceSkipHooks

Adapt CommitSkipHooks integration test to actually use a hook

Remove forceSkipHooks param from OnConfirm et al

Simplify tests
2025-03-22 11:04:28 +01:00
Stefan Haller ab9f4af636 [FEAT] Add Recursive Bulk Initialize and Update for Submodules (#4259)
- **PR Description**
This PR adds a new bulk action for submodules.
As I often work with a lot of submodules that are hierarchically
structured, the flag "--recursive" saves a lot of work.
2025-03-22 10:09:41 +01:00
Cesar Andres 5de735f1ac feat(submodules): add method to bulk init and update submodules 2025-03-22 09:54:32 +01:00
Stefan Haller 4b4d82e13c Specify a go release minor version (#4393)
- **PR Description**
Since they started releasing .0 versions with 1.21.0, the go version
need to be a full release version specifier and not a go language
version. They relented and added support for defaulting to a .0 release
if none is specified in later 1.23.x releases, but for users with local
1.21 or 1.22 toolchains the lack of a full release specifier breaks
them. And going forwards this is more technically correct anyway.

See https://github.com/golang/go/issues/62278#issuecomment-1693620853
2025-03-21 15:57:21 +01:00
Kevin Radloff 14ba0e7523 Specify a go release minor version
Since they started releasing .0 versions with 1.21.0, the go version
need to be a full release version specifier and not a go language
version. They relented and added support for defaulting to a .0 release
if none is specified in later 1.23.x releases, but for users with local
1.21 or 1.22 toolchains the lack of a full release specifier breaks
them. And going forwards this is more technically correct anyway.

See https://github.com/golang/go/issues/62278#issuecomment-1693620853
2025-03-21 08:28:18 -04:00
Stefan Haller 1107462c3e Add root node in file tree (#4346)
- **PR Description**

If files at the root level of the repository have changed, there was no
way to see the combined diff of all of them. Fix this by inserting a `/`
item in this case. This is useful in repositories such as git.git, which
have all their `.c` files at root level (gasp).

If only files inside some folder have changed, the root item gets
compressed away automatically, so nothing changes for that case (which I
guess should be more common for most people).

Addresses #4331.

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [x] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [ ] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [ ] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
* [ ] Docs have been updated if necessary
* [x] You've read through your own file changes for silly mistakes etc
2025-03-21 08:08:09 +01:00
Stefan Haller 26459523f5 Add a root item ("/") if files at top level have changed 2025-03-21 08:04:58 +01:00
Stefan Haller 0b42cfb568 Make Node.path private
This is in preparation for changing the meaning of path in the next commit.
2025-03-20 12:31:34 +01:00
Stefan Haller 3f4cb8bdda Use Path directly instead of GetPath getter inside the filetree package 2025-03-20 12:31:34 +01:00
Stefan Haller 53090b2c45 Use GetPath accessor outside of filetree package
In preparation of making it private to the package.
2025-03-20 12:31:34 +01:00
Stefan Haller 2dfc3491bd Rename Name to Path in File and CommitFile
Name was very confusing and misleading.
2025-03-20 12:31:34 +01:00
Stefan Haller 0b5504aa98 Cleanup: make integration test assertions for files panel more specific
Assert the entire lines using Equals instead of Contains. This makes the tests a
bit easier to read, and it makes it much easier to decide how they need to be
changed when we change the layout (like we do in the last commit of this
branch).

It is true that this requires changing all these tests for any future UI
changes, but I think this is a good price to pay; those adaptions are trivial
and can be done without thinking.
2025-03-20 11:58:50 +01:00
Stefan Haller 030c2e5aef Fix postFilterTest to actually do what it says
I'm pretty sure that the check for the main view was meant to be done with the
commit selected in the commits panel, not with the first file of the commit
files view selected, so it was pressing enter too early. It's pure coincidence
that the test worked.
2025-03-20 11:58:50 +01:00
Stefan Haller e8d3a7afec Add a "Content of selected file" entry to the copy menu for commit files (#4341)
- **PR Description**

Some people have the need to get at the file content of a file as it was
in an earlier commit. They expect to press `e` in the commit files view
and get the file opened in the state it was in at that commit, somehow.
I explained a few times (e.g. in #3902, #4109, and #4327) that this is
not how `e` should work; so if we want this as a feature, we'd need it
as a separate command. However, it's a bit tricky to implement; if we
check out the selected file to a temp file, it's unclear when to remove
the file again. Alternatively we could use EditAndWait to open the file,
then we would block until the user closes the editor and delete the file
then. This is probably not the best user experience though, e.g. it
wouldn't allow opening two files from an older commit at once.

Instead of finding solutions to all these questions, this PR takes a
simpler route and adds a command to the "Copy to Clipboard" menu that
allows copying the entire content of a file. Users can then open a new,
empty editor buffer and paste it in, this is almost as good as opening a
temp file, but without all the questions. This was suggested in #4327.

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [x] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [x] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [ ] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
* [ ] Docs have been updated if necessary
* [x] You've read through your own file changes for silly mistakes etc
2025-03-20 11:54:49 +01:00
Stefan Haller f7295a97c0 Add a "Content of selected file" entry to the copy menu for commit files
This is useful for copying the entire content of the selected file as it was at
the selected commit.

We only add it to the commit files panel; it is not needed in the files panel,
because there you can simply press "e" to edit the file.
2025-03-20 11:52:53 +01:00
Stefan Haller 357c046df2 Drop the git config cache when getting focus (#4376)
- **PR Description**

This allows changing git config values while lazygit is running (e.g. in a
different terminal tab, or even in lazygit's ":" shell prompt), and have them
take effect immediately, while still getting some benefit from caching them
while lazygit is in the foreground.

Addresses #4240.
2025-03-20 11:52:20 +01:00
Stefan Haller 3e15be576e Drop the git config cache when getting focus
This allows changing git config values while lazygit is running (e.g. in a
different terminal tab, or even in lazygit's ":" shell prompt), and have them
take effect immediately, while still getting some benefit from caching them
while lazygit is in the foreground.
2025-03-20 11:50:28 +01:00
Stefan Haller f98ad65f46 Cleanup: remove more unnecessary type arguments
Missed these in 44097384d3.
2025-03-20 11:50:28 +01:00
Stefan Haller f5f246478e Revert #4313 (Skip post-checkout hook when discarding changes) (#4407)
@jesseduffield:

> Let's revert this PR, given that, as @TimShilov says, the
post-checkout hook will receive a flag stating whether the checkout was
for a file or a branch, meaning it's within the user's power to just
update the hook script to only run on branch checkouts if desired.
2025-03-20 11:48:47 +01:00
Stefan Haller cf27399571 Revert "Skip post-checkout hook when discarding changes"
This reverts commit 964278255b.
2025-03-20 11:41:15 +01:00
Stefan Haller b3b8223364 Support home and end as alternatives to '<' and '>' (#4396)
- **PR Description**

Allow pressing `<home>` and `<end>` to jump to the beginning/end of
lists, in addition to the current keybindings of `<` and `>`.
2025-03-17 20:00:02 +01:00
Stefan Haller 95c5d51e64 Support home and end as alternatives to '<' and '>' 2025-03-17 19:58:01 +01:00
Stefan Haller 71c5fa9688 Commit only tracked files in tracked filter view (#4386)
- **PR Description**
When in tracked filter view, selecting a folder for staging should only
stage files that are tracked by git. Previously, it staged even
untracked files that were not in view.
All staged (even untracked) files are shown in tracked filter view.
Fixes #4245 

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [x] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [ ] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [ ] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
* [ ] Docs have been updated if necessary
* [x] You've read through your own file changes for silly mistakes etc

<!--
Be sure to name your PR with an imperative e.g. 'Add worktrees view'
see https://github.com/jesseduffield/lazygit/releases/tag/v0.40.0 for
examples
-->
2025-03-17 19:56:53 +01:00
Partho Kunda 7a1b63182d Commit only tracked files in tracked only filter view 2025-03-17 19:51:27 +01:00
Partho Kunda 220f06052a Show staged but untracked files in tracked only filter view 2025-03-17 19:50:58 +01:00
Stefan Haller a0dd3bec8e Fix commitPrefix setting with empty pattern (#4381)
Before we changed the commitPrefix config to a list in #4261, we had
this bug where the defaults section in `Config.md` would erroneously
list the default for commitPrefix as

```yml
  git:
    commitPrefix:
      pattern: ""
      replace: ""
```

This was not correct, commitPrefix was a pointer, and the default for
that was nil, which is not the same.

Now, some people copied and pasted the entire defaults section into
their config files, setting the commitPrefix to an empty pattern (which
is not the same as not setting it at all). And this caused the branch
name to be filled in to the subject field for every commit; see for
example https://github.com/jesseduffield/lazygit/discussions/3632.

New users copying the defaults section into their config file in the
current version no longer have this problem because now that
commitPrefix is a list, it is no longer included in the defaults
section. However, the migration that we added in #4261 would happily
carry over those empty strings into a list entry, so people migrating
from an older version still have the broken config in their config
files.

To work around the issue, ignore commit prefix settings whose pattern is
an empty string. I can't imagine a valid reason why people would
actually want to set the pattern to an empty string, so I assume this
only comes from the broken defaults problem described above.
2025-03-12 08:17:51 +01:00
Stefan Haller 05a18edc54 Add hint to not copy the whole defaults section into the config file
I'm not mentioning *why* it's not a good idea, and I'm not sure it's important
to explain this to users. I'll say it here:
- it exposes users to potential bugs like the one described in the previous
  commit
- if we decide to change the default of an option to something "better" (like we
  did with the "show graph" setting), users don't benefit from what we think is
  an improvement for most users
2025-03-12 08:15:36 +01:00
Stefan Haller 95940ee01e Ignore commit prefixes with an empty pattern
Before we changed the commitPrefix config to a list in #4261, we had this bug
where the defaults section in Config.md would erroneously list the default for
commitPrefix as

  git:
    commitPrefix:
      pattern: ""
      replace: ""

This was not correct, commitPrefix was a pointer, and the default for that was
nil, which is not the same.

Now, some people copied and pasted the entire defaults section into their config
files, setting the commitPrefix to an empty pattern (which is not the same as
not setting it at all). And this caused the branch name to be filled in to the
subject field for every commit; see for example
https://github.com/jesseduffield/lazygit/discussions/3632.

New users copying the defaults section into their config file in the current
version no longer have this problem because now that commitPrefix is a list, it
is no longer included in the defaults section. However, the migration that we
added in #4261 would happily carry over those empty strings into a list entry,
so people migrating from an older version still have the broken config in their
config files.

To work around the issue, ignore commit prefix settings whose pattern is an
empty string. I can't imagine a valid reason why people would actually want to
set the pattern to an empty string, so I assume this only comes from the broken
defaults problem described above.
2025-03-12 08:15:36 +01:00
Jesse Duffield c735a5e5aa URL encode gitlab brackets to make consistent with branch names (#4336)
- **PR Description**
Some operating systems 'open' implementations do not like when some
special characters are unencoded, so they will double-encode the branch
name, which we already encode.

This particularly matters since branch names with / are common.

Fixes https://github.com/jesseduffield/lazygit/issues/4321

- **Please check if the PR fulfills these requirements**

* [X] Cheatsheets are up-to-date (run `go generate ./...`)
* [X] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [X] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [ ] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [ ] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
* [ ] Docs have been updated if necessary
* [X] You've read through your own file changes for silly mistakes etc
2025-03-12 09:27:25 +11:00
Chris McDonnell c1ca1c8f99 URL encode gitlab brackets to make consistent with branch names
Some operating systems 'open' implementations do not like
when some special characters are unencoded, so they will
double-enconde the branch name, which we already encode.

This particularly matters since branch names with / are common
2025-03-12 09:04:04 +11:00
Stefan Haller d423d2ac3d Add an integration test for a config with a negative refspec (#4382)
- **PR Description**

This is a sanity check to verify that lazygit has been built with a
patched version of go-git that does not error on negative refspecs.

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [x] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [x] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [x] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
* [x] Docs have been updated if necessary
* [x] You've read through your own file changes for silly mistakes etc
2025-03-11 08:35:26 +01:00
Kevin Radloff b76b73943a Add an integration test for a config with a negative refspec 2025-03-11 08:33:41 +01:00
Stefan Haller bf6d2a1c28 Update to go 1.24 (#4377)
- **PR Description**
Update to go 1.24. This is required to use a newer version of go-git.

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [x] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [x] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [x] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
* [x] Docs have been updated if necessary
* [x] You've read through your own file changes for silly mistakes etc
2025-03-11 08:31:32 +01:00
Kevin Radloff be7583dd40 Update to go 1.24 2025-03-08 14:53:54 -05:00
Stefan Haller 19ac926116 Add acme editor preset (#4360)
- **PR Description**
Add editor preset for the acme editor (http://acme.cat-v.org/)

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [x] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [x] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [x] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
* [x] Docs have been updated if necessary
* [x] You've read through your own file changes for silly mistakes etc
2025-03-06 15:09:59 +01:00
rako f989425f8c Add acme editor preset 2025-03-06 15:02:53 +01:00
Stefan Haller 0b9f320132 Fix release schedule again (#4364)
There's no way to tell cron to run a job on the first Saturday of a
month, so we tell it to run every Saturday, and manually check whether
it's the first week of the month. This is not ideal because we'll get
notifications about failed releases three times a month, but it's better
than nothing for now.
2025-03-06 11:17:40 +01:00
Stefan Haller f7fd5217cb Fix release schedule again
There's no way to tell cron to run a job on the first Saturday of a month, so we
tell it to run every Saturday, and manually check whether it's the first week of
the month. This is not ideal because we'll get notifications about failed
releases three times a month, but it's better than nothing for now.
2025-03-06 10:04:15 +01:00
Stefan Haller 182cefcafc Fix changing language while lazygit is running (#4361)
- **PR Description**

When changing the language in the config file while lazygit is running,
the panel titles and tab titles weren't properly updated. I noticed this
while working on #4359.

Probably not an important feature for users, because they are unlikely
to change the language all the time; but it's still nice if this isn't
broken.
2025-03-06 08:27:29 +01:00
Stefan Haller 20ab6c8cc8 Set tab titles after reloading user config
This ensures that tab titles are updated to the new language when changing the
language in the config while lazygit is running.
2025-03-06 08:24:03 +01:00
Stefan Haller 8c014641d1 Set view titles in configureViewProperties rather than createAllViews
This ensures that they are updated to the new language when changing the
language in the config while lazygit is running.
2025-03-06 08:24:03 +01:00
Stefan Haller 1213e875bb Cleanup: standardize on accessing translations via gui.c.Tr 2025-03-06 08:24:03 +01:00
Stefan Haller 26cb4060d2 Fix layout of options view for non-english languages (#4359)
- **PR Description**

The width calculations didn't take multi-byte characters into account,
so the options bar was cut off too early for these.

Fixes #4353.
2025-03-06 08:22:21 +01:00
Stefan Haller b31607250d Fix layout of options view for non-english languages
The width calculations didn't take multi-byte characters into account.
2025-03-06 08:19:35 +01:00
Stefan Haller 6a15a5915b Use a waiting status for rewording a non-head commit (#4343)
- **PR Description**

Rewording a commit at the beginning of a long branch can take very long;
without this change, the commit message panel would stay visible with a
blinking cursor during that time, which is very confusing.

This has the slight downside that it will say "Rebasing" in the lower
right corner until the operation is done; but we already have this problem
when doing custom patch operations, or dropping changes from a commit, so it's not
new, and we can think about how to fix all these another time.

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [ ] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [x] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [ ] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
* [ ] Docs have been updated if necessary
* [x] You've read through your own file changes for silly mistakes etc
2025-03-06 08:18:34 +01:00
Stefan Haller e6d6ed4f9a Use Rewording instead of Committing when rewording the head commit
Now that we have this status, let's use it for the head case too, for
consistency.
2025-03-06 08:15:18 +01:00
Stefan Haller c18d8af9f4 Use a WithWaitingStatus for rewording a non-head commit
Rewording a commit at the beginning of a long branch can take very long;
without this change, the commit message panel would stay visible with a blinking
cursor during that time, which is very confusing.

This has the slight downside that it will say "Rebasing" in the lower right
corner until the operation is done; but we already have this problem when doing
custom patch operations, or dropping changes from a commit, so it's not new, and
we can think about how to fix all these another time.
2025-03-06 08:15:18 +01:00
Stefan Haller eda65cbade Avoid double Refresh when rewording the head commit
WithGpgHandling already does an async refresh when done, so there's no need to
do one here for the case of amending the head commit. On top of that,
WithGpgHandling uses WithWaitingStatus and works in the background, so the
Refresh here would come too early anyway.
2025-03-06 08:15:18 +01:00
Stefan Haller 41a7afb105 Don't call OnCommitSuccess in handleReword
All this does is clear the preserved commit message; however, we open the commit
message panel with PreserveMessage: false when rewording, so this is not
necessary.
2025-03-06 08:15:18 +01:00
Stefan Haller ddc14ef674 Support fish when running shell command (#4350)
- **PR Description**

This should allow using custom shell functions in lazygit's : prompt.

Fixes #4342.
2025-03-06 08:14:48 +01:00
Stefan Haller f28b6f439d Support fish when running shell command
This should allow using custom shell functions in lazygit's : prompt.
2025-03-06 08:11:32 +01:00
Stefan Haller 08751d6b60 Fix linter warnings (#4352)
- **PR Description**

Fix a few things that started to crop up in my editor recently, probably
because of an updated gopls or something (I didn't investigate more
closely).
2025-03-06 08:10:46 +01:00
Stefan Haller 44097384d3 Remove unnecessary type arguments
I'm getting warnings in my editor about these, probably because of an updated
gopls again.
2025-03-03 21:24:49 +01:00
Stefan Haller a4d6d020c4 Remove unused functions
For some reason the //nolint:golint,unused comment no longer seems to work after
I updated gopls (I think).

But I also don't understand why we mark unused stuff with linter comments
instead of just getting rid of it.
2025-03-03 21:23:29 +01:00
Stefan Haller 1b571f9e6b Custom commands submenus (#4324)
- **PR Description**

I want to be able to configure custom commands that I don't need very
often; I don't want these to pollute the global keybindings menu, and I
don't want to assign globally unique keybindings to them (because there
are only so many of these available, and also because I wouldn't be able
to remember them, because the commands are not used often). However, I
still want to invoke them through keybindings somehow.

I find that the perfect solution for this is to configure a menu that
contains custom commands. I can pop open the menu using only one key
that I need to remember, but I can access the individual custom commands
inside using keys that don't need to be unique with the rest of the
global keybindings.

In this PR we achieve this by adding an optional `subCommands` property
to customCommand that can be used instead of the other properties like
`command`, etc. This is an alternative approach to #4276, which added a
new top-level property for custom command menus.

Potentially closes #3799.

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [x] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [x] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [x] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
* [x] Docs have been updated if necessary
* [x] You've read through your own file changes for silly mistakes etc
2025-02-28 10:47:59 +01:00
Stefan Haller df17896de5 Validate properties of customCommand when commandMenu is used 2025-02-28 10:11:30 +01:00
Stefan Haller 22512d55a8 Add commandMenu property to custom commands 2025-02-28 10:11:30 +01:00
Stefan Haller e799976b8a Extract a method CustomCommand.GetDescription
We'll reuse it in the next commit.
2025-02-28 10:00:59 +01:00
Stefan Haller f93948cb23 Change customCommand fields to pointers
This allows us to tell whether they appear in the user's config file, which we
will need later in this branch.
2025-02-28 10:00:59 +01:00
Stefan Haller 0df5e08828 Don't set showOutput explicitly
It is false by default. This way there's one less change to make in the next
commit.
2025-02-28 10:00:59 +01:00
Stefan Haller 7238e41251 Fix a small mistake in Custom_Command_Keybindings.md
It has been 'e' instead of 'o' for quite a while now.
2025-02-28 10:00:59 +01:00
Stefan Haller 9ad50028f8 Fix wrong comment
This was backwards; we renamed Sha to Hash, so Sha is now deprecated, not Hash.
2025-02-28 10:00:59 +01:00
Stefan Haller a7b0ccf059 Refactor migrations to only marshall yaml twice (#4318)
- **PR Description**

In the solving of #4194 we had some discussion about the repeated
marshalling and unmarshalling of the content in our migrations.
https://github.com/jesseduffield/lazygit/issues/4194#issuecomment-2661375136.

I was feeling the itch to see if there was a meaningful performance
impact to that, and this is the results! I added a benchmark first that
uses the entirety of `Config.md`. This ensures a worst-case scenario for
the repeated parsing, and probably at least 1 user has done this for
their config. Here were the benchmark results prior and after this PR:


## Before
```
$ go test ./pkg/config/ -bench=. -benchmem -count=10
goos: linux
goarch: amd64
pkg: github.com/jesseduffield/lazygit/pkg/config
cpu: Intel(R) Core(TM) i7-8705G CPU @ 3.10GHz
BenchmarkMigrationOnLargeConfiguration-8              85          18165916 ns/op         1501442 B/op      19316 allocs/op
BenchmarkMigrationOnLargeConfiguration-8              50          21024442 ns/op         1501473 B/op      19316 allocs/op
BenchmarkMigrationOnLargeConfiguration-8              75          18814769 ns/op         1501509 B/op      19316 allocs/op
BenchmarkMigrationOnLargeConfiguration-8              60          17886812 ns/op         1501434 B/op      19316 allocs/op
BenchmarkMigrationOnLargeConfiguration-8              97          17767498 ns/op         1501448 B/op      19316 allocs/op
BenchmarkMigrationOnLargeConfiguration-8              62          18749923 ns/op         1501478 B/op      19316 allocs/op
BenchmarkMigrationOnLargeConfiguration-8              62          19248265 ns/op         1501467 B/op      19316 allocs/op
BenchmarkMigrationOnLargeConfiguration-8              92          14771199 ns/op         1501423 B/op      19316 allocs/op
BenchmarkMigrationOnLargeConfiguration-8              67          16345152 ns/op         1501440 B/op      19316 allocs/op
BenchmarkMigrationOnLargeConfiguration-8              98          16785737 ns/op         1501472 B/op      19316 allocs/op
PASS
ok      github.com/jesseduffield/lazygit/pkg/config     14.474s
```
so somewhere in the range of 14 to 21 ms added to startup time.

## After
```
$ go test ./pkg/config/ -bench=. -benchmem -count=10
goos: linux
goarch: amd64
pkg: github.com/jesseduffield/lazygit/pkg/config
cpu: Intel(R) Core(TM) i7-8705G CPU @ 3.10GHz
BenchmarkMigrationOnLargeConfiguration-8             440           2936145 ns/op          265800 B/op       3928 allocs/op
BenchmarkMigrationOnLargeConfiguration-8             428           3099183 ns/op          265787 B/op       3928 allocs/op
BenchmarkMigrationOnLargeConfiguration-8             355           3074069 ns/op          265785 B/op       3928 allocs/op
BenchmarkMigrationOnLargeConfiguration-8             478           3031144 ns/op          265792 B/op       3928 allocs/op
BenchmarkMigrationOnLargeConfiguration-8             375           2795470 ns/op          265785 B/op       3928 allocs/op
BenchmarkMigrationOnLargeConfiguration-8             406           2688491 ns/op          265791 B/op       3928 allocs/op
BenchmarkMigrationOnLargeConfiguration-8             375           3092990 ns/op          265791 B/op       3928 allocs/op
BenchmarkMigrationOnLargeConfiguration-8             379           2789022 ns/op          265785 B/op       3928 allocs/op
BenchmarkMigrationOnLargeConfiguration-8             417           3108530 ns/op          265785 B/op       3928 allocs/op
BenchmarkMigrationOnLargeConfiguration-8             487           2848502 ns/op          265788 B/op       3928 allocs/op
PASS
ok      github.com/jesseduffield/lazygit/pkg/config     15.352s
```
So somewhere between 2.6 and 3.1 ms.

Is that a meaningful speedup? I'm not sure, but it's something! I had to
add the bit of complexity mentioned in the comment to track whether we
had made any changes, but that doesn't feel fundamentally much more
complex to me, since we were already implicitly tracking that fact with
whether the string file changed.

- **Please check if the PR fulfills these requirements**

* [X] Cheatsheets are up-to-date (run `go generate ./...`)
* [X] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [ ] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [ ] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [ ] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
* [ ] Docs have been updated if necessary
* [X] You've read through your own file changes for silly mistakes etc
2025-02-28 08:40:40 +01:00
Chris McDonnell d39f883b4e Migrate to only doing marshalling twice, and compare via deep copy 2025-02-27 17:05:31 -05:00
Chris McDonnell a01ca19bb3 Set up benchmark on migration 2025-02-27 12:38:21 -05:00
973 changed files with 81031 additions and 14400 deletions
+10 -11
View File
@@ -1,7 +1,7 @@
name: Continuous Integration
env:
GO_VERSION: 1.22
GO_VERSION: 1.24
on:
push:
@@ -32,7 +32,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.22.x
go-version: 1.24.x
- name: Test code
# we're passing -short so that we skip the integration tests, which will be run in parallel below
run: |
@@ -49,8 +49,7 @@ jobs:
fail-fast: false
matrix:
git-version:
- 2.20.0 # oldest supported version
- 2.22.5
- 2.22.0 # oldest supported version
- 2.23.0
- 2.25.1
- 2.30.8
@@ -89,7 +88,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.22.x
go-version: 1.24.x
- name: Print git version
run: git --version
- name: Test code
@@ -115,7 +114,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.22.x
go-version: 1.24.x
- name: Build linux binary
run: |
GOOS=linux go build
@@ -142,7 +141,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.22.x
go-version: 1.24.x
- name: Check Vendor Directory
# ensure our vendor directory matches up with our go modules
run: |
@@ -168,11 +167,11 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.22.x
go-version: 1.24.x
- name: Lint
uses: golangci/golangci-lint-action@v6.1.0
uses: golangci/golangci-lint-action@v6.5.0
with:
version: v1.60
version: v1.64.6
- name: errors
run: golangci-lint run
if: ${{ failure() }}
@@ -197,7 +196,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.22.x
go-version: 1.24.x
- name: Download all coverage artifacts
uses: actions/download-artifact@v4
+39
View File
@@ -0,0 +1,39 @@
name: Close Issues
on:
issue_comment:
types: [created]
permissions:
issues: write
jobs:
close_issue:
runs-on: ubuntu-latest
if: ${{ github.event.issue.pull_request == null && startsWith(github.event.comment.body, '/close') }}
steps:
- uses: actions/github-script@v7
with:
script: |
const trustedUsers = ['ChrisMcD1', 'jesseduffield', 'stefanhaller']
const commenter = context.payload.comment.user.login
console.log(`Commenter: ${commenter}`)
if (!trustedUsers.includes(commenter)) {
console.log(`User ${commenter} is not trusted. Ignoring.`)
return
}
const issueNumber = context.payload.issue.number
const owner = context.repo.owner
const repo = context.repo.repo
await github.rest.issues.update({
owner,
repo,
issue_number: issueNumber,
state: 'closed'
})
console.log(`Closed issue #${issueNumber} by request from ${commenter}.`)
+12 -3
View File
@@ -2,8 +2,9 @@ name: Release
on:
schedule:
# Runs at 2:00 AM UTC on the first Saturday of every month
- cron: '0 2 1-7 * 6'
# Runs at 2:00 AM UTC on every Saturday
# We'll check below if it's the first Saturday of the month, and fail if not
- cron: '0 2 * * 6'
# Allow manual triggering of the workflow
workflow_dispatch:
inputs:
@@ -25,6 +26,14 @@ jobs:
check-and-release:
runs-on: ubuntu-latest
steps:
- name: Check for first Saturday of the month
if: ${{ github.event_name != 'workflow_dispatch' }}
run: |
if (( $(date +%e) > 7 )); then
echo "This is not the first Saturday of the month"
exit 1
fi
- name: Checkout Code
uses: actions/checkout@v4
with:
@@ -118,7 +127,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.22.x
go-version: 1.24.x
- name: Run goreleaser
uses: goreleaser/goreleaser-action@v4
+1 -1
View File
@@ -32,5 +32,5 @@ linters-settings:
max-func-lines: 0
run:
go: '1.22'
go: "1.24"
timeout: 10m
+1 -1
View File
@@ -2,7 +2,7 @@
# docker build -t lazygit .
# docker run -it lazygit:latest /bin/sh
FROM golang:1.22 as build
FROM golang:1.24 as build
WORKDIR /go/src/github.com/jesseduffield/lazygit/
COPY go.mod go.sum ./
RUN go mod download
+13 -5
View File
File diff suppressed because one or more lines are too long
+103 -38
View File
@@ -31,10 +31,27 @@ to the top of your config file or via [Visual Studio Code settings.json config][
## Default
This is only meant as a reference for what config options exist, and what their default values are. It is not meant to be copied and pasted into your config file as a whole; that's not a good idea for several reasons. It is recommended to include only those settings in your config file that you actually want to change.
<!-- START CONFIG YAML: AUTOMATICALLY GENERATED with `go generate ./..., DO NOT UPDATE MANUALLY -->
```yaml
# Config relating to the Lazygit UI
gui:
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-author-color
authorColors: {}
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-branch-color
branchColorPatterns: {}
# Custom icons for filenames and file extensions
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-files-icon--color
customIcons:
# Map of filenames to icon properties (icon and color)
filenames: {}
# Map of file extensions (including the dot) to icon properties (icon and color)
extensions: {}
# The number of lines you scroll by when scrolling the main window
scrollHeight: 2
@@ -188,9 +205,6 @@ gui:
# If true, show jump-to-window keybindings in window titles.
showPanelJumps: true
# Deprecated: use nerdFontsVersion instead
showIcons: false
# Nerd fonts version to use.
# One of: '2' | '3' | empty string (default)
# If empty, do not show icons.
@@ -323,6 +337,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).
# Possible values: 'none' | 'onlyMainBranches' | 'allBranches'
autoForwardBranches: onlyMainBranches
# If true, pass the --all arg to git fetch
fetchAll: true
@@ -335,9 +353,9 @@ git:
# 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}} --
# Command used to display git log of all branches in the main window.
# Deprecated: Use `allBranchesLogCmds` instead.
allBranchesLogCmd: git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium
# 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, do not spawn a separate process when using GPG
overrideGpg: false
@@ -345,6 +363,12 @@ git:
# If true, do not allow force pushes
disableForcePushing: false
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#predefined-commit-message-prefix
commitPrefix: []
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#predefined-commit-message-prefix
commitPrefixes: {}
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#predefined-branch-name-prefix
branchPrefix: ""
@@ -354,19 +378,6 @@ git:
# Config for showing the log in the commits view
log:
# 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/
#
# Deprecated: Configure this 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'
#
# Deprecated: Configure this 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`)
showWholeGraph: false
@@ -428,24 +439,6 @@ os:
# Command for opening a link. Should contain "{{link}}".
openLink: ""
# EditCommand is the command for editing a file.
# Deprecated: use Edit instead. Note that semantics are different:
# EditCommand is just the command itself, whereas Edit contains a
# "{{filename}}" variable.
editCommand: ""
# EditCommandTemplate is the command template for editing a file
# Deprecated: use EditAtLine instead.
editCommandTemplate: ""
# OpenCommand is the command for opening a file
# Deprecated: use Open instead.
openCommand: ""
# OpenLinkCommand is the command for opening a link
# Deprecated: use OpenLink instead.
openLinkCommand: ""
# CopyToClipboardCmd is the command for copying to clipboard.
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-command-for-copying-to-and-pasting-from-clipboard
copyToClipboardCmd: ""
@@ -454,9 +447,20 @@ 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.
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#using-aliases-or-functions-in-shell-commands
shellFunctionsFile: ""
# If true, don't display introductory popups upon opening Lazygit.
disableStartupPopups: false
# User-configured commands that can be invoked from within Lazygit
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Custom_Command_Keybindings.md
customCommands: []
# See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-pull-request-urls
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
# - 'create': initialize a new repo
@@ -485,6 +489,8 @@ keybinding:
scrollRight: L
gotoTop: <
gotoBottom: '>'
gotoTop-alt: <home>
gotoBottom-alt: <end>
toggleRangeSelect: v
rangeSelectDown: <s-down>
rangeSelectUp: <s-up>
@@ -500,6 +506,7 @@ keybinding:
- "3"
- "4"
- "5"
focusMainView: "0"
nextMatch: "n"
prevMatch: "N"
startSearch: /
@@ -509,6 +516,7 @@ keybinding:
goInto: <enter>
confirm: <enter>
confirmInEditor: <a-enter>
confirmInEditor-alt: <c-s>
remove: d
new: "n"
edit: e
@@ -581,6 +589,7 @@ keybinding:
rebaseBranch: r
renameBranch: R
mergeIntoCurrentBranch: M
moveCommitsToNewBranch: "N"
viewGitFlowOptions: i
fastForward: f
createTag: T
@@ -615,6 +624,7 @@ keybinding:
openInBrowser: o
viewBisectOptions: b
startInteractiveRebase: i
selectCommitsOfCurrentBranch: '*'
amendAttribute:
resetAuthor: a
setAuthor: A
@@ -661,32 +671,39 @@ os:
```
## Custom Command for Opening a Link
```yaml
os:
openLink: 'bash -C /path/to/your/shell-script.sh {{link}}'
```
Specify the external command to invoke when opening URL links (i.e. creating MR/PR in GitLab, BitBucket or GitHub). `{{link}}` will be replaced by the URL to be opened. A simple shell script can be used to further mangle the passed URL.
## Custom Command for Copying to and Pasting from Clipboard
```yaml
os:
copyToClipboardCmd: ''
```
Specify an external command to invoke when copying to clipboard is requested. `{{text}` will be replaced by text to be copied. Default is to copy to system clipboard.
If you are working on a terminal that supports OSC52, the following command will let you take advantage of it:
```yaml
os:
copyToClipboardCmd: printf "\033]52;c;$(printf {{text}} | base64 -w 0)\a" > /dev/tty
```
For tmux you need to wrap it with the [tmux escape sequence](https://github.com/tmux/tmux/wiki/FAQ#what-is-the-passthrough-escape-sequence-and-how-do-i-use-it), and enable passthrough in tmux config with `set -g allow-passthrough on`:
```yaml
os:
copyToClipboardCmd: printf "\033Ptmux;\033\033]52;c;$(printf {{text}} | base64 -w 0)\a\033\\" > /dev/tty
```
For the best of both worlds, we can let the command determine if we are running in a tmux session and send the correct sequence:
```yaml
os:
copyToClipboardCmd: >
@@ -698,10 +715,12 @@ os:
```
A custom command for reading from the clipboard can be set using
```yaml
os:
readFromClipboardCmd: ''
```
It is used, for example, when pasting a commit message into the commit message panel. The command is supposed to output the clipboard content to stdout.
## Configuring File Editing
@@ -715,7 +734,7 @@ os:
editPreset: 'vscode'
```
Supported presets are `vim`, `nvim`, `nvim-remote`, `lvim`, `emacs`, `nano`, `micro`, `vscode`, `sublime`, `bbedit`, `kakoune`, `helix`, `xcode`, and `zed`. In many cases lazygit will be able to guess the right preset from your $(git config core.editor), or an environment variable such as $VISUAL or $EDITOR.
Supported presets are `vim`, `nvim`, `nvim-remote`, `lvim`, `emacs`, `nano`, `micro`, `vscode`, `sublime`, `bbedit`, `kakoune`, `helix`, `xcode`, `zed` and `acme`. In many cases lazygit will be able to guess the right preset from your $(git config core.editor), or an environment variable such as $VISUAL or $EDITOR.
`nvim-remote` is an experimental preset for when you have invoked lazygit from within a neovim process, allowing lazygit to open the file from within the parent process rather than spawning a new one.
@@ -734,6 +753,21 @@ The `editInTerminal` option is used to decide whether lazygit needs to suspend i
Contributions of new editor presets are welcome; see the `getPreset` function in [`editor_presets.go`](https://github.com/jesseduffield/lazygit/blob/master/pkg/config/editor_presets.go).
## Using aliases or functions in shell commands
Lazygit has a command prompt (`:`) for quickly executing shell commands without having to quit lazygit or switch to a different terminal. Most people find it convenient to have their usual shell aliases or shell functions available at this prompt. To achieve this, put your alias definitions in a separate shell startup file (which you source from your normal startup file, i.e. from `.bashrc` or `.zshrc`), and then tell lazygit about this file like so:
```yml
os:
shellFunctionsFile: ~/.my_aliases.sh
```
For many people it might work well enough to use their entire shell config file (`~/.bashrc` or `~/.zshrc`) as the `shellFunctionsFile`, but these config files typically do a lot more than defining aliases (e.g. initialize the completion system, start an ssh-agent, etc.) and this may unnecessarily delay execution of shell commands.
When using zsh, aliases can't be used here, but functions can. It is easy to convert your existing aliases into functions, just change `alias l="ls -la"` to `l() ls -la`, for example. This way it will work as before both in the shell and in lazygit.
Note that the shell aliases file is not only used when executing shell commands, but also for [custom commands](Custom_Command_Keybindings.md), and when opening a file in the editor.
## Overriding default config file location
To override the default config directory, use `CONFIG_DIR="$HOME/.config/lazygit"`. This directory contains the config file in addition to some other files lazygit uses to keep track of state across sessions.
@@ -844,6 +878,27 @@ gui:
Note that the regular expressions are not implicitly anchored to the beginning/end of the branch name. If you want to do that, add leading `^` and/or trailing `$` as needed.
## Custom Files Icon & Color
You can customize the icon and color of files based on filenames or extensions:
```yaml
gui:
customIcons:
filenames:
"CONTRIBUTING.md": { icon: "\uede2", color: "#FEDDEF" }
"HACKING.md": { icon: "\uede2", color: "#FEDDEF" }
extensions:
".cat":
icon: "\U000f011b"
color: "#BC4009"
".dog":
icon: "\U000f0a43"
color: "#B6977E"
```
Note that there is no support for regular expressions.
## Example Coloring
![border example](../../assets/colored-border-example.png)
@@ -972,6 +1027,7 @@ In situations where certain naming pattern is used for branches, this can be use
Example:
Some branches:
- jsmith/AB-123
- cwilson/AB-125
@@ -980,6 +1036,15 @@ git:
branchPrefix: "firstlast/"
```
It's possible to use a dynamic prefix by using the `runCommand` function:
```yaml
git:
branchPrefix: "firstlast/{{ runCommand "date +\"%Y/%-m\"" }}/"
```
This would produce something like: `firstlast/2025/4/`
## Custom git log command
You can override the `git log` command that's used to render the log of the selected branch like so:
+44 -7
View File
@@ -1,6 +1,6 @@
# Custom Command Keybindings
You can add custom command keybindings in your config.yml (accessible by pressing 'o' on the status panel from within lazygit) like so:
You can add custom command keybindings in your config.yml (accessible by pressing 'e' on the status panel from within lazygit) like so:
```yml
customCommands:
@@ -14,7 +14,7 @@ customCommands:
- key: 'C'
context: 'global'
command: "git commit"
subprocess: true
output: terminal
- key: 'n'
context: 'localBranches'
prompts:
@@ -53,13 +53,11 @@ For a given custom command, here are the allowed fields:
| key | The key to trigger the command. Use a single letter or one of the values from [here](https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md). Custom commands without a key specified can be triggered by selecting them from the keybindings (`?`) menu | no |
| command | The command to run (using Go template syntax for placeholder values) | yes |
| context | The context in which to listen for the key (see [below](#contexts)) | yes |
| subprocess | Whether you want the command to run in a subprocess (e.g. if the command requires user input) | no |
| prompts | A list of prompts that will request user input before running the final command | no |
| loadingText | Text to display while waiting for command to finish | no |
| description | Label for the custom command when displayed in the keybindings menu | no |
| stream | Whether you want to stream the command's output to the Command Log panel | no |
| showOutput | Whether you want to show the command's output in a popup within Lazygit | no |
| outputTitle | The title to display in the popup panel if showOutput is true. If left unset, the command will be used as the title. | no |
| output | Where the output of the command should go. 'none' discards it, 'terminal' suspends lazygit and runs the command in the terminal (useful for commands that require user input), 'log' streams it to the command log, 'logWithPty' is like 'log' but runs the command in a pseudo terminal (can be useful for commands that produce colored output when the output is a terminal), and 'popup' shows it in a popup. | no |
| outputTitle | The title to display in the popup panel if output is set to 'popup'. If left unset, the command will be used as the title. | no |
| after | Actions to take after the command has completed | no |
Here are the options for the `after` key:
@@ -320,13 +318,52 @@ We don't support accessing all elements of a range selection yet. We might add t
command: "git format-patch {{.SelectedCommitRange.From}}^..{{.SelectedCommitRange.To}}"
```
We support the following functions:
### Quoting
Quote wraps a string in quotes with necessary escaping for the current platform.
```
git {{.SelectedFile.Name | quote}}
```
### Running a command
Runs a command and returns the output. If the command outputs more than a single line, it will produce an error.
```
initialValue: "username/{{ runCommand "date +\"%Y/%-m\"" }}/"
```
## Keybinding collisions
If your custom keybinding collides with an inbuilt keybinding that is defined for the same context, only the custom keybinding will be executed. This also applies to the global context. However, one caveat is that if you have a custom keybinding defined on the global context for some key, and there is an in-built keybinding defined for the same key and for a specific context (say the 'files' context), then the in-built keybinding will take precedence. See how to change in-built keybindings [here](https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#keybindings)
## Menus of custom commands
For custom commands that are not used very frequently it may be preferable to hide them in a menu; you can assign a key to open the menu, and the commands will appear inside. This has the advantage that you don't have to come up with individual unique keybindings for all those commands that you don't use often; the keybindings for the commands in the menu only need to be unique within the menu. Here is an example:
```yml
customCommands:
- key: X
description: "Copy/paste commits across repos"
commandMenu:
- key: c
command: 'git format-patch --stdout {{.SelectedCommitRange.From}}^..{{.SelectedCommitRange.To}} | pbcopy'
context: commits, subCommits
description: "Copy selected commits to clipboard"
- key: v
command: 'pbpaste | git am'
context: "commits"
description: "Paste selected commits from clipboard"
```
If you use the commandMenu property, none of the other properties except key and description can be used.
## Debugging
If you want to verify that your command actually does what you expect, you can wrap it in an 'echo' call and set `showOutput: true` so that it doesn't actually execute the command but you can see how the placeholders were resolved.
If you want to verify that your command actually does what you expect, you can wrap it in an 'echo' call and set `output: popup` so that it doesn't actually execute the command but you can see how the placeholders were resolved.
## More Examples
+37 -2
View File
@@ -40,8 +40,8 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|-----|--------|-------------|
| `` , `` | Previous page | |
| `` . `` | Next page | |
| `` < `` | Scroll to top | |
| `` > `` | Scroll to bottom | |
| `` < (<home>) `` | Scroll to top | |
| `` > (<end>) `` | Scroll to bottom | |
| `` v `` | Toggle range select | |
| `` <s-down> `` | Range select down | |
| `` <s-up> `` | Range select up | |
@@ -68,6 +68,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` ` `` | 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. |
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | Search the current view by text | |
## Commit summary
@@ -108,9 +109,14 @@ If you would instead like to start an interactive rebase from the selected commi
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Open commit in browser | |
| `` n `` | Create new branch off of commit | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
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 `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | Copy (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View files | |
| `` w `` | View worktree options | |
| `` / `` | Search the current view by text | |
@@ -152,6 +158,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` 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 |
| `` 0 `` | Focus main view | |
| `` / `` | Search the current view by text | |
## Local branches
@@ -162,6 +169,9 @@ If you would instead like to start an interactive rebase from the selected commi
| `` i `` | Show git-flow options | |
| `` <space> `` | Checkout | Checkout selected item. |
| `` n `` | New branch | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
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). |
| `` o `` | Create pull request | |
| `` O `` | View create pull request options | |
| `` <c-y> `` | Copy pull request URL to clipboard | |
@@ -177,6 +187,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` R `` | Rename branch | |
| `` u `` | View upstream options | View options relating to the branch's upstream e.g. setting/unsetting the upstream and resetting to the upstream. |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View commits | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
@@ -203,6 +214,9 @@ If you would instead like to start an interactive rebase from the selected commi
|-----|--------|-------------|
| `` mouse wheel down (fn+up) `` | Scroll down | |
| `` mouse wheel up (fn+down) `` | Scroll up | |
| `` <tab> `` | Switch view | Switch to other view (staged/unstaged changes). |
| `` <esc> `` | Exit back to side panel | |
| `` / `` | Search the current view by text | |
## Main panel (patch building)
@@ -258,10 +272,15 @@ If you would instead like to start an interactive rebase from the selected commi
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Open commit in browser | |
| `` n `` | Create new branch off of commit | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
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 `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | Copy (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <c-r> `` | Reset copied (cherry-picked) commits selection | |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View commits | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
@@ -280,6 +299,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` s `` | Sort order | |
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View commits | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
@@ -295,6 +315,14 @@ If you would instead like to start an interactive rebase from the selected commi
| `` f `` | Fetch | Fetch updates from the remote repository. This retrieves new commits and branches without merging them into your local branches. |
| `` / `` | Filter the current view by text | |
## Secondary
| Key | Action | Info |
|-----|--------|-------------|
| `` <tab> `` | Switch view | Switch to other view (staged/unstaged changes). |
| `` <esc> `` | Exit back to side panel | |
| `` / `` | Search the current view by text | |
## Stash
| Key | Action | Info |
@@ -304,6 +332,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` d `` | Drop | Remove the stash entry from the stash list. |
| `` n `` | New branch | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
| `` r `` | Rename stash | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View files | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
@@ -327,10 +356,15 @@ If you would instead like to start an interactive rebase from the selected commi
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Open commit in browser | |
| `` n `` | Create new branch off of commit | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
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 `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | Copy (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <c-r> `` | Reset copied (cherry-picked) commits selection | |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View files | |
| `` w `` | View worktree options | |
| `` / `` | Search the current view by text | |
@@ -360,6 +394,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` P `` | Push tag | Push the selected tag to a remote. You'll be prompted to select a remote. |
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View commits | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
+43 -8
View File
@@ -40,8 +40,8 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|-----|--------|-------------|
| `` , `` | 前のページ | |
| `` . `` | 次のページ | |
| `` < `` | 最上部までスクロール | |
| `` > `` | 最下部までスクロール | |
| `` < (<home>) `` | 最上部までスクロール | |
| `` > (<end>) `` | 最下部までスクロール | |
| `` v `` | 範囲選択を切り替え | |
| `` <s-down> `` | Range select down | |
| `` <s-up> `` | Range select up | |
@@ -51,6 +51,14 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` ] `` | 次のタブ | |
| `` [ `` | 前のタブ | |
## Secondary
| Key | Action | Info |
|-----|--------|-------------|
| `` <tab> `` | パネルを切り替え | Switch to other view (staged/unstaged changes). |
| `` <esc> `` | Exit back to side panel | |
| `` / `` | 検索を開始 | |
## Stash
| Key | Action | Info |
@@ -60,6 +68,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` d `` | Drop | Remove the stash entry from the stash list. |
| `` n `` | 新しいブランチを作成 | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
| `` r `` | Stashを変更 | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View files | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
@@ -73,10 +82,15 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` y `` | コミットの情報をコピー | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | ブラウザでコミットを開く | |
| `` n `` | コミットにブランチを作成 | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
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 `` | Reset | 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> `` | Reset copied (cherry-picked) commits selection | |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View files | |
| `` w `` | View worktree options | |
| `` / `` | 検索を開始 | |
@@ -106,7 +120,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` e `` | Edit (start interactive rebase) | コミットを編集 |
| `` i `` | Start interactive rebase | Start an interactive rebase for the commits on your branch. This will include all commits from the HEAD commit down to the first merge commit or main branch commit.
If you would instead like to start an interactive rebase from the selected commit, press `e`. |
| `` p `` | Pick | Mark the selected commit to be picked (when mid-rebase). This means that the commit will be retained upon continuing the rebase. |
| `` p `` | 選択 | Mark the selected commit to be picked (when mid-rebase). This means that the commit will be retained upon continuing the rebase. |
| `` F `` | Fixupコミットを作成 | このコミットに対するfixupコミットを作成 |
| `` S `` | Apply fixup commits | Squash all 'fixup!' commits, either above the selected commit, or all in current branch (autosquash). |
| `` <c-j> `` | コミットを1つ下に移動 | |
@@ -122,9 +136,14 @@ If you would instead like to start an interactive rebase from the selected commi
| `` y `` | コミットの情報をコピー | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | ブラウザでコミットを開く | |
| `` n `` | コミットにブランチを作成 | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
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 `` | Reset | 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-t> `` | Open external diff tool (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View files | |
| `` w `` | View worktree options | |
| `` / `` | 検索を開始 | |
@@ -138,7 +157,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` c `` | チェックアウト | Checkout file. This replaces the file in your working tree with the version from the selected commit. |
| `` d `` | Remove | Discard this commit's changes to this file. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes this file. |
| `` o `` | ファイルを開く | Open file in default application. |
| `` e `` | Edit | Open file in external editor. |
| `` e `` | 編集 | Open file in external editor. |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` <space> `` | Toggle file included in patch | Toggle whether the file is included in the custom patch. See https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
| `` a `` | Toggle all files | Add/remove all commit's files to custom patch. See https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
@@ -146,6 +165,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` ` `` | ファイルツリーの表示を切り替え | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory. |
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | 検索を開始 | |
## コミットメッセージ
@@ -177,7 +197,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` e `` | 設定ファイルを編集 | Open file in external editor. |
| `` u `` | 更新を確認 | |
| `` <enter> `` | 最近使用したリポジトリに切り替え | |
| `` a `` | すべてのブランチログを表示 | |
| `` a `` | Show/cycle all branch logs | |
## タグ
@@ -190,6 +210,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` P `` | タグをpush | Push the selected tag to a remote. You'll be prompted to select a remote. |
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | コミットを閲覧 | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
@@ -207,7 +228,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` A `` | 最新のコミットにamend | |
| `` 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> |
| `` e `` | Edit | Open file in external editor. |
| `` e `` | 編集 | Open file in external editor. |
| `` o `` | ファイルを開く | Open file in default application. |
| `` i `` | ファイルをignore | |
| `` r `` | ファイルをリフレッシュ | |
@@ -224,6 +245,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` 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 |
| `` 0 `` | Focus main view | |
| `` / `` | 検索を開始 | |
## ブランチ
@@ -234,11 +256,14 @@ If you would instead like to start an interactive rebase from the selected commi
| `` i `` | Show git-flow options | |
| `` <space> `` | チェックアウト | Checkout selected item. |
| `` n `` | 新しいブランチを作成 | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
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). |
| `` o `` | Pull Requestを作成 | |
| `` O `` | View create pull request options | |
| `` <c-y> `` | Pull RequestのURLをクリップボードにコピー | |
| `` c `` | Checkout by name | Checkout by name. In the input box you can enter '-' to switch to the last branch. |
| `` F `` | Force checkout | Force checkout selected branch. This will discard all local changes in your working directory before checking out the selected branch. |
| `` F `` | 強制的にチェックアウト | Force checkout selected branch. This will discard all local changes in your working directory before checking out the selected branch. |
| `` d `` | Delete | View delete options for local/remote branch. |
| `` r `` | Rebase | Rebase the checked-out branch onto the selected branch. |
| `` M `` | 現在のブランチにマージ | View options for merging the selected item into the current branch (regular merge, squash merge) |
@@ -249,6 +274,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` R `` | ブランチ名を変更 | |
| `` u `` | View upstream options | View options relating to the branch's upstream e.g. setting/unsetting the upstream and resetting to the upstream. |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | コミットを閲覧 | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
@@ -275,6 +301,9 @@ If you would instead like to start an interactive rebase from the selected commi
|-----|--------|-------------|
| `` mouse wheel down (fn+up) `` | 下にスクロール | |
| `` mouse wheel up (fn+down) `` | 上にスクロール | |
| `` <tab> `` | パネルを切り替え | Switch to other view (staged/unstaged changes). |
| `` <esc> `` | Exit back to side panel | |
| `` / `` | 検索を開始 | |
## メインパネル (Patch Building)
@@ -328,7 +357,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` <enter> `` | View branches | |
| `` n `` | リモートを新規追加 | |
| `` d `` | Remove | Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected. |
| `` e `` | Edit | リモートを編集 |
| `` e `` | 編集 | リモートを編集 |
| `` f `` | Fetch | リモートをfetch |
| `` / `` | Filter the current view by text | |
@@ -346,6 +375,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` s `` | 並び替え | |
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | コミットを閲覧 | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
@@ -359,10 +389,15 @@ If you would instead like to start an interactive rebase from the selected commi
| `` y `` | コミットの情報をコピー | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | ブラウザでコミットを開く | |
| `` n `` | コミットにブランチを作成 | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
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 `` | Reset | 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> `` | Reset copied (cherry-picked) commits selection | |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | コミットを閲覧 | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
+38 -3
View File
@@ -40,8 +40,8 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|-----|--------|-------------|
| `` , `` | 이전 페이지 | |
| `` . `` | 다음 페이지 | |
| `` < `` | 맨 위로 스크롤 | |
| `` > `` | 맨 아래로 스크롤 | |
| `` < (<home>) `` | 맨 위로 스크롤 | |
| `` > (<end>) `` | 맨 아래로 스크롤 | |
| `` v `` | 드래그 선택 전환 | |
| `` <s-down> `` | Range select down | |
| `` <s-up> `` | Range select up | |
@@ -60,14 +60,27 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` y `` | 커밋 attribute 복사 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | 브라우저에서 커밋 열기 | |
| `` n `` | 커밋에서 새 브랜치를 만듭니다. | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
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 | 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> `` | Reset cherry-picked (copied) commits selection | |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 커밋 보기 | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Secondary
| Key | Action | Info |
|-----|--------|-------------|
| `` <tab> `` | 패널 전환 | Switch to other view (staged/unstaged changes). |
| `` <esc> `` | Exit back to side panel | |
| `` / `` | 검색 시작 | |
## Stash
| Key | Action | Info |
@@ -77,6 +90,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` d `` | Drop | Remove the stash entry from the stash list. |
| `` n `` | 새 브랜치 생성 | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
| `` r `` | Rename stash | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View selected item's files | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
@@ -90,10 +104,15 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` y `` | 커밋 attribute 복사 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | 브라우저에서 커밋 열기 | |
| `` n `` | 커밋에서 새 브랜치를 만듭니다. | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
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 | 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> `` | Reset cherry-picked (copied) commits selection | |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View selected item's files | |
| `` w `` | View worktree options | |
| `` / `` | 검색 시작 | |
@@ -138,6 +157,9 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|-----|--------|-------------|
| `` mouse wheel down (fn+up) `` | 아래로 스크롤 | |
| `` mouse wheel up (fn+down) `` | 위로 스크롤 | |
| `` <tab> `` | 패널 전환 | Switch to other view (staged/unstaged changes). |
| `` <esc> `` | Exit back to side panel | |
| `` / `` | 검색 시작 | |
## 메인 패널 (Patch Building)
@@ -184,6 +206,9 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` i `` | Git-flow 옵션 보기 | |
| `` <space> `` | 체크아웃 | Checkout selected item. |
| `` n `` | 새 브랜치 생성 | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
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). |
| `` o `` | 풀 리퀘스트 생성 | |
| `` O `` | 풀 리퀘스트 생성 옵션 | |
| `` <c-y> `` | 풀 리퀘스트 URL을 클립보드에 복사 | |
@@ -199,6 +224,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` R `` | 브랜치 이름 변경 | |
| `` u `` | View upstream options | View options relating to the branch's upstream e.g. setting/unsetting the upstream and resetting to the upstream. |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 커밋 보기 | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
@@ -211,7 +237,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` e `` | 설정 파일 수정 | Open file in external editor. |
| `` u `` | 업데이트 확인 | |
| `` <enter> `` | 최근에 사용한 저장소로 전환 | |
| `` a `` | 모든 브랜치 로그 표시 | |
| `` a `` | Show/cycle all branch logs | |
## 서브모듈
@@ -252,6 +278,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` s `` | Sort order | |
| `` g `` | View reset options | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 커밋 보기 | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
@@ -287,9 +314,14 @@ If you would instead like to start an interactive rebase from the selected commi
| `` y `` | 커밋 attribute 복사 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | 브라우저에서 커밋 열기 | |
| `` n `` | 커밋에서 새 브랜치를 만듭니다. | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
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 | 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-t> `` | Open external diff tool (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View selected item's files | |
| `` w `` | View worktree options | |
| `` / `` | 검색 시작 | |
@@ -311,6 +343,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` ` `` | 파일 트리뷰로 전환 | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory. |
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | 검색 시작 | |
## 커밋메시지
@@ -331,6 +364,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` P `` | 태그를 push | Push the selected tag to a remote. You'll be prompted to select a remote. |
| `` g `` | 초기화 | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 커밋 보기 | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
@@ -365,6 +399,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` 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 |
| `` 0 `` | Focus main view | |
| `` / `` | 검색 시작 | |
## 확인 패널
+38 -3
View File
@@ -40,8 +40,8 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|-----|--------|-------------|
| `` , `` | Vorige pagina | |
| `` . `` | Volgende pagina | |
| `` < `` | Scroll naar boven | |
| `` > `` | Scroll naar beneden | |
| `` < (<home>) `` | Scroll naar boven | |
| `` > (<end>) `` | Scroll naar beneden | |
| `` v `` | Toggle drag selecteer | |
| `` <s-down> `` | Range select down | |
| `` <s-up> `` | Range select up | |
@@ -81,6 +81,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` 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 |
| `` 0 `` | Focus main view | |
| `` / `` | Start met zoeken | |
## Bevestigingspaneel
@@ -98,6 +99,9 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` i `` | Laat git-flow opties zien | |
| `` <space> `` | Uitchecken | Checkout selected item. |
| `` n `` | Nieuwe branch | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
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). |
| `` o `` | Maak een pull-request | |
| `` O `` | Bekijk opties voor pull-aanvraag | |
| `` <c-y> `` | Kopieer de URL van het pull-verzoek naar het klembord | |
@@ -113,6 +117,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` R `` | Hernoem branch | |
| `` u `` | View upstream options | View options relating to the branch's upstream e.g. setting/unsetting the upstream and resetting to the upstream. |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Bekijk commits | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
@@ -141,6 +146,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` ` `` | 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. |
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | Start met zoeken | |
## Commits
@@ -174,9 +180,14 @@ If you would instead like to start an interactive rebase from the selected commi
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Open commit in browser | |
| `` n `` | Creëer nieuwe branch van commit | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
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 `` | Bekijk reset opties | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | Kopieer commit (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Bekijk gecommite bestanden | |
| `` w `` | View worktree options | |
| `` / `` | Start met zoeken | |
@@ -211,6 +222,9 @@ If you would instead like to start an interactive rebase from the selected commi
|-----|--------|-------------|
| `` mouse wheel down (fn+up) `` | Scroll omlaag | |
| `` mouse wheel up (fn+down) `` | Scroll omhoog | |
| `` <tab> `` | Ga naar een ander paneel | Switch to other view (staged/unstaged changes). |
| `` <esc> `` | Exit back to side panel | |
| `` / `` | Start met zoeken | |
## Patch bouwen
@@ -236,10 +250,15 @@ If you would instead like to start an interactive rebase from the selected commi
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Open commit in browser | |
| `` n `` | Creëer nieuwe branch van commit | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
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 `` | Bekijk reset opties | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | Kopieer commit (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <c-r> `` | Reset cherry-picked (gekopieerde) commits selectie | |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Bekijk commits | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
@@ -258,6 +277,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` s `` | Sort order | |
| `` g `` | Bekijk reset opties | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Bekijk commits | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
@@ -273,6 +293,14 @@ If you would instead like to start an interactive rebase from the selected commi
| `` f `` | Fetch | Fetch remote |
| `` / `` | Filter the current view by text | |
## Secondary
| Key | Action | Info |
|-----|--------|-------------|
| `` <tab> `` | Ga naar een ander paneel | Switch to other view (staged/unstaged changes). |
| `` <esc> `` | Exit back to side panel | |
| `` / `` | Start met zoeken | |
## Staging
| Key | Action | Info |
@@ -304,6 +332,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` d `` | Laten vallen | Remove the stash entry from the stash list. |
| `` n `` | Nieuwe branch | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
| `` r `` | Rename stash | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Bekijk gecommite bestanden | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
@@ -316,7 +345,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` e `` | Verander config bestand | Open file in external editor. |
| `` u `` | Check voor updates | |
| `` <enter> `` | Wissel naar een recente repo | |
| `` a `` | Alle logs van de branch laten zien | |
| `` a `` | Show/cycle all branch logs | |
## Sub-commits
@@ -327,10 +356,15 @@ If you would instead like to start an interactive rebase from the selected commi
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Open commit in browser | |
| `` n `` | Creëer nieuwe branch van commit | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
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 `` | Bekijk reset opties | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` C `` | Kopieer commit (cherry-pick) | Mark commit as copied. Then, within the local commits view, you can press `V` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `<esc>` to cancel the selection. |
| `` <c-r> `` | Reset cherry-picked (gekopieerde) commits selectie | |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Bekijk gecommite bestanden | |
| `` w `` | View worktree options | |
| `` / `` | Start met zoeken | |
@@ -360,6 +394,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` P `` | Push tag | Push the selected tag to a remote. You'll be prompted to select a remote. |
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Bekijk commits | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
+38 -3
View File
@@ -40,8 +40,8 @@ _Legenda: `<c-b>` oznacza ctrl+b, `<a-b>` oznacza alt+b, `B` oznacza shift+b_
|-----|--------|-------------|
| `` , `` | Poprzednia strona | |
| `` . `` | Następna strona | |
| `` < `` | Przewiń do góry | |
| `` > `` | Przewiń do dołu | |
| `` < (<home>) `` | Przewiń do góry | |
| `` > (<end>) `` | Przewiń do dołu | |
| `` v `` | Przełącz zaznaczenie zakresu | |
| `` <s-down> `` | Zaznacz zakres w dół | |
| `` <s-up> `` | Zaznacz zakres w górę | |
@@ -82,13 +82,26 @@ Jeśli chcesz zamiast tego rozpocząć interaktywny rebase od wybranego commita,
| `` y `` | Kopiuj atrybut commita do schowka | Kopiuj atrybut commita do schowka (np. hash, URL, różnice, wiadomość, autor). |
| `` o `` | Otwórz commit w przeglądarce | |
| `` n `` | Utwórz nową gałąź z commita | |
| `` 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.
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 `` | Reset | Wyświetl opcje resetu (miękki/mieszany/twardy) do wybranego elementu. |
| `` C `` | Kopiuj (cherry-pick) | Oznacz commit jako skopiowany. Następnie, w widoku lokalnych commitów, możesz nacisnąć `V`, aby wkleić (cherry-pick) skopiowane commity do sprawdzonej gałęzi. W dowolnym momencie możesz nacisnąć `<esc>`, aby anulować zaznaczenie. |
| `` <c-t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Wyświetl pliki | |
| `` w `` | Zobacz opcje drzewa pracy | |
| `` / `` | Szukaj w bieżącym widoku po tekście | |
## Dodatkowy
| Key | Action | Info |
|-----|--------|-------------|
| `` <tab> `` | Przełącz widok | Przełącz na inny widok (zatwierdzone/niezatwierdzone zmiany). |
| `` <esc> `` | Exit back to side panel | |
| `` / `` | Szukaj w bieżącym widoku po tekście | |
## Drzewa pracy
| Key | Action | Info |
@@ -122,6 +135,9 @@ Jeśli chcesz zamiast tego rozpocząć interaktywny rebase od wybranego commita,
| `` i `` | Pokaż opcje git-flow | |
| `` <space> `` | Przełącz | Przełącz wybrany element. |
| `` n `` | Nowa gałąź | |
| `` 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.
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). |
| `` o `` | Utwórz żądanie ściągnięcia | |
| `` O `` | Zobacz opcje tworzenia pull requesta | |
| `` <c-y> `` | Kopiuj adres URL żądania ściągnięcia do schowka | |
@@ -137,6 +153,7 @@ Jeśli chcesz zamiast tego rozpocząć interaktywny rebase od wybranego commita,
| `` R `` | Zmień nazwę gałęzi | |
| `` u `` | Pokaż opcje upstream | Pokaż opcje dotyczące upstream gałęzi, np. ustawianie/usuwanie upstream i resetowanie do upstream. |
| `` <c-t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Pokaż commity | |
| `` w `` | Zobacz opcje drzewa pracy | |
| `` / `` | Filtruj bieżący widok po tekście | |
@@ -155,6 +172,9 @@ Jeśli chcesz zamiast tego rozpocząć interaktywny rebase od wybranego commita,
|-----|--------|-------------|
| `` mouse wheel down (fn+up) `` | Przewiń w dół | |
| `` mouse wheel up (fn+down) `` | Przewiń w górę | |
| `` <tab> `` | Przełącz widok | Przełącz na inny widok (zatwierdzone/niezatwierdzone zmiany). |
| `` <esc> `` | Exit back to side panel | |
| `` / `` | Szukaj w bieżącym widoku po tekście | |
## Panel główny (scalanie)
@@ -231,6 +251,7 @@ Jeśli chcesz zamiast tego rozpocząć interaktywny rebase od wybranego commita,
| `` 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 |
| `` 0 `` | Focus main view | |
| `` / `` | Szukaj w bieżącym widoku po tekście | |
## Pliki commita
@@ -250,6 +271,7 @@ Jeśli chcesz zamiast tego rozpocząć interaktywny rebase od wybranego commita,
| `` ` `` | Przełącz widok drzewa plików | Przełącz widok plików między płaskim a drzewem. Płaski układ pokazuje wszystkie ścieżki plików na jednej liście, układ drzewa grupuje pliki według katalogów. |
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | Szukaj w bieżącym widoku po tekście | |
## Podsumowanie commita
@@ -268,10 +290,15 @@ Jeśli chcesz zamiast tego rozpocząć interaktywny rebase od wybranego commita,
| `` y `` | Kopiuj atrybut commita do schowka | Kopiuj atrybut commita do schowka (np. hash, URL, różnice, wiadomość, autor). |
| `` o `` | Otwórz commit w przeglądarce | |
| `` n `` | Utwórz nową gałąź z commita | |
| `` 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.
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 `` | Reset | Wyświetl opcje resetu (miękki/mieszany/twardy) do wybranego elementu. |
| `` C `` | Kopiuj (cherry-pick) | Oznacz commit jako skopiowany. Następnie, w widoku lokalnych commitów, możesz nacisnąć `V`, aby wkleić (cherry-pick) skopiowane commity do sprawdzonej gałęzi. W dowolnym momencie możesz nacisnąć `<esc>`, aby anulować zaznaczenie. |
| `` <c-r> `` | Resetuj wybrane (cherry-picked) commity | |
| `` <c-t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Pokaż commity | |
| `` w `` | Zobacz opcje drzewa pracy | |
| `` / `` | Filtruj bieżący widok po tekście | |
@@ -285,6 +312,7 @@ Jeśli chcesz zamiast tego rozpocząć interaktywny rebase od wybranego commita,
| `` d `` | Usuń | Usuń wpis schowka z listy schowka. |
| `` n `` | Nowa gałąź | Utwórz nową gałąź z wybranego wpisu schowka. Działa poprzez przełączenie git na commit, na którym wpis schowka został utworzony, tworzenie nowej gałęzi z tego commita, a następnie zastosowanie wpisu schowka do nowej gałęzi jako dodatkowego commita. |
| `` r `` | Zmień nazwę schowka | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Wyświetl pliki | |
| `` w `` | Zobacz opcje drzewa pracy | |
| `` / `` | Filtruj bieżący widok po tekście | |
@@ -297,7 +325,7 @@ Jeśli chcesz zamiast tego rozpocząć interaktywny rebase od wybranego commita,
| `` e `` | Edytuj plik konfiguracyjny | Otwórz plik w zewnętrznym edytorze. |
| `` u `` | Sprawdź aktualizacje | |
| `` <enter> `` | Przełącz na ostatnie repozytorium | |
| `` a `` | Pokaż wszystkie gałęzie w logach | |
| `` a `` | Show/cycle all branch logs | |
## Sub-commity
@@ -308,10 +336,15 @@ Jeśli chcesz zamiast tego rozpocząć interaktywny rebase od wybranego commita,
| `` y `` | Kopiuj atrybut commita do schowka | Kopiuj atrybut commita do schowka (np. hash, URL, różnice, wiadomość, autor). |
| `` o `` | Otwórz commit w przeglądarce | |
| `` n `` | Utwórz nową gałąź z commita | |
| `` 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.
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 `` | Reset | Wyświetl opcje resetu (miękki/mieszany/twardy) do wybranego elementu. |
| `` C `` | Kopiuj (cherry-pick) | Oznacz commit jako skopiowany. Następnie, w widoku lokalnych commitów, możesz nacisnąć `V`, aby wkleić (cherry-pick) skopiowane commity do sprawdzonej gałęzi. W dowolnym momencie możesz nacisnąć `<esc>`, aby anulować zaznaczenie. |
| `` <c-r> `` | Resetuj wybrane (cherry-picked) commity | |
| `` <c-t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Wyświetl pliki | |
| `` w `` | Zobacz opcje drzewa pracy | |
| `` / `` | Szukaj w bieżącym widoku po tekście | |
@@ -341,6 +374,7 @@ Jeśli chcesz zamiast tego rozpocząć interaktywny rebase od wybranego commita,
| `` P `` | Wyślij tag | Wyślij wybrany tag do zdalnego. Zostaniesz poproszony o wybranie zdalnego. |
| `` g `` | Reset | Wyświetl opcje resetu (miękki/mieszany/twardy) do wybranego elementu. |
| `` <c-t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Pokaż commity | |
| `` w `` | Zobacz opcje drzewa pracy | |
| `` / `` | Filtruj bieżący widok po tekście | |
@@ -370,6 +404,7 @@ Jeśli chcesz zamiast tego rozpocząć interaktywny rebase od wybranego commita,
| `` s `` | Kolejność sortowania | |
| `` g `` | Reset | Wyświetl opcje resetu (miękki/mieszany/twardy) do wybranego elementu. |
| `` <c-t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Pokaż commity | |
| `` w `` | Zobacz opcje drzewa pracy | |
| `` / `` | Filtruj bieżący widok po tekście | |
+92 -57
View File
@@ -9,8 +9,8 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-r> `` | Mudar para um repositório recente | |
| `` <pgup> (fn+up/shift+k) `` | Scroll up main window | |
| `` <pgdown> (fn+down/shift+j) `` | Scroll down main window | |
| `` <pgup> (fn+up/shift+k) `` | Rolar janela principal para cima | |
| `` <pgdown> (fn+down/shift+j) `` | Rolar a janela principal para baixo | |
| `` @ `` | View command log options | View options for the command log e.g. show/hide the command log and focus the command log. |
| `` P `` | Empurre (Push) | Faça push do branch atual para o seu branch upstream. Se nenhum upstream estiver configurado, você será solicitado a configurar um branch a montante. |
| `` p `` | Puxar (Pull) | Puxe alterações do controle remoto para o ramo atual. Se nenhum upstream estiver configurado, será solicitado configurar um ramo a montante. |
@@ -18,8 +18,8 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` ( `` | Decrease rename similarity threshold | Decrease the similarity threshold for a deletion and addition pair to be treated as a rename. |
| `` } `` | Increase diff context size | Increase the amount of the context shown around changes in the diff view. |
| `` { `` | Decrease diff context size | Decrease the amount of the context shown around changes in the diff view. |
| `` : `` | Execute shell command | Bring up a prompt where you can enter a shell command to execute. |
| `` <c-p> `` | View custom patch options | |
| `` : `` | Executar comando da shell | Traga um prompt onde você pode digitar um comando shell para executar. |
| `` <c-p> `` | Ver opções de patch personalizadas | |
| `` m `` | Ver opções de mesclar/rebase | Ver opções para abortar/continuar/pular o merge/rebase atual. |
| `` 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) | |
@@ -29,7 +29,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` W `` | View diffing options | View options relating to diffing two refs e.g. diffing against selected ref, entering ref to diff against, and reversing the diff direction. |
| `` <c-e> `` | View diffing options | View options relating to diffing two refs e.g. diffing against selected ref, entering ref to diff against, and reversing the diff direction. |
| `` q `` | Sair | |
| `` <esc> `` | Cancel | |
| `` <esc> `` | Cancelar | |
| `` <c-w> `` | Toggle whitespace | Toggle whether or not whitespace changes are shown in the diff view. |
| `` z `` | Desfazer | O reflog será usado para determinar qual comando git para executar para desfazer o último comando git. Isto não inclui mudanças na árvore de trabalho; apenas compromissos são tidos em consideração. |
| `` <c-z> `` | Refazer | O reflog será usado para determinar qual comando git para executar para refazer o último comando git. Isto não inclui mudanças na árvore de trabalho; apenas compromissos são tidos em consideração. |
@@ -40,14 +40,14 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|-----|--------|-------------|
| `` , `` | Previous page | |
| `` . `` | Next page | |
| `` < `` | Scroll to top | |
| `` > `` | Scroll to bottom | |
| `` < (<home>) `` | Scroll to top | |
| `` > (<end>) `` | Scroll to bottom | |
| `` v `` | Toggle range select | |
| `` <s-down> `` | Range select down | |
| `` <s-up> `` | Range select up | |
| `` / `` | Search the current view by text | |
| `` H `` | Scroll left | |
| `` L `` | Scroll right | |
| `` H `` | Rolar à esquerda | |
| `` L `` | Scroll para a direita | |
| `` ] `` | Next tab | |
| `` [ `` | Previous tab | |
@@ -60,7 +60,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` <c-b> `` | Filtrar arquivos por status | |
| `` y `` | Copy to clipboard | |
| `` c `` | Commit | Submeter mudanças em staging |
| `` w `` | Commit changes without pre-commit hook | |
| `` w `` | Fazer commit de alterações sem pré-commit | |
| `` A `` | Alterar último commit | |
| `` C `` | Enviar alteração usando um editor Git | |
| `` <c-f> `` | Encontrar commit da base para consertar | Encontre o commit em que as suas mudanças atuais estão se baseando, para alterar/consertar o commit. Isso poupa-te você de ter que olhar pelos commits da sua branch um por um para ver qual commit deve ser alterado/consertado
@@ -70,19 +70,20 @@ Veja a documentação:
| `` o `` | Abrir arquivo | Abrir arquivo no aplicativo padrão. |
| `` i `` | Ignore or exclude file | |
| `` r `` | Atualizar arquivos | |
| `` s `` | Stash | Stash all changes. For other variations of stashing, use the view stash options keybinding. |
| `` S `` | View stash options | View stash options (e.g. stash all, stash staged, stash unstaged). |
| `` s `` | Stash | Stash todas as alterações. Para outras variações de armazenamento, use a fixação de teclas de armazenamento. |
| `` S `` | Ver opções de stash | Ver opções de stash (por exemplo, trash all, stash staged, stash unsttued). |
| `` a `` | Stage completo | Alternar para todos os arquivos na árvore de trabalho |
| `` <enter> `` | Stage lines / Colapso diretório | Se o item selecionado for um arquivo, o foco na exibição de preparo para o estágio de cenas/linhas individuais. Se o item selecionado for um diretório, recolher/expandi-lo. |
| `` d `` | Discard | View options for discarding changes to the selected file. |
| `` d `` | Descartar | Exibir opções para descartar alterações para o arquivo selecionado. |
| `` g `` | View upstream reset options | |
| `` D `` | Reset | View reset options for working tree (e.g. nuking the working tree). |
| `` 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 | Alternar a visualização de arquivo entre layout plano e layout de árvore. Layout plano mostra todos os caminhos de arquivo em uma única lista, layout de árvore agrupa arquivos por diretório. |
| `` <c-t> `` | Abrir ferramenta de diff externa (git difftool) | |
| `` M `` | Abrir ferramenta de merge externa | Execute `git mergetool`. |
| `` f `` | Buscar | Buscar alterações do controle remoto. |
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` - `` | 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 |
| `` 0 `` | Focus main view | |
| `` / `` | Search the current view by text | |
## Branches locais
@@ -93,21 +94,25 @@ Veja a documentação:
| `` i `` | Show git-flow options | |
| `` <space> `` | Verificar | Checar item selecionado |
| `` n `` | Nova branch | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
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). |
| `` o `` | Create pull request | |
| `` O `` | View create pull request options | |
| `` <c-y> `` | Copiar URL do pull request para área de transferência | |
| `` c `` | Checar por nome | Checar por nome. Na caixa de entrada você pode inserir '-' para trocar para a última branch |
| `` F `` | Forçar checagem | Forçar checagem da branch selecionada. Isso irá descartar todas as mudanças no seu diretório de trabalho antes cheque a branch selecionada |
| `` d `` | Delete | View delete options for local/remote branch. |
| `` d `` | Apagar | Ver opções de exclusão para a branch local/remoto. |
| `` r `` | Refazer | Refazer a branch checada na branch selecionada |
| `` M `` | Mesclar | Ver opções para mesclar o item selecionado no branch atual (mesclar regularmente, mesclar squash) |
| `` f `` | Avanço rápido | Encaminhamento rápido de branch selecionada a partir do upstream. |
| `` T `` | New tag | |
| `` s `` | Sort order | |
| `` g `` | Reset | |
| `` g `` | Restaurar | |
| `` R `` | Rename branch | |
| `` u `` | View upstream options | View options relating to the branch's upstream e.g. setting/unsetting the upstream and resetting to the upstream. |
| `` <c-t> `` | Abrir ferramenta de diff externa (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View commits | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
@@ -121,32 +126,34 @@ Veja a documentação:
| `` n `` | Nova branch | |
| `` M `` | Mesclar | Ver opções para mesclar o item selecionado no branch atual (mesclar regularmente, mesclar squash) |
| `` r `` | Refazer | Refazer a branch checada na branch selecionada |
| `` d `` | Delete | Delete the remote branch from the remote. |
| `` u `` | Set as upstream | Set the selected remote branch as the upstream of the checked-out branch. |
| `` d `` | Apagar | Excluir o branch remoto do controle remoto. |
| `` u `` | Definir como upstream | Definir o ramo remoto selecionado como fluxo do branch check-out. |
| `` s `` | Sort order | |
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` g `` | Restaurar | Ver opções de redefinição (soft/mixed/hard) para redefinir para o item selecionado. |
| `` <c-t> `` | Abrir ferramenta de diff externa (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View commits | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
## Commit files
## Commit arquivos
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy path to clipboard | |
| `` y `` | Copy to clipboard | |
| `` c `` | Verificar | Checkout file. This replaces the file in your working tree with the version from the selected commit. |
| `` d `` | Remove | Discard this commit's changes to this file. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes this file. |
| `` c `` | Verificar | Arquivo de check-out. Isso substitui o arquivo em sua árvore de trabalho com a versão do commit selecionado. |
| `` d `` | Remover | Descartar as alterações desse commit para este arquivo. Isso executa uma rebase interativa em segundo plano, então você pode ter um conflito de merge se um commit posterior também alterar este arquivo. |
| `` o `` | Abrir arquivo | Abrir arquivo no aplicativo padrão. |
| `` e `` | Editar | Abrir arquivo no editor externo. |
| `` <c-t> `` | Abrir ferramenta de diff externa (git difftool) | |
| `` <space> `` | Toggle file included in patch | Toggle whether the file is included in the custom patch. See https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
| `` a `` | Toggle all files | Add/remove all commit's files to custom patch. See https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
| `` <enter> `` | Enter file / Toggle directory collapsed | If a file is selected, enter the file so that you can add/remove individual lines to the custom patch. If a directory is selected, toggle the directory. |
| `` <space> `` | Alternar entre o arquivo incluído no patch | Alternar se o arquivo está incluído no patch personalizado. Veja https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
| `` a `` | Alternar todos os arquivos | Adicionar/remover todos os arquivos de commit para atualização personalizada. Consulte https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
| `` <enter> `` | Insira o arquivo / Alternar diretório recolhido | Se um arquivo estiver selecionado, insira o arquivo para que você possa adicionar/remover linhas individuais no patch personalizado. Se um diretório for selecionado, ative o diretório. |
| `` ` `` | Alternar exibição de árvore de arquivo | Alternar a visualização de arquivo entre layout plano e layout de árvore. Layout plano mostra todos os caminhos de arquivo em uma única lista, layout de árvore agrupa arquivos por diretório. |
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` - `` | 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 |
| `` 0 `` | Focus main view | |
| `` / `` | Search the current view by text | |
## Commits
@@ -165,8 +172,8 @@ Veja a documentação:
| `` i `` | Start interactive rebase | Start an interactive rebase for the commits on your branch. This will include all commits from the HEAD commit down to the first merge commit or main branch commit.
If you would instead like to start an interactive rebase from the selected commit, press `e`. |
| `` p `` | Escolher | Marque o commit selecionado para ser escolhido (quando meados da base). Isso significa que o commit será mantido ao continuar o rebase. |
| `` F `` | Create fixup commit | Create 'fixup!' commit for the selected commit. Later on, you can press `S` on this same commit to apply all above fixup commits. |
| `` S `` | Apply fixup commits | Squash all 'fixup!' commits, either above the selected commit, or all in current branch (autosquash). |
| `` F `` | Criar commit de correção | Crie o commit 'correção!' para o commit selecionado. Mais tarde, você pode pressionar `S` neste mesmo commit para aplicar todas os commits de correção acima. |
| `` S `` | Aplicar commits de correções | Aplicar Squash all 'correção!', seja acima do commit selecionado, ou tudo no branch atual (autosquash). |
| `` <c-j> `` | Mover commit um para baixo | |
| `` <c-k> `` | Mover o commit um para cima | |
| `` V `` | Colar (cherry-pick) | |
@@ -180,10 +187,15 @@ If you would instead like to start an interactive rebase from the selected commi
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Open commit in browser | |
| `` n `` | Create new branch off of commit | |
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` 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.
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 `` | Restaurar | Ver opções de redefinição (soft/mixed/hard) para redefinir para o item selecionado. |
| `` C `` | Copiar (cherry-pick) | Marcar commit como copiado. Então, dentro da visualização local de commits, você pode pressionar `V` para colar (cherry-pick) o(s) commit(s) copiado(s) em seu branch de check-out. A qualquer momento você pode pressionar `<esc>` para cancelar a seleção. |
| `` <c-t> `` | Abrir ferramenta de diff externa (git difftool) | |
| `` <enter> `` | View files | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Ver arquivos | |
| `` w `` | View worktree options | |
| `` / `` | Search the current view by text | |
@@ -201,10 +213,11 @@ If you would instead like to start an interactive rebase from the selected commi
| `` <c-o> `` | Copy tag to clipboard | |
| `` <space> `` | Verificar | Checar a tag selecionada como um HEAD, desanexado |
| `` n `` | New tag | Create new tag from current commit. You'll be prompted to enter a tag name and optional description. |
| `` d `` | Delete | View delete options for local/remote tag. |
| `` d `` | Apagar | Ver opções de exclusão para tag local/remoto. |
| `` P `` | Push tag | Push the selected tag to a remote. You'll be prompted to select a remote. |
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` g `` | Restaurar | Ver opções de redefinição (soft/mixed/hard) para redefinir para o item selecionado. |
| `` <c-t> `` | Abrir ferramenta de diff externa (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View commits | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
@@ -221,15 +234,18 @@ If you would instead like to start an interactive rebase from the selected commi
| Key | Action | Info |
|-----|--------|-------------|
| `` mouse wheel down (fn+up) `` | Scroll down | |
| `` mouse wheel up (fn+down) `` | Scroll up | |
| `` mouse wheel down (fn+up) `` | Rolar para baixo | |
| `` mouse wheel up (fn+down) `` | Rolar para cima | |
| `` <tab> `` | Mudar de visão | Alternar para outra visão (staged/não processadas alterações). |
| `` <esc> `` | Exit back to side panel | |
| `` / `` | Search the current view by text | |
## Painel Principal (preparação)
| Key | Action | Info |
|-----|--------|-------------|
| `` <left> `` | Go to previous hunk | |
| `` <right> `` | Go to next hunk | |
| `` <left> `` | Ir para o local anterior | |
| `` <right> `` | Ir para o próximo trecho | |
| `` v `` | Toggle range select | |
| `` a `` | Selecione o local | Ativa/desativa modo seleção de hunk |
| `` <c-o> `` | Copy selected text to clipboard | |
@@ -241,7 +257,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` <tab> `` | Mudar de visão | Alternar para outra visão (staged/não processadas alterações). |
| `` E `` | Editar hunk | Editar o local selecionado no editor externo. |
| `` c `` | Commit | Submeter mudanças em staging |
| `` w `` | Commit changes without pre-commit hook | |
| `` w `` | Fazer commit de alterações sem pré-commit | |
| `` C `` | Enviar alteração usando um editor Git | |
| `` <c-f> `` | Encontrar commit da base para consertar | Encontre o commit em que as suas mudanças atuais estão se baseando, para alterar/consertar o commit. Isso poupa-te você de ter que olhar pelos commits da sua branch um por um para ver qual commit deve ser alterado/consertado
Veja a documentação:
@@ -254,10 +270,10 @@ Veja a documentação:
|-----|--------|-------------|
| `` <space> `` | Escolha o local | |
| `` b `` | Pegar todos os pedaços | |
| `` <up> `` | Previous hunk | |
| `` <down> `` | Next hunk | |
| `` <left> `` | Previous conflict | |
| `` <right> `` | Next conflict | |
| `` <up> `` | Trecho anterior | |
| `` <down> `` | Próximo trecho | |
| `` <left> `` | Conflito anterior | |
| `` <right> `` | Próximo conflito | |
| `` 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. |
@@ -268,15 +284,15 @@ Veja a documentação:
| Key | Action | Info |
|-----|--------|-------------|
| `` <left> `` | Go to previous hunk | |
| `` <right> `` | Go to next hunk | |
| `` <left> `` | Ir para o local anterior | |
| `` <right> `` | Ir para o próximo trecho | |
| `` v `` | Toggle range select | |
| `` a `` | Selecione o local | Ativa/desativa modo seleção de hunk |
| `` <c-o> `` | Copy selected text to clipboard | |
| `` o `` | Abrir arquivo | Abrir arquivo no aplicativo padrão. |
| `` e `` | Editar arquivo | Abrir arquivo no editor externo. |
| `` <space> `` | Alternar linhas no caminho | |
| `` <esc> `` | Exit custom patch builder | |
| `` <esc> `` | Sair do construtor de patch personalizado | |
| `` / `` | Search the current view by text | |
## Reflog
@@ -288,10 +304,15 @@ Veja a documentação:
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Open commit in browser | |
| `` n `` | Create new branch off of commit | |
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` 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.
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 `` | Restaurar | Ver opções de redefinição (soft/mixed/hard) para redefinir para o item selecionado. |
| `` C `` | Copiar (cherry-pick) | Marcar commit como copiado. Então, dentro da visualização local de commits, você pode pressionar `V` para colar (cherry-pick) o(s) commit(s) copiado(s) em seu branch de check-out. A qualquer momento você pode pressionar `<esc>` para cancelar a seleção. |
| `` <c-r> `` | Reset copied (cherry-picked) commits selection | |
| `` <c-t> `` | Abrir ferramenta de diff externa (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | View commits | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
@@ -300,13 +321,21 @@ Veja a documentação:
| Key | Action | Info |
|-----|--------|-------------|
| `` <enter> `` | View branches | |
| `` n `` | New remote | |
| `` d `` | Remove | Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected. |
| `` <enter> `` | Ver branches | |
| `` n `` | Novo controle | |
| `` d `` | Remover | Remover o controle remoto. Quaisquer ramificações locais de rastreamento de um ramo remoto do controle não serão afetadas. |
| `` e `` | Editar | Edit the selected remote's name or URL. |
| `` f `` | Buscar | Fetch updates from the remote repository. This retrieves new commits and branches without merging them into your local branches. |
| `` / `` | Filter the current view by text | |
## Secundário
| Key | Action | Info |
|-----|--------|-------------|
| `` <tab> `` | Mudar de visão | Alternar para outra visão (staged/não processadas alterações). |
| `` <esc> `` | Exit back to side panel | |
| `` / `` | Search the current view by text | |
## Stash
| Key | Action | Info |
@@ -316,7 +345,8 @@ Veja a documentação:
| `` d `` | Descartar | Remova a entrada do stash da lista de armazenamento. |
| `` n `` | Nova branch | Criar um novo ramo a partir da entrada de lixo selecionada. Isso funciona verificando o commit do qual a entrada de lixo foi criada, criar um novo branch a partir desse commit e, em seguida, aplicar a entrada de lixo ao novo branch como um commit adicional. |
| `` r `` | Renomear o stasj | |
| `` <enter> `` | View files | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Ver arquivos | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
@@ -328,7 +358,7 @@ Veja a documentação:
| `` e `` | Editar arquivo de configuração | Abrir arquivo no editor externo. |
| `` u `` | Verificar atualização | |
| `` <enter> `` | Mudar para um repositório recente | |
| `` a `` | Mostrar todos os logs da branch | |
| `` a `` | Mostrar/ciclo todos os logs de filiais | |
## Sub-commits
@@ -339,11 +369,16 @@ Veja a documentação:
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Open commit in browser | |
| `` n `` | Create new branch off of commit | |
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` 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.
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 `` | Restaurar | Ver opções de redefinição (soft/mixed/hard) para redefinir para o item selecionado. |
| `` C `` | Copiar (cherry-pick) | Marcar commit como copiado. Então, dentro da visualização local de commits, você pode pressionar `V` para colar (cherry-pick) o(s) commit(s) copiado(s) em seu branch de check-out. A qualquer momento você pode pressionar `<esc>` para cancelar a seleção. |
| `` <c-r> `` | Reset copied (cherry-picked) commits selection | |
| `` <c-t> `` | Abrir ferramenta de diff externa (git difftool) | |
| `` <enter> `` | View files | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Ver arquivos | |
| `` w `` | View worktree options | |
| `` / `` | Search the current view by text | |
@@ -353,7 +388,7 @@ Veja a documentação:
|-----|--------|-------------|
| `` <c-o> `` | Copy submodule name to clipboard | |
| `` <enter> `` | Enter | Enter submodule. After entering the submodule, you can press `<esc>` to escape back to the parent repo. |
| `` d `` | Remove | Remove the selected submodule and its corresponding directory. |
| `` d `` | Remover | Remove the selected submodule and its corresponding directory. |
| `` u `` | Update | Update selected submodule. |
| `` n `` | New submodule | |
| `` e `` | Update submodule URL | |
@@ -375,5 +410,5 @@ Veja a documentação:
| `` n `` | New worktree | |
| `` <space> `` | Switch | Switch to the selected worktree. |
| `` o `` | Abrir no editor | |
| `` d `` | Remove | Remove the selected worktree. This will both delete the worktree's directory, as well as metadata about the worktree in the .git directory. |
| `` d `` | Remover | Remove the selected worktree. This will both delete the worktree's directory, as well as metadata about the worktree in the .git directory. |
| `` / `` | Filter the current view by text | |
+38 -3
View File
@@ -40,8 +40,8 @@ _Связки клавиш_
|-----|--------|-------------|
| `` , `` | Предыдущая страница | |
| `` . `` | Следующая страница | |
| `` < `` | Пролистать наверх | |
| `` > `` | Прокрутить вниз | |
| `` < (<home>) `` | Пролистать наверх | |
| `` > (<end>) `` | Прокрутить вниз | |
| `` v `` | Переключить выборку перетаскивания | |
| `` <s-down> `` | Range select down | |
| `` <s-up> `` | Range select up | |
@@ -61,6 +61,14 @@ _Связки клавиш_
| `` d `` | Remove | Remove the selected worktree. This will both delete the worktree's directory, as well as metadata about the worktree in the .git directory. |
| `` / `` | Filter the current view by text | |
## Вторичный
| Key | Action | Info |
|-----|--------|-------------|
| `` <tab> `` | Переключиться на другую панель (проиндексированные/непроиндексированные изменения) | Switch to other view (staged/unstaged changes). |
| `` <esc> `` | Exit back to side panel | |
| `` / `` | Найти | |
## Главная панель (Индексирование)
| Key | Action | Info |
@@ -89,6 +97,9 @@ _Связки клавиш_
|-----|--------|-------------|
| `` mouse wheel down (fn+up) `` | Прокрутить вниз | |
| `` mouse wheel up (fn+down) `` | Прокрутить вверх | |
| `` <tab> `` | Переключиться на другую панель (проиндексированные/непроиндексированные изменения) | Switch to other view (staged/unstaged changes). |
| `` <esc> `` | Exit back to side panel | |
| `` / `` | Найти | |
## Главная панель (Слияние)
@@ -130,10 +141,15 @@ _Связки клавиш_
| `` y `` | Скопировать атрибут коммита | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Открыть коммит в браузере | |
| `` n `` | Создать новую ветку с этого коммита | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
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-t> `` | Open external diff tool (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Просмотреть коммиты | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
@@ -169,9 +185,14 @@ If you would instead like to start an interactive rebase from the selected commi
| `` y `` | Скопировать атрибут коммита | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Открыть коммит в браузере | |
| `` n `` | Создать новую ветку с этого коммита | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
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-t> `` | Open external diff tool (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Просмотреть файлы выбранного элемента | |
| `` w `` | View worktree options | |
| `` / `` | Найти | |
@@ -184,6 +205,9 @@ If you would instead like to start an interactive rebase from the selected commi
| `` i `` | Показать параметры git-flow | |
| `` <space> `` | Переключить | Checkout selected item. |
| `` n `` | Новая ветка | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
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). |
| `` o `` | Создать запрос на принятие изменений | |
| `` O `` | Создать параметры запроса принятие изменений | |
| `` <c-y> `` | Скопировать URL запроса на принятие изменений в буфер обмена | |
@@ -199,6 +223,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` R `` | Переименовать ветку | |
| `` u `` | View upstream options | View options relating to the branch's upstream e.g. setting/unsetting the upstream and resetting to the upstream. |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Просмотреть коммиты | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
@@ -227,10 +252,15 @@ If you would instead like to start an interactive rebase from the selected commi
| `` y `` | Скопировать атрибут коммита | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Открыть коммит в браузере | |
| `` n `` | Создать новую ветку с этого коммита | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
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-t> `` | Open external diff tool (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Просмотреть файлы выбранного элемента | |
| `` w `` | View worktree options | |
| `` / `` | Найти | |
@@ -273,6 +303,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` ` `` | Переключить вид дерева файлов | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory. |
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | Найти | |
## Статус
@@ -283,7 +314,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` e `` | Редактировать файл конфигурации | Open file in external editor. |
| `` u `` | Проверить обновления | |
| `` <enter> `` | Переключиться на последний репозиторий | |
| `` a `` | Показать все логи ветки | |
| `` a `` | Show/cycle all branch logs | |
## Теги
@@ -296,6 +327,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` P `` | Отправить тег | Push the selected tag to a remote. You'll be prompted to select a remote. |
| `` g `` | Reset | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Просмотреть коммиты | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
@@ -314,6 +346,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` s `` | Порядок сортировки | |
| `` g `` | Просмотреть параметры сброса | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` <c-t> `` | Open external diff tool (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Просмотреть коммиты | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
@@ -359,6 +392,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` 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 |
| `` 0 `` | Focus main view | |
| `` / `` | Найти | |
## Хранилище
@@ -370,6 +404,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` d `` | Удалить припрятанные изменения из хранилища | Remove the stash entry from the stash list. |
| `` n `` | Новая ветка | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
| `` r `` | Переименовать хранилище | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Просмотреть файлы выбранного элемента | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
+91 -56
View File
@@ -18,7 +18,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` ( `` | Decrease rename similarity threshold | Decrease the similarity threshold for a deletion and addition pair to be treated as a rename. |
| `` } `` | 扩大差异视图中显示的上下文范围 | 增加diff视图中围绕更改显示的上下文数量 |
| `` { `` | 缩小差异视图中显示的上下文范围 | 减少diff视图中围绕更改显示的上下文数量 |
| `` : `` | Execute shell command | Bring up a prompt where you can enter a shell command to execute. |
| `` : `` | 执行 Shell 命令 | Bring up a prompt where you can enter a shell command to execute. |
| `` <c-p> `` | 查看自定义补丁选项 | |
| `` m `` | 查看 合并/变基 选项 | 查看当前合并或变基的中止、继续、跳过选项 |
| `` R `` | 刷新 | 刷新git状态(即在后台上运行`git status`,`git branch`等命令以更新面板内容) 不会运行`git fetch` |
@@ -31,8 +31,8 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` q `` | 退出 | |
| `` <esc> `` | 取消 | |
| `` <c-w> `` | 切换是否在差异视图中显示空白字符差异 | 切换是否在diff视图中显示空白更改 |
| `` z `` | (通过 reflog)撤销「实验功能」 | Reflog将用于确定运行哪个git命令来撤消最后一个git命令。这并不包括对工作树的更改,只考虑提交。 |
| `` <c-z> `` | (通过 reflog)重做「实验功能」 | Reflog将用于确定运行哪个git命令来重做上一个git命令。这并不包括对工作树的更改,只考虑提交。 |
| `` z `` | 撤销 | Reflog将用于确定运行哪个git命令来撤消最后一个git命令。这并不包括对工作树的更改,只考虑提交。 |
| `` <c-z> `` | 重做 | Reflog将用于确定运行哪个git命令来重做上一个git命令。这并不包括对工作树的更改,只考虑提交。 |
## 列表面板导航
@@ -40,8 +40,8 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
|-----|--------|-------------|
| `` , `` | 上一页 | |
| `` . `` | 下一页 | |
| `` < `` | 滚动到顶部 | |
| `` > `` | 滚动到底部 | |
| `` < (<home>) `` | 滚动到顶部 | |
| `` > (<end>) `` | 滚动到底部 | |
| `` v `` | 切换拖动选择 | |
| `` <s-down> `` | 向下扩展选择范围 | |
| `` <s-up> `` | 向上扩展选择范围 | |
@@ -51,7 +51,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` ] `` | 下一个标签 | |
| `` [ `` | 上一个标签 | |
## Reflog 页面
## Reflog
| Key | Action | Info |
|-----|--------|-------------|
@@ -60,37 +60,15 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` y `` | 复制提交属性到剪贴板 | 复制提交属性到剪贴板(例如,hash、URL、diff、消息、作者)。 |
| `` o `` | 在浏览器中打开提交 | |
| `` n `` | 从提交创建新分支 | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
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 `` | 查看重置选项 | 查看重置选项 (soft/mixed/hard) 用于重置到选择项 |
| `` C `` | 复制提交(拣选) | 标记提交为已复制。然后,在本地提交视图中,你可以按 `V` (Cherry-Pick) 将已复制的提交粘贴到已检出的分支中。任何时候都可以按 `<esc>` 来取消选择。 |
| `` <c-r> `` | 重置已拣选(复制)的提交 | |
| `` <c-t> `` | 使用外部差异比较工具(git difftool) | |
| `` <enter> `` | 查看提交 | |
| `` w `` | 查看工作区选项 | |
| `` / `` | 通过文本过滤当前视图 | |
## 分支页面
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | 将分支名称复制到剪贴板 | |
| `` i `` | 显示 git-flow 选项 | |
| `` <space> `` | 检出 | 检出选中的项目 |
| `` n `` | 新分支 | |
| `` o `` | 创建拉取请求 | |
| `` O `` | 创建拉取请求选项 | |
| `` <c-y> `` | 将拉取请求 URL 复制到剪贴板 | |
| `` c `` | 按名称检出 | 按名称检出。在输入框中,您可以输入'-' 来切换到最后一个分支。 |
| `` F `` | 强制检出 | 强制检出所选分支。这将在检出所选分支之前放弃工作目录中的所有本地更改。 |
| `` d `` | 删除 | 查看本地/远程分支的删除选项 |
| `` r `` | 将已检出的分支变基到该分支 | 将检出的分支变基到所选的分支上。 |
| `` M `` | 合并到当前检出的分支 | Merge selected branch into currently checked out branch. |
| `` f `` | 从上游快进此分支 | 将当前分支直接移动到远程追踪分支的最新提交 |
| `` T `` | 创建标签 | |
| `` s `` | 排序 | |
| `` g `` | 查看重置选项 | |
| `` R `` | 重命名分支 | |
| `` u `` | 查看上游选项 | 查看与分支上游相关的选项,例如设置/取消设置上游和重置为上游。 |
| `` <c-t> `` | 使用外部差异比较工具(git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 查看提交 | |
| `` w `` | 查看工作区选项 | |
| `` / `` | 通过文本过滤当前视图 | |
@@ -104,10 +82,15 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` y `` | 复制提交属性到剪贴板 | 复制提交属性到剪贴板(例如,hash、URL、diff、消息、作者)。 |
| `` o `` | 在浏览器中打开提交 | |
| `` n `` | 从提交创建新分支 | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
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 `` | 查看重置选项 | 查看重置选项 (soft/mixed/hard) 用于重置到选择项 |
| `` C `` | 复制提交(拣选) | 标记提交为已复制。然后,在本地提交视图中,你可以按 `V` (Cherry-Pick) 将已复制的提交粘贴到已检出的分支中。任何时候都可以按 `<esc>` 来取消选择。 |
| `` <c-r> `` | 重置已拣选(复制)的提交 | |
| `` <c-t> `` | 使用外部差异比较工具(git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 查看提交的文件 | |
| `` w `` | 查看工作区选项 | |
| `` / `` | 开始搜索 | |
@@ -144,16 +127,16 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` <c-r> `` | 重置已拣选(复制)的提交 | |
| `` b `` | 查看二分查找选项 | |
| `` s `` | 压缩(Squash) | 将已选提交压缩到该提交之下。这些选定的提交的消息会附加到该提交的消息之下。 |
| `` f `` | 修正(fixup) | 将选定的提交合并到其下面的提交中。与压缩类似,但所选提交的消息将被丢弃。 |
| `` f `` | 修正 (fixup) | 将选定的提交合并到其下面的提交中。与压缩类似,但所选提交的消息将被丢弃。 |
| `` r `` | 改写提交 | 重写所选提交的消息。 |
| `` R `` | 使用编辑器重命名提交 | |
| `` d `` | 删除提交 | 删除选中的提交。这将通过变基从分支中删除该提交,如果该提交修改的内容依赖于后续的提交,则需要解决合并冲突。 |
| `` e `` | 编辑(开始交互式变基) | 编辑提交 |
| `` i `` | 开始交互式变基 | 为分支上的提交启动交互式变基。这将包括从 HEAD 提交到第一个合并提交或主分支提交的所有提交。
如果您想从所选提交启动交互式变基,请按 `e`。 |
| `` p `` | 拣选(Pick) | 选择提交(变基过程中) |
| `` p `` | 拣选(Pick) | 标记选中的提交为 picked(变基过程中)。这意味该提交将在后续的变基中保留。 |
| `` F `` | 为此提交创建修正 | 创建修正提交 |
| `` S `` | 应用该修复提交 | 压缩在所选提交之上的所有“fixup!”提交(自动压缩) |
| `` S `` | 应用该修复提交 | 压缩所选提交之上或当前分支的所有 “fixup!” 提交(自动压缩)。 |
| `` <c-j> `` | 下移提交 | |
| `` <c-k> `` | 上移提交 | |
| `` V `` | 粘贴提交(拣选) | |
@@ -167,9 +150,14 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` y `` | 复制提交属性到剪贴板 | 复制提交属性到剪贴板(例如,hash、URL、diff、消息、作者)。 |
| `` o `` | 在浏览器中打开提交 | |
| `` n `` | 从提交创建新分支 | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
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 `` | 查看重置选项 | 查看重置选项 (soft/mixed/hard) 用于重置到选择项 |
| `` C `` | 复制提交(拣选) | 标记提交为已复制。然后,在本地提交视图中,你可以按 `V` (Cherry-Pick) 将已复制的提交粘贴到已检出的分支中。任何时候都可以按 `<esc>` 来取消选择。 |
| `` <c-t> `` | 使用外部差异比较工具(git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 查看提交的文件 | |
| `` w `` | 查看工作区选项 | |
| `` / `` | 开始搜索 | |
@@ -196,8 +184,9 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` a `` | 操作所有文件 | 添加或删除所有提交中的文件到自定义的补丁中。请参阅 https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches。 |
| `` <enter> `` | 输入文件以将所选行添加到补丁中(或切换目录折叠) | 如果已选择一个文件,则Enter进入该文件,以便您可以向自定义补丁添加/删除单独的行。如果选择了目录,则切换目录。 |
| `` ` `` | 切换文件树视图 | 在平铺部署与树布局之间切换文件视图。平铺布局在一个列表中展示所有文件路径,树布局则根据目录分组展示。 |
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` - `` | 折叠全部文件 | 折叠文件树中的全部目录 |
| `` = `` | 展开全部文件 | 展开文件树中的全部目录 |
| `` 0 `` | Focus main view | |
| `` / `` | 开始搜索 | |
## 文件
@@ -211,7 +200,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` c `` | 提交变更 | 提交暂存文件 |
| `` w `` | 提交变更而无需预先提交钩子 | |
| `` A `` | 修补最后一次提交 | |
| `` C `` | 提交变更(使用编辑器编辑提交信息) | |
| `` C `` | 使用 Git 编辑器提交变更 | |
| `` <c-f> `` | 找到用于修复的基准提交 | 找到您当前变更所基于的提交,以便于修正/改进该提交。这样做可以省去您逐一查看分支提交来确定应该修正/改进哪个提交的麻烦。请参阅文档: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
| `` e `` | 编辑 | 使用外部编辑器打开文件 |
| `` o `` | 打开文件 | 使用默认程序打开该文件 |
@@ -228,10 +217,42 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` <c-t> `` | 使用外部差异比较工具(git difftool) | |
| `` M `` | 打开外部合并工具(git mergetool) | 执行 `git mergetool`. |
| `` f `` | 抓取 | 从远程获取变更 |
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` - `` | 折叠全部文件 | 折叠文件树中的全部目录 |
| `` = `` | 展开全部文件 | 展开文件树中的全部目录 |
| `` 0 `` | Focus main view | |
| `` / `` | 开始搜索 | |
## 本地分支
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | 将分支名称复制到剪贴板 | |
| `` i `` | 显示 git-flow 选项 | |
| `` <space> `` | 检出 | 检出选中的项目 |
| `` n `` | 新分支 | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
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). |
| `` o `` | 创建拉取请求 | |
| `` O `` | 创建拉取请求选项 | |
| `` <c-y> `` | 将拉取请求 URL 复制到剪贴板 | |
| `` c `` | 按名称检出 | 按名称检出。在输入框中,您可以输入'-' 来切换到最后一个分支。 |
| `` F `` | 强制检出 | 强制检出所选分支。这将在检出所选分支之前放弃工作目录中的所有本地更改。 |
| `` d `` | 删除 | 查看本地/远程分支的删除选项 |
| `` r `` | 将已检出的分支变基到该分支 | 将检出的分支变基到所选的分支上。 |
| `` M `` | 合并到当前检出的分支 | Merge selected branch into currently checked out branch. |
| `` f `` | 从上游快进此分支 | 将当前分支直接移动到远程追踪分支的最新提交 |
| `` T `` | 创建标签 | |
| `` s `` | 排序 | |
| `` g `` | 查看重置选项 | |
| `` R `` | 重命名分支 | |
| `` u `` | 查看上游选项 | 查看与分支上游相关的选项,例如设置/取消设置上游和重置为上游。 |
| `` <c-t> `` | 使用外部差异比较工具(git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 查看提交 | |
| `` w `` | 查看工作区选项 | |
| `` / `` | 通过文本过滤当前视图 | |
## 构建补丁中
| Key | Action | Info |
@@ -247,21 +268,30 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` <esc> `` | 退出逐行模式 | |
| `` / `` | 开始搜索 | |
## 标签页面
## 标签
| Key | Action | Info |
|-----|--------|-------------|
| `` <c-o> `` | Copy tag to clipboard | |
| `` <c-o> `` | 将标签复制到剪贴板 | |
| `` <space> `` | 检出 | 检出选择的标签作为分离的HEAD |
| `` n `` | 创建标签 | 基于当前提交创建一个新标签。你将在弹窗中输入标签名称和描述(可选)。 |
| `` d `` | 删除 | 查看本地/远程标签的删除选项 |
| `` P `` | 推送标签 | 推送选择的标签到远端。你将在弹窗中选择一个远端。 |
| `` g `` | 重置 | 查看重置选项 (soft/mixed/hard) 用于重置到选择项 |
| `` <c-t> `` | 使用外部差异比较工具(git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 查看提交 | |
| `` w `` | 查看工作区选项 | |
| `` / `` | 通过文本过滤当前视图 | |
## 次要
| Key | Action | Info |
|-----|--------|-------------|
| `` <tab> `` | 切换到其他面板 | 切换到其他视图(已暂存/未暂存的变更) |
| `` <esc> `` | Exit back to side panel | |
| `` / `` | 开始搜索 | |
## 正在合并
| Key | Action | Info |
@@ -296,7 +326,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` E `` | 编辑代码块 | 在外部编辑器中编辑选中的代码块 |
| `` c `` | 提交变更 | 提交暂存文件 |
| `` w `` | 提交变更而无需预先提交钩子 | |
| `` C `` | 提交变更(使用编辑器编辑提交信息) | |
| `` C `` | 使用 Git 编辑器提交变更 | |
| `` <c-f> `` | 找到用于修复的基准提交 | 找到您当前变更所基于的提交,以便于修正/改进该提交。这样做可以省去您逐一查看分支提交来确定应该修正/改进哪个提交的麻烦。请参阅文档: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
| `` / `` | 开始搜索 | |
@@ -306,6 +336,9 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
|-----|--------|-------------|
| `` mouse wheel down (fn+up) `` | 向下滚动 | |
| `` mouse wheel up (fn+down) `` | 向上滚动 | |
| `` <tab> `` | 切换到其他面板 | 切换到其他视图(已暂存/未暂存的变更) |
| `` <esc> `` | Exit back to side panel | |
| `` / `` | 开始搜索 | |
## 状态
@@ -315,7 +348,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` e `` | 编辑配置文件 | 使用外部编辑器打开文件 |
| `` u `` | 检查更新 | |
| `` <enter> `` | 切换到最近的仓库 | |
| `` a `` | 显示所有分支的日志 | |
| `` a `` | Show/cycle all branch logs | |
## 确认面板
@@ -341,10 +374,22 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` d `` | 删除 | 从贮藏列表中删除该贮藏项 |
| `` n `` | 新分支 | 从选定的贮藏项创建一个新分支。这是通过 git 检查创建贮藏项的提交,从该提交创建一个新分支,然后将贮藏项作为附加提交应用到新分支来实现的。 |
| `` r `` | 重命名贮藏 | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 查看提交的文件 | |
| `` w `` | 查看工作区选项 | |
| `` / `` | 通过文本过滤当前视图 | |
## 远程
| Key | Action | Info |
|-----|--------|-------------|
| `` <enter> `` | 查看分支 | |
| `` n `` | 添加新的远程仓库 | |
| `` d `` | 删除 | 删除选中的远程。从远程跟踪远程分支的任何本地分支都不会受到影响。 |
| `` e `` | 编辑 | 编辑远程仓库 |
| `` f `` | 抓取 | 抓取远程仓库 |
| `` / `` | 通过文本过滤当前视图 | |
## 远程分支
| Key | Action | Info |
@@ -359,17 +404,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` s `` | 排序 | |
| `` g `` | 查看重置选项 | 查看重置选项 (soft/mixed/hard) 用于重置到选择项 |
| `` <c-t> `` | 使用外部差异比较工具(git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 查看提交 | |
| `` w `` | 查看工作区选项 | |
| `` / `` | 通过文本过滤当前视图 | |
## 远程页面
| Key | Action | Info |
|-----|--------|-------------|
| `` <enter> `` | 查看分支 | |
| `` n `` | 添加新的远程仓库 | |
| `` d `` | 删除 | 删除选中的远程。从远程跟踪远程分支的任何本地分支都不会受到影响。 |
| `` e `` | 编辑 | 编辑远程仓库 |
| `` f `` | 抓取 | 抓取远程仓库 |
| `` / `` | 通过文本过滤当前视图 | |
+38 -3
View File
@@ -40,8 +40,8 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B
|-----|--------|-------------|
| `` , `` | 上一頁 | |
| `` . `` | 下一頁 | |
| `` < `` | 捲動到頂部 | |
| `` > `` | 捲動到底部 | |
| `` < (<home>) `` | 捲動到頂部 | |
| `` > (<end>) `` | 捲動到底部 | |
| `` v `` | 切換拖曳選擇 | |
| `` <s-down> `` | Range select down | |
| `` <s-up> `` | Range select up | |
@@ -72,6 +72,9 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B
|-----|--------|-------------|
| `` mouse wheel down (fn+up) `` | 向下捲動 | |
| `` mouse wheel up (fn+down) `` | 向上捲動 | |
| `` <tab> `` | 切換至另一個面板 (已預存/未預存更改) | Switch to other view (staged/unstaged changes). |
| `` <esc> `` | Exit back to side panel | |
| `` / `` | 搜尋 | |
## 主面板(合併)
@@ -128,10 +131,15 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B
| `` y `` | 複製提交屬性 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | 在瀏覽器中開啟提交 | |
| `` n `` | 從提交建立新分支 | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
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 `` | 複製提交 (揀選) | 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> `` | 重設選定的揀選 (複製) 提交 | |
| `` <c-t> `` | 開啟外部差異工具 (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 檢視所選項目的檔案 | |
| `` w `` | 檢視工作目錄選項 | |
| `` / `` | 搜尋 | |
@@ -191,9 +199,14 @@ If you would instead like to start an interactive rebase from the selected commi
| `` y `` | 複製提交屬性 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | 在瀏覽器中開啟提交 | |
| `` n `` | 從提交建立新分支 | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
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 `` | 複製提交 (揀選) | 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-t> `` | 開啟外部差異工具 (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 檢視所選項目的檔案 | |
| `` w `` | 檢視工作目錄選項 | |
| `` / `` | 搜尋 | |
@@ -222,6 +235,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` ` `` | 顯示檔案樹狀視圖 | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory. |
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | 搜尋 | |
## 收藏 (Stash)
@@ -233,6 +247,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` d `` | 捨棄 | Remove the stash entry from the stash list. |
| `` n `` | 新分支 | Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit. |
| `` r `` | 重新命名收藏 | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 檢視所選項目的檔案 | |
| `` w `` | 檢視工作目錄選項 | |
| `` / `` | 搜尋 | |
@@ -246,10 +261,15 @@ If you would instead like to start an interactive rebase from the selected commi
| `` y `` | 複製提交屬性 | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | 在瀏覽器中開啟提交 | |
| `` n `` | 從提交建立新分支 | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
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 `` | 複製提交 (揀選) | 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> `` | 重設選定的揀選 (複製) 提交 | |
| `` <c-t> `` | 開啟外部差異工具 (git difftool) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 檢視提交 | |
| `` w `` | 檢視工作目錄選項 | |
| `` / `` | 搜尋 | |
@@ -262,6 +282,9 @@ If you would instead like to start an interactive rebase from the selected commi
| `` i `` | 顯示 git-flow 選項 | |
| `` <space> `` | 檢出 | 檢出選定的項目。 |
| `` n `` | 新分支 | |
| `` N `` | Move commits to new branch | Create a new branch and move the unpushed commits of the current branch to it. Useful if you meant to start new work and forgot to create a new branch first.
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). |
| `` o `` | 建立拉取請求 | |
| `` O `` | 建立拉取請求選項 | |
| `` <c-y> `` | 複製拉取請求的 URL 到剪貼板 | |
@@ -277,6 +300,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` R `` | 重新命名分支 | |
| `` u `` | 檢視遠端設定 | 檢視有關遠端分支的設定(例如重設至遠端) |
| `` <c-t> `` | 開啟外部差異工具 (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 檢視提交 | |
| `` w `` | 檢視工作目錄選項 | |
| `` / `` | 搜尋 | |
@@ -292,6 +316,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` P `` | 推送標籤 | Push the selected tag to a remote. You'll be prompted to select a remote. |
| `` g `` | 重設 | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` <c-t> `` | 開啟外部差異工具 (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 檢視提交 | |
| `` w `` | 檢視工作目錄選項 | |
| `` / `` | 搜尋 | |
@@ -326,6 +351,15 @@ If you would instead like to start an interactive rebase from the selected commi
| `` f `` | 擷取 | 同步遠端異動 |
| `` - `` | Collapse all files | Collapse all directories in the files tree |
| `` = `` | Expand all files | Expand all directories in the file tree |
| `` 0 `` | Focus main view | |
| `` / `` | 搜尋 | |
## 次要
| Key | Action | Info |
|-----|--------|-------------|
| `` <tab> `` | 切換至另一個面板 (已預存/未預存更改) | Switch to other view (staged/unstaged changes). |
| `` <esc> `` | Exit back to side panel | |
| `` / `` | 搜尋 | |
## 狀態
@@ -336,7 +370,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` e `` | 編輯設定檔案 | 使用外部編輯器開啟 |
| `` u `` | 檢查更新 | |
| `` <enter> `` | 切換到最近使用的版本庫 | |
| `` a `` | 顯示所有分支日誌 | |
| `` a `` | Show/cycle all branch logs | |
## 確認面板
@@ -370,6 +404,7 @@ If you would instead like to start an interactive rebase from the selected commi
| `` s `` | 排序規則 | |
| `` g `` | 檢視重設選項 | View reset options (soft/mixed/hard) for resetting onto selected item. |
| `` <c-t> `` | 開啟外部差異工具 (git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 檢視提交 | |
| `` w `` | 檢視工作目錄選項 | |
| `` / `` | 搜尋 | |
+31 -24
View File
@@ -1,8 +1,9 @@
module github.com/jesseduffield/lazygit
go 1.22
go 1.24.0
require (
dario.cat/mergo v1.0.1
github.com/adrg/xdg v0.4.0
github.com/atotto/clipboard v0.1.4
github.com/aybabtme/humanlog v0.4.1
@@ -11,11 +12,10 @@ require (
github.com/gdamore/tcell/v2 v2.8.1
github.com/go-errors/errors v1.5.1
github.com/gookit/color v1.4.2
github.com/imdario/mergo v0.3.11
github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d
github.com/jesseduffield/gocui v0.3.1-0.20250220081214-b376cb0857ac
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.20250522064656-07eb9218e08e
github.com/jesseduffield/kill v0.0.0-20250101124109-e216ddbe133a
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
@@ -30,51 +30,58 @@ require (
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/sirupsen/logrus v1.4.2
github.com/sirupsen/logrus v1.9.3
github.com/spf13/afero v1.9.5
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
github.com/stefanhaller/git-todo-parser v0.0.7-0.20240406123903-fd957137b6e2
github.com/stretchr/testify v1.8.1
github.com/stefanhaller/git-todo-parser v0.0.7-0.20250429125209-dcf39e4641f5
github.com/stretchr/testify v1.10.0
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
golang.org/x/exp v0.0.0-20220318154914-8dddf5d87bd8
golang.org/x/sync v0.11.0
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
golang.org/x/sync v0.14.0
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
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/cloudflare/circl v1.6.0 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emirpasic/gods v1.12.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fatih/color v1.9.0 // indirect
github.com/gdamore/encoding v1.0.1 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.0.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-logfmt/logfmt v0.5.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/hpcloud/tail v1.0.0 // indirect
github.com/invopop/jsonschema v0.10.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/onsi/ginkgo v1.10.3 // indirect
github.com/onsi/gomega v1.7.1 // indirect
github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // 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.1.0 // 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.2.1 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/term v0.29.0 // indirect
golang.org/x/text v0.22.0 // 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/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.25.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
)
+78 -81
View File
@@ -35,15 +35,20 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
@@ -60,20 +65,25 @@ 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/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -83,24 +93,22 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
github.com/fatih/color v1.7.1-0.20180516100307-2d684516a886/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
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/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
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=
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM=
github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12 h1:PbKy9zOy4aAKrJ5pibIRpVO2BXnK1Tlcg+caKI7Ox5M=
github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -113,6 +121,8 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
@@ -145,8 +155,9 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@@ -173,28 +184,24 @@ github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/integrii/flaggy v1.4.0 h1:A1x7SYx4jqu5NSrY14z8Z+0UyX2S5ygfJJrfolWR3zM=
github.com/integrii/flaggy v1.4.0/go.mod h1:tnTxHeTJbah0gQ6/K0RW0J7fMUBk9MCF5blhm43LNpI=
github.com/invopop/jsonschema v0.10.0 h1:c1ktzNLBun3LyQQhyty5WE3lulbOdIIyOVlkmDLehcE=
github.com/invopop/jsonschema v0.10.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 h1:EQP2Tv8TIcC6Y4RI+1ZbJDOHfGJ570tPeYVCqo7/tws=
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d h1:bO+OmbreIv91rCe8NmscRwhFSqkDJtzWCPV4Y+SQuXE=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
github.com/jesseduffield/gocui v0.3.1-0.20250220081214-b376cb0857ac h1:vUNTiVEB9Bz16pTJ5kNgb/1HhnWdSA1P0GfFLUJeITI=
github.com/jesseduffield/gocui v0.3.1-0.20250220081214-b376cb0857ac/go.mod h1:sLIyZ2J42R6idGdtemZzsiR3xY5EF0KsvYEGh3dQv3s=
github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c h1:tC2PaiisXAC5sOjDPfMArSnbswDObtCssx+xn28edX4=
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.20250522064656-07eb9218e08e h1:NuSXZHAUSo+dSAYArlaDpmKgxVpojuKvNcRkYJcQxkY=
github.com/jesseduffield/gocui v0.3.1-0.20250522064656-07eb9218e08e/go.mod h1:sLIyZ2J42R6idGdtemZzsiR3xY5EF0KsvYEGh3dQv3s=
github.com/jesseduffield/kill v0.0.0-20250101124109-e216ddbe133a h1:UDeJ3EBk04bXDLOPvuqM3on8HvyJfISw0+UMqW+0a4g=
github.com/jesseduffield/kill v0.0.0-20250101124109-e216ddbe133a/go.mod h1:FSWDLKT0NQpntbDd1H3lbz51fhCVlMzy/J0S6nM727Q=
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=
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e/go.mod h1:u60qdFGXRd36jyEXxetz0vQceQIxzI13lIo3EFUDf4I=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
@@ -202,16 +209,15 @@ github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uia
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/karimkhaleel/jsonschema v0.0.0-20231001195015-d933f0d94ea3 h1:s995u+gNQADMaixtNOs+jilRC/Q78q0UXSI7+4T0cDE=
github.com/karimkhaleel/jsonschema v0.0.0-20231001195015-d933f0d94ea3/go.mod h1:MCbEh21gjOzxc31udr3u4QM9DAdf8TFJCZz3u5hYIxA=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -237,20 +243,16 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
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-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY=
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/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/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
@@ -263,6 +265,8 @@ github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
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=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/samber/lo v1.31.0 h1:Sfa+/064Tdo4SvlohQUQzBhgSer9v/coGvKQI/XLWAM=
@@ -271,37 +275,35 @@ github.com/sanity-io/litter v1.5.2 h1:AnC8s9BMORWH5a4atZ4D6FPVvKGzHcnc5/IVTa87my
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/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
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=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/stefanhaller/git-todo-parser v0.0.7-0.20240406123903-fd957137b6e2 h1:RTNWemd/9z9A5L/AggEP3OdhRz5dXETB/wdAlYF0SuM=
github.com/stefanhaller/git-todo-parser v0.0.7-0.20240406123903-fd957137b6e2/go.mod h1:HFt9hGqMzgQ+gVxMKcvTvGaFz4Y0yYycqqAp2V3wcJY=
github.com/stefanhaller/git-todo-parser v0.0.7-0.20250429125209-dcf39e4641f5 h1:ZCI0NPs0xXd00Ej9lX+wwbHjQDkstJa3kUbX7WCOF8I=
github.com/stefanhaller/git-todo-parser v0.0.7-0.20250429125209-dcf39e4641f5/go.mod h1:HFt9hGqMzgQ+gVxMKcvTvGaFz4Y0yYycqqAp2V3wcJY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
github.com/thoas/go-funk v0.9.1/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q=
github.com/urfave/cli v1.20.1-0.20180226030253-8e01ec4cd3e2/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -315,21 +317,20 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -340,8 +341,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20220318154914-8dddf5d87bd8 h1:s/+U+w0teGzcoH2mdIlFQ6KfVKGaYpgyGdUefZrn9TU=
golang.org/x/exp v0.0.0-20220318154914-8dddf5d87bd8/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -372,7 +373,6 @@ golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -409,8 +409,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -436,17 +436,14 @@ 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.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
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-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -475,6 +472,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -484,6 +482,7 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -491,8 +490,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.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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=
@@ -502,8 +501,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.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
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=
@@ -518,8 +517,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.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
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=
@@ -669,8 +668,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
@@ -681,9 +680,7 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+1 -1
View File
@@ -148,7 +148,7 @@ func (app *App) validateGitVersion() (*git_commands.GitVersion, error) {
return nil, minVersionError
}
if version.IsOlderThan(2, 20, 0) {
if version.IsOlderThan(2, 22, 0) {
return nil, minVersionError
}
-36
View File
@@ -8,7 +8,6 @@ import (
"os/exec"
"strconv"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
@@ -33,7 +32,6 @@ const (
DaemonKindExitImmediately
DaemonKindRemoveUpdateRefsForCopiedBranch
DaemonKindCherryPick
DaemonKindMoveTodosUp
DaemonKindMoveTodosDown
DaemonKindInsertBreak
@@ -56,7 +54,6 @@ func getInstruction() Instruction {
mapping := map[DaemonKind]func(string) Instruction{
DaemonKindExitImmediately: deserializeInstruction[*ExitImmediatelyInstruction],
DaemonKindRemoveUpdateRefsForCopiedBranch: deserializeInstruction[*RemoveUpdateRefsForCopiedBranchInstruction],
DaemonKindCherryPick: deserializeInstruction[*CherryPickCommitsInstruction],
DaemonKindChangeTodoActions: deserializeInstruction[*ChangeTodoActionsInstruction],
DaemonKindDropMergeCommit: deserializeInstruction[*DropMergeCommitInstruction],
DaemonKindMoveFixupCommitDown: deserializeInstruction[*MoveFixupCommitDownInstruction],
@@ -180,39 +177,6 @@ func NewRemoveUpdateRefsForCopiedBranchInstruction() Instruction {
return &RemoveUpdateRefsForCopiedBranchInstruction{}
}
type CherryPickCommitsInstruction struct {
Todo string
}
func NewCherryPickCommitsInstruction(commits []*models.Commit) Instruction {
todoLines := lo.Map(commits, func(commit *models.Commit, _ int) TodoLine {
return TodoLine{
Action: "pick",
Commit: commit,
}
})
todo := TodoLinesToString(todoLines)
return &CherryPickCommitsInstruction{
Todo: todo,
}
}
func (self *CherryPickCommitsInstruction) Kind() DaemonKind {
return DaemonKindCherryPick
}
func (self *CherryPickCommitsInstruction) SerializedInstructions() string {
return serializeInstruction(self)
}
func (self *CherryPickCommitsInstruction) run(common *common.Common) error {
return handleInteractiveRebase(common, func(path string) error {
return utils.PrependStrToTodoFile(path, []byte(self.Todo))
})
}
type ChangeTodoActionsInstruction struct {
Changes []ChangeTodoAction
}
+1 -1
View File
@@ -22,7 +22,7 @@ func (self *TodoLine) ToString() string {
if self.Action == "break" {
return self.Action + "\n"
} else {
return self.Action + " " + self.Commit.Hash + " " + self.Commit.Name + "\n"
return self.Action + " " + self.Commit.Hash() + " " + self.Commit.Name + "\n"
}
}
+6 -4
View File
@@ -11,9 +11,12 @@
package cheatsheet
import (
"cmp"
"fmt"
"log"
"os"
"slices"
"strings"
"github.com/jesseduffield/generics/maps"
"github.com/jesseduffield/lazycore/pkg/utils"
@@ -23,7 +26,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/samber/lo"
"golang.org/x/exp/slices"
)
type bindingSection struct {
@@ -164,11 +166,11 @@ func getBindingSections(bindings []*types.Binding, tr *i18n.TranslationSet) []*b
},
)
slices.SortFunc(bindingGroups, func(a, b headerWithBindings) bool {
slices.SortFunc(bindingGroups, func(a, b headerWithBindings) int {
if a.header.priority != b.header.priority {
return a.header.priority > b.header.priority
return cmp.Compare(b.header.priority, a.header.priority)
}
return a.header.title < b.header.title
return strings.Compare(a.header.title, b.header.title)
})
return lo.Map(bindingGroups, func(hb headerWithBindings, _ int) *bindingSection {
+1 -1
View File
@@ -136,7 +136,7 @@ func NewGitCommandAux(
branchLoader := git_commands.NewBranchLoader(cmn, gitCommon, cmd, branchCommands.CurrentBranchInfo, configCommands)
commitFileLoader := git_commands.NewCommitFileLoader(cmn, cmd)
commitLoader := git_commands.NewCommitLoader(cmn, cmd, statusCommands.RebaseMode, gitCommon)
commitLoader := git_commands.NewCommitLoader(cmn, cmd, statusCommands.WorkingTreeState, gitCommon)
reflogCommitLoader := git_commands.NewReflogCommitLoader(cmn, cmd)
remoteLoader := git_commands.NewRemoteLoader(cmn, cmd, repo.Remotes)
worktreeLoader := git_commands.NewWorktreeLoader(gitCommon)
+3 -7
View File
@@ -30,16 +30,12 @@ func NewGitCmdObjBuilder(log *logrus.Entry, innerBuilder *oscommands.CmdObjBuild
var defaultEnvVar = "GIT_OPTIONAL_LOCKS=0"
func (self *gitCmdObjBuilder) New(args []string) oscommands.ICmdObj {
func (self *gitCmdObjBuilder) New(args []string) *oscommands.CmdObj {
return self.innerBuilder.New(args).AddEnvVars(defaultEnvVar)
}
func (self *gitCmdObjBuilder) NewShell(cmdStr string) oscommands.ICmdObj {
return self.innerBuilder.NewShell(cmdStr).AddEnvVars(defaultEnvVar)
}
func (self *gitCmdObjBuilder) NewInteractiveShell(cmdStr string) oscommands.ICmdObj {
return self.innerBuilder.NewInteractiveShell(cmdStr).AddEnvVars(defaultEnvVar)
func (self *gitCmdObjBuilder) NewShell(cmdStr string, shellFunctionsFile string) *oscommands.CmdObj {
return self.innerBuilder.NewShell(cmdStr, shellFunctionsFile).AddEnvVars(defaultEnvVar)
}
func (self *gitCmdObjBuilder) Quote(str string) string {
+4 -4
View File
@@ -20,12 +20,12 @@ type gitCmdObjRunner struct {
innerRunner oscommands.ICmdObjRunner
}
func (self *gitCmdObjRunner) Run(cmdObj oscommands.ICmdObj) error {
func (self *gitCmdObjRunner) Run(cmdObj *oscommands.CmdObj) error {
_, err := self.RunWithOutput(cmdObj)
return err
}
func (self *gitCmdObjRunner) RunWithOutput(cmdObj oscommands.ICmdObj) (string, error) {
func (self *gitCmdObjRunner) RunWithOutput(cmdObj *oscommands.CmdObj) (string, error) {
var output string
var err error
for i := 0; i < RetryCount; i++ {
@@ -44,7 +44,7 @@ func (self *gitCmdObjRunner) RunWithOutput(cmdObj oscommands.ICmdObj) (string, e
return output, err
}
func (self *gitCmdObjRunner) RunWithOutputs(cmdObj oscommands.ICmdObj) (string, string, error) {
func (self *gitCmdObjRunner) RunWithOutputs(cmdObj *oscommands.CmdObj) (string, string, error) {
var stdout, stderr string
var err error
for i := 0; i < RetryCount; i++ {
@@ -64,6 +64,6 @@ func (self *gitCmdObjRunner) RunWithOutputs(cmdObj oscommands.ICmdObj) (string,
}
// Retry logic not implemented here, but these commands typically don't need to obtain a lock.
func (self *gitCmdObjRunner) RunAndProcessLines(cmdObj oscommands.ICmdObj, onLine func(line string) (bool, error)) error {
func (self *gitCmdObjRunner) RunAndProcessLines(cmdObj *oscommands.CmdObj, onLine func(line string) (bool, error)) error {
return self.innerRunner.RunAndProcessLines(cmdObj, onLine)
}
+30 -11
View File
@@ -40,6 +40,15 @@ func (self *BranchCommands) NewWithoutTracking(name string, base string) error {
return self.cmd.New(cmdArgs).Run()
}
// NewWithoutCheckout creates a new branch without checking it out
func (self *BranchCommands) NewWithoutCheckout(name string, base string) error {
cmdArgs := NewGitCmd("branch").
Arg(name, base).
ToArgv()
return self.cmd.New(cmdArgs).Run()
}
// CreateWithUpstream creates a new branch with a given upstream, but without
// checking it out
func (self *BranchCommands) CreateWithUpstream(name string, upstream string) error {
@@ -145,7 +154,7 @@ func (self *BranchCommands) GetGraph(branchName string) (string, error) {
return self.GetGraphCmdObj(branchName).DontLog().RunWithOutput()
}
func (self *BranchCommands) GetGraphCmdObj(branchName string) oscommands.ICmdObj {
func (self *BranchCommands) GetGraphCmdObj(branchName string) *oscommands.CmdObj {
branchLogCmdTemplate := self.UserConfig().Git.BranchLogCmd
templateValues := map[string]string{
"branchName": self.cmd.Quote(branchName),
@@ -246,22 +255,24 @@ func (self *BranchCommands) Merge(branchName string, opts MergeOpts) error {
return self.cmd.New(cmdArgs).Run()
}
func (self *BranchCommands) AllBranchesLogCmdObj() oscommands.ICmdObj {
// Only choose between non-empty, non-identical commands
candidates := lo.Uniq(lo.WithoutEmpty(append([]string{
self.UserConfig().Git.AllBranchesLogCmd,
},
self.UserConfig().Git.AllBranchesLogCmds...,
)))
// Only choose between non-empty, non-identical commands
func (self *BranchCommands) allBranchesLogCandidates() []string {
return lo.Uniq(lo.WithoutEmpty(self.UserConfig().Git.AllBranchesLogCmds))
}
n := len(candidates)
func (self *BranchCommands) AllBranchesLogCmdObj() *oscommands.CmdObj {
candidates := self.allBranchesLogCandidates()
i := self.allBranchesLogCmdIndex
self.allBranchesLogCmdIndex = uint8((int(i) + 1) % n)
return self.cmd.New(str.ToArgv(candidates[i])).DontLog()
}
func (self *BranchCommands) RotateAllBranchesLogIdx() {
n := len(self.allBranchesLogCandidates())
i := self.allBranchesLogCmdIndex
self.allBranchesLogCmdIndex = uint8((int(i) + 1) % n)
}
func (self *BranchCommands) IsBranchMerged(branch *models.Branch, mainBranches *MainBranches) (bool, error) {
branchesToCheckAgainst := []string{"HEAD"}
if branch.RemoteBranchStoredLocally() {
@@ -285,3 +296,11 @@ func (self *BranchCommands) IsBranchMerged(branch *models.Branch, mainBranches *
return stdout == "", nil
}
func (self *BranchCommands) UpdateBranchRefs(updateCommands string) error {
cmdArgs := NewGitCmd("update-ref").
Arg("--stdin").
ToArgv()
return self.cmd.New(cmdArgs).SetStdin(updateCommands).Run()
}
+8 -13
View File
@@ -3,6 +3,7 @@ package git_commands
import (
"fmt"
"regexp"
"slices"
"strconv"
"strings"
"time"
@@ -14,7 +15,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
"golang.org/x/exp/slices"
"golang.org/x/sync/errgroup"
)
@@ -72,7 +72,7 @@ func (self *BranchLoader) Load(reflogCommits []*models.Commit,
onWorker func(func() error),
renderFunc func(),
) ([]*models.Branch, error) {
branches := self.obtainBranches(self.version.IsAtLeast(2, 22, 0))
branches := self.obtainBranches()
if self.AppState.LocalBranchSortOrder == "recency" {
reflogBranches := self.obtainReflogBranches(reflogCommits)
@@ -95,8 +95,8 @@ func (self *BranchLoader) Load(reflogCommits []*models.Commit,
// Sort branches that don't have a recency value alphabetically
// (we're really doing this for the sake of deterministic behaviour across git versions)
slices.SortFunc(branches, func(a *models.Branch, b *models.Branch) bool {
return a.Name < b.Name
slices.SortFunc(branches, func(a *models.Branch, b *models.Branch) int {
return strings.Compare(a.Name, b.Name)
})
branches = utils.Prepend(branches, branchesWithRecency...)
@@ -232,7 +232,7 @@ func (self *BranchLoader) GetBaseBranch(branch *models.Branch, mainBranches *Mai
return split[0], nil
}
func (self *BranchLoader) obtainBranches(canUsePushTrack bool) []*models.Branch {
func (self *BranchLoader) obtainBranches() []*models.Branch {
output, err := self.getRawBranches()
if err != nil {
panic(err)
@@ -255,7 +255,7 @@ func (self *BranchLoader) obtainBranches(canUsePushTrack bool) []*models.Branch
}
storeCommitDateAsRecency := self.AppState.LocalBranchSortOrder != "recency"
return obtainBranch(split, storeCommitDateAsRecency, canUsePushTrack), true
return obtainBranch(split, storeCommitDateAsRecency), true
})
}
@@ -298,7 +298,7 @@ var branchFields = []string{
}
// Obtain branch information from parsed line output of getRawBranches()
func obtainBranch(split []string, storeCommitDateAsRecency bool, canUsePushTrack bool) *models.Branch {
func obtainBranch(split []string, storeCommitDateAsRecency bool) *models.Branch {
headMarker := split[0]
fullName := split[1]
upstreamName := split[2]
@@ -310,12 +310,7 @@ func obtainBranch(split []string, storeCommitDateAsRecency bool, canUsePushTrack
name := strings.TrimPrefix(fullName, "heads/")
aheadForPull, behindForPull, gone := parseUpstreamInfo(upstreamName, track)
var aheadForPush, behindForPush string
if canUsePushTrack {
aheadForPush, behindForPush, _ = parseUpstreamInfo(upstreamName, pushTrack)
} else {
aheadForPush, behindForPush = aheadForPull, behindForPull
}
aheadForPush, behindForPush, _ := parseUpstreamInfo(upstreamName, pushTrack)
recency := ""
if storeCommitDateAsRecency {
@@ -119,7 +119,7 @@ func TestObtainBranch(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
branch := obtainBranch(s.input, s.storeCommitDateAsRecency, true)
branch := obtainBranch(s.input, s.storeCommitDateAsRecency)
assert.EqualValues(t, s.expectedBranch, branch)
})
}
+23 -18
View File
@@ -85,13 +85,11 @@ func (self *CommitCommands) ResetToCommit(hash string, strength string, envVars
Run()
}
func (self *CommitCommands) CommitCmdObj(summary string, description string) oscommands.ICmdObj {
func (self *CommitCommands) CommitCmdObj(summary string, description string, forceSkipHooks bool) *oscommands.CmdObj {
messageArgs := self.commitMessageArgs(summary, description)
skipHookPrefix := self.UserConfig().Git.SkipHookPrefix
cmdArgs := NewGitCmd("commit").
ArgIf(skipHookPrefix != "" && strings.HasPrefix(summary, skipHookPrefix), "--no-verify").
ArgIf(forceSkipHooks || (skipHookPrefix != "" && strings.HasPrefix(summary, skipHookPrefix)), "--no-verify").
ArgIf(self.signoffFlag() != "", self.signoffFlag()).
Arg(messageArgs...).
ToArgv()
@@ -99,17 +97,18 @@ func (self *CommitCommands) CommitCmdObj(summary string, description string) osc
return self.cmd.New(cmdArgs)
}
func (self *CommitCommands) RewordLastCommitInEditorCmdObj() oscommands.ICmdObj {
func (self *CommitCommands) RewordLastCommitInEditorCmdObj() *oscommands.CmdObj {
return self.cmd.New(NewGitCmd("commit").Arg("--allow-empty", "--amend", "--only").ToArgv())
}
func (self *CommitCommands) RewordLastCommitInEditorWithMessageFileCmdObj(tmpMessageFile string) oscommands.ICmdObj {
func (self *CommitCommands) RewordLastCommitInEditorWithMessageFileCmdObj(tmpMessageFile string) *oscommands.CmdObj {
return self.cmd.New(NewGitCmd("commit").
Arg("--allow-empty", "--amend", "--only", "--edit", "--file="+tmpMessageFile).ToArgv())
}
func (self *CommitCommands) CommitInEditorWithMessageFileCmdObj(tmpMessageFile string) oscommands.ICmdObj {
func (self *CommitCommands) CommitInEditorWithMessageFileCmdObj(tmpMessageFile string, forceSkipHooks bool) *oscommands.CmdObj {
return self.cmd.New(NewGitCmd("commit").
ArgIf(forceSkipHooks, "--no-verify").
Arg("--edit").
Arg("--file="+tmpMessageFile).
ArgIf(self.signoffFlag() != "", self.signoffFlag()).
@@ -117,7 +116,7 @@ func (self *CommitCommands) CommitInEditorWithMessageFileCmdObj(tmpMessageFile s
}
// RewordLastCommit rewords the topmost commit with the given message
func (self *CommitCommands) RewordLastCommit(summary string, description string) oscommands.ICmdObj {
func (self *CommitCommands) RewordLastCommit(summary string, description string) *oscommands.CmdObj {
messageArgs := self.commitMessageArgs(summary, description)
cmdArgs := NewGitCmd("commit").
@@ -139,7 +138,7 @@ func (self *CommitCommands) commitMessageArgs(summary string, description string
}
// runs git commit without the -m argument meaning it will invoke the user's editor
func (self *CommitCommands) CommitEditorCmdObj() oscommands.ICmdObj {
func (self *CommitCommands) CommitEditorCmdObj() *oscommands.CmdObj {
cmdArgs := NewGitCmd("commit").
ArgIf(self.signoffFlag() != "", self.signoffFlag()).
ToArgv()
@@ -247,7 +246,7 @@ func (self *CommitCommands) AmendHead() error {
return self.AmendHeadCmdObj().Run()
}
func (self *CommitCommands) AmendHeadCmdObj() oscommands.ICmdObj {
func (self *CommitCommands) AmendHeadCmdObj() *oscommands.CmdObj {
cmdArgs := NewGitCmd("commit").
Arg("--amend", "--no-edit", "--allow-empty").
ToArgv()
@@ -255,7 +254,7 @@ func (self *CommitCommands) AmendHeadCmdObj() oscommands.ICmdObj {
return self.cmd.New(cmdArgs)
}
func (self *CommitCommands) ShowCmdObj(hash string, filterPath string) oscommands.ICmdObj {
func (self *CommitCommands) ShowCmdObj(hash string, filterPath string) *oscommands.CmdObj {
contextSize := self.AppState.DiffContextSize
extDiffCmd := self.UserConfig().Git.Paging.ExternalDiffCommand
@@ -279,15 +278,21 @@ func (self *CommitCommands) ShowCmdObj(hash string, filterPath string) oscommand
return self.cmd.New(cmdArgs).DontLog()
}
// Revert reverts the selected commit by hash
func (self *CommitCommands) Revert(hash string) error {
cmdArgs := NewGitCmd("revert").Arg(hash).ToArgv()
return self.cmd.New(cmdArgs).Run()
func (self *CommitCommands) ShowFileContentCmdObj(hash string, filePath string) *oscommands.CmdObj {
cmdArgs := NewGitCmd("show").
Arg(fmt.Sprintf("%s:%s", hash, filePath)).
ToArgv()
return self.cmd.New(cmdArgs).DontLog()
}
func (self *CommitCommands) RevertMerge(hash string, parentNumber int) error {
cmdArgs := NewGitCmd("revert").Arg(hash, "-m", fmt.Sprintf("%d", parentNumber)).
// Revert reverts the selected commits by hash. If isMerge is true, we'll pass -m 1
// to say we want to revert the first parent of the merge commit, which is the one
// people want in 99.9% of cases. In current git versions we could unconditionally
// pass -m 1 even for non-merge commits, but older versions of git choke on it.
func (self *CommitCommands) Revert(hashes []string, isMerge bool) error {
cmdArgs := NewGitCmd("revert").
ArgIf(isMerge, "-m", "1").
Arg(hashes...).
ToArgv()
return self.cmd.New(cmdArgs).Run()
@@ -55,7 +55,7 @@ func getCommitFilesFromFilenames(filenames string) []*models.CommitFile {
return lo.Map(lo.Chunk(lines, 2), func(chunk []string, _ int) *models.CommitFile {
return &models.CommitFile{
ChangeStatus: chunk[0],
Name: chunk[1],
Path: chunk[1],
}
})
}
@@ -23,7 +23,7 @@ func TestGetCommitFilesFromFilenames(t *testing.T) {
input: "MM\x00Myfile\x00",
output: []*models.CommitFile{
{
Name: "Myfile",
Path: "Myfile",
ChangeStatus: "MM",
},
},
@@ -33,11 +33,11 @@ func TestGetCommitFilesFromFilenames(t *testing.T) {
input: "MM\x00Myfile\x00M \x00MyOtherFile\x00",
output: []*models.CommitFile{
{
Name: "Myfile",
Path: "Myfile",
ChangeStatus: "MM",
},
{
Name: "MyOtherFile",
Path: "MyOtherFile",
ChangeStatus: "M ",
},
},
@@ -47,15 +47,15 @@ func TestGetCommitFilesFromFilenames(t *testing.T) {
input: "MM\x00Myfile\x00M \x00MyOtherFile\x00 M\x00YetAnother\x00",
output: []*models.CommitFile{
{
Name: "Myfile",
Path: "Myfile",
ChangeStatus: "MM",
},
{
Name: "MyOtherFile",
Path: "MyOtherFile",
ChangeStatus: "M ",
},
{
Name: "YetAnother",
Path: "YetAnother",
ChangeStatus: " M",
},
},
+168 -82
View File
@@ -13,7 +13,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
@@ -31,10 +30,10 @@ type CommitLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
getRebaseMode func() (enums.RebaseMode, error)
readFile func(filename string) ([]byte, error)
walkFiles func(root string, fn filepath.WalkFunc) error
dotGitDir string
getWorkingTreeState func() models.WorkingTreeState
readFile func(filename string) ([]byte, error)
walkFiles func(root string, fn filepath.WalkFunc) error
dotGitDir string
*GitCommon
}
@@ -42,16 +41,16 @@ type CommitLoader struct {
func NewCommitLoader(
cmn *common.Common,
cmd oscommands.ICmdObjBuilder,
getRebaseMode func() (enums.RebaseMode, error),
getWorkingTreeState func() models.WorkingTreeState,
gitCommon *GitCommon,
) *CommitLoader {
return &CommitLoader{
Common: cmn,
cmd: cmd,
getRebaseMode: getRebaseMode,
readFile: os.ReadFile,
walkFiles: filepath.Walk,
GitCommon: gitCommon,
Common: cmn,
cmd: cmd,
getWorkingTreeState: getWorkingTreeState,
readFile: os.ReadFile,
walkFiles: filepath.Walk,
GitCommon: gitCommon,
}
}
@@ -67,20 +66,19 @@ type GetCommitsOptions struct {
// If non-empty, show divergence from this ref (left-right log)
RefToShowDivergenceFrom string
MainBranches *MainBranches
HashPool *utils.StringPool
}
// GetCommits obtains the commits of the current branch
func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit, error) {
commits := []*models.Commit{}
var rebasingCommits []*models.Commit
if opts.IncludeRebaseCommits && opts.FilterPath == "" {
var err error
rebasingCommits, err = self.MergeRebasingCommits(commits)
commits, err = self.MergeRebasingCommits(opts.HashPool, commits)
if err != nil {
return nil, err
}
commits = append(commits, rebasingCommits...)
}
wg := sync.WaitGroup{}
@@ -92,7 +90,7 @@ func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit,
defer wg.Done()
logErr = self.getLogCmd(opts).RunAndProcessLines(func(line string) (bool, error) {
commit := self.extractCommitFromLine(line, opts.RefToShowDivergenceFrom != "")
commit := self.extractCommitFromLine(opts.HashPool, line, opts.RefToShowDivergenceFrom != "")
commits = append(commits, commit)
return false, nil
})
@@ -124,10 +122,10 @@ func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit,
}
for _, commit := range commits {
if commit.Hash == firstPushedCommit {
if commit.Hash() == firstPushedCommit {
passedFirstPushedCommit = true
}
if commit.Status != models.StatusRebasing {
if !commit.IsTODO() {
if passedFirstPushedCommit {
commit.Status = models.StatusPushed
} else {
@@ -162,7 +160,7 @@ func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit,
return commits, nil
}
func (self *CommitLoader) MergeRebasingCommits(commits []*models.Commit) ([]*models.Commit, error) {
func (self *CommitLoader) MergeRebasingCommits(hashPool *utils.StringPool, commits []*models.Commit) ([]*models.Commit, error) {
// chances are we have as many commits as last time so we'll set the capacity to be the old length
result := make([]*models.Commit, 0, len(commits))
for i, commit := range commits {
@@ -172,24 +170,26 @@ func (self *CommitLoader) MergeRebasingCommits(commits []*models.Commit) ([]*mod
}
}
rebaseMode, err := self.getRebaseMode()
if err != nil {
return nil, err
workingTreeState := self.getWorkingTreeState()
addConflictedRebasingCommit := true
if workingTreeState.CherryPicking || workingTreeState.Reverting {
sequencerCommits, err := self.getHydratedSequencerCommits(hashPool, workingTreeState)
if err != nil {
return nil, err
}
result = append(sequencerCommits, result...)
addConflictedRebasingCommit = false
}
if rebaseMode == enums.REBASE_MODE_NONE {
// not in rebase mode so return original commits
return result, nil
if workingTreeState.Rebasing {
rebasingCommits, err := self.getHydratedRebasingCommits(hashPool, addConflictedRebasingCommit)
if err != nil {
return nil, err
}
if len(rebasingCommits) > 0 {
result = append(rebasingCommits, result...)
}
}
rebasingCommits, err := self.getHydratedRebasingCommits(rebaseMode)
if err != nil {
return nil, err
}
if len(rebasingCommits) > 0 {
result = append(rebasingCommits, result...)
}
return result, nil
}
@@ -197,7 +197,7 @@ func (self *CommitLoader) MergeRebasingCommits(commits []*models.Commit) ([]*mod
// then puts them into a commit object
// example input:
// 8ad01fe32fcc20f07bc6693f87aa4977c327f1e1|10 hours ago|Jesse Duffield| (HEAD -> master, tag: v0.15.2)|refresh commits when adding a tag
func (self *CommitLoader) extractCommitFromLine(line string, showDivergence bool) *models.Commit {
func (self *CommitLoader) extractCommitFromLine(hashPool *utils.StringPool, line string, showDivergence bool) *models.Commit {
split := strings.SplitN(line, "\x00", 8)
hash := split[0]
@@ -235,7 +235,7 @@ func (self *CommitLoader) extractCommitFromLine(line string, showDivergence bool
parents = strings.Split(parentHashes, " ")
}
return &models.Commit{
return models.NewCommit(hashPool, models.NewCommitOpts{
Hash: hash,
Name: message,
Tags: tags,
@@ -245,18 +245,40 @@ func (self *CommitLoader) extractCommitFromLine(line string, showDivergence bool
AuthorEmail: authorEmail,
Parents: parents,
Divergence: divergence,
}
})
}
func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode) ([]*models.Commit, error) {
commits := self.getRebasingCommits(rebaseMode)
func (self *CommitLoader) getHydratedRebasingCommits(hashPool *utils.StringPool, addConflictingCommit bool) ([]*models.Commit, error) {
todoFileHasShortHashes := self.version.IsOlderThan(2, 25, 2)
return self.getHydratedTodoCommits(hashPool, self.getRebasingCommits(hashPool, addConflictingCommit), todoFileHasShortHashes)
}
if len(commits) == 0 {
func (self *CommitLoader) getHydratedSequencerCommits(hashPool *utils.StringPool, workingTreeState models.WorkingTreeState) ([]*models.Commit, error) {
commits := self.getSequencerCommits(hashPool)
if len(commits) > 0 {
// If we have any commits in .git/sequencer/todo, then the last one of
// those is the conflicting one.
commits[len(commits)-1].Status = models.StatusConflicted
} else {
// For single-commit cherry-picks and reverts, git apparently doesn't
// use the sequencer; in that case, CHERRY_PICK_HEAD or REVERT_HEAD is
// our conflicting commit, so synthesize it here.
conflicedCommit := self.getConflictedSequencerCommit(hashPool, workingTreeState)
if conflicedCommit != nil {
commits = append(commits, conflicedCommit)
}
}
return self.getHydratedTodoCommits(hashPool, commits, true)
}
func (self *CommitLoader) getHydratedTodoCommits(hashPool *utils.StringPool, todoCommits []*models.Commit, todoFileHasShortHashes bool) ([]*models.Commit, error) {
if len(todoCommits) == 0 {
return nil, nil
}
commitHashes := lo.FilterMap(commits, func(commit *models.Commit, _ int) (string, bool) {
return commit.Hash, commit.Hash != ""
commitHashes := lo.FilterMap(todoCommits, func(commit *models.Commit, _ int) (string, bool) {
return commit.Hash(), commit.Hash() != ""
})
// note that we're not filtering these as we do non-rebasing commits just because
@@ -271,15 +293,15 @@ func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode
fullCommits := map[string]*models.Commit{}
err := cmdObj.RunAndProcessLines(func(line string) (bool, error) {
commit := self.extractCommitFromLine(line, false)
fullCommits[commit.Hash] = commit
commit := self.extractCommitFromLine(hashPool, line, false)
fullCommits[commit.Hash()] = commit
return false, nil
})
if err != nil {
return nil, err
}
findFullCommit := lo.Ternary(self.version.IsOlderThan(2, 25, 2),
findFullCommit := lo.Ternary(todoFileHasShortHashes,
func(hash string) *models.Commit {
for s, c := range fullCommits {
if strings.HasPrefix(s, hash) {
@@ -292,11 +314,11 @@ func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode
return fullCommits[hash]
})
hydratedCommits := make([]*models.Commit, 0, len(commits))
for _, rebasingCommit := range commits {
if rebasingCommit.Hash == "" {
hydratedCommits := make([]*models.Commit, 0, len(todoCommits))
for _, rebasingCommit := range todoCommits {
if rebasingCommit.Hash() == "" {
hydratedCommits = append(hydratedCommits, rebasingCommit)
} else if commit := findFullCommit(rebasingCommit.Hash); commit != nil {
} else if commit := findFullCommit(rebasingCommit.Hash()); commit != nil {
commit.Action = rebasingCommit.Action
commit.Status = rebasingCommit.Status
hydratedCommits = append(hydratedCommits, commit)
@@ -310,11 +332,7 @@ func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode
// git-rebase-todo example:
// pick ac446ae94ee560bdb8d1d057278657b251aaef17 ac446ae
// pick afb893148791a2fbd8091aeb81deba4930c73031 afb8931
func (self *CommitLoader) getRebasingCommits(rebaseMode enums.RebaseMode) []*models.Commit {
if rebaseMode != enums.REBASE_MODE_INTERACTIVE {
return nil
}
func (self *CommitLoader) getRebasingCommits(hashPool *utils.StringPool, addConflictingCommit bool) []*models.Commit {
bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/git-rebase-todo"))
if err != nil {
self.Log.Error(fmt.Sprintf("error occurred reading git-rebase-todo: %s", err.Error()))
@@ -332,13 +350,10 @@ func (self *CommitLoader) getRebasingCommits(rebaseMode enums.RebaseMode) []*mod
// See if the current commit couldn't be applied because it conflicted; if
// so, add a fake entry for it
if conflictedCommitHash := self.getConflictedCommit(todos); conflictedCommitHash != "" {
commits = append(commits, &models.Commit{
Hash: conflictedCommitHash,
Name: "",
Status: models.StatusRebasing,
Action: models.ActionConflict,
})
if addConflictingCommit {
if conflictedCommit := self.getConflictedCommit(hashPool, todos); conflictedCommit != nil {
commits = append(commits, conflictedCommit)
}
}
for _, t := range todos {
@@ -350,47 +365,45 @@ func (self *CommitLoader) getRebasingCommits(rebaseMode enums.RebaseMode) []*mod
// Command does not have a commit associated, skip
continue
}
commits = utils.Prepend(commits, &models.Commit{
commits = utils.Prepend(commits, models.NewCommit(hashPool, models.NewCommitOpts{
Hash: t.Commit,
Name: t.Msg,
Status: models.StatusRebasing,
Action: t.Command,
})
}))
}
return commits
}
func (self *CommitLoader) getConflictedCommit(todos []todo.Todo) string {
func (self *CommitLoader) getConflictedCommit(hashPool *utils.StringPool, todos []todo.Todo) *models.Commit {
bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/done"))
if err != nil {
self.Log.Error(fmt.Sprintf("error occurred reading rebase-merge/done: %s", err.Error()))
return ""
return nil
}
doneTodos, err := todo.Parse(bytes.NewBuffer(bytesContent), self.config.GetCoreCommentChar())
if err != nil {
self.Log.Error(fmt.Sprintf("error occurred while parsing rebase-merge/done file: %s", err.Error()))
return ""
return nil
}
amendFileExists := false
if _, err := os.Stat(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/amend")); err == nil {
amendFileExists = true
}
amendFileExists, _ := self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/amend"))
messageFileExists, _ := self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/message"))
return self.getConflictedCommitImpl(todos, doneTodos, amendFileExists)
return self.getConflictedCommitImpl(hashPool, todos, doneTodos, amendFileExists, messageFileExists)
}
func (self *CommitLoader) getConflictedCommitImpl(todos []todo.Todo, doneTodos []todo.Todo, amendFileExists bool) string {
func (self *CommitLoader) getConflictedCommitImpl(hashPool *utils.StringPool, todos []todo.Todo, doneTodos []todo.Todo, amendFileExists bool, messageFileExists bool) *models.Commit {
// Should never be possible, but just to be safe:
if len(doneTodos) == 0 {
self.Log.Error("no done entries in rebase-merge/done file")
return ""
return nil
}
lastTodo := doneTodos[len(doneTodos)-1]
if lastTodo.Command == todo.Break || lastTodo.Command == todo.Exec || lastTodo.Command == todo.Reword {
return ""
return nil
}
// In certain cases, git reschedules commands that failed. One example is if
@@ -401,7 +414,7 @@ func (self *CommitLoader) getConflictedCommitImpl(todos []todo.Todo, doneTodos [
// same, the command was rescheduled.
if len(doneTodos) > 0 && len(todos) > 0 && doneTodos[len(doneTodos)-1] == todos[0] {
// Command was rescheduled, no need to display it
return ""
return nil
}
// Older versions of git have a bug whereby, if a command is rescheduled,
@@ -426,26 +439,99 @@ func (self *CommitLoader) getConflictedCommitImpl(todos []todo.Todo, doneTodos [
if len(doneTodos) >= 3 && len(todos) > 0 && doneTodos[len(doneTodos)-2] == todos[0] &&
doneTodos[len(doneTodos)-1] == doneTodos[len(doneTodos)-3] {
// Command was rescheduled, no need to display it
return ""
return nil
}
if lastTodo.Command == todo.Edit {
if amendFileExists {
// Special case for "edit": if the "amend" file exists, the "edit"
// command was successful, otherwise it wasn't
return ""
return nil
}
if !messageFileExists {
// As an additional check, see if the "message" file exists; if it
// doesn't, it must be because a multi-commit cherry-pick or revert
// was performed in the meantime, which deleted both the amend file
// and the message file.
return nil
}
}
// I don't think this is ever possible, but again, just to be safe:
if lastTodo.Commit == "" {
self.Log.Error("last command in rebase-merge/done file doesn't have a commit")
return ""
return nil
}
// Any other todo that has a commit associated with it must have failed with
// a conflict, otherwise we wouldn't have stopped the rebase:
return lastTodo.Commit
return models.NewCommit(hashPool, models.NewCommitOpts{
Hash: lastTodo.Commit,
Action: lastTodo.Command,
Status: models.StatusConflicted,
})
}
func (self *CommitLoader) getSequencerCommits(hashPool *utils.StringPool) []*models.Commit {
bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "sequencer/todo"))
if err != nil {
self.Log.Error(fmt.Sprintf("error occurred reading sequencer/todo: %s", err.Error()))
// we assume an error means the file doesn't exist so we just return
return nil
}
commits := []*models.Commit{}
todos, err := todo.Parse(bytes.NewBuffer(bytesContent), self.config.GetCoreCommentChar())
if err != nil {
self.Log.Error(fmt.Sprintf("error occurred while parsing sequencer/todo file: %s", err.Error()))
return nil
}
for _, t := range todos {
if t.Commit == "" {
// Command does not have a commit associated, skip
continue
}
commits = utils.Prepend(commits, models.NewCommit(hashPool, models.NewCommitOpts{
Hash: t.Commit,
Name: t.Msg,
Status: models.StatusCherryPickingOrReverting,
Action: t.Command,
}))
}
return commits
}
func (self *CommitLoader) getConflictedSequencerCommit(hashPool *utils.StringPool, workingTreeState models.WorkingTreeState) *models.Commit {
var shaFile string
var action todo.TodoCommand
if workingTreeState.CherryPicking {
shaFile = "CHERRY_PICK_HEAD"
action = todo.Pick
} else if workingTreeState.Reverting {
shaFile = "REVERT_HEAD"
action = todo.Revert
} else {
return nil
}
bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), shaFile))
if err != nil {
self.Log.Error(fmt.Sprintf("error occurred reading %s: %s", shaFile, err.Error()))
// we assume an error means the file doesn't exist so we just return
return nil
}
lines := strings.Split(string(bytesContent), "\n")
if len(lines) == 0 {
return nil
}
return models.NewCommit(hashPool, models.NewCommitOpts{
Hash: lines[0],
Status: models.StatusConflicted,
Action: action,
})
}
func setCommitMergedStatuses(ancestor string, commits []*models.Commit) {
@@ -456,7 +542,7 @@ func setCommitMergedStatuses(ancestor string, commits []*models.Commit) {
passedAncestor := false
for i, commit := range commits {
// some commits aren't really commits and don't have hashes, such as the update-ref todo
if commit.Hash != "" && strings.HasPrefix(ancestor, commit.Hash) {
if commit.Hash() != "" && strings.HasPrefix(ancestor, commit.Hash()) {
passedAncestor = true
}
if commit.Status != models.StatusPushed && commit.Status != models.StatusUnpushed {
@@ -497,7 +583,7 @@ func (self *CommitLoader) getFirstPushedCommit(refName string) (string, error) {
}
// getLog gets the git log.
func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj {
func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) *oscommands.CmdObj {
gitLogOrder := self.AppState.GitLogOrder
refSpec := opts.RefName
@@ -524,4 +610,4 @@ func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj {
return self.cmd.New(cmdArgs).DontLog()
}
const prettyFormat = `--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s`
const prettyFormat = `--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s`
+119 -87
View File
@@ -8,9 +8,10 @@ import (
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
"github.com/stefanhaller/git-todo-parser/todo"
"github.com/stretchr/testify/assert"
)
@@ -28,52 +29,48 @@ var singleCommitOutput = strings.Replace(`0eea75e8c631fba6b58135697835d58ba4c18d
func TestGetCommits(t *testing.T) {
type scenario struct {
testName string
runner *oscommands.FakeCmdObjRunner
expectedCommits []*models.Commit
expectedError error
logOrder string
rebaseMode enums.RebaseMode
opts GetCommitsOptions
mainBranches []string
testName string
runner *oscommands.FakeCmdObjRunner
expectedCommitOpts []models.NewCommitOpts
expectedError error
logOrder string
opts GetCommitsOptions
mainBranches []string
}
scenarios := []scenario{
{
testName: "should return no commits if there are none",
logOrder: "topo-order",
rebaseMode: enums.REBASE_MODE_NONE,
opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", IncludeRebaseCommits: false},
testName: "should return no commits if there are none",
logOrder: "topo-order",
opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", IncludeRebaseCommits: false},
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
expectedCommits: []*models.Commit{},
expectedError: nil,
expectedCommitOpts: []models.NewCommitOpts{},
expectedError: nil,
},
{
testName: "should use proper upstream name for branch",
logOrder: "topo-order",
rebaseMode: enums.REBASE_MODE_NONE,
opts: GetCommitsOptions{RefName: "refs/heads/mybranch", RefForPushedStatus: "refs/heads/mybranch", IncludeRebaseCommits: false},
testName: "should use proper upstream name for branch",
logOrder: "topo-order",
opts: GetCommitsOptions{RefName: "refs/heads/mybranch", RefForPushedStatus: "refs/heads/mybranch", IncludeRebaseCommits: false},
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"merge-base", "refs/heads/mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
ExpectGitArgs([]string{"log", "refs/heads/mybranch", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
ExpectGitArgs([]string{"log", "refs/heads/mybranch", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
expectedCommits: []*models.Commit{},
expectedError: nil,
expectedCommitOpts: []models.NewCommitOpts{},
expectedError: nil,
},
{
testName: "should return commits if they are present",
logOrder: "topo-order",
rebaseMode: enums.REBASE_MODE_NONE,
opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", IncludeRebaseCommits: false},
mainBranches: []string{"master", "main", "develop"},
runner: oscommands.NewFakeRunner(t).
// here it's seeing which commits are yet to be pushed
ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
// here it's actually getting all the commits in a formatted form, one per line
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, commitsOutput, nil).
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, commitsOutput, nil).
// here it's testing which of the configured main branches have an upstream
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "master@{u}"}, "refs/remotes/origin/master", nil). // this one does
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "main@{u}"}, "", errors.New("error")). // this one doesn't, so it checks origin instead
@@ -84,7 +81,7 @@ func TestGetCommits(t *testing.T) {
// here it's seeing where our branch diverged from the master branch so that we can mark that commit and parent commits as 'merged'
ExpectGitArgs([]string{"merge-base", "HEAD", "refs/remotes/origin/master", "refs/remotes/origin/main"}, "26c07b1ab33860a1a7591a0638f9925ccf497ffa", nil),
expectedCommits: []*models.Commit{
expectedCommitOpts: []models.NewCommitOpts{
{
Hash: "0eea75e8c631fba6b58135697835d58ba4c18dbc",
Name: "better typing for rebase mode",
@@ -203,14 +200,13 @@ func TestGetCommits(t *testing.T) {
{
testName: "should not call merge-base for mainBranches if none exist",
logOrder: "topo-order",
rebaseMode: enums.REBASE_MODE_NONE,
opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", IncludeRebaseCommits: false},
mainBranches: []string{"master", "main"},
runner: oscommands.NewFakeRunner(t).
// here it's seeing which commits are yet to be pushed
ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
// here it's actually getting all the commits in a formatted form, one per line
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil).
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil).
// here it's testing which of the configured main branches exist; neither does
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "master@{u}"}, "", errors.New("error")).
ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/remotes/origin/master"}, "", errors.New("error")).
@@ -219,7 +215,7 @@ func TestGetCommits(t *testing.T) {
ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/remotes/origin/main"}, "", errors.New("error")).
ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/heads/main"}, "", errors.New("error")),
expectedCommits: []*models.Commit{
expectedCommitOpts: []models.NewCommitOpts{
{
Hash: "0eea75e8c631fba6b58135697835d58ba4c18dbc",
Name: "better typing for rebase mode",
@@ -240,14 +236,13 @@ func TestGetCommits(t *testing.T) {
{
testName: "should call merge-base for all main branches that exist",
logOrder: "topo-order",
rebaseMode: enums.REBASE_MODE_NONE,
opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", IncludeRebaseCommits: false},
mainBranches: []string{"master", "main", "develop", "1.0-hotfixes"},
runner: oscommands.NewFakeRunner(t).
// here it's seeing which commits are yet to be pushed
ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
// here it's actually getting all the commits in a formatted form, one per line
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil).
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil).
// here it's testing which of the configured main branches exist
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "master@{u}"}, "refs/remotes/origin/master", nil).
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "main@{u}"}, "", errors.New("error")).
@@ -258,7 +253,7 @@ func TestGetCommits(t *testing.T) {
// here it's seeing where our branch diverged from the master branch so that we can mark that commit and parent commits as 'merged'
ExpectGitArgs([]string{"merge-base", "HEAD", "refs/remotes/origin/master", "refs/remotes/origin/develop", "refs/remotes/origin/1.0-hotfixes"}, "26c07b1ab33860a1a7591a0638f9925ccf497ffa", nil),
expectedCommits: []*models.Commit{
expectedCommitOpts: []models.NewCommitOpts{
{
Hash: "0eea75e8c631fba6b58135697835d58ba4c18dbc",
Name: "better typing for rebase mode",
@@ -277,43 +272,41 @@ func TestGetCommits(t *testing.T) {
expectedError: nil,
},
{
testName: "should not specify order if `log.order` is `default`",
logOrder: "default",
rebaseMode: enums.REBASE_MODE_NONE,
opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", IncludeRebaseCommits: false},
testName: "should not specify order if `log.order` is `default`",
logOrder: "default",
opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", IncludeRebaseCommits: false},
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
expectedCommits: []*models.Commit{},
expectedError: nil,
expectedCommitOpts: []models.NewCommitOpts{},
expectedError: nil,
},
{
testName: "should set filter path",
logOrder: "default",
rebaseMode: enums.REBASE_MODE_NONE,
opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", FilterPath: "src"},
testName: "should set filter path",
logOrder: "default",
opts: GetCommitsOptions{RefName: "HEAD", RefForPushedStatus: "mybranch", FilterPath: "src"},
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"merge-base", "mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%m%x00%s", "--abbrev=40", "--follow", "--no-show-signature", "--", "src"}, "", nil),
ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%P%x00%m%x00%s", "--abbrev=40", "--follow", "--no-show-signature", "--", "src"}, "", nil),
expectedCommits: []*models.Commit{},
expectedError: nil,
expectedCommitOpts: []models.NewCommitOpts{},
expectedError: nil,
},
}
for _, scenario := range scenarios {
t.Run(scenario.testName, func(t *testing.T) {
common := utils.NewDummyCommon()
common := common.NewDummyCommon()
common.AppState = &config.AppState{}
common.AppState.GitLogOrder = scenario.logOrder
cmd := oscommands.NewDummyCmdObjBuilder(scenario.runner)
builder := &CommitLoader{
Common: common,
cmd: cmd,
getRebaseMode: func() (enums.RebaseMode, error) { return scenario.rebaseMode, nil },
dotGitDir: ".git",
Common: common,
cmd: cmd,
getWorkingTreeState: func() models.WorkingTreeState { return models.WorkingTreeState{} },
dotGitDir: ".git",
readFile: func(filename string) ([]byte, error) {
return []byte(""), nil
},
@@ -322,12 +315,17 @@ func TestGetCommits(t *testing.T) {
},
}
hashPool := &utils.StringPool{}
common.UserConfig().Git.MainBranches = scenario.mainBranches
opts := scenario.opts
opts.MainBranches = NewMainBranches(common, cmd)
opts.HashPool = hashPool
commits, err := builder.GetCommits(opts)
assert.Equal(t, scenario.expectedCommits, commits)
expectedCommits := lo.Map(scenario.expectedCommitOpts,
func(opts models.NewCommitOpts, _ int) *models.Commit { return models.NewCommit(hashPool, opts) })
assert.Equal(t, expectedCommits, commits)
assert.Equal(t, scenario.expectedError, err)
scenario.runner.CheckForMissingCalls()
@@ -336,19 +334,22 @@ func TestGetCommits(t *testing.T) {
}
func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
hashPool := &utils.StringPool{}
scenarios := []struct {
testName string
todos []todo.Todo
doneTodos []todo.Todo
amendFileExists bool
expectedHash string
testName string
todos []todo.Todo
doneTodos []todo.Todo
amendFileExists bool
messageFileExists bool
expectedResult *models.Commit
}{
{
testName: "no done todos",
todos: []todo.Todo{},
doneTodos: []todo.Todo{},
amendFileExists: false,
expectedHash: "",
expectedResult: nil,
},
{
testName: "common case (conflict)",
@@ -364,7 +365,11 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
},
},
amendFileExists: false,
expectedHash: "fa1afe1",
expectedResult: models.NewCommit(hashPool, models.NewCommitOpts{
Hash: "fa1afe1",
Action: todo.Pick,
Status: models.StatusConflicted,
}),
},
{
testName: "last command was 'break'",
@@ -373,7 +378,7 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
{Command: todo.Break},
},
amendFileExists: false,
expectedHash: "",
expectedResult: nil,
},
{
testName: "last command was 'exec'",
@@ -385,7 +390,7 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
},
},
amendFileExists: false,
expectedHash: "",
expectedResult: nil,
},
{
testName: "last command was 'reword'",
@@ -394,7 +399,7 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
{Command: todo.Reword},
},
amendFileExists: false,
expectedHash: "",
expectedResult: nil,
},
{
testName: "'pick' was rescheduled",
@@ -411,7 +416,7 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
},
},
amendFileExists: false,
expectedHash: "",
expectedResult: nil,
},
{
testName: "'pick' was rescheduled, buggy git version",
@@ -436,7 +441,7 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
},
},
amendFileExists: false,
expectedHash: "",
expectedResult: nil,
},
{
testName: "conflicting 'pick' after 'exec'",
@@ -461,7 +466,11 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
},
},
amendFileExists: false,
expectedHash: "fa1afe1",
expectedResult: models.NewCommit(hashPool, models.NewCommitOpts{
Hash: "fa1afe1",
Action: todo.Pick,
Status: models.StatusConflicted,
}),
},
{
testName: "'edit' with amend file",
@@ -473,10 +482,10 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
},
},
amendFileExists: true,
expectedHash: "",
expectedResult: nil,
},
{
testName: "'edit' without amend file",
testName: "'edit' without amend file but message file",
todos: []todo.Todo{},
doneTodos: []todo.Todo{
{
@@ -484,19 +493,37 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
Commit: "fa1afe1",
},
},
amendFileExists: false,
expectedHash: "fa1afe1",
amendFileExists: false,
messageFileExists: true,
expectedResult: models.NewCommit(hashPool, models.NewCommitOpts{
Hash: "fa1afe1",
Action: todo.Edit,
Status: models.StatusConflicted,
}),
},
{
testName: "'edit' without amend and without message file",
todos: []todo.Todo{},
doneTodos: []todo.Todo{
{
Command: todo.Edit,
Commit: "fa1afe1",
},
},
amendFileExists: false,
messageFileExists: false,
expectedResult: nil,
},
}
for _, scenario := range scenarios {
t.Run(scenario.testName, func(t *testing.T) {
common := utils.NewDummyCommon()
common := common.NewDummyCommon()
builder := &CommitLoader{
Common: common,
cmd: oscommands.NewDummyCmdObjBuilder(oscommands.NewFakeRunner(t)),
getRebaseMode: func() (enums.RebaseMode, error) { return enums.REBASE_MODE_INTERACTIVE, nil },
dotGitDir: ".git",
Common: common,
cmd: oscommands.NewDummyCmdObjBuilder(oscommands.NewFakeRunner(t)),
getWorkingTreeState: func() models.WorkingTreeState { return models.WorkingTreeState{Rebasing: true} },
dotGitDir: ".git",
readFile: func(filename string) ([]byte, error) {
return []byte(""), nil
},
@@ -505,30 +532,30 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
},
}
hash := builder.getConflictedCommitImpl(scenario.todos, scenario.doneTodos, scenario.amendFileExists)
assert.Equal(t, scenario.expectedHash, hash)
hash := builder.getConflictedCommitImpl(hashPool, scenario.todos, scenario.doneTodos, scenario.amendFileExists, scenario.messageFileExists)
assert.Equal(t, scenario.expectedResult, hash)
})
}
}
func TestCommitLoader_setCommitMergedStatuses(t *testing.T) {
type scenario struct {
testName string
commits []*models.Commit
ancestor string
expectedCommits []*models.Commit
testName string
commitOpts []models.NewCommitOpts
ancestor string
expectedCommitOpts []models.NewCommitOpts
}
scenarios := []scenario{
{
testName: "basic",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Hash: "12345", Name: "1", Action: models.ActionNone, Status: models.StatusUnpushed},
{Hash: "67890", Name: "2", Action: models.ActionNone, Status: models.StatusPushed},
{Hash: "abcde", Name: "3", Action: models.ActionNone, Status: models.StatusPushed},
},
ancestor: "67890",
expectedCommits: []*models.Commit{
expectedCommitOpts: []models.NewCommitOpts{
{Hash: "12345", Name: "1", Action: models.ActionNone, Status: models.StatusUnpushed},
{Hash: "67890", Name: "2", Action: models.ActionNone, Status: models.StatusMerged},
{Hash: "abcde", Name: "3", Action: models.ActionNone, Status: models.StatusMerged},
@@ -536,13 +563,13 @@ func TestCommitLoader_setCommitMergedStatuses(t *testing.T) {
},
{
testName: "with update-ref",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Hash: "12345", Name: "1", Action: models.ActionNone, Status: models.StatusUnpushed},
{Hash: "", Name: "", Action: todo.UpdateRef, Status: models.StatusNone},
{Hash: "abcde", Name: "3", Action: models.ActionNone, Status: models.StatusPushed},
},
ancestor: "deadbeef",
expectedCommits: []*models.Commit{
expectedCommitOpts: []models.NewCommitOpts{
{Hash: "12345", Name: "1", Action: models.ActionNone, Status: models.StatusUnpushed},
{Hash: "", Name: "", Action: todo.UpdateRef, Status: models.StatusNone},
{Hash: "abcde", Name: "3", Action: models.ActionNone, Status: models.StatusPushed},
@@ -552,9 +579,14 @@ func TestCommitLoader_setCommitMergedStatuses(t *testing.T) {
for _, scenario := range scenarios {
t.Run(scenario.testName, func(t *testing.T) {
expectedCommits := scenario.commits
setCommitMergedStatuses(scenario.ancestor, expectedCommits)
assert.Equal(t, scenario.expectedCommits, expectedCommits)
hashPool := &utils.StringPool{}
commits := lo.Map(scenario.commitOpts,
func(opts models.NewCommitOpts, _ int) *models.Commit { return models.NewCommit(hashPool, opts) })
setCommitMergedStatuses(scenario.ancestor, commits)
expectedCommits := lo.Map(scenario.expectedCommitOpts,
func(opts models.NewCommitOpts, _ int) *models.Commit { return models.NewCommit(hashPool, opts) })
assert.Equal(t, expectedCommits, commits)
})
}
}
+24 -2
View File
@@ -53,6 +53,7 @@ func TestCommitCommitCmdObj(t *testing.T) {
type scenario struct {
testName string
summary string
forceSkipHooks bool
description string
configSignoff bool
configSkipHookPrefix string
@@ -63,20 +64,39 @@ func TestCommitCommitCmdObj(t *testing.T) {
{
testName: "Commit",
summary: "test",
forceSkipHooks: false,
configSignoff: false,
configSkipHookPrefix: "",
expectedArgs: []string{"commit", "-m", "test"},
},
{
testName: "Commit with --no-verify flag",
testName: "Commit with --no-verify flag < only prefix",
summary: "WIP: test",
forceSkipHooks: false,
configSignoff: false,
configSkipHookPrefix: "WIP",
expectedArgs: []string{"commit", "--no-verify", "-m", "WIP: test"},
},
{
testName: "Commit with --no-verify flag < skip flag and prefix",
summary: "WIP: test",
forceSkipHooks: true,
configSignoff: false,
configSkipHookPrefix: "WIP",
expectedArgs: []string{"commit", "--no-verify", "-m", "WIP: test"},
},
{
testName: "Commit with --no-verify flag < skip flag no prefix",
summary: "test",
forceSkipHooks: true,
configSignoff: false,
configSkipHookPrefix: "WIP",
expectedArgs: []string{"commit", "--no-verify", "-m", "test"},
},
{
testName: "Commit with multiline message",
summary: "line1",
forceSkipHooks: false,
description: "line2",
configSignoff: false,
configSkipHookPrefix: "",
@@ -85,6 +105,7 @@ func TestCommitCommitCmdObj(t *testing.T) {
{
testName: "Commit with signoff",
summary: "test",
forceSkipHooks: false,
configSignoff: true,
configSkipHookPrefix: "",
expectedArgs: []string{"commit", "--signoff", "-m", "test"},
@@ -92,6 +113,7 @@ func TestCommitCommitCmdObj(t *testing.T) {
{
testName: "Commit with signoff and no-verify",
summary: "WIP: test",
forceSkipHooks: true,
configSignoff: true,
configSkipHookPrefix: "WIP",
expectedArgs: []string{"commit", "--no-verify", "--signoff", "-m", "WIP: test"},
@@ -107,7 +129,7 @@ func TestCommitCommitCmdObj(t *testing.T) {
runner := oscommands.NewFakeRunner(t).ExpectGitArgs(s.expectedArgs, "", nil)
instance := buildCommitCommands(commonDeps{userConfig: userConfig, runner: runner})
assert.NoError(t, instance.CommitCmdObj(s.summary, s.description).Run())
assert.NoError(t, instance.CommitCmdObj(s.summary, s.description, s.forceSkipHooks).Run())
runner.CheckForMissingCalls()
})
}
+24 -4
View File
@@ -57,15 +57,31 @@ func (self *ConfigCommands) GetPager(width int) string {
return utils.ResolvePlaceholderString(pagerTemplate, templateValues)
}
// UsingGpg tells us whether the user has gpg enabled so that we can know
// whether we need to run a subprocess to allow them to enter their password
func (self *ConfigCommands) UsingGpg() bool {
type GpgConfigKey string
const (
CommitGpgSign GpgConfigKey = "commit.gpgSign"
TagGpgSign GpgConfigKey = "tag.gpgSign"
)
// NeedsGpgSubprocess tells us whether the user has gpg enabled for the specified action type
// and needs a subprocess because they have a process where they manually
// enter their password every time a GPG action is taken
func (self *ConfigCommands) NeedsGpgSubprocess(key GpgConfigKey) bool {
overrideGpg := self.UserConfig().Git.OverrideGpg
if overrideGpg {
return false
}
return self.gitConfig.GetBool("commit.gpgsign")
return self.gitConfig.GetBool(string(key))
}
func (self *ConfigCommands) NeedsGpgSubprocessForCommit() bool {
return self.NeedsGpgSubprocess(CommitGpgSign)
}
func (self *ConfigCommands) GetGpgTagSign() bool {
return self.gitConfig.GetBool(string(TagGpgSign))
}
func (self *ConfigCommands) GetCoreEditor() string {
@@ -111,3 +127,7 @@ func (self *ConfigCommands) GetCoreCommentChar() byte {
func (self *ConfigCommands) GetRebaseUpdateRefs() bool {
return self.gitConfig.GetBool("rebase.updateRefs")
}
func (self *ConfigCommands) DropConfigCache() {
self.gitConfig.DropCache()
}
+21 -1
View File
@@ -1,6 +1,11 @@
package git_commands
import "github.com/mgutz/str"
import (
"fmt"
"strings"
"github.com/mgutz/str"
)
type CustomCommands struct {
*GitCommon
@@ -18,3 +23,18 @@ func NewCustomCommands(gitCommon *GitCommon) *CustomCommands {
func (self *CustomCommands) RunWithOutput(cmdStr string) (string, error) {
return self.cmd.New(str.ToArgv(cmdStr)).RunWithOutput()
}
// A function that can be used as a "runCommand" entry in the template.FuncMap of templates.
func (self *CustomCommands) TemplateFunctionRunCommand(cmdStr string) (string, error) {
output, err := self.RunWithOutput(cmdStr)
if err != nil {
return "", err
}
output = strings.TrimRight(output, "\r\n")
if strings.Contains(output, "\r\n") {
return "", fmt.Errorf("command output contains newlines: %s", output)
}
return output, nil
}
+1 -23
View File
@@ -7,10 +7,8 @@ import (
gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"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"
"github.com/spf13/afero"
)
@@ -33,7 +31,7 @@ func buildGitCommon(deps commonDeps) *GitCommon {
gitCommon.Common = deps.common
if gitCommon.Common == nil {
gitCommon.Common = utils.NewDummyCommonWithUserConfigAndAppState(deps.userConfig, deps.appState)
gitCommon.Common = common.NewDummyCommonWithUserConfigAndAppState(deps.userConfig, deps.appState)
}
if deps.fs != nil {
@@ -126,26 +124,6 @@ func buildWorkingTreeCommands(deps commonDeps) *WorkingTreeCommands {
return NewWorkingTreeCommands(gitCommon, submoduleCommands, fileLoader)
}
func buildPatchCommands(deps commonDeps) *PatchCommands { //nolint:golint,unused
gitCommon := buildGitCommon(deps)
rebaseCommands := buildRebaseCommands(deps)
commitCommands := buildCommitCommands(deps)
statusCommands := buildStatusCommands(deps)
stashCommands := buildStashCommands(deps)
loadFileFn := func(from string, to string, reverse bool, filename string, plain bool) (string, error) {
return "", nil
}
patchBuilder := patch.NewPatchBuilder(gitCommon.Log, loadFileFn)
return NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchBuilder)
}
func buildStatusCommands(deps commonDeps) *StatusCommands { //nolint:golint,unused
gitCommon := buildGitCommon(deps)
return NewStatusCommands(gitCommon)
}
func buildStashCommands(deps commonDeps) *StashCommands {
gitCommon := buildGitCommon(deps)
fileLoader := buildFileLoader(gitCommon)
+3 -3
View File
@@ -18,7 +18,7 @@ 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.ICmdObj {
func (self *DiffCommands) DiffCmdObj(diffArgs []string) *oscommands.CmdObj {
extDiffCmd := self.UserConfig().Git.Paging.ExternalDiffCommand
useExtDiff := extDiffCmd != ""
ignoreWhitespace := self.AppState.IgnoreWhitespaceInDiffView
@@ -83,7 +83,7 @@ type DiffToolCmdOptions struct {
Staged bool
}
func (self *DiffCommands) OpenDiffToolCmdObj(opts DiffToolCmdOptions) oscommands.ICmdObj {
func (self *DiffCommands) OpenDiffToolCmdObj(opts DiffToolCmdOptions) *oscommands.CmdObj {
return self.cmd.New(NewGitCmd("difftool").
Arg("--no-prompt").
ArgIf(opts.IsDirectory, "--dir-diff").
@@ -95,7 +95,7 @@ func (self *DiffCommands) OpenDiffToolCmdObj(opts DiffToolCmdOptions) oscommands
ToArgv())
}
func (self *DiffCommands) DiffIndexCmdObj(diffArgs ...string) oscommands.ICmdObj {
func (self *DiffCommands) DiffIndexCmdObj(diffArgs ...string) *oscommands.CmdObj {
return self.cmd.New(
NewGitCmd("diff-index").
Config("diff.noprefix=false").
+4 -4
View File
@@ -86,7 +86,7 @@ func (self *FileCommands) GetEditCmdStr(filenames []string) (string, bool) {
}
}
template, suspend := config.GetEditTemplate(&self.UserConfig().OS, self.guessDefaultEditor)
template, suspend := config.GetEditTemplate(self.os.Platform.Shell, &self.UserConfig().OS, self.guessDefaultEditor)
quotedFilenames := lo.Map(filenames, func(filename string, _ int) string { return self.cmd.Quote(filename) })
templateValues := map[string]string{
@@ -105,7 +105,7 @@ func (self *FileCommands) GetEditAtLineCmdStr(filename string, lineNumber int) (
}
}
template, suspend := config.GetEditAtLineTemplate(&self.UserConfig().OS, self.guessDefaultEditor)
template, suspend := config.GetEditAtLineTemplate(self.os.Platform.Shell, &self.UserConfig().OS, self.guessDefaultEditor)
templateValues := map[string]string{
"filename": self.cmd.Quote(filename),
@@ -124,7 +124,7 @@ func (self *FileCommands) GetEditAtLineAndWaitCmdStr(filename string, lineNumber
}
}
template := config.GetEditAtLineAndWaitTemplate(&self.UserConfig().OS, self.guessDefaultEditor)
template := config.GetEditAtLineAndWaitTemplate(self.os.Platform.Shell, &self.UserConfig().OS, self.guessDefaultEditor)
templateValues := map[string]string{
"filename": self.cmd.Quote(filename),
@@ -136,7 +136,7 @@ func (self *FileCommands) GetEditAtLineAndWaitCmdStr(filename string, lineNumber
}
func (self *FileCommands) GetOpenDirInEditorCmdStr(path string) (string, bool) {
template, suspend := config.GetOpenDirInEditorTemplate(&self.UserConfig().OS, self.guessDefaultEditor)
template, suspend := config.GetOpenDirInEditorTemplate(self.os.Platform.Shell, &self.UserConfig().OS, self.guessDefaultEditor)
templateValues := map[string]string{
"dir": self.cmd.Quote(path),
+11 -11
View File
@@ -68,12 +68,12 @@ func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File
}
file := &models.File{
Name: status.Name,
PreviousName: status.PreviousName,
Path: status.Path,
PreviousPath: status.PreviousPath,
DisplayString: status.StatusString,
}
if diff, ok := fileDiffs[status.Name]; ok {
if diff, ok := fileDiffs[status.Path]; ok {
file.LinesAdded = diff.LinesAdded
file.LinesDeleted = diff.LinesDeleted
}
@@ -87,7 +87,7 @@ func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File
worktreePaths := linkedWortkreePaths(self.Fs, self.repoPaths.RepoGitDirPath())
for _, file := range files {
for _, worktreePath := range worktreePaths {
absFilePath, err := filepath.Abs(file.Name)
absFilePath, err := filepath.Abs(file.Path)
if err != nil {
self.Log.Error(err)
continue
@@ -96,7 +96,7 @@ func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File
file.IsWorktree = true
// `git status` renders this worktree as a folder with a trailing slash but we'll represent it as a singular worktree
// If we include the slash, it will be rendered as a folder with a null file inside.
file.Name = strings.TrimSuffix(file.Name, "/")
file.Path = strings.TrimSuffix(file.Path, "/")
break
}
}
@@ -153,8 +153,8 @@ type GitStatusOptions struct {
type FileStatus struct {
StatusString string
Change string // ??, MM, AM, ...
Name string
PreviousName string
Path string
PreviousPath string
}
func (fileLoader *FileLoader) gitDiffNumStat() (string, error) {
@@ -197,14 +197,14 @@ func (self *FileLoader) gitStatus(opts GitStatusOptions) ([]FileStatus, error) {
status := FileStatus{
StatusString: original,
Change: original[:2],
Name: original[3:],
PreviousName: "",
Path: original[3:],
PreviousPath: "",
}
if strings.HasPrefix(status.Change, "R") {
// if a line starts with 'R' then the next line is the original file.
status.PreviousName = splitLines[i+1]
status.StatusString = fmt.Sprintf("%s %s -> %s", status.Change, status.PreviousName, status.Name)
status.PreviousPath = splitLines[i+1]
status.StatusString = fmt.Sprintf("%s %s -> %s", status.Change, status.PreviousPath, status.Path)
i++
}
+11 -11
View File
@@ -41,7 +41,7 @@ func TestFileGetStatusFiles(t *testing.T) {
showNumstatInFilesView: true,
expectedFiles: []*models.File{
{
Name: "file1.txt",
Path: "file1.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: true,
@@ -55,7 +55,7 @@ func TestFileGetStatusFiles(t *testing.T) {
LinesDeleted: 1,
},
{
Name: "file3.txt",
Path: "file3.txt",
HasStagedChanges: true,
HasUnstagedChanges: false,
Tracked: false,
@@ -69,7 +69,7 @@ func TestFileGetStatusFiles(t *testing.T) {
LinesDeleted: 2,
},
{
Name: "file2.txt",
Path: "file2.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: false,
@@ -83,7 +83,7 @@ func TestFileGetStatusFiles(t *testing.T) {
LinesDeleted: 0,
},
{
Name: "file4.txt",
Path: "file4.txt",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: false,
@@ -97,7 +97,7 @@ func TestFileGetStatusFiles(t *testing.T) {
LinesDeleted: 2,
},
{
Name: "file5.txt",
Path: "file5.txt",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: true,
@@ -119,7 +119,7 @@ func TestFileGetStatusFiles(t *testing.T) {
ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z", "--find-renames=50%"}, "MM a\nb.txt", nil),
expectedFiles: []*models.File{
{
Name: "a\nb.txt",
Path: "a\nb.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: true,
@@ -142,8 +142,8 @@ func TestFileGetStatusFiles(t *testing.T) {
),
expectedFiles: []*models.File{
{
Name: "after1.txt",
PreviousName: "before1.txt",
Path: "after1.txt",
PreviousPath: "before1.txt",
HasStagedChanges: true,
HasUnstagedChanges: false,
Tracked: true,
@@ -155,8 +155,8 @@ func TestFileGetStatusFiles(t *testing.T) {
ShortStatus: "R ",
},
{
Name: "after2.txt",
PreviousName: "before2.txt",
Path: "after2.txt",
PreviousPath: "before2.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: true,
@@ -179,7 +179,7 @@ func TestFileGetStatusFiles(t *testing.T) {
),
expectedFiles: []*models.File{
{
Name: "a -> b.txt",
Path: "a -> b.txt",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: false,
+2 -2
View File
@@ -24,7 +24,7 @@ func (self *FlowCommands) GitFlowEnabled() bool {
return self.config.GetGitFlowPrefixes() != ""
}
func (self *FlowCommands) FinishCmdObj(branchName string) (oscommands.ICmdObj, error) {
func (self *FlowCommands) FinishCmdObj(branchName string) (*oscommands.CmdObj, error) {
prefixes := self.config.GetGitFlowPrefixes()
// need to find out what kind of branch this is
@@ -54,7 +54,7 @@ func (self *FlowCommands) FinishCmdObj(branchName string) (oscommands.ICmdObj, e
return self.cmd.New(cmdArgs), nil
}
func (self *FlowCommands) StartCmdObj(branchType string, name string) oscommands.ICmdObj {
func (self *FlowCommands) StartCmdObj(branchType string, name string) *oscommands.CmdObj {
cmdArgs := NewGitCmd("flow").Arg(branchType, "start", name).ToArgv()
return self.cmd.New(cmdArgs)
+32 -10
View File
@@ -8,7 +8,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/app/daemon"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/stefanhaller/git-todo-parser/todo"
)
@@ -150,20 +149,20 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
// we can make this GPG thing possible it just means we need to do this in two parts:
// one where we handle the possibility of a credential request, and the other
// where we continue the rebase
if self.config.UsingGpg() {
if self.config.NeedsGpgSubprocessForCommit() {
return errors.New(self.Tr.DisabledForGPG)
}
baseIndex := sourceCommitIdx + 1
changes := []daemon.ChangeTodoAction{
{Hash: commits[sourceCommitIdx].Hash, NewAction: todo.Edit},
{Hash: commits[destinationCommitIdx].Hash, NewAction: todo.Edit},
{Hash: commits[sourceCommitIdx].Hash(), NewAction: todo.Edit},
{Hash: commits[destinationCommitIdx].Hash(), NewAction: todo.Edit},
}
self.os.LogCommand(logTodoChanges(changes), false)
err := self.rebase.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseHashOrRoot: commits[baseIndex].Hash,
baseHashOrRoot: commits[baseIndex].Hash(),
overrideEditor: true,
instruction: daemon.NewChangeTodoActionsInstruction(changes),
}).Run()
@@ -219,7 +218,7 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
func (self *PatchCommands) MovePatchIntoIndex(commits []*models.Commit, commitIdx int, stash bool) error {
if stash {
if err := self.stash.Push(self.Tr.StashPrefix + commits[commitIdx].Hash); err != nil {
if err := self.stash.Push(self.Tr.StashPrefix + commits[commitIdx].Hash()); err != nil {
return err
}
}
@@ -229,7 +228,7 @@ func (self *PatchCommands) MovePatchIntoIndex(commits []*models.Commit, commitId
}
if err := self.ApplyCustomPatch(true, true); err != nil {
if self.status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
if self.status.WorkingTreeState().Rebasing {
_ = self.rebase.AbortRebase()
}
return err
@@ -253,7 +252,7 @@ func (self *PatchCommands) MovePatchIntoIndex(commits []*models.Commit, commitId
self.rebase.onSuccessfulContinue = func() error {
// add patches to index
if err := self.ApplyPatch(patch, ApplyPatchOpts{Index: true, ThreeWay: true}); err != nil {
if self.status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
if self.status.WorkingTreeState().Rebasing {
_ = self.rebase.AbortRebase()
}
return err
@@ -303,7 +302,7 @@ func (self *PatchCommands) PullPatchIntoNewCommit(
return err
}
if err := self.commit.CommitCmdObj(commitSummary, commitDescription).Run(); err != nil {
if err := self.commit.CommitCmdObj(commitSummary, commitDescription, false).Run(); err != nil {
return err
}
@@ -315,6 +314,29 @@ func (self *PatchCommands) PullPatchIntoNewCommit(
return self.rebase.ContinueRebase()
}
func (self *PatchCommands) PullPatchIntoNewCommitBefore(
commits []*models.Commit,
commitIdx int,
commitSummary string,
commitDescription string,
) error {
if err := self.rebase.BeginInteractiveRebaseForCommit(commits, commitIdx+1, true); err != nil {
return err
}
if err := self.ApplyCustomPatch(false, false); err != nil {
_ = self.rebase.AbortRebase()
return err
}
if err := self.commit.CommitCmdObj(commitSummary, commitDescription, false).Run(); err != nil {
return err
}
self.PatchBuilder.Reset()
return self.rebase.ContinueRebase()
}
// We have just applied a patch in reverse to discard it from a commit; if we
// now try to apply the patch again to move it to a later commit, or to the
// index, then this would conflict "with itself" in case the patch contained
@@ -324,7 +346,7 @@ func (self *PatchCommands) diffHeadAgainstCommit(commit *models.Commit) (string,
cmdArgs := NewGitCmd("diff").
Config("diff.noprefix=false").
Arg("--no-ext-diff").
Arg("HEAD.." + commit.Hash).
Arg("HEAD.." + commit.Hash()).
ToArgv()
return self.cmd.New(cmdArgs).RunWithOutput()
+42 -50
View File
@@ -35,7 +35,7 @@ func NewRebaseCommands(
}
func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int, summary string, description string) error {
if self.config.UsingGpg() {
if self.config.NeedsGpgSubprocessForCommit() {
return errors.New(self.Tr.DisabledForGPG)
}
@@ -53,9 +53,9 @@ func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int, su
return self.ContinueRebase()
}
func (self *RebaseCommands) RewordCommitInEditor(commits []*models.Commit, index int) (oscommands.ICmdObj, error) {
func (self *RebaseCommands) RewordCommitInEditor(commits []*models.Commit, index int) (*oscommands.CmdObj, error) {
changes := []daemon.ChangeTodoAction{{
Hash: commits[index].Hash,
Hash: commits[index].Hash(),
NewAction: todo.Reword,
}}
self.os.LogCommand(logTodoChanges(changes), false)
@@ -80,7 +80,7 @@ func (self *RebaseCommands) SetCommitAuthor(commits []*models.Commit, start, end
func (self *RebaseCommands) AddCommitCoAuthor(commits []*models.Commit, start, end int, value string) error {
return self.GenericAmend(commits, start, end, func(commit *models.Commit) error {
return self.commit.AddCoAuthor(commit.Hash, value)
return self.commit.AddCoAuthor(commit.Hash(), value)
})
}
@@ -113,7 +113,7 @@ func (self *RebaseCommands) MoveCommitsDown(commits []*models.Commit, startIdx i
baseHashOrRoot := getBaseHashOrRoot(commits, endIdx+2)
hashes := lo.Map(commits[startIdx:endIdx+1], func(commit *models.Commit, _ int) string {
return commit.Hash
return commit.Hash()
})
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
@@ -127,7 +127,7 @@ func (self *RebaseCommands) MoveCommitsUp(commits []*models.Commit, startIdx int
baseHashOrRoot := getBaseHashOrRoot(commits, endIdx+1)
hashes := lo.Map(commits[startIdx:endIdx+1], func(commit *models.Commit, _ int) string {
return commit.Hash
return commit.Hash()
})
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
@@ -147,7 +147,7 @@ func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, startIdx
changes := lo.FilterMap(commits[startIdx:endIdx+1], func(commit *models.Commit, _ int) (daemon.ChangeTodoAction, bool) {
return daemon.ChangeTodoAction{
Hash: commit.Hash,
Hash: commit.Hash(),
NewAction: action,
}, !commit.IsMerge()
})
@@ -209,7 +209,7 @@ type PrepareInteractiveRebaseCommandOpts struct {
// PrepareInteractiveRebaseCommand returns the cmd for an interactive rebase
// we tell git to run lazygit to edit the todo list, and we pass the client
// lazygit instructions what to do with the todo file
func (self *RebaseCommands) PrepareInteractiveRebaseCommand(opts PrepareInteractiveRebaseCommandOpts) oscommands.ICmdObj {
func (self *RebaseCommands) PrepareInteractiveRebaseCommand(opts PrepareInteractiveRebaseCommandOpts) *oscommands.CmdObj {
ex := oscommands.GetLazygitPath()
cmdArgs := NewGitCmd("rebase").
@@ -218,7 +218,7 @@ func (self *RebaseCommands) PrepareInteractiveRebaseCommand(opts PrepareInteract
Arg("--keep-empty").
ArgIf(opts.keepCommitsThatBecomeEmpty && self.version.IsAtLeast(2, 26, 0), "--empty=keep").
Arg("--no-autosquash").
ArgIf(self.version.IsAtLeast(2, 22, 0), "--rebase-merges").
Arg("--rebase-merges").
ArgIf(opts.onto != "", "--onto", opts.onto).
Arg(opts.baseHashOrRoot).
ToArgv()
@@ -293,7 +293,7 @@ func (self *RebaseCommands) getHashOfLastCommitMade() (string, error) {
func (self *RebaseCommands) AmendTo(commits []*models.Commit, commitIndex int) error {
commit := commits[commitIndex]
if err := self.commit.CreateFixupCommit(commit.Hash); err != nil {
if err := self.commit.CreateFixupCommit(commit.Hash()); err != nil {
return err
}
@@ -305,7 +305,7 @@ func (self *RebaseCommands) AmendTo(commits []*models.Commit, commitIndex int) e
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseHashOrRoot: getBaseHashOrRoot(commits, commitIndex+1),
overrideEditor: true,
instruction: daemon.NewMoveFixupCommitDownInstruction(commit.Hash, fixupHash, true),
instruction: daemon.NewMoveFixupCommitDownInstruction(commit.Hash(), fixupHash, true),
}).Run()
}
@@ -318,7 +318,7 @@ func (self *RebaseCommands) MoveFixupCommitDown(commits []*models.Commit, target
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseHashOrRoot: getBaseHashOrRoot(commits, targetCommitIndex+1),
overrideEditor: true,
instruction: daemon.NewMoveFixupCommitDownInstruction(commits[targetCommitIndex].Hash, fixupHash, false),
instruction: daemon.NewMoveFixupCommitDownInstruction(commits[targetCommitIndex].Hash(), fixupHash, false),
}).Run()
}
@@ -326,7 +326,7 @@ func todoFromCommit(commit *models.Commit) utils.Todo {
if commit.Action == todo.UpdateRef {
return utils.Todo{Ref: commit.Name}
} else {
return utils.Todo{Hash: commit.Hash}
return utils.Todo{Hash: commit.Hash()}
}
}
@@ -334,7 +334,7 @@ func todoFromCommit(commit *models.Commit) utils.Todo {
func (self *RebaseCommands) EditRebaseTodo(commits []*models.Commit, action todo.TodoCommand) error {
commitsWithAction := lo.Map(commits, func(commit *models.Commit, _ int) utils.TodoChange {
return utils.TodoChange{
Hash: commit.Hash,
Hash: commit.Hash(),
NewAction: action,
}
})
@@ -383,7 +383,7 @@ func (self *RebaseCommands) MoveTodosUp(commits []*models.Commit) error {
// SquashAllAboveFixupCommits squashes all fixup! commits above the given one
func (self *RebaseCommands) SquashAllAboveFixupCommits(commit *models.Commit) error {
hashOrRoot := commit.Hash + "^"
hashOrRoot := commit.Hash() + "^"
if commit.IsFirstCommit() {
hashOrRoot = "--root"
}
@@ -400,7 +400,19 @@ func (self *RebaseCommands) SquashAllAboveFixupCommits(commit *models.Commit) er
func (self *RebaseCommands) BeginInteractiveRebaseForCommit(
commits []*models.Commit, commitIndex int, keepCommitsThatBecomeEmpty bool,
) error {
return self.BeginInteractiveRebaseForCommitRange(commits, commitIndex, commitIndex, keepCommitsThatBecomeEmpty)
if commitIndex < len(commits) && commits[commitIndex].IsMerge() {
if self.config.NeedsGpgSubprocessForCommit() {
return errors.New(self.Tr.DisabledForGPG)
}
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseHashOrRoot: getBaseHashOrRoot(commits, commitIndex),
instruction: daemon.NewInsertBreakInstruction(),
keepCommitsThatBecomeEmpty: keepCommitsThatBecomeEmpty,
}).Run()
} else {
return self.BeginInteractiveRebaseForCommitRange(commits, commitIndex, commitIndex, keepCommitsThatBecomeEmpty)
}
}
func (self *RebaseCommands) BeginInteractiveRebaseForCommitRange(
@@ -413,14 +425,14 @@ func (self *RebaseCommands) BeginInteractiveRebaseForCommitRange(
// we can make this GPG thing possible it just means we need to do this in two parts:
// one where we handle the possibility of a credential request, and the other
// where we continue the rebase
if self.config.UsingGpg() {
if self.config.NeedsGpgSubprocessForCommit() {
return errors.New(self.Tr.DisabledForGPG)
}
changes := make([]daemon.ChangeTodoAction, 0, end-start)
for commitIndex := end; commitIndex >= start; commitIndex-- {
changes = append(changes, daemon.ChangeTodoAction{
Hash: commits[commitIndex].Hash,
Hash: commits[commitIndex].Hash(),
NewAction: todo.Edit,
})
}
@@ -446,7 +458,7 @@ func (self *RebaseCommands) RebaseBranchFromBaseCommit(targetBranchName string,
}).Run()
}
func (self *RebaseCommands) GenericMergeOrRebaseActionCmdObj(commandType string, command string) oscommands.ICmdObj {
func (self *RebaseCommands) GenericMergeOrRebaseActionCmdObj(commandType string, command string) *oscommands.CmdObj {
cmdArgs := NewGitCmd(commandType).Arg("--" + command).ToArgv()
return self.cmd.New(cmdArgs)
@@ -485,7 +497,7 @@ func (self *RebaseCommands) GenericMergeOrRebaseAction(commandType string, comma
return nil
}
func (self *RebaseCommands) runSkipEditorCommand(cmdObj oscommands.ICmdObj) error {
func (self *RebaseCommands) runSkipEditorCommand(cmdObj *oscommands.CmdObj) error {
instruction := daemon.NewExitImmediatelyInstruction()
lazyGitPath := oscommands.GetLazygitPath()
return cmdObj.
@@ -533,41 +545,21 @@ func (self *RebaseCommands) DiscardOldFileChanges(commits []*models.Commit, comm
// CherryPickCommits begins an interactive rebase with the given hashes being cherry picked onto HEAD
func (self *RebaseCommands) CherryPickCommits(commits []*models.Commit) error {
commitLines := lo.Map(commits, func(commit *models.Commit, _ int) string {
return fmt.Sprintf("%s %s", utils.ShortHash(commit.Hash), commit.Name)
})
msg := utils.ResolvePlaceholderString(
self.Tr.Log.CherryPickCommits,
map[string]string{
"commitLines": strings.Join(commitLines, "\n"),
},
)
self.os.LogCommand(msg, false)
hasMergeCommit := lo.SomeBy(commits, func(c *models.Commit) bool { return c.IsMerge() })
cmdArgs := NewGitCmd("cherry-pick").
Arg("--allow-empty").
ArgIf(self.version.IsAtLeast(2, 45, 0), "--empty=keep", "--keep-redundant-commits").
ArgIf(hasMergeCommit, "-m1").
Arg(lo.Reverse(lo.Map(commits, func(c *models.Commit, _ int) string { return c.Hash() }))...).
ToArgv()
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseHashOrRoot: "HEAD",
instruction: daemon.NewCherryPickCommitsInstruction(commits),
}).Run()
}
// CherryPickCommitsDuringRebase simply prepends the given commits to the existing git-rebase-todo file
func (self *RebaseCommands) CherryPickCommitsDuringRebase(commits []*models.Commit) error {
todoLines := lo.Map(commits, func(commit *models.Commit, _ int) daemon.TodoLine {
return daemon.TodoLine{
Action: "pick",
Commit: commit,
}
})
todo := daemon.TodoLinesToString(todoLines)
filePath := filepath.Join(self.repoPaths.worktreeGitDirPath, "rebase-merge/git-rebase-todo")
return utils.PrependStrToTodoFile(filePath, []byte(todo))
return self.cmd.New(cmdArgs).Run()
}
func (self *RebaseCommands) DropMergeCommit(commits []*models.Commit, commitIndex int) error {
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseHashOrRoot: getBaseHashOrRoot(commits, commitIndex+1),
instruction: daemon.NewDropMergeCommitInstruction(commits[commitIndex].Hash),
instruction: daemon.NewDropMergeCommitInstruction(commits[commitIndex].Hash()),
}).Run()
}
@@ -579,7 +571,7 @@ func getBaseHashOrRoot(commits []*models.Commit, index int) string {
// be starting a rebase from 300 commits ago (which is the original commit limit
// at time of writing)
if index < len(commits) {
return commits[index].Hash
return commits[index].Hash()
} else {
return "--root"
}
+13 -18
View File
@@ -10,6 +10,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
)
@@ -54,16 +55,6 @@ func TestRebaseRebaseBranch(t *testing.T) {
assert.NoError(t, err)
},
},
{
testName: "successful rebase (< 2.22.0)",
arg: "master",
gitVersion: &GitVersion{2, 21, 9, ""},
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"rebase", "--interactive", "--autostash", "--keep-empty", "--no-autosquash", "master"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
@@ -78,7 +69,7 @@ func TestRebaseRebaseBranch(t *testing.T) {
// environment variables that suppress an interactive editor
func TestRebaseSkipEditorCommand(t *testing.T) {
cmdArgs := []string{"git", "blah"}
runner := oscommands.NewFakeRunner(t).ExpectFunc("matches editor env var", func(cmdObj oscommands.ICmdObj) bool {
runner := oscommands.NewFakeRunner(t).ExpectFunc("matches editor env var", func(cmdObj *oscommands.CmdObj) bool {
assert.EqualValues(t, cmdArgs, cmdObj.Args())
envVars := cmdObj.GetEnvVars()
for _, regexStr := range []string{
@@ -107,7 +98,7 @@ func TestRebaseDiscardOldFileChanges(t *testing.T) {
type scenario struct {
testName string
gitConfigMockResponses map[string]string
commits []*models.Commit
commitOpts []models.NewCommitOpts
commitIndex int
fileName []string
runner *oscommands.FakeCmdObjRunner
@@ -118,7 +109,7 @@ func TestRebaseDiscardOldFileChanges(t *testing.T) {
{
testName: "returns error when index outside of range of commits",
gitConfigMockResponses: nil,
commits: []*models.Commit{},
commitOpts: []models.NewCommitOpts{},
commitIndex: 0,
fileName: []string{"test999.txt"},
runner: oscommands.NewFakeRunner(t),
@@ -128,8 +119,8 @@ func TestRebaseDiscardOldFileChanges(t *testing.T) {
},
{
testName: "returns error when using gpg",
gitConfigMockResponses: map[string]string{"commit.gpgsign": "true"},
commits: []*models.Commit{{Name: "commit", Hash: "123456"}},
gitConfigMockResponses: map[string]string{"commit.gpgSign": "true"},
commitOpts: []models.NewCommitOpts{{Name: "commit", Hash: "123456"}},
commitIndex: 0,
fileName: []string{"test999.txt"},
runner: oscommands.NewFakeRunner(t),
@@ -140,7 +131,7 @@ func TestRebaseDiscardOldFileChanges(t *testing.T) {
{
testName: "checks out file if it already existed",
gitConfigMockResponses: nil,
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Name: "commit", Hash: "123456"},
{Name: "commit2", Hash: "abcdef"},
},
@@ -149,7 +140,7 @@ func TestRebaseDiscardOldFileChanges(t *testing.T) {
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{"-c", disableHooksFlag, "checkout", "HEAD^", "--", "test999.txt"}, "", nil).
ExpectGitArgs([]string{"checkout", "HEAD^", "--", "test999.txt"}, "", nil).
ExpectGitArgs([]string{"commit", "--amend", "--no-edit", "--allow-empty"}, "", nil).
ExpectGitArgs([]string{"rebase", "--continue"}, "", nil),
test: func(err error) {
@@ -168,7 +159,11 @@ func TestRebaseDiscardOldFileChanges(t *testing.T) {
gitConfig: git_config.NewFakeGitConfig(s.gitConfigMockResponses),
})
s.test(instance.DiscardOldFileChanges(s.commits, s.commitIndex, s.fileName))
hashPool := &utils.StringPool{}
commits := lo.Map(s.commitOpts,
func(opts models.NewCommitOpts, _ int) *models.Commit { return models.NewCommit(hashPool, opts) })
s.test(instance.DiscardOldFileChanges(commits, s.commitIndex, s.fileName))
s.runner.CheckForMissingCalls()
})
}
@@ -7,6 +7,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type ReflogCommitLoader struct {
@@ -23,14 +24,14 @@ func NewReflogCommitLoader(common *common.Common, cmd oscommands.ICmdObjBuilder)
// GetReflogCommits only returns the new reflog commits since the given lastReflogCommit
// if none is passed (i.e. it's value is nil) then we get all the reflog commits
func (self *ReflogCommitLoader) GetReflogCommits(lastReflogCommit *models.Commit, filterPath string, filterAuthor string) ([]*models.Commit, bool, error) {
func (self *ReflogCommitLoader) GetReflogCommits(hashPool *utils.StringPool, lastReflogCommit *models.Commit, filterPath string, filterAuthor string) ([]*models.Commit, bool, error) {
commits := make([]*models.Commit, 0)
cmdArgs := NewGitCmd("log").
Config("log.showSignature=false").
Arg("-g").
Arg("--abbrev=40").
Arg("--format=%h%x00%ct%x00%gs%x00%p").
Arg("--format=%h%x00%ct%x00%gs%x00%P").
ArgIf(filterAuthor != "", "--author="+filterAuthor).
ArgIf(filterPath != "", "--follow", "--", filterPath).
ToArgv()
@@ -39,7 +40,7 @@ func (self *ReflogCommitLoader) GetReflogCommits(lastReflogCommit *models.Commit
onlyObtainedNewReflogCommits := false
err := cmdObj.RunAndProcessLines(func(line string) (bool, error) {
commit, ok := self.parseLine(line)
commit, ok := self.parseLine(hashPool, line)
if !ok {
return false, nil
}
@@ -65,10 +66,10 @@ func (self *ReflogCommitLoader) GetReflogCommits(lastReflogCommit *models.Commit
}
func (self *ReflogCommitLoader) sameReflogCommit(a *models.Commit, b *models.Commit) bool {
return a.Hash == b.Hash && a.UnixTimestamp == b.UnixTimestamp && a.Name == b.Name
return a.Hash() == b.Hash() && a.UnixTimestamp == b.UnixTimestamp && a.Name == b.Name
}
func (self *ReflogCommitLoader) parseLine(line string) (*models.Commit, bool) {
func (self *ReflogCommitLoader) parseLine(hashPool *utils.StringPool, line string) (*models.Commit, bool) {
fields := strings.SplitN(line, "\x00", 4)
if len(fields) <= 3 {
return nil, false
@@ -82,11 +83,11 @@ func (self *ReflogCommitLoader) parseLine(line string) (*models.Commit, bool) {
parents = strings.Split(parentHashes, " ")
}
return &models.Commit{
return models.NewCommit(hashPool, models.NewCommitOpts{
Hash: fields[0],
Name: fields[2],
UnixTimestamp: int64(unixTimestamp),
Status: models.StatusReflog,
Parents: parents,
}, true
}), true
}
@@ -7,7 +7,9 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
"github.com/sanity-io/litter"
"github.com/stretchr/testify/assert"
)
@@ -26,29 +28,31 @@ func TestGetReflogCommits(t *testing.T) {
lastReflogCommit *models.Commit
filterPath string
filterAuthor string
expectedCommits []*models.Commit
expectedCommitOpts []models.NewCommitOpts
expectedOnlyObtainedNew bool
expectedError error
}
hashPool := &utils.StringPool{}
scenarios := []scenario{
{
testName: "no reflog entries",
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--abbrev=40", "--format=%h%x00%ct%x00%gs%x00%p"}, "", nil),
ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--abbrev=40", "--format=%h%x00%ct%x00%gs%x00%P"}, "", nil),
lastReflogCommit: nil,
expectedCommits: []*models.Commit{},
expectedCommitOpts: []models.NewCommitOpts{},
expectedOnlyObtainedNew: false,
expectedError: nil,
},
{
testName: "some reflog entries",
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--abbrev=40", "--format=%h%x00%ct%x00%gs%x00%p"}, reflogOutput, nil),
ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--abbrev=40", "--format=%h%x00%ct%x00%gs%x00%P"}, reflogOutput, nil),
lastReflogCommit: nil,
expectedCommits: []*models.Commit{
expectedCommitOpts: []models.NewCommitOpts{
{
Hash: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
@@ -91,16 +95,16 @@ func TestGetReflogCommits(t *testing.T) {
{
testName: "some reflog entries where last commit is given",
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--abbrev=40", "--format=%h%x00%ct%x00%gs%x00%p"}, reflogOutput, nil),
ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--abbrev=40", "--format=%h%x00%ct%x00%gs%x00%P"}, reflogOutput, nil),
lastReflogCommit: &models.Commit{
lastReflogCommit: models.NewCommit(hashPool, models.NewCommitOpts{
Hash: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from B to A",
Status: models.StatusReflog,
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
expectedCommits: []*models.Commit{
}),
expectedCommitOpts: []models.NewCommitOpts{
{
Hash: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
@@ -115,17 +119,17 @@ func TestGetReflogCommits(t *testing.T) {
{
testName: "when passing filterPath",
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--abbrev=40", "--format=%h%x00%ct%x00%gs%x00%p", "--follow", "--", "path"}, reflogOutput, nil),
ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--abbrev=40", "--format=%h%x00%ct%x00%gs%x00%P", "--follow", "--", "path"}, reflogOutput, nil),
lastReflogCommit: &models.Commit{
lastReflogCommit: models.NewCommit(hashPool, models.NewCommitOpts{
Hash: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from B to A",
Status: models.StatusReflog,
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
}),
filterPath: "path",
expectedCommits: []*models.Commit{
expectedCommitOpts: []models.NewCommitOpts{
{
Hash: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
@@ -140,17 +144,17 @@ func TestGetReflogCommits(t *testing.T) {
{
testName: "when passing filterAuthor",
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--abbrev=40", "--format=%h%x00%ct%x00%gs%x00%p", "--author=John Doe <john@doe.com>"}, reflogOutput, nil),
ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--abbrev=40", "--format=%h%x00%ct%x00%gs%x00%P", "--author=John Doe <john@doe.com>"}, reflogOutput, nil),
lastReflogCommit: &models.Commit{
lastReflogCommit: models.NewCommit(hashPool, models.NewCommitOpts{
Hash: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from B to A",
Status: models.StatusReflog,
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
}),
filterAuthor: "John Doe <john@doe.com>",
expectedCommits: []*models.Commit{
expectedCommitOpts: []models.NewCommitOpts{
{
Hash: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
@@ -165,11 +169,11 @@ func TestGetReflogCommits(t *testing.T) {
{
testName: "when command returns error",
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--abbrev=40", "--format=%h%x00%ct%x00%gs%x00%p"}, "", errors.New("haha")),
ExpectGitArgs([]string{"-c", "log.showSignature=false", "log", "-g", "--abbrev=40", "--format=%h%x00%ct%x00%gs%x00%P"}, "", errors.New("haha")),
lastReflogCommit: nil,
filterPath: "",
expectedCommits: nil,
expectedCommitOpts: nil,
expectedOnlyObtainedNew: false,
expectedError: errors.New("haha"),
},
@@ -178,15 +182,20 @@ func TestGetReflogCommits(t *testing.T) {
for _, scenario := range scenarios {
t.Run(scenario.testName, func(t *testing.T) {
builder := &ReflogCommitLoader{
Common: utils.NewDummyCommon(),
Common: common.NewDummyCommon(),
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
}
commits, onlyObtainednew, err := builder.GetReflogCommits(scenario.lastReflogCommit, scenario.filterPath, scenario.filterAuthor)
commits, onlyObtainednew, err := builder.GetReflogCommits(hashPool, scenario.lastReflogCommit, scenario.filterPath, scenario.filterAuthor)
assert.Equal(t, scenario.expectedOnlyObtainedNew, onlyObtainednew)
assert.Equal(t, scenario.expectedError, err)
t.Logf("actual commits: \n%s", litter.Sdump(commits))
assert.Equal(t, scenario.expectedCommits, commits)
var expectedCommits []*models.Commit
if scenario.expectedCommitOpts != nil {
expectedCommits = lo.Map(scenario.expectedCommitOpts,
func(opts models.NewCommitOpts, _ int) *models.Commit { return models.NewCommit(hashPool, opts) })
}
assert.Equal(t, expectedCommits, commits)
scenario.runner.CheckForMissingCalls()
})
+5 -5
View File
@@ -2,6 +2,7 @@ package git_commands
import (
"fmt"
"slices"
"strings"
"sync"
@@ -11,7 +12,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
"golang.org/x/exp/slices"
)
type RemoteLoader struct {
@@ -67,15 +67,15 @@ func (self *RemoteLoader) GetRemotes() ([]*models.Remote, error) {
})
// now lets sort our remotes by name alphabetically
slices.SortFunc(remotes, func(a, b *models.Remote) bool {
slices.SortFunc(remotes, func(a, b *models.Remote) int {
// we want origin at the top because we'll be most likely to want it
if a.Name == "origin" {
return true
return -1
}
if b.Name == "origin" {
return false
return 1
}
return strings.ToLower(a.Name) < strings.ToLower(b.Name)
return strings.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name))
})
return remotes, nil
+1 -1
View File
@@ -80,7 +80,7 @@ func (self *StashCommands) Hash(index int) (string, error) {
return strings.Trim(hash, "\r\n"), err
}
func (self *StashCommands) ShowStashEntryCmdObj(index int) oscommands.ICmdObj {
func (self *StashCommands) ShowStashEntryCmdObj(index int) *oscommands.CmdObj {
// "-u" is the same as "--include-untracked", but the latter fails in older git versions for some reason
cmdArgs := NewGitCmd("stash").Arg("show").
Arg("-p").
@@ -5,7 +5,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/stretchr/testify/assert"
)
@@ -50,7 +50,7 @@ func TestGetStashEntries(t *testing.T) {
t.Run(s.testName, func(t *testing.T) {
cmd := oscommands.NewDummyCmdObjBuilder(s.runner)
loader := NewStashLoader(utils.NewDummyCommon(), cmd)
loader := NewStashLoader(common.NewDummyCommon(), cmd)
assert.EqualValues(t, s.expectedStashEntries, loader.GetStashEntries(s.filterPath))
})
+49 -31
View File
@@ -5,7 +5,7 @@ import (
"path/filepath"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/commands/models"
)
type StatusCommands struct {
@@ -20,50 +20,68 @@ func NewStatusCommands(
}
}
// RebaseMode returns "" for non-rebase mode, "normal" for normal rebase
// and "interactive" for interactive rebase
func (self *StatusCommands) RebaseMode() (enums.RebaseMode, error) {
ok, err := self.IsInNormalRebase()
if err == nil && ok {
return enums.REBASE_MODE_NORMAL, nil
}
ok, err = self.IsInInteractiveRebase()
if err == nil && ok {
return enums.REBASE_MODE_INTERACTIVE, err
}
return enums.REBASE_MODE_NONE, err
}
func (self *StatusCommands) WorkingTreeState() enums.RebaseMode {
rebaseMode, _ := self.RebaseMode()
if rebaseMode != enums.REBASE_MODE_NONE {
return enums.REBASE_MODE_REBASING
}
merging, _ := self.IsInMergeState()
if merging {
return enums.REBASE_MODE_MERGING
}
return enums.REBASE_MODE_NONE
func (self *StatusCommands) WorkingTreeState() models.WorkingTreeState {
result := models.WorkingTreeState{}
result.Rebasing, _ = self.IsInRebase()
result.Merging, _ = self.IsInMergeState()
result.CherryPicking, _ = self.IsInCherryPick()
result.Reverting, _ = self.IsInRevert()
return result
}
func (self *StatusCommands) IsBareRepo() bool {
return self.repoPaths.isBareRepo
}
func (self *StatusCommands) IsInNormalRebase() (bool, error) {
func (self *StatusCommands) IsInRebase() (bool, error) {
exists, err := self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge"))
if err == nil && exists {
return true, nil
}
return self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-apply"))
}
func (self *StatusCommands) IsInInteractiveRebase() (bool, error) {
return self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge"))
}
// IsInMergeState states whether we are still mid-merge
func (self *StatusCommands) IsInMergeState() (bool, error) {
return self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "MERGE_HEAD"))
}
func (self *StatusCommands) IsInCherryPick() (bool, error) {
exists, err := self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "CHERRY_PICK_HEAD"))
if err != nil || !exists {
return exists, err
}
// Sometimes, CHERRY_PICK_HEAD is present during rebases even if no
// cherry-pick is in progress. I suppose this is because rebase used to be
// implemented as a series of cherry-picks, so this could be remnants of
// code that is shared between cherry-pick and rebase, or something. The way
// to tell if this is the case is to check for the presence of the
// stopped-sha file, which records the sha of the last pick that was
// executed before the rebase stopped, and seeing if the sha in that file is
// the same as the one in CHERRY_PICK_HEAD.
cherryPickHead, err := os.ReadFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "CHERRY_PICK_HEAD"))
if err != nil {
return false, err
}
stoppedSha, err := os.ReadFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge", "stopped-sha"))
if err != nil {
// If we get an error we assume the file doesn't exist
return true, nil
}
cherryPickHeadStr := strings.TrimSpace(string(cherryPickHead))
stoppedShaStr := strings.TrimSpace(string(stoppedSha))
// Need to use HasPrefix here because the cherry-pick HEAD is a full sha1,
// but stopped-sha is an abbreviated sha1
if strings.HasPrefix(cherryPickHeadStr, stoppedShaStr) {
return false, nil
}
return true, nil
}
func (self *StatusCommands) IsInRevert() (bool, error) {
return self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "REVERT_HEAD"))
}
// Full ref (e.g. "refs/heads/mybranch") of the branch that is currently
// being rebased, or empty string when we're not in a rebase
func (self *StatusCommands) BranchBeingRebased() string {
+11 -4
View File
@@ -237,28 +237,35 @@ func (self *SubmoduleCommands) Update(path string) error {
return self.cmd.New(cmdArgs).Run()
}
func (self *SubmoduleCommands) BulkInitCmdObj() oscommands.ICmdObj {
func (self *SubmoduleCommands) BulkInitCmdObj() *oscommands.CmdObj {
cmdArgs := NewGitCmd("submodule").Arg("init").
ToArgv()
return self.cmd.New(cmdArgs)
}
func (self *SubmoduleCommands) BulkUpdateCmdObj() oscommands.ICmdObj {
func (self *SubmoduleCommands) BulkUpdateCmdObj() *oscommands.CmdObj {
cmdArgs := NewGitCmd("submodule").Arg("update").
ToArgv()
return self.cmd.New(cmdArgs)
}
func (self *SubmoduleCommands) ForceBulkUpdateCmdObj() oscommands.ICmdObj {
func (self *SubmoduleCommands) ForceBulkUpdateCmdObj() *oscommands.CmdObj {
cmdArgs := NewGitCmd("submodule").Arg("update", "--force").
ToArgv()
return self.cmd.New(cmdArgs)
}
func (self *SubmoduleCommands) BulkDeinitCmdObj() oscommands.ICmdObj {
func (self *SubmoduleCommands) BulkUpdateRecursivelyCmdObj() *oscommands.CmdObj {
cmdArgs := NewGitCmd("submodule").Arg("update", "--init", "--recursive").
ToArgv()
return self.cmd.New(cmdArgs)
}
func (self *SubmoduleCommands) BulkDeinitCmdObj() *oscommands.CmdObj {
cmdArgs := NewGitCmd("submodule").Arg("deinit", "--all", "--force").
ToArgv()
+3 -3
View File
@@ -28,7 +28,7 @@ type PushOpts struct {
SetUpstream bool
}
func (self *SyncCommands) PushCmdObj(task gocui.Task, opts PushOpts) (oscommands.ICmdObj, error) {
func (self *SyncCommands) PushCmdObj(task gocui.Task, opts PushOpts) (*oscommands.CmdObj, error) {
if opts.UpstreamBranch != "" && opts.UpstreamRemote == "" {
return nil, errors.New(self.Tr.MustSpecifyOriginError)
}
@@ -62,7 +62,7 @@ func (self *SyncCommands) fetchCommandBuilder(fetchAll bool) *GitCommandBuilder
ArgIf(self.version.IsAtLeast(2, 29, 0), "--no-write-fetch-head")
}
func (self *SyncCommands) FetchCmdObj(task gocui.Task) oscommands.ICmdObj {
func (self *SyncCommands) FetchCmdObj(task gocui.Task) *oscommands.CmdObj {
cmdArgs := self.fetchCommandBuilder(self.UserConfig().Git.FetchAll).ToArgv()
cmdObj := self.cmd.New(cmdArgs)
@@ -74,7 +74,7 @@ func (self *SyncCommands) Fetch(task gocui.Task) error {
return self.FetchCmdObj(task).Run()
}
func (self *SyncCommands) FetchBackgroundCmdObj() oscommands.ICmdObj {
func (self *SyncCommands) FetchBackgroundCmdObj() *oscommands.CmdObj {
cmdArgs := self.fetchCommandBuilder(self.UserConfig().Git.FetchAll).ToArgv()
cmdObj := self.cmd.New(cmdArgs)
+14 -14
View File
@@ -12,14 +12,14 @@ func TestSyncPush(t *testing.T) {
type scenario struct {
testName string
opts PushOpts
test func(oscommands.ICmdObj, error)
test func(*oscommands.CmdObj, error)
}
scenarios := []scenario{
{
testName: "Push with force disabled",
opts: PushOpts{ForceWithLease: false},
test: func(cmdObj oscommands.ICmdObj, err error) {
test: func(cmdObj *oscommands.CmdObj, err error) {
assert.Equal(t, cmdObj.Args(), []string{"git", "push"})
assert.NoError(t, err)
},
@@ -27,7 +27,7 @@ func TestSyncPush(t *testing.T) {
{
testName: "Push with force-with-lease enabled",
opts: PushOpts{ForceWithLease: true},
test: func(cmdObj oscommands.ICmdObj, err error) {
test: func(cmdObj *oscommands.CmdObj, err error) {
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "--force-with-lease"})
assert.NoError(t, err)
},
@@ -35,7 +35,7 @@ func TestSyncPush(t *testing.T) {
{
testName: "Push with force enabled",
opts: PushOpts{Force: true},
test: func(cmdObj oscommands.ICmdObj, err error) {
test: func(cmdObj *oscommands.CmdObj, err error) {
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "--force"})
assert.NoError(t, err)
},
@@ -48,7 +48,7 @@ func TestSyncPush(t *testing.T) {
UpstreamRemote: "origin",
UpstreamBranch: "master",
},
test: func(cmdObj oscommands.ICmdObj, err error) {
test: func(cmdObj *oscommands.CmdObj, err error) {
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "origin", "refs/heads/master:master"})
assert.NoError(t, err)
},
@@ -62,7 +62,7 @@ func TestSyncPush(t *testing.T) {
UpstreamBranch: "master",
SetUpstream: true,
},
test: func(cmdObj oscommands.ICmdObj, err error) {
test: func(cmdObj *oscommands.CmdObj, err error) {
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "--set-upstream", "origin", "refs/heads/master-local:master"})
assert.NoError(t, err)
},
@@ -76,7 +76,7 @@ func TestSyncPush(t *testing.T) {
UpstreamBranch: "master",
SetUpstream: true,
},
test: func(cmdObj oscommands.ICmdObj, err error) {
test: func(cmdObj *oscommands.CmdObj, err error) {
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "--force-with-lease", "--set-upstream", "origin", "refs/heads/master:master"})
assert.NoError(t, err)
},
@@ -89,7 +89,7 @@ func TestSyncPush(t *testing.T) {
UpstreamBranch: "master",
SetUpstream: true,
},
test: func(cmdObj oscommands.ICmdObj, err error) {
test: func(cmdObj *oscommands.CmdObj, err error) {
assert.Error(t, err)
assert.EqualValues(t, "Must specify a remote if specifying a branch", err.Error())
},
@@ -109,14 +109,14 @@ func TestSyncFetch(t *testing.T) {
type scenario struct {
testName string
fetchAllConfig bool
test func(oscommands.ICmdObj)
test func(*oscommands.CmdObj)
}
scenarios := []scenario{
{
testName: "Fetch in foreground (all=false)",
fetchAllConfig: false,
test: func(cmdObj oscommands.ICmdObj) {
test: func(cmdObj *oscommands.CmdObj) {
assert.True(t, cmdObj.ShouldLog())
assert.Equal(t, cmdObj.GetCredentialStrategy(), oscommands.PROMPT)
assert.Equal(t, cmdObj.Args(), []string{"git", "fetch"})
@@ -125,7 +125,7 @@ func TestSyncFetch(t *testing.T) {
{
testName: "Fetch in foreground (all=true)",
fetchAllConfig: true,
test: func(cmdObj oscommands.ICmdObj) {
test: func(cmdObj *oscommands.CmdObj) {
assert.True(t, cmdObj.ShouldLog())
assert.Equal(t, cmdObj.GetCredentialStrategy(), oscommands.PROMPT)
assert.Equal(t, cmdObj.Args(), []string{"git", "fetch", "--all"})
@@ -147,14 +147,14 @@ func TestSyncFetchBackground(t *testing.T) {
type scenario struct {
testName string
fetchAllConfig bool
test func(oscommands.ICmdObj)
test func(*oscommands.CmdObj)
}
scenarios := []scenario{
{
testName: "Fetch in background (all=false)",
fetchAllConfig: false,
test: func(cmdObj oscommands.ICmdObj) {
test: func(cmdObj *oscommands.CmdObj) {
assert.False(t, cmdObj.ShouldLog())
assert.Equal(t, cmdObj.GetCredentialStrategy(), oscommands.FAIL)
assert.Equal(t, cmdObj.Args(), []string{"git", "fetch"})
@@ -163,7 +163,7 @@ func TestSyncFetchBackground(t *testing.T) {
{
testName: "Fetch in background (all=true)",
fetchAllConfig: true,
test: func(cmdObj oscommands.ICmdObj) {
test: func(cmdObj *oscommands.CmdObj) {
assert.False(t, cmdObj.ShouldLog())
assert.Equal(t, cmdObj.GetCredentialStrategy(), oscommands.FAIL)
assert.Equal(t, cmdObj.Args(), []string{"git", "fetch", "--all"})
+8 -5
View File
@@ -1,6 +1,9 @@
package git_commands
import "github.com/jesseduffield/gocui"
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
)
type TagCommands struct {
*GitCommon
@@ -12,24 +15,24 @@ func NewTagCommands(gitCommon *GitCommon) *TagCommands {
}
}
func (self *TagCommands) CreateLightweight(tagName string, ref string, force bool) error {
func (self *TagCommands) CreateLightweightObj(tagName string, ref string, force bool) *oscommands.CmdObj {
cmdArgs := NewGitCmd("tag").
ArgIf(force, "--force").
Arg("--", tagName).
ArgIf(len(ref) > 0, ref).
ToArgv()
return self.cmd.New(cmdArgs).Run()
return self.cmd.New(cmdArgs)
}
func (self *TagCommands) CreateAnnotated(tagName, ref, msg string, force bool) error {
func (self *TagCommands) CreateAnnotatedObj(tagName, ref, msg string, force bool) *oscommands.CmdObj {
cmdArgs := NewGitCmd("tag").Arg(tagName).
ArgIf(force, "--force").
ArgIf(len(ref) > 0, ref).
Arg("-m", msg).
ToArgv()
return self.cmd.New(cmdArgs).Run()
return self.cmd.New(cmdArgs)
}
func (self *TagCommands) HasTag(tagName string) bool {
+2 -2
View File
@@ -5,7 +5,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/stretchr/testify/assert"
)
@@ -46,7 +46,7 @@ func TestGetTags(t *testing.T) {
for _, scenario := range scenarios {
t.Run(scenario.testName, func(t *testing.T) {
loader := &TagLoader{
Common: utils.NewDummyCommon(),
Common: common.NewDummyCommon(),
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
}
+38 -24
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/models"
@@ -28,17 +29,21 @@ func NewWorkingTreeCommands(
}
}
func (self *WorkingTreeCommands) OpenMergeToolCmdObj() oscommands.ICmdObj {
func (self *WorkingTreeCommands) OpenMergeToolCmdObj() *oscommands.CmdObj {
return self.cmd.New(NewGitCmd("mergetool").ToArgv())
}
// StageFile stages a file
func (self *WorkingTreeCommands) StageFile(path string) error {
return self.StageFiles([]string{path})
return self.StageFiles([]string{path}, nil)
}
func (self *WorkingTreeCommands) StageFiles(paths []string) error {
cmdArgs := NewGitCmd("add").Arg("--").Arg(paths...).ToArgv()
func (self *WorkingTreeCommands) StageFiles(paths []string, extraArgs []string) error {
cmdArgs := NewGitCmd("add").
Arg(extraArgs...).
Arg("--").
Arg(paths...).
ToArgv()
return self.cmd.New(cmdArgs).Run()
}
@@ -88,11 +93,11 @@ func (self *WorkingTreeCommands) BeforeAndAfterFileForRename(file *models.File)
var beforeFile *models.File
var afterFile *models.File
for _, f := range filesWithoutRenames {
if f.Name == file.PreviousName {
if f.Path == file.PreviousPath {
beforeFile = f
}
if f.Name == file.Name {
if f.Path == file.Path {
afterFile = f
}
}
@@ -109,10 +114,6 @@ func (self *WorkingTreeCommands) BeforeAndAfterFileForRename(file *models.File)
return beforeFile, afterFile, nil
}
func newCheckoutCommand() *GitCommandBuilder {
return NewGitCmd("checkout").Config(fmt.Sprintf("core.hooksPath=%s", os.DevNull))
}
// DiscardAllFileChanges directly
func (self *WorkingTreeCommands) DiscardAllFileChanges(file *models.File) error {
if file.IsRename() {
@@ -134,13 +135,13 @@ func (self *WorkingTreeCommands) DiscardAllFileChanges(file *models.File) error
if file.ShortStatus == "AA" {
if err := self.cmd.New(
newCheckoutCommand().Arg("--ours", "--", file.Name).ToArgv(),
NewGitCmd("checkout").Arg("--ours", "--", file.Path).ToArgv(),
).Run(); err != nil {
return err
}
if err := self.cmd.New(
NewGitCmd("add").Arg("--", file.Name).ToArgv(),
NewGitCmd("add").Arg("--", file.Path).ToArgv(),
).Run(); err != nil {
return err
}
@@ -149,14 +150,14 @@ func (self *WorkingTreeCommands) DiscardAllFileChanges(file *models.File) error
if file.ShortStatus == "DU" {
return self.cmd.New(
NewGitCmd("rm").Arg("--", file.Name).ToArgv(),
NewGitCmd("rm").Arg("--", file.Path).ToArgv(),
).Run()
}
// if the file isn't tracked, we assume you want to delete it
if file.HasStagedChanges || file.HasMergeConflicts {
if err := self.cmd.New(
NewGitCmd("reset").Arg("--", file.Name).ToArgv(),
NewGitCmd("reset").Arg("--", file.Path).ToArgv(),
).Run(); err != nil {
return err
}
@@ -167,7 +168,7 @@ func (self *WorkingTreeCommands) DiscardAllFileChanges(file *models.File) error
}
if file.Added {
return self.os.RemoveFile(file.Name)
return self.os.RemoveFile(file.Path)
}
return self.DiscardUnstagedFileChanges(file)
@@ -193,13 +194,13 @@ func (self *WorkingTreeCommands) DiscardUnstagedDirChanges(node IFileNode) error
return err
}
cmdArgs := newCheckoutCommand().Arg("--", node.GetPath()).ToArgv()
cmdArgs := NewGitCmd("checkout").Arg("--", node.GetPath()).ToArgv()
if err := self.cmd.New(cmdArgs).Run(); err != nil {
return err
}
} else {
if file.Added && !file.HasStagedChanges {
return self.os.RemoveFile(file.Name)
return self.os.RemoveFile(file.Path)
}
if err := self.DiscardUnstagedFileChanges(file); err != nil {
@@ -226,19 +227,25 @@ func (self *WorkingTreeCommands) RemoveUntrackedDirFiles(node IFileNode) error {
}
func (self *WorkingTreeCommands) DiscardUnstagedFileChanges(file *models.File) error {
cmdArgs := newCheckoutCommand().Arg("--", file.Name).ToArgv()
cmdArgs := NewGitCmd("checkout").Arg("--", file.Path).ToArgv()
return self.cmd.New(cmdArgs).Run()
}
// Escapes special characters in a filename for gitignore and exclude files
func escapeFilename(filename string) string {
re := regexp.MustCompile(`^[!#]|[\[\]*]`)
return re.ReplaceAllString(filename, `\${0}`)
}
// Ignore adds a file to the gitignore for the repo
func (self *WorkingTreeCommands) Ignore(filename string) error {
return self.os.AppendLineToFile(".gitignore", filename)
return self.os.AppendLineToFile(".gitignore", escapeFilename(filename))
}
// Exclude adds a file to the .git/info/exclude for the repo
func (self *WorkingTreeCommands) Exclude(filename string) error {
excludeFile := filepath.Join(self.repoPaths.repoGitDirPath, "info", "exclude")
return self.os.AppendLineToFile(excludeFile, filename)
return self.os.AppendLineToFile(excludeFile, escapeFilename(filename))
}
// WorktreeFileDiff returns the diff of a file
@@ -248,7 +255,7 @@ func (self *WorkingTreeCommands) WorktreeFileDiff(file *models.File, plain bool,
return s
}
func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain bool, cached bool) oscommands.ICmdObj {
func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain bool, cached bool) *oscommands.CmdObj {
colorArg := self.UserConfig().Git.Paging.ColorArg
if plain {
colorArg = "never"
@@ -286,7 +293,7 @@ func (self *WorkingTreeCommands) ShowFileDiff(from string, to string, reverse bo
return self.ShowFileDiffCmdObj(from, to, reverse, fileName, plain).RunWithOutput()
}
func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reverse bool, fileName string, plain bool) oscommands.ICmdObj {
func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reverse bool, fileName string, plain bool) *oscommands.CmdObj {
contextSize := self.AppState.DiffContextSize
colorArg := self.UserConfig().Git.Paging.ColorArg
@@ -319,7 +326,7 @@ func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reve
// CheckoutFile checks out the file for the given commit
func (self *WorkingTreeCommands) CheckoutFile(commitHash, fileName string) error {
cmdArgs := newCheckoutCommand().Arg(commitHash, "--", fileName).
cmdArgs := NewGitCmd("checkout").Arg(commitHash, "--", fileName).
ToArgv()
return self.cmd.New(cmdArgs).Run()
@@ -327,7 +334,7 @@ func (self *WorkingTreeCommands) CheckoutFile(commitHash, fileName string) error
// DiscardAnyUnstagedFileChanges discards any unstaged file changes via `git checkout -- .`
func (self *WorkingTreeCommands) DiscardAnyUnstagedFileChanges() error {
cmdArgs := newCheckoutCommand().Arg("--", ".").
cmdArgs := NewGitCmd("checkout").Arg("--", ".").
ToArgv()
return self.cmd.New(cmdArgs).Run()
@@ -341,6 +348,13 @@ func (self *WorkingTreeCommands) RemoveTrackedFiles(name string) error {
return self.cmd.New(cmdArgs).Run()
}
func (self *WorkingTreeCommands) RemoveConflictedFile(name string) error {
cmdArgs := NewGitCmd("rm").Arg("--", name).
ToArgv()
return self.cmd.New(cmdArgs).Run()
}
// RemoveUntrackedFiles runs `git clean -fd`
func (self *WorkingTreeCommands) RemoveUntrackedFiles() error {
cmdArgs := NewGitCmd("clean").Arg("-fd").ToArgv()
+25 -29
View File
@@ -1,8 +1,6 @@
package git_commands
import (
"fmt"
"os"
"testing"
"github.com/go-errors/errors"
@@ -12,8 +10,6 @@ import (
"github.com/stretchr/testify/assert"
)
var disableHooksFlag = fmt.Sprintf("core.hooksPath=%s", os.DevNull)
func TestWorkingTreeStageFile(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"add", "--", "test.txt"}, "", nil)
@@ -30,7 +26,7 @@ func TestWorkingTreeStageFiles(t *testing.T) {
instance := buildWorkingTreeCommands(commonDeps{runner: runner})
assert.NoError(t, instance.StageFiles([]string{"test.txt", "test2.txt"}))
assert.NoError(t, instance.StageFiles([]string{"test.txt", "test2.txt"}, nil))
runner.CheckForMissingCalls()
}
@@ -87,7 +83,7 @@ func TestWorkingTreeDiscardAllFileChanges(t *testing.T) {
{
testName: "An error occurred when resetting",
file: &models.File{
Name: "test",
Path: "test",
HasStagedChanges: true,
},
removeFile: func(string) error { return nil },
@@ -98,7 +94,7 @@ func TestWorkingTreeDiscardAllFileChanges(t *testing.T) {
{
testName: "An error occurred when removing file",
file: &models.File{
Name: "test",
Path: "test",
Tracked: false,
Added: true,
},
@@ -111,57 +107,57 @@ func TestWorkingTreeDiscardAllFileChanges(t *testing.T) {
{
testName: "An error occurred with checkout",
file: &models.File{
Name: "test",
Path: "test",
Tracked: true,
HasStagedChanges: false,
},
removeFile: func(string) error { return nil },
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"-c", disableHooksFlag, "checkout", "--", "test"}, "", errors.New("error")),
ExpectGitArgs([]string{"checkout", "--", "test"}, "", errors.New("error")),
expectedError: "error",
},
{
testName: "Checkout only",
file: &models.File{
Name: "test",
Path: "test",
Tracked: true,
HasStagedChanges: false,
},
removeFile: func(string) error { return nil },
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"-c", disableHooksFlag, "checkout", "--", "test"}, "", nil),
ExpectGitArgs([]string{"checkout", "--", "test"}, "", nil),
expectedError: "",
},
{
testName: "Reset and checkout staged changes",
file: &models.File{
Name: "test",
Path: "test",
Tracked: true,
HasStagedChanges: true,
},
removeFile: func(string) error { return nil },
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"reset", "--", "test"}, "", nil).
ExpectGitArgs([]string{"-c", disableHooksFlag, "checkout", "--", "test"}, "", nil),
ExpectGitArgs([]string{"checkout", "--", "test"}, "", nil),
expectedError: "",
},
{
testName: "Reset and checkout merge conflicts",
file: &models.File{
Name: "test",
Path: "test",
Tracked: true,
HasMergeConflicts: true,
},
removeFile: func(string) error { return nil },
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"reset", "--", "test"}, "", nil).
ExpectGitArgs([]string{"-c", disableHooksFlag, "checkout", "--", "test"}, "", nil),
ExpectGitArgs([]string{"checkout", "--", "test"}, "", nil),
expectedError: "",
},
{
testName: "Reset and remove",
file: &models.File{
Name: "test",
Path: "test",
Tracked: false,
Added: true,
HasStagedChanges: true,
@@ -177,7 +173,7 @@ func TestWorkingTreeDiscardAllFileChanges(t *testing.T) {
{
testName: "Remove only",
file: &models.File{
Name: "test",
Path: "test",
Tracked: false,
Added: true,
HasStagedChanges: false,
@@ -224,7 +220,7 @@ func TestWorkingTreeDiff(t *testing.T) {
{
testName: "Default case",
file: &models.File{
Name: "test.txt",
Path: "test.txt",
HasStagedChanges: false,
Tracked: true,
},
@@ -239,7 +235,7 @@ func TestWorkingTreeDiff(t *testing.T) {
{
testName: "cached",
file: &models.File{
Name: "test.txt",
Path: "test.txt",
HasStagedChanges: false,
Tracked: true,
},
@@ -254,7 +250,7 @@ func TestWorkingTreeDiff(t *testing.T) {
{
testName: "plain",
file: &models.File{
Name: "test.txt",
Path: "test.txt",
HasStagedChanges: false,
Tracked: true,
},
@@ -269,7 +265,7 @@ func TestWorkingTreeDiff(t *testing.T) {
{
testName: "File not tracked and file has no staged changes",
file: &models.File{
Name: "test.txt",
Path: "test.txt",
HasStagedChanges: false,
Tracked: false,
},
@@ -284,7 +280,7 @@ func TestWorkingTreeDiff(t *testing.T) {
{
testName: "Default case (ignore whitespace)",
file: &models.File{
Name: "test.txt",
Path: "test.txt",
HasStagedChanges: false,
Tracked: true,
},
@@ -299,7 +295,7 @@ func TestWorkingTreeDiff(t *testing.T) {
{
testName: "Show diff with custom context size",
file: &models.File{
Name: "test.txt",
Path: "test.txt",
HasStagedChanges: false,
Tracked: true,
},
@@ -314,7 +310,7 @@ func TestWorkingTreeDiff(t *testing.T) {
{
testName: "Show diff with custom similarity threshold",
file: &models.File{
Name: "test.txt",
Path: "test.txt",
HasStagedChanges: false,
Tracked: true,
},
@@ -432,7 +428,7 @@ func TestWorkingTreeCheckoutFile(t *testing.T) {
commitHash: "11af912",
fileName: "test999.txt",
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"-c", disableHooksFlag, "checkout", "11af912", "--", "test999.txt"}, "", nil),
ExpectGitArgs([]string{"checkout", "11af912", "--", "test999.txt"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
@@ -442,7 +438,7 @@ func TestWorkingTreeCheckoutFile(t *testing.T) {
commitHash: "11af912",
fileName: "test999.txt",
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"-c", disableHooksFlag, "checkout", "11af912", "--", "test999.txt"}, "", errors.New("error")),
ExpectGitArgs([]string{"checkout", "11af912", "--", "test999.txt"}, "", errors.New("error")),
test: func(err error) {
assert.Error(t, err)
},
@@ -470,9 +466,9 @@ func TestWorkingTreeDiscardUnstagedFileChanges(t *testing.T) {
scenarios := []scenario{
{
testName: "valid case",
file: &models.File{Name: "test.txt"},
file: &models.File{Path: "test.txt"},
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"-c", disableHooksFlag, "checkout", "--", "test.txt"}, "", nil),
ExpectGitArgs([]string{"checkout", "--", "test.txt"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
@@ -499,7 +495,7 @@ func TestWorkingTreeDiscardAnyUnstagedFileChanges(t *testing.T) {
{
testName: "valid case",
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"-c", disableHooksFlag, "checkout", "--", "."}, "", nil),
ExpectGitArgs([]string{"checkout", "--", "."}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
@@ -15,6 +15,8 @@ type IGitConfig interface {
GetGeneral(string) string
// this is for when you want to pass 'mykey' and check if the result is truthy
GetBool(string) bool
DropCache()
}
type CachedGitConfig struct {
@@ -93,3 +95,10 @@ func isTruthy(value string) bool {
lcValue := strings.ToLower(value)
return lcValue == "true" || lcValue == "1" || lcValue == "yes" || lcValue == "on"
}
func (self *CachedGitConfig) DropCache() {
self.mutex.Lock()
defer self.mutex.Unlock()
self.cache = make(map[string]string)
}
@@ -27,3 +27,6 @@ func (self *FakeGitConfig) GetGeneral(args string) string {
func (self *FakeGitConfig) GetBool(key string) bool {
return isTruthy(self.Get(key))
}
func (self *FakeGitConfig) DropCache() {
}
+2 -2
View File
@@ -33,8 +33,8 @@ var bitbucketServiceDef = ServiceDefinition{
var gitLabServiceDef = ServiceDefinition{
provider: "gitlab",
pullRequestURLIntoDefaultBranch: "/-/merge_requests/new?merge_request[source_branch]={{.From}}",
pullRequestURLIntoTargetBranch: "/-/merge_requests/new?merge_request[source_branch]={{.From}}&merge_request[target_branch]={{.To}}",
pullRequestURLIntoDefaultBranch: "/-/merge_requests/new?merge_request%5Bsource_branch%5D={{.From}}",
pullRequestURLIntoTargetBranch: "/-/merge_requests/new?merge_request%5Bsource_branch%5D={{.From}}&merge_request%5Btarget_branch%5D={{.To}}",
commitURL: "/-/commit/{{.CommitHash}}",
regexStrings: defaultUrlRegexStrings,
repoURLTemplate: defaultRepoURLTemplate,
@@ -112,7 +112,7 @@ func TestGetPullRequestURL(t *testing.T) {
remoteUrl: "git@gitlab.com:peter/calculator.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/calculator/-/merge_requests/new?merge_request[source_branch]=feature%2Fui", url)
assert.Equal(t, "https://gitlab.com/peter/calculator/-/merge_requests/new?merge_request%5Bsource_branch%5D=feature%2Fui", url)
},
},
{
@@ -121,7 +121,7 @@ func TestGetPullRequestURL(t *testing.T) {
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/public/calculator/-/merge_requests/new?merge_request[source_branch]=feature%2Fui", url)
assert.Equal(t, "https://gitlab.com/peter/public/calculator/-/merge_requests/new?merge_request%5Bsource_branch%5D=feature%2Fui", url)
},
},
{
@@ -130,7 +130,7 @@ func TestGetPullRequestURL(t *testing.T) {
remoteUrl: "https://gitlab.com/peter/public/calculator.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/public/calculator/-/merge_requests/new?merge_request[source_branch]=feature%2Fui", url)
assert.Equal(t, "https://gitlab.com/peter/public/calculator/-/merge_requests/new?merge_request%5Bsource_branch%5D=feature%2Fui", url)
},
},
{
@@ -140,7 +140,7 @@ func TestGetPullRequestURL(t *testing.T) {
remoteUrl: "git@gitlab.com:peter/calculator.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/calculator/-/merge_requests/new?merge_request[source_branch]=feature%2Fcommit-ui&merge_request[target_branch]=epic%2Fui", url)
assert.Equal(t, "https://gitlab.com/peter/calculator/-/merge_requests/new?merge_request%5Bsource_branch%5D=feature%2Fcommit-ui&merge_request%5Btarget_branch%5D=epic%2Fui", url)
},
},
{
@@ -150,7 +150,7 @@ func TestGetPullRequestURL(t *testing.T) {
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/public/calculator/-/merge_requests/new?merge_request[source_branch]=feature%2Fcommit-ui&merge_request[target_branch]=epic%2Fui", url)
assert.Equal(t, "https://gitlab.com/peter/public/calculator/-/merge_requests/new?merge_request%5Bsource_branch%5D=feature%2Fcommit-ui&merge_request%5Btarget_branch%5D=epic%2Fui", url)
},
},
{
@@ -160,7 +160,7 @@ func TestGetPullRequestURL(t *testing.T) {
remoteUrl: "https://gitlab.com/peter/public/calculator.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/public/calculator/-/merge_requests/new?merge_request[source_branch]=feature%2Fcommit-ui&merge_request[target_branch]=epic%2Fui", url)
assert.Equal(t, "https://gitlab.com/peter/public/calculator/-/merge_requests/new?merge_request%5Bsource_branch%5D=feature%2Fcommit-ui&merge_request%5Btarget_branch%5D=epic%2Fui", url)
},
},
{
@@ -362,7 +362,7 @@ func TestGetPullRequestURL(t *testing.T) {
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://my.domain.test:1111/johndoe/social_network/-/merge_requests/new?merge_request[source_branch]=feature%2Fprofile-page", url)
assert.Equal(t, "https://my.domain.test:1111/johndoe/social_network/-/merge_requests/new?merge_request%5Bsource_branch%5D=feature%2Fprofile-page", url)
},
},
{
@@ -410,7 +410,7 @@ func TestGetPullRequestURL(t *testing.T) {
remoteUrl: "git@gitlab.com:me/public/repo-with-issues.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/me/public/repo-with-issues/-/merge_requests/new?merge_request[source_branch]=feature%2FsomeIssue%23123&merge_request[target_branch]=master", url)
assert.Equal(t, "https://gitlab.com/me/public/repo-with-issues/-/merge_requests/new?merge_request%5Bsource_branch%5D=feature%2FsomeIssue%23123&merge_request%5Btarget_branch%5D=master", url)
},
},
{
@@ -420,7 +420,7 @@ func TestGetPullRequestURL(t *testing.T) {
remoteUrl: "git@gitlab.com:me/public/repo-with-issues.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/me/public/repo-with-issues/-/merge_requests/new?merge_request[source_branch]=yolo&merge_request[target_branch]=archive%2Fnever-ending-feature%23666", url)
assert.Equal(t, "https://gitlab.com/me/public/repo-with-issues/-/merge_requests/new?merge_request%5Bsource_branch%5D=yolo&merge_request%5Btarget_branch%5D=archive%2Fnever-ending-feature%23666", url)
},
},
}
+64 -17
View File
@@ -4,13 +4,14 @@ import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
"github.com/stefanhaller/git-todo-parser/todo"
)
// Special commit hash for empty tree object
const EmptyTreeCommitHash = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
type CommitStatus int
type CommitStatus uint8
const (
StatusNone CommitStatus = iota
@@ -18,7 +19,8 @@ const (
StatusPushed
StatusMerged
StatusRebasing
StatusSelected
StatusCherryPickingOrReverting
StatusConflicted
StatusReflog
)
@@ -26,11 +28,9 @@ const (
// Conveniently for us, the todo package starts the enum at 1, and given
// that it doesn't have a "none" value, we're setting ours to 0
ActionNone todo.TodoCommand = 0
// "Comment" is the last one of the todo package's enum entries
ActionConflict = todo.Comment + 1
)
type Divergence int
type Divergence uint8
// For a divergence log (left/right comparison of two refs) this is set to
// either DivergenceLeft or DivergenceRight for each commit; for normal
@@ -43,35 +43,74 @@ const (
// Commit : A git commit
type Commit struct {
Hash string
hash *string
Name string
Status CommitStatus
Action todo.TodoCommand
Tags []string
ExtraInfo string // something like 'HEAD -> master, tag: v0.15.2'
AuthorName string // something like 'Jesse Duffield'
AuthorEmail string // something like 'jessedduffield@gmail.com'
UnixTimestamp int64
Divergence Divergence // set to DivergenceNone unless we are showing the divergence view
// Hashes of parent commits (will be multiple if it's a merge commit)
Parents []string
parents []*string
Status CommitStatus
Action todo.TodoCommand
Divergence Divergence // set to DivergenceNone unless we are showing the divergence view
}
type NewCommitOpts struct {
Hash string
Name string
Status CommitStatus
Action todo.TodoCommand
Tags []string
ExtraInfo string
AuthorName string
AuthorEmail string
UnixTimestamp int64
Divergence Divergence
Parents []string
}
func NewCommit(hashPool *utils.StringPool, opts NewCommitOpts) *Commit {
return &Commit{
hash: hashPool.Add(opts.Hash),
Name: opts.Name,
Status: opts.Status,
Action: opts.Action,
Tags: opts.Tags,
ExtraInfo: opts.ExtraInfo,
AuthorName: opts.AuthorName,
AuthorEmail: opts.AuthorEmail,
UnixTimestamp: opts.UnixTimestamp,
Divergence: opts.Divergence,
parents: lo.Map(opts.Parents, func(s string, _ int) *string { return hashPool.Add(s) }),
}
}
func (c *Commit) Hash() string {
return *c.hash
}
func (c *Commit) HashPtr() *string {
return c.hash
}
func (c *Commit) ShortHash() string {
return utils.ShortHash(c.Hash)
return utils.ShortHash(c.Hash())
}
func (c *Commit) FullRefName() string {
return c.Hash
return c.Hash()
}
func (c *Commit) RefName() string {
return c.Hash
return c.Hash()
}
func (c *Commit) ShortRefName() string {
return c.Hash[:7]
return c.Hash()[:7]
}
func (c *Commit) ParentRefName() string {
@@ -81,8 +120,16 @@ func (c *Commit) ParentRefName() string {
return c.RefName() + "^"
}
func (c *Commit) Parents() []string {
return lo.Map(c.parents, func(s *string, _ int) string { return *s })
}
func (c *Commit) ParentPtrs() []*string {
return c.parents
}
func (c *Commit) IsFirstCommit() bool {
return len(c.Parents) == 0
return len(c.parents) == 0
}
func (c *Commit) ID() string {
@@ -90,11 +137,11 @@ func (c *Commit) ID() string {
}
func (c *Commit) Description() string {
return fmt.Sprintf("%s %s", c.Hash[:7], c.Name)
return fmt.Sprintf("%s %s", c.Hash()[:7], c.Name)
}
func (c *Commit) IsMerge() bool {
return len(c.Parents) > 1
return len(c.parents) > 1
}
// returns true if this commit is not actually in the git log but instead
+4 -5
View File
@@ -2,18 +2,17 @@ package models
// CommitFile : A git commit file
type CommitFile struct {
// TODO: rename this to Path
Name string
Path string
ChangeStatus string // e.g. 'A' for added or 'M' for modified. This is based on the result from git diff --name-status
}
func (f *CommitFile) ID() string {
return f.Name
return f.Path
}
func (f *CommitFile) Description() string {
return f.Name
return f.Path
}
func (f *CommitFile) Added() bool {
@@ -25,5 +24,5 @@ func (f *CommitFile) Deleted() bool {
}
func (f *CommitFile) GetPath() string {
return f.Name
return f.Path
}
+28 -11
View File
@@ -1,6 +1,7 @@
package models
import (
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
@@ -8,8 +9,8 @@ import (
// File : A file from git status
// duplicating this for now
type File struct {
Name string
PreviousName string
Path string
PreviousPath string
HasStagedChanges bool
HasUnstagedChanges bool
Tracked bool
@@ -37,14 +38,14 @@ type IFile interface {
}
func (f *File) IsRename() bool {
return f.PreviousName != ""
return f.PreviousPath != ""
}
// Names returns an array containing just the filename, or in the case of a rename, the after filename and the before filename
func (f *File) Names() []string {
result := []string{f.Name}
if f.PreviousName != "" {
result = append(result, f.PreviousName)
result := []string{f.Path}
if f.PreviousPath != "" {
result = append(result, f.PreviousPath)
}
return result
}
@@ -55,11 +56,11 @@ func (f *File) Matches(f2 *File) bool {
}
func (f *File) ID() string {
return f.Name
return f.Path
}
func (f *File) Description() string {
return f.Name
return f.Path
}
func (f *File) IsSubmodule(configs []*SubmoduleConfig) bool {
@@ -68,7 +69,7 @@ func (f *File) IsSubmodule(configs []*SubmoduleConfig) bool {
func (f *File) SubmoduleConfig(configs []*SubmoduleConfig) *SubmoduleConfig {
for _, config := range configs {
if f.Name == config.Path {
if f.Path == config.Path {
return config
}
}
@@ -90,17 +91,33 @@ func (f *File) GetIsTracked() bool {
func (f *File) GetPath() string {
// TODO: remove concept of name; just use path
return f.Name
return f.Path
}
func (f *File) GetPreviousPath() string {
return f.PreviousName
return f.PreviousPath
}
func (f *File) GetIsFile() bool {
return true
}
func (f *File) GetMergeStateDescription(tr *i18n.TranslationSet) string {
m := map[string]string{
"DD": tr.MergeConflictDescription_DD,
"AU": tr.MergeConflictDescription_AU,
"UA": tr.MergeConflictDescription_UA,
"DU": tr.MergeConflictDescription_DU,
"UD": tr.MergeConflictDescription_UD,
}
if description, ok := m[f.ShortStatus]; ok {
return description
}
panic("should only be called if there's a merge conflict")
}
type StatusFields struct {
HasStagedChanges bool
HasUnstagedChanges bool
+112
View File
@@ -0,0 +1,112 @@
package models
import "github.com/jesseduffield/lazygit/pkg/i18n"
// The state of the working tree. Several of these can be true at once.
// In particular, the concrete multi-state combinations that can occur in
// practice are Rebasing+CherryPicking, and Rebasing+Reverting. Theoretically, I
// guess Rebasing+Merging could also happen, but it probably won't in practice.
type WorkingTreeState struct {
Rebasing bool
Merging bool
CherryPicking bool
Reverting bool
}
func (self WorkingTreeState) Any() bool {
return self.Rebasing || self.Merging || self.CherryPicking || self.Reverting
}
func (self WorkingTreeState) None() bool {
return !self.Any()
}
type EffectiveWorkingTreeState int
const (
// this means we're neither rebasing nor merging, cherry-picking, or reverting
WORKING_TREE_STATE_NONE EffectiveWorkingTreeState = iota
WORKING_TREE_STATE_REBASING
WORKING_TREE_STATE_MERGING
WORKING_TREE_STATE_CHERRY_PICKING
WORKING_TREE_STATE_REVERTING
)
// Effective returns the "current" state; if several states are true at once,
// this is the one that should be displayed in status views, and it's the one
// that the user can continue or abort.
//
// As an example, if you are stopped in an interactive rebase, and then you
// perform a cherry-pick, and the cherry-pick conflicts, then both
// WorkingTreeState.Rebasing and WorkingTreeState.CherryPicking are true.
// The effective state is cherry-picking, because that's the one you can
// continue or abort. It is not possible to continue the rebase without first
// aborting the cherry-pick.
func (self WorkingTreeState) Effective() EffectiveWorkingTreeState {
if self.Reverting {
return WORKING_TREE_STATE_REVERTING
}
if self.CherryPicking {
return WORKING_TREE_STATE_CHERRY_PICKING
}
if self.Merging {
return WORKING_TREE_STATE_MERGING
}
if self.Rebasing {
return WORKING_TREE_STATE_REBASING
}
return WORKING_TREE_STATE_NONE
}
func (self WorkingTreeState) Title(tr *i18n.TranslationSet) string {
return map[EffectiveWorkingTreeState]string{
WORKING_TREE_STATE_REBASING: tr.RebasingStatus,
WORKING_TREE_STATE_MERGING: tr.MergingStatus,
WORKING_TREE_STATE_CHERRY_PICKING: tr.CherryPickingStatus,
WORKING_TREE_STATE_REVERTING: tr.RevertingStatus,
}[self.Effective()]
}
func (self WorkingTreeState) LowerCaseTitle(tr *i18n.TranslationSet) string {
return map[EffectiveWorkingTreeState]string{
WORKING_TREE_STATE_REBASING: tr.LowercaseRebasingStatus,
WORKING_TREE_STATE_MERGING: tr.LowercaseMergingStatus,
WORKING_TREE_STATE_CHERRY_PICKING: tr.LowercaseCherryPickingStatus,
WORKING_TREE_STATE_REVERTING: tr.LowercaseRevertingStatus,
}[self.Effective()]
}
func (self WorkingTreeState) OptionsMenuTitle(tr *i18n.TranslationSet) string {
return map[EffectiveWorkingTreeState]string{
WORKING_TREE_STATE_REBASING: tr.RebaseOptionsTitle,
WORKING_TREE_STATE_MERGING: tr.MergeOptionsTitle,
WORKING_TREE_STATE_CHERRY_PICKING: tr.CherryPickOptionsTitle,
WORKING_TREE_STATE_REVERTING: tr.RevertOptionsTitle,
}[self.Effective()]
}
func (self WorkingTreeState) OptionsMapTitle(tr *i18n.TranslationSet) string {
return map[EffectiveWorkingTreeState]string{
WORKING_TREE_STATE_REBASING: tr.ViewRebaseOptions,
WORKING_TREE_STATE_MERGING: tr.ViewMergeOptions,
WORKING_TREE_STATE_CHERRY_PICKING: tr.ViewCherryPickOptions,
WORKING_TREE_STATE_REVERTING: tr.ViewRevertOptions,
}[self.Effective()]
}
func (self WorkingTreeState) CommandName() string {
return map[EffectiveWorkingTreeState]string{
WORKING_TREE_STATE_REBASING: "rebase",
WORKING_TREE_STATE_MERGING: "merge",
WORKING_TREE_STATE_CHERRY_PICKING: "cherry-pick",
WORKING_TREE_STATE_REVERTING: "revert",
}[self.Effective()]
}
func (self WorkingTreeState) CanShowTodos() bool {
return self.Rebasing || self.CherryPicking || self.Reverting
}
func (self WorkingTreeState) CanSkip() bool {
return self.Rebasing || self.CherryPicking || self.Reverting
}
+62 -76
View File
@@ -11,67 +11,6 @@ import (
// A command object is a general way to represent a command to be run on the
// command line.
type ICmdObj interface {
GetCmd() *exec.Cmd
// outputs string representation of command. Note that if the command was built
// using NewFromArgs, the output won't be quite the same as what you would type
// into a terminal e.g. 'sh -c git commit' as opposed to 'sh -c "git commit"'
ToString() string
// outputs args vector e.g. ["git", "commit", "-m", "my message"]
Args() []string
AddEnvVars(...string) ICmdObj
GetEnvVars() []string
// sets the working directory
SetWd(string) ICmdObj
// runs the command and returns an error if any
Run() error
// runs the command and returns the output as a string, and an error if any
RunWithOutput() (string, error)
// runs the command and returns stdout and stderr as a string, and an error if any
RunWithOutputs() (string, string, error)
// runs the command and runs a callback function on each line of the output. If the callback returns true for the boolean value, we kill the process and return.
RunAndProcessLines(onLine func(line string) (bool, error)) error
// Be calling DontLog(), we're saying that once we call Run(), we don't want to
// log the command in the UI (it'll still be logged in the log file). The general rule
// is that if a command doesn't change the git state (e.g. read commands like `git diff`)
// then we don't want to log it. If we are changing something (e.g. `git add .`) then
// we do. The only exception is if we're running a command in the background periodically
// like `git fetch`, which technically does mutate stuff but isn't something we need
// to notify the user about.
DontLog() ICmdObj
// This returns false if DontLog() was called
ShouldLog() bool
// when you call this, then call Run(), we'll stream the output to the cmdWriter (i.e. the command log panel)
StreamOutput() ICmdObj
// returns true if StreamOutput() was called
ShouldStreamOutput() bool
// if you call this before ShouldStreamOutput we'll consider an error with no
// stderr content as a non-error. Not yet supported for Run or RunWithOutput (
// but adding support is trivial)
IgnoreEmptyError() ICmdObj
// returns true if IgnoreEmptyError() was called
ShouldIgnoreEmptyError() bool
PromptOnCredentialRequest(task gocui.Task) ICmdObj
FailOnCredentialRequest() ICmdObj
WithMutex(mutex *deadlock.Mutex) ICmdObj
Mutex() *deadlock.Mutex
GetCredentialStrategy() CredentialStrategy
GetTask() gocui.Task
Clone() ICmdObj
}
type CmdObj struct {
cmd *exec.Cmd
@@ -83,6 +22,9 @@ type CmdObj struct {
// see StreamOutput()
streamOutput bool
// see UsePty()
usePty bool
// see IgnoreEmptyError()
ignoreEmptyError bool
@@ -109,12 +51,13 @@ const (
FAIL
)
var _ ICmdObj = &CmdObj{}
func (self *CmdObj) GetCmd() *exec.Cmd {
return self.cmd
}
// outputs string representation of command. Note that if the command was built
// using NewFromArgs, the output won't be quite the same as what you would type
// into a terminal e.g. 'sh -c git commit' as opposed to 'sh -c "git commit"'
func (self *CmdObj) ToString() string {
// if a given arg contains a space, we need to wrap it in quotes
quotedArgs := lo.Map(self.cmd.Args, func(arg string, _ int) string {
@@ -127,11 +70,19 @@ func (self *CmdObj) ToString() string {
return strings.Join(quotedArgs, " ")
}
// outputs args vector e.g. ["git", "commit", "-m", "my message"]
func (self *CmdObj) Args() []string {
return self.cmd.Args
}
func (self *CmdObj) AddEnvVars(vars ...string) ICmdObj {
// Set a string to be used as stdin for the command.
func (self *CmdObj) SetStdin(input string) *CmdObj {
self.cmd.Stdin = strings.NewReader(input)
return self
}
func (self *CmdObj) AddEnvVars(vars ...string) *CmdObj {
self.cmd.Env = append(self.cmd.Env, vars...)
return self
@@ -141,76 +92,111 @@ func (self *CmdObj) GetEnvVars() []string {
return self.cmd.Env
}
func (self *CmdObj) SetWd(wd string) ICmdObj {
// sets the working directory
func (self *CmdObj) SetWd(wd string) *CmdObj {
self.cmd.Dir = wd
return self
}
func (self *CmdObj) DontLog() ICmdObj {
// By calling DontLog(), we're saying that once we call Run(), we don't want to
// log the command in the UI (it'll still be logged in the log file). The general rule
// is that if a command doesn't change the git state (e.g. read commands like `git diff`)
// then we don't want to log it. If we are changing something (e.g. `git add .`) then
// we do. The only exception is if we're running a command in the background periodically
// like `git fetch`, which technically does mutate stuff but isn't something we need
// to notify the user about.
func (self *CmdObj) DontLog() *CmdObj {
self.dontLog = true
return self
}
// This returns false if DontLog() was called
func (self *CmdObj) ShouldLog() bool {
return !self.dontLog
}
func (self *CmdObj) StreamOutput() ICmdObj {
// when you call this, then call Run(), we'll stream the output to the cmdWriter (i.e. the command log panel)
func (self *CmdObj) StreamOutput() *CmdObj {
self.streamOutput = true
return self
}
// returns true if StreamOutput() was called
func (self *CmdObj) ShouldStreamOutput() bool {
return self.streamOutput
}
func (self *CmdObj) IgnoreEmptyError() ICmdObj {
// when you call this, then call Run(), we'll use a PTY to run the command. Only
// has an effect if StreamOutput() was also called. Ignored on Windows.
func (self *CmdObj) UsePty() *CmdObj {
self.usePty = true
return self
}
// returns true if UsePty() was called
func (self *CmdObj) ShouldUsePty() bool {
return self.usePty
}
// if you call this before ShouldStreamOutput we'll consider an error with no
// stderr content as a non-error. Not yet supported for Run or RunWithOutput (
// but adding support is trivial)
func (self *CmdObj) IgnoreEmptyError() *CmdObj {
self.ignoreEmptyError = true
return self
}
// returns true if IgnoreEmptyError() was called
func (self *CmdObj) ShouldIgnoreEmptyError() bool {
return self.ignoreEmptyError
}
func (self *CmdObj) Mutex() *deadlock.Mutex {
return self.mutex
}
func (self *CmdObj) WithMutex(mutex *deadlock.Mutex) ICmdObj {
func (self *CmdObj) WithMutex(mutex *deadlock.Mutex) *CmdObj {
self.mutex = mutex
return self
}
func (self *CmdObj) ShouldIgnoreEmptyError() bool {
return self.ignoreEmptyError
}
// runs the command and returns an error if any
func (self *CmdObj) Run() error {
return self.runner.Run(self)
}
// runs the command and returns the output as a string, and an error if any
func (self *CmdObj) RunWithOutput() (string, error) {
return self.runner.RunWithOutput(self)
}
// runs the command and returns stdout and stderr as a string, and an error if any
func (self *CmdObj) RunWithOutputs() (string, string, error) {
return self.runner.RunWithOutputs(self)
}
// runs the command and runs a callback function on each line of the output. If the callback
// returns true for the boolean value, we kill the process and return.
func (self *CmdObj) RunAndProcessLines(onLine func(line string) (bool, error)) error {
return self.runner.RunAndProcessLines(self, onLine)
}
func (self *CmdObj) PromptOnCredentialRequest(task gocui.Task) ICmdObj {
func (self *CmdObj) PromptOnCredentialRequest(task gocui.Task) *CmdObj {
self.credentialStrategy = PROMPT
self.usePty = true
self.task = task
return self
}
func (self *CmdObj) FailOnCredentialRequest() ICmdObj {
func (self *CmdObj) FailOnCredentialRequest() *CmdObj {
self.credentialStrategy = FAIL
self.usePty = true
return self
}
@@ -223,7 +209,7 @@ func (self *CmdObj) GetTask() gocui.Task {
return self.task
}
func (self *CmdObj) Clone() ICmdObj {
func (self *CmdObj) Clone() *CmdObj {
clone := &CmdObj{}
*clone = *self
clone.cmd = cloneCmd(self.cmd)
+10 -14
View File
@@ -11,11 +11,11 @@ import (
type ICmdObjBuilder interface {
// NewFromArgs takes a slice of strings like []string{"git", "commit"} and returns a new command object.
New(args []string) ICmdObj
New(args []string) *CmdObj
// NewShell takes a string like `git commit` and returns an executable shell command for it e.g. `sh -c 'git commit'`
NewShell(commandStr string) ICmdObj
// Like NewShell, but uses the user's shell rather than "bash", and passes -i to it
NewInteractiveShell(commandStr string) ICmdObj
// shellFunctionsFile is an optional file path that will be sourced before executing the command. Callers should pass
// the value of UserConfig.OS.ShellFunctionsFile.
NewShell(commandStr string, shellFunctionsFile string) *CmdObj
// Quote wraps a string in quotes with any necessary escaping applied. The reason for bundling this up with the other methods in this interface is that we basically always need to make use of this when creating new command objects.
Quote(str string) string
}
@@ -28,13 +28,13 @@ type CmdObjBuilder struct {
// poor man's version of explicitly saying that struct X implements interface Y
var _ ICmdObjBuilder = &CmdObjBuilder{}
func (self *CmdObjBuilder) New(args []string) ICmdObj {
func (self *CmdObjBuilder) New(args []string) *CmdObj {
cmdObj := self.NewWithEnviron(args, os.Environ())
return cmdObj
}
// A command with explicit environment from env
func (self *CmdObjBuilder) NewWithEnviron(args []string, env []string) ICmdObj {
func (self *CmdObjBuilder) NewWithEnviron(args []string, env []string) *CmdObj {
cmd := exec.Command(args[0], args[1:]...)
cmd.Env = env
@@ -44,20 +44,16 @@ func (self *CmdObjBuilder) NewWithEnviron(args []string, env []string) ICmdObj {
}
}
func (self *CmdObjBuilder) NewShell(commandStr string) ICmdObj {
func (self *CmdObjBuilder) NewShell(commandStr string, shellFunctionsFile string) *CmdObj {
if len(shellFunctionsFile) > 0 {
commandStr = fmt.Sprintf("%ssource %s\n%s", self.platform.PrefixForShellFunctionsFile, shellFunctionsFile, commandStr)
}
quotedCommand := self.quotedCommandString(commandStr)
cmdArgs := str.ToArgv(fmt.Sprintf("%s %s %s", self.platform.Shell, self.platform.ShellArg, quotedCommand))
return self.New(cmdArgs)
}
func (self *CmdObjBuilder) NewInteractiveShell(commandStr string) ICmdObj {
quotedCommand := self.quotedCommandString(commandStr + self.platform.InteractiveShellExit)
cmdArgs := str.ToArgv(fmt.Sprintf("%s %s %s %s", self.platform.InteractiveShell, self.platform.InteractiveShellArg, self.platform.ShellArg, quotedCommand))
return self.New(cmdArgs)
}
func (self *CmdObjBuilder) quotedCommandString(commandStr string) string {
// Windows does not seem to like quotes around the command
if self.platform.OS == "windows" {
+60 -17
View File
@@ -4,6 +4,7 @@ import (
"bufio"
"bytes"
"io"
"os/exec"
"regexp"
"strings"
"time"
@@ -11,14 +12,15 @@ import (
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sasha-s/go-deadlock"
"github.com/sirupsen/logrus"
)
type ICmdObjRunner interface {
Run(cmdObj ICmdObj) error
RunWithOutput(cmdObj ICmdObj) (string, error)
RunWithOutputs(cmdObj ICmdObj) (string, string, error)
RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error
Run(cmdObj *CmdObj) error
RunWithOutput(cmdObj *CmdObj) (string, error)
RunWithOutputs(cmdObj *CmdObj) (string, string, error)
RunAndProcessLines(cmdObj *CmdObj, onLine func(line string) (bool, error)) error
}
type cmdObjRunner struct {
@@ -28,7 +30,7 @@ type cmdObjRunner struct {
var _ ICmdObjRunner = &cmdObjRunner{}
func (self *cmdObjRunner) Run(cmdObj ICmdObj) error {
func (self *cmdObjRunner) Run(cmdObj *CmdObj) error {
if cmdObj.Mutex() != nil {
cmdObj.Mutex().Lock()
defer cmdObj.Mutex().Unlock()
@@ -46,7 +48,7 @@ func (self *cmdObjRunner) Run(cmdObj ICmdObj) error {
return err
}
func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
func (self *cmdObjRunner) RunWithOutput(cmdObj *CmdObj) (string, error) {
if cmdObj.Mutex() != nil {
cmdObj.Mutex().Lock()
defer cmdObj.Mutex().Unlock()
@@ -71,7 +73,7 @@ func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
return self.RunWithOutputAux(cmdObj)
}
func (self *cmdObjRunner) RunWithOutputs(cmdObj ICmdObj) (string, string, error) {
func (self *cmdObjRunner) RunWithOutputs(cmdObj *CmdObj) (string, string, error) {
if cmdObj.Mutex() != nil {
cmdObj.Mutex().Lock()
defer cmdObj.Mutex().Unlock()
@@ -96,7 +98,7 @@ func (self *cmdObjRunner) RunWithOutputs(cmdObj ICmdObj) (string, string, error)
return self.RunWithOutputsAux(cmdObj)
}
func (self *cmdObjRunner) RunWithOutputAux(cmdObj ICmdObj) (string, error) {
func (self *cmdObjRunner) RunWithOutputAux(cmdObj *CmdObj) (string, error) {
self.log.WithField("command", cmdObj.ToString()).Debug("RunCommand")
if cmdObj.ShouldLog() {
@@ -114,7 +116,7 @@ func (self *cmdObjRunner) RunWithOutputAux(cmdObj ICmdObj) (string, error) {
return output, err
}
func (self *cmdObjRunner) RunWithOutputsAux(cmdObj ICmdObj) (string, string, error) {
func (self *cmdObjRunner) RunWithOutputsAux(cmdObj *CmdObj) (string, string, error) {
self.log.WithField("command", cmdObj.ToString()).Debug("RunCommand")
if cmdObj.ShouldLog() {
@@ -139,7 +141,7 @@ func (self *cmdObjRunner) RunWithOutputsAux(cmdObj ICmdObj) (string, string, err
return stdout, stderr, err
}
func (self *cmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error {
func (self *cmdObjRunner) RunAndProcessLines(cmdObj *CmdObj, onLine func(line string) (bool, error)) error {
if cmdObj.Mutex() != nil {
cmdObj.Mutex().Lock()
defer cmdObj.Mutex().Unlock()
@@ -190,7 +192,7 @@ func (self *cmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line st
return nil
}
func (self *cmdObjRunner) logCmdObj(cmdObj ICmdObj) {
func (self *cmdObjRunner) logCmdObj(cmdObj *CmdObj) {
self.guiIO.logCommandFn(cmdObj.ToString(), true)
}
@@ -213,7 +215,7 @@ type cmdHandler struct {
close func() error
}
func (self *cmdObjRunner) runAndStream(cmdObj ICmdObj) error {
func (self *cmdObjRunner) runAndStream(cmdObj *CmdObj) error {
return self.runAndStreamAux(cmdObj, func(handler *cmdHandler, cmdWriter io.Writer) {
go func() {
_, _ = io.Copy(cmdWriter, handler.stdoutPipe)
@@ -222,7 +224,7 @@ func (self *cmdObjRunner) runAndStream(cmdObj ICmdObj) error {
}
func (self *cmdObjRunner) runAndStreamAux(
cmdObj ICmdObj,
cmdObj *CmdObj,
onRun func(*cmdHandler, io.Writer),
) error {
cmdWriter := self.guiIO.newCmdWriterFn()
@@ -236,7 +238,13 @@ func (self *cmdObjRunner) runAndStreamAux(
var stderr bytes.Buffer
cmd.Stderr = io.MultiWriter(cmdWriter, &stderr)
handler, err := self.getCmdHandler(cmd)
var handler *cmdHandler
var err error
if cmdObj.ShouldUsePty() {
handler, err = self.getCmdHandlerPty(cmd)
} else {
handler, err = self.getCmdHandlerNonPty(cmd)
}
if err != nil {
return err
}
@@ -297,7 +305,7 @@ var failPromptFn = func(CredentialType) <-chan string {
return ch
}
func (self *cmdObjRunner) runWithCredentialHandling(cmdObj ICmdObj) error {
func (self *cmdObjRunner) runWithCredentialHandling(cmdObj *CmdObj) error {
promptFn, err := self.getCredentialPromptFn(cmdObj)
if err != nil {
return err
@@ -306,7 +314,7 @@ func (self *cmdObjRunner) runWithCredentialHandling(cmdObj ICmdObj) error {
return self.runAndDetectCredentialRequest(cmdObj, promptFn)
}
func (self *cmdObjRunner) getCredentialPromptFn(cmdObj ICmdObj) (func(CredentialType) <-chan string, error) {
func (self *cmdObjRunner) getCredentialPromptFn(cmdObj *CmdObj) (func(CredentialType) <-chan string, error) {
switch cmdObj.GetCredentialStrategy() {
case PROMPT:
return self.guiIO.promptForCredentialFn, nil
@@ -322,7 +330,7 @@ func (self *cmdObjRunner) getCredentialPromptFn(cmdObj ICmdObj) (func(Credential
// promptUserForCredential is a function that gets executed when this function detect you need to fill in a password or passphrase
// The promptUserForCredential argument will be "username", "password" or "passphrase" and expects the user's password/passphrase or username back
func (self *cmdObjRunner) runAndDetectCredentialRequest(
cmdObj ICmdObj,
cmdObj *CmdObj,
promptUserForCredential func(CredentialType) <-chan string,
) error {
// setting the output to english so we can parse it for a username/password request
@@ -410,3 +418,38 @@ func (self *cmdObjRunner) getCheckForCredentialRequestFunc() func([]byte) (Crede
return 0, false
}
}
type Buffer struct {
b bytes.Buffer
m deadlock.Mutex
}
func (b *Buffer) Read(p []byte) (n int, err error) {
b.m.Lock()
defer b.m.Unlock()
return b.b.Read(p)
}
func (b *Buffer) Write(p []byte) (n int, err error) {
b.m.Lock()
defer b.m.Unlock()
return b.b.Write(p)
}
func (self *cmdObjRunner) getCmdHandlerNonPty(cmd *exec.Cmd) (*cmdHandler, error) {
stdoutReader, stdoutWriter := io.Pipe()
cmd.Stdout = stdoutWriter
buf := &Buffer{}
cmd.Stdin = buf
if err := cmd.Start(); err != nil {
return nil, err
}
return &cmdHandler{
stdoutPipe: stdoutReader,
stdinPipe: buf,
close: func() error { return nil },
}, nil
}
@@ -11,7 +11,7 @@ import (
// we define this separately for windows and non-windows given that windows does
// not have great PTY support and we need a PTY to handle a credential request
func (self *cmdObjRunner) getCmdHandler(cmd *exec.Cmd) (*cmdHandler, error) {
func (self *cmdObjRunner) getCmdHandlerPty(cmd *exec.Cmd) (*cmdHandler, error) {
ptmx, err := pty.Start(cmd)
if err != nil {
return nil, err
+3 -41
View File
@@ -4,48 +4,10 @@
package oscommands
import (
"bytes"
"io"
"os/exec"
"github.com/sasha-s/go-deadlock"
)
type Buffer struct {
b bytes.Buffer
m deadlock.Mutex
}
func (b *Buffer) Read(p []byte) (n int, err error) {
b.m.Lock()
defer b.m.Unlock()
return b.b.Read(p)
}
func (b *Buffer) Write(p []byte) (n int, err error) {
b.m.Lock()
defer b.m.Unlock()
return b.b.Write(p)
}
// TODO: Remove this hack and replace it with a proper way to run commands live on windows. We still have an issue where if a password is requested, the request for a password is written straight to stdout because we can't control the stdout of a subprocess of a subprocess. Keep an eye on https://github.com/creack/pty/pull/109
func (self *cmdObjRunner) getCmdHandler(cmd *exec.Cmd) (*cmdHandler, error) {
stdoutReader, stdoutWriter := io.Pipe()
cmd.Stdout = stdoutWriter
buf := &Buffer{}
cmd.Stdin = buf
if err := cmd.Start(); err != nil {
return nil, err
}
// because we don't yet have windows support for a pty, we instead just
// pass our standard stream handlers and because there's no pty to close
// we pass a no-op function for that.
return &cmdHandler{
stdoutPipe: stdoutReader,
stdinPipe: buf,
close: func() error { return nil },
}, nil
func (self *cmdObjRunner) getCmdHandlerPty(cmd *exec.Cmd) (*cmdHandler, error) {
// We don't have PTY support on Windows yet, so we just return a non-PTY handler.
return self.getCmdHandlerNonPty(cmd)
}
+11 -14
View File
@@ -8,7 +8,7 @@ import (
// NewDummyOSCommand creates a new dummy OSCommand for testing
func NewDummyOSCommand() *OSCommand {
osCmd := NewOSCommand(utils.NewDummyCommon(), config.NewDummyAppConfig(), dummyPlatform, NewNullGuiIO(utils.NewDummyLog()))
osCmd := NewOSCommand(common.NewDummyCommon(), config.NewDummyAppConfig(), dummyPlatform, NewNullGuiIO(utils.NewDummyLog()))
return osCmd
}
@@ -23,9 +23,9 @@ type OSCommandDeps struct {
}
func NewDummyOSCommandWithDeps(deps OSCommandDeps) *OSCommand {
common := deps.Common
if common == nil {
common = utils.NewDummyCommon()
cmn := deps.Common
if cmn == nil {
cmn = common.NewDummyCommon()
}
platform := deps.Platform
@@ -34,7 +34,7 @@ func NewDummyOSCommandWithDeps(deps OSCommandDeps) *OSCommand {
}
return &OSCommand{
Common: common,
Common: cmn,
Platform: platform,
getenvFn: deps.GetenvFn,
removeFileFn: deps.RemoveFileFn,
@@ -51,18 +51,15 @@ func NewDummyCmdObjBuilder(runner ICmdObjRunner) *CmdObjBuilder {
}
var dummyPlatform = &Platform{
OS: "darwin",
Shell: "bash",
InteractiveShell: "bash",
ShellArg: "-c",
InteractiveShellArg: "-i",
InteractiveShellExit: "; exit $?",
OpenCommand: "open {{filename}}",
OpenLinkCommand: "open {{link}}",
OS: "darwin",
Shell: "bash",
ShellArg: "-c",
OpenCommand: "open {{filename}}",
OpenLinkCommand: "open {{link}}",
}
func NewDummyOSCommandWithRunner(runner *FakeCmdObjRunner) *OSCommand {
osCommand := NewOSCommand(utils.NewDummyCommon(), config.NewDummyAppConfig(), dummyPlatform, NewNullGuiIO(utils.NewDummyLog()))
osCommand := NewOSCommand(common.NewDummyCommon(), config.NewDummyAppConfig(), dummyPlatform, NewNullGuiIO(utils.NewDummyLog()))
osCommand.Cmd = NewDummyCmdObjBuilder(runner)
return osCommand
@@ -28,7 +28,7 @@ type FakeCmdObjRunner struct {
type CmdObjMatcher struct {
description string
// returns true if the matcher matches the command object
test func(ICmdObj) bool
test func(*CmdObj) bool
// output of the command
output string
@@ -48,12 +48,12 @@ func (self *FakeCmdObjRunner) remainingExpectedCmds() []CmdObjMatcher {
})
}
func (self *FakeCmdObjRunner) Run(cmdObj ICmdObj) error {
func (self *FakeCmdObjRunner) Run(cmdObj *CmdObj) error {
_, err := self.RunWithOutput(cmdObj)
return err
}
func (self *FakeCmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
func (self *FakeCmdObjRunner) RunWithOutput(cmdObj *CmdObj) (string, error) {
self.mutex.Lock()
defer self.mutex.Unlock()
@@ -78,12 +78,12 @@ func (self *FakeCmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
return "", nil
}
func (self *FakeCmdObjRunner) RunWithOutputs(cmdObj ICmdObj) (string, string, error) {
func (self *FakeCmdObjRunner) RunWithOutputs(cmdObj *CmdObj) (string, string, error) {
output, err := self.RunWithOutput(cmdObj)
return output, "", err
}
func (self *FakeCmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error {
func (self *FakeCmdObjRunner) RunAndProcessLines(cmdObj *CmdObj, onLine func(line string) (bool, error)) error {
output, err := self.RunWithOutput(cmdObj)
if err != nil {
return err
@@ -105,7 +105,7 @@ func (self *FakeCmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(lin
return nil
}
func (self *FakeCmdObjRunner) ExpectFunc(description string, fn func(cmdObj ICmdObj) bool, output string, err error) *FakeCmdObjRunner {
func (self *FakeCmdObjRunner) ExpectFunc(description string, fn func(cmdObj *CmdObj) bool, output string, err error) *FakeCmdObjRunner {
self.mutex.Lock()
defer self.mutex.Unlock()
@@ -121,7 +121,7 @@ func (self *FakeCmdObjRunner) ExpectFunc(description string, fn func(cmdObj ICmd
func (self *FakeCmdObjRunner) ExpectArgs(expectedArgs []string, output string, err error) *FakeCmdObjRunner {
description := fmt.Sprintf("matches args %s", strings.Join(expectedArgs, " "))
self.ExpectFunc(description, func(cmdObj ICmdObj) bool {
self.ExpectFunc(description, func(cmdObj *CmdObj) bool {
return slices.Equal(expectedArgs, cmdObj.GetCmd().Args)
}, output, err)
@@ -130,7 +130,7 @@ func (self *FakeCmdObjRunner) ExpectArgs(expectedArgs []string, output string, e
func (self *FakeCmdObjRunner) ExpectGitArgs(expectedArgs []string, output string, err error) *FakeCmdObjRunner {
description := fmt.Sprintf("matches git args %s", strings.Join(expectedArgs, " "))
self.ExpectFunc(description, func(cmdObj ICmdObj) bool {
self.ExpectFunc(description, func(cmdObj *CmdObj) bool {
return slices.Equal(expectedArgs, cmdObj.GetCmd().Args[1:])
}, output, err)
+14 -16
View File
@@ -35,14 +35,12 @@ type OSCommand struct {
// Platform stores the os state
type Platform struct {
OS string
Shell string
InteractiveShell string
ShellArg string
InteractiveShellArg string
InteractiveShellExit string
OpenCommand string
OpenLinkCommand string
OS string
Shell string
ShellArg string
PrefixForShellFunctionsFile string
OpenCommand string
OpenLinkCommand string
}
// NewOSCommand os command runner
@@ -93,7 +91,7 @@ func (c *OSCommand) OpenFile(filename string) error {
"filename": c.Quote(filename),
}
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
return c.Cmd.NewShell(command).Run()
return c.Cmd.NewShell(command, c.UserConfig().OS.ShellFunctionsFile).Run()
}
func (c *OSCommand) OpenLink(link string) error {
@@ -110,7 +108,7 @@ func (c *OSCommand) OpenLink(link string) error {
}
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
return c.Cmd.NewShell(command).Run()
return c.Cmd.NewShell(command, c.UserConfig().OS.ShellFunctionsFile).Run()
}
// Quote wraps a message in platform-specific quotation marks
@@ -210,13 +208,13 @@ func (c *OSCommand) FileExists(path string) (bool, error) {
}
// PipeCommands runs a heap of commands and pipes their inputs/outputs together like A | B | C
func (c *OSCommand) PipeCommands(cmdObjs ...ICmdObj) error {
cmds := lo.Map(cmdObjs, func(cmdObj ICmdObj, _ int) *exec.Cmd {
func (c *OSCommand) PipeCommands(cmdObjs ...*CmdObj) error {
cmds := lo.Map(cmdObjs, func(cmdObj *CmdObj, _ int) *exec.Cmd {
return cmdObj.GetCmd()
})
logCmdStr := strings.Join(
lo.Map(cmdObjs, func(cmdObj ICmdObj, _ int) string {
lo.Map(cmdObjs, func(cmdObj *CmdObj, _ int) string {
return cmdObj.ToString()
}),
" | ",
@@ -299,7 +297,7 @@ func (c *OSCommand) CopyToClipboard(str string) error {
cmdStr := utils.ResolvePlaceholderString(c.UserConfig().OS.CopyToClipboardCmd, map[string]string{
"text": c.Cmd.Quote(str),
})
return c.Cmd.NewShell(cmdStr).Run()
return c.Cmd.NewShell(cmdStr, c.UserConfig().OS.ShellFunctionsFile).Run()
}
return clipboard.WriteAll(str)
@@ -310,7 +308,7 @@ func (c *OSCommand) PasteFromClipboard() (string, error) {
var err error
if c.UserConfig().OS.CopyToClipboardCmd != "" {
cmdStr := c.UserConfig().OS.ReadFromClipboardCmd
s, err = c.Cmd.NewShell(cmdStr).RunWithOutput()
s, err = c.Cmd.NewShell(cmdStr, c.UserConfig().OS.ShellFunctionsFile).RunWithOutput()
} else {
s, err = clipboard.ReadAll()
}
@@ -360,5 +358,5 @@ func (c *OSCommand) UpdateWindowTitle() error {
return getWdErr
}
argString := fmt.Sprint("title ", filepath.Base(path), " - Lazygit")
return c.Cmd.NewShell(argString).Run()
return c.Cmd.NewShell(argString, c.UserConfig().OS.ShellFunctionsFile).Run()
}
+9 -16
View File
@@ -12,25 +12,18 @@ import (
func GetPlatform() *Platform {
shell := getUserShell()
interactiveShell := shell
interactiveShellArg := "-i"
interactiveShellExit := "; exit $?"
if !(strings.HasSuffix(shell, "bash") || strings.HasSuffix(shell, "zsh")) {
interactiveShell = "bash"
interactiveShellArg = ""
interactiveShellExit = ""
prefixForShellFunctionsFile := ""
if strings.HasSuffix(shell, "bash") {
prefixForShellFunctionsFile = "shopt -s expand_aliases\n"
}
return &Platform{
OS: runtime.GOOS,
Shell: "bash",
InteractiveShell: interactiveShell,
ShellArg: "-c",
InteractiveShellArg: interactiveShellArg,
InteractiveShellExit: interactiveShellExit,
OpenCommand: "open {{filename}}",
OpenLinkCommand: "open {{link}}",
OS: runtime.GOOS,
Shell: shell,
ShellArg: "-c",
PrefixForShellFunctionsFile: prefixForShellFunctionsFile,
OpenCommand: "open {{filename}}",
OpenLinkCommand: "open {{link}}",
}
}
+3 -6
View File
@@ -2,11 +2,8 @@ package oscommands
func GetPlatform() *Platform {
return &Platform{
OS: "windows",
Shell: "cmd",
InteractiveShell: "cmd",
ShellArg: "/c",
InteractiveShellArg: "",
InteractiveShellExit: "",
OS: "windows",
Shell: "cmd",
ShellArg: "/c",
}
}
+3 -4
View File
@@ -1,7 +1,6 @@
package patch
import (
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
@@ -54,7 +53,7 @@ func (self *Patch) Lines() []*PatchLine {
// Returns the patch line index of the first line in the given hunk
func (self *Patch) HunkStartIdx(hunkIndex int) int {
hunkIndex = utils.Clamp(hunkIndex, 0, len(self.hunks)-1)
hunkIndex = lo.Clamp(hunkIndex, 0, len(self.hunks)-1)
result := len(self.header)
for i := 0; i < hunkIndex; i++ {
@@ -65,7 +64,7 @@ func (self *Patch) HunkStartIdx(hunkIndex int) int {
// Returns the patch line index of the last line in the given hunk
func (self *Patch) HunkEndIdx(hunkIndex int) int {
hunkIndex = utils.Clamp(hunkIndex, 0, len(self.hunks)-1)
hunkIndex = lo.Clamp(hunkIndex, 0, len(self.hunks)-1)
return self.HunkStartIdx(hunkIndex) + self.hunks[hunkIndex].lineCount() - 1
}
@@ -118,7 +117,7 @@ func (self *Patch) HunkContainingLine(idx int) int {
// Returns the patch line index of the next change (i.e. addition or deletion).
func (self *Patch) GetNextChangeIdx(idx int) int {
idx = utils.Clamp(idx, 0, self.LineCount()-1)
idx = lo.Clamp(idx, 0, self.LineCount()-1)
lines := self.Lines()
-22
View File
@@ -1,22 +0,0 @@
package enums
type RebaseMode int
const (
// this means we're neither rebasing nor merging
REBASE_MODE_NONE RebaseMode = iota
// this means normal rebase as opposed to interactive rebase
REBASE_MODE_NORMAL
REBASE_MODE_INTERACTIVE
// REBASE_MODE_REBASING is a general state that captures both REBASE_MODE_NORMAL and REBASE_MODE_INTERACTIVE
REBASE_MODE_REBASING
REBASE_MODE_MERGING
)
func (self RebaseMode) IsMerging() bool {
return self == REBASE_MODE_MERGING
}
func (self RebaseMode) IsRebasing() bool {
return self == REBASE_MODE_INTERACTIVE || self == REBASE_MODE_NORMAL || self == REBASE_MODE_REBASING
}
+33
View File
@@ -0,0 +1,33 @@
package common
import (
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/spf13/afero"
)
func NewDummyCommon() *Common {
tr := i18n.EnglishTranslationSet()
cmn := &Common{
Log: utils.NewDummyLog(),
Tr: tr,
Fs: afero.NewOsFs(),
}
cmn.SetUserConfig(config.GetDefaultConfig())
return cmn
}
func NewDummyCommonWithUserConfigAndAppState(userConfig *config.UserConfig, appState *config.AppState) *Common {
tr := i18n.EnglishTranslationSet()
cmn := &Common{
Log: utils.NewDummyLog(),
Tr: tr,
AppState: appState,
// TODO: remove dependency on actual filesystem in tests and switch to using
// in-memory for everything
Fs: afero.NewOsFs(),
}
cmn.SetUserConfig(userConfig)
return cmn
}
+196 -43
View File
@@ -1,14 +1,18 @@
package config
import (
"errors"
"fmt"
"log"
"os"
"path/filepath"
"reflect"
"strings"
"time"
"github.com/adrg/xdg"
"github.com/jesseduffield/generics/orderedset"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/jesseduffield/lazygit/pkg/utils/yaml_utils"
"github.com/samber/lo"
"gopkg.in/yaml.v3"
@@ -93,7 +97,7 @@ func NewAppConfig(
configFiles = []*ConfigFile{configFile}
}
userConfig, err := loadUserConfigWithDefaults(configFiles)
userConfig, err := loadUserConfigWithDefaults(configFiles, false)
if err != nil {
return nil, err
}
@@ -142,11 +146,11 @@ func findOrCreateConfigDir() (string, error) {
return folder, os.MkdirAll(folder, 0o755)
}
func loadUserConfigWithDefaults(configFiles []*ConfigFile) (*UserConfig, error) {
return loadUserConfig(configFiles, GetDefaultConfig())
func loadUserConfigWithDefaults(configFiles []*ConfigFile, isGuiInitialized bool) (*UserConfig, error) {
return loadUserConfig(configFiles, GetDefaultConfig(), isGuiInitialized)
}
func loadUserConfig(configFiles []*ConfigFile, base *UserConfig) (*UserConfig, error) {
func loadUserConfig(configFiles []*ConfigFile, base *UserConfig, isGuiInitialized bool) (*UserConfig, error) {
for _, configFile := range configFiles {
path := configFile.Path
statInfo, err := os.Stat(path)
@@ -191,7 +195,7 @@ func loadUserConfig(configFiles []*ConfigFile, base *UserConfig) (*UserConfig, e
return nil, err
}
content, err = migrateUserConfig(path, content)
content, err = migrateUserConfig(path, content, isGuiInitialized)
if err != nil {
return nil, err
}
@@ -212,32 +216,65 @@ func loadUserConfig(configFiles []*ConfigFile, base *UserConfig) (*UserConfig, e
return base, nil
}
type ChangesSet = orderedset.OrderedSet[string]
func NewChangesSet() *ChangesSet {
return orderedset.New[string]()
}
// Do any backward-compatibility migrations of things that have changed in the
// config over time; examples are renaming a key to a better name, moving a key
// from one container to another, or changing the type of a key (e.g. from bool
// to an enum).
func migrateUserConfig(path string, content []byte) ([]byte, error) {
changedContent, err := computeMigratedConfig(path, content)
func migrateUserConfig(path string, content []byte, isGuiInitialized bool) ([]byte, error) {
changes := NewChangesSet()
changedContent, didChange, err := computeMigratedConfig(path, content, changes)
if err != nil {
return nil, err
}
// Write config back if changed
if string(changedContent) != string(content) {
fmt.Println("Provided user config is deprecated but auto-fixable. Attempting to write fixed version back to file...")
if err := os.WriteFile(path, changedContent, 0o644); err != nil {
return nil, fmt.Errorf("While attempting to write back fixed user config to %s, an error occurred: %s", path, err)
}
fmt.Printf("Success. New config written to %s\n", path)
return changedContent, nil
// Nothing to do if config didn't change
if !didChange {
return content, nil
}
return content, nil
changesText := "The following changes were made:\n\n"
changesText += strings.Join(lo.Map(changes.ToSliceFromOldest(), func(change string, _ int) string {
return fmt.Sprintf("- %s\n", change)
}), "")
// Write config back
if !isGuiInitialized {
fmt.Printf("The user config file %s must be migrated. Attempting to do this automatically.\n", path)
fmt.Println(changesText)
}
if err := os.WriteFile(path, changedContent, 0o644); err != nil {
errorMsg := fmt.Sprintf("While attempting to write back migrated user config to %s, an error occurred: %s", path, err)
if isGuiInitialized {
errorMsg += "\n\n" + changesText
}
return nil, errors.New(errorMsg)
}
if !isGuiInitialized {
fmt.Printf("Config file saved successfully to %s\n", path)
}
return changedContent, nil
}
// A pure function helper for testing purposes
func computeMigratedConfig(path string, content []byte) ([]byte, error) {
changedContent := content
func computeMigratedConfig(path string, content []byte, changes *ChangesSet) ([]byte, bool, error) {
var err error
var rootNode yaml.Node
err = yaml.Unmarshal(content, &rootNode)
if err != nil {
return nil, false, fmt.Errorf("failed to parse YAML: %w", err)
}
var originalCopy yaml.Node
err = yaml.Unmarshal(content, &originalCopy)
if err != nil {
return nil, false, fmt.Errorf("failed to parse YAML, but only the second time!?!? How did that happen: %w", err)
}
pathsToReplace := []struct {
oldPath []string
@@ -248,46 +285,66 @@ func computeMigratedConfig(path string, content []byte) ([]byte, error) {
{[]string{"gui", "windowSize"}, "screenMode"},
}
var err error
for _, pathToReplace := range pathsToReplace {
changedContent, err = yaml_utils.RenameYamlKey(changedContent, pathToReplace.oldPath, pathToReplace.newName)
err, didReplace := yaml_utils.RenameYamlKey(&rootNode, pathToReplace.oldPath, pathToReplace.newName)
if err != nil {
return nil, fmt.Errorf("Couldn't migrate config file at `%s` for key %s: %s", path, strings.Join(pathToReplace.oldPath, "."), err)
return nil, false, fmt.Errorf("Couldn't migrate config file at `%s` for key %s: %s", path, strings.Join(pathToReplace.oldPath, "."), err)
}
if didReplace {
changes.Add(fmt.Sprintf("Renamed '%s' to '%s'", strings.Join(pathToReplace.oldPath, "."), pathToReplace.newName))
}
}
changedContent, err = changeNullKeybindingsToDisabled(changedContent)
err = changeNullKeybindingsToDisabled(&rootNode, changes)
if err != nil {
return nil, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
return nil, false, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
}
changedContent, err = changeElementToSequence(changedContent, []string{"git", "commitPrefix"})
err = changeElementToSequence(&rootNode, []string{"git", "commitPrefix"}, changes)
if err != nil {
return nil, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
return nil, false, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
}
changedContent, err = changeCommitPrefixesMap(changedContent)
err = changeCommitPrefixesMap(&rootNode, changes)
if err != nil {
return nil, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
return nil, false, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
}
err = changeCustomCommandStreamAndOutputToOutputEnum(&rootNode, changes)
if err != nil {
return nil, false, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
}
err = migrateAllBranchesLogCmd(&rootNode, changes)
if err != nil {
return nil, false, fmt.Errorf("Couldn't migrate config file at `%s`: %s", path, err)
}
// Add more migrations here...
return changedContent, nil
if reflect.DeepEqual(rootNode, originalCopy) {
return nil, false, nil
}
newContent, err := yaml_utils.YamlMarshal(&rootNode)
if err != nil {
return nil, false, fmt.Errorf("Failed to remarsal!\n %w", err)
}
return newContent, true, nil
}
func changeNullKeybindingsToDisabled(changedContent []byte) ([]byte, error) {
return yaml_utils.Walk(changedContent, func(node *yaml.Node, path string) bool {
func changeNullKeybindingsToDisabled(rootNode *yaml.Node, changes *ChangesSet) error {
return yaml_utils.Walk(rootNode, func(node *yaml.Node, path string) {
if strings.HasPrefix(path, "keybinding.") && node.Kind == yaml.ScalarNode && node.Tag == "!!null" {
node.Value = "<disabled>"
node.Tag = "!!str"
return true
changes.Add(fmt.Sprintf("Changed 'null' to '<disabled>' for keybinding '%s'", path))
}
return false
})
}
func changeElementToSequence(changedContent []byte, path []string) ([]byte, error) {
return yaml_utils.TransformNode(changedContent, path, func(node *yaml.Node) (bool, error) {
func changeElementToSequence(rootNode *yaml.Node, path []string, changes *ChangesSet) error {
return yaml_utils.TransformNode(rootNode, path, func(node *yaml.Node) error {
if node.Kind == yaml.MappingNode {
nodeContentCopy := node.Content
node.Kind = yaml.SequenceNode
@@ -298,15 +355,16 @@ func changeElementToSequence(changedContent []byte, path []string) ([]byte, erro
Content: nodeContentCopy,
}}
return true, nil
changes.Add(fmt.Sprintf("Changed '%s' to an array of strings", strings.Join(path, ".")))
return nil
}
return false, nil
return nil
})
}
func changeCommitPrefixesMap(changedContent []byte) ([]byte, error) {
return yaml_utils.TransformNode(changedContent, []string{"git", "commitPrefixes"}, func(prefixesNode *yaml.Node) (bool, error) {
changedAnyNodes := false
func changeCommitPrefixesMap(rootNode *yaml.Node, changes *ChangesSet) error {
return yaml_utils.TransformNode(rootNode, []string{"git", "commitPrefixes"}, func(prefixesNode *yaml.Node) error {
if prefixesNode.Kind == yaml.MappingNode {
for _, contentNode := range prefixesNode.Content {
if contentNode.Kind == yaml.MappingNode {
@@ -318,11 +376,106 @@ func changeCommitPrefixesMap(changedContent []byte) ([]byte, error) {
Kind: yaml.MappingNode,
Content: nodeContentCopy,
}}
changedAnyNodes = true
changes.Add("Changed 'git.commitPrefixes' elements to arrays of strings")
}
}
}
return changedAnyNodes, nil
return nil
})
}
func changeCustomCommandStreamAndOutputToOutputEnum(rootNode *yaml.Node, changes *ChangesSet) error {
return yaml_utils.Walk(rootNode, func(node *yaml.Node, path string) {
// We are being lazy here and rely on the fact that the only mapping
// nodes in the tree under customCommands are actual custom commands. If
// this ever changes (e.g. because we add a struct field to
// customCommand), then we need to change this to iterate properly.
if strings.HasPrefix(path, "customCommands[") && node.Kind == yaml.MappingNode {
output := ""
if streamKey, streamValue := yaml_utils.RemoveKey(node, "subprocess"); streamKey != nil {
if streamValue.Kind == yaml.ScalarNode && streamValue.Value == "true" {
output = "terminal"
changes.Add("Changed 'subprocess: true' to 'output: terminal' in custom command")
} else {
changes.Add("Deleted redundant 'subprocess: false' in custom command")
}
}
if streamKey, streamValue := yaml_utils.RemoveKey(node, "stream"); streamKey != nil {
if streamValue.Kind == yaml.ScalarNode && streamValue.Value == "true" && output == "" {
output = "log"
changes.Add("Changed 'stream: true' to 'output: log' in custom command")
} else {
changes.Add(fmt.Sprintf("Deleted redundant 'stream: %v' property in custom command", streamValue.Value))
}
}
if streamKey, streamValue := yaml_utils.RemoveKey(node, "showOutput"); streamKey != nil {
if streamValue.Kind == yaml.ScalarNode && streamValue.Value == "true" && output == "" {
changes.Add("Changed 'showOutput: true' to 'output: popup' in custom command")
output = "popup"
} else {
changes.Add(fmt.Sprintf("Deleted redundant 'showOutput: %v' property in custom command", streamValue.Value))
}
}
if output != "" {
outputKeyNode := &yaml.Node{
Kind: yaml.ScalarNode,
Value: "output",
Tag: "!!str",
}
outputValueNode := &yaml.Node{
Kind: yaml.ScalarNode,
Value: output,
Tag: "!!str",
}
node.Content = append(node.Content, outputKeyNode, outputValueNode)
}
}
})
}
// This migration is special because users have already defined
// a single element at `allBranchesLogCmd` and the sequence at `allBranchesLogCmds`.
// Some users have explicitly set `allBranchesLogCmd` to be an empty string in order
// to remove it, so in that case we just delete the element, and add nothing to the list
func migrateAllBranchesLogCmd(rootNode *yaml.Node, changes *ChangesSet) error {
return yaml_utils.TransformNode(rootNode, []string{"git"}, func(gitNode *yaml.Node) error {
cmdKeyNode, cmdValueNode := yaml_utils.LookupKey(gitNode, "allBranchesLogCmd")
// Nothing to do if they do not have the deprecated item
if cmdKeyNode == nil {
return nil
}
cmdsKeyNode, cmdsValueNode := yaml_utils.LookupKey(gitNode, "allBranchesLogCmds")
var change string
if cmdsKeyNode == nil {
// Create empty sequence node and attach it onto the root git node
// We will later populate it with the individual allBranchesLogCmd record
cmdsKeyNode = &yaml.Node{Kind: yaml.ScalarNode, Value: "allBranchesLogCmds"}
cmdsValueNode = &yaml.Node{Kind: yaml.SequenceNode, Content: []*yaml.Node{}}
gitNode.Content = append(gitNode.Content,
cmdsKeyNode,
cmdsValueNode,
)
change = "Created git.allBranchesLogCmds array containing value of git.allBranchesLogCmd"
} else {
if cmdsValueNode.Kind != yaml.SequenceNode {
return errors.New("You should have an allBranchesLogCmds defined as a sequence!")
}
change = "Prepended git.allBranchesLogCmd value to git.allBranchesLogCmds array"
}
if cmdValueNode.Value != "" {
// Prepending the individual element to make it show up first in the list, which was prior behavior
cmdsValueNode.Content = utils.Prepend(cmdsValueNode.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: cmdValueNode.Value})
changes.Add(change)
}
// Clear out the existing allBranchesLogCmd, now that we have migrated it into the list
_, _ = yaml_utils.RemoveKey(gitNode, "allBranchesLogCmd")
changes.Add("Removed obsolete git.allBranchesLogCmd")
return nil
})
}
@@ -366,7 +519,7 @@ func (c *AppConfig) GetUserConfigDir() string {
func (c *AppConfig) ReloadUserConfigForRepo(repoConfigFiles []*ConfigFile) error {
configFiles := append(c.globalUserConfigFiles, repoConfigFiles...)
userConfig, err := loadUserConfigWithDefaults(configFiles)
userConfig, err := loadUserConfigWithDefaults(configFiles, true)
if err != nil {
return err
}
@@ -391,7 +544,7 @@ func (c *AppConfig) ReloadChangedUserConfigFiles() (error, bool) {
return nil, false
}
userConfig, err := loadUserConfigWithDefaults(c.userConfigFiles)
userConfig, err := loadUserConfigWithDefaults(c.userConfigFiles, true)
if err != nil {
return err, false
}
File diff suppressed because it is too large Load Diff
+34 -13
View File
@@ -1,9 +1,12 @@
package config
import "os"
import (
"os"
"strings"
)
func GetEditTemplate(osConfig *OSConfig, guessDefaultEditor func() string) (string, bool) {
preset := getPreset(osConfig, guessDefaultEditor)
func GetEditTemplate(shell string, osConfig *OSConfig, guessDefaultEditor func() string) (string, bool) {
preset := getPreset(shell, osConfig, guessDefaultEditor)
template := osConfig.Edit
if template == "" {
template = preset.editTemplate
@@ -12,8 +15,8 @@ func GetEditTemplate(osConfig *OSConfig, guessDefaultEditor func() string) (stri
return template, getEditInTerminal(osConfig, preset)
}
func GetEditAtLineTemplate(osConfig *OSConfig, guessDefaultEditor func() string) (string, bool) {
preset := getPreset(osConfig, guessDefaultEditor)
func GetEditAtLineTemplate(shell string, osConfig *OSConfig, guessDefaultEditor func() string) (string, bool) {
preset := getPreset(shell, osConfig, guessDefaultEditor)
template := osConfig.EditAtLine
if template == "" {
template = preset.editAtLineTemplate
@@ -21,8 +24,8 @@ func GetEditAtLineTemplate(osConfig *OSConfig, guessDefaultEditor func() string)
return template, getEditInTerminal(osConfig, preset)
}
func GetEditAtLineAndWaitTemplate(osConfig *OSConfig, guessDefaultEditor func() string) string {
preset := getPreset(osConfig, guessDefaultEditor)
func GetEditAtLineAndWaitTemplate(shell string, osConfig *OSConfig, guessDefaultEditor func() string) string {
preset := getPreset(shell, osConfig, guessDefaultEditor)
template := osConfig.EditAtLineAndWait
if template == "" {
template = preset.editAtLineAndWaitTemplate
@@ -30,8 +33,8 @@ func GetEditAtLineAndWaitTemplate(osConfig *OSConfig, guessDefaultEditor func()
return template
}
func GetOpenDirInEditorTemplate(osConfig *OSConfig, guessDefaultEditor func() string) (string, bool) {
preset := getPreset(osConfig, guessDefaultEditor)
func GetOpenDirInEditorTemplate(shell string, osConfig *OSConfig, guessDefaultEditor func() string) (string, bool) {
preset := getPreset(shell, osConfig, guessDefaultEditor)
template := osConfig.OpenDirInEditor
if template == "" {
template = preset.openDirInEditorTemplate
@@ -50,17 +53,28 @@ type editPreset struct {
func returnBool(a bool) func() bool { return (func() bool { return a }) }
// IF YOU ADD A PRESET TO THIS FUNCTION YOU MUST UPDATE THE `Supported presets` SECTION OF docs/Config.md
func getPreset(osConfig *OSConfig, guessDefaultEditor func() string) *editPreset {
func getPreset(shell string, osConfig *OSConfig, guessDefaultEditor func() string) *editPreset {
var nvimRemoteEditTemplate, nvimRemoteEditAtLineTemplate, nvimRemoteOpenDirInEditorTemplate string
// By default fish doesn't have SHELL variable set, but it does have FISH_VERSION since Nov 2012.
if (strings.HasSuffix(shell, "fish")) || (os.Getenv("FISH_VERSION") != "") {
nvimRemoteEditTemplate = `begin; if test -z "$NVIM"; nvim -- {{filename}}; else; nvim --server "$NVIM" --remote-send "q"; nvim --server "$NVIM" --remote-tab {{filename}}; end; end`
nvimRemoteEditAtLineTemplate = `begin; if test -z "$NVIM"; nvim +{{line}} -- {{filename}}; else; nvim --server "$NVIM" --remote-send "q"; nvim --server "$NVIM" --remote-tab {{filename}}; nvim --server "$NVIM" --remote-send ":{{line}}<CR>"; end; end`
nvimRemoteOpenDirInEditorTemplate = `begin; if test -z "$NVIM"; nvim -- {{dir}}; else; nvim --server "$NVIM" --remote-send "q"; nvim --server "$NVIM" --remote-tab {{dir}}; end; end`
} else {
nvimRemoteEditTemplate = `[ -z "$NVIM" ] && (nvim -- {{filename}}) || (nvim --server "$NVIM" --remote-send "q" && nvim --server "$NVIM" --remote-tab {{filename}})`
nvimRemoteEditAtLineTemplate = `[ -z "$NVIM" ] && (nvim +{{line}} -- {{filename}}) || (nvim --server "$NVIM" --remote-send "q" && nvim --server "$NVIM" --remote-tab {{filename}} && nvim --server "$NVIM" --remote-send ":{{line}}<CR>")`
nvimRemoteOpenDirInEditorTemplate = `[ -z "$NVIM" ] && (nvim -- {{dir}}) || (nvim --server "$NVIM" --remote-send "q" && nvim --server "$NVIM" --remote-tab {{dir}})`
}
presets := map[string]*editPreset{
"vi": standardTerminalEditorPreset("vi"),
"vim": standardTerminalEditorPreset("vim"),
"nvim": standardTerminalEditorPreset("nvim"),
"nvim-remote": {
editTemplate: `[ -z "$NVIM" ] && (nvim -- {{filename}}) || (nvim --server "$NVIM" --remote-send "q" && nvim --server "$NVIM" --remote-tab {{filename}})`,
editAtLineTemplate: `[ -z "$NVIM" ] && (nvim +{{line}} -- {{filename}}) || (nvim --server "$NVIM" --remote-send "q" && nvim --server "$NVIM" --remote-tab {{filename}} && nvim --server "$NVIM" --remote-send ":{{line}}<CR>")`,
editTemplate: nvimRemoteEditTemplate,
editAtLineTemplate: nvimRemoteEditAtLineTemplate,
// No remote-wait support yet. See https://github.com/neovim/neovim/pull/17856
editAtLineAndWaitTemplate: `nvim +{{line}} {{filename}}`,
openDirInEditorTemplate: `[ -z "$NVIM" ] && (nvim -- {{dir}}) || (nvim --server "$NVIM" --remote-send "q" && nvim --server "$NVIM" --remote-tab {{dir}})`,
openDirInEditorTemplate: nvimRemoteOpenDirInEditorTemplate,
suspend: func() bool {
_, ok := os.LookupEnv("NVIM")
return !ok
@@ -126,6 +140,13 @@ func getPreset(osConfig *OSConfig, guessDefaultEditor func() string) *editPreset
openDirInEditorTemplate: "zed -- {{dir}}",
suspend: returnBool(false),
},
"acme": {
editTemplate: "B {{filename}}",
editAtLineTemplate: "B {{filename}}:{{line}}",
editAtLineAndWaitTemplate: "E {{filename}}:{{line}}",
openDirInEditorTemplate: "B {{dir}}",
suspend: returnBool(false),
},
}
// Some of our presets have a different name than the editor they are using.
+3 -3
View File
@@ -111,15 +111,15 @@ func TestGetEditTemplate(t *testing.T) {
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
template, suspend := GetEditTemplate(s.osConfig, s.guessDefaultEditor)
template, suspend := GetEditTemplate("bash", s.osConfig, s.guessDefaultEditor)
assert.Equal(t, s.expectedEditTemplate, template)
assert.Equal(t, s.expectedSuspend, suspend)
template, suspend = GetEditAtLineTemplate(s.osConfig, s.guessDefaultEditor)
template, suspend = GetEditAtLineTemplate("bash", s.osConfig, s.guessDefaultEditor)
assert.Equal(t, s.expectedEditAtLineTemplate, template)
assert.Equal(t, s.expectedSuspend, suspend)
template = GetEditAtLineAndWaitTemplate(s.osConfig, s.guessDefaultEditor)
template = GetEditAtLineAndWaitTemplate("bash", s.osConfig, s.guessDefaultEditor)
assert.Equal(t, s.expectedEditAtLineAndWaitTemplate, template)
})
}
+69 -28
View File
@@ -24,6 +24,7 @@ type UserConfig struct {
// If true, don't display introductory popups upon opening Lazygit.
DisableStartupPopups bool `yaml:"disableStartupPopups"`
// User-configured commands that can be invoked from within Lazygit
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Custom_Command_Keybindings.md
CustomCommands []CustomCommand `yaml:"customCommands" jsonschema:"uniqueItems=true"`
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-pull-request-urls
Services map[string]string `yaml:"services"`
@@ -53,9 +54,12 @@ type GuiConfig struct {
AuthorColors map[string]string `yaml:"authorColors"`
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-branch-color
// Deprecated: use branchColorPatterns instead
BranchColors map[string]string `yaml:"branchColors"`
BranchColors map[string]string `yaml:"branchColors" jsonschema:"deprecated"`
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-branch-color
BranchColorPatterns map[string]string `yaml:"branchColorPatterns"`
// Custom icons for filenames and file extensions
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-files-icon--color
CustomIcons CustomIconsConfig `yaml:"customIcons"`
// The number of lines you scroll by when scrolling the main window
ScrollHeight int `yaml:"scrollHeight" jsonschema:"minimum=1"`
// If true, allow scrolling past the bottom of the content in the main window
@@ -130,7 +134,7 @@ type GuiConfig struct {
// If true, show jump-to-window keybindings in window titles.
ShowPanelJumps bool `yaml:"showPanelJumps"`
// Deprecated: use nerdFontsVersion instead
ShowIcons bool `yaml:"showIcons"`
ShowIcons bool `yaml:"showIcons" jsonschema:"deprecated"`
// Nerd fonts version to use.
// One of: '2' | '3' | empty string (default)
// If empty, do not show icons.
@@ -240,6 +244,9 @@ type GitConfig struct {
AutoFetch bool `yaml:"autoFetch"`
// If true, periodically refresh files and submodules
AutoRefresh bool `yaml:"autoRefresh"`
// If not "none", lazygit will automatically fast-forward local branches to match their upstream after fetching. Applies to branches that are not the currently checked out branch, and only to those that are strictly behind their upstream (as opposed to diverged).
// Possible values: 'none' | 'onlyMainBranches' | 'allBranches'
AutoForwardBranches string `yaml:"autoForwardBranches" jsonschema:"enum=none,enum=onlyMainBranches,enum=allBranches"`
// 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
@@ -249,10 +256,7 @@ type GitConfig struct {
AutoStageResolvedConflicts bool `yaml:"autoStageResolvedConflicts"`
// Command used when displaying the current branch git log in the main window
BranchLogCmd string `yaml:"branchLogCmd"`
// Command used to display git log of all branches in the main window.
// Deprecated: Use `allBranchesLogCmds` instead.
AllBranchesLogCmd string `yaml:"allBranchesLogCmd"`
// Commands used to display git log of all branches in the main window, they will be cycled in order of appearance
// 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 []string `yaml:"allBranchesLogCmds"`
// If true, do not spawn a separate process when using GPG
OverrideGpg bool `yaml:"overrideGpg"`
@@ -379,6 +383,8 @@ type KeybindingUniversalConfig struct {
ScrollRight string `yaml:"scrollRight"`
GotoTop string `yaml:"gotoTop"`
GotoBottom string `yaml:"gotoBottom"`
GotoTopAlt string `yaml:"gotoTop-alt"`
GotoBottomAlt string `yaml:"gotoBottom-alt"`
ToggleRangeSelect string `yaml:"toggleRangeSelect"`
RangeSelectDown string `yaml:"rangeSelectDown"`
RangeSelectUp string `yaml:"rangeSelectUp"`
@@ -389,6 +395,7 @@ type KeybindingUniversalConfig struct {
NextBlockAlt2 string `yaml:"nextBlock-alt2"`
PrevBlockAlt2 string `yaml:"prevBlock-alt2"`
JumpToBlock []string `yaml:"jumpToBlock"`
FocusMainView string `yaml:"focusMainView"`
NextMatch string `yaml:"nextMatch"`
PrevMatch string `yaml:"prevMatch"`
StartSearch string `yaml:"startSearch"`
@@ -398,6 +405,7 @@ type KeybindingUniversalConfig struct {
GoInto string `yaml:"goInto"`
Confirm string `yaml:"confirm"`
ConfirmInEditor string `yaml:"confirmInEditor"`
ConfirmInEditorAlt string `yaml:"confirmInEditor-alt"`
Remove string `yaml:"remove"`
New string `yaml:"new"`
Edit string `yaml:"edit"`
@@ -472,6 +480,7 @@ type KeybindingBranchesConfig struct {
RebaseBranch string `yaml:"rebaseBranch"`
RenameBranch string `yaml:"renameBranch"`
MergeIntoCurrentBranch string `yaml:"mergeIntoCurrentBranch"`
MoveCommitsToNewBranch string `yaml:"moveCommitsToNewBranch"`
ViewGitFlowOptions string `yaml:"viewGitFlowOptions"`
FastForward string `yaml:"fastForward"`
CreateTag string `yaml:"createTag"`
@@ -510,6 +519,7 @@ type KeybindingCommitsConfig struct {
OpenInBrowser string `yaml:"openInBrowser"`
ViewBisectOptions string `yaml:"viewBisectOptions"`
StartInteractiveRebase string `yaml:"startInteractiveRebase"`
SelectCommitsOfCurrentBranch string `yaml:"selectCommitsOfCurrentBranch"`
}
type KeybindingAmendAttributeConfig struct {
@@ -566,7 +576,7 @@ type OSConfig struct {
// 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"`
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}}".
@@ -575,6 +585,18 @@ type OSConfig struct {
// Command for opening a link. Should contain "{{link}}".
OpenLink string `yaml:"openLink,omitempty"`
// CopyToClipboardCmd is the command for copying to clipboard.
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-command-for-copying-to-and-pasting-from-clipboard
CopyToClipboardCmd string `yaml:"copyToClipboardCmd,omitempty"`
// ReadFromClipboardCmd is the command for reading the clipboard.
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-command-for-copying-to-and-pasting-from-clipboard
ReadFromClipboardCmd string `yaml:"readFromClipboardCmd,omitempty"`
// 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 string `yaml:"shellFunctionsFile"`
// --------
// The following configs are all deprecated and kept for backward
@@ -584,27 +606,19 @@ type OSConfig struct {
// Deprecated: use Edit instead. Note that semantics are different:
// EditCommand is just the command itself, whereas Edit contains a
// "{{filename}}" variable.
EditCommand string `yaml:"editCommand,omitempty"`
EditCommand string `yaml:"editCommand,omitempty" jsonschema:"deprecated"`
// EditCommandTemplate is the command template for editing a file
// Deprecated: use EditAtLine instead.
EditCommandTemplate string `yaml:"editCommandTemplate,omitempty"`
EditCommandTemplate string `yaml:"editCommandTemplate,omitempty" jsonschema:"deprecated"`
// OpenCommand is the command for opening a file
// Deprecated: use Open instead.
OpenCommand string `yaml:"openCommand,omitempty"`
OpenCommand string `yaml:"openCommand,omitempty" jsonschema:"deprecated"`
// OpenLinkCommand is the command for opening a link
// Deprecated: use OpenLink instead.
OpenLinkCommand string `yaml:"openLinkCommand,omitempty"`
// CopyToClipboardCmd is the command for copying to clipboard.
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-command-for-copying-to-and-pasting-from-clipboard
CopyToClipboardCmd string `yaml:"copyToClipboardCmd,omitempty"`
// ReadFromClipboardCmd is the command for reading the clipboard.
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-command-for-copying-to-and-pasting-from-clipboard
ReadFromClipboardCmd string `yaml:"readFromClipboardCmd,omitempty"`
OpenLinkCommand string `yaml:"openLinkCommand,omitempty" jsonschema:"deprecated"`
}
type CustomCommandAfterHook struct {
@@ -614,26 +628,34 @@ type CustomCommandAfterHook struct {
type CustomCommand struct {
// The key to trigger the command. Use a single letter or one of the values from https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md
Key string `yaml:"key"`
// Instead of defining a single custom command, create a menu of custom commands. Useful for grouping related commands together under a single keybinding, and for keeping them out of the global keybindings menu.
// When using this, all other fields except Key and Description are ignored and must be empty.
CommandMenu []CustomCommand `yaml:"commandMenu"`
// The context in which to listen for the key. Valid values are: status, files, worktrees, localBranches, remotes, remoteBranches, tags, commits, reflogCommits, subCommits, commitFiles, stash, and global. Multiple contexts separated by comma are allowed; most useful for "commits, subCommits" or "files, commitFiles".
Context string `yaml:"context" jsonschema:"example=status,example=files,example=worktrees,example=localBranches,example=remotes,example=remoteBranches,example=tags,example=commits,example=reflogCommits,example=subCommits,example=commitFiles,example=stash,example=global"`
// The command to run (using Go template syntax for placeholder values)
Command string `yaml:"command" jsonschema:"example=git fetch {{.Form.Remote}} {{.Form.Branch}} && git checkout FETCH_HEAD"`
// If true, run the command in a subprocess (e.g. if the command requires user input)
Subprocess bool `yaml:"subprocess"`
// A list of prompts that will request user input before running the final command
Prompts []CustomCommandPrompt `yaml:"prompts"`
// Text to display while waiting for command to finish
LoadingText string `yaml:"loadingText" jsonschema:"example=Loading..."`
// Label for the custom command when displayed in the keybindings menu
Description string `yaml:"description"`
// If true, stream the command's output to the Command Log panel
Stream bool `yaml:"stream"`
// If true, show the command's output in a popup within Lazygit
ShowOutput bool `yaml:"showOutput"`
// The title to display in the popup panel if showOutput is true. If left unset, the command will be used as the title.
// Where the output of the command should go. 'none' discards it, 'terminal' suspends lazygit and runs the command in the terminal (useful for commands that require user input), 'log' streams it to the command log, 'logWithPty' is like 'log' but runs the command in a pseudo terminal (can be useful for commands that produce colored output when the output is a terminal), and 'popup' shows it in a popup.
Output string `yaml:"output" jsonschema:"enum=none,enum=terminal,enum=log,enum=logWithPty,enum=popup"`
// The title to display in the popup panel if output is set to 'popup'. If left unset, the command will be used as the title.
OutputTitle string `yaml:"outputTitle"`
// Actions to take after the command has completed
After CustomCommandAfterHook `yaml:"after"`
// [dev] Pointer so that we can tell whether it appears in the config file
After *CustomCommandAfterHook `yaml:"after"`
}
func (c *CustomCommand) GetDescription() string {
if c.Description != "" {
return c.Description
}
return c.Command
}
type CustomCommandPrompt struct {
@@ -689,6 +711,18 @@ type CustomCommandMenuOption struct {
Value string `yaml:"value" jsonschema:"example=feature,minLength=1"`
}
type CustomIconsConfig struct {
// Map of filenames to icon properties (icon and color)
Filenames map[string]IconProperties `yaml:"filenames"`
// Map of file extensions (including the dot) to icon properties (icon and color)
Extensions map[string]IconProperties `yaml:"extensions"`
}
type IconProperties struct {
Icon string `yaml:"icon"`
Color string `yaml:"color"`
}
func GetDefaultConfig() *UserConfig {
return &UserConfig{
Gui: GuiConfig{
@@ -783,10 +817,11 @@ func GetDefaultConfig() *UserConfig {
MainBranches: []string{"master", "main"},
AutoFetch: true,
AutoRefresh: true,
AutoForwardBranches: "onlyMainBranches",
FetchAll: true,
AutoStageResolvedConflicts: true,
BranchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --",
AllBranchesLogCmd: "git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium",
AllBranchesLogCmds: []string{"git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium"},
DisableForcePushing: false,
CommitPrefixes: map[string][]CommitPrefixConfig(nil),
BranchPrefix: "",
@@ -826,6 +861,8 @@ func GetDefaultConfig() *UserConfig {
ScrollRight: "L",
GotoTop: "<",
GotoBottom: ">",
GotoTopAlt: "<home>",
GotoBottomAlt: "<end>",
ToggleRangeSelect: "v",
RangeSelectDown: "<s-down>",
RangeSelectUp: "<s-up>",
@@ -836,6 +873,7 @@ func GetDefaultConfig() *UserConfig {
PrevBlockAlt2: "<backtab>",
NextBlockAlt2: "<tab>",
JumpToBlock: []string{"1", "2", "3", "4", "5"},
FocusMainView: "0",
NextMatch: "n",
PrevMatch: "N",
StartSearch: "/",
@@ -845,6 +883,7 @@ func GetDefaultConfig() *UserConfig {
GoInto: "<enter>",
Confirm: "<enter>",
ConfirmInEditor: "<a-enter>",
ConfirmInEditorAlt: "<c-s>",
Remove: "d",
New: "n",
Edit: "e",
@@ -916,6 +955,7 @@ func GetDefaultConfig() *UserConfig {
RebaseBranch: "r",
RenameBranch: "R",
MergeIntoCurrentBranch: "M",
MoveCommitsToNewBranch: "N",
ViewGitFlowOptions: "i",
FastForward: "f",
CreateTag: "T",
@@ -952,6 +992,7 @@ func GetDefaultConfig() *UserConfig {
OpenInBrowser: "o",
ViewBisectOptions: "b",
StartInteractiveRebase: "i",
SelectCommitsOfCurrentBranch: "*",
},
AmendAttribute: KeybindingAmendAttributeConfig{
ResetAuthor: "a",
+29
View File
@@ -19,6 +19,10 @@ func (config *UserConfig) Validate() error {
[]string{"none", "onlyArrow", "arrowAndNumber"}); err != nil {
return err
}
if err := validateEnum("git.autoForwardBranches", config.Git.AutoForwardBranches,
[]string{"none", "onlyMainBranches", "allBranches"}); err != nil {
return err
}
if err := validateKeybindings(config.Keybinding); err != nil {
return err
}
@@ -96,6 +100,31 @@ func validateCustomCommands(customCommands []CustomCommand) error {
if err := validateCustomCommandKey(customCommand.Key); err != nil {
return err
}
if len(customCommand.CommandMenu) > 0 {
if len(customCommand.Context) > 0 ||
len(customCommand.Command) > 0 ||
len(customCommand.Prompts) > 0 ||
len(customCommand.LoadingText) > 0 ||
len(customCommand.Output) > 0 ||
len(customCommand.OutputTitle) > 0 ||
customCommand.After != nil {
commandRef := ""
if len(customCommand.Key) > 0 {
commandRef = fmt.Sprintf(" with key '%s'", customCommand.Key)
}
return fmt.Errorf("Error with custom command%s: it is not allowed to use both commandMenu and any of the other fields except key and description.", commandRef)
}
if err := validateCustomCommands(customCommand.CommandMenu); err != nil {
return err
}
} else {
if err := validateEnum("customCommand.output", customCommand.Output,
[]string{"", "none", "terminal", "log", "logWithPty", "popup"}); err != nil {
return err
}
}
}
return nil
}
+91
View File
@@ -74,6 +74,97 @@ func TestUserConfigValidate_enums(t *testing.T) {
{value: "invalid_value", valid: false},
},
},
{
name: "Custom command keybinding in sub menu",
setup: func(config *UserConfig, value string) {
config.CustomCommands = []CustomCommand{
{
Key: "X",
Description: "My Custom Commands",
CommandMenu: []CustomCommand{
{Key: value, Command: "echo 'hello'", Context: "global"},
},
},
}
},
testCases: []testCase{
{value: "", valid: true},
{value: "<disabled>", valid: true},
{value: "q", valid: true},
{value: "<c-c>", valid: true},
{value: "invalid_value", valid: false},
},
},
{
name: "Custom command output",
setup: func(config *UserConfig, value string) {
config.CustomCommands = []CustomCommand{
{
Output: value,
},
}
},
testCases: []testCase{
{value: "", valid: true},
{value: "none", valid: true},
{value: "terminal", valid: true},
{value: "log", valid: true},
{value: "logWithPty", valid: true},
{value: "popup", valid: true},
{value: "invalid_value", valid: false},
},
},
{
name: "Custom command sub menu",
setup: func(config *UserConfig, _ string) {
config.CustomCommands = []CustomCommand{
{
Key: "X",
Description: "My Custom Commands",
CommandMenu: []CustomCommand{
{Key: "1", Command: "echo 'hello'", Context: "global"},
},
},
}
},
testCases: []testCase{
{value: "", valid: true},
},
},
{
name: "Custom command sub menu",
setup: func(config *UserConfig, _ string) {
config.CustomCommands = []CustomCommand{
{
Key: "X",
Context: "global", // context is not allowed for submenus
CommandMenu: []CustomCommand{
{Key: "1", Command: "echo 'hello'", Context: "global"},
},
},
}
},
testCases: []testCase{
{value: "", valid: false},
},
},
{
name: "Custom command sub menu",
setup: func(config *UserConfig, _ string) {
config.CustomCommands = []CustomCommand{
{
Key: "X",
LoadingText: "loading", // other properties are not allowed for submenus (using loadingText as an example)
CommandMenu: []CustomCommand{
{Key: "1", Command: "echo 'hello'", Context: "global"},
},
},
}
},
testCases: []testCase{
{value: "", valid: false},
},
},
}
for _, s := range scenarios {
+5 -1
View File
@@ -125,7 +125,11 @@ func (self *BackgroundRoutineMgr) goEvery(interval time.Duration, stop chan stru
func (self *BackgroundRoutineMgr) backgroundFetch() (err error) {
err = self.gui.git.Sync.FetchBackground()
_ = self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.ASYNC})
_ = self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.SYNC})
if err == nil {
err = self.gui.helpers.BranchesHelper.AutoForwardBranches()
}
return err
}
+4 -4
View File
@@ -141,9 +141,9 @@ func (gui *Gui) getRandomTip() string {
formattedKey(config.Universal.NextPage),
),
fmt.Sprintf(
"You can jump to the top/bottom of a panel using '%s' and '%s'",
formattedKey(config.Universal.GotoTop),
formattedKey(config.Universal.GotoBottom),
"You can jump to the top/bottom of a panel using '%s (or %s)' and '%s (or %s)'",
formattedKey(config.Universal.GotoTop), formattedKey(config.Universal.GotoTopAlt),
formattedKey(config.Universal.GotoBottom), formattedKey(config.Universal.GotoBottomAlt),
),
fmt.Sprintf(
"To collapse/expand a directory, press '%s'",
@@ -167,7 +167,7 @@ func (gui *Gui) getRandomTip() string {
// general advice
"`git commit` is really just the programmer equivalent of saving your game. Always do it before embarking on an ambitious change!",
"Try to separate commits that refactor code from commits that add new functionality: if they're squashed into the one commit, it can be hard to spot what's new.",
"Try to separate commits that refactor code from commits that add new functionality: if they're squashed into one commit, it can be hard to spot what's new.",
"If you ever want to experiment, it's easy to create a new branch off your current one and go nuts, then delete it afterwards",
"Always read through the diff of your changes before assigning somebody to review your code. Better for you to catch any silly mistakes than your colleagues!",
"If something goes wrong, you can always checkout a commit from your reflog to return to an earlier state",
+18 -12
View File
@@ -55,17 +55,7 @@ func (self *ContextMgr) Replace(c types.Context) {
self.Activate(c, types.OnFocusOpts{})
}
func (self *ContextMgr) Push(c types.Context, opts ...types.OnFocusOpts) {
if len(opts) > 1 {
panic("cannot pass multiple opts to Push")
}
singleOpts := types.OnFocusOpts{}
if len(opts) > 0 {
// using triple dot but you should only ever pass one of these opt structs
singleOpts = opts[0]
}
func (self *ContextMgr) Push(c types.Context, opts types.OnFocusOpts) {
if !c.IsFocusable() {
return
}
@@ -77,7 +67,7 @@ func (self *ContextMgr) Push(c types.Context, opts ...types.OnFocusOpts) {
}
if contextToActivate != nil {
self.Activate(contextToActivate, singleOpts)
self.Activate(contextToActivate, opts)
}
}
@@ -367,3 +357,19 @@ func (self *ContextMgr) CurrentPopup() []types.Context {
return context.GetKind() == types.TEMPORARY_POPUP || context.GetKind() == types.PERSISTENT_POPUP
})
}
func (self *ContextMgr) NextInStack(c types.Context) types.Context {
self.RLock()
defer self.RUnlock()
for i := range self.ContextStack {
if self.ContextStack[i].GetKey() == c.GetKey() {
if i == 0 {
return nil
}
return self.ContextStack[i-1]
}
}
panic("context not in stack")
}
+20 -8
View File
@@ -13,12 +13,13 @@ type BaseContext struct {
windowName string
onGetOptionsMap func() map[string]string
keybindingsFns []types.KeybindingsFn
mouseKeybindingsFns []types.MouseKeybindingsFn
onClickFn func() error
onRenderToMainFn func()
onFocusFn onFocusFn
onFocusLostFn onFocusLostFn
keybindingsFns []types.KeybindingsFn
mouseKeybindingsFns []types.MouseKeybindingsFn
onClickFn func() error
onClickFocusedMainViewFn onClickFocusedMainViewFn
onRenderToMainFn func()
onFocusFn onFocusFn
onFocusLostFn onFocusLostFn
focusable bool
transient bool
@@ -31,8 +32,9 @@ type BaseContext struct {
}
type (
onFocusFn = func(types.OnFocusOpts)
onFocusLostFn = func(types.OnFocusLostOpts)
onFocusFn = func(types.OnFocusOpts)
onFocusLostFn = func(types.OnFocusLostOpts)
onClickFocusedMainViewFn = func(mainViewName string, clickedLineIdx int) error
)
var _ types.IBaseContext = &BaseContext{}
@@ -144,10 +146,20 @@ func (self *BaseContext) AddOnClickFn(fn func() error) {
}
}
func (self *BaseContext) AddOnClickFocusedMainViewFn(fn onClickFocusedMainViewFn) {
if fn != nil {
self.onClickFocusedMainViewFn = fn
}
}
func (self *BaseContext) GetOnClick() func() error {
return self.onClickFn
}
func (self *BaseContext) GetOnClickFocusedMainView() onClickFocusedMainViewFn {
return self.onClickFocusedMainViewFn
}
func (self *BaseContext) AddOnRenderToMainFn(fn func()) {
if fn != nil {
self.onRenderToMainFn = fn
+1 -1
View File
@@ -30,7 +30,7 @@ func NewBranchesContext(c *ContextCommon) *BranchesContext {
c.State().GetItemOperation,
c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL,
c.Modes().Diffing.Ref,
c.Views().Branches.InnerWidth(),
c.Views().Branches.InnerWidth()+c.Views().Branches.OriginX(),
c.Tr,
c.UserConfig(),
c.Model().Worktrees,
+1 -1
View File
@@ -39,7 +39,7 @@ func NewCommitFilesContext(c *ContextCommon) *CommitFilesContext {
}
showFileIcons := icons.IsIconEnabled() && c.UserConfig().Gui.ShowFileIcons
lines := presentation.RenderCommitFileTree(viewModel, c.Git().Patch.PatchBuilder, showFileIcons)
lines := presentation.RenderCommitFileTree(viewModel, c.Git().Patch.PatchBuilder, showFileIcons, &c.UserConfig().Gui.CustomIcons)
return lo.Map(lines, func(line string, _ int) []string {
return []string{line}
})
+23 -8
View File
@@ -6,7 +6,6 @@ import (
"strconv"
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
@@ -43,6 +42,10 @@ type CommitMessageViewModel struct {
// invoked when pressing the switch-to-editor key binding
onSwitchToEditor func(string) error
// the following two fields are used for the display of the "hooks disabled" subtitle
forceSkipHooks bool
skipHooksPrefix string
// The message typed in before cycling through history
// We store this separately to 'preservedMessage' because 'preservedMessage'
// is specifically for committing staged files and we don't want this affected
@@ -149,12 +152,16 @@ func (self *CommitMessageContext) SetPanelState(
initialMessage string,
onConfirm func(string, string) error,
onSwitchToEditor func(string) error,
forceSkipHooks bool,
skipHooksPrefix string,
) {
self.viewModel.selectedindex = index
self.viewModel.preserveMessage = preserveMessage
self.viewModel.initialMessage = initialMessage
self.viewModel.onConfirm = onConfirm
self.viewModel.onSwitchToEditor = onSwitchToEditor
self.viewModel.forceSkipHooks = forceSkipHooks
self.viewModel.skipHooksPrefix = skipHooksPrefix
self.GetView().Title = summaryTitle
self.c.Views().CommitDescription.Title = descriptionTitle
@@ -167,16 +174,24 @@ func (self *CommitMessageContext) SetPanelState(
self.c.Views().CommitDescription.Visible = true
}
func (self *CommitMessageContext) RenderCommitLength() {
if self.c.UserConfig().Gui.CommitLength.Show {
self.c.Views().CommitMessage.Subtitle = getBufferLength(self.c.Views().CommitMessage)
} else {
self.c.Views().CommitMessage.Subtitle = ""
func (self *CommitMessageContext) RenderSubtitle() {
skipHookPrefix := self.viewModel.skipHooksPrefix
subject := self.c.Views().CommitMessage.TextArea.GetContent()
var subtitle string
if self.viewModel.forceSkipHooks || (skipHookPrefix != "" && strings.HasPrefix(subject, skipHookPrefix)) {
subtitle = self.c.Tr.CommitHooksDisabledSubTitle
}
if self.c.UserConfig().Gui.CommitLength.Show {
if subtitle != "" {
subtitle += "─"
}
subtitle += getBufferLength(subject)
}
self.c.Views().CommitMessage.Subtitle = subtitle
}
func getBufferLength(view *gocui.View) string {
return " " + strconv.Itoa(strings.Count(view.TextArea.GetContent(), "")-1) + " "
func getBufferLength(subject string) string {
return " " + strconv.Itoa(strings.Count(subject, "")-1) + " "
}
func (self *CommitMessageContext) SwitchToEditor(message string) error {
+2 -2
View File
@@ -98,8 +98,8 @@ type ContextTree struct {
SubCommits *SubCommitsContext
Stash *StashContext
Suggestions *SuggestionsContext
Normal types.Context
NormalSecondary types.Context
Normal *MainContext
NormalSecondary *MainContext
Staging *PatchExplorerContext
StagingSecondary *PatchExplorerContext
CustomPatchBuilder *PatchExplorerContext
+6
View File
@@ -26,6 +26,8 @@ type ListContextTrait struct {
func (self *ListContextTrait) IsListContext() {}
func (self *ListContextTrait) FocusLine() {
self.Context.FocusLine()
// Doing this at the end of the layout function because we need the view to be
// resized before we focus the line, otherwise if we're in accordion mode
// the view could be squashed and won't how to adjust the cursor/origin.
@@ -147,3 +149,7 @@ func (self *ListContextTrait) TotalContentHeight() int {
}
return result
}
func (self *ListContextTrait) IndexForGotoBottom() int {
return self.list.Len() - 1
}
+1 -1
View File
@@ -52,7 +52,7 @@ func (self *ListRenderer) ModelIndexToViewIndex(modelIndex int) int {
}
func (self *ListRenderer) ViewIndexToModelIndex(viewIndex int) int {
viewIndex = utils.Clamp(viewIndex, 0, self.list.Len()+self.numNonModelItems)
viewIndex = lo.Clamp(viewIndex, 0, self.list.Len()+self.numNonModelItems)
if self.modelIndicesByViewIndex != nil {
return self.modelIndicesByViewIndex[viewIndex]
}
+2 -2
View File
@@ -115,7 +115,7 @@ func TestListRenderer_renderLines(t *testing.T) {
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
viewModel := NewListViewModel[mystring](func() []mystring { return s.modelStrings })
viewModel := NewListViewModel(func() []mystring { return s.modelStrings })
var getNonModelItems func() []*NonModelItem
if s.nonModelIndices != nil {
getNonModelItems = func() []*NonModelItem {
@@ -236,7 +236,7 @@ func TestListRenderer_ModelIndexToViewIndex_and_back(t *testing.T) {
assert.Equal(t, len(s.viewIndices), len(s.expectedModelIndices))
modelInts := lo.Map(lo.Range(s.numModelItems), func(i int, _ int) myint { return myint(i) })
viewModel := NewListViewModel[myint](func() []myint { return modelInts })
viewModel := NewListViewModel(func() []myint { return modelInts })
var getNonModelItems func() []*NonModelItem
if s.nonModelIndices != nil {
getNonModelItems = func() []*NonModelItem {
+71 -10
View File
@@ -1,13 +1,13 @@
package context
import (
"fmt"
"log"
"strings"
"time"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/samber/lo"
@@ -32,16 +32,15 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
)
getDisplayStrings := func(startIdx int, endIdx int) [][]string {
selectedCommitHash := ""
var selectedCommitHashPtr *string
if c.Context().Current().GetKey() == LOCAL_COMMITS_CONTEXT_KEY {
selectedCommit := viewModel.GetSelected()
if selectedCommit != nil {
selectedCommitHash = selectedCommit.Hash
selectedCommitHashPtr = selectedCommit.HashPtr()
}
}
showYouAreHereLabel := c.Model().WorkingTreeStateAtLastCommitRefresh == enums.REBASE_MODE_REBASING
hasRebaseUpdateRefsConfig := c.Git().Config.GetRebaseUpdateRefs()
return presentation.GetCommitListDisplayStrings(
@@ -58,15 +57,59 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
c.UserConfig().Gui.ShortTimeFormat,
time.Now(),
c.UserConfig().Git.ParseEmoji,
selectedCommitHash,
selectedCommitHashPtr,
startIdx,
endIdx,
shouldShowGraph(c),
c.Model().BisectInfo,
showYouAreHereLabel,
)
}
getNonModelItems := func() []*NonModelItem {
result := []*NonModelItem{}
if c.Model().WorkingTreeStateAtLastCommitRefresh.CanShowTodos() {
if c.Model().WorkingTreeStateAtLastCommitRefresh.Rebasing {
result = append(result, &NonModelItem{
Index: 0,
Content: fmt.Sprintf("--- %s ---", c.Tr.PendingRebaseTodosSectionHeader),
})
}
if c.Model().WorkingTreeStateAtLastCommitRefresh.CherryPicking ||
c.Model().WorkingTreeStateAtLastCommitRefresh.Reverting {
_, firstCherryPickOrRevertTodo, found := lo.FindIndexOf(
c.Model().Commits, func(c *models.Commit) bool {
return c.Status == models.StatusCherryPickingOrReverting ||
c.Status == models.StatusConflicted
})
if !found {
firstCherryPickOrRevertTodo = 0
}
label := lo.Ternary(c.Model().WorkingTreeStateAtLastCommitRefresh.CherryPicking,
c.Tr.PendingCherryPicksSectionHeader,
c.Tr.PendingRevertsSectionHeader)
result = append(result, &NonModelItem{
Index: firstCherryPickOrRevertTodo,
Content: fmt.Sprintf("--- %s ---", label),
})
}
_, firstRealCommit, found := lo.FindIndexOf(
c.Model().Commits, func(c *models.Commit) bool {
return !c.IsTODO()
})
if !found {
firstRealCommit = 0
}
result = append(result, &NonModelItem{
Index: firstRealCommit,
Content: fmt.Sprintf("--- %s ---", c.Tr.CommitsSectionHeader),
})
}
return result
}
ctx := &LocalCommitsContext{
LocalCommitsViewModel: viewModel,
SearchTrait: NewSearchTrait(c),
@@ -83,6 +126,7 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
ListRenderer: ListRenderer{
list: viewModel,
getDisplayStrings: getDisplayStrings,
getNonModelItems: getNonModelItems,
},
c: c,
refreshViewportOnChange: true,
@@ -148,7 +192,7 @@ func (self *LocalCommitsContext) GetSelectedCommitHash() string {
if commit == nil {
return ""
}
return commit.Hash
return commit.Hash()
}
func (self *LocalCommitsContext) SelectCommitByHash(hash string) bool {
@@ -156,7 +200,7 @@ func (self *LocalCommitsContext) SelectCommitByHash(hash string) bool {
return false
}
if _, idx, found := lo.FindIndexOf(self.GetItems(), func(c *models.Commit) bool { return c.Hash == hash }); found {
if _, idx, found := lo.FindIndexOf(self.GetItems(), func(c *models.Commit) bool { return c.Hash() == hash }); found {
self.SetSelection(idx)
return true
}
@@ -175,7 +219,7 @@ func (self *LocalCommitsContext) RefForAdjustingLineNumberInDiff() string {
if commits == nil {
return ""
}
return commits[0].Hash
return commits[0].Hash()
}
func (self *LocalCommitsContext) ModelSearchResults(searchStr string, caseSensitive bool) []gocui.SearchPosition {
@@ -240,8 +284,25 @@ func searchModelCommits(caseSensitive bool, commits []*models.Commit, columnPosi
// that we render. So we just set the XStart and XEnd values to the
// start and end of the commit hash column, which is the second one.
result := gocui.SearchPosition{XStart: columnPositions[1], XEnd: columnPositions[2] - 1, Y: idx}
return result, strings.Contains(normalize(commit.Hash), searchStr) ||
return result, strings.Contains(normalize(commit.Hash()), searchStr) ||
strings.Contains(normalize(commit.Name), searchStr) ||
strings.Contains(normalize(commit.ExtraInfo), searchStr) // allow searching for tags
})
}
func (self *LocalCommitsContext) IndexForGotoBottom() int {
commits := self.GetCommits()
selectedIdx := self.GetSelectedLineIdx()
if selectedIdx >= 0 && selectedIdx < len(commits)-1 {
if commits[selectedIdx+1].Status != models.StatusMerged {
_, idx, found := lo.FindIndexOf(commits, func(c *models.Commit) bool {
return c.Status == models.StatusMerged
})
if found {
return idx - 1
}
}
}
return self.list.Len() - 1
}
+41
View File
@@ -0,0 +1,41 @@
package context
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type MainContext struct {
*SimpleContext
*SearchTrait
}
var _ types.ISearchableContext = (*MainContext)(nil)
func NewMainContext(
view *gocui.View,
windowName string,
key types.ContextKey,
c *ContextCommon,
) *MainContext {
ctx := &MainContext{
SimpleContext: NewSimpleContext(
NewBaseContext(NewBaseContextOpts{
Kind: types.MAIN_CONTEXT,
View: view,
WindowName: windowName,
Key: key,
Focusable: true,
HighlightOnFocus: false,
})),
SearchTrait: NewSearchTrait(c),
}
ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(func(int) error { return nil }))
return ctx
}
func (self *MainContext) ModelSearchResults(searchStr string, caseSensitive bool) []gocui.SearchPosition {
return nil
}

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