Compare commits

...
108 Commits
Author SHA1 Message Date
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
204 changed files with 4029 additions and 1403 deletions
+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}.`)
+11 -3
View File
File diff suppressed because one or more lines are too long
+6
View File
@@ -337,6 +337,10 @@ git:
# If true, periodically refresh files and submodules
autoRefresh: true
# If not "none", lazygit will automatically forward branches to 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
@@ -501,6 +505,7 @@ keybinding:
- "3"
- "4"
- "5"
focusMainView: "0"
nextMatch: "n"
prevMatch: "N"
startSearch: /
@@ -582,6 +587,7 @@ keybinding:
rebaseBranch: r
renameBranch: R
mergeIntoCurrentBranch: M
moveCommitsToNewBranch: "N"
viewGitFlowOptions: i
fastForward: f
createTag: T
+32
View File
@@ -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,10 +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 | |
@@ -153,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
@@ -163,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 | |
@@ -178,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 | |
@@ -204,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)
@@ -259,11 +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 | |
@@ -282,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 | |
@@ -297,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 |
@@ -306,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 | |
@@ -329,11 +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 | |
@@ -363,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 | |
+32
View File
@@ -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,11 +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 | |
| `` / `` | 検索を開始 | |
@@ -123,10 +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 | |
| `` / `` | 検索を開始 | |
@@ -148,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 | |
| `` / `` | 検索を開始 | |
## コミットメッセージ
@@ -192,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 | |
@@ -226,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 | |
| `` / `` | 検索を開始 | |
## ブランチ
@@ -236,6 +256,9 @@ 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をクリップボードにコピー | |
@@ -251,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 | |
@@ -277,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)
@@ -348,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 | |
@@ -361,11 +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 | |
+32
View File
@@ -60,15 +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 |
@@ -78,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 | |
@@ -91,11 +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 | |
| `` / `` | 검색 시작 | |
@@ -140,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)
@@ -186,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을 클립보드에 복사 | |
@@ -201,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 | |
@@ -254,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 | |
@@ -289,10 +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 | |
| `` / `` | 검색 시작 | |
@@ -314,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 | |
| `` / `` | 검색 시작 | |
## 커밋메시지
@@ -334,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 | |
@@ -368,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 | |
| `` / `` | 검색 시작 | |
## 확인 패널
+32
View File
@@ -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,10 +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 | |
@@ -212,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
@@ -237,11 +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 | |
@@ -260,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 | |
@@ -275,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 |
@@ -306,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 | |
@@ -329,11 +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 | |
@@ -363,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 | |
+32
View File
@@ -82,14 +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 |
@@ -123,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 | |
@@ -138,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 | |
@@ -156,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)
@@ -232,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
@@ -251,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
@@ -269,11 +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 | |
@@ -287,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 | |
@@ -310,11 +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 | |
@@ -344,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 | |
@@ -373,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 | |
+32
View File
@@ -83,6 +83,7 @@ Veja a documentação:
| `` f `` | Buscar | Buscar alterações do controle remoto. |
| `` - `` | Recolher todos os arquivos | Recolher todos os diretórios na árvore de arquivos |
| `` = `` | Expandir todos os arquivos | Expandir todos os diretórios na árvore do arquivo |
| `` 0 `` | Focus main view | |
| `` / `` | Search the current view by text | |
## Branches locais
@@ -93,6 +94,9 @@ 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 | |
@@ -108,6 +112,7 @@ Veja a documentação:
| `` 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 | |
@@ -126,6 +131,7 @@ Veja a documentação:
| `` s `` | Sort order | |
| `` 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 | |
@@ -147,6 +153,7 @@ Veja a documentação:
| `` ` `` | 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. |
| `` - `` | 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
@@ -180,10 +187,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 `` | 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) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Ver arquivos | |
| `` w `` | View worktree options | |
| `` / `` | Search the current view by text | |
@@ -206,6 +217,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 `` | 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 | |
@@ -224,6 +236,9 @@ If you would instead like to start an interactive rebase from the selected commi
|-----|--------|-------------|
| `` 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)
@@ -289,11 +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 | |
| `` 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 | |
@@ -309,6 +328,14 @@ Veja a documentação:
| `` 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 |
@@ -318,6 +345,7 @@ 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 | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | Ver arquivos | |
| `` w `` | View worktree options | |
| `` / `` | Filter the current view by text | |
@@ -341,11 +369,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 | |
| `` 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> `` | Ver arquivos | |
| `` w `` | View worktree options | |
| `` / `` | Search the current view by text | |
+32
View File
@@ -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,11 +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 | |
@@ -170,10 +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 | |
| `` / `` | Найти | |
@@ -186,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 запроса на принятие изменений в буфер обмена | |
@@ -201,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 | |
@@ -229,11 +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 | |
| `` / `` | Найти | |
@@ -276,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 | |
| `` / `` | Найти | |
## Статус
@@ -299,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 | |
@@ -317,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 | |
@@ -362,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 | |
| `` / `` | Найти | |
## Хранилище
@@ -373,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 | |
+32
View File
@@ -60,11 +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) | |
| `` * `` | Select commits of current branch | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 查看提交 | |
| `` w `` | 查看工作区选项 | |
| `` / `` | 通过文本过滤当前视图 | |
@@ -78,11 +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 `` | 查看工作区选项 | |
| `` / `` | 开始搜索 | |
@@ -142,10 +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 `` | 查看工作区选项 | |
| `` / `` | 开始搜索 | |
@@ -174,6 +186,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` ` `` | 切换文件树视图 | 在平铺部署与树布局之间切换文件视图。平铺布局在一个列表中展示所有文件路径,树布局则根据目录分组展示。 |
| `` - `` | 折叠全部文件 | 折叠文件树中的全部目录 |
| `` = `` | 展开全部文件 | 展开文件树中的全部目录 |
| `` 0 `` | Focus main view | |
| `` / `` | 开始搜索 | |
## 文件
@@ -206,6 +219,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` f `` | 抓取 | 从远程获取变更 |
| `` - `` | 折叠全部文件 | 折叠文件树中的全部目录 |
| `` = `` | 展开全部文件 | 展开文件树中的全部目录 |
| `` 0 `` | Focus main view | |
| `` / `` | 开始搜索 | |
## 本地分支
@@ -216,6 +230,9 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` 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 复制到剪贴板 | |
@@ -231,6 +248,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` R `` | 重命名分支 | |
| `` u `` | 查看上游选项 | 查看与分支上游相关的选项,例如设置/取消设置上游和重置为上游。 |
| `` <c-t> `` | 使用外部差异比较工具(git difftool) | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 查看提交 | |
| `` w `` | 查看工作区选项 | |
| `` / `` | 通过文本过滤当前视图 | |
@@ -261,10 +279,19 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` 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 |
@@ -309,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 | |
| `` / `` | 开始搜索 | |
## 状态
@@ -344,6 +374,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` d `` | 删除 | 从贮藏列表中删除该贮藏项 |
| `` n `` | 新分支 | 从选定的贮藏项创建一个新分支。这是通过 git 检查创建贮藏项的提交,从该提交创建一个新分支,然后将贮藏项作为附加提交应用到新分支来实现的。 |
| `` r `` | 重命名贮藏 | |
| `` 0 `` | Focus main view | |
| `` <enter> `` | 查看提交的文件 | |
| `` w `` | 查看工作区选项 | |
| `` / `` | 通过文本过滤当前视图 | |
@@ -373,6 +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 `` | 查看工作区选项 | |
| `` / `` | 通过文本过滤当前视图 | |
+32
View File
@@ -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,11 +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 `` | 檢視工作目錄選項 | |
| `` / `` | 搜尋 | |
@@ -192,10 +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 `` | 檢視工作目錄選項 | |
| `` / `` | 搜尋 | |
@@ -224,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)
@@ -235,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 `` | 檢視工作目錄選項 | |
| `` / `` | 搜尋 | |
@@ -248,11 +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 `` | 檢視工作目錄選項 | |
| `` / `` | 搜尋 | |
@@ -265,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 到剪貼板 | |
@@ -280,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 `` | 檢視工作目錄選項 | |
| `` / `` | 搜尋 | |
@@ -295,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 `` | 檢視工作目錄選項 | |
| `` / `` | 搜尋 | |
@@ -329,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 | |
| `` / `` | 搜尋 | |
## 狀態
@@ -373,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 `` | 檢視工作目錄選項 | |
| `` / `` | 搜尋 | |
+2 -2
View File
@@ -15,7 +15,7 @@ require (
github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/generics v0.0.0-20250406224309-4f541cb84918
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd
github.com/jesseduffield/gocui v0.3.1-0.20250408140206-7f1bb9232647
github.com/jesseduffield/gocui v0.3.1-0.20250421160159-82c9aaeba2b9
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
@@ -33,7 +33,7 @@ require (
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/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-20240719175910-8a7402abbf56
+4 -4
View File
@@ -194,8 +194,8 @@ github.com/jesseduffield/generics v0.0.0-20250406224309-4f541cb84918 h1:meoUDZGF
github.com/jesseduffield/generics v0.0.0-20250406224309-4f541cb84918/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk=
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.20250408140206-7f1bb9232647 h1:XpoXCyLGCB4mczPc1q/8dJefr6OB1xa7jyhH5a5eJqY=
github.com/jesseduffield/gocui v0.3.1-0.20250408140206-7f1bb9232647/go.mod h1:sLIyZ2J42R6idGdtemZzsiR3xY5EF0KsvYEGh3dQv3s=
github.com/jesseduffield/gocui v0.3.1-0.20250421160159-82c9aaeba2b9 h1:k23sCKHCNpAvwJP8Yr16CBUItuarmUHBGH7FaAm2glc=
github.com/jesseduffield/gocui v0.3.1-0.20250421160159-82c9aaeba2b9/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=
@@ -286,8 +286,8 @@ 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/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-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"
}
}
+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)
+17
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 {
@@ -285,3 +294,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 -9
View File
@@ -285,15 +285,14 @@ func (self *CommitCommands) ShowFileContentCmdObj(hash string, filePath string)
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) 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()
+167 -81
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 {
@@ -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`
+116 -85
View File
@@ -8,9 +8,9 @@ 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/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 +28,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 +80,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 +199,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 +214,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 +235,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 +252,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,28 +271,26 @@ 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,
},
}
@@ -310,10 +302,10 @@ func TestGetCommits(t *testing.T) {
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 +314,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 +333,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 +364,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 +377,7 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
{Command: todo.Break},
},
amendFileExists: false,
expectedHash: "",
expectedResult: nil,
},
{
testName: "last command was 'exec'",
@@ -385,7 +389,7 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
},
},
amendFileExists: false,
expectedHash: "",
expectedResult: nil,
},
{
testName: "last command was 'reword'",
@@ -394,7 +398,7 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
{Command: todo.Reword},
},
amendFileExists: false,
expectedHash: "",
expectedResult: nil,
},
{
testName: "'pick' was rescheduled",
@@ -411,7 +415,7 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
},
},
amendFileExists: false,
expectedHash: "",
expectedResult: nil,
},
{
testName: "'pick' was rescheduled, buggy git version",
@@ -436,7 +440,7 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
},
},
amendFileExists: false,
expectedHash: "",
expectedResult: nil,
},
{
testName: "conflicting 'pick' after 'exec'",
@@ -461,7 +465,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 +481,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,8 +492,26 @@ 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 {
@@ -493,10 +519,10 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
common := utils.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 +531,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 +562,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 +578,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)
})
}
}
+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),
+7 -8
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"
)
@@ -157,13 +156,13 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
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
@@ -324,7 +323,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()
+22 -42
View File
@@ -55,7 +55,7 @@ func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int, su
func (self *RebaseCommands) RewordCommitInEditor(commits []*models.Commit, index int) (oscommands.ICmdObj, 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()
})
@@ -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"
}
@@ -420,7 +420,7 @@ func (self *RebaseCommands) BeginInteractiveRebaseForCommitRange(
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,
})
}
@@ -533,41 +533,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 +559,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"
}
+10 -5
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"
)
@@ -97,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
@@ -108,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),
@@ -119,7 +120,7 @@ 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"}},
commitOpts: []models.NewCommitOpts{{Name: "commit", Hash: "123456"}},
commitIndex: 0,
fileName: []string{"test999.txt"},
runner: oscommands.NewFakeRunner(t),
@@ -130,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"},
},
@@ -158,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
}
@@ -8,6 +8,7 @@ import (
"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/sanity-io/litter"
"github.com/stretchr/testify/assert"
)
@@ -26,29 +27,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 +94,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 +118,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 +143,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 +168,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"),
},
@@ -182,11 +185,16 @@ func TestGetReflogCommits(t *testing.T) {
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()
})
+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 {
+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
+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
}
+9
View File
@@ -21,6 +21,9 @@ type ICmdObj interface {
// outputs args vector e.g. ["git", "commit", "-m", "my message"]
Args() []string
// Set a string to be used as stdin for the command.
SetStdin(input string) ICmdObj
AddEnvVars(...string) ICmdObj
GetEnvVars() []string
@@ -131,6 +134,12 @@ func (self *CmdObj) Args() []string {
return self.cmd.Args
}
func (self *CmdObj) SetStdin(input string) ICmdObj {
self.cmd.Stdin = strings.NewReader(input)
return self
}
func (self *CmdObj) AddEnvVars(vars ...string) ICmdObj {
self.cmd.Env = append(self.cmd.Env, vars...)
+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
}
+1 -1
View File
@@ -691,7 +691,7 @@ keybinding:
`)
func BenchmarkMigrationOnLargeConfiguration(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
_, _ = computeMigratedConfig("path doesn't matter", largeConfiguration)
}
}
+27 -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
+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)
})
}
+8
View File
@@ -244,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 forward branches to 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
@@ -395,6 +398,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"`
@@ -478,6 +482,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"`
@@ -821,6 +826,7 @@ 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}} --",
@@ -876,6 +882,7 @@ func GetDefaultConfig() *UserConfig {
PrevBlockAlt2: "<backtab>",
NextBlockAlt2: "<tab>",
JumpToBlock: []string{"1", "2", "3", "4", "5"},
FocusMainView: "0",
NextMatch: "n",
PrevMatch: "N",
StartSearch: "/",
@@ -956,6 +963,7 @@ func GetDefaultConfig() *UserConfig {
RebaseBranch: "r",
RenameBranch: "R",
MergeIntoCurrentBranch: "M",
MoveCommitsToNewBranch: "N",
ViewGitFlowOptions: "i",
FastForward: "f",
CreateTag: "T",
+4
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
}
+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
}
+1 -1
View File
@@ -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",
+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,
+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
+2
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.
+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]
}
+54 -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,7 +284,7 @@ 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
})
+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
}
+16 -32
View File
@@ -25,38 +25,22 @@ func NewContextTree(c *ContextCommon) *ContextTree {
Focusable: true,
}),
),
Files: NewWorkingTreeContext(c),
Submodules: NewSubmodulesContext(c),
Menu: NewMenuContext(c),
Remotes: NewRemotesContext(c),
Worktrees: NewWorktreesContext(c),
RemoteBranches: NewRemoteBranchesContext(c),
LocalCommits: NewLocalCommitsContext(c),
CommitFiles: commitFilesContext,
ReflogCommits: NewReflogCommitsContext(c),
SubCommits: NewSubCommitsContext(c),
Branches: NewBranchesContext(c),
Tags: NewTagsContext(c),
Stash: NewStashContext(c),
Suggestions: NewSuggestionsContext(c),
Normal: NewSimpleContext(
NewBaseContext(NewBaseContextOpts{
Kind: types.MAIN_CONTEXT,
View: c.Views().Main,
WindowName: "main",
Key: NORMAL_MAIN_CONTEXT_KEY,
Focusable: false,
}),
),
NormalSecondary: NewSimpleContext(
NewBaseContext(NewBaseContextOpts{
Kind: types.MAIN_CONTEXT,
View: c.Views().Secondary,
WindowName: "secondary",
Key: NORMAL_SECONDARY_CONTEXT_KEY,
Focusable: false,
}),
),
Files: NewWorkingTreeContext(c),
Submodules: NewSubmodulesContext(c),
Menu: NewMenuContext(c),
Remotes: NewRemotesContext(c),
Worktrees: NewWorktreesContext(c),
RemoteBranches: NewRemoteBranchesContext(c),
LocalCommits: NewLocalCommitsContext(c),
CommitFiles: commitFilesContext,
ReflogCommits: NewReflogCommitsContext(c),
SubCommits: NewSubCommitsContext(c),
Branches: NewBranchesContext(c),
Tags: NewTagsContext(c),
Stash: NewStashContext(c),
Suggestions: NewSuggestionsContext(c),
Normal: NewMainContext(c.Views().Main, "main", NORMAL_MAIN_CONTEXT_KEY, c),
NormalSecondary: NewMainContext(c.Views().Secondary, "secondary", NORMAL_SECONDARY_CONTEXT_KEY, c),
Staging: NewPatchExplorerContext(
c.Views().Staging,
"main",
+3
View File
@@ -54,6 +54,9 @@ func (self *SimpleContext) HandleFocusLost(opts types.OnFocusLostOpts) {
}
}
func (self *SimpleContext) FocusLine() {
}
func (self *SimpleContext) HandleRender() {
if self.handleRenderFunc != nil {
self.handleRenderFunc()
+4 -5
View File
@@ -46,11 +46,11 @@ func NewSubCommitsContext(
return [][]string{}
}
selectedCommitHash := ""
var selectedCommitHashPtr *string
if c.Context().Current().GetKey() == SUB_COMMITS_CONTEXT_KEY {
selectedCommit := viewModel.GetSelected()
if selectedCommit != nil {
selectedCommitHash = selectedCommit.Hash
selectedCommitHashPtr = selectedCommit.HashPtr()
}
}
branches := []*models.Branch{}
@@ -72,12 +72,11 @@ func NewSubCommitsContext(
c.UserConfig().Gui.ShortTimeFormat,
time.Now(),
c.UserConfig().Git.ParseEmoji,
selectedCommitHash,
selectedCommitHashPtr,
startIdx,
endIdx,
shouldShowGraph(c),
git_commands.NewNullBisectInfo(),
false,
)
}
@@ -222,7 +221,7 @@ func (self *SubCommitsContext) RefForAdjustingLineNumberInDiff() string {
if commits == nil {
return ""
}
return commits[0].Hash
return commits[0].Hash()
}
func (self *SubCommitsContext) ModelSearchResults(searchStr string, caseSensitive bool) []gocui.SearchPosition {
+2 -1
View File
@@ -3,6 +3,7 @@ package traits
import (
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
type RangeSelectMode int
@@ -85,7 +86,7 @@ func (self *ListCursor) clampValue(value int) int {
clampedValue := -1
length := self.getLength()
if length > 0 {
clampedValue = utils.Clamp(value, 0, length-1)
clampedValue = lo.Clamp(value, 0, length-1)
}
return clampedValue
+34 -4
View File
@@ -27,12 +27,11 @@ func (gui *Gui) resetHelpersAndControllers() {
helperCommon := gui.c
recordDirectoryHelper := helpers.NewRecordDirectoryHelper(helperCommon)
reposHelper := helpers.NewRecentReposHelper(helperCommon, recordDirectoryHelper, gui.onNewRepo)
refsHelper := helpers.NewRefsHelper(helperCommon)
rebaseHelper := helpers.NewMergeAndRebaseHelper(helperCommon)
refsHelper := helpers.NewRefsHelper(helperCommon, rebaseHelper)
suggestionsHelper := helpers.NewSuggestionsHelper(helperCommon)
worktreeHelper := helpers.NewWorktreeHelper(helperCommon, reposHelper, refsHelper, suggestionsHelper)
rebaseHelper := helpers.NewMergeAndRebaseHelper(helperCommon, refsHelper)
setCommitSummary := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitMessage })
setCommitDescription := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitDescription })
getCommitSummary := func() string {
@@ -180,7 +179,8 @@ func (gui *Gui) resetHelpersAndControllers() {
globalController := controllers.NewGlobalController(common)
contextLinesController := controllers.NewContextLinesController(common)
renameSimilarityThresholdController := controllers.NewRenameSimilarityThresholdController(common)
verticalScrollControllerFactory := controllers.NewVerticalScrollControllerFactory(common, &gui.viewBufferManagerMap)
verticalScrollControllerFactory := controllers.NewVerticalScrollControllerFactory(common)
viewSelectionControllerFactory := controllers.NewViewSelectionControllerFactory(common)
branchesController := controllers.NewBranchesController(common)
gitFlowController := controllers.NewGitFlowController(common)
@@ -189,6 +189,8 @@ func (gui *Gui) resetHelpersAndControllers() {
patchExplorerControllerFactory := controllers.NewPatchExplorerControllerFactory(common)
stagingController := controllers.NewStagingController(common, gui.State.Contexts.Staging, gui.State.Contexts.StagingSecondary, false)
stagingSecondaryController := controllers.NewStagingController(common, gui.State.Contexts.StagingSecondary, gui.State.Contexts.Staging, true)
mainViewController := controllers.NewMainViewController(common, gui.State.Contexts.Normal, gui.State.Contexts.NormalSecondary)
secondaryViewController := controllers.NewMainViewController(common, gui.State.Contexts.NormalSecondary, gui.State.Contexts.Normal)
patchBuildingController := controllers.NewPatchBuildingController(common)
snakeController := controllers.NewSnakeController(common)
reflogCommitsController := controllers.NewReflogCommitsController(common)
@@ -263,6 +265,22 @@ func (gui *Gui) resetHelpersAndControllers() {
))
}
for _, context := range []types.Context{
gui.State.Contexts.Files,
gui.State.Contexts.Branches,
gui.State.Contexts.RemoteBranches,
gui.State.Contexts.Tags,
gui.State.Contexts.LocalCommits,
gui.State.Contexts.ReflogCommits,
gui.State.Contexts.SubCommits,
gui.State.Contexts.CommitFiles,
gui.State.Contexts.Stash,
} {
controllers.AttachControllers(context, controllers.NewSwitchToFocusedMainViewController(
common, context,
))
}
for _, context := range []controllers.ContainsCommits{
gui.State.Contexts.LocalCommits,
gui.State.Contexts.ReflogCommits,
@@ -306,6 +324,18 @@ func (gui *Gui) resetHelpersAndControllers() {
mergeConflictsController,
)
controllers.AttachControllers(gui.State.Contexts.Normal,
mainViewController,
verticalScrollControllerFactory.Create(gui.State.Contexts.Normal),
viewSelectionControllerFactory.Create(gui.State.Contexts.Normal),
)
controllers.AttachControllers(gui.State.Contexts.NormalSecondary,
secondaryViewController,
verticalScrollControllerFactory.Create(gui.State.Contexts.NormalSecondary),
viewSelectionControllerFactory.Create(gui.State.Contexts.NormalSecondary),
)
controllers.AttachControllers(gui.State.Contexts.Files,
filesController,
)
+1
View File
@@ -7,6 +7,7 @@ func AttachControllers(context types.Context, controllers ...types.IController)
context.AddKeybindingsFn(controller.GetKeybindings)
context.AddMouseKeybindingsFn(controller.GetMouseKeybindings)
context.AddOnClickFn(controller.GetOnClick())
context.AddOnClickFocusedMainViewFn(controller.GetOnClickFocusedMainView())
context.AddOnRenderToMainFn(controller.GetOnRenderToMain())
context.AddOnFocusFn(controller.GetOnFocus())
context.AddOnFocusLostFn(controller.GetOnFocusLost())
+4
View File
@@ -19,6 +19,10 @@ func (self *baseController) GetOnClick() func() error {
return nil
}
func (self *baseController) GetOnClickFocusedMainView() func(mainViewName string, clickedLineIdx int) error {
return nil
}
func (self *baseController) GetOnRenderToMain() func() {
return nil
}
+22 -15
View File
@@ -80,6 +80,17 @@ func (self *BasicCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.CreateNewBranchFromCommit,
},
{
// Putting this in BasicCommitsController even though we really only want it in the commits
// panel. But I find it important that this ends up next to "New Branch", and I couldn't
// find another way to achieve this. It's not such a big deal to have it in subcommits and
// reflog too, I'd say.
Key: opts.GetKey(opts.Config.Branches.MoveCommitsToNewBranch),
Handler: self.c.Helpers().Refs.MoveCommitsToNewBranch,
GetDisabledReason: self.c.Helpers().Refs.CanMoveCommitsToNewBranch,
Description: self.c.Tr.MoveCommitsToNewBranch,
Tooltip: self.c.Tr.MoveCommitsToNewBranchTooltip,
},
{
Key: opts.GetKey(opts.Config.Commits.ViewResetOptions),
Handler: self.withItem(self.createResetMenu),
@@ -142,7 +153,7 @@ func (self *BasicCommitsController) getCommitMessageBody(hash string) string {
}
func (self *BasicCommitsController) copyCommitAttribute(commit *models.Commit) error {
commitMessageBody := self.getCommitMessageBody(commit.Hash)
commitMessageBody := self.getCommitMessageBody(commit.Hash())
var commitMessageBodyDisabled *types.DisabledReason
if commitMessageBody == "" {
commitMessageBodyDisabled = &types.DisabledReason{
@@ -224,16 +235,16 @@ func (self *BasicCommitsController) copyCommitAttribute(commit *models.Commit) e
func (self *BasicCommitsController) copyCommitHashToClipboard(commit *models.Commit) error {
self.c.LogAction(self.c.Tr.Actions.CopyCommitHashToClipboard)
if err := self.c.OS().CopyToClipboard(commit.Hash); err != nil {
if err := self.c.OS().CopyToClipboard(commit.Hash()); err != nil {
return err
}
self.c.Toast(fmt.Sprintf("'%s' %s", commit.Hash, self.c.Tr.CopiedToClipboard))
self.c.Toast(fmt.Sprintf("'%s' %s", commit.Hash(), self.c.Tr.CopiedToClipboard))
return nil
}
func (self *BasicCommitsController) copyCommitURLToClipboard(commit *models.Commit) error {
url, err := self.c.Helpers().Host.GetCommitURL(commit.Hash)
url, err := self.c.Helpers().Host.GetCommitURL(commit.Hash())
if err != nil {
return err
}
@@ -248,7 +259,7 @@ func (self *BasicCommitsController) copyCommitURLToClipboard(commit *models.Comm
}
func (self *BasicCommitsController) copyCommitDiffToClipboard(commit *models.Commit) error {
diff, err := self.c.Git().Commit.GetCommitDiff(commit.Hash)
diff, err := self.c.Git().Commit.GetCommitDiff(commit.Hash())
if err != nil {
return err
}
@@ -263,7 +274,7 @@ func (self *BasicCommitsController) copyCommitDiffToClipboard(commit *models.Com
}
func (self *BasicCommitsController) copyAuthorToClipboard(commit *models.Commit) error {
author, err := self.c.Git().Commit.GetCommitAuthor(commit.Hash)
author, err := self.c.Git().Commit.GetCommitAuthor(commit.Hash())
if err != nil {
return err
}
@@ -280,7 +291,7 @@ func (self *BasicCommitsController) copyAuthorToClipboard(commit *models.Commit)
}
func (self *BasicCommitsController) copyCommitMessageToClipboard(commit *models.Commit) error {
message, err := self.c.Git().Commit.GetCommitMessage(commit.Hash)
message, err := self.c.Git().Commit.GetCommitMessage(commit.Hash())
if err != nil {
return err
}
@@ -305,7 +316,7 @@ func (self *BasicCommitsController) copyCommitMessageBodyToClipboard(commitMessa
}
func (self *BasicCommitsController) copyCommitSubjectToClipboard(commit *models.Commit) error {
message, err := self.c.Git().Commit.GetCommitSubject(commit.Hash)
message, err := self.c.Git().Commit.GetCommitSubject(commit.Hash())
if err != nil {
return err
}
@@ -332,7 +343,7 @@ func (self *BasicCommitsController) copyCommitTagsToClipboard(commit *models.Com
}
func (self *BasicCommitsController) openInBrowser(commit *models.Commit) error {
url, err := self.c.Helpers().Host.GetCommitURL(commit.Hash)
url, err := self.c.Helpers().Host.GetCommitURL(commit.Hash())
if err != nil {
return err
}
@@ -350,7 +361,7 @@ func (self *BasicCommitsController) newBranch(commit *models.Commit) error {
}
func (self *BasicCommitsController) createResetMenu(commit *models.Commit) error {
return self.c.Helpers().Refs.CreateGitResetMenu(commit.Hash)
return self.c.Helpers().Refs.CreateGitResetMenu(commit.Hash())
}
func (self *BasicCommitsController) checkout(commit *models.Commit) error {
@@ -363,13 +374,9 @@ func (self *BasicCommitsController) copyRange(*models.Commit) error {
func (self *BasicCommitsController) canCopyCommits(selectedCommits []*models.Commit, startIdx int, endIdx int) *types.DisabledReason {
for _, commit := range selectedCommits {
if commit.Hash == "" {
if commit.Hash() == "" {
return &types.DisabledReason{Text: self.c.Tr.CannotCherryPickNonCommit, ShowErrorInPanel: true}
}
if commit.IsMerge() {
return &types.DisabledReason{Text: self.c.Tr.CannotCherryPickMergeCommit, ShowErrorInPanel: true}
}
}
return nil
+7 -7
View File
@@ -69,7 +69,7 @@ func (self *BisectController) openMidBisectMenu(info *git_commands.BisectInfo, c
// Originally we were allowing the user to, from the bisect menu, select whether
// they were talking about the selected commit or the current bisect commit,
// and that was a bit confusing (and required extra keypresses).
selectCurrentAfter := info.GetCurrentHash() == "" || info.GetCurrentHash() == commit.Hash
selectCurrentAfter := info.GetCurrentHash() == "" || info.GetCurrentHash() == commit.Hash()
// we need to wait to reselect if our bisect commits aren't ancestors of our 'start'
// ref, because we'll be reloading our commits in that case.
waitToReselect := selectCurrentAfter && !self.c.Git().Bisect.ReachableFromStart(info)
@@ -79,7 +79,7 @@ func (self *BisectController) openMidBisectMenu(info *git_commands.BisectInfo, c
// use the selected commit in that case.
bisecting := info.GetCurrentHash() != ""
hashToMark := lo.Ternary(bisecting, info.GetCurrentHash(), commit.Hash)
hashToMark := lo.Ternary(bisecting, info.GetCurrentHash(), commit.Hash())
shortHashToMark := utils.ShortHash(hashToMark)
// For marking a commit as bad, when we're not already bisecting, we require
@@ -131,12 +131,12 @@ func (self *BisectController) openMidBisectMenu(info *git_commands.BisectInfo, c
Key: 's',
},
}
if info.GetCurrentHash() != "" && info.GetCurrentHash() != commit.Hash {
if info.GetCurrentHash() != "" && info.GetCurrentHash() != commit.Hash() {
menuItems = append(menuItems, lo.ToPtr(types.MenuItem{
Label: fmt.Sprintf(self.c.Tr.Bisect.SkipSelected, commit.ShortHash()),
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.BisectSkip)
if err := self.c.Git().Bisect.Skip(commit.Hash); err != nil {
if err := self.c.Git().Bisect.Skip(commit.Hash()); err != nil {
return err
}
@@ -172,7 +172,7 @@ func (self *BisectController) openStartBisectMenu(info *git_commands.BisectInfo,
return err
}
if err := self.c.Git().Bisect.Mark(commit.Hash, info.NewTerm()); err != nil {
if err := self.c.Git().Bisect.Mark(commit.Hash(), info.NewTerm()); err != nil {
return err
}
@@ -189,7 +189,7 @@ func (self *BisectController) openStartBisectMenu(info *git_commands.BisectInfo,
return err
}
if err := self.c.Git().Bisect.Mark(commit.Hash, info.OldTerm()); err != nil {
if err := self.c.Git().Bisect.Mark(commit.Hash(), info.OldTerm()); err != nil {
return err
}
@@ -292,7 +292,7 @@ func (self *BisectController) selectCurrentBisectCommit() {
if info.GetCurrentHash() != "" {
// find index of commit with that hash, move cursor to that.
for i, commit := range self.c.Model().Commits {
if commit.Hash == info.GetCurrentHash() {
if commit.Hash() == info.GetCurrentHash() {
self.context().SetSelection(i)
self.context().HandleFocus(types.OnFocusOpts{})
break
@@ -57,6 +57,13 @@ func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*ty
Description: self.c.Tr.NewBranch,
DisplayOnScreen: true,
},
{
Key: opts.GetKey(opts.Config.Branches.MoveCommitsToNewBranch),
Handler: self.c.Helpers().Refs.MoveCommitsToNewBranch,
GetDisabledReason: self.c.Helpers().Refs.CanMoveCommitsToNewBranch,
Description: self.c.Tr.MoveCommitsToNewBranch,
Tooltip: self.c.Tr.MoveCommitsToNewBranchTooltip,
},
{
Key: opts.GetKey(opts.Config.Branches.CreatePullRequest),
Handler: self.withItem(self.handleCreatePullRequest),
+23 -25
View File
@@ -2,6 +2,7 @@ package controllers
import (
"errors"
"fmt"
"path/filepath"
"strings"
@@ -12,6 +13,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/constants"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
@@ -135,17 +137,6 @@ func (self *CommitFilesController) GetKeybindings(opts types.KeybindingsOpts) []
return bindings
}
func (self *CommitFilesController) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding {
return []*gocui.ViewMouseBinding{
{
ViewName: "patchBuilding",
Key: gocui.MouseLeft,
Handler: self.onClickMain,
FocusedView: self.context().GetViewName(),
},
}
}
func (self *CommitFilesController) context() *context.CommitFilesContext {
return self.c.Contexts().CommitFiles
}
@@ -163,13 +154,8 @@ func (self *CommitFilesController) GetOnRenderToMain() func() {
cmdObj := self.c.Git().WorkingTree.ShowFileDiffCmdObj(from, to, reverse, node.GetPath(), false)
task := types.NewRunPtyTask(cmdObj.GetCmd())
pair := self.c.MainViewPairs().Normal
if node.File != nil {
pair = self.c.MainViewPairs().PatchBuilding
}
self.c.RenderToMainViews(types.RefreshMainOpts{
Pair: pair,
Pair: self.c.MainViewPairs().Normal,
Main: &types.ViewUpdateOpts{
Title: self.c.Tr.Patch,
SubTitle: self.c.Helpers().Diff.IgnoringWhitespaceSubTitle(),
@@ -180,14 +166,6 @@ func (self *CommitFilesController) GetOnRenderToMain() func() {
}
}
func (self *CommitFilesController) onClickMain(opts gocui.ViewMouseBindingOpts) error {
node := self.context().GetSelected()
if node == nil {
return nil
}
return self.enterCommitFile(node, types.OnFocusOpts{ClickedWindowName: "main", ClickedViewLineIdx: opts.Y})
}
func (self *CommitFilesController) copyDiffToClipboard(path string, toastMessage string) error {
from, to := self.context().GetFromAndToForDiff()
from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(from)
@@ -404,6 +382,11 @@ func (self *CommitFilesController) openDiffTool(node *filetree.CommitFileNode) e
}
func (self *CommitFilesController) toggleForPatch(selectedNodes []*filetree.CommitFileNode) error {
if self.c.AppState.DiffContextSize == 0 {
return fmt.Errorf(self.c.Tr.Actions.NotEnoughContextToStage,
keybindings.Label(self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView))
}
toggle := func() error {
return self.c.WithWaitingStatus(self.c.Tr.UpdatingPatch, func(gocui.Task) error {
if !self.c.Git().Patch.PatchBuilder.Active() {
@@ -495,6 +478,11 @@ func (self *CommitFilesController) enterCommitFile(node *filetree.CommitFileNode
return self.handleToggleCommitFileDirCollapsed(node)
}
if self.c.AppState.DiffContextSize == 0 {
return fmt.Errorf(self.c.Tr.Actions.NotEnoughContextToStage,
keybindings.Label(self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView))
}
enterTheFile := func() error {
if !self.c.Git().Patch.PatchBuilder.Active() {
if err := self.startPatchBuilder(); err != nil {
@@ -555,6 +543,16 @@ func (self *CommitFilesController) expandAll() error {
return nil
}
func (self *CommitFilesController) GetOnClickFocusedMainView() func(mainViewName string, clickedLineIdx int) error {
return func(mainViewName string, clickedLineIdx int) error {
node := self.getSelectedItem()
if node != nil && node.File != nil {
return self.enterCommitFile(node, types.OnFocusOpts{ClickedWindowName: mainViewName, ClickedViewLineIdx: clickedLineIdx})
}
return nil
}
}
// NOTE: these functions are identical to those in files_controller.go (except for types) and
// could also be cleaned up with some generics
func normalisedSelectedCommitFileNodes(selectedNodes []*filetree.CommitFileNode) []*filetree.CommitFileNode {
@@ -22,6 +22,8 @@ var CONTEXT_KEYS_SHOWING_DIFFS = []types.ContextKey{
context.STAGING_SECONDARY_CONTEXT_KEY,
context.PATCH_BUILDING_MAIN_CONTEXT_KEY,
context.PATCH_BUILDING_SECONDARY_CONTEXT_KEY,
context.NORMAL_MAIN_CONTEXT_KEY,
context.NORMAL_SECONDARY_CONTEXT_KEY,
}
type ContextLinesController struct {
@@ -97,7 +99,7 @@ func (self *ContextLinesController) applyChange() error {
self.c.Toast(fmt.Sprintf(self.c.Tr.DiffContextSizeChanged, self.c.AppState.DiffContextSize))
self.c.SaveAppStateAndLogError()
currentContext := self.c.Context().CurrentStatic()
currentContext := self.currentSidePanel()
switch currentContext.GetKey() {
// we make an exception for our staging and patch building contexts because they actually need to refresh their state afterwards.
case context.PATCH_BUILDING_MAIN_CONTEXT_KEY:
@@ -121,6 +123,16 @@ func (self *ContextLinesController) checkCanChangeContext() error {
func (self *ContextLinesController) isShowingDiff() bool {
return lo.Contains(
CONTEXT_KEYS_SHOWING_DIFFS,
self.c.Context().CurrentStatic().GetKey(),
self.currentSidePanel().GetKey(),
)
}
func (self *ContextLinesController) currentSidePanel() types.Context {
currentContext := self.c.Context().CurrentStatic()
if currentContext.GetKey() == context.NORMAL_MAIN_CONTEXT_KEY ||
currentContext.GetKey() == context.NORMAL_SECONDARY_CONTEXT_KEY {
return currentContext.GetParentContext()
}
return currentContext
}
@@ -5,7 +5,6 @@ import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
@@ -44,7 +43,7 @@ func (self *CustomPatchOptionsMenuAction) Call() error {
},
}
if self.c.Git().Patch.PatchBuilder.CanRebase && self.c.Git().Status.WorkingTreeState() == enums.REBASE_MODE_NONE {
if self.c.Git().Patch.PatchBuilder.CanRebase && self.c.Git().Status.WorkingTreeState().None() {
menuItems = append(menuItems, []*types.MenuItem{
{
Label: fmt.Sprintf(self.c.Tr.RemovePatchFromOriginalCommit, self.c.Git().Patch.PatchBuilder.To),
@@ -68,7 +67,7 @@ func (self *CustomPatchOptionsMenuAction) Call() error {
if self.c.Context().Current().GetKey() == self.c.Contexts().LocalCommits.GetKey() {
selectedCommit := self.c.Contexts().LocalCommits.GetSelected()
if selectedCommit != nil && self.c.Git().Patch.PatchBuilder.To != selectedCommit.Hash {
if selectedCommit != nil && self.c.Git().Patch.PatchBuilder.To != selectedCommit.Hash() {
var disabledReason *types.DisabledReason
if self.c.Contexts().LocalCommits.AreMultipleItemsSelected() {
@@ -81,7 +80,7 @@ func (self *CustomPatchOptionsMenuAction) Call() error {
append(
[]*types.MenuItem{
{
Label: fmt.Sprintf(self.c.Tr.MovePatchToSelectedCommit, selectedCommit.Hash),
Label: fmt.Sprintf(self.c.Tr.MovePatchToSelectedCommit, selectedCommit.Hash()),
Tooltip: self.c.Tr.MovePatchToSelectedCommitTooltip,
OnPress: self.handleMovePatchToSelectedCommit,
Key: 'm',
@@ -107,7 +106,7 @@ func (self *CustomPatchOptionsMenuAction) Call() error {
func (self *CustomPatchOptionsMenuAction) getPatchCommitIndex() int {
for index, commit := range self.c.Model().Commits {
if commit.Hash == self.c.Git().Patch.PatchBuilder.To {
if commit.Hash() == self.c.Git().Patch.PatchBuilder.To {
return index
}
}
@@ -115,7 +114,7 @@ func (self *CustomPatchOptionsMenuAction) getPatchCommitIndex() int {
}
func (self *CustomPatchOptionsMenuAction) validateNormalWorkingTreeState() (bool, error) {
if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE {
if self.c.Git().Status.WorkingTreeState().Any() {
return false, errors.New(self.c.Tr.CantPatchWhileRebasingError)
}
return true, nil
+30 -55
View File
@@ -208,36 +208,12 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types
func (self *FilesController) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding {
return []*gocui.ViewMouseBinding{
{
ViewName: "main",
Key: gocui.MouseLeft,
Handler: self.onClickMain,
FocusedView: self.context().GetViewName(),
},
{
ViewName: "patchBuilding",
Key: gocui.MouseLeft,
Handler: self.onClickMain,
FocusedView: self.context().GetViewName(),
},
{
ViewName: "mergeConflicts",
Key: gocui.MouseLeft,
Handler: self.onClickMain,
FocusedView: self.context().GetViewName(),
},
{
ViewName: "secondary",
Key: gocui.MouseLeft,
Handler: self.onClickSecondary,
FocusedView: self.context().GetViewName(),
},
{
ViewName: "patchBuildingSecondary",
Key: gocui.MouseLeft,
Handler: self.onClickSecondary,
FocusedView: self.context().GetViewName(),
},
}
}
@@ -304,11 +280,6 @@ func (self *FilesController) GetOnRenderToMain() func() {
self.c.Helpers().MergeConflicts.ResetMergeState()
pair := self.c.MainViewPairs().Normal
if node.File != nil {
pair = self.c.MainViewPairs().Staging
}
split := self.c.UserConfig().Gui.SplitDiff == "always" || (node.GetHasUnstagedChanges() && node.GetHasStagedChanges())
mainShowsStaged := !split && node.GetHasStagedChanges()
@@ -318,7 +289,7 @@ func (self *FilesController) GetOnRenderToMain() func() {
title = self.c.Tr.StagedChanges
}
refreshOpts := types.RefreshMainOpts{
Pair: pair,
Pair: self.c.MainViewPairs().Normal,
Main: &types.ViewUpdateOpts{
Task: types.NewRunPtyTask(cmdObj.GetCmd()),
SubTitle: self.c.Helpers().Diff.IgnoringWhitespaceSubTitle(),
@@ -352,6 +323,16 @@ func (self *FilesController) GetOnClick() func() error {
})
}
func (self *FilesController) GetOnClickFocusedMainView() func(mainViewName string, clickedLineIdx int) error {
return func(mainViewName string, clickedLineIdx int) error {
node := self.getSelectedItem()
if node != nil && node.File != nil {
return self.EnterFile(types.OnFocusOpts{ClickedWindowName: mainViewName, ClickedViewLineIdx: clickedLineIdx})
}
return nil
}
}
// if we are dealing with a status for which there is no key in this map,
// then we won't optimistically render: we'll just let `git status` tell
// us what the new status is.
@@ -574,7 +555,8 @@ func (self *FilesController) EnterFile(opts types.OnFocusOpts) error {
return self.handleNonInlineConflict(file)
}
self.c.Context().Push(self.c.Contexts().Staging, opts)
context := lo.Ternary(opts.ClickedWindowName == "secondary", self.c.Contexts().StagingSecondary, self.c.Contexts().Staging)
self.c.Context().Push(context, opts)
return nil
}
@@ -829,12 +811,12 @@ func (self *FilesController) handleAmendCommitPress() error {
func (self *FilesController) isResolvingConflicts() bool {
commits := self.c.Model().Commits
for _, c := range commits {
if c.Status != models.StatusRebasing {
break
}
if c.Action == models.ActionConflict {
if c.Status == models.StatusConflicted {
return true
}
if !c.IsTODO() {
break
}
}
return false
}
@@ -1205,32 +1187,25 @@ func (self *FilesController) onClickMain(opts gocui.ViewMouseBindingOpts) error
return self.EnterFile(types.OnFocusOpts{ClickedWindowName: "main", ClickedViewLineIdx: opts.Y})
}
func (self *FilesController) onClickSecondary(opts gocui.ViewMouseBindingOpts) error {
return self.EnterFile(types.OnFocusOpts{ClickedWindowName: "secondary", ClickedViewLineIdx: opts.Y})
}
func (self *FilesController) fetch() error {
return self.c.WithWaitingStatus(self.c.Tr.FetchingStatus, func(task gocui.Task) error {
if err := self.fetchAux(task); err != nil {
return err
self.c.LogAction("Fetch")
err := self.c.Git().Sync.Fetch(task)
if err != nil && strings.Contains(err.Error(), "exit status 128") {
return errors.New(self.c.Tr.PassUnameWrong)
}
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
_ = self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.SYNC})
if err == nil {
err = self.c.Helpers().BranchesHelper.AutoForwardBranches()
}
return err
})
}
func (self *FilesController) fetchAux(task gocui.Task) (err error) {
self.c.LogAction("Fetch")
err = self.c.Git().Sync.Fetch(task)
if err != nil && strings.Contains(err.Error(), "exit status 128") {
return errors.New(self.c.Tr.PassUnameWrong)
}
_ = self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.ASYNC})
return err
}
// Couldn't think of a better term than 'normalised'. Alas.
// The idea is that when you select a range of nodes, you will often have both
// a node and its parent node selected. If we are trying to discard changes to the
+15 -5
View File
@@ -35,11 +35,12 @@ func (self *GlobalController) GetKeybindings(opts types.KeybindingsOpts) []*type
OpensMenu: true,
},
{
Key: opts.GetKey(opts.Config.Universal.CreateRebaseOptionsMenu),
Handler: opts.Guards.NoPopupPanel(self.c.Helpers().MergeAndRebase.CreateRebaseOptionsMenu),
Description: self.c.Tr.ViewMergeRebaseOptions,
Tooltip: self.c.Tr.ViewMergeRebaseOptionsTooltip,
OpensMenu: true,
Key: opts.GetKey(opts.Config.Universal.CreateRebaseOptionsMenu),
Handler: opts.Guards.NoPopupPanel(self.c.Helpers().MergeAndRebase.CreateRebaseOptionsMenu),
Description: self.c.Tr.ViewMergeRebaseOptions,
Tooltip: self.c.Tr.ViewMergeRebaseOptionsTooltip,
OpensMenu: true,
GetDisabledReason: self.canShowRebaseOptions,
},
{
Key: opts.GetKey(opts.Config.Universal.Refresh),
@@ -191,3 +192,12 @@ func (self *GlobalController) escape() error {
func (self *GlobalController) toggleWhitespace() error {
return (&ToggleWhitespaceAction{c: self.c}).Call()
}
func (self *GlobalController) canShowRebaseOptions() *types.DisabledReason {
if self.c.Model().WorkingTreeStateAtLastCommitRefresh.None() {
return &types.DisabledReason{
Text: self.c.Tr.NotMergingOrRebasing,
}
}
return nil
}
@@ -2,6 +2,7 @@ package helpers
import (
"errors"
"fmt"
"strings"
"github.com/jesseduffield/gocui"
@@ -263,3 +264,34 @@ func (self *BranchesHelper) deleteRemoteBranches(remoteBranches []*models.Remote
}
return nil
}
func (self *BranchesHelper) AutoForwardBranches() error {
if self.c.UserConfig().Git.AutoForwardBranches == "none" {
return nil
}
allBranches := self.c.UserConfig().Git.AutoForwardBranches == "allBranches"
branches := self.c.Model().Branches
updateCommands := ""
// The first branch is the currently checked out branch; skip it
for _, branch := range branches[1:] {
if branch.RemoteBranchStoredLocally() && (allBranches || lo.Contains(self.c.UserConfig().Git.MainBranches, branch.Name)) {
isStrictlyBehind := branch.IsBehindForPull() && !branch.IsAheadForPull()
if isStrictlyBehind {
updateCommands += fmt.Sprintf("update %s %s %s\n", branch.FullRefName(), branch.FullUpstreamRefName(), branch.CommitHash)
}
}
}
if updateCommands == "" {
return nil
}
self.c.LogAction(self.c.Tr.Actions.AutoForwardBranches)
self.c.LogCommand(strings.TrimRight(updateCommands, "\n"), false)
err := self.c.Git().Branch.UpdateBranchRefs(updateCommands)
_ = self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES}, Mode: types.SYNC})
return err
}
@@ -44,7 +44,7 @@ func (self *CherryPickHelper) CopyRange(commitsList []*models.Commit, context ty
commitSet := self.getData().SelectedHashSet()
allCommitsCopied := lo.EveryBy(commitsList[startIdx:endIdx+1], func(commit *models.Commit) bool {
return commitSet.Includes(commit.Hash)
return commitSet.Includes(commit.Hash())
})
// if all selected commits are already copied, we'll uncopy them
@@ -77,41 +77,23 @@ func (self *CherryPickHelper) Paste() error {
"numCommits": strconv.Itoa(len(self.getData().CherryPickedCommits)),
}),
HandleConfirm: func() error {
isInRebase, err := self.c.Git().Status.IsInInteractiveRebase()
if err != nil {
return err
}
if isInRebase {
if err := self.c.Git().Rebase.CherryPickCommitsDuringRebase(self.getData().CherryPickedCommits); err != nil {
return err
}
err = self.c.Refresh(types.RefreshOptions{
Mode: types.SYNC, Scope: []types.RefreshableView{types.REBASE_COMMITS},
})
if err != nil {
return err
}
return self.Reset()
}
return self.c.WithWaitingStatus(self.c.Tr.CherryPickingStatus, func(gocui.Task) error {
self.c.LogAction(self.c.Tr.Actions.CherryPick)
err := self.c.Git().Rebase.CherryPickCommits(self.getData().CherryPickedCommits)
err = self.rebaseHelper.CheckMergeOrRebase(err)
result := self.c.Git().Rebase.CherryPickCommits(self.getData().CherryPickedCommits)
err := self.rebaseHelper.CheckMergeOrRebase(result)
if err != nil {
return err
return result
}
// If we're in an interactive rebase at this point, it must
// If we're in the cherry-picking state at this point, it must
// be because there were conflicts. Don't clear the copied
// commits in this case, since we might want to abort and
// try pasting them again.
isInRebase, err = self.c.Git().Status.IsInInteractiveRebase()
if err != nil {
return err
// commits in this case, since we might want to abort and try
// pasting them again.
isInCherryPick, result := self.c.Git().Status.IsInCherryPick()
if result != nil {
return result
}
if !isInRebase {
if !isInCherryPick {
self.getData().DidPaste = true
self.rerender()
}
+1 -1
View File
@@ -66,7 +66,7 @@ func (self *DiffHelper) GetUpdateTaskForRenderingCommitsDiff(commit *models.Comm
return task
}
cmdObj := self.c.Git().Commit.ShowCmdObj(commit.Hash, self.c.Modes().Filtering.GetPath())
cmdObj := self.c.Git().Commit.ShowCmdObj(commit.Hash(), self.c.Modes().Filtering.GetPath())
return types.NewRunPtyTask(cmdObj.GetCmd())
}
+1 -1
View File
@@ -342,6 +342,6 @@ func (self *FixupHelper) blameAddedLines(commits []*models.Commit, addedLineHunk
func (self *FixupHelper) findCommit(commits []*models.Commit, hash string) (*models.Commit, int, bool) {
return lo.FindIndexOf(commits, func(commit *models.Commit) bool {
return commit.Hash == hash
return commit.Hash() == hash
})
}
@@ -10,7 +10,6 @@ import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
@@ -18,17 +17,14 @@ import (
)
type MergeAndRebaseHelper struct {
c *HelperCommon
refsHelper *RefsHelper
c *HelperCommon
}
func NewMergeAndRebaseHelper(
c *HelperCommon,
refsHelper *RefsHelper,
) *MergeAndRebaseHelper {
return &MergeAndRebaseHelper{
c: c,
refsHelper: refsHelper,
c: c,
}
}
@@ -51,7 +47,7 @@ func (self *MergeAndRebaseHelper) CreateRebaseOptionsMenu() error {
{option: REBASE_OPTION_ABORT, key: 'a'},
}
if self.c.Git().Status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
if self.c.Git().Status.WorkingTreeState().CanSkip() {
options = append(options, optionAndKey{
option: REBASE_OPTION_SKIP, key: 's',
})
@@ -67,13 +63,7 @@ func (self *MergeAndRebaseHelper) CreateRebaseOptionsMenu() error {
}
})
var title string
if self.c.Git().Status.WorkingTreeState() == enums.REBASE_MODE_MERGING {
title = self.c.Tr.MergeOptionsTitle
} else {
title = self.c.Tr.RebaseOptionsTitle
}
title := self.c.Git().Status.WorkingTreeState().OptionsMenuTitle(self.c.Tr)
return self.c.Menu(types.CreateMenuOptions{Title: title, Items: menuItems})
}
@@ -84,12 +74,13 @@ func (self *MergeAndRebaseHelper) ContinueRebase() error {
func (self *MergeAndRebaseHelper) genericMergeCommand(command string) error {
status := self.c.Git().Status.WorkingTreeState()
if status != enums.REBASE_MODE_MERGING && status != enums.REBASE_MODE_REBASING {
if status.None() {
return errors.New(self.c.Tr.NotMergingOrRebasing)
}
self.c.LogAction(fmt.Sprintf("Merge/Rebase: %s", command))
if status == enums.REBASE_MODE_REBASING {
effectiveStatus := status.Effective()
if effectiveStatus == models.WORKING_TREE_STATE_REBASING {
todoFile, err := os.ReadFile(
filepath.Join(self.c.Git().RepoPaths.WorktreeGitDirPath(), "rebase-merge/git-rebase-todo"),
)
@@ -103,23 +94,15 @@ func (self *MergeAndRebaseHelper) genericMergeCommand(command string) error {
}
}
commandType := ""
switch status {
case enums.REBASE_MODE_MERGING:
commandType = "merge"
case enums.REBASE_MODE_REBASING:
commandType = "rebase"
default:
// shouldn't be possible to land here
}
commandType := status.CommandName()
// we should end up with a command like 'git merge --continue'
// it's impossible for a rebase to require a commit so we'll use a subprocess only if it's a merge
needsSubprocess := (status == enums.REBASE_MODE_MERGING && command != REBASE_OPTION_ABORT && self.c.UserConfig().Git.Merging.ManualCommit) ||
needsSubprocess := (effectiveStatus == models.WORKING_TREE_STATE_MERGING && command != REBASE_OPTION_ABORT && self.c.UserConfig().Git.Merging.ManualCommit) ||
// but we'll also use a subprocess if we have exec todos; those are likely to be lengthy build
// tasks whose output the user will want to see in the terminal
(status == enums.REBASE_MODE_REBASING && command != REBASE_OPTION_ABORT && self.hasExecTodos())
(effectiveStatus == models.WORKING_TREE_STATE_REBASING && command != REBASE_OPTION_ABORT && self.hasExecTodos())
if needsSubprocess {
// TODO: see if we should be calling more of the code from self.Git.Rebase.GenericMergeOrRebaseAction
@@ -136,7 +119,7 @@ func (self *MergeAndRebaseHelper) genericMergeCommand(command string) error {
func (self *MergeAndRebaseHelper) hasExecTodos() bool {
for _, commit := range self.c.Model().Commits {
if commit.Status != models.StatusRebasing {
if !commit.IsTODO() {
break
}
if commit.Action == todo.Exec {
@@ -152,6 +135,8 @@ var conflictStrings = []string{
"fix conflicts",
"Resolve all conflicts manually",
"Merge conflict in file",
"hint: after resolving the conflicts",
"CONFLICT (content):",
}
func isMergeConflictErr(errStr string) bool {
@@ -199,7 +184,7 @@ func (self *MergeAndRebaseHelper) CheckForConflicts(result error) error {
}
func (self *MergeAndRebaseHelper) PromptForConflictHandling() error {
mode := self.workingTreeStateNoun()
mode := self.c.Git().Status.WorkingTreeState().CommandName()
return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.FoundConflictsTitle,
Items: []*types.MenuItem{
@@ -224,7 +209,7 @@ func (self *MergeAndRebaseHelper) PromptForConflictHandling() error {
func (self *MergeAndRebaseHelper) AbortMergeOrRebaseWithConfirm() error {
// prompt user to confirm that they want to abort, then do it
mode := self.workingTreeStateNoun()
mode := self.c.Git().Status.WorkingTreeState().CommandName()
self.c.Confirm(types.ConfirmOpts{
Title: fmt.Sprintf(self.c.Tr.AbortTitle, mode),
Prompt: fmt.Sprintf(self.c.Tr.AbortPrompt, mode),
@@ -236,23 +221,11 @@ func (self *MergeAndRebaseHelper) AbortMergeOrRebaseWithConfirm() error {
return nil
}
func (self *MergeAndRebaseHelper) workingTreeStateNoun() string {
workingTreeState := self.c.Git().Status.WorkingTreeState()
switch workingTreeState {
case enums.REBASE_MODE_NONE:
return ""
case enums.REBASE_MODE_MERGING:
return "merge"
default:
return "rebase"
}
}
// PromptToContinueRebase asks the user if they want to continue the rebase/merge that's in progress
func (self *MergeAndRebaseHelper) PromptToContinueRebase() error {
self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.Continue,
Prompt: self.c.Tr.ConflictsResolved,
Prompt: fmt.Sprintf(self.c.Tr.ConflictsResolved, self.c.Git().Status.WorkingTreeState().CommandName()),
HandleConfirm: func() error {
// By the time we get here, we might have unstaged changes again,
// e.g. if the user had to fix build errors after resolving the
@@ -294,14 +267,14 @@ func (self *MergeAndRebaseHelper) PromptToContinueRebase() error {
}
func (self *MergeAndRebaseHelper) RebaseOntoRef(ref string) error {
checkedOutBranch := self.refsHelper.GetCheckedOutRef()
checkedOutBranchName := self.refsHelper.GetCheckedOutRef().Name
checkedOutBranch := self.c.Model().Branches[0]
checkedOutBranchName := checkedOutBranch.Name
var disabledReason, baseBranchDisabledReason *types.DisabledReason
if checkedOutBranchName == ref {
disabledReason = &types.DisabledReason{Text: self.c.Tr.CantRebaseOntoSelf}
}
baseBranch, err := self.c.Git().Loaders.BranchLoader.GetBaseBranch(checkedOutBranch, self.refsHelper.c.Model().MainBranches)
baseBranch, err := self.c.Git().Loaders.BranchLoader.GetBaseBranch(checkedOutBranch, self.c.Model().MainBranches)
if err != nil {
return err
}
@@ -407,7 +380,7 @@ func (self *MergeAndRebaseHelper) MergeRefIntoCheckedOutBranch(refName string) e
if self.c.Git().Branch.IsHeadDetached() {
return errors.New("Cannot merge branch in detached head state. You might have checked out a commit directly or a remote branch, in which case you should checkout the local branch you want to be on")
}
checkedOutBranchName := self.refsHelper.GetCheckedOutRef().Name
checkedOutBranchName := self.c.Model().Branches[0].Name
if checkedOutBranchName == refName {
return errors.New(self.c.Tr.CantMergeBranchIntoItself)
}
+2 -4
View File
@@ -4,8 +4,6 @@ import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/samber/lo"
@@ -116,12 +114,12 @@ func (self *ModeHelper) Statuses() []ModeStatus {
},
{
IsActive: func() bool {
return !self.suppressRebasingMode && self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE
return !self.suppressRebasingMode && self.c.Git().Status.WorkingTreeState().Any()
},
Description: func() string {
workingTreeState := self.c.Git().Status.WorkingTreeState()
return self.withResetButton(
presentation.FormatWorkingTreeStateTitle(self.c.Tr, workingTreeState), style.FgYellow,
workingTreeState.Title(self.c.Tr), style.FgYellow,
)
},
Reset: self.mergeAndRebaseHelper.AbortMergeOrRebaseWithConfirm,
@@ -4,7 +4,6 @@ import (
"errors"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/gui/patch_exploring"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
@@ -22,7 +21,7 @@ func NewPatchBuildingHelper(
}
func (self *PatchBuildingHelper) ValidateNormalWorkingTreeState() (bool, error) {
if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE {
if self.c.Git().Status.WorkingTreeState().Any() {
return false, errors.New(self.c.Tr.CantPatchWhileRebasingError)
}
return true, nil
@@ -9,7 +9,6 @@ import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts"
@@ -333,6 +332,7 @@ func (self *RefreshHelper) refreshCommitsWithLimit() error {
RefForPushedStatus: checkedOutBranchName,
All: self.c.Contexts().LocalCommits.GetShowWholeGitGraph(),
MainBranches: self.c.Model().MainBranches,
HashPool: self.c.Model().HashPool,
},
)
if err != nil {
@@ -361,6 +361,7 @@ func (self *RefreshHelper) refreshSubCommitsWithLimit() error {
RefToShowDivergenceFrom: self.c.Contexts().SubCommits.GetRefToShowDivergenceFrom(),
RefForPushedStatus: self.c.Contexts().SubCommits.GetRef().FullRefName(),
MainBranches: self.c.Model().MainBranches,
HashPool: self.c.Model().HashPool,
},
)
if err != nil {
@@ -407,7 +408,7 @@ func (self *RefreshHelper) refreshRebaseCommits() error {
self.c.Mutexes().LocalCommitsMutex.Lock()
defer self.c.Mutexes().LocalCommitsMutex.Unlock()
updatedCommits, err := self.c.Git().Loaders.CommitLoader.MergeRebasingCommits(self.c.Model().Commits)
updatedCommits, err := self.c.Git().Loaders.CommitLoader.MergeRebasingCommits(self.c.Model().HashPool, self.c.Model().Commits)
if err != nil {
return err
}
@@ -456,7 +457,7 @@ func (self *RefreshHelper) refreshBranches(refreshWorktrees bool, keepBranchSele
// which allows us to order them correctly. So if we're filtering we'll just
// manually load all the reflog commits here
var err error
reflogCommits, _, err = self.c.Git().Loaders.ReflogCommitLoader.GetReflogCommits(nil, "", "")
reflogCommits, _, err = self.c.Git().Loaders.ReflogCommitLoader.GetReflogCommits(self.c.Model().HashPool, nil, "", "")
if err != nil {
self.c.Log.Error(err)
}
@@ -583,7 +584,7 @@ func (self *RefreshHelper) refreshStateFiles() error {
}
}
if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE && conflictFileCount == 0 && prevConflictFileCount > 0 {
if self.c.Git().Status.WorkingTreeState().Any() && conflictFileCount == 0 && prevConflictFileCount > 0 {
self.c.OnUIThread(func() error { return self.mergeAndRebaseHelper.PromptToContinueRebase() })
}
@@ -625,7 +626,7 @@ func (self *RefreshHelper) refreshReflogCommits() error {
refresh := func(stateCommits *[]*models.Commit, filterPath string, filterAuthor string) error {
commits, onlyObtainedNewReflogCommits, err := self.c.Git().Loaders.ReflogCommitLoader.
GetReflogCommits(lastReflogCommit, filterPath, filterAuthor)
GetReflogCommits(self.c.Model().HashPool, lastReflogCommit, filterPath, filterAuthor)
if err != nil {
return err
}
+175 -3
View File
@@ -17,13 +17,17 @@ import (
type RefsHelper struct {
c *HelperCommon
rebaseHelper *MergeAndRebaseHelper
}
func NewRefsHelper(
c *HelperCommon,
rebaseHelper *MergeAndRebaseHelper,
) *RefsHelper {
return &RefsHelper{
c: c,
c: c,
rebaseHelper: rebaseHelper,
}
}
@@ -264,10 +268,10 @@ func (self *RefsHelper) CreateGitResetMenu(ref string) error {
func (self *RefsHelper) CreateCheckoutMenu(commit *models.Commit) error {
branches := lo.Filter(self.c.Model().Branches, func(branch *models.Branch, _ int) bool {
return commit.Hash == branch.CommitHash && branch.Name != self.c.Model().CheckedOutBranch
return commit.Hash() == branch.CommitHash && branch.Name != self.c.Model().CheckedOutBranch
})
hash := commit.Hash
hash := commit.Hash()
menuItems := []*types.MenuItem{
{
@@ -388,6 +392,174 @@ func (self *RefsHelper) NewBranch(from string, fromFormattedName string, suggest
return nil
}
func (self *RefsHelper) MoveCommitsToNewBranch() error {
currentBranch := self.c.Model().Branches[0]
baseBranchRef, err := self.c.Git().Loaders.BranchLoader.GetBaseBranch(currentBranch, self.c.Model().MainBranches)
if err != nil {
return err
}
withNewBranchNamePrompt := func(baseBranchName string, f func(string, string) error) {
prompt := utils.ResolvePlaceholderString(
self.c.Tr.NewBranchNameBranchOff,
map[string]string{
"branchName": baseBranchName,
},
)
self.c.Prompt(types.PromptOpts{
Title: prompt,
HandleConfirm: func(response string) error {
self.c.LogAction(self.c.Tr.MoveCommitsToNewBranch)
newBranchName := SanitizedBranchName(response)
return self.c.WithWaitingStatus(self.c.Tr.MovingCommitsToNewBranchStatus, func(gocui.Task) error {
return f(currentBranch.Name, newBranchName)
})
},
})
}
isMainBranch := lo.Contains(self.c.UserConfig().Git.MainBranches, currentBranch.Name)
if isMainBranch {
prompt := utils.ResolvePlaceholderString(
self.c.Tr.MoveCommitsToNewBranchFromMainPrompt,
map[string]string{
"baseBranchName": currentBranch.Name,
},
)
self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.MoveCommitsToNewBranch,
Prompt: prompt,
HandleConfirm: func() error {
withNewBranchNamePrompt(currentBranch.Name, self.moveCommitsToNewBranchStackedOnCurrentBranch)
return nil
},
})
return nil
}
shortBaseBranchName := ShortBranchName(baseBranchRef)
prompt := utils.ResolvePlaceholderString(
self.c.Tr.MoveCommitsToNewBranchMenuPrompt,
map[string]string{
"baseBranchName": shortBaseBranchName,
},
)
return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.MoveCommitsToNewBranch,
Prompt: prompt,
Items: []*types.MenuItem{
{
Label: fmt.Sprintf(self.c.Tr.MoveCommitsToNewBranchFromBaseItem, shortBaseBranchName),
OnPress: func() error {
withNewBranchNamePrompt(shortBaseBranchName, func(currentBranch string, newBranchName string) error {
return self.moveCommitsToNewBranchOffOfMainBranch(currentBranch, newBranchName, baseBranchRef)
})
return nil
},
},
{
Label: fmt.Sprintf(self.c.Tr.MoveCommitsToNewBranchStackedItem, currentBranch.Name),
OnPress: func() error {
withNewBranchNamePrompt(currentBranch.Name, self.moveCommitsToNewBranchStackedOnCurrentBranch)
return nil
},
},
},
})
}
func (self *RefsHelper) moveCommitsToNewBranchStackedOnCurrentBranch(currentBranch string, newBranchName string) error {
if err := self.c.Git().Branch.NewWithoutCheckout(newBranchName, "HEAD"); err != nil {
return err
}
mustStash := IsWorkingTreeDirty(self.c.Model().Files)
if mustStash {
if err := self.c.Git().Stash.Push(self.c.Tr.StashPrefix + currentBranch); err != nil {
return err
}
}
if err := self.c.Git().Commit.ResetToCommit("@{u}", "hard", []string{}); err != nil {
return err
}
if err := self.c.Git().Branch.Checkout(newBranchName, git_commands.CheckoutOptions{}); err != nil {
return err
}
if mustStash {
if err := self.c.Git().Stash.Pop(0); err != nil {
return err
}
}
self.c.Contexts().LocalCommits.SetSelection(0)
self.c.Contexts().Branches.SetSelection(0)
return self.c.Refresh(types.RefreshOptions{Mode: types.BLOCK_UI, KeepBranchSelectionIndex: true})
}
func (self *RefsHelper) moveCommitsToNewBranchOffOfMainBranch(currentBranch string, newBranchName string, baseBranchRef string) error {
commitsToCherryPick := lo.Filter(self.c.Model().Commits, func(commit *models.Commit, _ int) bool {
return commit.Status == models.StatusUnpushed
})
mustStash := IsWorkingTreeDirty(self.c.Model().Files)
if mustStash {
if err := self.c.Git().Stash.Push(self.c.Tr.StashPrefix + currentBranch); err != nil {
return err
}
}
if err := self.c.Git().Commit.ResetToCommit("@{u}", "hard", []string{}); err != nil {
return err
}
if err := self.c.Git().Branch.NewWithoutTracking(newBranchName, baseBranchRef); err != nil {
return err
}
err := self.c.Git().Rebase.CherryPickCommits(commitsToCherryPick)
err = self.rebaseHelper.CheckMergeOrRebaseWithRefreshOptions(err, types.RefreshOptions{Mode: types.SYNC})
if err != nil {
return err
}
if mustStash {
if err := self.c.Git().Stash.Pop(0); err != nil {
return err
}
}
self.c.Contexts().LocalCommits.SetSelection(0)
self.c.Contexts().Branches.SetSelection(0)
return self.c.Refresh(types.RefreshOptions{Mode: types.BLOCK_UI, KeepBranchSelectionIndex: true})
}
func (self *RefsHelper) CanMoveCommitsToNewBranch() *types.DisabledReason {
if len(self.c.Model().Branches) == 0 {
return &types.DisabledReason{Text: self.c.Tr.NoBranchesThisRepo}
}
currentBranch := self.GetCheckedOutRef()
if currentBranch.DetachedHead {
return &types.DisabledReason{Text: self.c.Tr.CannotMoveCommitsFromDetachedHead, ShowErrorInPanel: true}
}
if !currentBranch.RemoteBranchStoredLocally() {
return &types.DisabledReason{Text: self.c.Tr.CannotMoveCommitsNoUpstream, ShowErrorInPanel: true}
}
if currentBranch.IsBehindForPull() {
return &types.DisabledReason{Text: self.c.Tr.CannotMoveCommitsBehindUpstream, ShowErrorInPanel: true}
}
if !currentBranch.IsAheadForPull() {
return &types.DisabledReason{Text: self.c.Tr.CannotMoveCommitsNoUnpushedCommits, ShowErrorInPanel: true}
}
return nil
}
// SanitizedBranchName will remove all spaces in favor of a dash "-" to meet
// git's branch naming requirement.
func SanitizedBranchName(input string) string {
@@ -45,6 +45,7 @@ func (self *SubCommitsHelper) ViewSubCommits(opts ViewSubCommitsOpts) error {
RefForPushedStatus: opts.Ref.FullRefName(),
RefToShowDivergenceFrom: opts.RefToShowDivergenceFrom,
MainBranches: self.c.Model().MainBranches,
HashPool: self.c.Model().HashPool,
},
)
if err != nil {
@@ -10,6 +10,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/samber/lo"
)
type WorkingTreeHelper struct {
@@ -34,25 +35,27 @@ func NewWorkingTreeHelper(
}
func (self *WorkingTreeHelper) AnyStagedFiles() bool {
for _, file := range self.c.Model().Files {
if file.HasStagedChanges {
return true
}
}
return false
return AnyStagedFiles(self.c.Model().Files)
}
func AnyStagedFiles(files []*models.File) bool {
return lo.SomeBy(files, func(f *models.File) bool { return f.HasStagedChanges })
}
func (self *WorkingTreeHelper) AnyTrackedFiles() bool {
for _, file := range self.c.Model().Files {
if file.Tracked {
return true
}
}
return false
return AnyTrackedFiles(self.c.Model().Files)
}
func AnyTrackedFiles(files []*models.File) bool {
return lo.SomeBy(files, func(f *models.File) bool { return f.Tracked })
}
func (self *WorkingTreeHelper) IsWorkingTreeDirty() bool {
return self.AnyStagedFiles() || self.AnyTrackedFiles()
return IsWorkingTreeDirty(self.c.Model().Files)
}
func IsWorkingTreeDirty(files []*models.File) bool {
return AnyStagedFiles(files) || AnyTrackedFiles(files)
}
func (self *WorkingTreeHelper) FileForSubmodule(submodule *models.SubmoduleConfig) *models.File {
+3
View File
@@ -74,6 +74,9 @@ func (self *ListController) scrollHorizontal(scrollFunc func()) error {
scrollFunc()
self.context.HandleFocus(types.OnFocusOpts{})
if self.context.NeedsRerenderOnWidthChange() == types.NEEDS_RERENDER_ON_WIDTH_CHANGE_WHEN_WIDTH_CHANGES {
self.context.HandleRender()
}
return nil
}
+61 -66
View File
@@ -1,14 +1,12 @@
package controllers
import (
"fmt"
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/context/traits"
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
@@ -250,8 +248,8 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
},
{
Key: opts.GetKey(opts.Config.Commits.RevertCommit),
Handler: self.withItem(self.revert),
GetDisabledReason: self.require(self.singleItemSelected()),
Handler: self.withItemsRange(self.revert),
GetDisabledReason: self.require(self.itemRangeSelected()),
Description: self.c.Tr.Revert,
Tooltip: self.c.Tr.RevertCommitTooltip,
},
@@ -361,7 +359,7 @@ func (self *LocalCommitsController) fixup(selectedCommits []*models.Commit, star
}
func (self *LocalCommitsController) reword(commit *models.Commit) error {
commitMessage, err := self.c.Git().Commit.GetCommitMessage(commit.Hash)
commitMessage, err := self.c.Git().Commit.GetCommitMessage(commit.Hash())
if err != nil {
return err
}
@@ -558,7 +556,7 @@ func (self *LocalCommitsController) startInteractiveRebaseWithEdit(
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(gocui.Task) error {
self.c.LogAction(self.c.Tr.Actions.EditCommit)
selectionRangeAndMode := self.getSelectionRangeAndMode()
err := self.c.Git().Rebase.EditRebase(commitsToEdit[len(commitsToEdit)-1].Hash)
err := self.c.Git().Rebase.EditRebase(commitsToEdit[len(commitsToEdit)-1].Hash())
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebaseWithRefreshOptions(
err,
types.RefreshOptions{Mode: types.BLOCK_UI, Then: func() error {
@@ -566,7 +564,7 @@ func (self *LocalCommitsController) startInteractiveRebaseWithEdit(
for _, c := range commitsToEdit[:len(commitsToEdit)-1] {
// Merge commits can't be set to "edit", so just skip them
if !c.IsMerge() {
todos = append(todos, &models.Commit{Hash: c.Hash, Action: todo.Pick})
todos = append(todos, models.NewCommit(self.c.Model().HashPool, models.NewCommitOpts{Hash: c.Hash(), Action: todo.Pick}))
}
}
if len(todos) > 0 {
@@ -591,8 +589,8 @@ type SelectionRangeAndMode struct {
func (self *LocalCommitsController) getSelectionRangeAndMode() SelectionRangeAndMode {
selectedIdx, rangeStartIdx, rangeSelectMode := self.context().GetSelectionRangeAndMode()
commits := self.c.Model().Commits
selectedHash := commits[selectedIdx].Hash
rangeStartHash := commits[rangeStartIdx].Hash
selectedHash := commits[selectedIdx].Hash()
rangeStartHash := commits[rangeStartIdx].Hash()
return SelectionRangeAndMode{selectedHash, rangeStartHash, rangeSelectMode}
}
@@ -601,10 +599,10 @@ func (self *LocalCommitsController) restoreSelectionRangeAndMode(selectionRangeA
// new lines can be added for update-ref commands in the TODO file, due to
// stacked branches. So the selected commits may be in different positions in the list.
_, newSelectedIdx, ok1 := lo.FindIndexOf(self.c.Model().Commits, func(c *models.Commit) bool {
return c.Hash == selectionRangeAndMode.selectedHash
return c.Hash() == selectionRangeAndMode.selectedHash
})
_, newRangeStartIdx, ok2 := lo.FindIndexOf(self.c.Model().Commits, func(c *models.Commit) bool {
return c.Hash == selectionRangeAndMode.rangeStartHash
return c.Hash() == selectionRangeAndMode.rangeStartHash
})
if ok1 && ok2 {
self.context().SetSelectionRangeAndMode(newSelectedIdx, newRangeStartIdx, selectionRangeAndMode.mode)
@@ -682,7 +680,12 @@ func (self *LocalCommitsController) rewordEnabled(commit *models.Commit) *types.
}
func (self *LocalCommitsController) isRebasing() bool {
return self.c.Model().WorkingTreeStateAtLastCommitRefresh != enums.REBASE_MODE_NONE
return self.c.Model().WorkingTreeStateAtLastCommitRefresh.Any()
}
func (self *LocalCommitsController) isCherryPickingOrReverting() bool {
return self.c.Model().WorkingTreeStateAtLastCommitRefresh.CherryPicking ||
self.c.Model().WorkingTreeStateAtLastCommitRefresh.Reverting
}
func (self *LocalCommitsController) moveDown(selectedCommits []*models.Commit, startIdx int, endIdx int) error {
@@ -854,25 +857,34 @@ func (self *LocalCommitsController) addCoAuthor(start, end int) error {
return nil
}
func (self *LocalCommitsController) revert(commit *models.Commit) error {
if commit.IsMerge() {
return self.createRevertMergeCommitMenu(commit)
}
self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.Actions.RevertCommit,
Prompt: utils.ResolvePlaceholderString(
func (self *LocalCommitsController) revert(commits []*models.Commit, start, end int) error {
var promptText string
if len(commits) == 1 {
promptText = utils.ResolvePlaceholderString(
self.c.Tr.ConfirmRevertCommit,
map[string]string{
"selectedCommit": commit.ShortHash(),
}),
"selectedCommit": commits[0].ShortHash(),
})
} else {
promptText = self.c.Tr.ConfirmRevertCommitRange
}
hashes := lo.Map(commits, func(c *models.Commit, _ int) string { return c.Hash() })
isMerge := lo.SomeBy(commits, func(c *models.Commit) bool { return c.IsMerge() })
self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.Actions.RevertCommit,
Prompt: promptText,
HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.RevertCommit)
return self.c.WithWaitingStatusSync(self.c.Tr.RevertingStatus, func() error {
if err := self.c.Git().Commit.Revert(commit.Hash); err != nil {
result := self.c.Git().Commit.Revert(hashes, isMerge)
if err := self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(result); err != nil {
return err
}
return self.afterRevertCommit()
self.context().MoveSelection(len(commits))
return self.c.Refresh(types.RefreshOptions{
Mode: types.SYNC, Scope: []types.RefreshableView{types.COMMITS, types.BRANCHES},
})
})
},
})
@@ -880,39 +892,6 @@ func (self *LocalCommitsController) revert(commit *models.Commit) error {
return nil
}
func (self *LocalCommitsController) createRevertMergeCommitMenu(commit *models.Commit) error {
menuItems := make([]*types.MenuItem, len(commit.Parents))
for i, parentHash := range commit.Parents {
message, err := self.c.Git().Commit.GetCommitMessageFirstLine(parentHash)
if err != nil {
return err
}
menuItems[i] = &types.MenuItem{
Label: fmt.Sprintf("%s: %s", utils.SafeTruncate(parentHash, 8), message),
OnPress: func() error {
parentNumber := i + 1
self.c.LogAction(self.c.Tr.Actions.RevertCommit)
return self.c.WithWaitingStatusSync(self.c.Tr.RevertingStatus, func() error {
if err := self.c.Git().Commit.RevertMerge(commit.Hash, parentNumber); err != nil {
return err
}
return self.afterRevertCommit()
})
},
}
}
return self.c.Menu(types.CreateMenuOptions{Title: self.c.Tr.SelectParentCommitForMerge, Items: menuItems})
}
func (self *LocalCommitsController) afterRevertCommit() error {
self.context().MoveSelection(1)
return self.c.Refresh(types.RefreshOptions{
Mode: types.SYNC, Scope: []types.RefreshableView{types.COMMITS, types.BRANCHES},
})
}
func (self *LocalCommitsController) createFixupCommit(commit *models.Commit) error {
var disabledReasonWhenFilesAreNeeded *types.DisabledReason
if len(self.c.Model().Files) == 0 {
@@ -932,7 +911,7 @@ func (self *LocalCommitsController) createFixupCommit(commit *models.Commit) err
return self.c.Helpers().WorkingTree.WithEnsureCommittableFiles(func() error {
self.c.LogAction(self.c.Tr.Actions.CreateFixupCommit)
return self.c.WithWaitingStatusSync(self.c.Tr.CreatingFixupCommitStatus, func() error {
if err := self.c.Git().Commit.CreateFixupCommit(commit.Hash); err != nil {
if err := self.c.Git().Commit.CreateFixupCommit(commit.Hash()); err != nil {
return err
}
@@ -976,7 +955,7 @@ func (self *LocalCommitsController) moveFixupCommitToOwnerStackedBranch(targetCo
return nil
}
if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE {
if self.c.Git().Status.WorkingTreeState().Any() {
// Can't move commits while rebasing
return nil
}
@@ -999,7 +978,7 @@ func (self *LocalCommitsController) moveFixupCommitToOwnerStackedBranch(targetCo
headOfOwnerBranchIdx := -1
for i := self.context().GetSelectedLineIdx(); i > 0; i-- {
if lo.SomeBy(self.c.Model().Branches, func(b *models.Branch) bool {
return b.CommitHash == self.c.Model().Commits[i].Hash
return b.CommitHash == self.c.Model().Commits[i].Hash()
}) {
headOfOwnerBranchIdx = i
break
@@ -1014,7 +993,7 @@ func (self *LocalCommitsController) moveFixupCommitToOwnerStackedBranch(targetCo
}
func (self *LocalCommitsController) createAmendCommit(commit *models.Commit, includeFileChanges bool) error {
commitMessage, err := self.c.Git().Commit.GetCommitMessage(commit.Hash)
commitMessage, err := self.c.Git().Commit.GetCommitMessage(commit.Hash())
if err != nil {
return err
}
@@ -1160,7 +1139,7 @@ func isFixupCommit(subject string) (string, bool) {
}
func (self *LocalCommitsController) createTag(commit *models.Commit) error {
return self.c.Helpers().Tags.OpenCreateTagPrompt(commit.Hash, func() {})
return self.c.Helpers().Tags.OpenCreateTagPrompt(commit.Hash(), func() {})
}
func (self *LocalCommitsController) openSearch() error {
@@ -1325,11 +1304,11 @@ func (self *LocalCommitsController) canPaste() *types.DisabledReason {
}
func (self *LocalCommitsController) markAsBaseCommit(commit *models.Commit) error {
if commit.Hash == self.c.Modes().MarkedBaseCommit.GetHash() {
if commit.Hash() == self.c.Modes().MarkedBaseCommit.GetHash() {
// Reset when invoking it again on the marked commit
self.c.Modes().MarkedBaseCommit.SetHash("")
} else {
self.c.Modes().MarkedBaseCommit.SetHash(commit.Hash)
self.c.Modes().MarkedBaseCommit.SetHash(commit.Hash())
}
self.c.PostRefreshUpdate(self.c.Contexts().LocalCommits)
return nil
@@ -1389,7 +1368,7 @@ func (self *LocalCommitsController) canMoveDown(selectedCommits []*models.Commit
if self.isRebasing() {
commits := self.c.Model().Commits
if !commits[endIdx+1].IsTODO() || commits[endIdx+1].Action == models.ActionConflict {
if !commits[endIdx+1].IsTODO() || commits[endIdx+1].Status == models.StatusConflicted {
return &types.DisabledReason{Text: self.c.Tr.CannotMoveAnyFurther}
}
}
@@ -1405,7 +1384,7 @@ func (self *LocalCommitsController) canMoveUp(selectedCommits []*models.Commit,
if self.isRebasing() {
commits := self.c.Model().Commits
if !commits[startIdx-1].IsTODO() || commits[startIdx-1].Action == models.ActionConflict {
if !commits[startIdx-1].IsTODO() || commits[startIdx-1].Status == models.StatusConflicted {
return &types.DisabledReason{Text: self.c.Tr.CannotMoveAnyFurther}
}
}
@@ -1415,6 +1394,10 @@ func (self *LocalCommitsController) canMoveUp(selectedCommits []*models.Commit,
// Ensures that if we are mid-rebase, we're only selecting valid commits (non-conflict TODO commits)
func (self *LocalCommitsController) midRebaseCommandEnabled(selectedCommits []*models.Commit, startIdx int, endIdx int) *types.DisabledReason {
if self.isCherryPickingOrReverting() {
return &types.DisabledReason{Text: self.c.Tr.NotAllowedMidCherryPickOrRevert}
}
if !self.isRebasing() {
return nil
}
@@ -1434,6 +1417,10 @@ func (self *LocalCommitsController) midRebaseCommandEnabled(selectedCommits []*m
// Ensures that if we are mid-rebase, we're only selecting commits that can be moved
func (self *LocalCommitsController) midRebaseMoveCommandEnabled(selectedCommits []*models.Commit, startIdx int, endIdx int) *types.DisabledReason {
if self.isCherryPickingOrReverting() {
return &types.DisabledReason{Text: self.c.Tr.NotAllowedMidCherryPickOrRevert}
}
if !self.isRebasing() {
if lo.SomeBy(selectedCommits, func(c *models.Commit) bool { return c.IsMerge() }) {
return &types.DisabledReason{Text: self.c.Tr.CannotMoveMergeCommit}
@@ -1458,6 +1445,10 @@ func (self *LocalCommitsController) midRebaseMoveCommandEnabled(selectedCommits
}
func (self *LocalCommitsController) canDropCommits(selectedCommits []*models.Commit, startIdx int, endIdx int) *types.DisabledReason {
if self.isCherryPickingOrReverting() {
return &types.DisabledReason{Text: self.c.Tr.NotAllowedMidCherryPickOrRevert}
}
if !self.isRebasing() {
if len(selectedCommits) > 1 && lo.SomeBy(selectedCommits, func(c *models.Commit) bool { return c.IsMerge() }) {
return &types.DisabledReason{Text: self.c.Tr.DroppingMergeRequiresSingleSelection}
@@ -1502,6 +1493,10 @@ func isChangeOfRebaseTodoAllowed(oldAction todo.TodoCommand) bool {
}
func (self *LocalCommitsController) pickEnabled(selectedCommits []*models.Commit, startIdx int, endIdx int) *types.DisabledReason {
if self.isCherryPickingOrReverting() {
return &types.DisabledReason{Text: self.c.Tr.NotAllowedMidCherryPickOrRevert}
}
if !self.isRebasing() {
// if not rebasing, we're going to do a pull so we don't care about the selection
return nil
+118
View File
@@ -0,0 +1,118 @@
package controllers
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type MainViewController struct {
baseController
c *ControllerCommon
context *context.MainContext
otherContext *context.MainContext
}
var _ types.IController = &MainViewController{}
func NewMainViewController(
c *ControllerCommon,
context *context.MainContext,
otherContext *context.MainContext,
) *MainViewController {
return &MainViewController{
baseController: baseController{},
c: c,
context: context,
otherContext: otherContext,
}
}
func (self *MainViewController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
return []*types.Binding{
{
Key: opts.GetKey(opts.Config.Universal.TogglePanel),
Handler: self.togglePanel,
Description: self.c.Tr.ToggleStagingView,
Tooltip: self.c.Tr.ToggleStagingViewTooltip,
DisplayOnScreen: true,
},
{
Key: opts.GetKey(opts.Config.Universal.Return),
Handler: self.escape,
Description: self.c.Tr.ExitFocusedMainView,
},
{
// overriding this because we want to read all of the task's output before we start searching
Key: opts.GetKey(opts.Config.Universal.StartSearch),
Handler: self.openSearch,
Description: self.c.Tr.StartSearch,
Tag: "navigation",
},
}
}
func (self *MainViewController) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding {
return []*gocui.ViewMouseBinding{
{
ViewName: self.context.GetViewName(),
Key: gocui.MouseLeft,
Handler: func(opts gocui.ViewMouseBindingOpts) error {
if self.isFocused() {
return self.onClick(opts)
}
self.context.SetParentContext(self.otherContext.GetParentContext())
self.c.Context().Push(self.context, types.OnFocusOpts{
ClickedWindowName: self.context.GetWindowName(),
ClickedViewLineIdx: opts.Y,
})
return nil
},
},
}
}
func (self *MainViewController) Context() types.Context {
return self.context
}
func (self *MainViewController) togglePanel() error {
if self.otherContext.GetView().Visible {
self.otherContext.SetParentContext(self.context.GetParentContext())
self.c.Context().Push(self.otherContext, types.OnFocusOpts{})
}
return nil
}
func (self *MainViewController) escape() error {
self.c.Context().Pop()
return nil
}
func (self *MainViewController) onClick(opts gocui.ViewMouseBindingOpts) error {
parentCtx := self.context.GetParentContext()
if parentCtx.GetOnClickFocusedMainView() != nil {
return parentCtx.GetOnClickFocusedMainView()(self.context.GetViewName(), opts.Y)
}
return nil
}
func (self *MainViewController) openSearch() error {
if manager := self.c.GetViewBufferManagerForView(self.context.GetView()); manager != nil {
manager.ReadToEnd(func() {
self.c.OnUIThread(func() error {
return self.c.Helpers().Search.OpenSearchPrompt(self.context)
})
})
}
return nil
}
func (self *MainViewController) isFocused() bool {
return self.c.Context().Current().GetKey() == self.context.GetKey()
}
@@ -1,8 +1,11 @@
package controllers
import (
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/samber/lo"
)
type PatchExplorerControllerFactory struct {
@@ -295,13 +298,49 @@ func (self *PatchExplorerController) CopySelectedToClipboard() error {
selected := self.context.GetState().PlainRenderSelected()
self.c.LogAction(self.c.Tr.Actions.CopySelectedTextToClipboard)
if err := self.c.OS().CopyToClipboard(selected); err != nil {
if err := self.c.OS().CopyToClipboard(dropDiffPrefix(selected)); err != nil {
return err
}
return nil
}
// Removes '+' or '-' from the beginning of each line in the diff string, except
// when both '+' and '-' lines are present, or diff header lines, in which case
// the diff is returned unchanged. This is useful for copying parts of diffs to
// the clipboard in order to paste them into code.
func dropDiffPrefix(diff string) string {
lines := strings.Split(strings.TrimRight(diff, "\n"), "\n")
const (
PLUS int = iota
MINUS
CONTEXT
OTHER
)
linesByType := lo.GroupBy(lines, func(line string) int {
switch {
case strings.HasPrefix(line, "+"):
return PLUS
case strings.HasPrefix(line, "-"):
return MINUS
case strings.HasPrefix(line, " "):
return CONTEXT
}
return OTHER
})
hasLinesOfType := func(lineType int) bool { return len(linesByType[lineType]) > 0 }
keepPrefix := hasLinesOfType(OTHER) || (hasLinesOfType(PLUS) && hasLinesOfType(MINUS))
if keepPrefix {
return diff
}
return strings.Join(lo.Map(lines, func(line string, _ int) string { return line[1:] + "\n" }), "")
}
func (self *PatchExplorerController) isFocused() bool {
return self.c.Context().Current().GetKey() == self.context.GetKey()
}
@@ -0,0 +1,89 @@
package controllers
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_dropDiffPrefix(t *testing.T) {
scenarios := []struct {
name string
diff string
expectedResult string
}{
{
name: "empty string",
diff: "",
expectedResult: "",
},
{
name: "only added lines",
diff: `+line1
+line2
`,
expectedResult: `line1
line2
`,
},
{
name: "added lines with context",
diff: ` line1
+line2
`,
expectedResult: `line1
line2
`,
},
{
name: "only deleted lines",
diff: `-line1
-line2
`,
expectedResult: `line1
line2
`,
},
{
name: "deleted lines with context",
diff: `-line1
line2
`,
expectedResult: `line1
line2
`,
},
{
name: "only context",
diff: ` line1
line2
`,
expectedResult: `line1
line2
`,
},
{
name: "added and deleted lines",
diff: `+line1
-line2
`,
expectedResult: `+line1
-line2
`,
},
{
name: "hunk header lines",
diff: `@@ -1,8 +1,11 @@
line1
`,
expectedResult: `@@ -1,8 +1,11 @@
line1
`,
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
assert.Equal(t, s.expectedResult, dropDiffPrefix(s.diff))
})
}
}
@@ -45,7 +45,7 @@ func (self *ReflogCommitsController) GetOnRenderToMain() func() {
if commit == nil {
task = types.NewRenderStringTask("No reflog history")
} else {
cmdObj := self.c.Git().Commit.ShowCmdObj(commit.Hash, self.c.Modes().Filtering.GetPath())
cmdObj := self.c.Git().Commit.ShowCmdObj(commit.Hash(), self.c.Modes().Filtering.GetPath())
task = types.NewRunPtyTask(cmdObj.GetCmd())
}
@@ -15,6 +15,8 @@ var CONTEXT_KEYS_SHOWING_RENAMES = []types.ContextKey{
context.SUB_COMMITS_CONTEXT_KEY,
context.LOCAL_COMMITS_CONTEXT_KEY,
context.STASH_CONTEXT_KEY,
context.NORMAL_MAIN_CONTEXT_KEY,
context.NORMAL_SECONDARY_CONTEXT_KEY,
}
type RenameSimilarityThresholdController struct {
@@ -82,7 +84,7 @@ func (self *RenameSimilarityThresholdController) applyChange() error {
self.c.Toast(fmt.Sprintf(self.c.Tr.RenameSimilarityThresholdChanged, self.c.AppState.RenameSimilarityThreshold))
self.c.SaveAppStateAndLogError()
currentContext := self.c.Context().CurrentStatic()
currentContext := self.currentSidePanel()
switch currentContext.GetKey() {
// we make an exception for our files context, because it actually need to refresh its state afterwards.
case context.FILES_CONTEXT_KEY:
@@ -96,6 +98,16 @@ func (self *RenameSimilarityThresholdController) applyChange() error {
func (self *RenameSimilarityThresholdController) isShowingRenames() bool {
return lo.Contains(
CONTEXT_KEYS_SHOWING_RENAMES,
self.c.Context().CurrentStatic().GetKey(),
self.currentSidePanel().GetKey(),
)
}
func (self *RenameSimilarityThresholdController) currentSidePanel() types.Context {
currentContext := self.c.Context().CurrentStatic()
if currentContext.GetKey() == context.NORMAL_MAIN_CONTEXT_KEY ||
currentContext.GetKey() == context.NORMAL_SECONDARY_CONTEXT_KEY {
return currentContext.GetParentContext()
}
return currentContext
}
+4 -8
View File
@@ -7,7 +7,6 @@ import (
"time"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/constants"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/style"
@@ -109,19 +108,16 @@ func (self *StatusController) onClick(opts gocui.ViewMouseBindingOpts) error {
upstreamStatus := utils.Decolorise(presentation.BranchStatus(currentBranch, types.ItemOperationNone, self.c.Tr, time.Now(), self.c.UserConfig()))
repoName := self.c.Git().RepoPaths.RepoName()
workingTreeState := self.c.Git().Status.WorkingTreeState()
switch workingTreeState {
case enums.REBASE_MODE_REBASING, enums.REBASE_MODE_MERGING:
workingTreeStatus := fmt.Sprintf("(%s)", presentation.FormatWorkingTreeStateLower(self.c.Tr, workingTreeState))
if workingTreeState.Any() {
workingTreeStatus := fmt.Sprintf("(%s)", workingTreeState.LowerCaseTitle(self.c.Tr))
if cursorInSubstring(opts.X, upstreamStatus+" ", workingTreeStatus) {
return self.c.Helpers().MergeAndRebase.CreateRebaseOptionsMenu()
}
if cursorInSubstring(opts.X, upstreamStatus+" "+workingTreeStatus+" ", repoName) {
return self.c.Helpers().Repos.CreateRecentReposMenu()
}
default:
if cursorInSubstring(opts.X, upstreamStatus+" ", repoName) {
return self.c.Helpers().Repos.CreateRecentReposMenu()
}
} else if cursorInSubstring(opts.X, upstreamStatus+" ", repoName) {
return self.c.Helpers().Repos.CreateRecentReposMenu()
}
return nil
@@ -0,0 +1,82 @@
package controllers
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
// This controller is for all contexts that can focus their main view.
var _ types.IController = &SwitchToFocusedMainViewController{}
type SwitchToFocusedMainViewController struct {
baseController
c *ControllerCommon
context types.Context
}
func NewSwitchToFocusedMainViewController(
c *ControllerCommon,
context types.Context,
) *SwitchToFocusedMainViewController {
return &SwitchToFocusedMainViewController{
baseController: baseController{},
c: c,
context: context,
}
}
func (self *SwitchToFocusedMainViewController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
bindings := []*types.Binding{
{
Key: opts.GetKey(opts.Config.Universal.FocusMainView),
Handler: self.handleFocusMainView,
Description: self.c.Tr.FocusMainView,
},
}
return bindings
}
func (self *SwitchToFocusedMainViewController) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding {
return []*gocui.ViewMouseBinding{
{
ViewName: "main",
Key: gocui.MouseLeft,
Handler: self.onClickMain,
FocusedView: self.context.GetViewName(),
},
{
ViewName: "secondary",
Key: gocui.MouseLeft,
Handler: self.onClickSecondary,
FocusedView: self.context.GetViewName(),
},
}
}
func (self *SwitchToFocusedMainViewController) Context() types.Context {
return self.context
}
func (self *SwitchToFocusedMainViewController) onClickMain(opts gocui.ViewMouseBindingOpts) error {
return self.focusMainView("main")
}
func (self *SwitchToFocusedMainViewController) onClickSecondary(opts gocui.ViewMouseBindingOpts) error {
return self.focusMainView("secondary")
}
func (self *SwitchToFocusedMainViewController) handleFocusMainView() error {
return self.focusMainView("main")
}
func (self *SwitchToFocusedMainViewController) focusMainView(mainViewName string) error {
mainViewContext := self.c.Helpers().Window.GetContextForWindow(mainViewName)
mainViewContext.SetParentContext(self.context)
if context, ok := mainViewContext.(types.ISearchableContext); ok {
context.ClearSearchString()
}
self.c.Context().Push(mainViewContext, types.OnFocusOpts{})
return nil
}
+5 -6
View File
@@ -5,7 +5,6 @@ import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -78,7 +77,7 @@ func (self *UndoController) reflogUndo() error {
undoEnvVars := []string{"GIT_REFLOG_ACTION=[lazygit undo]"}
undoingStatus := self.c.Tr.UndoingStatus
if self.c.Git().Status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
if self.c.Git().Status.WorkingTreeState().Any() {
return errors.New(self.c.Tr.CantUndoWhileRebasing)
}
@@ -142,7 +141,7 @@ func (self *UndoController) reflogRedo() error {
redoEnvVars := []string{"GIT_REFLOG_ACTION=[lazygit redo]"}
redoingStatus := self.c.Tr.RedoingStatus
if self.c.Git().Status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
if self.c.Git().Status.WorkingTreeState().Any() {
return errors.New(self.c.Tr.CantRedoWhileRebasing)
}
@@ -208,7 +207,7 @@ func (self *UndoController) parseReflogForActions(onUserAction func(counter int,
prevCommitHash := ""
if len(reflogCommits)-1 >= reflogCommitIdx+1 {
prevCommitHash = reflogCommits[reflogCommitIdx+1].Hash
prevCommitHash = reflogCommits[reflogCommitIdx+1].Hash()
}
if rebaseFinishCommitHash == "" {
@@ -217,11 +216,11 @@ func (self *UndoController) parseReflogForActions(onUserAction func(counter int,
} else if ok, _ := utils.FindStringSubmatch(reflogCommit.Name, `^\[lazygit redo\]`); ok {
counter--
} else if ok, _ := utils.FindStringSubmatch(reflogCommit.Name, `^rebase (-i )?\(abort\)|^rebase (-i )?\(finish\)`); ok {
rebaseFinishCommitHash = reflogCommit.Hash
rebaseFinishCommitHash = reflogCommit.Hash()
} else if ok, match := utils.FindStringSubmatch(reflogCommit.Name, `^checkout: moving from ([\S]+) to ([\S]+)`); ok {
action = &reflogAction{kind: CHECKOUT, from: match[1], to: match[2]}
} else if ok, _ := utils.FindStringSubmatch(reflogCommit.Name, `^commit|^reset: moving to|^pull`); ok {
action = &reflogAction{kind: COMMIT, from: prevCommitHash, to: reflogCommit.Hash}
action = &reflogAction{kind: COMMIT, from: prevCommitHash, to: reflogCommit.Hash()}
} else if ok, _ := utils.FindStringSubmatch(reflogCommit.Name, `^rebase (-i )?\(start\)`); ok {
// if we're here then we must be currently inside an interactive rebase
action = &reflogAction{kind: CURRENT_REBASE, from: prevCommitHash}
@@ -3,29 +3,25 @@ package controllers
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/tasks"
)
// given we have no fields here, arguably we shouldn't even need this factory
// struct, but we're maintaining consistency with the other files.
type VerticalScrollControllerFactory struct {
c *ControllerCommon
viewBufferManagerMap *map[string]*tasks.ViewBufferManager
c *ControllerCommon
}
func NewVerticalScrollControllerFactory(c *ControllerCommon, viewBufferManagerMap *map[string]*tasks.ViewBufferManager) *VerticalScrollControllerFactory {
func NewVerticalScrollControllerFactory(c *ControllerCommon) *VerticalScrollControllerFactory {
return &VerticalScrollControllerFactory{
c: c,
viewBufferManagerMap: viewBufferManagerMap,
c: c,
}
}
func (self *VerticalScrollControllerFactory) Create(context types.Context) types.IController {
return &VerticalScrollController{
baseController: baseController{},
c: self.c,
context: context,
viewBufferManagerMap: self.viewBufferManagerMap,
baseController: baseController{},
c: self.c,
context: context,
}
}
@@ -33,8 +29,7 @@ type VerticalScrollController struct {
baseController
c *ControllerCommon
context types.Context
viewBufferManagerMap *map[string]*tasks.ViewBufferManager
context types.Context
}
func (self *VerticalScrollController) Context() types.Context {
@@ -74,7 +69,7 @@ func (self *VerticalScrollController) HandleScrollDown() error {
scrollHeight := self.c.UserConfig().Gui.ScrollHeight
self.context.GetViewTrait().ScrollDown(scrollHeight)
if manager, ok := (*self.viewBufferManagerMap)[self.context.GetViewName()]; ok {
if manager := self.c.GetViewBufferManagerForView(self.context.GetView()); manager != nil {
manager.ReadLines(scrollHeight)
}
@@ -0,0 +1,109 @@
package controllers
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type ViewSelectionControllerFactory struct {
c *ControllerCommon
}
func NewViewSelectionControllerFactory(c *ControllerCommon) *ViewSelectionControllerFactory {
return &ViewSelectionControllerFactory{
c: c,
}
}
func (self *ViewSelectionControllerFactory) Create(context types.Context) types.IController {
return &ViewSelectionController{
baseController: baseController{},
c: self.c,
context: context,
}
}
type ViewSelectionController struct {
baseController
c *ControllerCommon
context types.Context
}
func (self *ViewSelectionController) Context() types.Context {
return self.context
}
func (self *ViewSelectionController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
return []*types.Binding{
{Tag: "navigation", Key: opts.GetKey(opts.Config.Universal.PrevItem), Handler: self.handlePrevLine},
{Tag: "navigation", Key: opts.GetKey(opts.Config.Universal.PrevItemAlt), Handler: self.handlePrevLine},
{Tag: "navigation", Key: opts.GetKey(opts.Config.Universal.NextItem), Handler: self.handleNextLine},
{Tag: "navigation", Key: opts.GetKey(opts.Config.Universal.NextItemAlt), Handler: self.handleNextLine},
{Tag: "navigation", Key: opts.GetKey(opts.Config.Universal.PrevPage), Handler: self.handlePrevPage, Description: self.c.Tr.PrevPage},
{Tag: "navigation", Key: opts.GetKey(opts.Config.Universal.NextPage), Handler: self.handleNextPage, Description: self.c.Tr.NextPage},
{Tag: "navigation", Key: opts.GetKey(opts.Config.Universal.GotoTop), Handler: self.handleGotoTop, Description: self.c.Tr.GotoTop, Alternative: "<home>"},
{Tag: "navigation", Key: opts.GetKey(opts.Config.Universal.GotoBottom), Handler: self.handleGotoBottom, Description: self.c.Tr.GotoBottom, Alternative: "<end>"},
{Tag: "navigation", Key: opts.GetKey(opts.Config.Universal.GotoTopAlt), Handler: self.handleGotoTop},
{Tag: "navigation", Key: opts.GetKey(opts.Config.Universal.GotoBottomAlt), Handler: self.handleGotoBottom},
}
}
func (self *ViewSelectionController) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding {
return []*gocui.ViewMouseBinding{}
}
func (self *ViewSelectionController) handleLineChange(delta int) {
if delta > 0 {
if manager := self.c.GetViewBufferManagerForView(self.context.GetView()); manager != nil {
manager.ReadLines(delta)
}
}
v := self.Context().GetView()
if delta < 0 {
v.ScrollUp(-delta)
} else {
v.ScrollDown(delta)
}
}
func (self *ViewSelectionController) handlePrevLine() error {
self.handleLineChange(-1)
return nil
}
func (self *ViewSelectionController) handleNextLine() error {
self.handleLineChange(1)
return nil
}
func (self *ViewSelectionController) handlePrevPage() error {
self.handleLineChange(-self.context.GetViewTrait().PageDelta())
return nil
}
func (self *ViewSelectionController) handleNextPage() error {
self.handleLineChange(self.context.GetViewTrait().PageDelta())
return nil
}
func (self *ViewSelectionController) handleGotoTop() error {
v := self.Context().GetView()
self.handleLineChange(-v.ViewLinesHeight())
return nil
}
func (self *ViewSelectionController) handleGotoBottom() error {
if manager := self.c.GetViewBufferManagerForView(self.context.GetView()); manager != nil {
manager.ReadToEnd(func() {
self.c.OnUIThread(func() error {
v := self.Context().GetView()
self.handleLineChange(v.ViewLinesHeight())
return nil
})
})
}
return nil
}
+1 -1
View File
@@ -20,7 +20,7 @@ func (gui *Gui) scrollDownView(view *gocui.View) {
scrollHeight := gui.c.UserConfig().Gui.ScrollHeight
view.ScrollDown(scrollHeight)
if manager, ok := gui.viewBufferManagerMap[view.Name()]; ok {
if manager := gui.getViewBufferManagerForView(view); manager != nil {
manager.ReadLines(scrollHeight)
}
}
+10
View File
@@ -558,6 +558,7 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs) types.Context {
FilesTrie: patricia.NewTrie(),
Authors: map[string]*models.Author{},
MainBranches: git_commands.NewMainBranches(gui.c.Common, gui.os.Cmd),
HashPool: &utils.StringPool{},
},
Modes: &types.Modes{
Filtering: filtering.New(startArgs.FilterPath, ""),
@@ -578,6 +579,15 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs) types.Context {
return initialContext(contextTree, startArgs)
}
func (self *Gui) getViewBufferManagerForView(view *gocui.View) *tasks.ViewBufferManager {
manager, ok := self.viewBufferManagerMap[view.Name()]
if !ok {
return nil
}
return manager
}
func initialWindowViewNameMap(contextTree *context.ContextTree) *utils.ThreadSafeMap[string, string] {
result := utils.NewThreadSafeMap[string, string]()
+5
View File
@@ -7,6 +7,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/tasks"
)
// hacking this by including the gui struct for now until we split more things out
@@ -128,6 +129,10 @@ func (self *guiCommon) MainViewPairs() types.MainViewPairs {
}
}
func (self *guiCommon) GetViewBufferManagerForView(view *gocui.View) *tasks.ViewBufferManager {
return self.gui.getViewBufferManagerForView(view)
}
func (self *guiCommon) State() types.IStateAccessor {
return self.gui.stateAccessor
}
+2 -2
View File
@@ -32,10 +32,10 @@ func (gui *Gui) layout(g *gocui.Gui) error {
newMainHeight := viewDimensions["main"].Y1 - viewDimensions["main"].Y0 + 1
heightDiff := newMainHeight - prevMainHeight
if heightDiff > 0 {
if manager, ok := gui.viewBufferManagerMap["main"]; ok {
if manager := gui.getViewBufferManagerForView(gui.Views.Main); manager != nil {
manager.ReadLines(heightDiff)
}
if manager, ok := gui.viewBufferManagerMap["secondary"]; ok {
if manager := gui.getViewBufferManagerForView(gui.Views.Secondary); manager != nil {
manager.ReadLines(heightDiff)
}
}
+3 -2
View File
@@ -4,6 +4,7 @@ import (
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
// State represents the selection state of the merge conflict context.
@@ -37,14 +38,14 @@ func (s *State) setConflictIndex(index int) {
if len(s.conflicts) == 0 {
s.conflictIndex = 0
} else {
s.conflictIndex = utils.Clamp(index, 0, len(s.conflicts)-1)
s.conflictIndex = lo.Clamp(index, 0, len(s.conflicts)-1)
}
s.setSelectionIndex(s.selectionIndex)
}
func (s *State) setSelectionIndex(index int) {
if selections := s.availableSelections(); len(selections) != 0 {
s.selectionIndex = utils.Clamp(index, 0, len(selections)-1)
s.selectionIndex = lo.Clamp(index, 0, len(selections)-1)
}
}
@@ -39,31 +39,27 @@ func (self *CherryPicking) SelectedHashSet() *set.Set[string] {
}
hashes := lo.Map(self.CherryPickedCommits, func(commit *models.Commit, _ int) string {
return commit.Hash
return commit.Hash()
})
return set.NewFromSlice(hashes)
}
func (self *CherryPicking) Add(selectedCommit *models.Commit, commitsList []*models.Commit) {
commitSet := self.SelectedHashSet()
commitSet.Add(selectedCommit.Hash)
commitSet.Add(selectedCommit.Hash())
self.update(commitSet, commitsList)
}
func (self *CherryPicking) Remove(selectedCommit *models.Commit, commitsList []*models.Commit) {
commitSet := self.SelectedHashSet()
commitSet.Remove(selectedCommit.Hash)
commitSet.Remove(selectedCommit.Hash())
self.update(commitSet, commitsList)
}
func (self *CherryPicking) update(selectedHashSet *set.Set[string], commitsList []*models.Commit) {
cherryPickedCommits := lo.Filter(commitsList, func(commit *models.Commit, _ int) bool {
return selectedHashSet.Includes(commit.Hash)
})
self.CherryPickedCommits = lo.Map(cherryPickedCommits, func(commit *models.Commit, _ int) *models.Commit {
return &models.Commit{Name: commit.Name, Hash: commit.Hash}
self.CherryPickedCommits = lo.Filter(commitsList, func(commit *models.Commit, _ int) bool {
return selectedHashSet.Includes(commit.Hash())
})
}
+2 -8
View File
@@ -82,16 +82,10 @@ func (self *OptionsMapMgr) renderContextOptionsMap() {
}
// Mode-specific global keybindings
if self.c.Model().WorkingTreeStateAtLastCommitRefresh.IsRebasing() {
if state := self.c.Model().WorkingTreeStateAtLastCommitRefresh; state.Any() {
optionsMap = utils.Prepend(optionsMap, bindingInfo{
key: keybindings.Label(self.c.KeybindingsOpts().Config.Universal.CreateRebaseOptionsMenu),
description: self.c.Tr.ViewRebaseOptions,
style: style.FgYellow,
})
} else if self.c.Model().WorkingTreeStateAtLastCommitRefresh.IsMerging() {
optionsMap = utils.Prepend(optionsMap, bindingInfo{
key: keybindings.Label(self.c.KeybindingsOpts().Config.Universal.CreateRebaseOptionsMenu),
description: self.c.Tr.ViewMergeOptions,
description: state.OptionsMapTitle(self.c.Tr),
style: style.FgYellow,
})
}
+4 -4
View File
@@ -21,7 +21,7 @@ type authorNameCacheKey struct {
var (
authorInitialCache = make(map[string]string)
authorNameCache = make(map[authorNameCacheKey]string)
authorStyleCache = make(map[string]style.TextStyle)
authorStyleCache = make(map[string]*style.TextStyle)
)
const authorNameWildcard = "*"
@@ -73,7 +73,7 @@ func AuthorWithLength(authorName string, length int) string {
return LongAuthor(authorName, length)
}
func AuthorStyle(authorName string) style.TextStyle {
func AuthorStyle(authorName string) *style.TextStyle {
if value, ok := authorStyleCache[authorName]; ok {
return value
}
@@ -85,9 +85,9 @@ func AuthorStyle(authorName string) style.TextStyle {
value := trueColorStyle(authorName)
authorStyleCache[authorName] = value
authorStyleCache[authorName] = &value
return value
return &value
}
func trueColorStyle(str string) style.TextStyle {
+3 -3
View File
@@ -20,7 +20,7 @@ import (
)
type colorMatcher struct {
patterns map[string]style.TextStyle
patterns map[string]*style.TextStyle
isRegex bool // NOTE: this value is needed only until the deprecated branchColors config is removed and only regex color patterns are used
}
@@ -142,14 +142,14 @@ func (m *colorMatcher) match(name string) (*style.TextStyle, bool) {
if m.isRegex {
for pattern, style := range m.patterns {
if matched, _ := regexp.MatchString(pattern, name); matched {
return &style, true
return style, true
}
}
} else {
// old behavior using the deprecated branchColors behavior matching on branch type
branchType := strings.Split(name, "/")[0]
if value, ok := m.patterns[branchType]; ok {
return &value, true
return value, true
}
}
+28 -36
View File
@@ -28,7 +28,7 @@ type pipeSetCacheKey struct {
}
var (
pipeSetCache = make(map[pipeSetCacheKey][][]*graph.Pipe)
pipeSetCache = make(map[pipeSetCacheKey][][]graph.Pipe)
mutex deadlock.Mutex
)
@@ -51,12 +51,11 @@ func GetCommitListDisplayStrings(
shortTimeFormat string,
now time.Time,
parseEmoji bool,
selectedCommitHash string,
selectedCommitHashPtr *string,
startIdx int,
endIdx int,
showGraph bool,
bisectInfo *git_commands.BisectInfo,
showYouAreHereLabel bool,
) [][]string {
mutex.Lock()
defer mutex.Unlock()
@@ -103,7 +102,7 @@ func GetCommitListDisplayStrings(
graphLines := graph.RenderAux(
graphPipeSets,
graphCommits,
selectedCommitHash,
selectedCommitHashPtr,
)
allGraphLines = append(allGraphLines, graphLines...)
}
@@ -120,7 +119,7 @@ func GetCommitListDisplayStrings(
graphLines := graph.RenderAux(
graphPipeSets,
graphCommits,
selectedCommitHash,
selectedCommitHashPtr,
)
allGraphLines = append(allGraphLines, graphLines...)
}
@@ -141,7 +140,7 @@ func GetCommitListDisplayStrings(
graphLines := graph.RenderAux(
graphPipeSets,
graphCommits,
selectedCommitHash,
selectedCommitHashPtr,
)
getGraphLine = func(idx int) string {
if idx >= graphOffset {
@@ -176,7 +175,7 @@ func GetCommitListDisplayStrings(
!lo.Contains(common.UserConfig().Git.MainBranches, b.Name) &&
// Don't show a marker for the head commit unless the
// rebase.updateRefs config is on
(hasRebaseUpdateRefsConfig || b.CommitHash != commits[0].Hash)
(hasRebaseUpdateRefsConfig || b.CommitHash != commits[0].Hash())
}))
lines := make([][]string, 0, len(filteredCommits))
@@ -184,13 +183,8 @@ func GetCommitListDisplayStrings(
willBeRebased := markedBaseCommit == ""
for i, commit := range filteredCommits {
unfilteredIdx := i + startIdx
bisectStatus = getBisectStatus(unfilteredIdx, commit.Hash, bisectInfo, bisectBounds)
isYouAreHereCommit := false
if showYouAreHereLabel && (commit.Action == models.ActionConflict || unfilteredIdx == rebaseOffset) {
isYouAreHereCommit = true
showYouAreHereLabel = false
}
isMarkedBaseCommit := commit.Hash != "" && commit.Hash == markedBaseCommit
bisectStatus = getBisectStatus(unfilteredIdx, commit.Hash(), bisectInfo, bisectBounds)
isMarkedBaseCommit := commit.Hash() != "" && commit.Hash() == markedBaseCommit
if isMarkedBaseCommit {
willBeRebased = true
}
@@ -211,7 +205,6 @@ func GetCommitListDisplayStrings(
fullDescription,
bisectStatus,
bisectInfo,
isYouAreHereCommit,
))
}
return lines
@@ -225,11 +218,11 @@ func getbisectBounds(commits []*models.Commit, bisectInfo *git_commands.BisectIn
bisectBounds := &bisectBounds{}
for i, commit := range commits {
if commit.Hash == bisectInfo.GetNewHash() {
if commit.Hash() == bisectInfo.GetNewHash() {
bisectBounds.newIndex = i
}
status, ok := bisectInfo.Status(commit.Hash)
status, ok := bisectInfo.Status(commit.Hash())
if ok && status == git_commands.BisectStatusOld {
bisectBounds.oldIndex = i
return bisectBounds
@@ -252,11 +245,11 @@ func indexOfFirstNonTODOCommit(commits []*models.Commit) int {
return 0
}
func loadPipesets(commits []*models.Commit) [][]*graph.Pipe {
func loadPipesets(commits []*models.Commit) [][]graph.Pipe {
// given that our cache key is a commit hash and a commit count, it's very important that we don't actually try to render pipes
// when dealing with things like filtered commits.
cacheKey := pipeSetCacheKey{
commitHash: commits[0].Hash,
commitHash: commits[0].Hash(),
commitCount: len(commits),
divergence: commits[0].Divergence,
}
@@ -265,7 +258,7 @@ func loadPipesets(commits []*models.Commit) [][]*graph.Pipe {
if !ok {
// pipe sets are unique to a commit head. and a commit count. Sometimes we haven't loaded everything for that.
// so let's just cache it based on that.
getStyle := func(commit *models.Commit) style.TextStyle {
getStyle := func(commit *models.Commit) *style.TextStyle {
return authors.AuthorStyle(commit.AuthorName)
}
pipeSets = graph.GetPipeSets(commits, getStyle)
@@ -364,17 +357,16 @@ func displayCommit(
fullDescription bool,
bisectStatus BisectStatus,
bisectInfo *git_commands.BisectInfo,
isYouAreHereCommit bool,
) []string {
bisectString := getBisectStatusText(bisectStatus, bisectInfo)
hashString := ""
hashColor := getHashColor(commit, diffName, cherryPickedCommitHashSet, bisectStatus, bisectInfo)
hashLength := common.UserConfig().Gui.CommitHashLength
if hashLength >= len(commit.Hash) {
hashString = hashColor.Sprint(commit.Hash)
if hashLength >= len(commit.Hash()) {
hashString = hashColor.Sprint(commit.Hash())
} else if hashLength > 0 {
hashString = hashColor.Sprint(commit.Hash[:hashLength])
hashString = hashColor.Sprint(commit.Hash()[:hashLength])
} else if !icons.IsIconEnabled() { // hashLength <= 0
hashString = hashColor.Sprint("*")
}
@@ -395,8 +387,7 @@ func displayCommit(
actionString := ""
if commit.Action != models.ActionNone {
todoString := lo.Ternary(commit.Action == models.ActionConflict, "conflict", commit.Action.String())
actionString = actionColorMap(commit.Action).Sprint(todoString) + " "
actionString = actionColorMap(commit.Action, commit.Status).Sprint(commit.Action.String())
}
tagString := ""
@@ -409,7 +400,7 @@ func displayCommit(
tagString = theme.DiffTerminalColor.SetBold().Sprint(strings.Join(commit.Tags, " ")) + " "
}
if branchHeadsToVisualize.Includes(commit.Hash) &&
if branchHeadsToVisualize.Includes(commit.Hash()) &&
// Don't show branch head on commits that are already merged to a main branch
commit.Status != models.StatusMerged &&
// Don't show branch head on a "pick" todo if the rebase.updateRefs config is on
@@ -428,9 +419,8 @@ func displayCommit(
}
mark := ""
if isYouAreHereCommit {
color := lo.Ternary(commit.Action == models.ActionConflict, style.FgRed, style.FgYellow)
youAreHere := color.Sprintf("<-- %s ---", common.Tr.YouAreHere)
if commit.Status == models.StatusConflicted {
youAreHere := style.FgRed.Sprintf("<-- %s ---", common.Tr.ConflictLabel)
mark = fmt.Sprintf("%s ", youAreHere)
} else if isMarkedBaseCommit {
rebaseFromHere := style.FgYellow.Sprint(common.Tr.MarkedCommitMarker)
@@ -492,7 +482,7 @@ func getHashColor(
return getBisectStatusColor(bisectStatus)
}
diffed := commit.Hash != "" && commit.Hash == diffName
diffed := commit.Hash() != "" && commit.Hash() == diffName
hashColor := theme.DefaultTextColor
switch commit.Status {
case models.StatusUnpushed:
@@ -501,7 +491,7 @@ func getHashColor(
hashColor = style.FgYellow
case models.StatusMerged:
hashColor = style.FgGreen
case models.StatusRebasing:
case models.StatusRebasing, models.StatusCherryPickingOrReverting, models.StatusConflicted:
hashColor = style.FgBlue
case models.StatusReflog:
hashColor = style.FgBlue
@@ -510,7 +500,7 @@ func getHashColor(
if diffed {
hashColor = theme.DiffTerminalColor
} else if cherryPickedCommitHashSet.Includes(commit.Hash) {
} else if cherryPickedCommitHashSet.Includes(commit.Hash()) {
hashColor = theme.CherryPickedCommitTextStyle
} else if commit.Divergence == models.DivergenceRight && commit.Status != models.StatusMerged {
hashColor = style.FgBlue
@@ -519,7 +509,11 @@ func getHashColor(
return hashColor
}
func actionColorMap(action todo.TodoCommand) style.TextStyle {
func actionColorMap(action todo.TodoCommand, status models.CommitStatus) style.TextStyle {
if status == models.StatusConflicted {
return style.FgRed
}
switch action {
case todo.Pick:
return style.FgCyan
@@ -529,8 +523,6 @@ func actionColorMap(action todo.TodoCommand) style.TextStyle {
return style.FgGreen
case todo.Fixup:
return style.FgMagenta
case models.ActionConflict:
return style.FgRed
default:
return style.FgYellow
}
+45 -74
View File
@@ -11,6 +11,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
"github.com/stefanhaller/git-todo-parser/todo"
"github.com/stretchr/testify/assert"
"github.com/xo/terminfo"
@@ -23,7 +24,7 @@ func formatExpected(expected string) string {
func TestGetCommitListDisplayStrings(t *testing.T) {
scenarios := []struct {
testName string
commits []*models.Commit
commitOpts []models.NewCommitOpts
branches []*models.Branch
currentBranchName string
hasUpdateRefConfig bool
@@ -35,18 +36,17 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
shortTimeFormat string
now time.Time
parseEmoji bool
selectedCommitHash string
selectedCommitHashPtr *string
startIdx int
endIdx int
showGraph bool
bisectInfo *git_commands.BisectInfo
showYouAreHereLabel bool
expected string
focus bool
}{
{
testName: "no commits",
commits: []*models.Commit{},
commitOpts: []models.NewCommitOpts{},
startIdx: 0,
endIdx: 1,
showGraph: false,
@@ -57,7 +57,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
},
{
testName: "some commits",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Name: "commit1", Hash: "hash1"},
{Name: "commit2", Hash: "hash2"},
},
@@ -74,7 +74,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
},
{
testName: "commit with tags",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Name: "commit1", Hash: "hash1", Tags: []string{"tag1", "tag2"}},
{Name: "commit2", Hash: "hash2"},
},
@@ -91,7 +91,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
},
{
testName: "show local branch head, except the current branch, main branches, or merged branches",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Name: "commit1", Hash: "hash1"},
{Name: "commit2", Hash: "hash2"},
{Name: "commit3", Hash: "hash3"},
@@ -120,7 +120,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
},
{
testName: "show local branch head for head commit if updateRefs is on",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Name: "commit1", Hash: "hash1"},
{Name: "commit2", Hash: "hash2"},
},
@@ -143,7 +143,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
},
{
testName: "don't show local branch head for head commit if updateRefs is off",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Name: "commit1", Hash: "hash1"},
{Name: "commit2", Hash: "hash2"},
},
@@ -166,7 +166,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
},
{
testName: "show local branch head and tag if both exist",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Name: "commit1", Hash: "hash1"},
{Name: "commit2", Hash: "hash2", Tags: []string{"some-tag"}},
{Name: "commit3", Hash: "hash3"},
@@ -188,7 +188,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
},
{
testName: "showing graph",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Name: "commit1", Hash: "hash1", Parents: []string{"hash2", "hash3"}},
{Name: "commit2", Hash: "hash2", Parents: []string{"hash3"}},
{Name: "commit3", Hash: "hash3", Parents: []string{"hash4"}},
@@ -211,7 +211,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
},
{
testName: "showing graph, including rebase commits",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Name: "commit1", Hash: "hash1", Parents: []string{"hash2", "hash3"}, Action: todo.Pick},
{Name: "commit2", Hash: "hash2", Parents: []string{"hash3"}, Action: todo.Pick},
{Name: "commit3", Hash: "hash3", Parents: []string{"hash4"}},
@@ -223,19 +223,18 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
showGraph: true,
bisectInfo: git_commands.NewNullBisectInfo(),
cherryPickedCommitHashSet: set.New[string](),
showYouAreHereLabel: true,
now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
expected: formatExpected(`
hash1 pick commit1
hash2 pick commit2
hash3 ◯ <-- YOU ARE HERE --- commit3
hash4 ◯ commit4
hash5 ◯ commit5
hash1 pick commit1
hash2 pick commit2
hash3 ◯ commit3
hash4 ◯ commit4
hash5 ◯ commit5
`),
},
{
testName: "showing graph, including rebase commits, with offset",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Name: "commit1", Hash: "hash1", Parents: []string{"hash2", "hash3"}, Action: todo.Pick},
{Name: "commit2", Hash: "hash2", Parents: []string{"hash3"}, Action: todo.Pick},
{Name: "commit3", Hash: "hash3", Parents: []string{"hash4"}},
@@ -247,18 +246,17 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
showGraph: true,
bisectInfo: git_commands.NewNullBisectInfo(),
cherryPickedCommitHashSet: set.New[string](),
showYouAreHereLabel: true,
now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
expected: formatExpected(`
hash2 pick commit2
hash3 ◯ <-- YOU ARE HERE --- commit3
hash4 ◯ commit4
hash5 ◯ commit5
hash2 pick commit2
hash3 ◯ commit3
hash4 ◯ commit4
hash5 ◯ commit5
`),
},
{
testName: "startIdx is past TODO commits",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Name: "commit1", Hash: "hash1", Parents: []string{"hash2", "hash3"}, Action: todo.Pick},
{Name: "commit2", Hash: "hash2", Parents: []string{"hash3"}, Action: todo.Pick},
{Name: "commit3", Hash: "hash3", Parents: []string{"hash4"}},
@@ -270,7 +268,6 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
showGraph: true,
bisectInfo: git_commands.NewNullBisectInfo(),
cherryPickedCommitHashSet: set.New[string](),
showYouAreHereLabel: true,
now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
expected: formatExpected(`
hash4 ◯ commit4
@@ -279,7 +276,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
},
{
testName: "only showing TODO commits",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Name: "commit1", Hash: "hash1", Parents: []string{"hash2", "hash3"}, Action: todo.Pick},
{Name: "commit2", Hash: "hash2", Parents: []string{"hash3"}, Action: todo.Pick},
{Name: "commit3", Hash: "hash3", Parents: []string{"hash4"}},
@@ -291,16 +288,15 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
showGraph: true,
bisectInfo: git_commands.NewNullBisectInfo(),
cherryPickedCommitHashSet: set.New[string](),
showYouAreHereLabel: true,
now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
expected: formatExpected(`
hash1 pick commit1
hash2 pick commit2
hash1 pick commit1
hash2 pick commit2
`),
},
{
testName: "no TODO commits, towards bottom",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Name: "commit1", Hash: "hash1", Parents: []string{"hash2", "hash3"}},
{Name: "commit2", Hash: "hash2", Parents: []string{"hash3"}},
{Name: "commit3", Hash: "hash3", Parents: []string{"hash4"}},
@@ -312,7 +308,6 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
showGraph: true,
bisectInfo: git_commands.NewNullBisectInfo(),
cherryPickedCommitHashSet: set.New[string](),
showYouAreHereLabel: true,
now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
expected: formatExpected(`
hash5 ◯ commit5
@@ -320,7 +315,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
},
{
testName: "only TODO commits except last",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Name: "commit1", Hash: "hash1", Parents: []string{"hash2", "hash3"}, Action: todo.Pick},
{Name: "commit2", Hash: "hash2", Parents: []string{"hash3"}, Action: todo.Pick},
{Name: "commit3", Hash: "hash3", Parents: []string{"hash4"}, Action: todo.Pick},
@@ -332,36 +327,15 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
showGraph: true,
bisectInfo: git_commands.NewNullBisectInfo(),
cherryPickedCommitHashSet: set.New[string](),
showYouAreHereLabel: true,
now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
expected: formatExpected(`
hash1 pick commit1
hash2 pick commit2
`),
},
{
testName: "don't show YOU ARE HERE label when not asked for (e.g. in branches panel)",
commits: []*models.Commit{
{Name: "commit1", Hash: "hash1", Parents: []string{"hash2"}, Action: todo.Pick},
{Name: "commit2", Hash: "hash2", Parents: []string{"hash3"}},
{Name: "commit3", Hash: "hash3", Parents: []string{"hash4"}},
},
startIdx: 0,
endIdx: 3,
showGraph: true,
bisectInfo: git_commands.NewNullBisectInfo(),
cherryPickedCommitHashSet: set.New[string](),
showYouAreHereLabel: false,
now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
expected: formatExpected(`
hash1 pick commit1
hash2 ◯ commit2
hash3 ◯ commit3
hash1 pick commit1
hash2 pick commit2
`),
},
{
testName: "graph in divergence view - all commits visible",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Name: "commit1", Hash: "hash1r", Parents: []string{"hash2r"}, Divergence: models.DivergenceRight},
{Name: "commit2", Hash: "hash2r", Parents: []string{"hash3r", "hash5r"}, Divergence: models.DivergenceRight},
{Name: "commit3", Hash: "hash3r", Parents: []string{"hash4r"}, Divergence: models.DivergenceRight},
@@ -376,7 +350,6 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
showGraph: true,
bisectInfo: git_commands.NewNullBisectInfo(),
cherryPickedCommitHashSet: set.New[string](),
showYouAreHereLabel: false,
now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
expected: formatExpected(`
↓ hash1r ◯ commit1
@@ -391,7 +364,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
},
{
testName: "graph in divergence view - not all remote commits visible",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Name: "commit1", Hash: "hash1r", Parents: []string{"hash2r"}, Divergence: models.DivergenceRight},
{Name: "commit2", Hash: "hash2r", Parents: []string{"hash3r", "hash5r"}, Divergence: models.DivergenceRight},
{Name: "commit3", Hash: "hash3r", Parents: []string{"hash4r"}, Divergence: models.DivergenceRight},
@@ -406,7 +379,6 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
showGraph: true,
bisectInfo: git_commands.NewNullBisectInfo(),
cherryPickedCommitHashSet: set.New[string](),
showYouAreHereLabel: false,
now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
expected: formatExpected(`
↓ hash3r ◯ │ commit3
@@ -419,7 +391,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
},
{
testName: "graph in divergence view - not all local commits",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Name: "commit1", Hash: "hash1r", Parents: []string{"hash2r"}, Divergence: models.DivergenceRight},
{Name: "commit2", Hash: "hash2r", Parents: []string{"hash3r", "hash5r"}, Divergence: models.DivergenceRight},
{Name: "commit3", Hash: "hash3r", Parents: []string{"hash4r"}, Divergence: models.DivergenceRight},
@@ -434,7 +406,6 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
showGraph: true,
bisectInfo: git_commands.NewNullBisectInfo(),
cherryPickedCommitHashSet: set.New[string](),
showYouAreHereLabel: false,
now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
expected: formatExpected(`
↓ hash1r ◯ commit1
@@ -446,7 +417,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
},
{
testName: "graph in divergence view - no remote commits visible",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Name: "commit1", Hash: "hash1r", Parents: []string{"hash2r"}, Divergence: models.DivergenceRight},
{Name: "commit2", Hash: "hash2r", Parents: []string{"hash3r", "hash5r"}, Divergence: models.DivergenceRight},
{Name: "commit3", Hash: "hash3r", Parents: []string{"hash4r"}, Divergence: models.DivergenceRight},
@@ -461,7 +432,6 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
showGraph: true,
bisectInfo: git_commands.NewNullBisectInfo(),
cherryPickedCommitHashSet: set.New[string](),
showYouAreHereLabel: false,
now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
expected: formatExpected(`
↑ hash2l ⏣─╮ commit2
@@ -472,7 +442,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
},
{
testName: "graph in divergence view - no local commits visible",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Name: "commit1", Hash: "hash1r", Parents: []string{"hash2r"}, Divergence: models.DivergenceRight},
{Name: "commit2", Hash: "hash2r", Parents: []string{"hash3r", "hash5r"}, Divergence: models.DivergenceRight},
{Name: "commit3", Hash: "hash3r", Parents: []string{"hash4r"}, Divergence: models.DivergenceRight},
@@ -487,7 +457,6 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
showGraph: true,
bisectInfo: git_commands.NewNullBisectInfo(),
cherryPickedCommitHashSet: set.New[string](),
showYouAreHereLabel: false,
now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
expected: formatExpected(`
↓ hash1r ◯ commit1
@@ -496,7 +465,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
},
{
testName: "graph in divergence view - no remote commits present",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Name: "commit1", Hash: "hash1l", Parents: []string{"hash2l"}, Divergence: models.DivergenceLeft},
{Name: "commit2", Hash: "hash2l", Parents: []string{"hash3l", "hash4l"}, Divergence: models.DivergenceLeft},
{Name: "commit3", Hash: "hash3l", Parents: []string{"hash4l"}, Divergence: models.DivergenceLeft},
@@ -508,7 +477,6 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
showGraph: true,
bisectInfo: git_commands.NewNullBisectInfo(),
cherryPickedCommitHashSet: set.New[string](),
showYouAreHereLabel: false,
now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
expected: formatExpected(`
↑ hash1l ◯ commit1
@@ -520,7 +488,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
},
{
testName: "graph in divergence view - no local commits present",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Name: "commit1", Hash: "hash1r", Parents: []string{"hash2r"}, Divergence: models.DivergenceRight},
{Name: "commit2", Hash: "hash2r", Parents: []string{"hash3r", "hash5r"}, Divergence: models.DivergenceRight},
{Name: "commit3", Hash: "hash3r", Parents: []string{"hash4r"}, Divergence: models.DivergenceRight},
@@ -530,7 +498,6 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
showGraph: true,
bisectInfo: git_commands.NewNullBisectInfo(),
cherryPickedCommitHashSet: set.New[string](),
showYouAreHereLabel: false,
now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
expected: formatExpected(`
↓ hash1r ◯ commit1
@@ -540,7 +507,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
},
{
testName: "custom time format",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Name: "commit1", Hash: "hash1", UnixTimestamp: 1577844184, AuthorName: "Jesse Duffield"},
{Name: "commit2", Hash: "hash2", UnixTimestamp: 1576844184, AuthorName: "Jesse Duffield"},
},
@@ -577,9 +544,14 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
for _, s := range scenarios {
if !focusing || s.focus {
t.Run(s.testName, func(t *testing.T) {
hashPool := &utils.StringPool{}
commits := lo.Map(s.commitOpts,
func(opts models.NewCommitOpts, _ int) *models.Commit { return models.NewCommit(hashPool, opts) })
result := GetCommitListDisplayStrings(
common,
s.commits,
commits,
s.branches,
s.currentBranchName,
s.hasUpdateRefConfig,
@@ -591,12 +563,11 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
s.shortTimeFormat,
s.now,
s.parseEmoji,
s.selectedCommitHash,
s.selectedCommitHashPtr,
s.startIdx,
s.endIdx,
s.showGraph,
s.bisectInfo,
s.showYouAreHereLabel,
)
renderedLines, _ := utils.RenderDisplayStrings(result, nil)
+3 -1
View File
@@ -149,8 +149,10 @@ func TestRenderCommitFileTree(t *testing.T) {
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
hashPool := &utils.StringPool{}
viewModel := filetree.NewCommitFileTreeViewModel(func() []*models.CommitFile { return s.files }, utils.NewDummyLog(), true)
viewModel.SetRef(&models.Commit{})
viewModel.SetRef(models.NewCommit(hashPool, models.NewCommitOpts{Hash: "1234"}))
viewModel.SetTree()
for _, path := range s.collapsedPaths {
viewModel.ToggleCollapsed(path)
+9 -9
View File
@@ -25,7 +25,7 @@ type Cell struct {
up, down, left, right bool
cellType cellType
rightStyle *style.TextStyle
style style.TextStyle
style *style.TextStyle
}
func (cell *Cell) render(writer io.StringWriter) {
@@ -44,7 +44,7 @@ func (cell *Cell) render(writer io.StringWriter) {
var rightStyle *style.TextStyle
if cell.rightStyle == nil {
rightStyle = &cell.style
rightStyle = cell.style
} else {
rightStyle = cell.rightStyle
}
@@ -59,7 +59,7 @@ func (cell *Cell) render(writer io.StringWriter) {
styledSecondChar = cachedSprint(*rightStyle, second)
}
_, _ = writer.WriteString(cachedSprint(cell.style, adjustedFirst))
_, _ = writer.WriteString(cachedSprint(*cell.style, adjustedFirst))
_, _ = writer.WriteString(styledSecondChar)
}
@@ -104,19 +104,19 @@ func (cell *Cell) reset() {
cell.right = false
}
func (cell *Cell) setUp(style style.TextStyle) *Cell {
func (cell *Cell) setUp(style *style.TextStyle) *Cell {
cell.up = true
cell.style = style
return cell
}
func (cell *Cell) setDown(style style.TextStyle) *Cell {
func (cell *Cell) setDown(style *style.TextStyle) *Cell {
cell.down = true
cell.style = style
return cell
}
func (cell *Cell) setLeft(style style.TextStyle) *Cell {
func (cell *Cell) setLeft(style *style.TextStyle) *Cell {
cell.left = true
if !cell.up && !cell.down {
// vertical trumps left
@@ -126,15 +126,15 @@ func (cell *Cell) setLeft(style style.TextStyle) *Cell {
}
//nolint:unparam
func (cell *Cell) setRight(style style.TextStyle, override bool) *Cell {
func (cell *Cell) setRight(style *style.TextStyle, override bool) *Cell {
cell.right = true
if cell.rightStyle == nil || override {
cell.rightStyle = &style
cell.rightStyle = style
}
return cell
}
func (cell *Cell) setStyle(style style.TextStyle) *Cell {
func (cell *Cell) setStyle(style *style.TextStyle) *Cell {
cell.style = style
return cell
}
+82 -90
View File
@@ -23,58 +23,53 @@ const (
)
type Pipe struct {
fromPos int
toPos int
fromHash string
toHash string
fromHash *string
toHash *string
style *style.TextStyle
fromPos int16
toPos int16
kind PipeKind
style style.TextStyle
}
var highlightStyle = style.FgLightWhite.SetBold()
var (
highlightStyle = style.FgLightWhite.SetBold()
EmptyTreeCommitHash = models.EmptyTreeCommitHash
StartCommitHash = "START"
)
func ContainsCommitHash(pipes []*Pipe, hash string) bool {
for _, pipe := range pipes {
if equalHashes(pipe.fromHash, hash) {
return true
}
}
return false
}
func (self Pipe) left() int {
func (self Pipe) left() int16 {
return min(self.fromPos, self.toPos)
}
func (self Pipe) right() int {
func (self Pipe) right() int16 {
return max(self.fromPos, self.toPos)
}
func RenderCommitGraph(commits []*models.Commit, selectedCommitHash string, getStyle func(c *models.Commit) style.TextStyle) []string {
func RenderCommitGraph(commits []*models.Commit, selectedCommitHashPtr *string, getStyle func(c *models.Commit) *style.TextStyle) []string {
pipeSets := GetPipeSets(commits, getStyle)
if len(pipeSets) == 0 {
return nil
}
lines := RenderAux(pipeSets, commits, selectedCommitHash)
lines := RenderAux(pipeSets, commits, selectedCommitHashPtr)
return lines
}
func GetPipeSets(commits []*models.Commit, getStyle func(c *models.Commit) style.TextStyle) [][]*Pipe {
func GetPipeSets(commits []*models.Commit, getStyle func(c *models.Commit) *style.TextStyle) [][]Pipe {
if len(commits) == 0 {
return nil
}
pipes := []*Pipe{{fromPos: 0, toPos: 0, fromHash: "START", toHash: commits[0].Hash, kind: STARTS, style: style.FgDefault}}
pipes := []Pipe{{fromPos: 0, toPos: 0, fromHash: &StartCommitHash, toHash: commits[0].HashPtr(), kind: STARTS, style: &style.FgDefault}}
return lo.Map(commits, func(commit *models.Commit, _ int) []*Pipe {
return lo.Map(commits, func(commit *models.Commit, _ int) []Pipe {
pipes = getNextPipes(pipes, commit, getStyle)
return pipes
})
}
func RenderAux(pipeSets [][]*Pipe, commits []*models.Commit, selectedCommitHash string) []string {
func RenderAux(pipeSets [][]Pipe, commits []*models.Commit, selectedCommitHashPtr *string) []string {
maxProcs := runtime.GOMAXPROCS(0)
// splitting up the rendering of the graph into multiple goroutines allows us to render the graph in parallel
@@ -98,7 +93,7 @@ func RenderAux(pipeSets [][]*Pipe, commits []*models.Commit, selectedCommitHash
if k > 0 {
prevCommit = commits[k-1]
}
line := renderPipeSet(pipeSet, selectedCommitHash, prevCommit)
line := renderPipeSet(pipeSet, selectedCommitHashPtr, prevCommit)
innerLines = append(innerLines, line)
}
chunks[i] = innerLines
@@ -111,8 +106,8 @@ func RenderAux(pipeSets [][]*Pipe, commits []*models.Commit, selectedCommitHash
return lo.Flatten(chunks)
}
func getNextPipes(prevPipes []*Pipe, commit *models.Commit, getStyle func(c *models.Commit) style.TextStyle) []*Pipe {
maxPos := 0
func getNextPipes(prevPipes []Pipe, commit *models.Commit, getStyle func(c *models.Commit) *style.TextStyle) []Pipe {
maxPos := int16(0)
for _, pipe := range prevPipes {
if pipe.toPos > maxPos {
maxPos = pipe.toPos
@@ -121,16 +116,16 @@ func getNextPipes(prevPipes []*Pipe, commit *models.Commit, getStyle func(c *mod
// a pipe that terminated in the previous line has no bearing on the current line
// so we'll filter those out
currentPipes := lo.Filter(prevPipes, func(pipe *Pipe, _ int) bool {
currentPipes := lo.Filter(prevPipes, func(pipe Pipe, _ int) bool {
return pipe.kind != TERMINATES
})
newPipes := make([]*Pipe, 0, len(currentPipes)+len(commit.Parents))
newPipes := make([]Pipe, 0, len(currentPipes)+len(commit.ParentPtrs()))
// start by assuming that we've got a brand new commit not related to any preceding commit.
// (this only happens when we're doing `git log --all`). These will be tacked onto the far end.
pos := maxPos + 1
for _, pipe := range currentPipes {
if equalHashes(pipe.toHash, commit.Hash) {
if equalHashes(pipe.toHash, commit.HashPtr()) {
// turns out this commit does have a descendant so we'll place it right under the first instance
pos = pipe.toPos
break
@@ -138,74 +133,72 @@ func getNextPipes(prevPipes []*Pipe, commit *models.Commit, getStyle func(c *mod
}
// a taken spot is one where a current pipe is ending on
// Note: this set and similar ones below use int instead of int16 because
// that's much more efficient. We cast the int16 values we store in these
// sets to int on every access.
takenSpots := set.New[int]()
// a traversed spot is one where a current pipe is starting on, ending on, or passing through
traversedSpots := set.New[int]()
if len(commit.Parents) > 0 { // merge commit
newPipes = append(newPipes, &Pipe{
fromPos: pos,
toPos: pos,
fromHash: commit.Hash,
toHash: commit.Parents[0],
kind: STARTS,
style: getStyle(commit),
})
} else if len(commit.Parents) == 0 { // root commit
newPipes = append(newPipes, &Pipe{
fromPos: pos,
toPos: pos,
fromHash: commit.Hash,
toHash: models.EmptyTreeCommitHash,
kind: STARTS,
style: getStyle(commit),
})
var toHash *string
if commit.IsFirstCommit() {
toHash = &EmptyTreeCommitHash
} else {
toHash = commit.ParentPtrs()[0]
}
newPipes = append(newPipes, Pipe{
fromPos: pos,
toPos: pos,
fromHash: commit.HashPtr(),
toHash: toHash,
kind: STARTS,
style: getStyle(commit),
})
traversedSpotsForContinuingPipes := set.New[int]()
for _, pipe := range currentPipes {
if !equalHashes(pipe.toHash, commit.Hash) {
traversedSpotsForContinuingPipes.Add(pipe.toPos)
if !equalHashes(pipe.toHash, commit.HashPtr()) {
traversedSpotsForContinuingPipes.Add(int(pipe.toPos))
}
}
getNextAvailablePosForContinuingPipe := func() int {
i := 0
getNextAvailablePosForContinuingPipe := func() int16 {
i := int16(0)
for {
if !traversedSpots.Includes(i) {
if !traversedSpots.Includes(int(i)) {
return i
}
i++
}
}
getNextAvailablePosForNewPipe := func() int {
i := 0
getNextAvailablePosForNewPipe := func() int16 {
i := int16(0)
for {
// a newly created pipe is not allowed to end on a spot that's already taken,
// nor on a spot that's been traversed by a continuing pipe.
if !takenSpots.Includes(i) && !traversedSpotsForContinuingPipes.Includes(i) {
if !takenSpots.Includes(int(i)) && !traversedSpotsForContinuingPipes.Includes(int(i)) {
return i
}
i++
}
}
traverse := func(from, to int) {
traverse := func(from, to int16) {
left, right := from, to
if left > right {
left, right = right, left
}
for i := left; i <= right; i++ {
traversedSpots.Add(i)
traversedSpots.Add(int(i))
}
takenSpots.Add(to)
takenSpots.Add(int(to))
}
for _, pipe := range currentPipes {
if equalHashes(pipe.toHash, commit.Hash) {
if equalHashes(pipe.toHash, commit.HashPtr()) {
// terminating here
newPipes = append(newPipes, &Pipe{
newPipes = append(newPipes, Pipe{
fromPos: pipe.toPos,
toPos: pos,
fromHash: pipe.fromHash,
@@ -217,7 +210,7 @@ func getNextPipes(prevPipes []*Pipe, commit *models.Commit, getStyle func(c *mod
} else if pipe.toPos < pos {
// continuing here
availablePos := getNextAvailablePosForContinuingPipe()
newPipes = append(newPipes, &Pipe{
newPipes = append(newPipes, Pipe{
fromPos: pipe.toPos,
toPos: availablePos,
fromHash: pipe.fromHash,
@@ -230,34 +223,34 @@ func getNextPipes(prevPipes []*Pipe, commit *models.Commit, getStyle func(c *mod
}
if commit.IsMerge() {
for _, parent := range commit.Parents[1:] {
for _, parent := range commit.ParentPtrs()[1:] {
availablePos := getNextAvailablePosForNewPipe()
// need to act as if continuing pipes are going to continue on the same line.
newPipes = append(newPipes, &Pipe{
newPipes = append(newPipes, Pipe{
fromPos: pos,
toPos: availablePos,
fromHash: commit.Hash,
fromHash: commit.HashPtr(),
toHash: parent,
kind: STARTS,
style: getStyle(commit),
})
takenSpots.Add(availablePos)
takenSpots.Add(int(availablePos))
}
}
for _, pipe := range currentPipes {
if !equalHashes(pipe.toHash, commit.Hash) && pipe.toPos > pos {
if !equalHashes(pipe.toHash, commit.HashPtr()) && pipe.toPos > pos {
// continuing on, potentially moving left to fill in a blank spot
last := pipe.toPos
for i := pipe.toPos; i > pos; i-- {
if takenSpots.Includes(i) || traversedSpots.Includes(i) {
if takenSpots.Includes(int(i)) || traversedSpots.Includes(int(i)) {
break
} else {
last = i
}
}
newPipes = append(newPipes, &Pipe{
newPipes = append(newPipes, Pipe{
fromPos: pipe.toPos,
toPos: last,
fromHash: pipe.fromHash,
@@ -270,7 +263,7 @@ func getNextPipes(prevPipes []*Pipe, commit *models.Commit, getStyle func(c *mod
}
// not efficient but doing it for now: sorting my pipes by toPos, then by kind
slices.SortFunc(newPipes, func(a, b *Pipe) int {
slices.SortFunc(newPipes, func(a, b Pipe) int {
if a.toPos == b.toPos {
return cmp.Compare(a.kind, b.kind)
}
@@ -281,12 +274,12 @@ func getNextPipes(prevPipes []*Pipe, commit *models.Commit, getStyle func(c *mod
}
func renderPipeSet(
pipes []*Pipe,
selectedCommitHash string,
pipes []Pipe,
selectedCommitHashPtr *string,
prevCommit *models.Commit,
) string {
maxPos := 0
commitPos := 0
maxPos := int16(0)
commitPos := int16(0)
startCount := 0
for _, pipe := range pipes {
if pipe.kind == STARTS {
@@ -302,11 +295,11 @@ func renderPipeSet(
}
isMerge := startCount > 1
cells := lo.Map(lo.Range(maxPos+1), func(i int, _ int) *Cell {
return &Cell{cellType: CONNECTION, style: style.FgDefault}
cells := lo.Map(lo.Range(int(maxPos)+1), func(i int, _ int) *Cell {
return &Cell{cellType: CONNECTION, style: &style.FgDefault}
})
renderPipe := func(pipe *Pipe, style style.TextStyle, overrideRightStyle bool) {
renderPipe := func(pipe *Pipe, style *style.TextStyle, overrideRightStyle bool) {
left := pipe.left()
right := pipe.right()
@@ -329,10 +322,10 @@ func renderPipeSet(
// we don't want to highlight two commits if they're contiguous. We only want
// to highlight multiple things if there's an actual visible pipe involved.
highlight := true
if prevCommit != nil && equalHashes(prevCommit.Hash, selectedCommitHash) {
if prevCommit != nil && equalHashes(prevCommit.HashPtr(), selectedCommitHashPtr) {
highlight = false
for _, pipe := range pipes {
if equalHashes(pipe.fromHash, selectedCommitHash) && (pipe.kind != TERMINATES || pipe.fromPos != pipe.toPos) {
if equalHashes(pipe.fromHash, selectedCommitHashPtr) && (pipe.kind != TERMINATES || pipe.fromPos != pipe.toPos) {
highlight = true
}
}
@@ -340,19 +333,19 @@ func renderPipeSet(
// so we have our commit pos again, now it's time to build the cells.
// we'll handle the one that's sourced from our selected commit last so that it can override the other cells.
selectedPipes, nonSelectedPipes := utils.Partition(pipes, func(pipe *Pipe) bool {
return highlight && equalHashes(pipe.fromHash, selectedCommitHash)
selectedPipes, nonSelectedPipes := utils.Partition(pipes, func(pipe Pipe) bool {
return highlight && equalHashes(pipe.fromHash, selectedCommitHashPtr)
})
for _, pipe := range nonSelectedPipes {
if pipe.kind == STARTS {
renderPipe(pipe, pipe.style, true)
renderPipe(&pipe, pipe.style, true)
}
}
for _, pipe := range nonSelectedPipes {
if pipe.kind != STARTS && !(pipe.kind == TERMINATES && pipe.fromPos == commitPos && pipe.toPos == commitPos) {
renderPipe(pipe, pipe.style, false)
renderPipe(&pipe, pipe.style, false)
}
}
@@ -362,9 +355,9 @@ func renderPipeSet(
}
}
for _, pipe := range selectedPipes {
renderPipe(pipe, highlightStyle, true)
renderPipe(&pipe, &highlightStyle, true)
if pipe.toPos == commitPos {
cells[pipe.toPos].setStyle(highlightStyle)
cells[pipe.toPos].setStyle(&highlightStyle)
}
}
@@ -384,13 +377,12 @@ func renderPipeSet(
return writer.String()
}
func equalHashes(a, b string) bool {
// if our selectedCommitHash is an empty string we treat that as meaning there is no selected commit hash
if a == "" || b == "" {
func equalHashes(a, b *string) bool {
// if our selectedCommitHashPtr is nil, there is no selected commit
if a == nil || b == nil {
return false
}
length := min(len(a), len(b))
// parent hashes are only stored up to 20 characters for some reason so we'll truncate to that for comparison
return a[:length] == b[:length]
// We know that all hashes are stored in the pool, so we can compare their addresses
return a == b
}
+153 -134
View File
@@ -11,6 +11,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui/presentation/authors"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
"github.com/xo/terminfo"
)
@@ -18,12 +19,12 @@ import (
func TestRenderCommitGraph(t *testing.T) {
tests := []struct {
name string
commits []*models.Commit
commitOpts []models.NewCommitOpts
expectedOutput string
}{
{
name: "with some merges",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Hash: "1", Parents: []string{"2"}},
{Hash: "2", Parents: []string{"3"}},
{Hash: "3", Parents: []string{"4"}},
@@ -57,7 +58,7 @@ func TestRenderCommitGraph(t *testing.T) {
},
{
name: "with a path that has room to move to the left",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Hash: "1", Parents: []string{"2"}},
{Hash: "2", Parents: []string{"3", "4"}},
{Hash: "4", Parents: []string{"3", "5"}},
@@ -75,7 +76,7 @@ func TestRenderCommitGraph(t *testing.T) {
},
{
name: "with a new commit",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Hash: "1", Parents: []string{"2"}},
{Hash: "2", Parents: []string{"3", "4"}},
{Hash: "4", Parents: []string{"3", "5"}},
@@ -95,7 +96,7 @@ func TestRenderCommitGraph(t *testing.T) {
},
{
name: "with a path that has room to move to the left and continues",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Hash: "1", Parents: []string{"2"}},
{Hash: "2", Parents: []string{"3", "4"}},
{Hash: "3", Parents: []string{"5", "4"}},
@@ -113,7 +114,7 @@ func TestRenderCommitGraph(t *testing.T) {
},
{
name: "with a path that has room to move to the left and continues",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Hash: "1", Parents: []string{"2"}},
{Hash: "2", Parents: []string{"3", "4"}},
{Hash: "3", Parents: []string{"5", "4"}},
@@ -133,7 +134,7 @@ func TestRenderCommitGraph(t *testing.T) {
},
{
name: "with a path that has room to move to the left and continues",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Hash: "1", Parents: []string{"2", "3"}},
{Hash: "3", Parents: []string{"2"}},
{Hash: "2", Parents: []string{"4", "5"}},
@@ -149,7 +150,7 @@ func TestRenderCommitGraph(t *testing.T) {
},
{
name: "new merge path fills gap before continuing path on right",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Hash: "1", Parents: []string{"2", "3", "4", "5"}},
{Hash: "4", Parents: []string{"2"}},
{Hash: "2", Parents: []string{"A"}},
@@ -165,7 +166,7 @@ func TestRenderCommitGraph(t *testing.T) {
},
{
name: "with a path that has room to move to the left and continues",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Hash: "1", Parents: []string{"2"}},
{Hash: "2", Parents: []string{"3", "4"}},
{Hash: "3", Parents: []string{"5", "4"}},
@@ -187,7 +188,7 @@ func TestRenderCommitGraph(t *testing.T) {
},
{
name: "with a path that has room to move to the left and continues",
commits: []*models.Commit{
commitOpts: []models.NewCommitOpts{
{Hash: "1", Parents: []string{"2"}},
{Hash: "2", Parents: []string{"3", "4"}},
{Hash: "3", Parents: []string{"5", "4"}},
@@ -218,8 +219,12 @@ func TestRenderCommitGraph(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
getStyle := func(c *models.Commit) style.TextStyle { return style.FgDefault }
lines := RenderCommitGraph(test.commits, "blah", getStyle)
hashPool := &utils.StringPool{}
getStyle := func(c *models.Commit) *style.TextStyle { return &style.FgDefault }
commits := lo.Map(test.commitOpts,
func(opts models.NewCommitOpts, _ int) *models.Commit { return models.NewCommit(hashPool, opts) })
lines := RenderCommitGraph(commits, hashPool.Add("blah"), getStyle)
trimmedExpectedOutput := ""
for _, line := range strings.Split(strings.TrimPrefix(test.expectedOutput, "\n"), "\n") {
@@ -230,7 +235,7 @@ func TestRenderCommitGraph(t *testing.T) {
output := ""
for i, line := range lines {
description := test.commits[i].Hash
description := test.commitOpts[i].Hash
output += strings.TrimSpace(description+" "+utils.Decolorise(line)) + "\n"
}
t.Log("\nactual: \n" + output)
@@ -251,9 +256,12 @@ func TestRenderPipeSet(t *testing.T) {
magenta := style.FgMagenta
nothing := style.Nothing
hashPool := &utils.StringPool{}
pool := func(s string) *string { return hashPool.Add(s) }
tests := []struct {
name string
pipes []*Pipe
pipes []Pipe
commit *models.Commit
prevCommit *models.Commit
expectedStr string
@@ -261,33 +269,33 @@ func TestRenderPipeSet(t *testing.T) {
}{
{
name: "single cell",
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "a", toHash: "b", kind: TERMINATES, style: cyan},
{fromPos: 0, toPos: 0, fromHash: "b", toHash: "c", kind: STARTS, style: green},
pipes: []Pipe{
{fromPos: 0, toPos: 0, fromHash: pool("a"), toHash: pool("b"), kind: TERMINATES, style: &cyan},
{fromPos: 0, toPos: 0, fromHash: pool("b"), toHash: pool("c"), kind: STARTS, style: &green},
},
prevCommit: &models.Commit{Hash: "a"},
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a"}),
expectedStr: "◯",
expectedStyles: []style.TextStyle{green},
},
{
name: "single cell, selected",
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "a", toHash: "selected", kind: TERMINATES, style: cyan},
{fromPos: 0, toPos: 0, fromHash: "selected", toHash: "c", kind: STARTS, style: green},
pipes: []Pipe{
{fromPos: 0, toPos: 0, fromHash: pool("a"), toHash: pool("selected"), kind: TERMINATES, style: &cyan},
{fromPos: 0, toPos: 0, fromHash: pool("selected"), toHash: pool("c"), kind: STARTS, style: &green},
},
prevCommit: &models.Commit{Hash: "a"},
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a"}),
expectedStr: "◯",
expectedStyles: []style.TextStyle{highlightStyle},
},
{
name: "terminating hook and starting hook, selected",
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "a", toHash: "selected", kind: TERMINATES, style: cyan},
{fromPos: 1, toPos: 0, fromHash: "c", toHash: "selected", kind: TERMINATES, style: yellow},
{fromPos: 0, toPos: 0, fromHash: "selected", toHash: "d", kind: STARTS, style: green},
{fromPos: 0, toPos: 1, fromHash: "selected", toHash: "e", kind: STARTS, style: green},
pipes: []Pipe{
{fromPos: 0, toPos: 0, fromHash: pool("a"), toHash: pool("selected"), kind: TERMINATES, style: &cyan},
{fromPos: 1, toPos: 0, fromHash: pool("c"), toHash: pool("selected"), kind: TERMINATES, style: &yellow},
{fromPos: 0, toPos: 0, fromHash: pool("selected"), toHash: pool("d"), kind: STARTS, style: &green},
{fromPos: 0, toPos: 1, fromHash: pool("selected"), toHash: pool("e"), kind: STARTS, style: &green},
},
prevCommit: &models.Commit{Hash: "a"},
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a"}),
expectedStr: "⏣─╮",
expectedStyles: []style.TextStyle{
highlightStyle, highlightStyle, highlightStyle,
@@ -295,13 +303,13 @@ func TestRenderPipeSet(t *testing.T) {
},
{
name: "terminating hook and starting hook, prioritise the terminating one",
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "a", toHash: "b", kind: TERMINATES, style: red},
{fromPos: 1, toPos: 0, fromHash: "c", toHash: "b", kind: TERMINATES, style: magenta},
{fromPos: 0, toPos: 0, fromHash: "b", toHash: "d", kind: STARTS, style: green},
{fromPos: 0, toPos: 1, fromHash: "b", toHash: "e", kind: STARTS, style: green},
pipes: []Pipe{
{fromPos: 0, toPos: 0, fromHash: pool("a"), toHash: pool("b"), kind: TERMINATES, style: &red},
{fromPos: 1, toPos: 0, fromHash: pool("c"), toHash: pool("b"), kind: TERMINATES, style: &magenta},
{fromPos: 0, toPos: 0, fromHash: pool("b"), toHash: pool("d"), kind: STARTS, style: &green},
{fromPos: 0, toPos: 1, fromHash: pool("b"), toHash: pool("e"), kind: STARTS, style: &green},
},
prevCommit: &models.Commit{Hash: "a"},
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a"}),
expectedStr: "⏣─│",
expectedStyles: []style.TextStyle{
green, green, magenta,
@@ -309,14 +317,14 @@ func TestRenderPipeSet(t *testing.T) {
},
{
name: "starting and terminating pipe sharing some space",
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "a1", toHash: "a2", kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromHash: "a2", toHash: "a3", kind: STARTS, style: yellow},
{fromPos: 1, toPos: 1, fromHash: "b1", toHash: "b2", kind: CONTINUES, style: magenta},
{fromPos: 3, toPos: 0, fromHash: "e1", toHash: "a2", kind: TERMINATES, style: green},
{fromPos: 0, toPos: 2, fromHash: "a2", toHash: "c3", kind: STARTS, style: yellow},
pipes: []Pipe{
{fromPos: 0, toPos: 0, fromHash: pool("a1"), toHash: pool("a2"), kind: TERMINATES, style: &red},
{fromPos: 0, toPos: 0, fromHash: pool("a2"), toHash: pool("a3"), kind: STARTS, style: &yellow},
{fromPos: 1, toPos: 1, fromHash: pool("b1"), toHash: pool("b2"), kind: CONTINUES, style: &magenta},
{fromPos: 3, toPos: 0, fromHash: pool("e1"), toHash: pool("a2"), kind: TERMINATES, style: &green},
{fromPos: 0, toPos: 2, fromHash: pool("a2"), toHash: pool("c3"), kind: STARTS, style: &yellow},
},
prevCommit: &models.Commit{Hash: "a1"},
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a1"}),
expectedStr: "⏣─│─┬─╯",
expectedStyles: []style.TextStyle{
yellow, yellow, magenta, yellow, yellow, green, green,
@@ -324,14 +332,14 @@ func TestRenderPipeSet(t *testing.T) {
},
{
name: "starting and terminating pipe sharing some space, with selection",
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "a1", toHash: "selected", kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromHash: "selected", toHash: "a3", kind: STARTS, style: yellow},
{fromPos: 1, toPos: 1, fromHash: "b1", toHash: "b2", kind: CONTINUES, style: magenta},
{fromPos: 3, toPos: 0, fromHash: "e1", toHash: "selected", kind: TERMINATES, style: green},
{fromPos: 0, toPos: 2, fromHash: "selected", toHash: "c3", kind: STARTS, style: yellow},
pipes: []Pipe{
{fromPos: 0, toPos: 0, fromHash: pool("a1"), toHash: pool("selected"), kind: TERMINATES, style: &red},
{fromPos: 0, toPos: 0, fromHash: pool("selected"), toHash: pool("a3"), kind: STARTS, style: &yellow},
{fromPos: 1, toPos: 1, fromHash: pool("b1"), toHash: pool("b2"), kind: CONTINUES, style: &magenta},
{fromPos: 3, toPos: 0, fromHash: pool("e1"), toHash: pool("selected"), kind: TERMINATES, style: &green},
{fromPos: 0, toPos: 2, fromHash: pool("selected"), toHash: pool("c3"), kind: STARTS, style: &yellow},
},
prevCommit: &models.Commit{Hash: "a1"},
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a1"}),
expectedStr: "⏣───╮ ╯",
expectedStyles: []style.TextStyle{
highlightStyle, highlightStyle, highlightStyle, highlightStyle, highlightStyle, nothing, green,
@@ -339,13 +347,13 @@ func TestRenderPipeSet(t *testing.T) {
},
{
name: "many terminating pipes",
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "a1", toHash: "a2", kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromHash: "a2", toHash: "a3", kind: STARTS, style: yellow},
{fromPos: 1, toPos: 0, fromHash: "b1", toHash: "a2", kind: TERMINATES, style: magenta},
{fromPos: 2, toPos: 0, fromHash: "c1", toHash: "a2", kind: TERMINATES, style: green},
pipes: []Pipe{
{fromPos: 0, toPos: 0, fromHash: pool("a1"), toHash: pool("a2"), kind: TERMINATES, style: &red},
{fromPos: 0, toPos: 0, fromHash: pool("a2"), toHash: pool("a3"), kind: STARTS, style: &yellow},
{fromPos: 1, toPos: 0, fromHash: pool("b1"), toHash: pool("a2"), kind: TERMINATES, style: &magenta},
{fromPos: 2, toPos: 0, fromHash: pool("c1"), toHash: pool("a2"), kind: TERMINATES, style: &green},
},
prevCommit: &models.Commit{Hash: "a1"},
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a1"}),
expectedStr: "◯─┴─╯",
expectedStyles: []style.TextStyle{
yellow, magenta, magenta, green, green,
@@ -353,14 +361,14 @@ func TestRenderPipeSet(t *testing.T) {
},
{
name: "starting pipe passing through",
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "a1", toHash: "a2", kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromHash: "a2", toHash: "a3", kind: STARTS, style: yellow},
{fromPos: 0, toPos: 3, fromHash: "a2", toHash: "d3", kind: STARTS, style: yellow},
{fromPos: 1, toPos: 1, fromHash: "b1", toHash: "b3", kind: CONTINUES, style: magenta},
{fromPos: 2, toPos: 2, fromHash: "c1", toHash: "c3", kind: CONTINUES, style: green},
pipes: []Pipe{
{fromPos: 0, toPos: 0, fromHash: pool("a1"), toHash: pool("a2"), kind: TERMINATES, style: &red},
{fromPos: 0, toPos: 0, fromHash: pool("a2"), toHash: pool("a3"), kind: STARTS, style: &yellow},
{fromPos: 0, toPos: 3, fromHash: pool("a2"), toHash: pool("d3"), kind: STARTS, style: &yellow},
{fromPos: 1, toPos: 1, fromHash: pool("b1"), toHash: pool("b3"), kind: CONTINUES, style: &magenta},
{fromPos: 2, toPos: 2, fromHash: pool("c1"), toHash: pool("c3"), kind: CONTINUES, style: &green},
},
prevCommit: &models.Commit{Hash: "a1"},
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a1"}),
expectedStr: "⏣─│─│─╮",
expectedStyles: []style.TextStyle{
yellow, yellow, magenta, yellow, green, yellow, yellow,
@@ -368,14 +376,14 @@ func TestRenderPipeSet(t *testing.T) {
},
{
name: "starting and terminating path crossing continuing path",
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "a1", toHash: "a2", kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromHash: "a2", toHash: "a3", kind: STARTS, style: yellow},
{fromPos: 0, toPos: 1, fromHash: "a2", toHash: "b3", kind: STARTS, style: yellow},
{fromPos: 1, toPos: 1, fromHash: "b1", toHash: "a2", kind: CONTINUES, style: green},
{fromPos: 2, toPos: 0, fromHash: "c1", toHash: "a2", kind: TERMINATES, style: magenta},
pipes: []Pipe{
{fromPos: 0, toPos: 0, fromHash: pool("a1"), toHash: pool("a2"), kind: TERMINATES, style: &red},
{fromPos: 0, toPos: 0, fromHash: pool("a2"), toHash: pool("a3"), kind: STARTS, style: &yellow},
{fromPos: 0, toPos: 1, fromHash: pool("a2"), toHash: pool("b3"), kind: STARTS, style: &yellow},
{fromPos: 1, toPos: 1, fromHash: pool("b1"), toHash: pool("a2"), kind: CONTINUES, style: &green},
{fromPos: 2, toPos: 0, fromHash: pool("c1"), toHash: pool("a2"), kind: TERMINATES, style: &magenta},
},
prevCommit: &models.Commit{Hash: "a1"},
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a1"}),
expectedStr: "⏣─│─╯",
expectedStyles: []style.TextStyle{
yellow, yellow, green, magenta, magenta,
@@ -383,14 +391,14 @@ func TestRenderPipeSet(t *testing.T) {
},
{
name: "another clash of starting and terminating paths",
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "a1", toHash: "a2", kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromHash: "a2", toHash: "a3", kind: STARTS, style: yellow},
{fromPos: 0, toPos: 1, fromHash: "a2", toHash: "b3", kind: STARTS, style: yellow},
{fromPos: 2, toPos: 2, fromHash: "c1", toHash: "c3", kind: CONTINUES, style: green},
{fromPos: 3, toPos: 0, fromHash: "d1", toHash: "a2", kind: TERMINATES, style: magenta},
pipes: []Pipe{
{fromPos: 0, toPos: 0, fromHash: pool("a1"), toHash: pool("a2"), kind: TERMINATES, style: &red},
{fromPos: 0, toPos: 0, fromHash: pool("a2"), toHash: pool("a3"), kind: STARTS, style: &yellow},
{fromPos: 0, toPos: 1, fromHash: pool("a2"), toHash: pool("b3"), kind: STARTS, style: &yellow},
{fromPos: 2, toPos: 2, fromHash: pool("c1"), toHash: pool("c3"), kind: CONTINUES, style: &green},
{fromPos: 3, toPos: 0, fromHash: pool("d1"), toHash: pool("a2"), kind: TERMINATES, style: &magenta},
},
prevCommit: &models.Commit{Hash: "a1"},
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a1"}),
expectedStr: "⏣─┬─│─╯",
expectedStyles: []style.TextStyle{
yellow, yellow, yellow, magenta, green, magenta, magenta,
@@ -398,11 +406,11 @@ func TestRenderPipeSet(t *testing.T) {
},
{
name: "commit whose previous commit is selected",
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "selected", toHash: "a2", kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromHash: "a2", toHash: "a3", kind: STARTS, style: yellow},
pipes: []Pipe{
{fromPos: 0, toPos: 0, fromHash: pool("selected"), toHash: pool("a2"), kind: TERMINATES, style: &red},
{fromPos: 0, toPos: 0, fromHash: pool("a2"), toHash: pool("a3"), kind: STARTS, style: &yellow},
},
prevCommit: &models.Commit{Hash: "selected"},
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "selected"}),
expectedStr: "◯",
expectedStyles: []style.TextStyle{
yellow,
@@ -410,11 +418,11 @@ func TestRenderPipeSet(t *testing.T) {
},
{
name: "commit whose previous commit is selected and is a merge commit",
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "selected", toHash: "a2", kind: TERMINATES, style: red},
{fromPos: 1, toPos: 1, fromHash: "selected", toHash: "b3", kind: CONTINUES, style: red},
pipes: []Pipe{
{fromPos: 0, toPos: 0, fromHash: pool("selected"), toHash: pool("a2"), kind: TERMINATES, style: &red},
{fromPos: 1, toPos: 1, fromHash: pool("selected"), toHash: pool("b3"), kind: CONTINUES, style: &red},
},
prevCommit: &models.Commit{Hash: "selected"},
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "selected"}),
expectedStr: "◯ │",
expectedStyles: []style.TextStyle{
highlightStyle, nothing, highlightStyle,
@@ -422,12 +430,12 @@ func TestRenderPipeSet(t *testing.T) {
},
{
name: "commit whose previous commit is selected and is a merge commit, with continuing pipe inbetween",
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "selected", toHash: "a2", kind: TERMINATES, style: red},
{fromPos: 1, toPos: 1, fromHash: "z1", toHash: "z3", kind: CONTINUES, style: green},
{fromPos: 2, toPos: 2, fromHash: "selected", toHash: "b3", kind: CONTINUES, style: red},
pipes: []Pipe{
{fromPos: 0, toPos: 0, fromHash: pool("selected"), toHash: pool("a2"), kind: TERMINATES, style: &red},
{fromPos: 1, toPos: 1, fromHash: pool("z1"), toHash: pool("z3"), kind: CONTINUES, style: &green},
{fromPos: 2, toPos: 2, fromHash: pool("selected"), toHash: pool("b3"), kind: CONTINUES, style: &red},
},
prevCommit: &models.Commit{Hash: "selected"},
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "selected"}),
expectedStr: "◯ │ │",
expectedStyles: []style.TextStyle{
highlightStyle, nothing, green, nothing, highlightStyle,
@@ -435,13 +443,13 @@ func TestRenderPipeSet(t *testing.T) {
},
{
name: "when previous commit is selected, not a merge commit, and spawns a continuing pipe",
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "a1", toHash: "a2", kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromHash: "a2", toHash: "a3", kind: STARTS, style: green},
{fromPos: 0, toPos: 1, fromHash: "a2", toHash: "b3", kind: STARTS, style: green},
{fromPos: 1, toPos: 0, fromHash: "selected", toHash: "a2", kind: TERMINATES, style: yellow},
pipes: []Pipe{
{fromPos: 0, toPos: 0, fromHash: pool("a1"), toHash: pool("a2"), kind: TERMINATES, style: &red},
{fromPos: 0, toPos: 0, fromHash: pool("a2"), toHash: pool("a3"), kind: STARTS, style: &green},
{fromPos: 0, toPos: 1, fromHash: pool("a2"), toHash: pool("b3"), kind: STARTS, style: &green},
{fromPos: 1, toPos: 0, fromHash: pool("selected"), toHash: pool("a2"), kind: TERMINATES, style: &yellow},
},
prevCommit: &models.Commit{Hash: "selected"},
prevCommit: models.NewCommit(hashPool, models.NewCommitOpts{Hash: "selected"}),
expectedStr: "⏣─╯",
expectedStyles: []style.TextStyle{
highlightStyle, highlightStyle, highlightStyle,
@@ -454,7 +462,7 @@ func TestRenderPipeSet(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actualStr := renderPipeSet(test.pipes, "selected", test.prevCommit)
actualStr := renderPipeSet(test.pipes, pool("selected"), test.prevCommit)
t.Log("actual cells:")
t.Log(actualStr)
expectedStr := ""
@@ -474,50 +482,53 @@ func TestRenderPipeSet(t *testing.T) {
}
func TestGetNextPipes(t *testing.T) {
hashPool := &utils.StringPool{}
pool := func(s string) *string { return hashPool.Add(s) }
tests := []struct {
prevPipes []*Pipe
prevPipes []Pipe
commit *models.Commit
expected []*Pipe
expected []Pipe
}{
{
prevPipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "a", toHash: "b", kind: STARTS, style: style.FgDefault},
prevPipes: []Pipe{
{fromPos: 0, toPos: 0, fromHash: pool("a"), toHash: pool("b"), kind: STARTS, style: &style.FgDefault},
},
commit: &models.Commit{
commit: models.NewCommit(hashPool, models.NewCommitOpts{
Hash: "b",
Parents: []string{"c"},
},
expected: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "a", toHash: "b", kind: TERMINATES, style: style.FgDefault},
{fromPos: 0, toPos: 0, fromHash: "b", toHash: "c", kind: STARTS, style: style.FgDefault},
}),
expected: []Pipe{
{fromPos: 0, toPos: 0, fromHash: pool("a"), toHash: pool("b"), kind: TERMINATES, style: &style.FgDefault},
{fromPos: 0, toPos: 0, fromHash: pool("b"), toHash: pool("c"), kind: STARTS, style: &style.FgDefault},
},
},
{
prevPipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "a", toHash: "b", kind: TERMINATES, style: style.FgDefault},
{fromPos: 0, toPos: 0, fromHash: "b", toHash: "c", kind: STARTS, style: style.FgDefault},
{fromPos: 0, toPos: 1, fromHash: "b", toHash: "d", kind: STARTS, style: style.FgDefault},
prevPipes: []Pipe{
{fromPos: 0, toPos: 0, fromHash: pool("a"), toHash: pool("b"), kind: TERMINATES, style: &style.FgDefault},
{fromPos: 0, toPos: 0, fromHash: pool("b"), toHash: pool("c"), kind: STARTS, style: &style.FgDefault},
{fromPos: 0, toPos: 1, fromHash: pool("b"), toHash: pool("d"), kind: STARTS, style: &style.FgDefault},
},
commit: &models.Commit{
commit: models.NewCommit(hashPool, models.NewCommitOpts{
Hash: "d",
Parents: []string{"e"},
},
expected: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "b", toHash: "c", kind: CONTINUES, style: style.FgDefault},
{fromPos: 1, toPos: 1, fromHash: "b", toHash: "d", kind: TERMINATES, style: style.FgDefault},
{fromPos: 1, toPos: 1, fromHash: "d", toHash: "e", kind: STARTS, style: style.FgDefault},
}),
expected: []Pipe{
{fromPos: 0, toPos: 0, fromHash: pool("b"), toHash: pool("c"), kind: CONTINUES, style: &style.FgDefault},
{fromPos: 1, toPos: 1, fromHash: pool("b"), toHash: pool("d"), kind: TERMINATES, style: &style.FgDefault},
{fromPos: 1, toPos: 1, fromHash: pool("d"), toHash: pool("e"), kind: STARTS, style: &style.FgDefault},
},
},
{
prevPipes: []*Pipe{
{fromPos: 0, toPos: 0, fromHash: "a", toHash: "root", kind: TERMINATES, style: style.FgDefault},
prevPipes: []Pipe{
{fromPos: 0, toPos: 0, fromHash: pool("a"), toHash: pool("root"), kind: TERMINATES, style: &style.FgDefault},
},
commit: &models.Commit{
commit: models.NewCommit(hashPool, models.NewCommitOpts{
Hash: "root",
Parents: []string{},
},
expected: []*Pipe{
{fromPos: 1, toPos: 1, fromHash: "root", toHash: models.EmptyTreeCommitHash, kind: STARTS, style: style.FgDefault},
}),
expected: []Pipe{
{fromPos: 1, toPos: 1, fromHash: pool("root"), toHash: pool(models.EmptyTreeCommitHash), kind: STARTS, style: &style.FgDefault},
},
},
}
@@ -526,11 +537,11 @@ func TestGetNextPipes(t *testing.T) {
defer color.ForceSetColorLevel(oldColorLevel)
for _, test := range tests {
getStyle := func(c *models.Commit) style.TextStyle { return style.FgDefault }
getStyle := func(c *models.Commit) *style.TextStyle { return &style.FgDefault }
pipes := getNextPipes(test.prevPipes, test.commit, getStyle)
// rendering cells so that it's easier to see what went wrong
actualStr := renderPipeSet(pipes, "selected", nil)
expectedStr := renderPipeSet(test.expected, "selected", nil)
actualStr := renderPipeSet(pipes, pool("selected"), nil)
expectedStr := renderPipeSet(test.expected, pool("selected"), nil)
t.Log("expected cells:")
t.Log(expectedStr)
t.Log("actual cells:")
@@ -543,19 +554,21 @@ func BenchmarkRenderCommitGraph(b *testing.B) {
oldColorLevel := color.ForceSetColorLevel(terminfo.ColorLevelMillions)
defer color.ForceSetColorLevel(oldColorLevel)
commits := generateCommits(50)
getStyle := func(commit *models.Commit) style.TextStyle {
hashPool := &utils.StringPool{}
commits := generateCommits(hashPool, 50)
getStyle := func(commit *models.Commit) *style.TextStyle {
return authors.AuthorStyle(commit.AuthorName)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
RenderCommitGraph(commits, "selected", getStyle)
for b.Loop() {
RenderCommitGraph(commits, hashPool.Add("selected"), getStyle)
}
}
func generateCommits(count int) []*models.Commit {
func generateCommits(hashPool *utils.StringPool, count int) []*models.Commit {
rnd := rand.New(rand.NewSource(1234))
pool := []*models.Commit{{Hash: "a", AuthorName: "A"}}
pool := []*models.Commit{models.NewCommit(hashPool, models.NewCommitOpts{Hash: "a", AuthorName: "A"})}
commits := make([]*models.Commit, 0, count)
authorPool := []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"}
for len(commits) < count {
@@ -565,22 +578,28 @@ func generateCommits(count int) []*models.Commit {
// I need to pick a random number of parents to add
parentCount := rnd.Intn(2) + 1
parentHashes := currentCommit.Parents()
for j := 0; j < parentCount; j++ {
reuseParent := rnd.Intn(6) != 1 && j <= len(pool)-1 && j != 0
var newParent *models.Commit
if reuseParent {
newParent = pool[j]
} else {
newParent = &models.Commit{
Hash: fmt.Sprintf("%s%d", currentCommit.Hash, j),
newParent = models.NewCommit(hashPool, models.NewCommitOpts{
Hash: fmt.Sprintf("%s%d", currentCommit.Hash(), j),
AuthorName: authorPool[rnd.Intn(len(authorPool))],
}
})
pool = append(pool, newParent)
}
currentCommit.Parents = append(currentCommit.Parents, newParent.Hash)
parentHashes = append(parentHashes, newParent.Hash())
}
commits = append(commits, currentCommit)
changedCommit := models.NewCommit(hashPool, models.NewCommitOpts{
Hash: currentCommit.Hash(),
AuthorName: currentCommit.AuthorName,
Parents: parentHashes,
})
commits = append(commits, changedCommit)
}
return commits

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