Compare commits

...
298 Commits
Author SHA1 Message Date
Jesse Duffield c6c4346d48 Merge pull request #2551 from stefanhaller/fix-initial-context-activation 2023-05-01 21:22:43 +10:00
Jesse Duffield ee9ae8f07f Merge pull request #2552 from stefanhaller/support-stacked-branches 2023-05-01 21:07:19 +10:00
README-bot 3ec416047f Updated README.md 2023-04-30 02:22:54 +00:00
Jesse Duffield 8ed29f2b7d Merge pull request #2390 from seand52/revamp-commit-message 2023-04-30 12:22:42 +10:00
Jesse Duffield 9adbef40de Refresh staging panel when committing
We now refresh the staging panel when doing an unscoped refresh, so that if we commit from the staging panel we escape
back to the files panel if need be. But that causes flickering when doing an unscoped refresh from other contexts,
because the refreshStagingPanel function assumes that the staging panel has focus. So we're adding a guard at the top
of that function to early exit if we don't have focus.
2023-04-30 12:17:34 +10:00
Jesse Duffield 0cd5257523 Retain commit message when cycling history
When cycling history, we want to make it so that upon returning to the original prompt, you get your text back.
Importantly, we don't want to use the existing preservedMessage field for that because that's only for preserving
a NEW commit message, and we don't want the history stuff of the commit reword flow to overwrite that.
2023-04-30 12:17:34 +10:00
Sean 49da7b482d Split commit message panel into commit summary and commit description panel
When we use the one panel for the entire commit message, its tricky to have a keybinding both for adding a newline and submitting.
By having two panels: one for the summary line and one for the description, we allow for 'enter' to submit the message when done from the summary panel,
and 'enter' to add a newline when done from the description panel. Alt-enter, for those who can use that key combo, also works for submitting the message
from the description panel. For those who can't use that key combo, and don't want to remap the keybinding, they can hit tab to go back to the summary panel
and then 'enter' to submit the message.

We have some awkwardness in that both contexts (i.e. panels) need to appear and disappear in tandem and we don't have a great way of handling that concept,
so we just push both contexts one after the other, and likewise remove both contexts when we escape.
2023-04-30 12:17:34 +10:00
Stefan Haller d675117289 Fix activation of initial context
This broke with 40f6767cfc77; the symptom is that starting lazygit with a git
arg (e.g. "lazygit log") wouldn't activate the requested panel correctly. While
it would show in the expanded view, it didn't have a green frame, and keyboard
events would go to the files panel.
2023-04-29 07:37:44 +02:00
Jesse Duffield e63858215e refactor moveFixupCommitDown 2023-04-29 07:28:33 +02:00
Stefan Haller 3fe4db9316 Make RebaseCommands.AmendTo more robust
This fixes two problems with the "amend commit with staged changes" command:

1. Amending to a fixup commit didn't work (this would create a commmit with the
   title "fixup! fixup! original title" and keep that at the top of the branch)
2. Unrelated fixup commits would be squashed too.

The added integration test verifies that both of these problems are fixed.
2023-04-29 07:28:33 +02:00
Jesse Duffield 185bbf0c75 Refactor to tighten interface to lazygit daemon 2023-04-29 07:28:33 +02:00
Stefan Haller a8586ba57e Refactor: simplify PrepareInteractiveRebaseCommand API
Instead of passing a bunch of different options in
PrepareInteractiveRebaseCommandOpts, where it was unclear how they interact if
several are set, have only a single field "instruction" which can be set to one
of various different instructions.

The functionality of replacing the entire todo file with our own is no longer
available; it is only possible to prepend todos to the existing file.

Also, instead of using different env vars for the various rebase operations that
we want to tell the daemon to do, use a single one that contains a json-encoded
struct with all available instructions. This makes the protocol much clearer,
and makes it easier to extend in the future.
2023-04-29 07:28:33 +02:00
Stefan Haller dad7a70bf8 Implement moving commits up/down in terms of daemon 2023-04-29 07:28:33 +02:00
Stefan Haller 3791f0b2fa Implement "move patch to selected commit" in terms of daemon 2023-04-29 07:28:33 +02:00
Stefan Haller b8fbe9756e Implement squash, fixup, drop, and reword in terms of daemon 2023-04-29 07:28:33 +02:00
Stefan Haller ab25600ccb Extract EditRebaseTodo into a function in utils.rebaseTodo
We want to reuse it from the daemon code in the next commit.
2023-04-29 07:28:33 +02:00
Stefan Haller d50c58b4c6 Implement "edit commit" in terms of the new EditRebase function 2023-04-29 07:28:33 +02:00
Stefan Haller 5645a662de Use --rebase-merges for interactive rebase
At the moment it doesn't make a big difference, because the vast majority of
callers create a list of todos themselves to completely replace what git came up
with. We're changing this in the following commits though, and then it's helpful
to preserve merges.
2023-04-29 07:28:33 +02:00
Stefan Haller a41218551d Put gitCommon.version back in deps_test.go
This was reverted in 3546ab8f21, but shouldn't have.
2023-04-29 07:28:33 +02:00
Stefan Haller d210107caa Bump github.com/fsmiamoto/git-todo-parser to latest version 2023-04-29 07:28:33 +02:00
Jesse Duffield 826128a8e0 Merge pull request #2578 from jesseduffield/enforce-lowercase-filenames 2023-04-29 13:08:06 +10:00
Jesse Duffield aec46942a8 enforce lowercase filenames 2023-04-29 13:05:05 +10:00
Jesse Duffield aa70723e3a Merge pull request #2558 from stefanhaller/allow-resetting-author-during-rebase 2023-04-29 12:44:14 +10:00
Jesse Duffield 7db54a948a Merge pull request #2548 from AKARSHITJOSHI/fix/tagPush 2023-04-29 12:43:55 +10:00
README-bot 32556480da Updated README.md 2023-04-29 02:39:27 +00:00
Jesse Duffield 2553015029 Merge pull request #2577 from jbrains/add-editor-preset-for-kakoune 2023-04-29 12:39:10 +10:00
J. B. Rainsberger 6c010a788c Add an editor preset for kakoune (kakoune.org). 2023-04-27 13:41:54 -03:00
Jesse Duffield b17c38befd Merge pull request #2567 from jesseduffield/bump-clipboard-package 2023-04-24 13:42:18 +10:00
Jesse Duffield 79dc1d9052 Merge pull request #2557 from noahziheng/feature/add-gitea-pr 2023-04-24 13:42:05 +10:00
README-bot 28d2b1432b Updated README.md 2023-04-24 03:37:27 +00:00
Jesse Duffield ed98e11fa0 Merge pull request #2555 from Ryooooooga/revert-force-if-includes 2023-04-24 13:37:14 +10:00
Andre Mueller 07a22e69e7 bump clipboard package for WSL support 2023-04-24 13:33:27 +10:00
Noah Gao bf3dd79b7a feat: add gitea to hosting service 2023-04-18 16:16:09 +00:00
Stefan Haller 21072226d2 Don't allow resetting non-HEAD commits (including rebase todos) during rebase 2023-04-18 17:34:07 +02:00
Stefan Haller b09000194a Allow resetting author of HEAD commit during rebase 2023-04-18 17:33:33 +02:00
Ryooooooga 3546ab8f21 Revert "feat: support for push --force-if-includes"
This reverts commit e00f248cf7.
2023-04-17 19:37:33 +09:00
Personal 9a13447b97 Change push tag command
Signed-off-by: AKARSHITJOSHI <akarshitjoshi@gmail.com>
2023-04-16 10:37:11 +05:30
Jesse Duffield 1efb565b22 Merge pull request #2370 from AzraelSec/rebase-with-todo-edit 2023-04-15 19:17:06 +10:00
AzraelSec 08a445eb9d test: check focus on commits after performing an advanced rebase 2023-04-15 11:01:55 +02:00
AzraelSec 28501fbf77 chore: add focus on local commits after interactively rebase 2023-04-15 10:42:36 +02:00
Jesse Duffield 8f1f712841 use lowercase text for menu items (as we're still yet to standardise on 'Sentence case') 2023-04-15 17:29:31 +10:00
AzraelSec b82b6a2992 test: add integration test to verify the interactive rebase correctly work 2023-04-15 17:26:08 +10:00
AzraelSec ddcd6be245 refactor: introduce a struct to pack the
`PrepareInteractiveRebaseCommand` function
2023-04-15 17:26:08 +10:00
Jesse Duffield 711be78811 extract out function 2023-04-15 17:26:08 +10:00
AzraelSec 3422b1e218 test: update the UI to follow the new rebase type selection instead of confirm the previous popup 2023-04-15 17:26:08 +10:00
AzraelSec a3fdf91714 feat: allow to perform a rebase with breaking before the first commit 2023-04-15 17:26:08 +10:00
AzraelSec 368f9c8cb3 feat: let interactive rebase prepend commands to the default todo file 2023-04-15 17:26:08 +10:00
README-bot e18b4a4cc3 Updated README.md 2023-04-15 07:16:26 +00:00
Jesse Duffield 46718e25ca Merge pull request #2547 from stefanhaller/more-robust-todo-rewriting 2023-04-15 17:16:11 +10:00
Stefan Haller d675eb6507 Don't allow changing the type of certain rebase todos
We already show "merge" todo entries when starting an interactive rebase with
--rebase-merges outside of lazygit. Changing the type of a merge entry to "pick"
or "edit" doesn't make sense and shouldn't be allowed. Earlier in this branch we
have started to show "update-ref" entries, these can't be changed either (they
can be moved, though).

You might argue that it should be possible to change them to "drop", but in the
case of "update-ref" this doesn't make sense either, because "drop" needs a Sha
and we don't have one here. Also, you would then be able to later change it back
to "pick", so we would have to remember that this isn't allowed for this
particular drop entry; that's messy, so just disallow all editing.
2023-04-15 08:36:03 +02:00
Stefan Haller dc4e88f8a4 Make moving todo commits more robust 2023-04-15 08:36:03 +02:00
Stefan Haller 120dd1530a Make EditRebaseTodo more robust
It used to work on the assumption that rebasing commits in lazygit's model
correspond one-to-one to lines in the git-rebase-todo file, which isn't
necessarily true (e.g. when users use "git rebase --edit-todo" at the custom
command prompt and add a "break" between lines).
2023-04-15 08:36:03 +02:00
Stefan Haller 860a8d102b Add integration test for dropping a todo commit when there's an update-ref
The test shows how we are accidentally dropping the wrong commit in this case.
We'll fix that in the next commit.
2023-04-15 08:36:03 +02:00
Stefan Haller a304fed68c Add GitVersion field to NewIntegrationTestArgs
It can be used to specify which git versions a given test should or should not run on.
2023-04-15 08:36:03 +02:00
Stefan Haller 227b0b781c Show update-ref commands in rebase todo list
This is useful when working with stacked branches, because you can now move
"pick" entries across an update-ref command and you can tell exactly which
branch the commit will end up in.

It's also useful to spot situations where the --update-refs option didn't work
as desired. For example, if you duplicate a branch and want to rebase only one
of the branches but not the other (maybe for testing); if you have
rebase.updateRefs=true in your git config, then rebasing one branch will move
the other branch along. To solve this we'll have to introduce a way to delete
the update-ref entry (maybe by hitting backspace?); this is out of scope for
this PR, so for now users will have to type "git rebase --edit-todo" into the
custom command prompt to sort this out.

We will also have to prevent users from trying to turn update-ref commands into
other commands like "pick" or "drop"; we'll do this later in this branch.
2023-04-15 08:36:03 +02:00
Stefan Haller 740474c10c Visualize branch heads in commits panel
Useful when working with stacked branches.
2023-04-15 08:36:03 +02:00
Stefan Haller a0d179b6dc Make getHydratedRebasingCommits more robust
So far the algorithm worked on the assumption that the output of the "git show"
command corresponds one-to-one to the lines of the rebase-todo file. This
assumption doesn't hold once we start to include todo lines that don't have a
sha (like update-ref), or when the todo file contains multiple entries for the
same sha. This should never happen normally, but it can if users manually edit
the todo file and duplicate a line.
2023-04-15 08:36:03 +02:00
Stefan Haller c53c5e47ef Store commit.Action as an enum instead of a string
The main reason for doing this (besides the reasons given for Status in the
previous commit) is that it allows us to easily convert from TodoCommand to
Action and back. This will be needed later in the branch. Fortunately,
TodoCommand is one-based, so this allows us to add an ActionNone constant with
the value 0.
2023-04-15 08:36:03 +02:00
Stefan Haller 188773511e Store commit.Status as an enum instead of a string
This is unrelated to the changes in this PR, but since we are doing the same
thing for the commit.Action field in the next commit, it makes sense to do it
for Status too for consistency. Modelling this as an enum feels more natural
than modelling it as a string, since there's a finite set of possible values.
And it saves a little bit of memory (not very much, since none of the strings
were heap-allocated, but still).
2023-04-15 08:36:03 +02:00
Stefan Haller 62c5c32fbb Bump github.com/fsmiamoto/git-todo-parser to latest main version 2023-04-15 08:36:03 +02:00
Jesse Duffield 981ba4c66c Merge pull request #2550 from jesseduffield/handle-flakey-tests 2023-04-14 21:15:22 +10:00
Jesse Duffield 7f5465a27b fix flaky tests 2023-04-14 21:01:45 +10:00
Jesse Duffield 275ed12486 Merge pull request #2545 from stefanhaller/fix-integration-test-name 2023-04-14 20:29:20 +10:00
Stefan Haller 6a340ec840 Reorder tests 2023-04-13 19:17:08 +02:00
Stefan Haller 3535cd0f94 Rename test files to match test names 2023-04-13 19:17:08 +02:00
Stefan Haller b1a56249f5 Add CI job to check that the test list is up to date 2023-04-13 19:17:08 +02:00
Jesse Duffield 82c54ed3d2 Merge pull request #2544 from scallaway/git-diff-detect-renames 2023-04-13 21:47:59 +10:00
Jesse Duffield bd62b519de Merge pull request #2496 from jesseduffield/feature/prevent-history-custom-command 2023-04-13 21:25:35 +10:00
Jesse Duffield 04e0a9bb45 Merge pull request #2523 from stefanhaller/editor-config 2023-04-13 21:22:17 +10:00
Luka Markušić 2b4ac986a2 Don't add custom command to history if it starts with space
Add tests for custom command with leading space
2023-04-13 21:20:46 +10:00
Jesse Duffield caedf57484 Merge pull request #2539 from axieax/ssh-git-url-parsing-fix 2023-04-13 21:15:58 +10:00
Stefan Haller 046b0d9daa Show warning about deprecated edit configs
We print this to the terminal after lazygit quits rather than showing it in a
panel at startup, so as to not annoy people too much. Hopefully it will still be
prominent enough this way.
2023-04-13 13:14:00 +02:00
Stefan Haller e3c5e07b20 Update documentation 2023-04-13 13:14:00 +02:00
Stefan Haller e4e16fa38e Change OpenCommand to Open and OpenLinkCommand to OpenLink
We do this for consistency with the edit settings. The old names are kept as a
fallback for now.
2023-04-13 13:14:00 +02:00
Stefan Haller b7e029adc7 Don't set platform defaults on OSConfig struct immediately
Instead, query the platform defaults only if the config is empty. This will be
necessary later to distinguish an empty config from a default config, so that we
can give deprecation warnings.
2023-04-13 13:14:00 +02:00
Stefan Haller 08d679c3a8 Remove line number support for "open" command
The "open" command is supposed to behave in the same way as double-clicking a
file in the Finder/Explorer. The concept of jumping to a specific line in the
file doesn't make sense for this; use "edit" instead.
2023-04-13 13:14:00 +02:00
Stefan Haller 2947b56134 Add support for falling back to legacy edit config 2023-04-13 13:14:00 +02:00
Stefan Haller 659d668e16 Implement edit presets 2023-04-13 13:14:00 +02:00
Stefan Haller 7bbcec965b Cleanup: fix copy/paste error in comment 2023-04-13 13:14:00 +02:00
Stefan Haller 24de156592 Fix windows tests
Now that the tests run again, it turns out that they actually fail, so fix them.
2023-04-13 13:14:00 +02:00
Stefan Haller 8d3cce4a49 Rename test files so that test discovery works again
These files were renamed from os_windows_test.go to os_test_windows.go (etc.) in
95b2e9540a. Since then, the tests have no longer run, since go only looks for
tests in files ending with "test.go".

It isn't important that the file name ends with "_windows.go", since there are
already build constrains in the files themselves.
2023-04-13 13:14:00 +02:00
README-bot 95757ceb3b Updated README.md 2023-04-13 11:05:22 +00:00
Jesse Duffield 2087a02f01 Merge pull request #2541 from stefanhaller/fix-debugger-config 2023-04-13 21:05:02 +10:00
Scott Callaway 6ffe98abac feat: remove --no-renames flag from main panel diffs (to show renamed files) 2023-04-13 10:57:38 +01:00
Scott Callaway 046cb942c2 fix: organise commit test file
Pulled this out into a separate commit since it was unrelated to the
feature coming behind it.

This just cleans up the `commit_test.go` file slightly (for the method
that I was working on) so that the tests are built in a way that is
slightly more readable - testing each configuration option individually
without combining any of them.
2023-04-12 12:31:06 +01:00
Stefan Haller fc2f8b7b20 Make debugger config work when changing repos while debugging
When changing repos while debugging, the current working directory changes,
which means that a daemon lazygit doesn't find the debugger_config.yml file any
more when you do an interactive rebase. Fix this by using an absolute path for
the --use-config-file option.
2023-04-04 10:26:42 +02:00
Andrew 298dae23e8 fix: generalize parsing of ssh git urls 2023-04-03 12:10:30 +10:00
Jesse Duffield a02b54e1b7 Merge pull request #2497 from stefanhaller/fix-initial-scroll-bar-size 2023-04-02 16:38:07 +10:00
Jesse Duffield 0af4e5a843 prevent unnecessary re-renders of view 2023-04-02 15:44:05 +10:00
README-bot e0503b9922 Updated README.md 2023-04-02 00:26:39 +00:00
Jesse Duffield ef239c04fb Merge pull request #2485 from stefanhaller/interactive-rebase-improvements 2023-04-02 10:26:19 +10:00
Stefan Haller d508badd62 Better error message when trying to amend a commit other than head during rebase 2023-04-01 08:16:15 +02:00
Luka Markušić e7d0116312 Allow amending the head commit during interactive rebase 2023-04-01 08:16:15 +02:00
Stefan Haller 85fdb700ba Extract amendHead function into new AmendHelper 2023-04-01 08:16:15 +02:00
Stefan Haller 7513d77567 Add integration test for amending from the files panel 2023-04-01 08:16:15 +02:00
Stefan Haller c757063264 Better error message when trying to edit or move a non-todo commit during rebase
Previously we would have tried to do the rebase, resulting in a long and
somewhat cryptic error message from git; now we check ourselves and show a less
intimidating message.
2023-04-01 08:16:15 +02:00
Stefan Haller b24955063c Allow rewording the head commit during interactive rebase 2023-04-01 08:16:15 +02:00
Stefan Haller 605bc026a1 Set promptToReturnFromSubprocess to false for integration tests
There is no way how we could confirm the prompt in an integration test.
2023-04-01 08:16:15 +02:00
Stefan Haller c6930e0538 Cleanup: use commit.isTODO() consistently
It seems cleaner than checking the Status for "rebasing".
2023-04-01 08:16:15 +02:00
Jesse Duffield 3a59aba46d Merge pull request #2521 from jesseduffield/fix-reflog-text-colour 2023-03-26 17:10:31 +11:00
Jesse Duffield 213ae8dd07 fix reflog text colour by defaulting every view to the same foreground colour 2023-03-26 15:24:09 +11:00
Jesse Duffield 4780953cef Merge pull request #2377 from shinhs0506/clear-staging-after-commit 2023-03-24 19:13:00 +11:00
README-bot 11bc8b87fa Updated README.md 2023-03-24 07:55:25 +00:00
Jesse Duffield 3bfbc9d255 Merge pull request #2518 from jesseduffield/remove-old-integration-test-stuff 2023-03-24 18:55:06 +11:00
Jesse Duffield 8121a0cc74 remove old integration test recording code 2023-03-24 18:42:11 +11:00
Stefan Haller 4adca84d68 Make sure scrollbars have the right size initially
We refresh the view after reading just enough to fill it, so that we see the
initial content as quickly as possible, but then we continue reading enough
lines so that we can tell how long the scrollbar needs to be, and then we
refresh again. This can result in slight flicker of the scrollbar when it is
first drawn with a bigger size and then jumps to a smaller size; however, that's
a good tradeoff for a solution that provides both good speed and accuracy.
2023-03-21 18:26:18 +01:00
Stefan Haller b7c61aa883 Push initial context instead of just putting it in the context array
This makes sure activateContext gets called on it.
2023-03-20 20:14:13 +11:00
Stefan Haller 40f6767cfc Avoid deactivating and activating when pushing the current context again
When calling PushContext, do nothing if the context to be pushed is already on
top of the stack. Avoids flicker in certain situations.
2023-03-20 20:14:13 +11:00
John Shin 776d8f4d2e refresh the staging panel on successful commit
apply formatting
2023-03-20 20:13:59 +11:00
Jesse Duffield 4b67a45a16 Merge pull request #2515 from stefanhaller/fix-deprecated-rand-seed 2023-03-20 20:12:25 +11:00
Jesse Duffield a82d952f48 Merge pull request #2495 from jesseduffield/feature/remove-altreturn 2023-03-20 20:11:35 +11:00
Stefan Haller 549ce09f71 Fix deprecated rand.Seed 2023-03-19 10:00:19 +01:00
Jesse Duffield cef804f27a Merge pull request #2513 from jesseduffield/refactor-patch-handling 2023-03-19 16:53:02 +11:00
Jesse Duffield 60f902f026 rename patch manager to patch builder 2023-03-19 16:35:57 +11:00
Jesse Duffield 7ce3165afa specify view when assertion on line count fails 2023-03-19 16:30:39 +11:00
Jesse Duffield c28e25524a bump gocui to fix race condition 2023-03-19 16:30:39 +11:00
Jesse Duffield 73c7dc9c5d refactor patch code 2023-03-19 16:30:39 +11:00
Jesse Duffield b542579db3 Better escape code parsing (thanks to Ryooooooga) (#2514) 2023-03-19 15:41:47 +11:00
Jesse Duffield e6274af015 appease golangci-lint (#2512) 2023-03-19 11:20:29 +11:00
README-bot 4f57bf22c9 Updated README.md 2023-03-19 00:09:11 +00:00
Luka MarkušićandJesse Duffield 8dbd7d44ff Fix checking for credentials performance (#2452)
Co-authored-by: Jesse Duffield <jessedduffield@gmail.com>
2023-03-19 11:08:54 +11:00
Stefan Haller 4b4dccfd7d Fix "move patch into new commit" for partial hunk (#2507) 2023-03-18 18:17:47 +11:00
Shruti Chaturvedi 81ea3107ed Uffizzi PR: Update Uffizzi Workflows (#2502) 2023-03-18 11:33:07 +11:00
Jens Kutilek 5c8bc790ff Make arrows consistent (#2501) 2023-03-18 11:32:44 +11:00
yk-kdandyk-kd b5d612e6d6 Add border config (#2344)
Co-authored-by: yk-kd <yosuke.komada@gmail.com>
2023-03-18 11:23:31 +11:00
README-bot dea279920c Updated README.md 2023-03-17 23:26:33 +00:00
Jesse Duffield 5834440767 Merge pull request #2500 from Ryooooooga/fix-commit-loader 2023-03-18 10:26:15 +11:00
Ryooooooga 55fb3ef4e6 fix(commit_loader): fix log command 2023-03-16 20:13:23 +09:00
Luka Markušić f314cb3763 Remove alternative confirmation and return keymappings 2023-03-09 10:32:00 +01:00
README-bot 516e963392 Updated README.md 2023-03-08 09:45:26 +00:00
Jesse Duffield 804a134aa5 Merge pull request #2471 from stefanhaller/improve-custom-patch-conflict-handling 2023-03-08 20:45:10 +11:00
Jesse Duffield 82fc6fb111 Merge pull request #2491 from TylerBarnes/patch-1 2023-03-08 10:26:06 +11:00
Jesse Duffield 0bda93d4c3 Add more unit tests 2023-03-08 09:19:23 +11:00
Tyler Barnes 6735bf4c89 Update Config.md 2023-03-07 11:54:47 -08:00
Stefan Haller 4bd1322941 Rename WillBeAppliedReverse to Reverse
This is the only "reverse"-related option that is left, so use a less clumsy
name for it.
2023-03-07 13:40:07 +01:00
Stefan Haller 45cf993982 Remove the PatchOptions.Reverse option
All callers pass false now (except for the tests, which we simply remove), so we
don't need the option any more.
2023-03-07 13:39:45 +01:00
Stefan Haller e4659145e8 Use WillBeAppliedReverse (and git apply --reverse) in the staging panel too
It's simpler to have only one way of reversing a patch.
2023-03-07 13:38:19 +01:00
Stefan Haller bf6e9a1bd3 Reenable failing test 2023-03-07 09:49:34 +01:00
Stefan Haller a68cd6af9c Concatenate patches to apply them all at once
This fixes the problem that patching would stop at the first file that has a
conflict. We always want to patch all files.

Also, it's faster for large patches, and the code is a little bit simpler too.
2023-03-07 09:49:34 +01:00
Stefan Haller 4ca012dbfb Add test for reverse-applying a patch that conflicts
The patch contains changes to two files; the first one conflicts, the second
doesn't. Note how it only applies changes to the first file at this point in the
branch; we'll fix this in the next commit.

This test would fail on master for multiple reasons.
2023-03-07 09:49:34 +01:00
Stefan Haller 6bd1c1d068 Remove parameters that are no longer needed
All callers in this file now use reverseOnGenerate=false and
keepOriginalHeader=true, so hard-code that in the call to ModifiedPatchForLines
and get rid of the parameters.
2023-03-07 09:49:34 +01:00
Stefan Haller 5d692e8961 Remove the keepOriginalHeader retry loop
The loop is pointless for two reasons:
- git apply --3way has this fallback built in already. If it can't do a
  three-way merge, it will fall back to applying the patch normally.
- However, the only situation where it does this is when it can't do a 3-way
  merge at all because it can't find the necessary ancestor blob. This can only
  happen if you transfer a patch between different repos that don't have the
  same blobs available; we are applying the patch to the same repo that is was
  just generated from, so a 3-way merge is always possible. (Now that we fixed
  the bug in the previous commit, that is.)

But the retry loop is not only pointless, it was actually harmful, because when
a 3-way patch fails with a conflict, git will put conflict markers in the
patched file and then exit with a non-zero exit status. So the retry loop would
try to patch the already patched file again, and this almost certainly fails,
but with a cryptic error message such as "error: main.go: does not exist in
index".
2023-03-07 09:49:34 +01:00
Stefan Haller 9cc33c479b Use forward patches and --reverse flag for partial patches too
There's no reason to have two different ways of applying patches for whole-file
patches and partial patches; use --reverse for both. Not only does this simplify
the code a bit, but it fixes an actual problem: when reverseOnGenerate and
keepOriginalHeader are both true, the generated patch header is broken (the two
blobs in the line `index 6d1959b..6dc5f84 100644` are swapped). Git fails to do
a proper three-way merge in that case, as it expects the first of the two blobs
to be the common ancestor.

It would be possible to fix this by extending ModifiedPatchForLines to swap the
two blobs in this case; but this would prevent us from concatenating all patches
and apply them in one go, which we are going to do later in the branch.
2023-03-07 09:49:34 +01:00
Stefan Haller c79e360584 Add patch option WillBeAppliedReverse
It's not used yet, but covered with tests already.
2023-03-07 09:49:34 +01:00
Stefan Haller f76cc27956 Bundle the reverse and keepOriginalHeader flags into a PatchOptions struct
We are going to add one more flag in the next commit.

Note that we are not using the struct inside patch_manager.go; we keep passing
the individual flags there. The reason for this will become more obvious later
in this branch.
2023-03-07 09:49:34 +01:00
Stefan Haller 5a50bfd179 Fix opening the current test file from the integration test gui 2023-03-07 09:49:34 +01:00
Jesse Duffield c36333af3d Merge pull request #2433 from Ryooooooga/subcommits-limit 2023-03-06 18:22:57 +11:00
Jesse Duffield 4d78d76a44 Merge pull request #2486 from humblepenguinn/master 2023-03-06 17:34:08 +11:00
README-bot d08a9849c3 Updated README.md 2023-03-06 06:32:51 +00:00
Jesse Duffield 3791b5057a Merge pull request #2481 from stefanhaller/fix-accordion-mode-for-custom-patch 2023-03-06 17:32:28 +11:00
Humble Penguin dfe5c805c4 generated cheatsheets and ran code formatting 2023-03-05 07:06:34 +05:00
Humble Penguin e94ff63bc5 issue #2473 2023-03-05 06:47:21 +05:00
Stefan Haller 723504a290 Keep side context in context stack when pushing a main context
This fixes accordion mode for the commit files panel. When entering a file, the
commit files panel should stay expanded.
2023-03-04 15:07:48 +01:00
README-bot 40a29fd622 Updated README.md 2023-03-03 10:06:38 +00:00
Jesse Duffield 648748781d Merge pull request #2483 from stefanhaller/fix-setting-selectedLineBgColor 2023-03-03 21:06:19 +11:00
Stefan Haller 63e8b8c01c Fix setting gui.selectedRangeBgColor as a hex value 2023-03-03 08:46:43 +01:00
Stefan Haller ed47529604 Add some tests for GetTextStyle
The tests show that setting a hex color doesn't work; we'll fix that in the next
commit.
2023-03-03 08:46:43 +01:00
Jesse Duffield ff5d0155db Merge pull request #2467 from jesseduffield/remove-file 2023-03-01 19:23:55 +11:00
Jesse Duffield 48acf3818f Merge pull request #2479 from stefanhaller/fix-you-are-here-issues 2023-03-01 19:21:26 +11:00
Stefan Haller de3e4838ad Store WorkingTreeState in model
This is the working tree state at the time the model commits were loaded. This
avoids a visual glitch with the "You Are Here" label appearing at times when it
is not supposed to.
2023-03-01 09:12:00 +01:00
Stefan Haller 6af8f278d0 Don't put "<--- YOU ARE HERE" in the commit model's name
Instead, derive it from context at display time (if we're rebasing, it's the
first non-todo commit). This fixes the problem that unfolding the current
commit's files in the local commits panel would show junk in the frame's title.

Along the way we make sure to only display the "<--- YOU ARE HERE" string in the
local commits panel; previously it would show for the top commit of a branch or
tag if mid-rebase.
2023-03-01 09:12:00 +01:00
README-bot e3c6887e5d Updated README.md 2023-03-01 07:28:41 +00:00
Jesse Duffield c34e0deca7 Merge pull request #2478 from stefanhaller/make-test-more-robust 2023-03-01 18:28:23 +11:00
README-bot 75b2c50a55 Updated README.md 2023-02-27 23:12:06 +00:00
Jesse Duffield af7c71d1ee Merge pull request #2476 from pereBohigas/feature/add_kotlin_icon 2023-02-28 10:11:47 +11:00
Stefan Haller 161bb684fa Make integration test more robust
If you ran this test enough times it would eventually fail; this happened
whenever the resulting squashed commit had a sha that happened to start with
"02". We test that "commit 02" does not appear in the diff window, but in that
case it did, at the very top of the window.

A better fix might be to change the commit message that we use in CreateNCommits
to something other than "commit XY", but that would require touching tons of
tests, so this is the easier fix.
2023-02-27 08:53:06 +01:00
Ryooooooga a624e0457f feat(subcommits): load unlimited sub-commits 2023-02-27 15:29:00 +09:00
README-bot f2aa7e7e28 Updated README.md 2023-02-26 02:07:25 +00:00
Jesse Duffield 75aa339b4e Merge pull request #2477 from jesseduffield/migrate-merge-conflict-tests 2023-02-26 13:07:11 +11:00
Jesse Duffield 45d45d2397 show file tree by default in integration tests 2023-02-26 13:01:51 +11:00
Jesse Duffield f7e8b2dd71 cleanup integration test code 2023-02-26 12:54:13 +11:00
Jesse Duffield 8b5d59c238 remove legacy integration tests 2023-02-26 11:34:18 +11:00
Jesse Duffield f82f4f6dbc disable auto-refresh in integration tests 2023-02-26 11:23:18 +11:00
Jesse Duffield ff3c5d331e migrate merge conflict tests 2023-02-26 11:22:24 +11:00
Pere Bohigas adef3bd4ca Add icon for Kotlin script files 2023-02-25 15:14:26 +01:00
README-bot 33f9f81c8c Updated README.md 2023-02-25 10:40:16 +00:00
Jesse Duffield f6fafc65ee Merge pull request #2475 from jesseduffield/migrate-patch-building-tests 2023-02-25 21:40:02 +11:00
Jesse Duffield 9c645088bf give CI longer wait times before failing assertions 2023-02-25 21:37:16 +11:00
Jesse Duffield dd1bf629b8 migrate patch building tests 2023-02-25 21:37:16 +11:00
Pere Bohigas c80a94aa7a Add icon for Kotlin files 2023-02-25 10:51:43 +01:00
README-bot 6c0b805137 Updated README.md 2023-02-25 00:45:44 +00:00
Jesse Duffield 8a69d994c0 Merge pull request #2474 from jesseduffield/improve-staging-tests 2023-02-25 11:45:29 +11:00
Jesse Duffield 037e957282 fix PullMergeConflict integration test 2023-02-25 11:39:24 +11:00
Jesse Duffield db011d8e34 Improve staging panel integration tests 2023-02-25 11:35:41 +11:00
Jesse Duffield 752526c880 Merge pull request #2470 from jesseduffield/migrate-staging-tests 2023-02-23 22:38:01 +11:00
Jesse Duffield c63fed2074 migrate staging tests 2023-02-23 22:29:40 +11:00
Jesse Duffield 1b52a0d83f Merge pull request #2463 from Ryooooooga/bump-tcell 2023-02-23 20:20:31 +11:00
Jesse Duffield 909a3b6791 remove erroneously added file 2023-02-22 22:35:08 +11:00
Jesse Duffield 558ceb64c7 Merge pull request #2466 from jesseduffield/migrate-reflog-tests 2023-02-22 22:33:57 +11:00
Jesse Duffield 1034962c7e migrate more tests 2023-02-22 22:29:01 +11:00
Jesse Duffield eabe7f462a migrate more tests 2023-02-22 21:57:32 +11:00
Jesse Duffield 22c10479d5 migrate reflog integration tests 2023-02-22 21:15:03 +11:00
Jesse Duffield f0572238cb Merge pull request #2465 from jesseduffield/migrate-rebase-tests 2023-02-22 19:40:12 +11:00
Jesse Duffield 78f3a7a478 migrate interactive rebase integration tests 2023-02-22 19:36:31 +11:00
Ryooooooga 90772e1eaa build: bump tcell version 2023-02-21 21:53:55 +09:00
Jesse Duffield 526c9dea9b Merge pull request #2293 from jesseduffield/feature/make-discarding-harder 2023-02-21 22:03:25 +11:00
Jesse Duffield c244c8f231 Merge pull request #2462 from jesseduffield/migrate-push-tests 2023-02-21 21:56:18 +11:00
Jesse Duffield bfde06d049 migrate push tests 2023-02-21 21:50:03 +11:00
Jesse Duffield 6b8abb7887 Merge pull request #2458 from jesseduffield/migrate-stash-tests 2023-02-20 22:11:47 +11:00
Jesse Duffield 2b6a109e38 migrate stash tests 2023-02-20 21:52:27 +11:00
Jesse Duffield 71a30155dc rerun test generator 2023-02-20 19:29:15 +11:00
Jesse Duffield 6ee20840b2 migrate switch tab from menu test 2023-02-20 19:28:52 +11:00
Jesse Duffield 56424eb1aa remove x keybinding for opening menu so we now only use '?' 2023-02-20 19:28:45 +11:00
Jesse Duffield 38c7030b0f mention path in tooltips 2023-02-20 19:28:45 +11:00
Luka Markušić 31b8524fe6 Add tooltips for discarding 2023-02-20 19:28:45 +11:00
Luka Markušić 0ae34aeeb7 Make discarding items less error prone
The menu is opened by `d` so this makes it harder to mess things up by
accidentally pressing `dd`.
2023-02-20 19:28:45 +11:00
Jesse Duffield e1c376ef54 Merge pull request #2453 from stefanhaller/allow-rebasing-to-first-commit 2023-02-20 19:21:37 +11:00
Jesse Duffield c13f550d63 Merge pull request #2455 from jesseduffield/wow-even-more-test-migrations 2023-02-20 19:06:35 +11:00
Jesse Duffield 06351c1adf remove old tag tests 2023-02-20 19:01:08 +11:00
Jesse Duffield 0b55eaca1d add create tag from commit test 2023-02-20 19:01:08 +11:00
Jesse Duffield ee8c31880c add reset to tag test 2023-02-20 19:01:08 +11:00
Jesse Duffield daf8176dd7 add tag checkout test 2023-02-20 19:01:08 +11:00
Jesse Duffield 76109a4c44 sync test list whenever running a test in vscode 2023-02-20 19:01:08 +11:00
Jesse Duffield 082d342bf8 add tag tests 2023-02-20 19:01:08 +11:00
Jesse Duffield 39c56553b3 show tag message 2023-02-20 19:01:08 +11:00
Stefan Haller c5cd217a65 Allow squashing fixups above the first commit of a repo
This includes amending changes into a given commit, since that's implemented in
terms of the former.
2023-02-20 08:29:43 +01:00
Jesse Duffield 9e2a3a87dd improved commit test 2023-02-20 18:20:23 +11:00
Jesse Duffield ac580ae6a0 migrate undo2 2023-02-20 18:20:23 +11:00
Jesse Duffield 9e1e20fef2 Merge pull request #2421 from Ryooooooga/tag-on-branch 2023-02-20 18:20:16 +11:00
Jesse Duffield 4b49bd406f Update pkg/integration/tests/branch/create_tag.go 2023-02-20 17:58:08 +11:00
Stefan Haller 7351907474 Add integration tests for rebasing to the initial commit 2023-02-20 07:40:05 +01:00
Stefan Haller a349e886ce Allow interactive rebasing all the way down to the first commit
Pass --root instead of a sha when we want to rebase down to the initial commit.
2023-02-20 07:40:05 +01:00
Stefan Haller dd61c49a15 Better error message for trying to squash or fixup the first commit
It's not so much the total number of commits that matters here, it's just
whether we are on the first one. (This includes the other condition.)

This allows us to get rid of the condition in rebase.go.
2023-02-20 07:40:04 +01:00
README-bot 1c3db24e44 Updated README.md 2023-02-20 06:33:57 +00:00
Jesse Duffield 15962e489d Merge pull request #2451 from stefanhaller/edit-by-breaking-after-current-commit 2023-02-20 17:33:37 +11:00
Stefan Haller ac9515d8c7 Revert "fix: improve backward compatibility"
Since we now require git 2.20, we don't need this any more.

This reverts commit 7c5f33980f.
2023-02-19 16:13:31 +01:00
RyogaandJesse Duffield 72a92d748f test: fix TagNamesAt
Co-authored-by: Jesse Duffield <jessedduffield@gmail.com>
2023-02-19 23:35:38 +09:00
Ryooooooga 36c2b00336 test: add an integration test for creating tag on branches 2023-02-19 23:35:38 +09:00
Ryooooooga 67b08ac239 feat: support to create tag on branch 2023-02-19 23:31:46 +09:00
Stefan Haller 67b8ef449c Edit by breaking after current commit
Instead of rebasing from the commit below the current one and then setting the
current one to "edit", we rebase from the current one and insert a "break" after
it. In most cases the behavior is exactly the same as before, except that the
new method also works if the current commit is a merge commit. This is useful if
you want to create a new commit at the very beginning of your branch (by editing
the last commit before your branch).
2023-02-19 10:21:01 +01:00
Stefan Haller bb856ad7c6 Bump minimum required git version to 2.20
We need this because the next commit is going to make use of the "break"
interactive rebase instruction, which was added in 2.20.
2023-02-19 10:20:14 +01:00
Jesse Duffield b54b8ae746 Merge pull request #2450 from jesseduffield/migrate-more-tests 2023-02-19 15:51:17 +11:00
Jesse Duffield 65bd0ab431 migrate undo test 2023-02-19 15:48:09 +11:00
Jesse Duffield 93b9e1bd19 migrate merge conflict undo test 2023-02-19 15:48:09 +11:00
Jesse Duffield a51f64814c show snapshot of lazygit when test fails for easier investigation 2023-02-19 15:48:09 +11:00
Jesse Duffield b5e325b0a4 migrate revert merge test 2023-02-19 15:48:09 +11:00
Jesse Duffield 88c76868ba migrate initial open test 2023-02-19 15:48:09 +11:00
Jesse Duffield e471567b5d remove already migrated test 2023-02-19 15:48:09 +11:00
Jesse Duffield 13ee0f0a5d migrate open to branches with cli arg test 2023-02-19 15:48:09 +11:00
Jesse Duffield 76a1b501f2 migrate more force push tests 2023-02-19 15:48:09 +11:00
Jesse Duffield 7201a91b69 remove unneeded config setting 2023-02-19 13:51:37 +11:00
Jesse Duffield 31d796ee75 migrate forcePush integration test 2023-02-19 13:38:15 +11:00
Jesse Duffield 0ac869a415 allow syncing tests from vscode 2023-02-19 13:38:07 +11:00
Jesse Duffield 9da9143aed Merge pull request #2449 from jesseduffield/migrate-pull-tests 2023-02-19 12:49:49 +11:00
Jesse Duffield b0383ba73a update readme 2023-02-19 12:42:48 +11:00
Jesse Duffield a3096e720c migrate pullAndSetUpstream test 2023-02-19 12:42:48 +11:00
Jesse Duffield c599aaed51 migrate pull integration test 2023-02-19 11:48:21 +11:00
Jesse Duffield f999bbce7c add code generator for creating tests list 2023-02-19 11:48:09 +11:00
Jesse Duffield bff076c70a rename key to Pull 2023-02-19 11:42:00 +11:00
Stefan Haller 979c3d6278 Fix yellow/red coloring of pushed/unpushed commits in branch commits panel (#2448) 2023-02-19 10:13:46 +11:00
README-bot 1fc8823825 Updated README.md 2023-02-18 23:11:27 +00:00
Jesse Duffield eeb9c02836 Merge pull request #2447 from stefanhaller/cleanup-leftovers-from-2444 2023-02-19 10:11:12 +11:00
stk 4d39668743 Undo a change made in #2444 that we didn't end up needing 2023-02-18 09:53:31 +01:00
README-bot 627cc285f7 Updated README.md 2023-02-17 23:34:46 +00:00
Jesse Duffield bee338f93b Merge pull request #2444 from stefanhaller/fix-selecting-next-stageable-line 2023-02-18 10:34:31 +11:00
Jesse Duffield 01bf7f21e6 bump gocui 2023-02-18 10:28:09 +11:00
Jesse Duffield c517d1e0a2 update view cursor when selecting new line in patch explorer view 2023-02-18 10:19:34 +11:00
stk 8cad8cda8f Don't bother setting view cursor pos for staging/stagingSecondary views
Now that the cursor highlight is never shown (see previous commit), there's no
reason to update the cursor position any more.
2023-02-15 21:32:21 +01:00
stk 6b81e6adca Turn highlighting off in staging/stagingSecondary views
There are two reasons for doing this:
1. The view cursor position is often out of sync with the selected line; see
   first commit of this branch.
2. The highlighting is already turned off when the view loses focus, and never
   turned back on thereafter. So just turn it off from the start then.
2023-02-15 21:29:38 +01:00
stk b499eba1a8 Select next stageable line correctly after staging a range of lines
We already have this very convenient behavior of jumping to the next stageable
line after staging something. However, while this worked well for staging
single lines or hunks, it didn't work correctly when staging a range of lines;
in this case we want to start searching from the first line of the range.
2023-02-15 21:29:00 +01:00
stk 97daec7228 Add test demonstrating selection bug when staging a range of lines
The selected line is not in the right position after staging a range of lines;
see next commit.
2023-02-15 21:28:05 +01:00
stk ff2a799200 Make SelectedLine/SelectedLineIdx work in staging/stagingSecondary views
While we try to keep the view's cursor position in sync with the context state's
selectedLineIdx (at least when pressing up or down), there are enough situations
where the two run out of sync; for example when initially opening the view, or
after staging a hunk, or when scrolling the view using the wheel. While it would
be possible to fix these situations to keep them always in sync, it doesn't seem
worth it, because the view's cursor position isn't really used for anything
else. So we rather special-case the SelectedLine/SelectedLineIdx functions of
ViewDriver to query the context state's selectedLineIdx directly if it is a
patch explorer context.
2023-02-15 21:22:11 +01:00
Jesse Duffield 31fcec16d9 Merge pull request #2429 from stefanhaller/do-not-autosquash-in-regular-rebase 2023-02-15 20:21:08 +11:00
README-bot 0d395e4051 Updated README.md 2023-02-14 09:54:05 +00:00
Jesse Duffield 09178a1276 Merge pull request #2435 from Ryooooooga/return-alt1 2023-02-14 20:53:42 +11:00
Jesse Duffield b66aa42ee5 Merge pull request #2437 from jesseduffield/migrate-even-more-tests 2023-02-12 18:16:22 +11:00
Jesse Duffield 3cfdae4116 migrate submodule reset test 2023-02-12 18:12:01 +11:00
Jesse Duffield d7956c481d migrate submodule enter test 2023-02-12 18:12:01 +11:00
Jesse Duffield 7a3291a1f7 fix test 2023-02-12 18:12:01 +11:00
Jesse Duffield 08c2b46d04 better visibility for tui 2023-02-12 10:47:45 +11:00
Jesse Duffield 1c48842277 migrate submodule remove test 2023-02-12 10:47:45 +11:00
Jesse Duffield 010f6d7f6e migrate submodule add test 2023-02-12 10:47:41 +11:00
Ryooooooga 39c20bc634 chore: change to work return-alt1 on all views 2023-02-11 21:19:47 +09:00
Jesse Duffield 823d95a8c6 Merge pull request #2418 from jesseduffield/feature/copy-remote-branch-to-clipboard 2023-02-11 10:53:31 +11:00
Jesse Duffield 225cd3cc60 Merge pull request #2412 from Ryooooooga/disable-help-on-suggestions 2023-02-11 10:52:49 +11:00
README-bot 91d62da577 Updated README.md 2023-02-10 23:52:06 +00:00
Jesse Duffield 9f71ed6c41 Merge pull request #2432 from Ryooooooga/remove-unknown-view-error-msg 2023-02-11 10:51:49 +11:00
Ryooooooga 984eb95cb7 chore: remove UNKNOWN_VIEW_ERROR_MSG 2023-02-10 21:26:51 +09:00
stk 1da762c295 Explicitly pass --no-autosquash when rebasing
This fixes the problem shown in the previous commit.
2023-02-09 18:21:11 +01:00
stk e357c00d4d Add an integration test showing a problem with autosquash during normal rebase
For users who have the rebase.autoSquash git config set to true, any regular
rebase will squash fixups in addition to rebasing. Not good -- we'll fix that in
the next commit.
2023-02-09 17:35:20 +01:00
Ryooooooga 1be6c522d8 fix: disable menu key binding while displaying popup 2023-02-09 20:13:41 +09:00
Luka Markušić 8af59c3e6e Copy remote branch name to clipboard 2023-02-09 11:56:12 +01:00
Jesse Duffield c713d19383 Merge pull request #2417 from stefanhaller/make_integration_tests_more_robust 2023-02-09 21:55:30 +11:00
Jesse Duffield 4f7324bad0 Merge pull request #2413 from stefanhaller/allow-ignore-whitespace-everywhere 2023-02-09 21:55:12 +11:00
README-bot aed3d56840 Updated README.md 2023-02-09 10:48:48 +00:00
Jesse Duffield 3745ba506b Merge pull request #2428 from jesseduffield/fix-linting 2023-02-09 21:48:28 +11:00
Jesse Duffield 5e2254395a fix linting issue 2023-02-09 21:45:14 +11:00
stk b243f30f48 Disable ~/.gitconfig when running integration tests
A global ~/.gitconfig file can have influence on how integration tests behave;
in my case, I had the option "merge.conflictStyle" set to "diff3", which made
the integration test "cherry_pick_conflict" fail because the diff was different
from what the test expected.

Make this more robust by telling git to ignore the global config file when
running tests.
2023-02-07 17:26:45 +01:00
stk e57843d947 Add integration test for ignoring whitespace in diff 2023-02-07 13:33:10 +01:00
stk 5bb6198219 Allow ignoring whitespace in diff in commits panel 2023-02-07 12:14:29 +01:00
stk bbaeab68e1 Better redrawing after toggling "ignore whitespace"
There's no need for refreshing anything; all that's needed is to re-focus the
selected list item. This way it will also work in other panels, which we are
about to add in the next commit.
2023-02-07 12:09:17 +01:00
stk 946c1dff99 Cleanup: remove extra space 2023-02-07 12:09:15 +01:00
stk d838965a41 Make "Toggle whitespace in diff view" a global key binding
Since it is going to affect a number of views later in the branch, it's easier
to make it global than to find all views that are affected.
2023-02-07 09:25:38 +01:00
Jesse Duffield 469938ee9b Merge pull request #2342 from knutwalker/override-git-sequence-editor-for-rebase 2023-02-05 13:52:35 +11:00
README-bot f7af2b991d Updated README.md 2023-02-04 23:30:57 +00:00
Jesse Duffield 75c3ab8a4a Merge pull request #2411 from Ryooooooga/default-color
fix https://github.com/jesseduffield/lazygit/issues/2410
2023-02-05 10:30:42 +11:00
Ryooooooga 7bd0c779c7 fix: fix default color to be white 2023-02-03 23:36:59 +09:00
Paul Horn bc7873144e Override GIT_SEQUENCE_EDITOR for rebase commands
I noticed that `$GIT_SEQUENCE_EDITOR` is overridden in `PrepareInteractiveRebaseCommand`
but not in `runSkipEditorCommand`.

Before this change, some commands such as `SquashAllAboveFixupCommits`
would not work when a different sequence editor, e.g.
[git-interactive-rebase-tool](https://github.com/MitMaro/git-interactive-rebase-tool)
is configured.
2023-01-01 04:37:19 +01:00
3693 changed files with 19460 additions and 16224 deletions
+9 -31
View File
@@ -46,36 +46,6 @@ jobs:
# we're passing -short so that we skip the integration tests, which will be run in parallel below
run: |
go test ./... -short
integration-tests-old:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
parallelism: [5]
index: [0,1,2,3,4]
name: "Integration Tests (Old pattern) (${{ matrix.index }}/${{ matrix.parallelism }})"
env:
GOFLAGS: -mod=vendor
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.18.x
- name: Cache build
uses: actions/cache@v1
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-test
restore-keys: |
${{runner.os}}-go-
- name: Test code
# for file.allow thing see https://vielmetti.typepad.com/logbook/2022/10/git-security-fixes-lead-to-fatal-transport-file-not-allowed-error-in-ci-systems-cve-2022-39253.html
run: |
git config --global protocol.file.allow always && PARALLEL_TOTAL=${{ matrix.parallelism }} PARALLEL_INDEX=${{ matrix.index }} go test pkg/integration/deprecated/*.go
integration-tests:
runs-on: ubuntu-latest
name: "Integration Tests"
@@ -98,8 +68,9 @@ jobs:
restore-keys: |
${{runner.os}}-go-
- name: Test code
# LONG_WAIT_BEFORE_FAIL means that for a given test assertion, we'll wait longer before failing
run: |
go test pkg/integration/clients/*.go
LONG_WAIT_BEFORE_FAIL=true go test pkg/integration/clients/*.go
build:
runs-on: ubuntu-latest
env:
@@ -164,6 +135,13 @@ jobs:
# ensure our vendor directory matches up with our go modules
run: |
go mod vendor && git diff --exit-code || (echo "Unexpected change to vendor directory. Run 'go mod vendor' locally and commit the changes" && exit 1)
- name: Check Integration Test List
# ensure our integration test list is up to date
run: |
go generate pkg/integration/tests/tests.go && git diff --exit-code || (echo "Integration test list not up to date. Run 'go generate pkg/integration/tests/tests.go' locally and commit the changes" && exit 1)
shell: bash # needed so that we get "-o pipefail"
- name: Check Filenames
run: scripts/check_filenames.sh
lint:
runs-on: ubuntu-latest
env:
+4 -12
View File
@@ -20,12 +20,12 @@ jobs:
run: echo "UUID_APP_TAG=$(uuidgen)" >> $GITHUB_ENV
- name: Docker metadata
id: meta
uses: docker/metadata-action@v3
uses: docker/metadata-action@v4
with:
images: registry.uffizzi.com/${{ env.UUID_APP_TAG }}
tags: type=raw,value=60d
- name: Build and Push Image to registry.uffizzi.com ephemeral registry
uses: docker/build-push-action@v2
uses: docker/build-push-action@v3
with:
push: true
context: ./
@@ -60,17 +60,11 @@ jobs:
name: preview-spec
path: docker-compose.rendered.yml
retention-days: 2
- name: Serialize PR Event to File
run: |
cat << EOF > event.json
${{ toJSON(github.event) }}
EOF
- name: Upload PR Event as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: event.json
path: ${{ github.event_path }}
retention-days: 2
delete-preview:
@@ -79,11 +73,9 @@ jobs:
if: ${{ github.event.action == 'closed' }}
steps:
# If this PR is closing, we will not render a compose file nor pass it to the next workflow.
- name: Serialize PR Event to File
run: echo '${{ toJSON(github.event) }}' > event.json
- name: Upload PR Event as Artifact
uses: actions/upload-artifact@v3
with:
name: preview-spec
path: event.json
path: ${{ github.event_path }}
retention-days: 2
+10 -4
View File
@@ -7,11 +7,11 @@ on:
types:
- completed
jobs:
cache-compose-file:
name: Cache Compose File
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
outputs:
compose-file-cache-key: ${{ env.COMPOSE_FILE_HASH }}
pr-number: ${{ env.PR_NUMBER }}
@@ -29,6 +29,9 @@ jobs:
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "preview-spec"
})[0];
if (matchArtifact === undefined) {
throw TypeError('Build Artifact not found!');
}
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
@@ -39,16 +42,18 @@ jobs:
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/preview-spec.zip`, Buffer.from(download.data));
- name: 'Unzip artifact'
run: unzip preview-spec.zip
- name: Read Event into ENV
run: |
echo 'EVENT_JSON<<EOF' >> $GITHUB_ENV
cat event.json >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
echo -e '\nEOF' >> $GITHUB_ENV
- name: Hash Rendered Compose File
id: hash
# If the previous workflow was triggered by a PR close event, we will not have a compose file artifact.
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
run: echo "COMPOSE_FILE_HASH=$(md5sum docker-compose.rendered.yml | awk '{ print $1 }')" >> $GITHUB_ENV
- name: Cache Rendered Compose File
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
uses: actions/cache@v3
@@ -70,7 +75,8 @@ jobs:
name: Use Remote Workflow to Preview on Uffizzi
needs:
- cache-compose-file
uses: UffizziCloud/preview-action/.github/workflows/reusable.yaml@v2.6.1
if: ${{ github.event.workflow_run.conclusion == 'success' }}
uses: UffizziCloud/preview-action/.github/workflows/reusable.yaml@v2
with:
# If this workflow was triggered by a PR close event, cache-key will be an empty string
# and this reusable workflow will delete the preview deployment.
@@ -81,4 +87,4 @@ jobs:
permissions:
contents: read
pull-requests: write
id-token: write
id-token: write
+1 -8
View File
@@ -35,14 +35,7 @@ lazygit.exe
test/git_server/data
# we'll scrap these lines once we've fully moved over to the new integration test approach
test/integration/*/actual/
test/integration/*/used_config/
# these sample hooks waste too much space
test/integration/*/expected/**/hooks/
test/integration/*/expected_remote/**/hooks/
test/integration_new/**
test/results/**
oryxBuildBinary
__debug_bin
+2 -2
View File
@@ -7,7 +7,7 @@
"request": "launch",
"mode": "auto",
"program": "main.go",
"args": ["--debug", "--use-config-file=.vscode/debugger_config.yml"],
"args": ["--debug", "--use-config-file=${workspaceFolder}/.vscode/debugger_config.yml"],
"console": "integratedTerminal",
"presentation": {
"hidden": true
@@ -19,7 +19,7 @@
"request": "launch",
"mode": "auto",
"program": "main.go",
"args": ["--logs", "--use-config-file=.vscode/debugger_config.yml"],
"args": ["--logs", "--use-config-file=${workspaceFolder}/.vscode/debugger_config.yml"],
"console": "integratedTerminal",
"presentation": {
"hidden": true
+16 -4
View File
@@ -24,7 +24,7 @@
{
"label": "Run current file integration test",
"type": "shell",
"command": "go run cmd/integration_test/main.go cli ${relativeFile}",
"command": "go generate pkg/integration/tests/tests.go && go run cmd/integration_test/main.go cli ${relativeFile}",
"problemMatcher": [],
"group": {
"kind": "test",
@@ -37,7 +37,7 @@
{
"label": "Run current file integration test (slow)",
"type": "shell",
"command": "go run cmd/integration_test/main.go cli --slow ${relativeFile}",
"command": "go generate pkg/integration/tests/tests.go && go run cmd/integration_test/main.go cli --slow ${relativeFile}",
"problemMatcher": [],
"group": {
"kind": "test",
@@ -49,7 +49,7 @@
{
"label": "Run current file integration test (sandbox)",
"type": "shell",
"command": "go run cmd/integration_test/main.go cli --sandbox ${relativeFile}",
"command": "go generate pkg/integration/tests/tests.go && go run cmd/integration_test/main.go cli --sandbox ${relativeFile}",
"problemMatcher": [],
"group": {
"kind": "test",
@@ -69,6 +69,18 @@
"presentation": {
"focus": true
}
}
},
{
"label": "Sync tests list",
"type": "shell",
"command": "go generate pkg/integration/tests/tests.go",
"problemMatcher": [],
"group": {
"kind": "test",
},
"presentation": {
"focus": true
}
},
],
}
+1 -1
View File
File diff suppressed because one or more lines are too long
-5
View File
@@ -17,11 +17,6 @@ Usage:
If you pass no test names, it runs all tests
Accepted environment variables:
KEY_PRESS_DELAY (e.g. 200): the number of milliseconds to wait between keypresses
MODE:
* ask (default): if a snapshot test fails, asks if you want to update the snapshot
* check: if a snapshot test fails, exits with an error
* update: if a snapshot test fails, updates the snapshot
* sandbox: uses the test's setup step to run the test in a sandbox where you can do whatever you want
TUI mode:
> go run cmd/integration_test/main.go tui
+33 -40
View File
@@ -9,7 +9,7 @@ Default path for the config file:
For old installations (slightly embarrassing: I didn't realise at the time that you didn't need to supply a vendor name to the path so I just used my name):
- Linux: `~/.config/jesseduffield/lazygit/config.yml`
- MacOS: `~/Library/Application Support/jesseduffield/lazygit/config.yml`
- MacOS: `~/Library/Application\ Support/jesseduffield/lazygit/config.yml`
- Windows: `%APPDATA%\jesseduffield\lazygit\config.yml`
If you want to change the config directory:
@@ -63,6 +63,7 @@ gui:
commandLogSize: 8
splitDiff: 'auto' # one of 'auto' | 'always'
skipRewordInEditorWarning: false # for skipping the confirmation before launching the reword editor
border: 'single' # one of 'single' | 'double' | 'rounded' | 'hidden'
git:
paging:
colorArg: always
@@ -95,9 +96,12 @@ git:
parseEmoji: false
diffContextSize: 3 # how many lines of context are shown around a change in diffs
os:
editCommand: '' # see 'Configuring File Editing' section
editCommandTemplate: ''
openCommand: ''
editPreset: '' # see 'Configuring File Editing' section
edit: ''
editAtLine: ''
editAtLineAndWait: ''
open: ''
openLink: ''
refresher:
refreshInterval: 10 # File/submodule refresh interval in seconds. Auto-refresh can be disabled via option 'git.autoRefresh'.
fetchInterval: 60 # Re-fetch interval in seconds. Auto-fetch can be disabled via option 'git.autoFetch'.
@@ -115,8 +119,6 @@ keybinding:
quit: 'q'
quit-alt1: '<c-c>' # alternative/alias of quit
return: '<esc>' # return to previous menu, will quit if there's nowhere to return
# When set to a printable character, this will work for returning from non-prompt panels
return-alt1: null
quitWithoutChangingDirectory: 'Q'
togglePanel: '<tab>' # goto the next panel
prevItem: '<up>' # go one line up
@@ -136,13 +138,12 @@ keybinding:
jumpToBlock: ['1', '2', '3', '4', '5'] # goto the Nth block / panel
nextMatch: 'n'
prevMatch: 'N'
optionMenu: 'x' # show help menu
optionMenu: null # show help menu
optionMenu-alt1: '?' # show help menu
select: '<space>'
goInto: '<enter>'
openRecentRepos: '<c-r>'
confirm: '<enter>'
confirm-alt1: 'y'
remove: 'd'
new: 'n'
edit: 'e'
@@ -170,7 +171,6 @@ keybinding:
diffingMenu-alt: '<c-e>' # deprecated
copyToClipboard: '<c-o>'
submitEditorText: '<enter>'
appendNewline: '<a-enter>'
extrasMenu: '@'
toggleWhitespaceInDiffView: '<c-w>'
increaseContextInDiffView: '}'
@@ -203,6 +203,7 @@ keybinding:
mergeIntoCurrentBranch: 'M'
viewGitFlowOptions: 'i'
fastForward: 'f' # fast-forward this branch from its upstream
createTag: 'T'
pushTag: 'P'
setUpstream: 'u' # set as upstream of checked-out branch
fetchRemote: 'f'
@@ -269,40 +270,41 @@ os:
### Configuring File Editing
Lazygit will edit a file with the first set editor in the following:
There are two commands for opening files, `o` for "open" and `e` for "edit". `o`
acts as if the file was double-clicked in the Finder/Explorer, so it also works
for non-text files, whereas `e` opens the file in an editor. `e` can also jump
to the right line in the file if you invoke it from the staging panel, for
example.
1. config.yaml
To tell lazygit which editor to use for the `e` command, the easiest way to do
that is to provide an editPreset config, e.g.
```yaml
os:
editCommand: 'vim' # as an example
editPreset: 'vscode'
```
2. \$(git config core.editor)
3. \$GIT_EDITOR
4. \$VISUAL
5. \$EDITOR
6. \$(which vi)
Supported presets are `vim`, `emacs`, `nano`, `vscode`, `sublime`, `bbedit`, and
`xcode`. In many cases lazygit will be able to guess the right preset from your
$(git config core.editor), or an environment variable such as $VISUAL or $EDITOR.
Lazygit will log an error if none of these options are set.
You can specify the current line number when you're in the patch explorer.
If for some reason you are not happy with the default commands from a preset, or
there simply is no preset for your editor, you can customize the commands by
setting the `edit`, `editAtLine`, and `editAtLineAndWait` options, e.g.:
```yaml
os:
editCommand: 'vim'
editCommandTemplate: '{{editor}} +{{line}} -- {{filename}}'
edit: 'myeditor {{filename}}'
editAtLine: 'myeditor --line={{line}} {{filename}}'
editAtLineAndWait: 'myeditor --block --line={{line}} {{filename}}'
editInTerminal: true
```
or
The `editInTerminal` option is used to decide whether lazygit needs to suspend
itself to the background before calling the editor.
```yaml
os:
editCommand: 'code'
editCommandTemplate: '{{editor}} --goto -- {{filename}}:{{line}}'
```
`{{editor}}` in `editCommandTemplate` is replaced with the value of `editCommand`.
Contributions of new editor presets are welcome; see the `getPreset` function in
[`editor_presets.go`](https://github.com/jesseduffield/lazygit/blob/master/pkg/config/editor_presets.go).
### Overriding default config file location
@@ -318,15 +320,6 @@ or
LG_CONFIG_FILE="$HOME/.base_lg_conf,$HOME/.light_theme_lg_conf" lazygit
```
### Recommended Config Values
for users of VSCode
```yaml
os:
openCommand: 'code -rg {{filename}}'
```
## Color Attributes
For color attributes you can choose an array of attributes (with max one color attribute)
@@ -480,7 +473,7 @@ services:
Where:
- `gitDomain` stands for the domain used by git itself (i.e. the one present on clone URLs), e.g. `git.work.com`
- `provider` is one of `github`, `bitbucket`, `bitbucketServer`, `azuredevops` or `gitlab`
- `provider` is one of `github`, `bitbucket`, `bitbucketServer`, `azuredevops`, `gitlab` or `gitea`
- `webDomain` is the URL where your git service exposes a web interface and APIs, e.g. `gitservice.work.com`
## Predefined commit message prefix
+10 -8
View File
@@ -11,13 +11,14 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>m</kbd>: view merge/rebase options
<kbd>ctrl+p</kbd>: view custom patch options
<kbd>R</kbd>: refresh
<kbd>x</kbd>: open menu
<kbd>?</kbd>: open menu
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode
<kbd>ctrl+s</kbd>: view filter-by-path options
<kbd>W</kbd>: open diff menu
<kbd>ctrl+e</kbd>: open diff menu
<kbd>@</kbd>: open command log menu
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
<kbd>:</kbd>: execute custom command
@@ -92,7 +93,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
<kbd>d</kbd>: view 'discard changes' options
<kbd>space</kbd>: toggle staged
<kbd>ctrl+b</kbd>: Filter files (staged/unstaged)
@@ -131,6 +131,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>r</kbd>: rebase checked-out branch onto this branch
<kbd>M</kbd>: merge into currently checked out branch
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>T</kbd>: create tag
<kbd>g</kbd>: view reset options
<kbd>R</kbd>: rename branch
<kbd>u</kbd>: set/unset upstream
@@ -142,8 +143,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>e</kbd>: edit file
<kbd>o</kbd>: open file
<kbd>◄</kbd>: select previous conflict
<kbd>►</kbd>: select next conflict
<kbd>◀</kbd>: select previous conflict
<kbd>▶</kbd>: select next conflict
<kbd>▲</kbd>: select previous hunk
<kbd>▼</kbd>: select next hunk
<kbd>z</kbd>: undo
@@ -163,8 +164,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Main Panel (Patch Building)
<pre>
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>◀</kbd>: select previous hunk
<kbd>▶</kbd>: select next hunk
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
@@ -178,8 +179,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Main Panel (Staging)
<pre>
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>◀</kbd>: select previous hunk
<kbd>▶</kbd>: select next hunk
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
@@ -214,6 +215,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Remote Branches
<pre>
<kbd>ctrl+o</kbd>: copy branch name to clipboard
<kbd>space</kbd>: checkout
<kbd>n</kbd>: new branch
<kbd>M</kbd>: merge into currently checked out branch
+10 -8
View File
@@ -11,13 +11,14 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>m</kbd>: view merge/rebase options
<kbd>ctrl+p</kbd>: view custom patch options
<kbd>R</kbd>: リフレッシュ
<kbd>x</kbd>: メニューを開く
<kbd>?</kbd>: メニューを開く
<kbd>+</kbd>: 次のスクリーンモード (normal/half/fullscreen)
<kbd>_</kbd>: 前のスクリーンモード
<kbd>ctrl+s</kbd>: view filter-by-path options
<kbd>W</kbd>: 差分メニューを開く
<kbd>ctrl+e</kbd>: 差分メニューを開く
<kbd>@</kbd>: コマンドログメニューを開く
<kbd>ctrl+w</kbd>: 空白文字の差分の表示有無を切り替え
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
<kbd>:</kbd>: カスタムコマンドを実行
@@ -152,7 +153,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>ctrl+o</kbd>: ファイル名をクリップボードにコピー
<kbd>ctrl+w</kbd>: 空白文字の差分の表示有無を切り替え
<kbd>d</kbd>: view 'discard changes' options
<kbd>space</kbd>: ステージ/アンステージ
<kbd>ctrl+b</kbd>: ファイルをフィルタ (ステージ/アンステージ)
@@ -191,6 +191,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>r</kbd>: rebase checked-out branch onto this branch
<kbd>M</kbd>: 現在のブランチにマージ
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>T</kbd>: タグを作成
<kbd>g</kbd>: view reset options
<kbd>R</kbd>: ブランチ名を変更
<kbd>u</kbd>: set/unset upstream
@@ -202,8 +203,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>e</kbd>: ファイルを編集
<kbd>o</kbd>: ファイルを開く
<kbd>◄</kbd>: 前のコンフリクトを選択
<kbd>►</kbd>: 次のコンフリクトを選択
<kbd>◀</kbd>: 前のコンフリクトを選択
<kbd>▶</kbd>: 次のコンフリクトを選択
<kbd>▲</kbd>: 前のhunkを選択
<kbd>▼</kbd>: 次のhunkを選択
<kbd>z</kbd>: アンドゥ
@@ -223,8 +224,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## メインパネル (Patch Building)
<pre>
<kbd>◄</kbd>: 前のhunkを選択
<kbd>►</kbd>: 次のhunkを選択
<kbd>◀</kbd>: 前のhunkを選択
<kbd>▶</kbd>: 次のhunkを選択
<kbd>v</kbd>: 範囲選択を切り替え
<kbd>V</kbd>: 範囲選択を切り替え
<kbd>a</kbd>: hunk選択を切り替え
@@ -238,8 +239,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## メインパネル (Staging)
<pre>
<kbd>◄</kbd>: 前のhunkを選択
<kbd>►</kbd>: 次のhunkを選択
<kbd>◀</kbd>: 前のhunkを選択
<kbd>▶</kbd>: 次のhunkを選択
<kbd>v</kbd>: 範囲選択を切り替え
<kbd>V</kbd>: 範囲選択を切り替え
<kbd>a</kbd>: hunk選択を切り替え
@@ -268,6 +269,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## リモートブランチ
<pre>
<kbd>ctrl+o</kbd>: ブランチ名をクリップボードにコピー
<kbd>space</kbd>: チェックアウト
<kbd>n</kbd>: 新しいブランチを作成
<kbd>M</kbd>: 現在のブランチにマージ
+10 -8
View File
@@ -11,13 +11,14 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>m</kbd>: view merge/rebase options
<kbd>ctrl+p</kbd>: 커스텀 Patch 옵션 보기
<kbd>R</kbd>: 새로고침
<kbd>x</kbd>: 매뉴 열기
<kbd>?</kbd>: 매뉴 열기
<kbd>+</kbd>: 다음 스크린 모드 (normal/half/fullscreen)
<kbd>_</kbd>: 이전 스크린 모드
<kbd>ctrl+s</kbd>: view filter-by-path options
<kbd>W</kbd>: Diff 메뉴 열기
<kbd>ctrl+e</kbd>: Diff 메뉴 열기
<kbd>@</kbd>: 명령어 로그 메뉴 열기
<kbd>ctrl+w</kbd>: 공백문자를 Diff 뷰에서 표시 여부 전환
<kbd>}</kbd>: diff 보기의 변경 사항 주위에 표시되는 컨텍스트의 크기를 늘리기
<kbd>{</kbd>: diff 보기의 변경 사항 주위에 표시되는 컨텍스트 크기 줄이기
<kbd>:</kbd>: execute custom command
@@ -87,8 +88,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>e</kbd>: 파일 편집
<kbd>o</kbd>: 파일 닫기
<kbd>◄</kbd>: 이전 충돌을 선택
<kbd>►</kbd>: 다음 충돌을 선택
<kbd>◀</kbd>: 이전 충돌을 선택
<kbd>▶</kbd>: 다음 충돌을 선택
<kbd>▲</kbd>: 이전 hunk를 선택
<kbd>▼</kbd>: 다음 hunk를 선택
<kbd>z</kbd>: 되돌리기
@@ -108,8 +109,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## 메인 패널 (Patch Building)
<pre>
<kbd>◄</kbd>: 이전 hunk를 선택
<kbd>►</kbd>: 다음 hunk를 선택
<kbd>◀</kbd>: 이전 hunk를 선택
<kbd>▶</kbd>: 다음 hunk를 선택
<kbd>v</kbd>: 드래그 선택 전환
<kbd>V</kbd>: 드래그 선택 전환
<kbd>a</kbd>: toggle select hunk
@@ -123,8 +124,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## 메인 패널 (Staging)
<pre>
<kbd>◄</kbd>: 이전 hunk를 선택
<kbd>►</kbd>: 다음 hunk를 선택
<kbd>◀</kbd>: 이전 hunk를 선택
<kbd>▶</kbd>: 다음 hunk를 선택
<kbd>v</kbd>: 드래그 선택 전환
<kbd>V</kbd>: 드래그 선택 전환
<kbd>a</kbd>: toggle select hunk
@@ -157,6 +158,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>r</kbd>: 체크아웃된 브랜치를 이 브랜치에 리베이스
<kbd>M</kbd>: 현재 브랜치에 병합
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>T</kbd>: 태그를 생성
<kbd>g</kbd>: view reset options
<kbd>R</kbd>: 브랜치 이름 변경
<kbd>u</kbd>: set/unset upstream
@@ -198,6 +200,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## 원격 브랜치
<pre>
<kbd>ctrl+o</kbd>: 브랜치명을 클립보드에 복사
<kbd>space</kbd>: 체크아웃
<kbd>n</kbd>: 새 브랜치 생성
<kbd>M</kbd>: 현재 브랜치에 병합
@@ -271,7 +274,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>ctrl+o</kbd>: 파일명을 클립보드에 복사
<kbd>ctrl+w</kbd>: 공백문자를 Diff 뷰에서 표시 여부 전환
<kbd>d</kbd>: view 'discard changes' options
<kbd>space</kbd>: Staged 전환
<kbd>ctrl+b</kbd>: 파일을 필터하기 (Staged/unstaged)
+10 -8
View File
@@ -11,13 +11,14 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>m</kbd>: bekijk merge/rebase opties
<kbd>ctrl+p</kbd>: bekijk aangepaste patch opties
<kbd>R</kbd>: verversen
<kbd>x</kbd>: open menu
<kbd>?</kbd>: open menu
<kbd>+</kbd>: volgende scherm modus (normaal/half/groot)
<kbd>_</kbd>: vorige scherm modus
<kbd>ctrl+s</kbd>: bekijk scoping opties
<kbd>W</kbd>: open diff menu
<kbd>ctrl+e</kbd>: open diff menu
<kbd>@</kbd>: open command log menu
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
<kbd>:</kbd>: voer aangepaste commando uit
@@ -45,7 +46,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>ctrl+o</kbd>: kopieer de bestandsnaam naar het klembord
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
<kbd>d</kbd>: bekijk 'veranderingen ongedaan maken' opties
<kbd>space</kbd>: toggle staged
<kbd>ctrl+b</kbd>: Filter files (staged/unstaged)
@@ -84,6 +84,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>r</kbd>: rebase branch
<kbd>M</kbd>: merge in met huidige checked out branch
<kbd>f</kbd>: fast-forward deze branch vanaf zijn upstream
<kbd>T</kbd>: creëer tag
<kbd>g</kbd>: bekijk reset opties
<kbd>R</kbd>: hernoem branch
<kbd>u</kbd>: set/unset upstream
@@ -142,8 +143,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>e</kbd>: verander bestand
<kbd>o</kbd>: open bestand
<kbd>◄</kbd>: selecteer voorgaand conflict
<kbd>►</kbd>: selecteer volgende conflict
<kbd>◀</kbd>: selecteer voorgaand conflict
<kbd>▶</kbd>: selecteer volgende conflict
<kbd>▲</kbd>: selecteer bovenste hunk
<kbd>▼</kbd>: selecteer onderste hunk
<kbd>z</kbd>: ongedaan maken
@@ -163,8 +164,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Patch Bouwen
<pre>
<kbd>◄</kbd>: selecteer de vorige hunk
<kbd>►</kbd>: selecteer de volgende hunk
<kbd>◀</kbd>: selecteer de vorige hunk
<kbd>▶</kbd>: selecteer de volgende hunk
<kbd>v</kbd>: toggle drag selecteer
<kbd>V</kbd>: toggle drag selecteer
<kbd>a</kbd>: toggle selecteer hunk
@@ -193,6 +194,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Remote Branches
<pre>
<kbd>ctrl+o</kbd>: kopieer branch name naar klembord
<kbd>space</kbd>: uitchecken
<kbd>n</kbd>: nieuwe branch
<kbd>M</kbd>: merge in met huidige checked out branch
@@ -216,8 +218,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Staging
<pre>
<kbd>◄</kbd>: selecteer de vorige hunk
<kbd>►</kbd>: selecteer de volgende hunk
<kbd>◀</kbd>: selecteer de vorige hunk
<kbd>▶</kbd>: selecteer de volgende hunk
<kbd>v</kbd>: toggle drag selecteer
<kbd>V</kbd>: toggle drag selecteer
<kbd>a</kbd>: toggle selecteer hunk
+10 -8
View File
@@ -11,13 +11,14 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>m</kbd>: widok scalenia/opcje zmiany bazy
<kbd>ctrl+p</kbd>: view custom patch options
<kbd>R</kbd>: odśwież
<kbd>x</kbd>: open menu
<kbd>?</kbd>: open menu
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode
<kbd>ctrl+s</kbd>: view filter-by-path options
<kbd>W</kbd>: open diff menu
<kbd>ctrl+e</kbd>: open diff menu
<kbd>@</kbd>: open command log menu
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
<kbd>:</kbd>: wykonaj własną komendę
@@ -90,6 +91,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>r</kbd>: zmiana bazy gałęzi
<kbd>M</kbd>: scal do obecnej gałęzi
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>T</kbd>: create tag
<kbd>g</kbd>: wyświetl opcje resetu
<kbd>R</kbd>: rename branch
<kbd>u</kbd>: set/unset upstream
@@ -99,8 +101,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Main Panel (Patch Building)
<pre>
<kbd>◄</kbd>: poprzedni kawałek
<kbd>►</kbd>: następny kawałek
<kbd>◀</kbd>: poprzedni kawałek
<kbd>▶</kbd>: następny kawałek
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
@@ -115,7 +117,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
<kbd>d</kbd>: pokaż opcje porzucania zmian
<kbd>space</kbd>: przełącz stan poczekalni
<kbd>ctrl+b</kbd>: Filter files (staged/unstaged)
@@ -155,8 +156,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Poczekalnia
<pre>
<kbd>◄</kbd>: poprzedni kawałek
<kbd>►</kbd>: następny kawałek
<kbd>◀</kbd>: poprzedni kawałek
<kbd>▶</kbd>: następny kawałek
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
@@ -191,6 +192,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Remote Branches
<pre>
<kbd>ctrl+o</kbd>: copy branch name to clipboard
<kbd>space</kbd>: przełącz
<kbd>n</kbd>: nowa gałąź
<kbd>M</kbd>: scal do obecnej gałęzi
@@ -216,8 +218,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>e</kbd>: edytuj plik
<kbd>o</kbd>: otwórz plik
<kbd>◄</kbd>: poprzedni konflikt
<kbd>►</kbd>: następny konflikt
<kbd>◀</kbd>: poprzedni konflikt
<kbd>▶</kbd>: następny konflikt
<kbd>▲</kbd>: wybierz poprzedni kawałek
<kbd>▼</kbd>: wybierz następny kawałek
<kbd>z</kbd>: cofnij
+10 -8
View File
@@ -11,13 +11,14 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>m</kbd>: 查看 合并/变基 选项
<kbd>ctrl+p</kbd>: 查看自定义补丁选项
<kbd>R</kbd>: 刷新
<kbd>x</kbd>: 打开菜单
<kbd>?</kbd>: 打开菜单
<kbd>+</kbd>: 下一屏模式(正常/半屏/全屏)
<kbd>_</kbd>: 上一屏模式
<kbd>ctrl+s</kbd>: 查看按路径过滤选项
<kbd>W</kbd>: 打开 diff 菜单
<kbd>ctrl+e</kbd>: 打开 diff 菜单
<kbd>@</kbd>: 打开命令日志菜单
<kbd>ctrl+w</kbd>: 切换是否在差异视图中显示空白字符差异
<kbd>}</kbd>: 扩大差异视图中显示的上下文范围
<kbd>{</kbd>: 缩小差异视图中显示的上下文范围
<kbd>:</kbd>: 执行自定义命令
@@ -72,6 +73,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>r</kbd>: 将已检出的分支变基到该分支
<kbd>M</kbd>: 合并到当前检出的分支
<kbd>f</kbd>: 从上游快进此分支
<kbd>T</kbd>: 创建标签
<kbd>g</kbd>: 查看重置选项
<kbd>R</kbd>: 重命名分支
<kbd>u</kbd>: set/unset upstream
@@ -157,7 +159,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>ctrl+o</kbd>: 将文件名复制到剪贴板
<kbd>ctrl+w</kbd>: 切换是否在差异视图中显示空白字符差异
<kbd>d</kbd>: 查看'放弃更改'选项
<kbd>space</kbd>: 切换暂存状态
<kbd>ctrl+b</kbd>: Filter files (staged/unstaged)
@@ -183,8 +184,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## 构建补丁中
<pre>
<kbd>◄</kbd>: 选择上一个区块
<kbd>►</kbd>: 选择下一个区块
<kbd>◀</kbd>: 选择上一个区块
<kbd>▶</kbd>: 选择下一个区块
<kbd>v</kbd>: 切换拖动选择
<kbd>V</kbd>: 切换拖动选择
<kbd>a</kbd>: 切换选择区块
@@ -211,8 +212,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<pre>
<kbd>e</kbd>: 编辑文件
<kbd>o</kbd>: 打开文件
<kbd>◄</kbd>: 选择上一个冲突
<kbd>►</kbd>: 选择下一个冲突
<kbd>◀</kbd>: 选择上一个冲突
<kbd>▶</kbd>: 选择下一个冲突
<kbd>▲</kbd>: 选择顶部块
<kbd>▼</kbd>: 选择底部块
<kbd>z</kbd>: 撤销
@@ -225,8 +226,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## 正在暂存
<pre>
<kbd>◄</kbd>: 选择上一个区块
<kbd>►</kbd>: 选择下一个区块
<kbd>◀</kbd>: 选择上一个区块
<kbd>▶</kbd>: 选择下一个区块
<kbd>v</kbd>: 切换拖动选择
<kbd>V</kbd>: 切换拖动选择
<kbd>a</kbd>: 切换选择区块
@@ -274,6 +275,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## 远程分支
<pre>
<kbd>ctrl+o</kbd>: 将分支名称复制到剪贴板
<kbd>space</kbd>: 检出
<kbd>n</kbd>: 新分支
<kbd>M</kbd>: 合并到当前检出的分支
+8 -8
View File
@@ -4,21 +4,21 @@ go 1.18
require (
github.com/OpenPeeDeeP/xdg v1.0.0
github.com/atotto/clipboard v0.1.2
github.com/atotto/clipboard v0.1.4
github.com/aybabtme/humanlog v0.4.1
github.com/cli/safeexec v1.0.0
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
github.com/creack/pty v1.1.11
github.com/fsmiamoto/git-todo-parser v0.0.2
github.com/fsmiamoto/git-todo-parser v0.0.4
github.com/fsnotify/fsnotify v1.4.7
github.com/gdamore/tcell/v2 v2.5.4
github.com/gdamore/tcell/v2 v2.6.0
github.com/go-errors/errors v1.4.2
github.com/gookit/color v1.4.2
github.com/imdario/mergo v0.3.11
github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d
github.com/jesseduffield/gocui v0.3.1-0.20230105224502-214a0df08da7
github.com/jesseduffield/gocui v0.3.1-0.20230324073941-36f2e87458fa
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
@@ -62,14 +62,14 @@ require (
github.com/onsi/ginkgo v1.10.3 // indirect
github.com/onsi/gomega v1.7.1 // indirect
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect
github.com/rivo/uniseg v0.4.3 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/xanzy/ssh-agent v0.2.1 // indirect
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
golang.org/x/exp v0.0.0-20220318154914-8dddf5d87bd8 // indirect
golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect
golang.org/x/sys v0.3.0 // indirect
golang.org/x/term v0.3.0 // indirect
golang.org/x/text v0.5.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)
+19 -13
View File
@@ -8,6 +8,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY=
github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aybabtme/humanlog v0.4.1 h1:D8d9um55rrthJsP8IGSHBcti9lTb/XknmDAX6Zy8tek=
github.com/aybabtme/humanlog v0.4.1/go.mod h1:B0bnQX4FTSU3oftPMTTPvENCy8LqixLDvYJA9TUCAGo=
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
@@ -28,15 +30,15 @@ github.com/fatih/color v1.7.1-0.20180516100307-2d684516a886/go.mod h1:Zm6kSWBoL9
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/fsmiamoto/git-todo-parser v0.0.2 h1:l6Y+9q7jbM+yK/w6kASpHO7ejL9ARCErm3tCEqOT278=
github.com/fsmiamoto/git-todo-parser v0.0.2/go.mod h1:B+AgTbNE2BARvJqzXygThzqxLIaEWvwr2sxKYYb0Fas=
github.com/fsmiamoto/git-todo-parser v0.0.4 h1:fzcGaoAFDHWzJRKw//CSZFrXucsLKplIvOSab3FtWWM=
github.com/fsmiamoto/git-todo-parser v0.0.4/go.mod h1:B+AgTbNE2BARvJqzXygThzqxLIaEWvwr2sxKYYb0Fas=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
github.com/gdamore/tcell/v2 v2.5.4 h1:TGU4tSjD3sCL788vFNeJnTdzpNKIw1H5dgLnJRQVv/k=
github.com/gdamore/tcell/v2 v2.5.4/go.mod h1:dZgRy5v4iMobMEcWNYBtREnDZAT9DYmfqIkrgEMxLyw=
github.com/gdamore/tcell/v2 v2.6.0 h1:OKbluoP9VYmJwZwq/iLb4BxwKcwGthaa1YNBJIyCySg=
github.com/gdamore/tcell/v2 v2.6.0/go.mod h1:be9omFATkdr0D9qewWW3d+MEvl5dha+Etb5y65J2H8Y=
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
@@ -72,8 +74,8 @@ github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 h1:EQP2Tv8T
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d h1:bO+OmbreIv91rCe8NmscRwhFSqkDJtzWCPV4Y+SQuXE=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
github.com/jesseduffield/gocui v0.3.1-0.20230105224502-214a0df08da7 h1:a7T87NGCrmFhzWBWvaaPQppjbY9/w0Vl4hoB7RnFaug=
github.com/jesseduffield/gocui v0.3.1-0.20230105224502-214a0df08da7/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
github.com/jesseduffield/gocui v0.3.1-0.20230324073941-36f2e87458fa h1:E9G1mj94rMal1YLaABwdxLUUgKq+xGbElFjHRNaDJUg=
github.com/jesseduffield/gocui v0.3.1-0.20230324073941-36f2e87458fa/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0=
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo=
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY=
@@ -136,8 +138,9 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw=
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/samber/lo v1.31.0 h1:Sfa+/064Tdo4SvlohQUQzBhgSer9v/coGvKQI/XLWAM=
@@ -205,19 +208,22 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+1 -1
View File
@@ -123,7 +123,7 @@ func (app *App) validateGitVersion() (*git_commands.GitVersion, error) {
return nil, minVersionError
}
if version.IsOlderThan(2, 0, 0) {
if version.IsOlderThan(2, 20, 0) {
return nil, minVersionError
}
+252 -52
View File
@@ -1,13 +1,17 @@
package daemon
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"strconv"
"github.com/fsmiamoto/git-todo-parser/todo"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/env"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
// Sometimes lazygit will be invoked in daemon mode from a parent lazygit process.
@@ -15,33 +19,58 @@ import (
// For example, if we want to ensure that a git command doesn't hang due to
// waiting for an editor to save a commit message, we can tell git to invoke lazygit
// as the editor via 'GIT_EDITOR=lazygit', and use the env var
// 'LAZYGIT_DAEMON_KIND=EXIT_IMMEDIATELY' to specify that we want to run lazygit
// as a daemon which simply exits immediately. Any additional arguments we want
// to pass to a daemon can be done via other env vars.
// 'LAZYGIT_DAEMON_KIND=1' (exit immediately) to specify that we want to run lazygit
// as a daemon which simply exits immediately.
//
// 'Daemon' is not the best name for this, because it's not a persistent background
// process, but it's close enough.
type DaemonKind string
type DaemonKind int
const (
InteractiveRebase DaemonKind = "INTERACTIVE_REBASE"
ExitImmediately DaemonKind = "EXIT_IMMEDIATELY"
// for when we fail to parse the daemon kind
DaemonKindUnknown DaemonKind = iota
DaemonKindExitImmediately
DaemonKindCherryPick
DaemonKindMoveTodoUp
DaemonKindMoveTodoDown
DaemonKindInsertBreak
DaemonKindChangeTodoActions
DaemonKindMoveFixupCommitDown
)
const (
DaemonKindEnvKey string = "LAZYGIT_DAEMON_KIND"
RebaseTODOEnvKey string = "LAZYGIT_REBASE_TODO"
// Contains json-encoded arguments to the daemon
DaemonInstructionEnvKey string = "LAZYGIT_DAEMON_INSTRUCTION"
)
type Daemon interface {
Run() error
func getInstruction() Instruction {
jsonData := os.Getenv(DaemonInstructionEnvKey)
mapping := map[DaemonKind]func(string) Instruction{
DaemonKindExitImmediately: deserializeInstruction[*ExitImmediatelyInstruction],
DaemonKindCherryPick: deserializeInstruction[*CherryPickCommitsInstruction],
DaemonKindChangeTodoActions: deserializeInstruction[*ChangeTodoActionsInstruction],
DaemonKindMoveFixupCommitDown: deserializeInstruction[*MoveFixupCommitDownInstruction],
DaemonKindMoveTodoUp: deserializeInstruction[*MoveTodoUpInstruction],
DaemonKindMoveTodoDown: deserializeInstruction[*MoveTodoDownInstruction],
DaemonKindInsertBreak: deserializeInstruction[*InsertBreakInstruction],
}
return mapping[getDaemonKind()](jsonData)
}
func Handle(common *common.Common) {
d := getDaemon(common)
if d == nil {
if !InDaemonMode() {
return
}
if err := d.Run(); err != nil {
instruction := getInstruction()
if err := instruction.run(common); err != nil {
log.Fatal(err)
}
@@ -49,58 +78,229 @@ func Handle(common *common.Common) {
}
func InDaemonMode() bool {
return getDaemonKind() != ""
}
func getDaemon(common *common.Common) Daemon {
switch getDaemonKind() {
case InteractiveRebase:
return &rebaseDaemon{c: common}
case ExitImmediately:
return &exitImmediatelyDaemon{c: common}
}
return nil
return getDaemonKind() != DaemonKindUnknown
}
func getDaemonKind() DaemonKind {
return DaemonKind(os.Getenv(DaemonKindEnvKey))
intValue, err := strconv.Atoi(os.Getenv(DaemonKindEnvKey))
if err != nil {
return DaemonKindUnknown
}
return DaemonKind(intValue)
}
type rebaseDaemon struct {
c *common.Common
// An Instruction is a command to be run by lazygit in daemon mode.
// It is serialized to json and passed to lazygit via environment variables
type Instruction interface {
Kind() DaemonKind
SerializedInstructions() string
// runs the instruction
run(common *common.Common) error
}
func (self *rebaseDaemon) Run() error {
self.c.Log.Info("Lazygit invoked as interactive rebase demon")
self.c.Log.Info("args: ", os.Args)
func serializeInstruction[T any](instruction T) string {
jsonData, err := json.Marshal(instruction)
if err != nil {
// this should never happen
panic(err)
}
if strings.HasSuffix(os.Args[1], "git-rebase-todo") {
if err := os.WriteFile(os.Args[1], []byte(os.Getenv(RebaseTODOEnvKey)), 0o644); err != nil {
return err
return string(jsonData)
}
func deserializeInstruction[T Instruction](jsonData string) Instruction {
var instruction T
err := json.Unmarshal([]byte(jsonData), &instruction)
if err != nil {
panic(err)
}
return instruction
}
func ToEnvVars(instruction Instruction) []string {
return []string{
fmt.Sprintf("%s=%d", DaemonKindEnvKey, instruction.Kind()),
fmt.Sprintf("%s=%s", DaemonInstructionEnvKey, instruction.SerializedInstructions()),
}
}
type ExitImmediatelyInstruction struct{}
func (self *ExitImmediatelyInstruction) Kind() DaemonKind {
return DaemonKindExitImmediately
}
func (self *ExitImmediatelyInstruction) SerializedInstructions() string {
return serializeInstruction(self)
}
func (self *ExitImmediatelyInstruction) run(common *common.Common) error {
return nil
}
func NewExitImmediatelyInstruction() Instruction {
return &ExitImmediatelyInstruction{}
}
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,
}
} else if strings.HasSuffix(os.Args[1], filepath.Join(gitDir(), "COMMIT_EDITMSG")) { // TODO: test
// if we are rebasing and squashing, we'll see a COMMIT_EDITMSG
// but in this case we don't need to edit it, so we'll just return
} else {
self.c.Log.Info("Lazygit demon did not match on any use cases")
})
todo := TodoLinesToString(todoLines)
return &CherryPickCommitsInstruction{
Todo: todo,
}
return nil
}
func gitDir() string {
dir := env.GetGitDirEnv()
if dir == "" {
return ".git"
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
}
func NewChangeTodoActionsInstruction(changes []ChangeTodoAction) Instruction {
return &ChangeTodoActionsInstruction{
Changes: changes,
}
return dir
}
type exitImmediatelyDaemon struct {
c *common.Common
func (self *ChangeTodoActionsInstruction) Kind() DaemonKind {
return DaemonKindChangeTodoActions
}
func (self *exitImmediatelyDaemon) Run() error {
return nil
func (self *ChangeTodoActionsInstruction) SerializedInstructions() string {
return serializeInstruction(self)
}
func (self *ChangeTodoActionsInstruction) run(common *common.Common) error {
return handleInteractiveRebase(common, func(path string) error {
for _, c := range self.Changes {
if err := utils.EditRebaseTodo(path, c.Sha, todo.Pick, c.NewAction); err != nil {
return err
}
}
return nil
})
}
// Takes the sha of some commit, and the sha of a fixup commit that was created
// at the end of the branch, then moves the fixup commit down to right after the
// original commit, changing its type to "fixup"
type MoveFixupCommitDownInstruction struct {
OriginalSha string
FixupSha string
}
func NewMoveFixupCommitDownInstruction(originalSha string, fixupSha string) Instruction {
return &MoveFixupCommitDownInstruction{
OriginalSha: originalSha,
FixupSha: fixupSha,
}
}
func (self *MoveFixupCommitDownInstruction) Kind() DaemonKind {
return DaemonKindMoveFixupCommitDown
}
func (self *MoveFixupCommitDownInstruction) SerializedInstructions() string {
return serializeInstruction(self)
}
func (self *MoveFixupCommitDownInstruction) run(common *common.Common) error {
return handleInteractiveRebase(common, func(path string) error {
return utils.MoveFixupCommitDown(path, self.OriginalSha, self.FixupSha)
})
}
type MoveTodoUpInstruction struct {
Sha string
}
func NewMoveTodoUpInstruction(sha string) Instruction {
return &MoveTodoUpInstruction{
Sha: sha,
}
}
func (self *MoveTodoUpInstruction) Kind() DaemonKind {
return DaemonKindMoveTodoUp
}
func (self *MoveTodoUpInstruction) SerializedInstructions() string {
return serializeInstruction(self)
}
func (self *MoveTodoUpInstruction) run(common *common.Common) error {
return handleInteractiveRebase(common, func(path string) error {
return utils.MoveTodoUp(path, self.Sha, todo.Pick)
})
}
type MoveTodoDownInstruction struct {
Sha string
}
func NewMoveTodoDownInstruction(sha string) Instruction {
return &MoveTodoDownInstruction{
Sha: sha,
}
}
func (self *MoveTodoDownInstruction) Kind() DaemonKind {
return DaemonKindMoveTodoDown
}
func (self *MoveTodoDownInstruction) SerializedInstructions() string {
return serializeInstruction(self)
}
func (self *MoveTodoDownInstruction) run(common *common.Common) error {
return handleInteractiveRebase(common, func(path string) error {
return utils.MoveTodoDown(path, self.Sha, todo.Pick)
})
}
type InsertBreakInstruction struct{}
func NewInsertBreakInstruction() Instruction {
return &InsertBreakInstruction{}
}
func (self *InsertBreakInstruction) Kind() DaemonKind {
return DaemonKindInsertBreak
}
func (self *InsertBreakInstruction) SerializedInstructions() string {
return serializeInstruction(self)
}
func (self *InsertBreakInstruction) run(common *common.Common) error {
return handleInteractiveRebase(common, func(path string) error {
return utils.PrependStrToTodoFile(path, []byte("break\n"))
})
}
+64
View File
@@ -0,0 +1,64 @@
package daemon
import (
"os"
"path/filepath"
"strings"
"github.com/fsmiamoto/git-todo-parser/todo"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/env"
)
type TodoLine struct {
Action string
Commit *models.Commit
}
func (self *TodoLine) ToString() string {
if self.Action == "break" {
return self.Action + "\n"
} else {
return self.Action + " " + self.Commit.Sha + " " + self.Commit.Name + "\n"
}
}
func TodoLinesToString(todoLines []TodoLine) string {
lines := slices.Map(todoLines, func(todoLine TodoLine) string {
return todoLine.ToString()
})
return strings.Join(slices.Reverse(lines), "")
}
type ChangeTodoAction struct {
Sha string
NewAction todo.TodoCommand
}
func handleInteractiveRebase(common *common.Common, f func(path string) error) error {
common.Log.Info("Lazygit invoked as interactive rebase demon")
common.Log.Info("args: ", os.Args)
path := os.Args[1]
if strings.HasSuffix(path, "git-rebase-todo") {
return f(path)
} else if strings.HasSuffix(path, filepath.Join(gitDir(), "COMMIT_EDITMSG")) { // TODO: test
// if we are rebasing and squashing, we'll see a COMMIT_EDITMSG
// but in this case we don't need to edit it, so we'll just return
} else {
common.Log.Info("Lazygit demon did not match on any use cases")
}
return nil
}
func gitDir() string {
dir := env.GetGitDirEnv()
if dir == "" {
return ".git"
}
return dir
}
+28 -27
View File
@@ -87,33 +87,34 @@ func writeString(file *os.File, str string) {
func localisedTitle(tr *i18n.TranslationSet, str string) string {
contextTitleMap := map[string]string{
"global": tr.GlobalTitle,
"navigation": tr.NavigationTitle,
"branches": tr.BranchesTitle,
"localBranches": tr.LocalBranchesTitle,
"files": tr.FilesTitle,
"status": tr.StatusTitle,
"submodules": tr.SubmodulesTitle,
"subCommits": tr.SubCommitsTitle,
"remoteBranches": tr.RemoteBranchesTitle,
"remotes": tr.RemotesTitle,
"reflogCommits": tr.ReflogCommitsTitle,
"tags": tr.TagsTitle,
"commitFiles": tr.CommitFilesTitle,
"commitMessage": tr.CommitMessageTitle,
"commits": tr.CommitsTitle,
"confirmation": tr.ConfirmationTitle,
"information": tr.InformationTitle,
"main": tr.NormalTitle,
"patchBuilding": tr.PatchBuildingTitle,
"mergeConflicts": tr.MergingTitle,
"staging": tr.StagingTitle,
"menu": tr.MenuTitle,
"search": tr.SearchTitle,
"secondary": tr.SecondaryTitle,
"stash": tr.StashTitle,
"suggestions": tr.SuggestionsCheatsheetTitle,
"extras": tr.ExtrasTitle,
"global": tr.GlobalTitle,
"navigation": tr.NavigationTitle,
"branches": tr.BranchesTitle,
"localBranches": tr.LocalBranchesTitle,
"files": tr.FilesTitle,
"status": tr.StatusTitle,
"submodules": tr.SubmodulesTitle,
"subCommits": tr.SubCommitsTitle,
"remoteBranches": tr.RemoteBranchesTitle,
"remotes": tr.RemotesTitle,
"reflogCommits": tr.ReflogCommitsTitle,
"tags": tr.TagsTitle,
"commitFiles": tr.CommitFilesTitle,
"commitMessage": tr.CommitMessageTitle,
"commitDescription": tr.CommitDescriptionTitle,
"commits": tr.CommitsTitle,
"confirmation": tr.ConfirmationTitle,
"information": tr.InformationTitle,
"main": tr.NormalTitle,
"patchBuilding": tr.PatchBuildingTitle,
"mergeConflicts": tr.MergingTitle,
"staging": tr.StagingTitle,
"menu": tr.MenuTitle,
"search": tr.SearchTitle,
"secondary": tr.SecondaryTitle,
"stash": tr.StashTitle,
"suggestions": tr.SuggestionsCheatsheetTitle,
"extras": tr.ExtrasTitle,
}
title, ok := contextTitleMap[str]
+9 -4
View File
@@ -117,9 +117,14 @@ func NewGitCommandAux(
workingTreeCommands := git_commands.NewWorkingTreeCommands(gitCommon, submoduleCommands, fileLoader)
rebaseCommands := git_commands.NewRebaseCommands(gitCommon, commitCommands, workingTreeCommands)
stashCommands := git_commands.NewStashCommands(gitCommon, fileLoader, workingTreeCommands)
// TODO: have patch manager take workingTreeCommands in its entirety
patchManager := patch.NewPatchManager(cmn.Log, workingTreeCommands.ApplyPatch, workingTreeCommands.ShowFileDiff)
patchCommands := git_commands.NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchManager)
// TODO: have patch builder take workingTreeCommands in its entirety
patchBuilder := patch.NewPatchBuilder(cmn.Log, workingTreeCommands.ApplyPatch,
func(from string, to string, reverse bool, filename string, plain bool) (string, error) {
// TODO: make patch builder take Gui.IgnoreWhitespaceInDiffView into
// account. For now we just pass false.
return workingTreeCommands.ShowFileDiff(from, to, reverse, filename, plain, false)
})
patchCommands := git_commands.NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchBuilder)
bisectCommands := git_commands.NewBisectCommands(gitCommon)
branchLoader := git_commands.NewBranchLoader(cmn, branchCommands.GetRawBranches, branchCommands.CurrentBranchInfo, configCommands)
@@ -128,7 +133,7 @@ func NewGitCommandAux(
reflogCommitLoader := git_commands.NewReflogCommitLoader(cmn, cmd)
remoteLoader := git_commands.NewRemoteLoader(cmn, cmd, repo.Remotes)
stashLoader := git_commands.NewStashLoader(cmn, cmd)
tagLoader := git_commands.NewTagLoader(cmn, version, cmd)
tagLoader := git_commands.NewTagLoader(cmn, cmd)
return &GitCommand{
Branch: branchCommands,
+37 -13
View File
@@ -8,6 +8,8 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
)
var ErrInvalidCommitIndex = errors.New("invalid commit index")
type CommitCommands struct {
*GitCommon
}
@@ -18,11 +20,6 @@ func NewCommitCommands(gitCommon *GitCommon) *CommitCommands {
}
}
// RewordLastCommit rewords the topmost commit with the given message
func (self *CommitCommands) RewordLastCommit(message string) error {
return self.cmd.New("git commit --allow-empty --amend --only -m " + self.cmd.Quote(message)).Run()
}
// ResetAuthor resets the author of the topmost commit
func (self *CommitCommands) ResetAuthor() error {
return self.cmd.New("git commit --allow-empty --only --no-edit --amend --reset-author").Run()
@@ -45,11 +42,7 @@ func (self *CommitCommands) ResetToCommit(sha string, strength string, envVars [
}
func (self *CommitCommands) CommitCmdObj(message string) oscommands.ICmdObj {
splitMessage := strings.Split(message, "\n")
lineArgs := ""
for _, line := range splitMessage {
lineArgs += fmt.Sprintf(" -m %s", self.cmd.Quote(line))
}
messageArgs := self.commitMessageArgs(message)
skipHookPrefix := self.UserConfig.Git.SkipHookPrefix
noVerifyFlag := ""
@@ -57,7 +50,23 @@ func (self *CommitCommands) CommitCmdObj(message string) oscommands.ICmdObj {
noVerifyFlag = " --no-verify"
}
return self.cmd.New(fmt.Sprintf("git commit%s%s%s", noVerifyFlag, self.signoffFlag(), lineArgs))
return self.cmd.New(fmt.Sprintf("git commit%s%s%s", noVerifyFlag, self.signoffFlag(), messageArgs))
}
// RewordLastCommit rewords the topmost commit with the given message
func (self *CommitCommands) RewordLastCommit(message string) error {
messageArgs := self.commitMessageArgs(message)
return self.cmd.New(fmt.Sprintf("git commit --allow-empty --amend --only%s", messageArgs)).Run()
}
func (self *CommitCommands) commitMessageArgs(message string) string {
msg, description, _ := strings.Cut(message, "\n")
descriptionArgs := ""
if description != "" {
descriptionArgs = fmt.Sprintf(" -m %s", self.cmd.Quote(description))
}
return fmt.Sprintf(" -m %s%s", self.cmd.Quote(msg), descriptionArgs)
}
// runs git commit without the -m argument meaning it will invoke the user's editor
@@ -149,14 +158,19 @@ func (self *CommitCommands) AmendHeadCmdObj() oscommands.ICmdObj {
return self.cmd.New("git commit --amend --no-edit --allow-empty")
}
func (self *CommitCommands) ShowCmdObj(sha string, filterPath string) oscommands.ICmdObj {
func (self *CommitCommands) ShowCmdObj(sha string, filterPath string, ignoreWhitespace bool) oscommands.ICmdObj {
contextSize := self.UserConfig.Git.DiffContextSize
filterPathArg := ""
if filterPath != "" {
filterPathArg = fmt.Sprintf(" -- %s", self.cmd.Quote(filterPath))
}
ignoreWhitespaceArg := ""
if ignoreWhitespace {
ignoreWhitespaceArg = " --ignore-all-space"
}
cmdStr := fmt.Sprintf("git show --submodule --color=%s --unified=%d --no-renames --stat -p %s %s", self.UserConfig.Git.Paging.ColorArg, contextSize, sha, filterPathArg)
cmdStr := fmt.Sprintf("git show --submodule --color=%s --unified=%d --stat -p %s%s%s",
self.UserConfig.Git.Paging.ColorArg, contextSize, sha, ignoreWhitespaceArg, filterPathArg)
return self.cmd.New(cmdStr).DontLog()
}
@@ -173,3 +187,13 @@ func (self *CommitCommands) RevertMerge(sha string, parentNumber int) error {
func (self *CommitCommands) CreateFixupCommit(sha string) error {
return self.cmd.New(fmt.Sprintf("git commit --fixup=%s", sha)).Run()
}
// a value of 0 means the head commit, 1 is the parent commit, etc
func (self *CommitCommands) GetCommitMessageFromHistory(value int) (string, error) {
hash, _ := self.cmd.New(fmt.Sprintf("git log -1 --skip=%d --pretty=%%H", value)).DontLog().RunWithOutput()
formattedHash := strings.TrimSpace(hash)
if len(formattedHash) == 0 {
return "", ErrInvalidCommitIndex
}
return self.GetCommitMessage(formattedHash)
}
+33 -31
View File
@@ -15,7 +15,6 @@ import (
"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/gui/style"
)
// context:
@@ -68,10 +67,6 @@ type GetCommitsOptions struct {
func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit, error) {
commits := []*models.Commit{}
var rebasingCommits []*models.Commit
rebaseMode, err := self.getRebaseMode()
if err != nil {
return nil, err
}
if opts.IncludeRebaseCommits && opts.FilterPath == "" {
var err error
@@ -94,7 +89,7 @@ func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit,
if commit.Sha == firstPushedCommit {
passedFirstPushedCommit = true
}
commit.Status = map[bool]string{true: "unpushed", false: "pushed"}[!passedFirstPushedCommit]
commit.Status = map[bool]models.CommitStatus{true: models.StatusUnpushed, false: models.StatusPushed}[!passedFirstPushedCommit]
commits = append(commits, commit)
return false, nil
})
@@ -106,12 +101,6 @@ func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit,
return commits, nil
}
if rebaseMode != enums.REBASE_MODE_NONE {
currentCommit := commits[len(rebasingCommits)]
youAreHere := style.FgYellow.Sprintf("<-- %s ---", self.Tr.YouAreHere)
currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name)
}
commits, err = self.setCommitMergedStatuses(opts.RefName, commits)
if err != nil {
return nil, err
@@ -124,7 +113,7 @@ func (self *CommitLoader) MergeRebasingCommits(commits []*models.Commit) ([]*mod
// 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 {
if commit.Status != "rebasing" { // removing the existing rebase commits so we can add the refreshed ones
if !commit.IsTODO() { // removing the existing rebase commits so we can add the refreshed ones
result = append(result, commits[i:]...)
break
}
@@ -205,8 +194,8 @@ func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode
return nil, nil
}
commitShas := slices.Map(commits, func(commit *models.Commit) string {
return commit.Sha
commitShas := slices.FilterMap(commits, func(commit *models.Commit) (string, bool) {
return commit.Sha, commit.Sha != ""
})
// note that we're not filtering these as we do non-rebasing commits just because
@@ -220,20 +209,26 @@ func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode
),
).DontLog()
hydratedCommits := make([]*models.Commit, 0, len(commits))
i := 0
fullCommits := map[string]*models.Commit{}
err = cmdObj.RunAndProcessLines(func(line string) (bool, error) {
commit := self.extractCommitFromLine(line)
matchingCommit := commits[i]
commit.Action = matchingCommit.Action
commit.Status = matchingCommit.Status
hydratedCommits = append(hydratedCommits, commit)
i++
fullCommits[commit.Sha] = commit
return false, nil
})
if err != nil {
return nil, err
}
hydratedCommits := make([]*models.Commit, 0, len(commits))
for _, rebasingCommit := range commits {
if rebasingCommit.Sha == "" {
hydratedCommits = append(hydratedCommits, rebasingCommit)
} else if commit := fullCommits[rebasingCommit.Sha]; commit != nil {
commit.Action = rebasingCommit.Action
commit.Status = rebasingCommit.Status
hydratedCommits = append(hydratedCommits, commit)
}
}
return hydratedCommits, nil
}
@@ -316,15 +311,17 @@ func (self *CommitLoader) getInteractiveRebasingCommits() ([]*models.Commit, err
}
for _, t := range todos {
if t.Commit == "" {
if t.Command == todo.UpdateRef {
t.Msg = strings.TrimPrefix(t.Ref, "refs/heads/")
} else if t.Commit == "" {
// Command does not have a commit associated, skip
continue
}
commits = slices.Prepend(commits, &models.Commit{
Sha: t.Commit,
Name: t.Msg,
Status: "rebasing",
Action: t.Command.String(),
Status: models.StatusRebasing,
Action: t.Command,
})
}
@@ -343,7 +340,7 @@ func (self *CommitLoader) commitFromPatch(content string) *models.Commit {
return &models.Commit{
Sha: sha,
Name: name,
Status: "rebasing",
Status: models.StatusRebasing,
}
}
@@ -360,11 +357,11 @@ func (self *CommitLoader) setCommitMergedStatuses(refName string, commits []*mod
if strings.HasPrefix(ancestor, commit.Sha) {
passedAncestor = true
}
if commit.Status != "pushed" {
if commit.Status != models.StatusPushed {
continue
}
if passedAncestor {
commits[i].Status = "merged"
commits[i].Status = models.StatusMerged
}
}
return commits, nil
@@ -401,7 +398,9 @@ func ignoringWarnings(commandOutput string) string {
func (self *CommitLoader) getFirstPushedCommit(refName string) (string, error) {
output, err := self.cmd.
New(
fmt.Sprintf("git merge-base %s %s@{u}", self.cmd.Quote(refName), self.cmd.Quote(refName)),
fmt.Sprintf("git merge-base %s %s@{u}",
self.cmd.Quote(refName),
self.cmd.Quote(strings.TrimPrefix(refName, "refs/heads/"))),
).
DontLog().
RunWithOutput()
@@ -419,9 +418,11 @@ func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj {
limitFlag = " -300"
}
followFlag := ""
filterFlag := ""
if opts.FilterPath != "" {
filterFlag = fmt.Sprintf(" --follow -- %s", self.cmd.Quote(opts.FilterPath))
followFlag = " --follow"
filterFlag = fmt.Sprintf(" %s", self.cmd.Quote(opts.FilterPath))
}
config := self.UserConfig.Git.Log
@@ -437,13 +438,14 @@ func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj {
return self.cmd.New(
fmt.Sprintf(
"git -c log.showSignature=false log %s%s%s --oneline %s%s --abbrev=%d%s",
"git log %s%s%s --oneline %s%s --abbrev=%d%s --no-show-signature --%s",
self.cmd.Quote(opts.RefName),
orderFlag,
allFlag,
prettyFormat,
limitFlag,
40,
followFlag,
filterFlag,
),
).DontLog()
+45 -19
View File
@@ -42,7 +42,20 @@ func TestGetCommits(t *testing.T) {
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
runner: oscommands.NewFakeRunner(t).
Expect(`git merge-base "HEAD" "HEAD"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
Expect(`git -c log.showSignature=false log "HEAD" --topo-order --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40`, "", nil),
Expect(`git log "HEAD" --topo-order --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40 --no-show-signature --`, "", nil),
expectedCommits: []*models.Commit{},
expectedError: nil,
},
{
testName: "should use proper upstream name for branch",
logOrder: "topo-order",
rebaseMode: enums.REBASE_MODE_NONE,
currentBranchName: "mybranch",
opts: GetCommitsOptions{RefName: "refs/heads/mybranch", IncludeRebaseCommits: false},
runner: oscommands.NewFakeRunner(t).
Expect(`git merge-base "refs/heads/mybranch" "mybranch"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
Expect(`git log "refs/heads/mybranch" --topo-order --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40 --no-show-signature --`, "", nil),
expectedCommits: []*models.Commit{},
expectedError: nil,
@@ -57,7 +70,7 @@ func TestGetCommits(t *testing.T) {
// here it's seeing which commits are yet to be pushed
Expect(`git merge-base "HEAD" "HEAD"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
// here it's actually getting all the commits in a formatted form, one per line
Expect(`git -c log.showSignature=false log "HEAD" --topo-order --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40`, commitsOutput, nil).
Expect(`git log "HEAD" --topo-order --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40 --no-show-signature --`, commitsOutput, nil).
// here it's seeing where our branch diverged from the master branch so that we can mark that commit and parent commits as 'merged'
Expect(`git merge-base "HEAD" "master"`, "26c07b1ab33860a1a7591a0638f9925ccf497ffa", nil),
@@ -65,8 +78,8 @@ func TestGetCommits(t *testing.T) {
{
Sha: "0eea75e8c631fba6b58135697835d58ba4c18dbc",
Name: "better typing for rebase mode",
Status: "unpushed",
Action: "",
Status: models.StatusUnpushed,
Action: models.ActionNone,
Tags: []string{},
ExtraInfo: "(HEAD -> better-tests)",
AuthorName: "Jesse Duffield",
@@ -79,8 +92,8 @@ func TestGetCommits(t *testing.T) {
{
Sha: "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164",
Name: "fix logging",
Status: "pushed",
Action: "",
Status: models.StatusPushed,
Action: models.ActionNone,
Tags: []string{},
ExtraInfo: "(origin/better-tests)",
AuthorName: "Jesse Duffield",
@@ -93,8 +106,8 @@ func TestGetCommits(t *testing.T) {
{
Sha: "e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c",
Name: "refactor",
Status: "pushed",
Action: "",
Status: models.StatusPushed,
Action: models.ActionNone,
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
@@ -107,8 +120,8 @@ func TestGetCommits(t *testing.T) {
{
Sha: "d8084cd558925eb7c9c38afeed5725c21653ab90",
Name: "WIP",
Status: "pushed",
Action: "",
Status: models.StatusPushed,
Action: models.ActionNone,
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
@@ -121,8 +134,8 @@ func TestGetCommits(t *testing.T) {
{
Sha: "65f910ebd85283b5cce9bf67d03d3f1a9ea3813a",
Name: "WIP",
Status: "pushed",
Action: "",
Status: models.StatusPushed,
Action: models.ActionNone,
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
@@ -135,8 +148,8 @@ func TestGetCommits(t *testing.T) {
{
Sha: "26c07b1ab33860a1a7591a0638f9925ccf497ffa",
Name: "WIP",
Status: "merged",
Action: "",
Status: models.StatusMerged,
Action: models.ActionNone,
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
@@ -149,8 +162,8 @@ func TestGetCommits(t *testing.T) {
{
Sha: "3d4470a6c072208722e5ae9a54bcb9634959a1c5",
Name: "WIP",
Status: "merged",
Action: "",
Status: models.StatusMerged,
Action: models.ActionNone,
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
@@ -163,8 +176,8 @@ func TestGetCommits(t *testing.T) {
{
Sha: "053a66a7be3da43aacdc7aa78e1fe757b82c4dd2",
Name: "refactoring the config struct",
Status: "merged",
Action: "",
Status: models.StatusMerged,
Action: models.ActionNone,
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
@@ -185,7 +198,20 @@ func TestGetCommits(t *testing.T) {
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
runner: oscommands.NewFakeRunner(t).
Expect(`git merge-base "HEAD" "HEAD"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
Expect(`git -c log.showSignature=false log "HEAD" --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40`, "", nil),
Expect(`git log "HEAD" --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40 --no-show-signature --`, "", nil),
expectedCommits: []*models.Commit{},
expectedError: nil,
},
{
testName: "should set filter path",
logOrder: "default",
rebaseMode: enums.REBASE_MODE_NONE,
currentBranchName: "master",
opts: GetCommitsOptions{RefName: "HEAD", FilterPath: "src"},
runner: oscommands.NewFakeRunner(t).
Expect(`git merge-base "HEAD" "HEAD"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
Expect(`git log "HEAD" --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40 --follow --no-show-signature -- "src"`, "", nil),
expectedCommits: []*models.Commit{},
expectedError: nil,
+90 -22
View File
@@ -9,12 +9,32 @@ import (
)
func TestCommitRewordCommit(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, "", nil)
instance := buildCommitCommands(commonDeps{runner: runner})
type scenario struct {
testName string
runner *oscommands.FakeCmdObjRunner
input string
}
scenarios := []scenario{
{
"Single line reword",
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, "", nil),
"test",
},
{
"Multi line reword",
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test", "-m", "line 2\nline 3"}, "", nil),
"test\nline 2\nline 3",
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildCommitCommands(commonDeps{runner: s.runner})
assert.NoError(t, instance.RewordLastCommit("test"))
runner.CheckForMissingCalls()
assert.NoError(t, instance.RewordLastCommit(s.input))
s.runner.CheckForMissingCalls()
})
}
}
func TestCommitResetToCommit(t *testing.T) {
@@ -177,30 +197,41 @@ func TestCommitCreateFixupCommit(t *testing.T) {
func TestCommitShowCmdObj(t *testing.T) {
type scenario struct {
testName string
filterPath string
contextSize int
expected string
testName string
filterPath string
contextSize int
ignoreWhitespace bool
expected string
}
scenarios := []scenario{
{
testName: "Default case without filter path",
filterPath: "",
contextSize: 3,
expected: "git show --submodule --color=always --unified=3 --no-renames --stat -p 1234567890 ",
testName: "Default case without filter path",
filterPath: "",
contextSize: 3,
ignoreWhitespace: false,
expected: "git show --submodule --color=always --unified=3 --stat -p 1234567890",
},
{
testName: "Default case with filter path",
filterPath: "file.txt",
contextSize: 3,
expected: `git show --submodule --color=always --unified=3 --no-renames --stat -p 1234567890 -- "file.txt"`,
testName: "Default case with filter path",
filterPath: "file.txt",
contextSize: 3,
ignoreWhitespace: false,
expected: `git show --submodule --color=always --unified=3 --stat -p 1234567890 -- "file.txt"`,
},
{
testName: "Show diff with custom context size",
filterPath: "",
contextSize: 77,
expected: "git show --submodule --color=always --unified=77 --no-renames --stat -p 1234567890 ",
testName: "Show diff with custom context size",
filterPath: "",
contextSize: 77,
ignoreWhitespace: false,
expected: "git show --submodule --color=always --unified=77 --stat -p 1234567890",
},
{
testName: "Show diff, ignoring whitespace",
filterPath: "",
contextSize: 77,
ignoreWhitespace: true,
expected: "git show --submodule --color=always --unified=77 --stat -p 1234567890 --ignore-all-space",
},
}
@@ -212,7 +243,7 @@ func TestCommitShowCmdObj(t *testing.T) {
instance := buildCommitCommands(commonDeps{userConfig: userConfig})
cmdStr := instance.ShowCmdObj("1234567890", s.filterPath).ToString()
cmdStr := instance.ShowCmdObj("1234567890", s.filterPath, s.ignoreWhitespace).ToString()
assert.Equal(t, s.expected, cmdStr)
})
}
@@ -263,3 +294,40 @@ Merge pull request #1750 from mark2185/fix-issue-template
})
}
}
func TestGetCommitMessageFromHistory(t *testing.T) {
type scenario struct {
testName string
runner *oscommands.FakeCmdObjRunner
test func(string, error)
}
scenarios := []scenario{
{
"Empty message",
oscommands.NewFakeRunner(t).Expect("git log -1 --skip=2 --pretty=%H", "", nil).Expect("git rev-list --format=%B --max-count=1 ", "", nil),
func(output string, err error) {
assert.Error(t, err)
},
},
{
"Default case to retrieve a commit in history",
oscommands.NewFakeRunner(t).Expect("git log -1 --skip=2 --pretty=%H", "sha3 \n", nil).Expect("git rev-list --format=%B --max-count=1 sha3", `commit sha3
use generics to DRY up context code`, nil),
func(output string, err error) {
assert.NoError(t, err)
assert.Equal(t, "use generics to DRY up context code", output)
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildCommitCommands(commonDeps{runner: s.runner})
output, err := instance.GetCommitMessageFromHistory(2)
s.test(output, err)
})
}
}
+82 -1
View File
@@ -3,8 +3,10 @@ package git_commands
import (
"os"
"strconv"
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -27,7 +29,7 @@ func (self *FileCommands) Cat(fileName string) (string, error) {
return string(buf), nil
}
func (self *FileCommands) GetEditCmdStr(filename string, lineNumber int) (string, error) {
func (self *FileCommands) GetEditCmdStrLegacy(filename string, lineNumber int) (string, error) {
editor := self.UserConfig.OS.EditCommand
if editor == "" {
@@ -72,3 +74,82 @@ func (self *FileCommands) GetEditCmdStr(filename string, lineNumber int) (string
}
return utils.ResolvePlaceholderString(editCmdTemplate, templateValues), nil
}
func (self *FileCommands) GetEditCmdStr(filename string) (string, bool) {
// Legacy support for old config; to be removed at some point
if self.UserConfig.OS.Edit == "" && self.UserConfig.OS.EditCommandTemplate != "" {
if cmdStr, err := self.GetEditCmdStrLegacy(filename, 1); err == nil {
return cmdStr, true
}
}
template, editInTerminal := config.GetEditTemplate(&self.UserConfig.OS, self.guessDefaultEditor)
templateValues := map[string]string{
"filename": self.cmd.Quote(filename),
}
cmdStr := utils.ResolvePlaceholderString(template, templateValues)
return cmdStr, editInTerminal
}
func (self *FileCommands) GetEditAtLineCmdStr(filename string, lineNumber int) (string, bool) {
// Legacy support for old config; to be removed at some point
if self.UserConfig.OS.EditAtLine == "" && self.UserConfig.OS.EditCommandTemplate != "" {
if cmdStr, err := self.GetEditCmdStrLegacy(filename, lineNumber); err == nil {
return cmdStr, true
}
}
template, editInTerminal := config.GetEditAtLineTemplate(&self.UserConfig.OS, self.guessDefaultEditor)
templateValues := map[string]string{
"filename": self.cmd.Quote(filename),
"line": strconv.Itoa(lineNumber),
}
cmdStr := utils.ResolvePlaceholderString(template, templateValues)
return cmdStr, editInTerminal
}
func (self *FileCommands) GetEditAtLineAndWaitCmdStr(filename string, lineNumber int) string {
// Legacy support for old config; to be removed at some point
if self.UserConfig.OS.EditAtLineAndWait == "" && self.UserConfig.OS.EditCommandTemplate != "" {
if cmdStr, err := self.GetEditCmdStrLegacy(filename, lineNumber); err == nil {
return cmdStr
}
}
template := config.GetEditAtLineAndWaitTemplate(&self.UserConfig.OS, self.guessDefaultEditor)
templateValues := map[string]string{
"filename": self.cmd.Quote(filename),
"line": strconv.Itoa(lineNumber),
}
cmdStr := utils.ResolvePlaceholderString(template, templateValues)
return cmdStr
}
func (self *FileCommands) guessDefaultEditor() string {
// Try to query a few places where editors get configured
editor := self.config.GetCoreEditor()
if editor == "" {
editor = self.os.Getenv("GIT_EDITOR")
}
if editor == "" {
editor = self.os.Getenv("VISUAL")
}
if editor == "" {
editor = self.os.Getenv("EDITOR")
}
if editor != "" {
// At this point, it might be more than just the name of the editor;
// e.g. it might be "code -w" or "vim -u myvim.rc". So assume that
// everything up to the first space is the editor name.
editor = strings.Split(editor, " ")[0]
}
return editor
}
+201 -2
View File
@@ -10,7 +10,7 @@ import (
"github.com/stretchr/testify/assert"
)
func TestEditFileCmdStr(t *testing.T) {
func TestEditFileCmdStrLegacy(t *testing.T) {
type scenario struct {
filename string
configEditCommand string
@@ -172,7 +172,206 @@ func TestEditFileCmdStr(t *testing.T) {
getenv: s.getenv,
})
s.test(instance.GetEditCmdStr(s.filename, 1))
s.test(instance.GetEditCmdStrLegacy(s.filename, 1))
s.runner.CheckForMissingCalls()
}
}
func TestEditFileCmd(t *testing.T) {
type scenario struct {
filename string
osConfig config.OSConfig
expectedCmdStr string
expectedEditInTerminal bool
}
scenarios := []scenario{
{
filename: "test",
osConfig: config.OSConfig{},
expectedCmdStr: `vim -- "test"`,
expectedEditInTerminal: true,
},
{
filename: "test",
osConfig: config.OSConfig{
Edit: "nano {{filename}}",
},
expectedCmdStr: `nano "test"`,
expectedEditInTerminal: true,
},
{
filename: "file/with space",
osConfig: config.OSConfig{
EditPreset: "sublime",
},
expectedCmdStr: `subl -- "file/with space"`,
expectedEditInTerminal: false,
},
}
for _, s := range scenarios {
userConfig := config.GetDefaultConfig()
userConfig.OS = s.osConfig
instance := buildFileCommands(commonDeps{
userConfig: userConfig,
})
cmdStr, editInTerminal := instance.GetEditCmdStr(s.filename)
assert.Equal(t, s.expectedCmdStr, cmdStr)
assert.Equal(t, s.expectedEditInTerminal, editInTerminal)
}
}
func TestEditFileAtLineCmd(t *testing.T) {
type scenario struct {
filename string
lineNumber int
osConfig config.OSConfig
expectedCmdStr string
expectedEditInTerminal bool
}
scenarios := []scenario{
{
filename: "test",
lineNumber: 42,
osConfig: config.OSConfig{},
expectedCmdStr: `vim +42 -- "test"`,
expectedEditInTerminal: true,
},
{
filename: "test",
lineNumber: 35,
osConfig: config.OSConfig{
EditAtLine: "nano +{{line}} {{filename}}",
},
expectedCmdStr: `nano +35 "test"`,
expectedEditInTerminal: true,
},
{
filename: "file/with space",
lineNumber: 12,
osConfig: config.OSConfig{
EditPreset: "sublime",
},
expectedCmdStr: `subl -- "file/with space":12`,
expectedEditInTerminal: false,
},
}
for _, s := range scenarios {
userConfig := config.GetDefaultConfig()
userConfig.OS = s.osConfig
instance := buildFileCommands(commonDeps{
userConfig: userConfig,
})
cmdStr, editInTerminal := instance.GetEditAtLineCmdStr(s.filename, s.lineNumber)
assert.Equal(t, s.expectedCmdStr, cmdStr)
assert.Equal(t, s.expectedEditInTerminal, editInTerminal)
}
}
func TestEditFileAtLineAndWaitCmd(t *testing.T) {
type scenario struct {
filename string
lineNumber int
osConfig config.OSConfig
expectedCmdStr string
}
scenarios := []scenario{
{
filename: "test",
lineNumber: 42,
osConfig: config.OSConfig{},
expectedCmdStr: `vim +42 -- "test"`,
},
{
filename: "file/with space",
lineNumber: 12,
osConfig: config.OSConfig{
EditPreset: "sublime",
},
expectedCmdStr: `subl --wait -- "file/with space":12`,
},
}
for _, s := range scenarios {
userConfig := config.GetDefaultConfig()
userConfig.OS = s.osConfig
instance := buildFileCommands(commonDeps{
userConfig: userConfig,
})
cmdStr := instance.GetEditAtLineAndWaitCmdStr(s.filename, s.lineNumber)
assert.Equal(t, s.expectedCmdStr, cmdStr)
}
}
func TestGuessDefaultEditor(t *testing.T) {
type scenario struct {
gitConfigMockResponses map[string]string
getenv func(string) string
expectedResult string
}
scenarios := []scenario{
{
gitConfigMockResponses: nil,
getenv: func(env string) string {
return ""
},
expectedResult: "",
},
{
gitConfigMockResponses: map[string]string{"core.editor": "nano"},
getenv: func(env string) string {
return ""
},
expectedResult: "nano",
},
{
gitConfigMockResponses: map[string]string{"core.editor": "code -w"},
getenv: func(env string) string {
return ""
},
expectedResult: "code",
},
{
gitConfigMockResponses: nil,
getenv: func(env string) string {
if env == "VISUAL" {
return "emacs"
}
return ""
},
expectedResult: "emacs",
},
{
gitConfigMockResponses: nil,
getenv: func(env string) string {
if env == "EDITOR" {
return "bbedit -w"
}
return ""
},
expectedResult: "bbedit",
},
}
for _, s := range scenarios {
instance := buildFileCommands(commonDeps{
gitConfig: git_config.NewFakeGitConfig(s.gitConfigMockResponses),
getenv: s.getenv,
})
assert.Equal(t, s.expectedResult, instance.guessDefaultEditor())
}
}
+66 -51
View File
@@ -3,7 +3,9 @@ package git_commands
import (
"fmt"
"github.com/fsmiamoto/git-todo-parser/todo"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/app/daemon"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
@@ -16,7 +18,7 @@ type PatchCommands struct {
status *StatusCommands
stash *StashCommands
PatchManager *patch.PatchManager
PatchBuilder *patch.PatchBuilder
}
func NewPatchCommands(
@@ -25,7 +27,7 @@ func NewPatchCommands(
commit *CommitCommands,
status *StatusCommands,
stash *StashCommands,
patchManager *patch.PatchManager,
patchBuilder *patch.PatchBuilder,
) *PatchCommands {
return &PatchCommands{
GitCommon: gitCommon,
@@ -33,7 +35,7 @@ func NewPatchCommands(
commit: commit,
status: status,
stash: stash,
PatchManager: patchManager,
PatchBuilder: patchBuilder,
}
}
@@ -44,10 +46,8 @@ func (self *PatchCommands) DeletePatchesFromCommit(commits []*models.Commit, com
}
// apply each patch in reverse
if err := self.PatchManager.ApplyPatches(true); err != nil {
if err := self.rebase.AbortRebase(); err != nil {
return err
}
if err := self.PatchBuilder.ApplyPatches(true); err != nil {
_ = self.rebase.AbortRebase()
return err
}
@@ -57,7 +57,7 @@ func (self *PatchCommands) DeletePatchesFromCommit(commits []*models.Commit, com
}
self.rebase.onSuccessfulContinue = func() error {
self.PatchManager.Reset()
self.PatchBuilder.Reset()
return nil
}
@@ -72,10 +72,9 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
}
// apply each patch forward
if err := self.PatchManager.ApplyPatches(false); err != nil {
if err := self.rebase.AbortRebase(); err != nil {
return err
}
if err := self.PatchBuilder.ApplyPatches(false); err != nil {
// Don't abort the rebase here; this might cause conflicts, so give
// the user a chance to resolve them
return err
}
@@ -85,7 +84,7 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
}
self.rebase.onSuccessfulContinue = func() error {
self.PatchManager.Reset()
self.PatchBuilder.Reset()
return nil
}
@@ -106,24 +105,24 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
baseIndex := sourceCommitIdx + 1
todoLines := self.rebase.BuildTodoLines(commits[0:baseIndex], func(commit *models.Commit, i int) string {
if i == sourceCommitIdx || i == destinationCommitIdx {
return "edit"
} else {
return "pick"
}
})
changes := []daemon.ChangeTodoAction{
{Sha: commits[sourceCommitIdx].Sha, NewAction: todo.Edit},
{Sha: commits[destinationCommitIdx].Sha, NewAction: todo.Edit},
}
self.os.LogCommand(logTodoChanges(changes), false)
err := self.rebase.PrepareInteractiveRebaseCommand(commits[baseIndex].Sha, todoLines, true).Run()
err := self.rebase.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: commits[baseIndex].Sha,
overrideEditor: true,
instruction: daemon.NewChangeTodoActionsInstruction(changes),
}).Run()
if err != nil {
return err
}
// apply each patch in reverse
if err := self.PatchManager.ApplyPatches(true); err != nil {
if err := self.rebase.AbortRebase(); err != nil {
return err
}
if err := self.PatchBuilder.ApplyPatches(true); err != nil {
_ = self.rebase.AbortRebase()
return err
}
@@ -132,6 +131,12 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
return err
}
patch, err := self.diffHeadAgainstCommit(commits[sourceCommitIdx])
if err != nil {
_ = self.rebase.AbortRebase()
return err
}
if self.rebase.onSuccessfulContinue != nil {
return errors.New("You are midway through another rebase operation. Please abort to start again")
}
@@ -139,10 +144,9 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
self.rebase.onSuccessfulContinue = func() error {
// now we should be up to the destination, so let's apply forward these patches to that.
// ideally we would ensure we're on the right commit but I'm not sure if that check is necessary
if err := self.PatchManager.ApplyPatches(false); err != nil {
if err := self.rebase.AbortRebase(); err != nil {
return err
}
if err := self.rebase.workingTree.ApplyPatch(patch, "index", "3way"); err != nil {
// Don't abort the rebase here; this might cause conflicts, so give
// the user a chance to resolve them
return err
}
@@ -152,7 +156,7 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
}
self.rebase.onSuccessfulContinue = func() error {
self.PatchManager.Reset()
self.PatchBuilder.Reset()
return nil
}
@@ -173,11 +177,9 @@ func (self *PatchCommands) MovePatchIntoIndex(commits []*models.Commit, commitId
return err
}
if err := self.PatchManager.ApplyPatches(true); err != nil {
if err := self.PatchBuilder.ApplyPatches(true); err != nil {
if self.status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
if err := self.rebase.AbortRebase(); err != nil {
return err
}
_ = self.rebase.AbortRebase()
}
return err
}
@@ -187,17 +189,21 @@ func (self *PatchCommands) MovePatchIntoIndex(commits []*models.Commit, commitId
return err
}
patch, err := self.diffHeadAgainstCommit(commits[commitIdx])
if err != nil {
_ = self.rebase.AbortRebase()
return err
}
if self.rebase.onSuccessfulContinue != nil {
return errors.New("You are midway through another rebase operation. Please abort to start again")
}
self.rebase.onSuccessfulContinue = func() error {
// add patches to index
if err := self.PatchManager.ApplyPatches(false); err != nil {
if err := self.rebase.workingTree.ApplyPatch(patch, "index", "3way"); err != nil {
if self.status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
if err := self.rebase.AbortRebase(); err != nil {
return err
}
_ = self.rebase.AbortRebase()
}
return err
}
@@ -208,7 +214,7 @@ func (self *PatchCommands) MovePatchIntoIndex(commits []*models.Commit, commitId
}
}
self.PatchManager.Reset()
self.PatchBuilder.Reset()
return nil
}
@@ -220,10 +226,8 @@ func (self *PatchCommands) PullPatchIntoNewCommit(commits []*models.Commit, comm
return err
}
if err := self.PatchManager.ApplyPatches(true); err != nil {
if err := self.rebase.AbortRebase(); err != nil {
return err
}
if err := self.PatchBuilder.ApplyPatches(true); err != nil {
_ = self.rebase.AbortRebase()
return err
}
@@ -232,18 +236,20 @@ func (self *PatchCommands) PullPatchIntoNewCommit(commits []*models.Commit, comm
return err
}
// add patches to index
if err := self.PatchManager.ApplyPatches(false); err != nil {
if err := self.rebase.AbortRebase(); err != nil {
return err
}
patch, err := self.diffHeadAgainstCommit(commits[commitIdx])
if err != nil {
_ = self.rebase.AbortRebase()
return err
}
if err := self.rebase.workingTree.ApplyPatch(patch, "index", "3way"); err != nil {
_ = self.rebase.AbortRebase()
return err
}
head_message, _ := self.commit.GetHeadCommitMessage()
new_message := fmt.Sprintf("Split from \"%s\"", head_message)
err := self.commit.CommitCmdObj(new_message).Run()
if err != nil {
if err := self.commit.CommitCmdObj(new_message).Run(); err != nil {
return err
}
@@ -251,6 +257,15 @@ func (self *PatchCommands) PullPatchIntoNewCommit(commits []*models.Commit, comm
return errors.New("You are midway through another rebase operation. Please abort to start again")
}
self.PatchManager.Reset()
self.PatchBuilder.Reset()
return self.rebase.ContinueRebase()
}
// We have just applied a patch in reverse to discard it from a commit; if we
// now try to apply the patch again to move it to a later commit, or to the
// index, then this would conflict "with itself" in case the patch contained
// only some lines of a range of adjacent added lines. To solve this, we
// get the diff of HEAD and the original commit and then apply that.
func (self *PatchCommands) diffHeadAgainstCommit(commit *models.Commit) (string, error) {
return self.cmd.New(fmt.Sprintf("git diff HEAD..%s", commit.Sha)).RunWithOutput()
}
+149 -144
View File
@@ -2,15 +2,17 @@ package git_commands
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/fsmiamoto/git-todo-parser/todo"
"github.com/go-errors/errors"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/app/daemon"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
type RebaseCommands struct {
@@ -34,7 +36,7 @@ func NewRebaseCommands(
}
func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int, message string) error {
if index == 0 {
if models.IsHeadCommit(commits, index) {
// we've selected the top commit so no rebase is required
return self.commit.RewordLastCommit(message)
}
@@ -54,12 +56,16 @@ func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int, me
}
func (self *RebaseCommands) RewordCommitInEditor(commits []*models.Commit, index int) (oscommands.ICmdObj, error) {
todo, sha, err := self.BuildSingleActionTodo(commits, index, "reword")
if err != nil {
return nil, err
}
changes := []daemon.ChangeTodoAction{{
Sha: commits[index].Sha,
NewAction: todo.Reword,
}}
self.os.LogCommand(logTodoChanges(changes), false)
return self.PrepareInteractiveRebaseCommand(sha, todo, false), nil
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: getBaseShaOrRoot(commits, index+1),
instruction: daemon.NewChangeTodoActionsInstruction(changes),
}), nil
}
func (self *RebaseCommands) ResetCommitAuthor(commits []*models.Commit, index int) error {
@@ -75,7 +81,7 @@ func (self *RebaseCommands) SetCommitAuthor(commits []*models.Commit, index int,
}
func (self *RebaseCommands) GenericAmend(commits []*models.Commit, index int, f func() error) error {
if index == 0 {
if models.IsHeadCommit(commits, index) {
// we've selected the top commit so no rebase is required
return f()
}
@@ -95,33 +101,79 @@ func (self *RebaseCommands) GenericAmend(commits []*models.Commit, index int, f
}
func (self *RebaseCommands) MoveCommitDown(commits []*models.Commit, index int) error {
// we must ensure that we have at least two commits after the selected one
if len(commits) <= index+2 {
// assuming they aren't picking the bottom commit
return errors.New(self.Tr.NoRoom)
}
baseShaOrRoot := getBaseShaOrRoot(commits, index+2)
orderedCommits := append(commits[0:index], commits[index+1], commits[index])
sha := commits[index].Sha
todoLines := self.BuildTodoLinesSingleAction(orderedCommits, "pick")
self.os.LogCommand(fmt.Sprintf("Moving TODO down: %s", utils.ShortSha(sha)), false)
return self.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todoLines, true).Run()
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: baseShaOrRoot,
instruction: daemon.NewMoveTodoDownInstruction(sha),
overrideEditor: true,
}).Run()
}
func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, index int, action string) error {
todo, sha, err := self.BuildSingleActionTodo(commits, index, action)
if err != nil {
return err
func (self *RebaseCommands) MoveCommitUp(commits []*models.Commit, index int) error {
baseShaOrRoot := getBaseShaOrRoot(commits, index+1)
sha := commits[index].Sha
self.os.LogCommand(fmt.Sprintf("Moving TODO up: %s", utils.ShortSha(sha)), false)
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: baseShaOrRoot,
instruction: daemon.NewMoveTodoUpInstruction(sha),
overrideEditor: true,
}).Run()
}
func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, index int, action todo.TodoCommand) error {
baseIndex := index + 1
if action == todo.Squash || action == todo.Fixup {
baseIndex++
}
return self.PrepareInteractiveRebaseCommand(sha, todo, true).Run()
baseShaOrRoot := getBaseShaOrRoot(commits, baseIndex)
changes := []daemon.ChangeTodoAction{{
Sha: commits[index].Sha,
NewAction: action,
}}
self.os.LogCommand(logTodoChanges(changes), false)
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: baseShaOrRoot,
overrideEditor: true,
instruction: daemon.NewChangeTodoActionsInstruction(changes),
}).Run()
}
func (self *RebaseCommands) EditRebase(branchRef string) error {
self.os.LogCommand(fmt.Sprintf("Beginning interactive rebase at '%s'", branchRef), false)
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: branchRef,
instruction: daemon.NewInsertBreakInstruction(),
}).Run()
}
func logTodoChanges(changes []daemon.ChangeTodoAction) string {
changeTodoStr := strings.Join(slices.Map(changes, func(c daemon.ChangeTodoAction) string {
return fmt.Sprintf("%s:%s", c.Sha, c.NewAction)
}), "\n")
return fmt.Sprintf("Changing TODO actions: %s", changeTodoStr)
}
type PrepareInteractiveRebaseCommandOpts struct {
baseShaOrRoot string
instruction daemon.Instruction
overrideEditor bool
}
// PrepareInteractiveRebaseCommand returns the cmd for an interactive rebase
// we tell git to run lazygit to edit the todo list, and we pass the client
// lazygit a todo string to write to the todo file
func (self *RebaseCommands) PrepareInteractiveRebaseCommand(baseSha string, todoLines []TodoLine, overrideEditor bool) oscommands.ICmdObj {
todo := self.buildTodo(todoLines)
func (self *RebaseCommands) PrepareInteractiveRebaseCommand(opts PrepareInteractiveRebaseCommandOpts) oscommands.ICmdObj {
ex := oscommands.GetLazygitPath()
debug := "FALSE"
@@ -129,134 +181,89 @@ func (self *RebaseCommands) PrepareInteractiveRebaseCommand(baseSha string, todo
debug = "TRUE"
}
cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty %s", baseSha)
rebaseMergesArg := " --rebase-merges"
if self.version.IsOlderThan(2, 22, 0) {
rebaseMergesArg = ""
}
cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty --empty=keep --no-autosquash%s %s",
rebaseMergesArg, opts.baseShaOrRoot)
self.Log.WithField("command", cmdStr).Debug("RunCommand")
cmdObj := self.cmd.New(cmdStr)
gitSequenceEditor := ex
if todo == "" {
gitSequenceEditor = "true"
if opts.instruction != nil {
cmdObj.AddEnvVars(daemon.ToEnvVars(opts.instruction)...)
} else {
self.os.LogCommand(fmt.Sprintf("Creating TODO file for interactive rebase: \n\n%s", todo), false)
gitSequenceEditor = "true"
}
cmdObj.AddEnvVars(
daemon.DaemonKindEnvKey+"="+string(daemon.InteractiveRebase),
daemon.RebaseTODOEnvKey+"="+todo,
"DEBUG="+debug,
"LANG=en_US.UTF-8", // Force using EN as language
"LC_ALL=en_US.UTF-8", // Force using EN as language
"GIT_SEQUENCE_EDITOR="+gitSequenceEditor,
)
if overrideEditor {
if opts.overrideEditor {
cmdObj.AddEnvVars("GIT_EDITOR=" + ex)
}
return cmdObj
}
// produces TodoLines where every commit is picked (or dropped for merge commits) except for the commit at the given index, which
// will have the given action applied to it.
func (self *RebaseCommands) BuildSingleActionTodo(commits []*models.Commit, actionIndex int, action string) ([]TodoLine, string, error) {
baseIndex := actionIndex + 1
if len(commits) <= baseIndex {
return nil, "", errors.New(self.Tr.CannotRebaseOntoFirstCommit)
}
if action == "squash" || action == "fixup" {
baseIndex++
if len(commits) <= baseIndex {
return nil, "", errors.New(self.Tr.CannotSquashOntoSecondCommit)
}
}
todoLines := self.BuildTodoLines(commits[0:baseIndex], func(commit *models.Commit, i int) string {
if i == actionIndex {
return action
} else if commit.IsMerge() {
// your typical interactive rebase will actually drop merge commits by default. Damn git CLI, you scary!
// doing this means we don't need to worry about rebasing over merges which always causes problems.
// you typically shouldn't be doing rebases that pass over merge commits anyway.
return "drop"
} else {
return "pick"
}
})
return todoLines, commits[baseIndex].Sha, nil
}
// AmendTo amends the given commit with whatever files are staged
func (self *RebaseCommands) AmendTo(sha string) error {
if err := self.commit.CreateFixupCommit(sha); err != nil {
func (self *RebaseCommands) AmendTo(commits []*models.Commit, commitIndex int) error {
commit := commits[commitIndex]
if err := self.commit.CreateFixupCommit(commit.Sha); err != nil {
return err
}
return self.SquashAllAboveFixupCommits(sha)
}
// EditRebaseTodo sets the action at a given index in the git-rebase-todo file
func (self *RebaseCommands) EditRebaseTodo(index int, action string) error {
fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo")
bytes, err := os.ReadFile(fileName)
// Get the sha of the commit we just created
fixupSha, err := self.cmd.New("git rev-parse --verify HEAD").RunWithOutput()
if err != nil {
return err
}
content := strings.Split(string(bytes), "\n")
commitCount := self.getTodoCommitCount(content)
// we have the most recent commit at the bottom whereas the todo file has
// it at the bottom, so we need to subtract our index from the commit count
contentIndex := commitCount - 1 - index
splitLine := strings.Split(content[contentIndex], " ")
content[contentIndex] = action + " " + strings.Join(splitLine[1:], " ")
result := strings.Join(content, "\n")
return os.WriteFile(fileName, []byte(result), 0o644)
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: getBaseShaOrRoot(commits, commitIndex+1),
overrideEditor: true,
instruction: daemon.NewMoveFixupCommitDownInstruction(commit.Sha, fixupSha),
}).Run()
}
func (self *RebaseCommands) getTodoCommitCount(content []string) int {
// count lines that are not blank and are not comments
commitCount := 0
for _, line := range content {
if line != "" && !strings.HasPrefix(line, "#") {
commitCount++
}
}
return commitCount
// EditRebaseTodo sets the action for a given rebase commit in the git-rebase-todo file
func (self *RebaseCommands) EditRebaseTodo(commit *models.Commit, action todo.TodoCommand) error {
return utils.EditRebaseTodo(
filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo"), commit.Sha, commit.Action, action)
}
// MoveTodoDown moves a rebase todo item down by one position
func (self *RebaseCommands) MoveTodoDown(index int) error {
func (self *RebaseCommands) MoveTodoDown(commit *models.Commit) error {
fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo")
bytes, err := os.ReadFile(fileName)
if err != nil {
return err
}
return utils.MoveTodoDown(fileName, commit.Sha, commit.Action)
}
content := strings.Split(string(bytes), "\n")
commitCount := self.getTodoCommitCount(content)
contentIndex := commitCount - 1 - index
rearrangedContent := append(content[0:contentIndex-1], content[contentIndex], content[contentIndex-1])
rearrangedContent = append(rearrangedContent, content[contentIndex+1:]...)
result := strings.Join(rearrangedContent, "\n")
return os.WriteFile(fileName, []byte(result), 0o644)
// MoveTodoDown moves a rebase todo item down by one position
func (self *RebaseCommands) MoveTodoUp(commit *models.Commit) error {
fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo")
return utils.MoveTodoUp(fileName, commit.Sha, commit.Action)
}
// SquashAllAboveFixupCommits squashes all fixup! commits above the given one
func (self *RebaseCommands) SquashAllAboveFixupCommits(sha string) error {
func (self *RebaseCommands) SquashAllAboveFixupCommits(commit *models.Commit) error {
shaOrRoot := commit.Sha + "^"
if commit.IsFirstCommit() {
shaOrRoot = "--root"
}
return self.runSkipEditorCommand(
self.cmd.New(
fmt.Sprintf(
"git rebase --interactive --rebase-merges --autostash --autosquash %s^",
sha,
"git rebase --interactive --rebase-merges --autostash --autosquash %s",
shaOrRoot,
),
),
)
@@ -276,17 +283,22 @@ func (self *RebaseCommands) BeginInteractiveRebaseForCommit(commits []*models.Co
return errors.New(self.Tr.DisabledForGPG)
}
todo, sha, err := self.BuildSingleActionTodo(commits, commitIndex, "edit")
if err != nil {
return err
}
changes := []daemon.ChangeTodoAction{{
Sha: commits[commitIndex].Sha,
NewAction: todo.Edit,
}}
self.os.LogCommand(logTodoChanges(changes), false)
return self.PrepareInteractiveRebaseCommand(sha, todo, true).Run()
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: getBaseShaOrRoot(commits, commitIndex+1),
overrideEditor: true,
instruction: daemon.NewChangeTodoActionsInstruction(changes),
}).Run()
}
// RebaseBranch interactive rebases onto a branch
func (self *RebaseCommands) RebaseBranch(branchName string) error {
return self.PrepareInteractiveRebaseCommand(branchName, nil, false).Run()
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{baseShaOrRoot: branchName}).Run()
}
func (self *RebaseCommands) GenericMergeOrRebaseActionCmdObj(commandType string, command string) oscommands.ICmdObj {
@@ -327,14 +339,16 @@ func (self *RebaseCommands) GenericMergeOrRebaseAction(commandType string, comma
}
func (self *RebaseCommands) runSkipEditorCommand(cmdObj oscommands.ICmdObj) error {
instruction := daemon.NewExitImmediatelyInstruction()
lazyGitPath := oscommands.GetLazygitPath()
return cmdObj.
AddEnvVars(
daemon.DaemonKindEnvKey+"="+string(daemon.ExitImmediately),
"GIT_EDITOR="+lazyGitPath,
"GIT_SEQUENCE_EDITOR="+lazyGitPath,
"EDITOR="+lazyGitPath,
"VISUAL="+lazyGitPath,
).
AddEnvVars(daemon.ToEnvVars(instruction)...).
Run()
}
@@ -368,36 +382,27 @@ func (self *RebaseCommands) DiscardOldFileChanges(commits []*models.Commit, comm
// CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD
func (self *RebaseCommands) CherryPickCommits(commits []*models.Commit) error {
todoLines := self.BuildTodoLinesSingleAction(commits, "pick")
return self.PrepareInteractiveRebaseCommand("HEAD", todoLines, false).Run()
}
func (self *RebaseCommands) buildTodo(todoLines []TodoLine) string {
lines := slices.Map(todoLines, func(todoLine TodoLine) string {
return todoLine.ToString()
commitLines := lo.Map(commits, func(commit *models.Commit, _ int) string {
return fmt.Sprintf("%s %s", utils.ShortSha(commit.Sha), commit.Name)
})
self.os.LogCommand(fmt.Sprintf("Cherry-picking commits:\n%s", strings.Join(commitLines, "\n")), false)
return strings.Join(slices.Reverse(lines), "")
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: "HEAD",
instruction: daemon.NewCherryPickCommitsInstruction(commits),
}).Run()
}
func (self *RebaseCommands) BuildTodoLines(commits []*models.Commit, f func(*models.Commit, int) string) []TodoLine {
return slices.MapWithIndex(commits, func(commit *models.Commit, i int) TodoLine {
return TodoLine{Action: f(commit, i), Commit: commit}
})
}
func (self *RebaseCommands) BuildTodoLinesSingleAction(commits []*models.Commit, action string) []TodoLine {
return self.BuildTodoLines(commits, func(commit *models.Commit, i int) string {
return action
})
}
type TodoLine struct {
Action string
Commit *models.Commit
}
func (self *TodoLine) ToString() string {
return self.Action + " " + self.Commit.Sha + " " + self.Commit.Name + "\n"
// we can't start an interactive rebase from the first commit without passing the
// '--root' arg
func getBaseShaOrRoot(commits []*models.Commit, index int) string {
// We assume that the commits slice contains the initial commit of the repo.
// Technically this assumption could prove false, but it's unlikely you'll
// 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].Sha
} else {
return "--root"
}
}
+6 -4
View File
@@ -2,6 +2,7 @@ package git_commands
import (
"regexp"
"strconv"
"testing"
"github.com/go-errors/errors"
@@ -26,7 +27,7 @@ func TestRebaseRebaseBranch(t *testing.T) {
testName: "successful rebase",
arg: "master",
runner: oscommands.NewFakeRunner(t).
Expect(`git rebase --interactive --autostash --keep-empty master`, "", nil),
Expect(`git rebase --interactive --autostash --keep-empty --empty=keep --no-autosquash master`, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
@@ -35,7 +36,7 @@ func TestRebaseRebaseBranch(t *testing.T) {
testName: "unsuccessful rebase",
arg: "master",
runner: oscommands.NewFakeRunner(t).
Expect(`git rebase --interactive --autostash --keep-empty master`, "", errors.New("error")),
Expect(`git rebase --interactive --autostash --keep-empty --empty=keep --no-autosquash master`, "", errors.New("error")),
test: func(err error) {
assert.Error(t, err)
},
@@ -62,7 +63,8 @@ func TestRebaseSkipEditorCommand(t *testing.T) {
`^VISUAL=.*$`,
`^EDITOR=.*$`,
`^GIT_EDITOR=.*$`,
"^" + daemon.DaemonKindEnvKey + "=" + string(daemon.ExitImmediately) + "$",
`^GIT_SEQUENCE_EDITOR=.*$`,
"^" + daemon.DaemonKindEnvKey + "=" + strconv.Itoa(int(daemon.DaemonKindExitImmediately)) + "$",
} {
regexStr := regexStr
foundMatch := lo.ContainsBy(envVars, func(envVar string) bool {
@@ -124,7 +126,7 @@ func TestRebaseDiscardOldFileChanges(t *testing.T) {
commitIndex: 0,
fileName: "test999.txt",
runner: oscommands.NewFakeRunner(t).
Expect(`git rebase --interactive --autostash --keep-empty abcdef`, "", nil).
Expect(`git rebase --interactive --autostash --keep-empty --empty=keep --no-autosquash abcdef`, "", nil).
Expect(`git cat-file -e HEAD^:"test999.txt"`, "", nil).
Expect(`git checkout HEAD^ -- "test999.txt"`, "", nil).
Expect(`git commit --amend --no-edit --allow-empty`, "", nil).
@@ -52,7 +52,7 @@ func (self *ReflogCommitLoader) GetReflogCommits(lastReflogCommit *models.Commit
Sha: fields[0],
Name: fields[2],
UnixTimestamp: int64(unixTimestamp),
Status: "reflog",
Status: models.StatusReflog,
Parents: parents,
}
@@ -51,35 +51,35 @@ func TestGetReflogCommits(t *testing.T) {
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
Status: "reflog",
Status: models.StatusReflog,
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from B to A",
Status: "reflog",
Status: models.StatusReflog,
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
Status: "reflog",
Status: models.StatusReflog,
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from master to A",
Status: "reflog",
Status: models.StatusReflog,
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
{
Sha: "f4ddf2f0d4be4ccc7efa",
Name: "checkout: moving from A to master",
Status: "reflog",
Status: models.StatusReflog,
UnixTimestamp: 1643149435,
Parents: []string{"51baa8c1"},
},
@@ -95,7 +95,7 @@ func TestGetReflogCommits(t *testing.T) {
lastReflogCommit: &models.Commit{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from B to A",
Status: "reflog",
Status: models.StatusReflog,
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
@@ -103,7 +103,7 @@ func TestGetReflogCommits(t *testing.T) {
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
Status: "reflog",
Status: models.StatusReflog,
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
@@ -119,7 +119,7 @@ func TestGetReflogCommits(t *testing.T) {
lastReflogCommit: &models.Commit{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from B to A",
Status: "reflog",
Status: models.StatusReflog,
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
@@ -128,7 +128,7 @@ func TestGetReflogCommits(t *testing.T) {
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
Status: "reflog",
Status: models.StatusReflog,
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
+1 -5
View File
@@ -29,11 +29,7 @@ func (self *SyncCommands) PushCmdObj(opts PushOpts) (oscommands.ICmdObj, error)
cmdStr := "git push"
if opts.Force {
if self.version.IsOlderThan(2, 30, 0) {
cmdStr += " --force-with-lease"
} else {
cmdStr += " --force-with-lease --force-if-includes"
}
cmdStr += " --force-with-lease"
}
if opts.SetUpstream {
+1 -31
View File
@@ -10,7 +10,6 @@ import (
func TestSyncPush(t *testing.T) {
type scenario struct {
testName string
version *GitVersion
opts PushOpts
test func(oscommands.ICmdObj, error)
}
@@ -18,7 +17,6 @@ func TestSyncPush(t *testing.T) {
scenarios := []scenario{
{
testName: "Push with force disabled",
version: &GitVersion{2, 29, 3, ""},
opts: PushOpts{Force: false},
test: func(cmdObj oscommands.ICmdObj, err error) {
assert.Equal(t, cmdObj.ToString(), "git push")
@@ -27,25 +25,14 @@ func TestSyncPush(t *testing.T) {
},
{
testName: "Push with force enabled",
version: &GitVersion{2, 29, 3, ""},
opts: PushOpts{Force: true},
test: func(cmdObj oscommands.ICmdObj, err error) {
assert.Equal(t, cmdObj.ToString(), "git push --force-with-lease")
assert.NoError(t, err)
},
},
{
testName: "Push with force enabled (>= 2.30.0)",
version: &GitVersion{2, 30, 0, ""},
opts: PushOpts{Force: true},
test: func(cmdObj oscommands.ICmdObj, err error) {
assert.Equal(t, cmdObj.ToString(), "git push --force-with-lease --force-if-includes")
assert.NoError(t, err)
},
},
{
testName: "Push with force disabled, upstream supplied",
version: &GitVersion{2, 29, 3, ""},
opts: PushOpts{
Force: false,
UpstreamRemote: "origin",
@@ -58,7 +45,6 @@ func TestSyncPush(t *testing.T) {
},
{
testName: "Push with force disabled, setting upstream",
version: &GitVersion{2, 29, 3, ""},
opts: PushOpts{
Force: false,
UpstreamRemote: "origin",
@@ -72,7 +58,6 @@ func TestSyncPush(t *testing.T) {
},
{
testName: "Push with force enabled, setting upstream",
version: &GitVersion{2, 29, 3, ""},
opts: PushOpts{
Force: true,
UpstreamRemote: "origin",
@@ -84,23 +69,8 @@ func TestSyncPush(t *testing.T) {
assert.NoError(t, err)
},
},
{
testName: "Push with force enabled, setting upstream (>= 2.30.0)",
version: &GitVersion{2, 30, 0, ""},
opts: PushOpts{
Force: true,
UpstreamRemote: "origin",
UpstreamBranch: "master",
SetUpstream: true,
},
test: func(cmdObj oscommands.ICmdObj, err error) {
assert.Equal(t, cmdObj.ToString(), `git push --force-with-lease --force-if-includes --set-upstream "origin" "master"`)
assert.NoError(t, err)
},
},
{
testName: "Push with remote branch but no origin",
version: &GitVersion{2, 29, 3, ""},
opts: PushOpts{
Force: true,
UpstreamRemote: "",
@@ -117,7 +87,7 @@ func TestSyncPush(t *testing.T) {
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildSyncCommands(commonDeps{gitVersion: s.version})
instance := buildSyncCommands(commonDeps{})
s.test(instance.PushCmdObj(s.opts))
})
}
+13 -5
View File
@@ -14,12 +14,20 @@ func NewTagCommands(gitCommon *GitCommon) *TagCommands {
}
}
func (self *TagCommands) CreateLightweight(tagName string, commitSha string) error {
return self.cmd.New(fmt.Sprintf("git tag -- %s %s", self.cmd.Quote(tagName), commitSha)).Run()
func (self *TagCommands) CreateLightweight(tagName string, ref string) error {
if len(ref) > 0 {
return self.cmd.New(fmt.Sprintf("git tag -- %s %s", self.cmd.Quote(tagName), self.cmd.Quote(ref))).Run()
} else {
return self.cmd.New(fmt.Sprintf("git tag -- %s", self.cmd.Quote(tagName))).Run()
}
}
func (self *TagCommands) CreateAnnotated(tagName, commitSha, msg string) error {
return self.cmd.New(fmt.Sprintf("git tag %s %s -m %s", tagName, commitSha, self.cmd.Quote(msg))).Run()
func (self *TagCommands) CreateAnnotated(tagName, ref, msg string) error {
if len(ref) > 0 {
return self.cmd.New(fmt.Sprintf("git tag %s %s -m %s", self.cmd.Quote(tagName), self.cmd.Quote(ref), self.cmd.Quote(msg))).Run()
} else {
return self.cmd.New(fmt.Sprintf("git tag %s -m %s", self.cmd.Quote(tagName), self.cmd.Quote(msg))).Run()
}
}
func (self *TagCommands) Delete(tagName string) error {
@@ -27,5 +35,5 @@ func (self *TagCommands) Delete(tagName string) error {
}
func (self *TagCommands) Push(remoteName string, tagName string) error {
return self.cmd.New(fmt.Sprintf("git push %s %s", self.cmd.Quote(remoteName), self.cmd.Quote(tagName))).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
return self.cmd.New(fmt.Sprintf("git push %s tag %s", self.cmd.Quote(remoteName), self.cmd.Quote(tagName))).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
}
+17 -15
View File
@@ -1,7 +1,7 @@
package git_commands
import (
"fmt"
"regexp"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/models"
@@ -12,40 +12,42 @@ import (
type TagLoader struct {
*common.Common
version *GitVersion
cmd oscommands.ICmdObjBuilder
cmd oscommands.ICmdObjBuilder
}
func NewTagLoader(
common *common.Common,
version *GitVersion,
cmd oscommands.ICmdObjBuilder,
) *TagLoader {
return &TagLoader{
Common: common,
version: version,
cmd: cmd,
Common: common,
cmd: cmd,
}
}
func (self *TagLoader) GetTags() ([]*models.Tag, error) {
// get remote branches, sorted by creation date (descending)
// see: https://git-scm.com/docs/git-tag#Documentation/git-tag.txt---sortltkeygt
sortKey := "-creatordate"
if self.version.IsOlderThan(2, 7, 0) {
sortKey = "-v:refname"
}
tagsOutput, err := self.cmd.New(fmt.Sprintf(`git tag --list --sort=%s`, sortKey)).DontLog().RunWithOutput()
tagsOutput, err := self.cmd.New(`git tag --list -n --sort=-creatordate`).DontLog().RunWithOutput()
if err != nil {
return nil, err
}
split := utils.SplitLines(tagsOutput)
tags := slices.Map(split, func(tagName string) *models.Tag {
lineRegex := regexp.MustCompile(`^([^\s]+)(\s+)?(.*)$`)
tags := slices.Map(split, func(line string) *models.Tag {
matches := lineRegex.FindStringSubmatch(line)
tagName := matches[1]
message := ""
if len(matches) > 3 {
message = matches[3]
}
return &models.Tag{
Name: tagName,
Name: tagName,
Message: message,
}
})
+14 -47
View File
@@ -9,18 +9,14 @@ import (
"github.com/stretchr/testify/assert"
)
const tagsOutput = `v0.34
v0.33
v0.32.2
v0.32.1
v0.32
testtag
const tagsOutput = `tag1 this is my message
tag2
tag3 this is my other message
`
func TestGetTags(t *testing.T) {
type scenario struct {
testName string
gitVersion *GitVersion
runner *oscommands.FakeCmdObjRunner
expectedTags []*models.Tag
expectedError error
@@ -28,48 +24,20 @@ func TestGetTags(t *testing.T) {
scenarios := []scenario{
{
testName: "should return no tags if there are none",
gitVersion: &GitVersion{2, 7, 0, ""},
testName: "should return no tags if there are none",
runner: oscommands.NewFakeRunner(t).
Expect(`git tag --list --sort=-creatordate`, "", nil),
Expect(`git tag --list -n --sort=-creatordate`, "", nil),
expectedTags: []*models.Tag{},
expectedError: nil,
},
{
testName: "should return no tags if there are none (< 2.7.0)",
gitVersion: &GitVersion{2, 6, 7, ""},
testName: "should return tags if present",
runner: oscommands.NewFakeRunner(t).
Expect(`git tag --list --sort=-v:refname`, "", nil),
expectedTags: []*models.Tag{},
expectedError: nil,
},
{
testName: "should return tags if present",
gitVersion: &GitVersion{2, 7, 0, ""},
runner: oscommands.NewFakeRunner(t).
Expect(`git tag --list --sort=-creatordate`, tagsOutput, nil),
Expect(`git tag --list -n --sort=-creatordate`, tagsOutput, nil),
expectedTags: []*models.Tag{
{Name: "v0.34"},
{Name: "v0.33"},
{Name: "v0.32.2"},
{Name: "v0.32.1"},
{Name: "v0.32"},
{Name: "testtag"},
},
expectedError: nil,
},
{
testName: "should return tags if present (< 2.7.0)",
gitVersion: &GitVersion{2, 6, 7, ""},
runner: oscommands.NewFakeRunner(t).
Expect(`git tag --list --sort=-v:refname`, tagsOutput, nil),
expectedTags: []*models.Tag{
{Name: "v0.34"},
{Name: "v0.33"},
{Name: "v0.32.2"},
{Name: "v0.32.1"},
{Name: "v0.32"},
{Name: "testtag"},
{Name: "tag1", Message: "this is my message"},
{Name: "tag2", Message: ""},
{Name: "tag3", Message: "this is my other message"},
},
expectedError: nil,
},
@@ -78,11 +46,10 @@ func TestGetTags(t *testing.T) {
for _, scenario := range scenarios {
scenario := scenario
t.Run(scenario.testName, func(t *testing.T) {
loader := NewTagLoader(
utils.NewDummyCommon(),
scenario.gitVersion,
oscommands.NewDummyCmdObjBuilder(scenario.runner),
)
loader := &TagLoader{
Common: utils.NewDummyCommon(),
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
}
tags, err := loader.GetTags()
+5 -1
View File
@@ -32,7 +32,7 @@ func ParseGitVersion(versionStr string) (*GitVersion, error) {
// versionStr should be something like:
// git version 2.39.0
// git version 2.37.1 (Apple Git-137.1)
re := regexp.MustCompile(`[^\d]+(\d+)(\.\d+)?(\.\d+)?(.*)`)
re := regexp.MustCompile(`[^\d]*(\d+)(\.\d+)?(\.\d+)?(.*)`)
matches := re.FindStringSubmatch(versionStr)
if len(matches) < 5 {
@@ -65,3 +65,7 @@ func (v *GitVersion) IsOlderThan(major, minor, patch int) bool {
required := major*1000*1000 + minor*1000 + patch
return actual < required
}
func (v *GitVersion) IsOlderThanVersion(version *GitVersion) bool {
return v.IsOlderThan(version.Major, version.Minor, version.Patch)
}
+14 -5
View File
@@ -287,11 +287,15 @@ func (self *WorkingTreeCommands) SaveTemporaryPatch(patch string) (string, error
// ShowFileDiff get the diff of specified from and to. Typically this will be used for a single commit so it'll be 123abc^..123abc
// but when we're in diff mode it could be any 'from' to any 'to'. The reverse flag is also here thanks to diff mode.
func (self *WorkingTreeCommands) ShowFileDiff(from string, to string, reverse bool, fileName string, plain bool) (string, error) {
return self.ShowFileDiffCmdObj(from, to, reverse, fileName, plain).RunWithOutput()
func (self *WorkingTreeCommands) ShowFileDiff(from string, to string, reverse bool, fileName string, plain bool,
ignoreWhitespace bool,
) (string, error) {
return self.ShowFileDiffCmdObj(from, to, reverse, fileName, plain, ignoreWhitespace).RunWithOutput()
}
func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reverse bool, fileName string, plain bool) oscommands.ICmdObj {
func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reverse bool, fileName string, plain bool,
ignoreWhitespace bool,
) oscommands.ICmdObj {
colorArg := self.UserConfig.Git.Paging.ColorArg
contextSize := self.UserConfig.Git.DiffContextSize
if plain {
@@ -303,11 +307,16 @@ func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reve
reverseFlag = " -R"
}
ignoreWhitespaceFlag := ""
if ignoreWhitespace {
ignoreWhitespaceFlag = " --ignore-all-space"
}
return self.cmd.
New(
fmt.Sprintf(
"git diff --submodule --no-ext-diff --unified=%d --no-renames --color=%s%s%s%s -- %s",
contextSize, colorArg, pad(from), pad(to), reverseFlag, self.cmd.Quote(fileName)),
"git diff --submodule --no-ext-diff --unified=%d --no-renames --color=%s%s%s%s%s -- %s",
contextSize, colorArg, pad(from), pad(to), reverseFlag, ignoreWhitespaceFlag, self.cmd.Quote(fileName)),
).
DontLog()
}
+34 -20
View File
@@ -323,38 +323,52 @@ func TestWorkingTreeDiff(t *testing.T) {
func TestWorkingTreeShowFileDiff(t *testing.T) {
type scenario struct {
testName string
from string
to string
reverse bool
plain bool
contextSize int
runner *oscommands.FakeCmdObjRunner
testName string
from string
to string
reverse bool
plain bool
ignoreWhitespace bool
contextSize int
runner *oscommands.FakeCmdObjRunner
}
const expectedResult = "pretend this is an actual git diff"
scenarios := []scenario{
{
testName: "Default case",
from: "1234567890",
to: "0987654321",
reverse: false,
plain: false,
contextSize: 3,
testName: "Default case",
from: "1234567890",
to: "0987654321",
reverse: false,
plain: false,
ignoreWhitespace: false,
contextSize: 3,
runner: oscommands.NewFakeRunner(t).
Expect(`git diff --submodule --no-ext-diff --unified=3 --no-renames --color=always 1234567890 0987654321 -- "test.txt"`, expectedResult, nil),
},
{
testName: "Show diff with custom context size",
from: "1234567890",
to: "0987654321",
reverse: false,
plain: false,
contextSize: 123,
testName: "Show diff with custom context size",
from: "1234567890",
to: "0987654321",
reverse: false,
plain: false,
ignoreWhitespace: false,
contextSize: 123,
runner: oscommands.NewFakeRunner(t).
Expect(`git diff --submodule --no-ext-diff --unified=123 --no-renames --color=always 1234567890 0987654321 -- "test.txt"`, expectedResult, nil),
},
{
testName: "Default case (ignore whitespace)",
from: "1234567890",
to: "0987654321",
reverse: false,
plain: false,
ignoreWhitespace: true,
contextSize: 3,
runner: oscommands.NewFakeRunner(t).
Expect(`git diff --submodule --no-ext-diff --unified=3 --no-renames --color=always 1234567890 0987654321 --ignore-all-space -- "test.txt"`, expectedResult, nil),
},
}
for _, s := range scenarios {
@@ -365,7 +379,7 @@ func TestWorkingTreeShowFileDiff(t *testing.T) {
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner, userConfig: userConfig})
result, err := instance.ShowFileDiff(s.from, s.to, s.reverse, "test.txt", s.plain)
result, err := instance.ShowFileDiff(s.from, s.to, s.reverse, "test.txt", s.plain, s.ignoreWhitespace)
assert.NoError(t, err)
assert.Equal(t, expectedResult, result)
s.runner.CheckForMissingCalls()
+16 -1
View File
@@ -4,7 +4,7 @@ package hosting_service
// at regoio.herokuapp.com
var defaultUrlRegexStrings = []string{
`^(?:https?|ssh)://[^/]+/(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
`^git@.*:(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
`^.*?@.*:(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
}
var defaultRepoURLTemplate = "https://{{.webDomain}}/{{.owner}}/{{.repo}}"
@@ -64,12 +64,22 @@ var bitbucketServerServiceDef = ServiceDefinition{
repoURLTemplate: "https://{{.webDomain}}/projects/{{.project}}/repos/{{.repo}}",
}
var giteaServiceDef = ServiceDefinition{
provider: "gitea",
pullRequestURLIntoDefaultBranch: "/compare/{{.From}}",
pullRequestURLIntoTargetBranch: "/compare/{{.To}}...{{.From}}",
commitURL: "/commit/{{.CommitSha}}",
regexStrings: defaultUrlRegexStrings,
repoURLTemplate: defaultRepoURLTemplate,
}
var serviceDefinitions = []ServiceDefinition{
githubServiceDef,
bitbucketServiceDef,
gitLabServiceDef,
azdoServiceDef,
bitbucketServerServiceDef,
giteaServiceDef,
}
var defaultServiceDomains = []ServiceDomain{
@@ -93,4 +103,9 @@ var defaultServiceDomains = []ServiceDomain{
gitDomain: "dev.azure.com",
webDomain: "dev.azure.com",
},
{
serviceDefinition: giteaServiceDef,
gitDomain: "try.gitea.io",
webDomain: "try.gitea.io",
},
}
@@ -13,7 +13,7 @@ import (
"github.com/jesseduffield/generics/slices"
)
// This package is for handling logic specific to a git hosting service like github, gitlab, bitbucket, etc.
// This package is for handling logic specific to a git hosting service like github, gitlab, bitbucket, gitea, etc.
// Different git hosting services have different URL formats for when you want to open a PR or view a commit,
// and this package's responsibility is to determine which service you're using based on the remote URL,
// and then which URL you need for whatever use case you have.
@@ -3,8 +3,8 @@ package hosting_service
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/fakes"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/test"
"github.com/stretchr/testify/assert"
)
@@ -86,6 +86,16 @@ func TestGetPullRequestURL(t *testing.T) {
assert.Equal(t, "https://github.com/peter/calculator/compare/feature%2Foperations...feature%2Fsum-operation?expand=1", url)
},
},
{
testName: "Opens a link to new pull request on github with specific target branch (different git username)",
from: "feature/sum-operation",
to: "feature/operations",
remoteUrl: "ssh://org-12345@github.com:peter/calculator.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://github.com/peter/calculator/compare/feature%2Foperations...feature%2Fsum-operation?expand=1", url)
},
},
{
testName: "Opens a link to new pull request on github with https remote url with specific target branch",
from: "feature/sum-operation",
@@ -254,6 +264,60 @@ func TestGetPullRequestURL(t *testing.T) {
assert.Equal(t, "https://mycompany.bitbucket.com/projects/myproject/repos/myrepo/pull-requests?create&targetBranch=dev&sourceBranch=feature%2Fnew", url)
},
},
{
testName: "Opens a link to new pull request on Gitea Server (SSH)",
from: "feature/new",
remoteUrl: "ssh://git@mycompany.gitea.io/myproject/myrepo.git",
configServiceDomains: map[string]string{
// valid configuration for a gitea server URL
"mycompany.gitea.io": "gitea:mycompany.gitea.io",
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://mycompany.gitea.io/myproject/myrepo/compare/feature%2Fnew", url)
},
},
{
testName: "Opens a link to new pull request on Gitea Server (SSH) with specific target",
from: "feature/new",
to: "dev",
remoteUrl: "ssh://git@mycompany.gitea.io/myproject/myrepo.git",
configServiceDomains: map[string]string{
// valid configuration for a gitea server URL
"mycompany.gitea.io": "gitea:mycompany.gitea.io",
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://mycompany.gitea.io/myproject/myrepo/compare/dev...feature%2Fnew", url)
},
},
{
testName: "Opens a link to new pull request on Gitea Server (HTTP)",
from: "feature/new",
remoteUrl: "https://mycompany.gitea.io/myproject/myrepo.git",
configServiceDomains: map[string]string{
// valid configuration for a gitea server URL
"mycompany.gitea.io": "gitea:mycompany.gitea.io",
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://mycompany.gitea.io/myproject/myrepo/compare/feature%2Fnew", url)
},
},
{
testName: "Opens a link to new pull request on Gitea Server (HTTP) with specific target",
from: "feature/new",
to: "dev",
remoteUrl: "https://mycompany.gitea.io/myproject/myrepo.git",
configServiceDomains: map[string]string{
// valid configuration for a gitea server URL
"mycompany.gitea.io": "gitea:mycompany.gitea.io",
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://mycompany.gitea.io/myproject/myrepo/compare/dev...feature%2Fnew", url)
},
},
{
testName: "Throws an error if git service is unsupported",
from: "feature/divide-operation",
@@ -300,7 +364,7 @@ func TestGetPullRequestURL(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature%2Fprofile-page&t=1", url)
},
expectedLoggedErrors: []string{"Unknown git service type: 'noservice'. Expected one of github, bitbucket, gitlab, azuredevops, bitbucketServer"},
expectedLoggedErrors: []string{"Unknown git service type: 'noservice'. Expected one of github, bitbucket, gitlab, azuredevops, bitbucketServer, gitea"},
},
{
testName: "Escapes reserved URL characters in from branch name",
@@ -328,7 +392,7 @@ func TestGetPullRequestURL(t *testing.T) {
s := s
t.Run(s.testName, func(t *testing.T) {
tr := i18n.EnglishTranslationSet()
log := &test.FakeFieldLogger{}
log := &fakes.FakeFieldLogger{}
hostingServiceMgr := NewHostingServiceMgr(log, &tr, s.remoteUrl, s.configServiceDomains)
s.test(hostingServiceMgr.GetPullRequestURL(s.from, s.to))
log.AssertErrors(t, s.expectedLoggedErrors)
+26 -3
View File
@@ -3,18 +3,37 @@ package models
import (
"fmt"
"github.com/fsmiamoto/git-todo-parser/todo"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// Special commit hash for empty tree object
const EmptyTreeCommitHash = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
type CommitStatus int
const (
StatusNone CommitStatus = iota
StatusUnpushed
StatusPushed
StatusMerged
StatusRebasing
StatusSelected
StatusReflog
)
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
)
// Commit : A git commit
type Commit struct {
Sha string
Name string
Status string // one of "unpushed", "pushed", "merged", "rebasing" or "selected"
Action string // one of "", "pick", "edit", "squash", "reword", "drop", "fixup"
Status CommitStatus
Action todo.TodoCommand
Tags []string
ExtraInfo string // something like 'HEAD -> master, tag: v0.15.2'
AuthorName string // something like 'Jesse Duffield'
@@ -63,5 +82,9 @@ func (c *Commit) IsMerge() bool {
// returns true if this commit is not actually in the git log but instead
// is from a TODO file for an interactive rebase.
func (c *Commit) IsTODO() bool {
return c.Action != ""
return c.Action != ActionNone
}
func IsHeadCommit(commits []*Commit, index int) bool {
return !commits[index].IsTODO() && (index == 0 || commits[index-1].IsTODO())
}
+4 -1
View File
@@ -3,6 +3,9 @@ package models
// Tag : A git tag
type Tag struct {
Name string
// this is either the first line of the message of an annotated tag, or the
// first line of a commit message for a lightweight tag
Message string
}
func (t *Tag) FullRefName() string {
@@ -22,5 +25,5 @@ func (t *Tag) ID() string {
}
func (t *Tag) Description() string {
return "tag " + t.Name
return t.Message
}
+1 -1
View File
@@ -52,7 +52,7 @@ func (self *CmdObjBuilder) NewFromArgs(args []string) ICmdObj {
}
func (self *CmdObjBuilder) NewShell(commandStr string) ICmdObj {
quotedCommand := ""
var quotedCommand string
// Windows does not seem to like quotes around the command
if self.platform.OS == "windows" {
quotedCommand = strings.NewReplacer(
+24 -11
View File
@@ -323,6 +323,23 @@ func (self *cmdObjRunner) processOutput(reader io.Reader, writer io.Writer, prom
// having a function that returns a function because we need to maintain some state inbetween calls hence the closure
func (self *cmdObjRunner) getCheckForCredentialRequestFunc() func([]byte) (CredentialType, bool) {
var ttyText strings.Builder
prompts := map[string]CredentialType{
`Password:`: Password,
`.+'s password:`: Password,
`Password\s*for\s*'.+':`: Password,
`Username\s*for\s*'.+':`: Username,
`Enter\s*passphrase\s*for\s*key\s*'.+':`: Passphrase,
`Enter\s*PIN\s*for\s*.+\s*key\s*.+:`: PIN,
}
compiledPrompts := map[*regexp.Regexp]CredentialType{}
for pattern, askFor := range prompts {
compiledPattern := regexp.MustCompile(pattern)
compiledPrompts[compiledPattern] = askFor
}
newlineRegex := regexp.MustCompile("\n")
// this function takes each word of output from the command and builds up a string to see if we're being asked for a password
return func(newBytes []byte) (CredentialType, bool) {
_, err := ttyText.Write(newBytes)
@@ -330,22 +347,18 @@ func (self *cmdObjRunner) getCheckForCredentialRequestFunc() func([]byte) (Crede
self.log.Error(err)
}
prompts := map[string]CredentialType{
`Password:`: Password,
`.+'s password:`: Password,
`Password\s*for\s*'.+':`: Password,
`Username\s*for\s*'.+':`: Username,
`Enter\s*passphrase\s*for\s*key\s*'.+':`: Passphrase,
`Enter\s*PIN\s*for\s*.+\s*key\s*.+:`: PIN,
}
for pattern, askFor := range prompts {
if match, _ := regexp.MatchString(pattern, ttyText.String()); match {
for pattern, askFor := range compiledPrompts {
if match := pattern.Match([]byte(ttyText.String())); match {
ttyText.Reset()
return askFor, true
}
}
if indices := newlineRegex.FindIndex([]byte(ttyText.String())); indices != nil {
newText := []byte(ttyText.String()[indices[1]:])
ttyText.Reset()
ttyText.Write(newText)
}
return 0, false
}
}
@@ -0,0 +1,109 @@
package oscommands
import (
"strings"
"testing"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func getRunner() *cmdObjRunner {
log := utils.NewDummyLog()
return &cmdObjRunner{
log: log,
guiIO: NewNullGuiIO(log),
}
}
func TestProcessOutput(t *testing.T) {
defaultPromptUserForCredential := func(ct CredentialType) string {
switch ct {
case Password:
return "password"
case Username:
return "username"
case Passphrase:
return "passphrase"
case PIN:
return "pin"
default:
panic("unexpected credential type")
}
}
scenarios := []struct {
name string
promptUserForCredential func(CredentialType) string
output string
expectedToWrite string
}{
{
name: "no output",
promptUserForCredential: defaultPromptUserForCredential,
output: "",
expectedToWrite: "",
},
{
name: "password prompt",
promptUserForCredential: defaultPromptUserForCredential,
output: "Password:",
expectedToWrite: "password",
},
{
name: "password prompt 2",
promptUserForCredential: defaultPromptUserForCredential,
output: "Bill's password:",
expectedToWrite: "password",
},
{
name: "password prompt 3",
promptUserForCredential: defaultPromptUserForCredential,
output: "Password for 'Bill':",
expectedToWrite: "password",
},
{
name: "username prompt",
promptUserForCredential: defaultPromptUserForCredential,
output: "Username for 'Bill':",
expectedToWrite: "username",
},
{
name: "passphrase prompt",
promptUserForCredential: defaultPromptUserForCredential,
output: "Enter passphrase for key '123':",
expectedToWrite: "passphrase",
},
{
name: "pin prompt",
promptUserForCredential: defaultPromptUserForCredential,
output: "Enter PIN for key '123':",
expectedToWrite: "pin",
},
{
name: "username and password prompt",
promptUserForCredential: defaultPromptUserForCredential,
output: "Password:\nUsername for 'Alice':\n",
expectedToWrite: "passwordusername",
},
{
name: "user submits empty credential",
promptUserForCredential: func(ct CredentialType) string { return "" },
output: "Password:\n",
expectedToWrite: "",
},
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
runner := getRunner()
reader := strings.NewReader(scenario.output)
writer := &strings.Builder{}
runner.processOutput(reader, writer, scenario.promptUserForCredential)
if writer.String() != scenario.expectedToWrite {
t.Errorf("expected to write '%s' but got '%s'", scenario.expectedToWrite, writer.String())
}
})
}
}
+16 -7
View File
@@ -78,21 +78,30 @@ func FileType(path string) string {
}
func (c *OSCommand) OpenFile(filename string) error {
return c.OpenFileAtLine(filename, 1)
}
func (c *OSCommand) OpenFileAtLine(filename string, lineNumber int) error {
commandTemplate := c.UserConfig.OS.OpenCommand
commandTemplate := c.UserConfig.OS.Open
if commandTemplate == "" {
// Legacy support
commandTemplate = c.UserConfig.OS.OpenCommand
}
if commandTemplate == "" {
commandTemplate = config.GetPlatformDefaultConfig().Open
}
templateValues := map[string]string{
"filename": c.Quote(filename),
"line": fmt.Sprintf("%d", lineNumber),
}
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
return c.Cmd.NewShell(command).Run()
}
func (c *OSCommand) OpenLink(link string) error {
commandTemplate := c.UserConfig.OS.OpenLinkCommand
commandTemplate := c.UserConfig.OS.OpenLink
if commandTemplate == "" {
// Legacy support
commandTemplate = c.UserConfig.OS.OpenLinkCommand
}
if commandTemplate == "" {
commandTemplate = config.GetPlatformDefaultConfig().OpenLink
}
templateValues := map[string]string{
"link": c.Quote(link),
}
@@ -75,7 +75,7 @@ func TestOSCommandOpenFileDarwin(t *testing.T) {
for _, s := range scenarios {
oSCmd := NewDummyOSCommandWithRunner(s.runner)
oSCmd.Platform.OS = "darwin"
oSCmd.UserConfig.OS.OpenCommand = "open {{filename}}"
oSCmd.UserConfig.OS.Open = "open {{filename}}"
s.test(oSCmd.OpenFile(s.filename))
}
@@ -135,7 +135,7 @@ func TestOSCommandOpenFileLinux(t *testing.T) {
for _, s := range scenarios {
oSCmd := NewDummyOSCommandWithRunner(s.runner)
oSCmd.Platform.OS = "linux"
oSCmd.UserConfig.OS.OpenCommand = `xdg-open {{filename}} > /dev/null`
oSCmd.UserConfig.OS.Open = `xdg-open {{filename}} > /dev/null`
s.test(oSCmd.OpenFile(s.filename))
}
@@ -6,6 +6,7 @@ package oscommands
import (
"testing"
"github.com/cli/safeexec"
"github.com/go-errors/errors"
"github.com/stretchr/testify/assert"
)
@@ -19,11 +20,13 @@ func TestOSCommandOpenFileWindows(t *testing.T) {
test func(error)
}
fullCmdPath, _ := safeexec.LookPath("cmd")
scenarios := []scenario{
{
filename: "test",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/c", "start", "", "test"}, "", errors.New("error")),
ExpectArgs([]string{fullCmdPath, "/c", "start", "", "test"}, "", errors.New("error")),
test: func(err error) {
assert.Error(t, err)
},
@@ -31,7 +34,7 @@ func TestOSCommandOpenFileWindows(t *testing.T) {
{
filename: "test",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/c", "start", "", "test"}, "", nil),
ExpectArgs([]string{fullCmdPath, "/c", "start", "", "test"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
@@ -39,7 +42,7 @@ func TestOSCommandOpenFileWindows(t *testing.T) {
{
filename: "filename with spaces",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/c", "start", "", "filename with spaces"}, "", nil),
ExpectArgs([]string{fullCmdPath, "/c", "start", "", "filename with spaces"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
@@ -47,7 +50,7 @@ func TestOSCommandOpenFileWindows(t *testing.T) {
{
filename: "let's_test_with_single_quote",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/c", "start", "", "let's_test_with_single_quote"}, "", nil),
ExpectArgs([]string{fullCmdPath, "/c", "start", "", "let's_test_with_single_quote"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
@@ -55,7 +58,7 @@ func TestOSCommandOpenFileWindows(t *testing.T) {
{
filename: "$USER.txt",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/c", "start", "", "$USER.txt"}, "", nil),
ExpectArgs([]string{fullCmdPath, "/c", "start", "", "$USER.txt"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
+169
View File
@@ -0,0 +1,169 @@
package patch
import (
"strings"
"github.com/jesseduffield/generics/set"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/samber/lo"
)
type patchPresenter struct {
patch *Patch
// if true, all following fields are ignored
plain bool
isFocused bool
// first line index for selected cursor range
firstLineIndex int
// last line index for selected cursor range
lastLineIndex int
// line indices for tagged lines (e.g. lines added to a custom patch)
incLineIndices *set.Set[int]
}
// formats the patch as a plain string
func formatPlain(patch *Patch) string {
presenter := &patchPresenter{
patch: patch,
plain: true,
incLineIndices: set.New[int](),
}
return presenter.format()
}
func formatRangePlain(patch *Patch, startIdx int, endIdx int) string {
lines := patch.Lines()[startIdx : endIdx+1]
return strings.Join(
lo.Map(lines, func(line *PatchLine, _ int) string {
return line.Content + "\n"
}),
"",
)
}
type FormatViewOpts struct {
IsFocused bool
// first line index for selected cursor range
FirstLineIndex int
// last line index for selected cursor range
LastLineIndex int
// line indices for tagged lines (e.g. lines added to a custom patch)
IncLineIndices *set.Set[int]
}
// formats the patch for rendering within a view, meaning it's coloured and
// highlights selected items
func formatView(patch *Patch, opts FormatViewOpts) string {
includedLineIndices := opts.IncLineIndices
if includedLineIndices == nil {
includedLineIndices = set.New[int]()
}
presenter := &patchPresenter{
patch: patch,
plain: false,
isFocused: opts.IsFocused,
firstLineIndex: opts.FirstLineIndex,
lastLineIndex: opts.LastLineIndex,
incLineIndices: includedLineIndices,
}
return presenter.format()
}
func (self *patchPresenter) format() string {
// if we have no changes in our patch (i.e. no additions or deletions) then
// the patch is effectively empty and we can return an empty string
if !self.patch.ContainsChanges() {
return ""
}
stringBuilder := &strings.Builder{}
lineIdx := 0
appendLine := func(line string) {
_, _ = stringBuilder.WriteString(line + "\n")
lineIdx++
}
appendFormattedLine := func(line string, style style.TextStyle) {
formattedLine := self.formatLine(
line,
style,
lineIdx,
)
appendLine(formattedLine)
}
for _, line := range self.patch.header {
appendFormattedLine(line, theme.DefaultTextColor.SetBold())
}
for _, hunk := range self.patch.hunks {
appendLine(
self.formatLine(
hunk.formatHeaderStart(),
style.FgCyan,
lineIdx,
) +
// we're splitting the line into two parts: the diff header and the context
// We explicitly pass 'included' as false here so that we're only tagging the
// first half of the line as included if the line is indeed included.
self.formatLineAux(
hunk.headerContext,
theme.DefaultTextColor,
lineIdx,
false,
),
)
for _, line := range hunk.bodyLines {
appendFormattedLine(line.Content, self.patchLineStyle(line))
}
}
return stringBuilder.String()
}
func (self *patchPresenter) patchLineStyle(patchLine *PatchLine) style.TextStyle {
switch patchLine.Kind {
case ADDITION:
return style.FgGreen
case DELETION:
return style.FgRed
default:
return theme.DefaultTextColor
}
}
func (self *patchPresenter) formatLine(str string, textStyle style.TextStyle, index int) string {
included := self.incLineIndices.Includes(index)
return self.formatLineAux(str, textStyle, index, included)
}
// 'selected' means you've got it highlighted with your cursor
// 'included' means the line has been included in the patch (only applicable when
// building a patch)
func (self *patchPresenter) formatLineAux(str string, textStyle style.TextStyle, index int, included bool) string {
if self.plain {
return str
}
selected := self.isFocused && index >= self.firstLineIndex && index <= self.lastLineIndex
if selected {
textStyle = textStyle.MergeStyle(theme.SelectedRangeBgColor)
}
firstCharStyle := textStyle
if included {
firstCharStyle = firstCharStyle.MergeStyle(style.BgGreen)
}
if len(str) < 2 {
return firstCharStyle.Sprint(str)
}
return firstCharStyle.Sprint(str[:1]) + textStyle.Sprint(str[1:])
}
+49 -125
View File
@@ -1,143 +1,67 @@
package patch
import (
"fmt"
"strings"
import "fmt"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
// Example hunk:
// @@ -16,2 +14,3 @@ func (f *CommitFile) Description() string {
// return f.Name
// -}
// +
// +// test
type PatchHunk struct {
FirstLineIdx int
oldStart int
newStart int
heading string
bodyLines []string
type Hunk struct {
// the line number of the first line in the old file ('16' in the above example)
oldStart int
// the line number of the first line in the new file ('14' in the above example)
newStart int
// the context at the end of the header line (' func (f *CommitFile) Description() string {' in the above example)
headerContext string
// the body of the hunk, excluding the header line
bodyLines []*PatchLine
}
func (hunk *PatchHunk) LastLineIdx() int {
return hunk.FirstLineIdx + len(hunk.bodyLines)
// Returns the number of lines in the hunk in the original file ('2' in the above example)
func (self *Hunk) oldLength() int {
return nLinesWithKind(self.bodyLines, []PatchLineKind{CONTEXT, DELETION})
}
func newHunk(lines []string, firstLineIdx int) *PatchHunk {
header := lines[0]
bodyLines := lines[1:]
oldStart, newStart, heading := headerInfo(header)
return &PatchHunk{
oldStart: oldStart,
newStart: newStart,
heading: heading,
FirstLineIdx: firstLineIdx,
bodyLines: bodyLines,
}
// Returns the number of lines in the hunk in the new file ('3' in the above example)
func (self *Hunk) newLength() int {
return nLinesWithKind(self.bodyLines, []PatchLineKind{CONTEXT, ADDITION})
}
func headerInfo(header string) (int, int, string) {
match := hunkHeaderRegexp.FindStringSubmatch(header)
oldStart := utils.MustConvertToInt(match[1])
newStart := utils.MustConvertToInt(match[2])
heading := match[3]
return oldStart, newStart, heading
// Returns true if the hunk contains any changes (i.e. if it's not just a context hunk).
// We'll end up with a context hunk if we're transforming a patch and one of the hunks
// has no selected lines.
func (self *Hunk) containsChanges() bool {
return nLinesWithKind(self.bodyLines, []PatchLineKind{ADDITION, DELETION}) > 0
}
func (hunk *PatchHunk) updatedLines(lineIndices []int, reverse bool) []string {
skippedNewlineMessageIndex := -1
newLines := []string{}
// Returns the number of lines in the hunk, including the header line
func (self *Hunk) lineCount() int {
return len(self.bodyLines) + 1
}
lineIdx := hunk.FirstLineIdx
for _, line := range hunk.bodyLines {
lineIdx++ // incrementing at the start to skip the header line
if line == "" {
break
}
isLineSelected := lo.Contains(lineIndices, lineIdx)
// Returns all lines in the hunk, including the header line
func (self *Hunk) allLines() []*PatchLine {
lines := []*PatchLine{{Content: self.formatHeaderLine(), Kind: HUNK_HEADER}}
lines = append(lines, self.bodyLines...)
return lines
}
firstChar, content := line[:1], line[1:]
transformedFirstChar := transformedFirstChar(firstChar, reverse, isLineSelected)
// Returns the header line, including the unified diff header and the context
func (self *Hunk) formatHeaderLine() string {
return fmt.Sprintf("%s%s", self.formatHeaderStart(), self.headerContext)
}
if isLineSelected || (transformedFirstChar == "\\" && skippedNewlineMessageIndex != lineIdx) || transformedFirstChar == " " {
newLines = append(newLines, transformedFirstChar+content)
continue
}
if transformedFirstChar == "+" {
// we don't want to include the 'newline at end of file' line if it involves an addition we're not including
skippedNewlineMessageIndex = lineIdx + 1
}
// Returns the first part of the header line i.e. the unified diff part (excluding any context)
func (self *Hunk) formatHeaderStart() string {
newLengthDisplay := ""
newLength := self.newLength()
// if the new length is 1, it's omitted
if newLength != 1 {
newLengthDisplay = fmt.Sprintf(",%d", newLength)
}
return newLines
}
func transformedFirstChar(firstChar string, reverse bool, isLineSelected bool) string {
if reverse {
if !isLineSelected && firstChar == "+" {
return " "
} else if firstChar == "-" {
return "+"
} else if firstChar == "+" {
return "-"
} else {
return firstChar
}
}
if !isLineSelected && firstChar == "-" {
return " "
}
return firstChar
}
func (hunk *PatchHunk) formatHeader(oldStart int, oldLength int, newStart int, newLength int, heading string) string {
return fmt.Sprintf("@@ -%d,%d +%d,%d @@%s\n", oldStart, oldLength, newStart, newLength, heading)
}
func (hunk *PatchHunk) formatWithChanges(lineIndices []int, reverse bool, startOffset int) (int, string) {
bodyLines := hunk.updatedLines(lineIndices, reverse)
startOffset, header, ok := hunk.updatedHeader(bodyLines, startOffset, reverse)
if !ok {
return startOffset, ""
}
return startOffset, header + strings.Join(bodyLines, "")
}
func (hunk *PatchHunk) updatedHeader(newBodyLines []string, startOffset int, reverse bool) (int, string, bool) {
changeCount := nLinesWithPrefix(newBodyLines, []string{"+", "-"})
oldLength := nLinesWithPrefix(newBodyLines, []string{" ", "-"})
newLength := nLinesWithPrefix(newBodyLines, []string{"+", " "})
if changeCount == 0 {
// if nothing has changed we just return nothing
return startOffset, "", false
}
var oldStart int
if reverse {
oldStart = hunk.newStart
} else {
oldStart = hunk.oldStart
}
var newStartOffset int
// if the hunk went from zero to positive length, we need to increment the starting point by one
// if the hunk went from positive to zero length, we need to decrement the starting point by one
if oldLength == 0 {
newStartOffset = 1
} else if newLength == 0 {
newStartOffset = -1
} else {
newStartOffset = 0
}
newStart := oldStart + startOffset + newStartOffset
newStartOffset = startOffset + newLength - oldLength
formattedHeader := hunk.formatHeader(oldStart, oldLength, newStart, newLength, hunk.heading)
return newStartOffset, formattedHeader, true
return fmt.Sprintf("@@ -%d,%d +%d%s @@", self.oldStart, self.oldLength(), self.newStart, newLengthDisplay)
}
+85
View File
@@ -0,0 +1,85 @@
package patch
import (
"regexp"
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
)
var hunkHeaderRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)[^\+]+\+(\d+)[^@]+@@(.*)$`)
func Parse(patchStr string) *Patch {
// ignore trailing newline.
lines := strings.Split(strings.TrimSuffix(patchStr, "\n"), "\n")
hunks := []*Hunk{}
patchHeader := []string{}
var currentHunk *Hunk
for _, line := range lines {
if strings.HasPrefix(line, "@@") {
oldStart, newStart, headerContext := headerInfo(line)
currentHunk = &Hunk{
oldStart: oldStart,
newStart: newStart,
headerContext: headerContext,
bodyLines: []*PatchLine{},
}
hunks = append(hunks, currentHunk)
} else if currentHunk != nil {
currentHunk.bodyLines = append(currentHunk.bodyLines, newHunkLine(line))
} else {
patchHeader = append(patchHeader, line)
}
}
return &Patch{
hunks: hunks,
header: patchHeader,
}
}
func headerInfo(header string) (int, int, string) {
match := hunkHeaderRegexp.FindStringSubmatch(header)
oldStart := utils.MustConvertToInt(match[1])
newStart := utils.MustConvertToInt(match[2])
headerContext := match[3]
return oldStart, newStart, headerContext
}
func newHunkLine(line string) *PatchLine {
if line == "" {
return &PatchLine{
Kind: CONTEXT,
Content: "",
}
}
firstChar := line[:1]
kind := parseFirstChar(firstChar)
return &PatchLine{
Kind: kind,
Content: line,
}
}
func parseFirstChar(firstChar string) PatchLineKind {
switch firstChar {
case " ":
return CONTEXT
case "+":
return ADDITION
case "-":
return DELETION
case "\\":
return NEWLINE_MESSAGE
}
return CONTEXT
}
+151
View File
@@ -0,0 +1,151 @@
package patch
import (
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
type Patch struct {
// header of the patch (split on newlines) e.g.
// diff --git a/filename b/filename
// index dcd3485..1ba5540 100644
// --- a/filename
// +++ b/filename
header []string
// hunks of the patch
hunks []*Hunk
}
// Returns a new patch with the specified transformation applied (e.g.
// only selecting a subset of changes).
// Leaves the original patch unchanged.
func (self *Patch) Transform(opts TransformOpts) *Patch {
return transform(self, opts)
}
// Returns the patch as a plain string
func (self *Patch) FormatPlain() string {
return formatPlain(self)
}
// Returns a range of lines from the patch as a plain string (range is inclusive)
func (self *Patch) FormatRangePlain(startIdx int, endIdx int) string {
return formatRangePlain(self, startIdx, endIdx)
}
// Returns the patch as a string with ANSI color codes for displaying in a view
func (self *Patch) FormatView(opts FormatViewOpts) string {
return formatView(self, opts)
}
// Returns the lines of the patch
func (self *Patch) Lines() []*PatchLine {
lines := []*PatchLine{}
for _, line := range self.header {
lines = append(lines, &PatchLine{Content: line, Kind: PATCH_HEADER})
}
for _, hunk := range self.hunks {
lines = append(lines, hunk.allLines()...)
}
return lines
}
// 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)
result := len(self.header)
for i := 0; i < hunkIndex; i++ {
result += self.hunks[i].lineCount()
}
return result
}
// 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)
return self.HunkStartIdx(hunkIndex) + self.hunks[hunkIndex].lineCount() - 1
}
func (self *Patch) ContainsChanges() bool {
return lo.SomeBy(self.hunks, func(hunk *Hunk) bool {
return hunk.containsChanges()
})
}
// Takes a line index in the patch and returns the line number in the new file.
// If the line is a header line, returns 1.
// If the line is a hunk header line, returns the first file line number in that hunk.
// If the line is out of range below, returns the last file line number in the last hunk.
func (self *Patch) LineNumberOfLine(idx int) int {
if idx < len(self.header) || len(self.hunks) == 0 {
return 1
}
hunkIdx := self.HunkContainingLine(idx)
// cursor out of range, return last file line number
if hunkIdx == -1 {
lastHunk := self.hunks[len(self.hunks)-1]
return lastHunk.newStart + lastHunk.newLength() - 1
}
hunk := self.hunks[hunkIdx]
hunkStartIdx := self.HunkStartIdx(hunkIdx)
idxInHunk := idx - hunkStartIdx
if idxInHunk == 0 {
return hunk.oldStart
}
lines := hunk.bodyLines[:idxInHunk-1]
offset := nLinesWithKind(lines, []PatchLineKind{ADDITION, CONTEXT})
return hunk.oldStart + offset
}
// Returns hunk index containing the line at the given patch line index
func (self *Patch) HunkContainingLine(idx int) int {
for hunkIdx, hunk := range self.hunks {
hunkStartIdx := self.HunkStartIdx(hunkIdx)
if idx >= hunkStartIdx && idx < hunkStartIdx+hunk.lineCount() {
return hunkIdx
}
}
return -1
}
// 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)
lines := self.Lines()
for i, line := range lines[idx:] {
if line.isChange() {
return i + idx
}
}
// there are no changes from the cursor onwards so we'll instead
// return the index of the last change
for i := len(lines) - 1; i >= 0; i-- {
line := lines[i]
if line.isChange() {
return i
}
}
// should not be possible
return 0
}
// Returns the length of the patch in lines
func (self *Patch) LineCount() int {
count := len(self.header)
for _, hunk := range self.hunks {
count += hunk.lineCount()
}
return count
}
@@ -33,8 +33,8 @@ type (
loadFileDiffFunc func(from string, to string, reverse bool, filename string, plain bool) (string, error)
)
// PatchManager manages the building of a patch for a commit to be applied to another commit (or the working tree, or removed from the current commit). We also support building patches from things like stashes, for which there is less flexibility
type PatchManager struct {
// PatchBuilder manages the building of a patch for a commit to be applied to another commit (or the working tree, or removed from the current commit). We also support building patches from things like stashes, for which there is less flexibility
type PatchBuilder struct {
// To is the commit sha if we're dealing with files of a commit, or a stash ref for a stash
To string
From string
@@ -53,17 +53,16 @@ type PatchManager struct {
loadFileDiff loadFileDiffFunc
}
// NewPatchManager returns a new PatchManager
func NewPatchManager(log *logrus.Entry, applyPatch applyPatchFunc, loadFileDiff loadFileDiffFunc) *PatchManager {
return &PatchManager{
// NewPatchBuilder returns a new PatchBuilder
func NewPatchBuilder(log *logrus.Entry, applyPatch applyPatchFunc, loadFileDiff loadFileDiffFunc) *PatchBuilder {
return &PatchBuilder{
Log: log,
applyPatch: applyPatch,
loadFileDiff: loadFileDiff,
}
}
// NewPatchManager returns a new PatchManager
func (p *PatchManager) Start(from, to string, reverse bool, canRebase bool) {
func (p *PatchBuilder) Start(from, to string, reverse bool, canRebase bool) {
p.To = to
p.From = from
p.reverse = reverse
@@ -71,7 +70,7 @@ func (p *PatchManager) Start(from, to string, reverse bool, canRebase bool) {
p.fileInfoMap = map[string]*fileInfo{}
}
func (p *PatchManager) addFileWhole(info *fileInfo) {
func (p *PatchBuilder) addFileWhole(info *fileInfo) {
info.mode = WHOLE
lineCount := len(strings.Split(info.diff, "\n"))
// add every line index
@@ -82,12 +81,12 @@ func (p *PatchManager) addFileWhole(info *fileInfo) {
}
}
func (p *PatchManager) removeFile(info *fileInfo) {
func (p *PatchBuilder) removeFile(info *fileInfo) {
info.mode = UNSELECTED
info.includedLineIndices = nil
}
func (p *PatchManager) AddFileWhole(filename string) error {
func (p *PatchBuilder) AddFileWhole(filename string) error {
info, err := p.getFileInfo(filename)
if err != nil {
return err
@@ -98,7 +97,7 @@ func (p *PatchManager) AddFileWhole(filename string) error {
return nil
}
func (p *PatchManager) RemoveFile(filename string) error {
func (p *PatchBuilder) RemoveFile(filename string) error {
info, err := p.getFileInfo(filename)
if err != nil {
return err
@@ -117,7 +116,7 @@ func getIndicesForRange(first, last int) []int {
return indices
}
func (p *PatchManager) getFileInfo(filename string) (*fileInfo, error) {
func (p *PatchBuilder) getFileInfo(filename string) (*fileInfo, error) {
info, ok := p.fileInfoMap[filename]
if ok {
return info, nil
@@ -137,7 +136,7 @@ func (p *PatchManager) getFileInfo(filename string) (*fileInfo, error) {
return info, nil
}
func (p *PatchManager) AddFileLineRange(filename string, firstLineIdx, lastLineIdx int) error {
func (p *PatchBuilder) AddFileLineRange(filename string, firstLineIdx, lastLineIdx int) error {
info, err := p.getFileInfo(filename)
if err != nil {
return err
@@ -148,7 +147,7 @@ func (p *PatchManager) AddFileLineRange(filename string, firstLineIdx, lastLineI
return nil
}
func (p *PatchManager) RemoveFileLineRange(filename string, firstLineIdx, lastLineIdx int) error {
func (p *PatchBuilder) RemoveFileLineRange(filename string, firstLineIdx, lastLineIdx int) error {
info, err := p.getFileInfo(filename)
if err != nil {
return err
@@ -162,44 +161,46 @@ func (p *PatchManager) RemoveFileLineRange(filename string, firstLineIdx, lastLi
return nil
}
func (p *PatchManager) renderPlainPatchForFile(filename string, reverse bool, keepOriginalHeader bool) string {
func (p *PatchBuilder) RenderPatchForFile(filename string, plain bool, reverse bool) string {
info, err := p.getFileInfo(filename)
if err != nil {
p.Log.Error(err)
return ""
}
switch info.mode {
case WHOLE:
// use the whole diff
// the reverse flag is only for part patches so we're ignoring it here
return info.diff
case PART:
// generate a new diff with just the selected lines
return ModifiedPatchForLines(p.Log, filename, info.diff, info.includedLineIndices, reverse, keepOriginalHeader)
default:
if info.mode == UNSELECTED {
return ""
}
}
func (p *PatchManager) RenderPatchForFile(filename string, plain bool, reverse bool, keepOriginalHeader bool) string {
patch := p.renderPlainPatchForFile(filename, reverse, keepOriginalHeader)
if plain {
return patch
if info.mode == WHOLE && plain {
// Use the whole diff (spares us parsing it and then formatting it).
// TODO: see if this is actually noticeably faster.
// The reverse flag is only for part patches so we're ignoring it here.
return info.diff
}
parser := NewPatchParser(p.Log, patch)
// not passing included lines because we don't want to see them in the secondary panel
return parser.Render(false, -1, -1, nil)
patch := Parse(info.diff).
Transform(TransformOpts{
Reverse: reverse,
IncludedLineIndices: info.includedLineIndices,
})
if plain {
return patch.FormatPlain()
} else {
return patch.FormatView(FormatViewOpts{
IsFocused: false,
})
}
}
func (p *PatchManager) renderEachFilePatch(plain bool) []string {
func (p *PatchBuilder) renderEachFilePatch(plain bool) []string {
// sort files by name then iterate through and render each patch
filenames := maps.Keys(p.fileInfoMap)
sort.Strings(filenames)
patches := slices.Map(filenames, func(filename string) string {
return p.RenderPatchForFile(filename, plain, false, true)
return p.RenderPatchForFile(filename, plain, false)
})
output := slices.Filter(patches, func(patch string) bool {
return patch != ""
@@ -208,17 +209,11 @@ func (p *PatchManager) renderEachFilePatch(plain bool) []string {
return output
}
func (p *PatchManager) RenderAggregatedPatchColored(plain bool) string {
result := ""
for _, patch := range p.renderEachFilePatch(plain) {
if patch != "" {
result += patch + "\n"
}
}
return result
func (p *PatchBuilder) RenderAggregatedPatch(plain bool) string {
return strings.Join(p.renderEachFilePatch(plain), "")
}
func (p *PatchManager) GetFileStatus(filename string, parent string) PatchStatus {
func (p *PatchBuilder) GetFileStatus(filename string, parent string) PatchStatus {
if parent != p.To {
return UNSELECTED
}
@@ -231,7 +226,7 @@ func (p *PatchManager) GetFileStatus(filename string, parent string) PatchStatus
return info.mode
}
func (p *PatchManager) GetFileIncLineIndices(filename string) ([]int, error) {
func (p *PatchBuilder) GetFileIncLineIndices(filename string) ([]int, error) {
info, err := p.getFileInfo(filename)
if err != nil {
return nil, err
@@ -239,56 +234,36 @@ func (p *PatchManager) GetFileIncLineIndices(filename string) ([]int, error) {
return info.includedLineIndices, nil
}
func (p *PatchManager) ApplyPatches(reverse bool) error {
// for whole patches we'll apply the patch in reverse
// but for part patches we'll apply a reverse patch forwards
func (p *PatchBuilder) ApplyPatches(reverse bool) error {
patch := ""
applyFlags := []string{"index", "3way"}
if reverse {
applyFlags = append(applyFlags, "reverse")
}
for filename, info := range p.fileInfoMap {
if info.mode == UNSELECTED {
continue
}
applyFlags := []string{"index", "3way"}
reverseOnGenerate := false
if reverse {
if info.mode == WHOLE {
applyFlags = append(applyFlags, "reverse")
} else {
reverseOnGenerate = true
}
}
var err error
// first run we try with the original header, then without
for _, keepOriginalHeader := range []bool{true, false} {
patch := p.RenderPatchForFile(filename, true, reverseOnGenerate, keepOriginalHeader)
if patch == "" {
continue
}
if err = p.applyPatch(patch, applyFlags...); err != nil {
continue
}
break
}
if err != nil {
return err
}
patch += p.RenderPatchForFile(filename, true, reverse)
}
return nil
return p.applyPatch(patch, applyFlags...)
}
// clears the patch
func (p *PatchManager) Reset() {
func (p *PatchBuilder) Reset() {
p.To = ""
p.fileInfoMap = map[string]*fileInfo{}
}
func (p *PatchManager) Active() bool {
func (p *PatchBuilder) Active() bool {
return p.To != ""
}
func (p *PatchManager) IsEmpty() bool {
func (p *PatchBuilder) IsEmpty() bool {
for _, fileInfo := range p.fileInfoMap {
if fileInfo.mode == WHOLE || (fileInfo.mode == PART && len(fileInfo.includedLineIndices) > 0) {
return false
@@ -299,6 +274,16 @@ func (p *PatchManager) IsEmpty() bool {
}
// if any of these things change we'll need to reset and start a new patch
func (p *PatchManager) NewPatchRequired(from string, to string, reverse bool) bool {
func (p *PatchBuilder) NewPatchRequired(from string, to string, reverse bool) bool {
return from != p.From || to != p.To || reverse != p.reverse
}
func (p *PatchBuilder) AllFilesInPatch() []string {
files := make([]string, 0, len(p.fileInfoMap))
for filename := range p.fileInfoMap {
files = append(files, filename)
}
return files
}
+30
View File
@@ -0,0 +1,30 @@
package patch
import "github.com/samber/lo"
type PatchLineKind int
const (
PATCH_HEADER PatchLineKind = iota
HUNK_HEADER
ADDITION
DELETION
CONTEXT
NEWLINE_MESSAGE
)
type PatchLine struct {
Kind PatchLineKind
Content string // something like '+ hello' (note the first character is not removed)
}
func (self *PatchLine) isChange() bool {
return self.Kind == ADDITION || self.Kind == DELETION
}
// Returns the number of lines in the given slice that have one of the given kinds
func nLinesWithKind(lines []*PatchLine, kinds []PatchLineKind) int {
return lo.CountBy(lines, func(line *PatchLine) bool {
return lo.Contains(kinds, line.Kind)
})
}
-167
View File
@@ -1,167 +0,0 @@
package patch
import (
"fmt"
"regexp"
"strings"
"github.com/sirupsen/logrus"
)
var (
hunkHeaderRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)[^\+]+\+(\d+)[^@]+@@(.*)$`)
patchHeaderRegexp = regexp.MustCompile(`(?ms)(^diff.*?)^@@`)
)
func GetHeaderFromDiff(diff string) string {
match := patchHeaderRegexp.FindStringSubmatch(diff)
if len(match) <= 1 {
return ""
}
return match[1]
}
func GetHunksFromDiff(diff string) []*PatchHunk {
hunks := []*PatchHunk{}
firstLineIdx := -1
var hunkLines []string //nolint:prealloc
pastDiffHeader := false
for lineIdx, line := range strings.SplitAfter(diff, "\n") {
isHunkHeader := strings.HasPrefix(line, "@@ -")
if isHunkHeader {
if pastDiffHeader { // we need to persist the current hunk
hunks = append(hunks, newHunk(hunkLines, firstLineIdx))
}
pastDiffHeader = true
firstLineIdx = lineIdx
hunkLines = []string{line}
continue
}
if !pastDiffHeader { // skip through the stuff that precedes the first hunk
continue
}
hunkLines = append(hunkLines, line)
}
if pastDiffHeader {
hunks = append(hunks, newHunk(hunkLines, firstLineIdx))
}
return hunks
}
type PatchModifier struct {
Log *logrus.Entry
filename string
hunks []*PatchHunk
header string
}
func NewPatchModifier(log *logrus.Entry, filename string, diffText string) *PatchModifier {
return &PatchModifier{
Log: log,
filename: filename,
hunks: GetHunksFromDiff(diffText),
header: GetHeaderFromDiff(diffText),
}
}
func (d *PatchModifier) ModifiedPatchForLines(lineIndices []int, reverse bool, keepOriginalHeader bool) string {
// step one is getting only those hunks which we care about
hunksInRange := []*PatchHunk{}
outer:
for _, hunk := range d.hunks {
// if there is any line in our lineIndices array that the hunk contains, we append it
for _, lineIdx := range lineIndices {
if lineIdx >= hunk.FirstLineIdx && lineIdx <= hunk.LastLineIdx() {
hunksInRange = append(hunksInRange, hunk)
continue outer
}
}
}
// step 2 is collecting all the hunks with new headers
startOffset := 0
formattedHunks := ""
var formattedHunk string
for _, hunk := range hunksInRange {
startOffset, formattedHunk = hunk.formatWithChanges(lineIndices, reverse, startOffset)
formattedHunks += formattedHunk
}
if formattedHunks == "" {
return ""
}
var fileHeader string
// for staging/unstaging lines we don't want the original header because
// it makes git confused e.g. when dealing with deleted/added files
// but with building and applying patches the original header gives git
// information it needs to cleanly apply patches
if keepOriginalHeader {
fileHeader = d.header
} else {
fileHeader = fmt.Sprintf("--- a/%s\n+++ b/%s\n", d.filename, d.filename)
}
return fileHeader + formattedHunks
}
func (d *PatchModifier) ModifiedPatchForRange(firstLineIdx int, lastLineIdx int, reverse bool, keepOriginalHeader bool) string {
// generate array of consecutive line indices from our range
selectedLines := []int{}
for i := firstLineIdx; i <= lastLineIdx; i++ {
selectedLines = append(selectedLines, i)
}
return d.ModifiedPatchForLines(selectedLines, reverse, keepOriginalHeader)
}
func (d *PatchModifier) OriginalPatchLength() int {
if len(d.hunks) == 0 {
return 0
}
return d.hunks[len(d.hunks)-1].LastLineIdx()
}
func ModifiedPatchForRange(log *logrus.Entry, filename string, diffText string, firstLineIdx int, lastLineIdx int, reverse bool, keepOriginalHeader bool) string {
p := NewPatchModifier(log, filename, diffText)
return p.ModifiedPatchForRange(firstLineIdx, lastLineIdx, reverse, keepOriginalHeader)
}
func ModifiedPatchForLines(log *logrus.Entry, filename string, diffText string, includedLineIndices []int, reverse bool, keepOriginalHeader bool) string {
p := NewPatchModifier(log, filename, diffText)
return p.ModifiedPatchForLines(includedLineIndices, reverse, keepOriginalHeader)
}
// I want to know, given a hunk, what line a given index is on
func (hunk *PatchHunk) LineNumberOfLine(idx int) int {
n := idx - hunk.FirstLineIdx - 1
if n < 0 {
n = 0
} else if n >= len(hunk.bodyLines) {
n = len(hunk.bodyLines) - 1
}
lines := hunk.bodyLines[0:n]
offset := nLinesWithPrefix(lines, []string{"+", " "})
return hunk.newStart + offset
}
func nLinesWithPrefix(lines []string, chars []string) int {
result := 0
for _, line := range lines {
for _, char := range chars {
if line[:1] == char {
result++
}
}
}
return result
}
-228
View File
@@ -1,228 +0,0 @@
package patch
import (
"regexp"
"strings"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/samber/lo"
"github.com/sirupsen/logrus"
)
type PatchLineKind int
const (
PATCH_HEADER PatchLineKind = iota
COMMIT_SHA
COMMIT_DESCRIPTION
HUNK_HEADER
ADDITION
DELETION
CONTEXT
NEWLINE_MESSAGE
)
// the job of this file is to parse a diff, find out where the hunks begin and end, which lines are stageable, and how to find the next hunk from the current position or the next stageable line from the current position.
type PatchLine struct {
Kind PatchLineKind
Content string // something like '+ hello' (note the first character is not removed)
}
type PatchParser struct {
Log *logrus.Entry
PatchLines []*PatchLine
PatchHunks []*PatchHunk
HunkStarts []int
StageableLines []int // rename to mention we're talking about indexes
}
// NewPatchParser builds a new branch list builder
func NewPatchParser(log *logrus.Entry, patch string) *PatchParser {
hunkStarts, stageableLines, patchLines := parsePatch(patch)
patchHunks := GetHunksFromDiff(patch)
return &PatchParser{
Log: log,
HunkStarts: hunkStarts, // deprecated
StageableLines: stageableLines,
PatchLines: patchLines,
PatchHunks: patchHunks,
}
}
// GetHunkContainingLine takes a line index and an offset and finds the hunk
// which contains the line index, then returns the hunk considering the offset.
// e.g. if the offset is 1 it will return the next hunk.
func (p *PatchParser) GetHunkContainingLine(lineIndex int, offset int) *PatchHunk {
if len(p.PatchHunks) == 0 {
return nil
}
for index, hunk := range p.PatchHunks {
if lineIndex >= hunk.FirstLineIdx && lineIndex <= hunk.LastLineIdx() {
resultIndex := index + offset
if resultIndex < 0 {
resultIndex = 0
} else if resultIndex > len(p.PatchHunks)-1 {
resultIndex = len(p.PatchHunks) - 1
}
return p.PatchHunks[resultIndex]
}
}
// if your cursor is past the last hunk, select the last hunk
if lineIndex > p.PatchHunks[len(p.PatchHunks)-1].LastLineIdx() {
return p.PatchHunks[len(p.PatchHunks)-1]
}
// otherwise select the first
return p.PatchHunks[0]
}
// selected means you've got it highlighted with your cursor
// included means the line has been included in the patch (only applicable when
// building a patch)
func (l *PatchLine) render(selected bool, included bool) string {
content := l.Content
if len(content) == 0 {
content = " " // using the space so that we can still highlight if necessary
}
// for hunk headers we need to start off cyan and then use white for the message
if l.Kind == HUNK_HEADER {
re := regexp.MustCompile("(@@.*?@@)(.*)")
match := re.FindStringSubmatch(content)
return coloredString(style.FgCyan, match[1], selected, included) + coloredString(theme.DefaultTextColor, match[2], selected, false)
}
var textStyle style.TextStyle
switch l.Kind {
case PATCH_HEADER:
textStyle = textStyle.SetBold()
case ADDITION:
textStyle = style.FgGreen
case DELETION:
textStyle = style.FgRed
case COMMIT_SHA:
textStyle = style.FgYellow
default:
textStyle = theme.DefaultTextColor
}
return coloredString(textStyle, content, selected, included)
}
func coloredString(textStyle style.TextStyle, str string, selected bool, included bool) string {
if selected {
textStyle = textStyle.MergeStyle(theme.SelectedRangeBgColor)
}
firstCharStyle := textStyle
if included {
firstCharStyle = firstCharStyle.MergeStyle(style.BgGreen)
}
if len(str) < 2 {
return firstCharStyle.Sprint(str)
}
return firstCharStyle.Sprint(str[:1]) + textStyle.Sprint(str[1:])
}
func parsePatch(patch string) ([]int, []int, []*PatchLine) {
lines := strings.Split(patch, "\n")
hunkStarts := []int{}
stageableLines := []int{}
pastFirstHunkHeader := false
pastCommitDescription := true
patchLines := make([]*PatchLine, len(lines))
var lineKind PatchLineKind
var firstChar string
for index, line := range lines {
firstChar = " "
if len(line) > 0 {
firstChar = line[:1]
}
if index == 0 && strings.HasPrefix(line, "commit") {
lineKind = COMMIT_SHA
pastCommitDescription = false
} else if !pastCommitDescription {
if strings.HasPrefix(line, "diff") || strings.HasPrefix(line, "---") {
pastCommitDescription = true
lineKind = PATCH_HEADER
} else {
lineKind = COMMIT_DESCRIPTION
}
} else if firstChar == "@" {
pastFirstHunkHeader = true
hunkStarts = append(hunkStarts, index)
lineKind = HUNK_HEADER
} else if pastFirstHunkHeader {
switch firstChar {
case "-":
lineKind = DELETION
stageableLines = append(stageableLines, index)
case "+":
lineKind = ADDITION
stageableLines = append(stageableLines, index)
case "\\":
lineKind = NEWLINE_MESSAGE
case " ":
lineKind = CONTEXT
}
} else {
lineKind = PATCH_HEADER
}
patchLines[index] = &PatchLine{Kind: lineKind, Content: line}
}
return hunkStarts, stageableLines, patchLines
}
// Render returns the coloured string of the diff with any selected lines highlighted
func (p *PatchParser) Render(isFocused bool, firstLineIndex int, lastLineIndex int, incLineIndices []int) string {
contentToDisplay := slices.Some(p.PatchLines, func(line *PatchLine) bool {
return line.Content != ""
})
if !contentToDisplay {
return ""
}
renderedLines := slices.MapWithIndex(p.PatchLines, func(patchLine *PatchLine, index int) string {
selected := isFocused && index >= firstLineIndex && index <= lastLineIndex
included := lo.Contains(incLineIndices, index)
return patchLine.render(selected, included)
})
result := strings.Join(renderedLines, "\n")
return result
}
// RenderLinesPlain returns the non-coloured string of diff part from firstLineIndex to
// lastLineIndex
func (p *PatchParser) RenderLinesPlain(firstLineIndex, lastLineIndex int) string {
return renderLinesPlain(p.PatchLines[firstLineIndex : lastLineIndex+1])
}
func renderLinesPlain(lines []*PatchLine) string {
renderedLines := slices.Map(lines, func(line *PatchLine) string {
return line.Content + "\n"
})
return strings.Join(renderedLines, "")
}
// GetNextStageableLineIndex takes a line index and returns the line index of the next stageable line
// note this will actually include the current index if it is stageable
func (p *PatchParser) GetNextStageableLineIndex(currentIndex int) int {
for _, lineIndex := range p.StageableLines {
if lineIndex >= currentIndex {
return lineIndex
}
}
return p.StageableLines[len(p.StageableLines)-1]
}
@@ -1,8 +1,6 @@
package patch
import (
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
@@ -69,6 +67,20 @@ index e48a11c..b2ab81b 100644
...
`
const twoChangesInOneHunk = `diff --git a/filename b/filename
index 9320895..6d79956 100644
--- a/filename
+++ b/filename
@@ -1,5 +1,5 @@
apple
-grape
+kiwi
orange
-pear
+banana
lemon
`
const newFile = `diff --git a/newfile b/newfile
new file mode 100644
index 0000000..4e680cc
@@ -98,8 +110,7 @@ const exampleHunk = `@@ -1,5 +1,5 @@
...
`
// TestModifyPatchForRange is a function.
func TestModifyPatchForRange(t *testing.T) {
func TestTransform(t *testing.T) {
type scenario struct {
testName string
filename string
@@ -116,7 +127,6 @@ func TestModifyPatchForRange(t *testing.T) {
filename: "filename",
firstLineIndex: -1,
lastLineIndex: -1,
reverse: false,
diffText: simpleDiff,
expected: "",
},
@@ -125,7 +135,6 @@ func TestModifyPatchForRange(t *testing.T) {
filename: "filename",
firstLineIndex: 5,
lastLineIndex: 5,
reverse: false,
diffText: simpleDiff,
expected: "",
},
@@ -134,7 +143,6 @@ func TestModifyPatchForRange(t *testing.T) {
filename: "filename",
firstLineIndex: 0,
lastLineIndex: 11,
reverse: false,
diffText: simpleDiff,
expected: `--- a/filename
+++ b/filename
@@ -152,7 +160,6 @@ func TestModifyPatchForRange(t *testing.T) {
filename: "filename",
firstLineIndex: 6,
lastLineIndex: 6,
reverse: false,
diffText: simpleDiff,
expected: `--- a/filename
+++ b/filename
@@ -169,7 +176,6 @@ func TestModifyPatchForRange(t *testing.T) {
filename: "filename",
firstLineIndex: 7,
lastLineIndex: 7,
reverse: false,
diffText: simpleDiff,
expected: `--- a/filename
+++ b/filename
@@ -187,7 +193,6 @@ func TestModifyPatchForRange(t *testing.T) {
filename: "filename",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: false,
diffText: simpleDiff,
expected: `--- a/filename
+++ b/filename
@@ -198,59 +203,6 @@ func TestModifyPatchForRange(t *testing.T) {
...
...
...
`,
},
{
testName: "whole range reversed",
filename: "filename",
firstLineIndex: 0,
lastLineIndex: 11,
reverse: true,
diffText: simpleDiff,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,5 @@
apple
+orange
-grape
...
...
...
`,
},
{
testName: "removal reversed",
filename: "filename",
firstLineIndex: 6,
lastLineIndex: 6,
reverse: true,
diffText: simpleDiff,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,6 @@
apple
+orange
grape
...
...
...
`,
},
{
testName: "removal reversed",
filename: "filename",
firstLineIndex: 7,
lastLineIndex: 7,
reverse: true,
diffText: simpleDiff,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,4 @@
apple
-grape
...
...
...
`,
},
{
@@ -258,7 +210,6 @@ func TestModifyPatchForRange(t *testing.T) {
filename: "filename",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: false,
diffText: addNewlineToEndOfFile,
expected: `--- a/filename
+++ b/filename
@@ -272,37 +223,21 @@ func TestModifyPatchForRange(t *testing.T) {
`,
},
{
testName: "add newline to end of file, addition only",
testName: "add newline to end of file, reversed",
filename: "filename",
firstLineIndex: 8,
lastLineIndex: 8,
firstLineIndex: -100,
lastLineIndex: 100,
reverse: true,
diffText: addNewlineToEndOfFile,
expected: `--- a/filename
+++ b/filename
@@ -60,4 +60,5 @@ grape
...
...
...
+last line
\ No newline at end of file
last line
`,
},
{
testName: "add newline to end of file, removal only",
filename: "filename",
firstLineIndex: 10,
lastLineIndex: 10,
reverse: true,
diffText: addNewlineToEndOfFile,
expected: `--- a/filename
+++ b/filename
@@ -60,4 +60,3 @@ grape
@@ -60,4 +60,4 @@ grape
...
...
...
-last line
\ No newline at end of file
+last line
`,
},
{
@@ -310,7 +245,24 @@ func TestModifyPatchForRange(t *testing.T) {
filename: "filename",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: false,
diffText: removeNewlinefromEndOfFile,
expected: `--- a/filename
+++ b/filename
@@ -60,4 +60,4 @@ grape
...
...
...
-last line
+last line
\ No newline at end of file
`,
},
{
testName: "remove newline from end of file, reversed",
filename: "filename",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: true,
diffText: removeNewlinefromEndOfFile,
expected: `--- a/filename
+++ b/filename
@@ -328,7 +280,6 @@ func TestModifyPatchForRange(t *testing.T) {
filename: "filename",
firstLineIndex: 8,
lastLineIndex: 8,
reverse: false,
diffText: removeNewlinefromEndOfFile,
expected: `--- a/filename
+++ b/filename
@@ -337,6 +288,24 @@ func TestModifyPatchForRange(t *testing.T) {
...
...
-last line
`,
},
{
testName: "remove newline from end of file, removal only, reversed",
filename: "filename",
firstLineIndex: 8,
lastLineIndex: 8,
reverse: true,
diffText: removeNewlinefromEndOfFile,
expected: `--- a/filename
+++ b/filename
@@ -60,5 +60,4 @@ grape
...
...
...
-last line
last line
\ No newline at end of file
`,
},
{
@@ -344,7 +313,6 @@ func TestModifyPatchForRange(t *testing.T) {
filename: "filename",
firstLineIndex: 9,
lastLineIndex: 9,
reverse: false,
diffText: removeNewlinefromEndOfFile,
expected: `--- a/filename
+++ b/filename
@@ -355,6 +323,23 @@ func TestModifyPatchForRange(t *testing.T) {
last line
+last line
\ No newline at end of file
`,
},
{
testName: "remove newline from end of file, addition only, reversed",
filename: "filename",
firstLineIndex: 9,
lastLineIndex: 9,
reverse: true,
diffText: removeNewlinefromEndOfFile,
expected: `--- a/filename
+++ b/filename
@@ -60,3 +60,4 @@ grape
...
...
...
+last line
\ No newline at end of file
`,
},
{
@@ -362,7 +347,6 @@ func TestModifyPatchForRange(t *testing.T) {
filename: "filename",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: false,
diffText: twoHunks,
expected: `--- a/filename
+++ b/filename
@@ -389,7 +373,6 @@ func TestModifyPatchForRange(t *testing.T) {
filename: "filename",
firstLineIndex: 7,
lastLineIndex: 15,
reverse: false,
diffText: twoHunks,
expected: `--- a/filename
+++ b/filename
@@ -408,32 +391,6 @@ func TestModifyPatchForRange(t *testing.T) {
...
...
...
`,
},
{
testName: "staging part of both hunks, reversed",
filename: "filename",
firstLineIndex: 7,
lastLineIndex: 15,
reverse: true,
diffText: twoHunks,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,4 @@
apple
-orange
...
...
...
@@ -8,8 +7,7 @@ grape
...
...
...
-pear
lemon
...
...
...
`,
},
{
@@ -441,7 +398,6 @@ func TestModifyPatchForRange(t *testing.T) {
filename: "newfile",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: false,
diffText: newFile,
expected: `--- a/newfile
+++ b/newfile
@@ -456,28 +412,12 @@ func TestModifyPatchForRange(t *testing.T) {
filename: "newfile",
firstLineIndex: 6,
lastLineIndex: 7,
reverse: false,
diffText: newFile,
expected: `--- a/newfile
+++ b/newfile
@@ -0,0 +1,2 @@
+apple
+orange
`,
},
{
testName: "adding a new file, reversed",
filename: "newfile",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: true,
diffText: newFile,
expected: `--- a/newfile
+++ b/newfile
@@ -1,3 +0,0 @@
-apple
-orange
-grape
`,
},
{
@@ -485,11 +425,10 @@ func TestModifyPatchForRange(t *testing.T) {
filename: "newfile",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: false,
diffText: addNewlineToPreviouslyEmptyFile,
expected: `--- a/newfile
+++ b/newfile
@@ -0,0 +1,1 @@
@@ -0,0 +1 @@
+new line
\ No newline at end of file
`,
@@ -499,13 +438,49 @@ func TestModifyPatchForRange(t *testing.T) {
filename: "newfile",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: true,
diffText: addNewlineToPreviouslyEmptyFile,
reverse: true,
expected: `--- a/newfile
+++ b/newfile
@@ -1,1 +0,0 @@
-new line
@@ -0,0 +1 @@
+new line
\ No newline at end of file
`,
},
{
testName: "adding part of a hunk",
filename: "filename",
firstLineIndex: 6,
lastLineIndex: 7,
reverse: false,
diffText: twoChangesInOneHunk,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,5 @@
apple
-grape
+kiwi
orange
pear
lemon
`,
},
{
testName: "adding part of a hunk, reverse",
filename: "filename",
firstLineIndex: 6,
lastLineIndex: 7,
reverse: true,
diffText: twoChangesInOneHunk,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,5 @@
apple
-grape
+kiwi
orange
banana
lemon
`,
},
}
@@ -513,37 +488,128 @@ func TestModifyPatchForRange(t *testing.T) {
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
result := ModifiedPatchForRange(nil, s.filename, s.diffText, s.firstLineIndex, s.lastLineIndex, s.reverse, false)
if !assert.Equal(t, s.expected, result) {
fmt.Println(result)
}
lineIndices := ExpandRange(s.firstLineIndex, s.lastLineIndex)
result := Parse(s.diffText).
Transform(TransformOpts{
Reverse: s.reverse,
FileNameOverride: s.filename,
IncludedLineIndices: lineIndices,
}).
FormatPlain()
assert.Equal(t, s.expected, result)
})
}
}
func TestLineNumberOfLine(t *testing.T) {
type scenario struct {
func TestParseAndFormatPlain(t *testing.T) {
scenarios := []struct {
testName string
hunk *PatchHunk
idx int
expected int
}
scenarios := []scenario{
patchStr string
}{
{
testName: "nothing selected",
hunk: newHunk(strings.SplitAfter(exampleHunk, "\n"), 10),
idx: 15,
expected: 3,
testName: "simpleDiff",
patchStr: simpleDiff,
},
{
testName: "addNewlineToEndOfFile",
patchStr: addNewlineToEndOfFile,
},
{
testName: "removeNewlinefromEndOfFile",
patchStr: removeNewlinefromEndOfFile,
},
{
testName: "twoHunks",
patchStr: twoHunks,
},
{
testName: "twoChangesInOneHunk",
patchStr: twoChangesInOneHunk,
},
{
testName: "newFile",
patchStr: newFile,
},
{
testName: "addNewlineToPreviouslyEmptyFile",
patchStr: addNewlineToPreviouslyEmptyFile,
},
{
testName: "exampleHunk",
patchStr: exampleHunk,
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
result := s.hunk.LineNumberOfLine(s.idx)
if !assert.Equal(t, s.expected, result) {
fmt.Println(result)
// here we parse the patch, then format it, and ensure the result
// matches the original patch. Note that unified diffs allow omitting
// the new length in a hunk header if the value is 1, and currently we always
// omit the new length in such cases.
patch := Parse(s.patchStr)
result := formatPlain(patch)
assert.Equal(t, s.patchStr, result)
})
}
}
func TestLineNumberOfLine(t *testing.T) {
type scenario struct {
testName string
patchStr string
indexes []int
expecteds []int
}
scenarios := []scenario{
{
testName: "twoHunks",
patchStr: twoHunks,
// this is really more of a characteristic test than anything.
indexes: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 1000},
expecteds: []int{1, 1, 1, 1, 1, 1, 2, 2, 3, 4, 5, 8, 8, 9, 10, 11, 12, 13, 14, 15, 15, 15, 15, 15, 15},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
for i, idx := range s.indexes {
patch := Parse(s.patchStr)
result := patch.LineNumberOfLine(idx)
assert.Equal(t, s.expecteds[i], result)
}
})
}
}
func TestGetNextStageableLineIndex(t *testing.T) {
type scenario struct {
testName string
patchStr string
indexes []int
expecteds []int
}
scenarios := []scenario{
{
testName: "twoHunks",
patchStr: twoHunks,
indexes: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 1000},
expecteds: []int{6, 6, 6, 6, 6, 6, 6, 7, 15, 15, 15, 15, 15, 15, 15, 15, 16, 16, 16, 16, 16, 16, 16, 16, 16},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
for i, idx := range s.indexes {
patch := Parse(s.patchStr)
result := patch.GetNextChangeIdx(idx)
assert.Equal(t, s.expecteds[i], result)
}
})
}
+156
View File
@@ -0,0 +1,156 @@
package patch
import "github.com/samber/lo"
type patchTransformer struct {
patch *Patch
opts TransformOpts
}
type TransformOpts struct {
// Create a patch that will applied in reverse with `git apply --reverse`.
// This affects how unselected lines are treated when only parts of a hunk
// are selected: usually, for unselected lines we change '-' lines to
// context lines and remove '+' lines, but when Reverse is true we need to
// turn '+' lines into context lines and remove '-' lines.
Reverse bool
// If set, we will replace the original header with one referring to this file name.
// For staging/unstaging lines we don't want the original header because
// it makes git confused e.g. when dealing with deleted/added files
// but with building and applying patches the original header gives git
// information it needs to cleanly apply patches
FileNameOverride string
// The indices of lines that should be included in the patch.
IncludedLineIndices []int
}
func transform(patch *Patch, opts TransformOpts) *Patch {
transformer := &patchTransformer{
patch: patch,
opts: opts,
}
return transformer.transform()
}
// helper function that takes a start and end index and returns a slice of all
// indexes inbetween (inclusive)
func ExpandRange(start int, end int) []int {
expanded := []int{}
for i := start; i <= end; i++ {
expanded = append(expanded, i)
}
return expanded
}
func (self *patchTransformer) transform() *Patch {
header := self.transformHeader()
hunks := self.transformHunks()
return &Patch{
header: header,
hunks: hunks,
}
}
func (self *patchTransformer) transformHeader() []string {
if self.opts.FileNameOverride != "" {
return []string{
"--- a/" + self.opts.FileNameOverride,
"+++ b/" + self.opts.FileNameOverride,
}
} else {
return self.patch.header
}
}
func (self *patchTransformer) transformHunks() []*Hunk {
newHunks := make([]*Hunk, 0, len(self.patch.hunks))
startOffset := 0
var formattedHunk *Hunk
for i, hunk := range self.patch.hunks {
startOffset, formattedHunk = self.transformHunk(
hunk,
startOffset,
self.patch.HunkStartIdx(i),
)
if formattedHunk.containsChanges() {
newHunks = append(newHunks, formattedHunk)
}
}
return newHunks
}
func (self *patchTransformer) transformHunk(hunk *Hunk, startOffset int, firstLineIdx int) (int, *Hunk) {
newLines := self.transformHunkLines(hunk, firstLineIdx)
newNewStart, newStartOffset := self.transformHunkHeader(newLines, hunk.oldStart, startOffset)
newHunk := &Hunk{
bodyLines: newLines,
oldStart: hunk.oldStart,
newStart: newNewStart,
headerContext: hunk.headerContext,
}
return newStartOffset, newHunk
}
func (self *patchTransformer) transformHunkLines(hunk *Hunk, firstLineIdx int) []*PatchLine {
skippedNewlineMessageIndex := -1
newLines := []*PatchLine{}
for i, line := range hunk.bodyLines {
lineIdx := i + firstLineIdx + 1 // plus one for header line
if line.Content == "" {
break
}
isLineSelected := lo.Contains(self.opts.IncludedLineIndices, lineIdx)
if isLineSelected || (line.Kind == NEWLINE_MESSAGE && skippedNewlineMessageIndex != lineIdx) || line.Kind == CONTEXT {
newLines = append(newLines, line)
continue
}
if (line.Kind == DELETION && !self.opts.Reverse) || (line.Kind == ADDITION && self.opts.Reverse) {
content := " " + line.Content[1:]
newLines = append(newLines, &PatchLine{
Kind: CONTEXT,
Content: content,
})
continue
}
if line.Kind == ADDITION {
// we don't want to include the 'newline at end of file' line if it involves an addition we're not including
skippedNewlineMessageIndex = lineIdx + 1
}
}
return newLines
}
func (self *patchTransformer) transformHunkHeader(newBodyLines []*PatchLine, oldStart int, startOffset int) (int, int) {
oldLength := nLinesWithKind(newBodyLines, []PatchLineKind{CONTEXT, DELETION})
newLength := nLinesWithKind(newBodyLines, []PatchLineKind{CONTEXT, ADDITION})
var newStartOffset int
// if the hunk went from zero to positive length, we need to increment the starting point by one
// if the hunk went from positive to zero length, we need to decrement the starting point by one
if oldLength == 0 {
newStartOffset = 1
} else if newLength == 0 {
newStartOffset = -1
} else {
newStartOffset = 0
}
newStart := oldStart + startOffset + newStartOffset
newStartOffset = startOffset + newLength - oldLength
return newStart, newStartOffset
}
+2 -4
View File
@@ -6,9 +6,7 @@ package config
// GetPlatformDefaultConfig gets the defaults for the platform
func GetPlatformDefaultConfig() OSConfig {
return OSConfig{
EditCommand: ``,
EditCommandTemplate: "",
OpenCommand: "open -- {{filename}}",
OpenLinkCommand: "open {{link}}",
Open: "open -- {{filename}}",
OpenLink: "open {{link}}",
}
}
+20 -9
View File
@@ -2,6 +2,7 @@ package config
import (
"io/ioutil"
"os"
"strings"
)
@@ -10,21 +11,31 @@ func isWSL() bool {
return err == nil && strings.Contains(string(data), "microsoft")
}
func isContainer() bool {
data, err := ioutil.ReadFile("/proc/1/cgroup")
if strings.Contains(string(data), "docker") ||
strings.Contains(string(data), "/lxc/") ||
[]string{string(data)}[0] != "systemd" &&
[]string{string(data)}[0] != "init" ||
os.Getenv("container") != "" {
return err == nil && true
}
return err == nil && false
}
// GetPlatformDefaultConfig gets the defaults for the platform
func GetPlatformDefaultConfig() OSConfig {
if isWSL() {
if isWSL() && !isContainer() {
return OSConfig{
EditCommand: ``,
EditCommandTemplate: "",
OpenCommand: `powershell.exe start explorer.exe {{filename}} >/dev/null`,
OpenLinkCommand: `powershell.exe start {{link}} >/dev/null`,
Open: `powershell.exe start explorer.exe {{filename}} >/dev/null`,
OpenLink: `powershell.exe start {{link}} >/dev/null`,
}
}
return OSConfig{
EditCommand: ``,
EditCommandTemplate: "",
OpenCommand: `xdg-open {{filename}} >/dev/null`,
OpenLinkCommand: `xdg-open {{link}} >/dev/null`,
Open: `xdg-open {{filename}} >/dev/null`,
OpenLink: `xdg-open {{link}} >/dev/null`,
}
}
+2 -4
View File
@@ -3,9 +3,7 @@ package config
// GetPlatformDefaultConfig gets the defaults for the platform
func GetPlatformDefaultConfig() OSConfig {
return OSConfig{
EditCommand: ``,
EditCommandTemplate: "",
OpenCommand: `start "" {{filename}}`,
OpenLinkCommand: `start "" {{link}}`,
Open: `start "" {{filename}}`,
OpenLink: `start "" {{link}}`,
}
}
+111
View File
@@ -0,0 +1,111 @@
package config
func GetEditTemplate(osConfig *OSConfig, guessDefaultEditor func() string) (string, bool) {
preset := getPreset(osConfig, guessDefaultEditor)
template := osConfig.Edit
if template == "" {
template = preset.editTemplate
}
return template, getEditInTerminal(osConfig, preset)
}
func GetEditAtLineTemplate(osConfig *OSConfig, guessDefaultEditor func() string) (string, bool) {
preset := getPreset(osConfig, guessDefaultEditor)
template := osConfig.EditAtLine
if template == "" {
template = preset.editAtLineTemplate
}
return template, getEditInTerminal(osConfig, preset)
}
func GetEditAtLineAndWaitTemplate(osConfig *OSConfig, guessDefaultEditor func() string) string {
preset := getPreset(osConfig, guessDefaultEditor)
template := osConfig.EditAtLineAndWait
if template == "" {
template = preset.editAtLineAndWaitTemplate
}
return template
}
type editPreset struct {
editTemplate string
editAtLineTemplate string
editAtLineAndWaitTemplate string
editInTerminal bool
}
func getPreset(osConfig *OSConfig, guessDefaultEditor func() string) *editPreset {
presets := map[string]*editPreset{
"vi": standardTerminalEditorPreset("vi"),
"vim": standardTerminalEditorPreset("vim"),
"nvim": standardTerminalEditorPreset("nvim"),
"emacs": standardTerminalEditorPreset("emacs"),
"nano": standardTerminalEditorPreset("nano"),
"kakoune": standardTerminalEditorPreset("kakoune"),
"vscode": {
editTemplate: "code --reuse-window -- {{filename}}",
editAtLineTemplate: "code --reuse-window --goto -- {{filename}}:{{line}}",
editAtLineAndWaitTemplate: "code --reuse-window --goto --wait -- {{filename}}:{{line}}",
editInTerminal: false,
},
"sublime": {
editTemplate: "subl -- {{filename}}",
editAtLineTemplate: "subl -- {{filename}}:{{line}}",
editAtLineAndWaitTemplate: "subl --wait -- {{filename}}:{{line}}",
editInTerminal: false,
},
"bbedit": {
editTemplate: "bbedit -- {{filename}}",
editAtLineTemplate: "bbedit +{{line}} -- {{filename}}",
editAtLineAndWaitTemplate: "bbedit +{{line}} --wait -- {{filename}}",
editInTerminal: false,
},
"xcode": {
editTemplate: "xed -- {{filename}}",
editAtLineTemplate: "xed --line {{line}} -- {{filename}}",
editAtLineAndWaitTemplate: "xed --line {{line}} --wait -- {{filename}}",
editInTerminal: false,
},
}
// Some of our presets have a different name than the editor they are using.
editorToPreset := map[string]string{
"kak": "kakoune",
"code": "vscode",
"subl": "sublime",
"xed": "xcode",
}
presetName := osConfig.EditPreset
if presetName == "" {
defaultEditor := guessDefaultEditor()
if presets[defaultEditor] != nil {
presetName = defaultEditor
} else if p := editorToPreset[defaultEditor]; p != "" {
presetName = p
}
}
if presetName == "" || presets[presetName] == nil {
presetName = "vim"
}
return presets[presetName]
}
func standardTerminalEditorPreset(editor string) *editPreset {
return &editPreset{
editTemplate: editor + " -- {{filename}}",
editAtLineTemplate: editor + " +{{line}} -- {{filename}}",
editAtLineAndWaitTemplate: editor + " +{{line}} -- {{filename}}",
editInTerminal: true,
}
}
func getEditInTerminal(osConfig *OSConfig, preset *editPreset) bool {
if osConfig.EditInTerminal != nil {
return *osConfig.EditInTerminal
}
return preset.editInTerminal
}
+126
View File
@@ -0,0 +1,126 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetEditTemplate(t *testing.T) {
trueVal := true
scenarios := []struct {
name string
osConfig *OSConfig
guessDefaultEditor func() string
expectedEditTemplate string
expectedEditAtLineTemplate string
expectedEditAtLineAndWaitTemplate string
expectedEditInTerminal bool
}{
{
"Default template is vim",
&OSConfig{},
func() string { return "" },
"vim -- {{filename}}",
"vim +{{line}} -- {{filename}}",
"vim +{{line}} -- {{filename}}",
true,
},
{
"Setting a preset",
&OSConfig{
EditPreset: "vscode",
},
func() string { return "" },
"code --reuse-window -- {{filename}}",
"code --reuse-window --goto -- {{filename}}:{{line}}",
"code --reuse-window --goto --wait -- {{filename}}:{{line}}",
false,
},
{
"Setting a preset wins over guessed editor",
&OSConfig{
EditPreset: "vscode",
},
func() string { return "nano" },
"code --reuse-window -- {{filename}}",
"code --reuse-window --goto -- {{filename}}:{{line}}",
"code --reuse-window --goto --wait -- {{filename}}:{{line}}",
false,
},
{
"Overriding a preset with explicit config (edit)",
&OSConfig{
EditPreset: "vscode",
Edit: "myeditor {{filename}}",
EditInTerminal: &trueVal,
},
func() string { return "" },
"myeditor {{filename}}",
"code --reuse-window --goto -- {{filename}}:{{line}}",
"code --reuse-window --goto --wait -- {{filename}}:{{line}}",
true,
},
{
"Overriding a preset with explicit config (edit at line)",
&OSConfig{
EditPreset: "vscode",
EditAtLine: "myeditor --line={{line}} {{filename}}",
EditInTerminal: &trueVal,
},
func() string { return "" },
"code --reuse-window -- {{filename}}",
"myeditor --line={{line}} {{filename}}",
"code --reuse-window --goto --wait -- {{filename}}:{{line}}",
true,
},
{
"Overriding a preset with explicit config (edit at line and wait)",
&OSConfig{
EditPreset: "vscode",
EditAtLineAndWait: "myeditor --line={{line}} -w {{filename}}",
EditInTerminal: &trueVal,
},
func() string { return "" },
"code --reuse-window -- {{filename}}",
"code --reuse-window --goto -- {{filename}}:{{line}}",
"myeditor --line={{line}} -w {{filename}}",
true,
},
{
"Unknown preset name",
&OSConfig{
EditPreset: "thisPresetDoesNotExist",
},
func() string { return "" },
"vim -- {{filename}}",
"vim +{{line}} -- {{filename}}",
"vim +{{line}} -- {{filename}}",
true,
},
{
"Guessing a preset from guessed editor",
&OSConfig{},
func() string { return "emacs" },
"emacs -- {{filename}}",
"emacs +{{line}} -- {{filename}}",
"emacs +{{line}} -- {{filename}}",
true,
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
template, editInTerminal := GetEditTemplate(s.osConfig, s.guessDefaultEditor)
assert.Equal(t, s.expectedEditTemplate, template)
assert.Equal(t, s.expectedEditInTerminal, editInTerminal)
template, editInTerminal = GetEditAtLineTemplate(s.osConfig, s.guessDefaultEditor)
assert.Equal(t, s.expectedEditAtLineTemplate, template)
assert.Equal(t, s.expectedEditInTerminal, editInTerminal)
template = GetEditAtLineAndWaitTemplate(s.osConfig, s.guessDefaultEditor)
assert.Equal(t, s.expectedEditAtLineAndWaitTemplate, template)
})
}
}
+54 -16
View File
@@ -52,6 +52,7 @@ type GuiConfig struct {
SplitDiff string `yaml:"splitDiff"`
SkipRewordInEditorWarning bool `yaml:"skipRewordInEditorWarning"`
WindowSize string `yaml:"windowSize"`
Border string `yaml:"border"`
}
type ThemeConfig struct {
@@ -137,7 +138,6 @@ type KeybindingUniversalConfig struct {
Quit string `yaml:"quit"`
QuitAlt1 string `yaml:"quit-alt1"`
Return string `yaml:"return"`
ReturnAlt1 string `yaml:"return-alt1"`
QuitWithoutChangingDirectory string `yaml:"quitWithoutChangingDirectory"`
TogglePanel string `yaml:"togglePanel"`
PrevItem string `yaml:"prevItem"`
@@ -165,7 +165,7 @@ type KeybindingUniversalConfig struct {
Select string `yaml:"select"`
GoInto string `yaml:"goInto"`
Confirm string `yaml:"confirm"`
ConfirmAlt1 string `yaml:"confirm-alt1"`
ConfirmInEditor string `yaml:"confirmInEditor"`
Remove string `yaml:"remove"`
New string `yaml:"new"`
Edit string `yaml:"edit"`
@@ -178,8 +178,8 @@ type KeybindingUniversalConfig struct {
ScrollDownMainAlt2 string `yaml:"scrollDownMain-alt2"`
ExecuteCustomCommand string `yaml:"executeCustomCommand"`
CreateRebaseOptionsMenu string `yaml:"createRebaseOptionsMenu"`
PushFiles string `yaml:"pushFiles"`
PullFiles string `yaml:"pullFiles"`
Push string `yaml:"pushFiles"` // 'Files' appended for legacy reasons
Pull string `yaml:"pullFiles"` // 'Files' appended for legacy reasons
Refresh string `yaml:"refresh"`
CreatePatchOptionsMenu string `yaml:"createPatchOptionsMenu"`
NextTab string `yaml:"nextTab"`
@@ -194,7 +194,6 @@ type KeybindingUniversalConfig struct {
CopyToClipboard string `yaml:"copyToClipboard"`
OpenRecentRepos string `yaml:"openRecentRepos"`
SubmitEditorText string `yaml:"submitEditorText"`
AppendNewline string `yaml:"appendNewline"`
ExtrasMenu string `yaml:"extrasMenu"`
ToggleWhitespaceInDiffView string `yaml:"toggleWhitespaceInDiffView"`
IncreaseContextInDiffView string `yaml:"increaseContextInDiffView"`
@@ -235,6 +234,7 @@ type KeybindingBranchesConfig struct {
MergeIntoCurrentBranch string `yaml:"mergeIntoCurrentBranch"`
ViewGitFlowOptions string `yaml:"viewGitFlowOptions"`
FastForward string `yaml:"fastForward"`
CreateTag string `yaml:"createTag"`
PushTag string `yaml:"pushTag"`
SetUpstream string `yaml:"setUpstream"`
FetchRemote string `yaml:"fetchRemote"`
@@ -257,7 +257,7 @@ type KeybindingCommitsConfig struct {
CherryPickCopy string `yaml:"cherryPickCopy"`
CherryPickCopyRange string `yaml:"cherryPickCopyRange"`
PasteCommits string `yaml:"pasteCommits"`
TagCommit string `yaml:"tagCommit"`
CreateTag string `yaml:"tagCommit"`
CheckoutCommit string `yaml:"checkoutCommit"`
ResetCherryPick string `yaml:"resetCherryPick"`
CopyCommitAttributeToClipboard string `yaml:"copyCommitAttributeToClipboard"`
@@ -291,16 +291,54 @@ type KeybindingSubmodulesConfig struct {
// OSConfig contains config on the level of the os
type OSConfig struct {
// EditCommand is the command for editing a file
// Command for editing a file. Should contain "{{filename}}".
Edit string `yaml:"edit,omitempty"`
// Command for editing a file at a given line number. Should contain
// "{{filename}}", and may optionally contain "{{line}}".
EditAtLine string `yaml:"editAtLine,omitempty"`
// Same as EditAtLine, except that the command needs to wait until the
// window is closed.
EditAtLineAndWait string `yaml:"editAtLineAndWait,omitempty"`
// Whether the given edit commands use the terminal. Used to decide whether
// lazygit needs to suspend to the background before calling the editor.
// Pointer to bool so that we can distinguish unset (nil) from false.
EditInTerminal *bool `yaml:"editInTerminal,omitempty"`
// A built-in preset that sets all of the above settings. Supported presets
// are defined in the getPreset function in editor_presets.go.
EditPreset string `yaml:"editPreset,omitempty"`
// Command for opening a file, as if the file is double-clicked. Should
// contain "{{filename}}", but doesn't support "{{line}}".
Open string `yaml:"open,omitempty"`
// Command for opening a link. Should contain "{{link}}".
OpenLink string `yaml:"openLink,omitempty"`
// --------
// The following configs are all deprecated and kept for backward
// compatibility. They will be removed in the future.
// EditCommand is the command for editing a file.
// Deprecated: use Edit instead. Note that semantics are different:
// EditCommand is just the command itself, whereas Edit contains a
// "{{filename}}" variable.
EditCommand string `yaml:"editCommand,omitempty"`
// EditCommandTemplate is the command template for editing a file
// Deprecated: use EditAtLine instead.
EditCommandTemplate string `yaml:"editCommandTemplate,omitempty"`
// OpenCommand is the command for opening a file
// Deprecated: use Open instead.
OpenCommand string `yaml:"openCommand,omitempty"`
// OpenCommand is the command for opening a link
// OpenLinkCommand is the command for opening a link
// Deprecated: use OpenLink instead.
OpenLinkCommand string `yaml:"openLinkCommand,omitempty"`
}
@@ -381,6 +419,7 @@ func GetDefaultConfig() *UserConfig {
CommandLogSize: 8,
SplitDiff: "auto",
SkipRewordInEditorWarning: false,
Border: "single",
},
Git: GitConfig{
Paging: PagingConfig{
@@ -426,7 +465,6 @@ func GetDefaultConfig() *UserConfig {
Quit: "q",
QuitAlt1: "<c-c>",
Return: "<esc>",
ReturnAlt1: "",
QuitWithoutChangingDirectory: "Q",
TogglePanel: "<tab>",
PrevItem: "<up>",
@@ -449,12 +487,12 @@ func GetDefaultConfig() *UserConfig {
NextMatch: "n",
PrevMatch: "N",
StartSearch: "/",
OptionMenu: "x",
OptionMenu: "",
OptionMenuAlt1: "?",
Select: "<space>",
GoInto: "<enter>",
Confirm: "<enter>",
ConfirmAlt1: "y",
ConfirmInEditor: "<a-enter>",
Remove: "d",
New: "n",
Edit: "e",
@@ -468,8 +506,8 @@ func GetDefaultConfig() *UserConfig {
ScrollDownMainAlt2: "<c-d>",
ExecuteCustomCommand: ":",
CreateRebaseOptionsMenu: "m",
PushFiles: "P",
PullFiles: "p",
Push: "P",
Pull: "p",
Refresh: "R",
CreatePatchOptionsMenu: "<c-p>",
NextTab: "]",
@@ -483,7 +521,6 @@ func GetDefaultConfig() *UserConfig {
DiffingMenuAlt: "<c-e>",
CopyToClipboard: "<c-o>",
SubmitEditorText: "<enter>",
AppendNewline: "<a-enter>",
ExtrasMenu: "@",
ToggleWhitespaceInDiffView: "<c-w>",
IncreaseContextInDiffView: "}",
@@ -521,6 +558,7 @@ func GetDefaultConfig() *UserConfig {
MergeIntoCurrentBranch: "M",
ViewGitFlowOptions: "i",
FastForward: "f",
CreateTag: "T",
PushTag: "P",
SetUpstream: "u",
FetchRemote: "f",
@@ -542,7 +580,7 @@ func GetDefaultConfig() *UserConfig {
CherryPickCopy: "c",
CherryPickCopyRange: "C",
PasteCommits: "v",
TagCommit: "T",
CreateTag: "T",
CheckoutCommit: "<space>",
ResetCherryPick: "<c-R>",
CopyCommitAttributeToClipboard: "y",
@@ -570,7 +608,7 @@ func GetDefaultConfig() *UserConfig {
BulkMenu: "b",
},
},
OS: GetPlatformDefaultConfig(),
OS: OSConfig{},
DisableStartupPopups: false,
CustomCommands: []CustomCommand(nil),
Services: map[string]string(nil),
+1 -1
View File
@@ -1,4 +1,4 @@
package test
package fakes
import (
"fmt"
+3 -3
View File
@@ -79,7 +79,7 @@ func (gui *Gui) getRandomTip() string {
// keybindings and lazygit-specific advice
fmt.Sprintf(
"To force push, press '%s' and then if the push is rejected you will be asked if you want to force push",
formattedKey(config.Universal.PushFiles),
formattedKey(config.Universal.Push),
),
fmt.Sprintf(
"To filter commits by path, press '%s'",
@@ -191,7 +191,7 @@ func (gui *Gui) getRandomTip() string {
),
}
rand.Seed(time.Now().UnixNano())
randomIndex := rand.Intn(len(tips))
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
randomIndex := rnd.Intn(len(tips))
return tips[randomIndex]
}
+2 -1
View File
@@ -15,7 +15,8 @@ func (gui *Gui) commitFilesRenderToMain() error {
to := ref.RefName()
from, reverse := gui.State.Modes.Diffing.GetFromAndReverseArgsForDiff(ref.ParentRefName())
cmdObj := gui.git.WorkingTree.ShowFileDiffCmdObj(from, to, reverse, node.GetPath(), false)
cmdObj := gui.git.WorkingTree.ShowFileDiffCmdObj(from, to, reverse, node.GetPath(), false,
gui.IgnoreWhitespaceInDiffView)
task := types.NewRunPtyTask(cmdObj.GetCmd())
pair := gui.c.MainViewPairs().Normal
-1
View File
@@ -15,7 +15,6 @@ func (gui *Gui) handleCommitMessageFocused() error {
map[string]string{
"keyBindClose": keybindings.Label(gui.c.UserConfig.Keybinding.Universal.Return),
"keyBindConfirm": keybindings.Label(gui.c.UserConfig.Keybinding.Universal.Confirm),
"keyBindNewLine": keybindings.Label(gui.c.UserConfig.Keybinding.Universal.AppendNewline),
},
)
+12 -3
View File
@@ -1,6 +1,7 @@
package gui
import (
"github.com/fsmiamoto/git-todo-parser/todo"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
@@ -34,8 +35,16 @@ func (gui *Gui) branchCommitsRenderToMain() error {
commit := gui.State.Contexts.LocalCommits.GetSelected()
if commit == nil {
task = types.NewRenderStringTask(gui.c.Tr.NoCommitsThisBranch)
} else if commit.Action == todo.UpdateRef {
task = types.NewRenderStringTask(
utils.ResolvePlaceholderString(
gui.c.Tr.UpdateRefHere,
map[string]string{
"ref": commit.Name,
}))
} else {
cmdObj := gui.git.Commit.ShowCmdObj(commit.Sha, gui.State.Modes.Filtering.GetPath())
cmdObj := gui.git.Commit.ShowCmdObj(commit.Sha, gui.State.Modes.Filtering.GetPath(),
gui.IgnoreWhitespaceInDiffView)
task = types.NewRunPtyTask(cmdObj.GetCmd())
}
@@ -50,8 +59,8 @@ func (gui *Gui) branchCommitsRenderToMain() error {
}
func (gui *Gui) secondaryPatchPanelUpdateOpts() *types.ViewUpdateOpts {
if gui.git.Patch.PatchManager.Active() {
patch := gui.git.Patch.PatchManager.RenderAggregatedPatchColored(false)
if gui.git.Patch.PatchBuilder.Active() {
patch := gui.git.Patch.PatchBuilder.RenderAggregatedPatch(false)
return &types.ViewUpdateOpts{
Task: types.NewRenderStringWithoutScrollTask(patch),
+4 -22
View File
@@ -78,17 +78,11 @@ func (gui *Gui) getMessageHeight(wrap bool, message string, width int) int {
return lineCount
}
func (gui *Gui) getConfirmationPanelDimensions(wrap bool, prompt string) (int, int, int, int) {
panelWidth := gui.getConfirmationPanelWidth()
panelHeight := gui.getMessageHeight(wrap, prompt, panelWidth)
return gui.getConfirmationPanelDimensionsAux(panelWidth, panelHeight)
func (gui *Gui) getPopupPanelDimensionsForContentHeight(panelWidth, contentHeight int) (int, int, int, int) {
return gui.getPopupPanelDimensionsAux(panelWidth, contentHeight)
}
func (gui *Gui) getConfirmationPanelDimensionsForContentHeight(panelWidth, contentHeight int) (int, int, int, int) {
return gui.getConfirmationPanelDimensionsAux(panelWidth, contentHeight)
}
func (gui *Gui) getConfirmationPanelDimensionsAux(panelWidth int, panelHeight int) (int, int, int, int) {
func (gui *Gui) getPopupPanelDimensionsAux(panelWidth int, panelHeight int) (int, int, int, int) {
width, height := gui.g.Size()
if panelHeight > height*3/4 {
panelHeight = height * 3 / 4
@@ -186,7 +180,7 @@ func (gui *Gui) createPopupPanel(ctx context.Context, opts types.CreatePopupPane
}
confirmationView := gui.Views.Confirmation
confirmationView.Editable = opts.Editable
confirmationView.Editor = gocui.EditorFunc(gui.defaultEditor)
confirmationView.Editor = gocui.EditorFunc(gui.promptEditor)
if opts.Editable {
textArea := confirmationView.TextArea
@@ -241,11 +235,6 @@ func (gui *Gui) setKeyBindings(cancel context.CancelFunc, opts types.CreatePopup
Key: keybindings.GetKey(keybindingConfig.Universal.Confirm),
Handler: onConfirm,
},
{
ViewName: "confirmation",
Key: keybindings.GetKey(keybindingConfig.Universal.ConfirmAlt1),
Handler: onConfirm,
},
{
ViewName: "confirmation",
Key: keybindings.GetKey(keybindingConfig.Universal.Return),
@@ -266,11 +255,6 @@ func (gui *Gui) setKeyBindings(cancel context.CancelFunc, opts types.CreatePopup
Key: keybindings.GetKey(keybindingConfig.Universal.Confirm),
Handler: onSuggestionConfirm,
},
{
ViewName: "suggestions",
Key: keybindings.GetKey(keybindingConfig.Universal.ConfirmAlt1),
Handler: onSuggestionConfirm,
},
{
ViewName: "suggestions",
Key: keybindings.GetKey(keybindingConfig.Universal.Return),
@@ -295,10 +279,8 @@ func (gui *Gui) setKeyBindings(cancel context.CancelFunc, opts types.CreatePopup
func (gui *Gui) clearConfirmationViewKeyBindings() {
keybindingConfig := gui.c.UserConfig.Keybinding
_ = gui.g.DeleteKeybinding("confirmation", keybindings.GetKey(keybindingConfig.Universal.Confirm), gocui.ModNone)
_ = gui.g.DeleteKeybinding("confirmation", keybindings.GetKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone)
_ = gui.g.DeleteKeybinding("confirmation", keybindings.GetKey(keybindingConfig.Universal.Return), gocui.ModNone)
_ = gui.g.DeleteKeybinding("suggestions", keybindings.GetKey(keybindingConfig.Universal.Confirm), gocui.ModNone)
_ = gui.g.DeleteKeybinding("suggestions", keybindings.GetKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone)
_ = gui.g.DeleteKeybinding("suggestions", keybindings.GetKey(keybindingConfig.Universal.Return), gocui.ModNone)
}
+52 -6
View File
@@ -9,6 +9,7 @@ import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/samber/lo"
)
// This file is for the management of contexts. There is a context stack such that
@@ -52,7 +53,7 @@ func (gui *Gui) pushContext(c types.Context, opts types.OnFocusOpts) error {
return nil
}
contextsToDeactivate := gui.pushToContextStack(c)
contextsToDeactivate, contextToActivate := gui.pushToContextStack(c)
for _, contextToDeactivate := range contextsToDeactivate {
if err := gui.deactivateContext(contextToDeactivate, types.OnFocusLostOpts{NewContextKey: c.GetKey()}); err != nil {
@@ -60,16 +61,28 @@ func (gui *Gui) pushContext(c types.Context, opts types.OnFocusOpts) error {
}
}
return gui.activateContext(c, opts)
if contextToActivate == nil {
return nil
}
return gui.activateContext(contextToActivate, opts)
}
// Adjusts the context stack based on the context that's being pushed and returns contexts to deactivate
func (gui *Gui) pushToContextStack(c types.Context) []types.Context {
// Adjusts the context stack based on the context that's being pushed and
// returns (contexts to deactivate, context to activate)
func (gui *Gui) pushToContextStack(c types.Context) ([]types.Context, types.Context) {
contextsToDeactivate := []types.Context{}
gui.State.ContextManager.Lock()
defer gui.State.ContextManager.Unlock()
if len(gui.State.ContextManager.ContextStack) > 0 &&
c == gui.State.ContextManager.ContextStack[len(gui.State.ContextManager.ContextStack)-1] {
// Context being pushed is already on top of the stack: nothing to
// deactivate or activate
return contextsToDeactivate, nil
}
if len(gui.State.ContextManager.ContextStack) == 0 {
gui.State.ContextManager.ContextStack = append(gui.State.ContextManager.ContextStack, c)
} else if c.GetKind() == types.SIDE_CONTEXT {
@@ -78,12 +91,15 @@ func (gui *Gui) pushToContextStack(c types.Context) []types.Context {
gui.State.ContextManager.ContextStack = []types.Context{c}
} else if c.GetKind() == types.MAIN_CONTEXT {
// if we're switching to a main context, remove all other main contexts in the stack
contextsToKeep := []types.Context{}
for _, stackContext := range gui.State.ContextManager.ContextStack {
if stackContext.GetKind() == types.MAIN_CONTEXT {
contextsToDeactivate = append(contextsToDeactivate, stackContext)
} else {
contextsToKeep = append(contextsToKeep, stackContext)
}
}
gui.State.ContextManager.ContextStack = []types.Context{c}
gui.State.ContextManager.ContextStack = append(contextsToKeep, c)
} else {
topContext := gui.currentContextWithoutLock()
@@ -105,7 +121,7 @@ func (gui *Gui) pushToContextStack(c types.Context) []types.Context {
}
}
return contextsToDeactivate
return contextsToDeactivate, c
}
func (gui *Gui) popContext() error {
@@ -131,6 +147,36 @@ func (gui *Gui) popContext() error {
return gui.activateContext(newContext, types.OnFocusOpts{})
}
func (gui *Gui) removeContexts(contextsToRemove []types.Context) error {
gui.State.ContextManager.Lock()
if len(gui.State.ContextManager.ContextStack) == 1 {
gui.State.ContextManager.Unlock()
return nil
}
rest := lo.Filter(gui.State.ContextManager.ContextStack, func(context types.Context, _ int) bool {
for _, contextToRemove := range contextsToRemove {
if context.GetKey() == contextToRemove.GetKey() {
return false
}
}
return true
})
gui.State.ContextManager.ContextStack = rest
contextToActivate := rest[len(rest)-1]
gui.State.ContextManager.Unlock()
for _, context := range contextsToRemove {
if err := gui.deactivateContext(context, types.OnFocusLostOpts{NewContextKey: contextToActivate.GetKey()}); err != nil {
return err
}
}
// activate the item at the top of the stack
return gui.activateContext(contextToActivate, types.OnFocusOpts{})
}
func (gui *Gui) deactivateContext(c types.Context, opts types.OnFocusLostOpts) error {
view, _ := gui.g.View(c.GetViewName())
+3
View File
@@ -20,6 +20,7 @@ type BaseContext struct {
focusable bool
transient bool
hasControlledBounds bool
highlightOnFocus bool
*ParentContextMgr
}
@@ -34,6 +35,7 @@ type NewBaseContextOpts struct {
Focusable bool
Transient bool
HasUncontrolledBounds bool // negating for the sake of making false the default
HighlightOnFocus bool
OnGetOptionsMap func() map[string]string
}
@@ -52,6 +54,7 @@ func NewBaseContext(opts NewBaseContextOpts) *BaseContext {
focusable: opts.Focusable,
transient: opts.Transient,
hasControlledBounds: hasControlledBounds,
highlightOnFocus: opts.HighlightOnFocus,
ParentContextMgr: &ParentContextMgr{},
viewTrait: viewTrait,
}
+95
View File
@@ -0,0 +1,95 @@
package context
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type CommitMessageContext struct {
types.Context
viewModel *CommitMessageViewModel
}
// when selectedIndex (see below) is set to this value, it means that we're not
// currently viewing a commit message of an existing commit: instead we're making our own
// new commit message
const NoCommitIndex = -1
type CommitMessageViewModel struct {
// index of the commit message, where -1 is 'no commit', 0 is the HEAD commit, 1
// is the prior commit, and so on
selectedindex int
// if true, then upon escaping from the commit message panel, we will preserve
// the message so that it's still shown next time we open the panel
preserveMessage bool
// the full preserved message (combined summary and description)
preservedMessage string
// invoked when pressing enter in the commit message panel
onConfirm func(string) error
// The message typed in before cycling through history
// We store this separately to 'preservedMessage' because 'preservedMessage'
// is specifically for committing staged files and we don't want this affected
// by cycling through history in the context of rewording an old commit.
historyMessage string
}
func NewCommitMessageContext(
view *gocui.View,
opts ContextCallbackOpts,
) *CommitMessageContext {
viewModel := &CommitMessageViewModel{}
return &CommitMessageContext{
viewModel: viewModel,
Context: NewSimpleContext(
NewBaseContext(NewBaseContextOpts{
Kind: types.PERSISTENT_POPUP,
View: view,
WindowName: "commitMessage",
Key: COMMIT_MESSAGE_CONTEXT_KEY,
Focusable: true,
HasUncontrolledBounds: true,
}),
opts,
),
}
}
func (self *CommitMessageContext) SetSelectedIndex(value int) {
self.viewModel.selectedindex = value
}
func (self *CommitMessageContext) GetSelectedIndex() int {
return self.viewModel.selectedindex
}
func (self *CommitMessageContext) GetPreserveMessage() bool {
return self.viewModel.preserveMessage
}
func (self *CommitMessageContext) GetPreservedMessage() string {
return self.viewModel.preservedMessage
}
func (self *CommitMessageContext) SetPreservedMessage(message string) {
self.viewModel.preservedMessage = message
}
func (self *CommitMessageContext) GetHistoryMessage() string {
return self.viewModel.historyMessage
}
func (self *CommitMessageContext) SetHistoryMessage(message string) {
self.viewModel.historyMessage = message
}
func (self *CommitMessageContext) OnConfirm(message string) error {
return self.viewModel.onConfirm(message)
}
func (self *CommitMessageContext) SetPanelState(index int, title string, preserveMessage bool, onConfirm func(string) error) {
self.viewModel.selectedindex = index
self.viewModel.preserveMessage = preserveMessage
self.viewModel.onConfirm = onConfirm
self.GetView().Title = title
}
+11 -8
View File
@@ -33,13 +33,14 @@ const (
INFORMATION_CONTEXT_KEY types.ContextKey = "information"
LIMIT_CONTEXT_KEY types.ContextKey = "limit"
MENU_CONTEXT_KEY types.ContextKey = "menu"
CONFIRMATION_CONTEXT_KEY types.ContextKey = "confirmation"
SEARCH_CONTEXT_KEY types.ContextKey = "search"
COMMIT_MESSAGE_CONTEXT_KEY types.ContextKey = "commitMessage"
SUBMODULES_CONTEXT_KEY types.ContextKey = "submodules"
SUGGESTIONS_CONTEXT_KEY types.ContextKey = "suggestions"
COMMAND_LOG_CONTEXT_KEY types.ContextKey = "cmdLog"
MENU_CONTEXT_KEY types.ContextKey = "menu"
CONFIRMATION_CONTEXT_KEY types.ContextKey = "confirmation"
SEARCH_CONTEXT_KEY types.ContextKey = "search"
COMMIT_MESSAGE_CONTEXT_KEY types.ContextKey = "commitMessage"
COMMIT_DESCRIPTION_CONTEXT_KEY types.ContextKey = "commitDescription"
SUBMODULES_CONTEXT_KEY types.ContextKey = "submodules"
SUGGESTIONS_CONTEXT_KEY types.ContextKey = "suggestions"
COMMAND_LOG_CONTEXT_KEY types.ContextKey = "cmdLog"
)
var AllContextKeys = []types.ContextKey{
@@ -97,7 +98,8 @@ type ContextTree struct {
CustomPatchBuilderSecondary types.Context
MergeConflicts *MergeConflictsContext
Confirmation types.Context
CommitMessage types.Context
CommitMessage *CommitMessageContext
CommitDescription types.Context
CommandLog types.Context
// display contexts
@@ -129,6 +131,7 @@ func (self *ContextTree) Flatten() []types.Context {
self.Menu,
self.Confirmation,
self.CommitMessage,
self.CommitDescription,
self.MergeConflicts,
self.StagingSecondary,
+10 -9
View File
@@ -42,12 +42,13 @@ func NewMergeConflictsContext(
mutex: &deadlock.Mutex{},
Context: NewSimpleContext(
NewBaseContext(NewBaseContextOpts{
Kind: types.MAIN_CONTEXT,
View: view,
WindowName: "main",
Key: MERGE_CONFLICTS_CONTEXT_KEY,
OnGetOptionsMap: getOptionsMap,
Focusable: true,
Kind: types.MAIN_CONTEXT,
View: view,
WindowName: "main",
Key: MERGE_CONFLICTS_CONTEXT_KEY,
OnGetOptionsMap: getOptionsMap,
Focusable: true,
HighlightOnFocus: true,
}),
opts,
),
@@ -77,7 +78,7 @@ func (self *MergeConflictsContext) IsUserScrolling() bool {
func (self *MergeConflictsContext) RenderAndFocus(isFocused bool) error {
self.setContent(isFocused)
self.focusSelection()
self.FocusSelection()
self.c.Render()
@@ -104,9 +105,9 @@ func (self *MergeConflictsContext) setContent(isFocused bool) {
self.GetView().SetContent(self.GetContentToRender(isFocused))
}
func (self *MergeConflictsContext) focusSelection() {
func (self *MergeConflictsContext) FocusSelection() {
if !self.IsUserScrolling() {
_ = self.GetView().SetOrigin(self.GetView().OriginX(), self.GetOriginY())
_ = self.GetView().SetOriginY(self.GetOriginY())
}
}
+12 -12
View File
@@ -37,11 +37,12 @@ func NewPatchExplorerContext(
mutex: &deadlock.Mutex{},
getIncludedLineIndices: getIncludedLineIndices,
SimpleContext: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
View: view,
WindowName: windowName,
Key: key,
Kind: types.MAIN_CONTEXT,
Focusable: true,
View: view,
WindowName: windowName,
Key: key,
Kind: types.MAIN_CONTEXT,
Focusable: true,
HighlightOnFocus: true,
}), ContextCallbackOpts{
OnFocus: onFocus,
OnFocusLost: onFocusLost,
@@ -68,7 +69,7 @@ func (self *PatchExplorerContext) GetIncludedLineIndices() []int {
func (self *PatchExplorerContext) RenderAndFocus(isFocused bool) error {
self.setContent(isFocused)
self.focusSelection()
self.FocusSelection()
self.c.Render()
return nil
@@ -83,7 +84,7 @@ func (self *PatchExplorerContext) Render(isFocused bool) error {
}
func (self *PatchExplorerContext) Focus() error {
self.focusSelection()
self.FocusSelection()
self.c.Render()
return nil
@@ -93,19 +94,18 @@ func (self *PatchExplorerContext) setContent(isFocused bool) {
self.GetView().SetContent(self.GetContentToRender(isFocused))
}
func (self *PatchExplorerContext) focusSelection() {
func (self *PatchExplorerContext) FocusSelection() {
view := self.GetView()
state := self.GetState()
_, viewHeight := view.Size()
bufferHeight := viewHeight - 1
_, origin := view.Origin()
selectedLineIdx := state.GetSelectedLineIdx()
newOriginY := state.CalculateOrigin(origin, bufferHeight)
newOrigin := state.CalculateOrigin(origin, bufferHeight)
_ = view.SetOriginY(newOriginY)
_ = view.SetOriginY(newOrigin)
_ = view.SetCursor(0, selectedLineIdx-newOrigin)
view.SetCursorY(state.GetSelectedLineIdx() - newOriginY)
}
func (self *PatchExplorerContext) GetContentToRender(isFocused bool) string {
+4
View File
@@ -50,6 +50,10 @@ func NewDisplayContext(key types.ContextKey, view *gocui.View, windowName string
}
func (self *SimpleContext) HandleFocus(opts types.OnFocusOpts) error {
if self.highlightOnFocus {
self.GetViewTrait().SetHighlight(true)
}
if self.OnFocus != nil {
if err := self.OnFocus(opts); err != nil {
return err
+20 -5
View File
@@ -30,7 +30,8 @@ func NewSubCommitsContext(
) *SubCommitsContext {
viewModel := &SubCommitsViewModel{
BasicViewModel: NewBasicViewModel(getModel),
refName: "",
ref: nil,
limitCommits: true,
}
return &SubCommitsContext{
@@ -60,12 +61,18 @@ func NewSubCommitsContext(
type SubCommitsViewModel struct {
// name of the ref that the sub-commits are shown for
refName string
ref types.Ref
*BasicViewModel[*models.Commit]
limitCommits bool
}
func (self *SubCommitsViewModel) SetRefName(refName string) {
self.refName = refName
func (self *SubCommitsViewModel) SetRef(ref types.Ref) {
self.ref = ref
}
func (self *SubCommitsViewModel) GetRef() types.Ref {
return self.ref
}
func (self *SubCommitsContext) GetSelectedItemId() string {
@@ -94,5 +101,13 @@ func (self *SubCommitsContext) GetCommits() []*models.Commit {
}
func (self *SubCommitsContext) Title() string {
return fmt.Sprintf(self.c.Tr.SubCommitsDynamicTitle, utils.TruncateWithEllipsis(self.refName, 50))
return fmt.Sprintf(self.c.Tr.SubCommitsDynamicTitle, utils.TruncateWithEllipsis(self.ref.RefName(), 50))
}
func (self *SubCommitsContext) SetLimitCommits(value bool) {
self.limitCommits = value
}
func (self *SubCommitsContext) GetLimitCommits() bool {
return self.limitCommits
}
+20 -8
View File
@@ -150,15 +150,15 @@ func (gui *Gui) contextTree() *context.ContextTree {
func(opts types.OnFocusLostOpts) error {
gui.Views.PatchBuilding.Wrap = true
if gui.git.Patch.PatchManager.IsEmpty() {
gui.git.Patch.PatchManager.Reset()
if gui.git.Patch.PatchBuilder.IsEmpty() {
gui.git.Patch.PatchBuilder.Reset()
}
return nil
},
func() []int {
filename := gui.State.Contexts.CommitFiles.GetSelectedPath()
includedLineIndices, err := gui.git.Patch.PatchManager.GetFileIncLineIndices(filename)
includedLineIndices, err := gui.git.Patch.PatchBuilder.GetFileIncLineIndices(filename)
if err != nil {
gui.Log.Error(err)
return nil
@@ -217,17 +217,29 @@ func (gui *Gui) contextTree() *context.ContextTree {
},
},
),
CommitMessage: context.NewSimpleContext(
CommitMessage: context.NewCommitMessageContext(
gui.Views.CommitMessage,
context.ContextCallbackOpts{
OnFocus: OnFocusWrapper(gui.handleCommitMessageFocused),
},
),
CommitDescription: context.NewSimpleContext(
context.NewBaseContext(context.NewBaseContextOpts{
Kind: types.PERSISTENT_POPUP,
View: gui.Views.CommitMessage,
WindowName: "commitMessage",
Key: context.COMMIT_MESSAGE_CONTEXT_KEY,
View: gui.Views.CommitDescription,
WindowName: "commitDescription",
Key: context.COMMIT_DESCRIPTION_CONTEXT_KEY,
Focusable: true,
HasUncontrolledBounds: true,
}),
context.ContextCallbackOpts{
OnFocus: OnFocusWrapper(gui.handleCommitMessageFocused),
OnFocus: func(opts types.OnFocusOpts) error {
_, err := gui.g.SetViewBeneath("commitDescription", "commitMessage", 10)
if err != nil {
return err
}
return nil
},
},
),
Search: context.NewSimpleContext(
+38 -25
View File
@@ -25,10 +25,25 @@ func (gui *Gui) resetControllers() {
rebaseHelper := helpers.NewMergeAndRebaseHelper(helperCommon, gui.State.Contexts, gui.git, refsHelper)
suggestionsHelper := helpers.NewSuggestionsHelper(helperCommon, model, gui.refreshSuggestions)
setCommitMessage := gui.getSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitMessage })
getSavedCommitMessage := func() string {
return gui.State.savedCommitMessage
setCommitSummary := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitMessage })
setCommitDescription := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitDescription })
getCommitSummary := func() string {
return strings.TrimSpace(gui.Views.CommitMessage.TextArea.GetContent())
}
getCommitDescription := func() string {
return strings.TrimSpace(gui.Views.CommitDescription.TextArea.GetContent())
}
commitsHelper := helpers.NewCommitsHelper(helperCommon,
gui.State.Model,
gui.State.Contexts,
getCommitSummary,
setCommitSummary,
getCommitDescription,
setCommitDescription,
gui.RenderCommitLength,
)
gpgHelper := helpers.NewGpgHelper(helperCommon, gui.os, gui.git)
gui.helpers = &helpers.Helpers{
Refs: refsHelper,
Host: helpers.NewHostHelper(helperCommon, gui.git),
@@ -36,9 +51,9 @@ func (gui *Gui) resetControllers() {
Bisect: helpers.NewBisectHelper(helperCommon, gui.git),
Suggestions: suggestionsHelper,
Files: helpers.NewFilesHelper(helperCommon, gui.git, osCommand),
WorkingTree: helpers.NewWorkingTreeHelper(helperCommon, gui.git, gui.State.Contexts, refsHelper, model, setCommitMessage, getSavedCommitMessage),
WorkingTree: helpers.NewWorkingTreeHelper(helperCommon, gui.git, gui.State.Contexts, refsHelper, model, setCommitSummary, commitsHelper, gpgHelper),
Tags: helpers.NewTagsHelper(helperCommon, gui.git),
GPG: helpers.NewGpgHelper(helperCommon, gui.os, gui.git),
GPG: gpgHelper,
MergeAndRebase: rebaseHelper,
MergeConflicts: helpers.NewMergeConflictsHelper(helperCommon, gui.State.Contexts, gui.git),
CherryPick: helpers.NewCherryPickHelper(
@@ -48,7 +63,9 @@ func (gui *Gui) resetControllers() {
func() *cherrypicking.CherryPicking { return gui.State.Modes.CherryPicking },
rebaseHelper,
),
Upstream: helpers.NewUpstreamHelper(helperCommon, model, suggestionsHelper.GetRemoteBranchesSuggestionsFunc),
Upstream: helpers.NewUpstreamHelper(helperCommon, model, suggestionsHelper.GetRemoteBranchesSuggestionsFunc),
AmendHelper: helpers.NewAmendHelper(helperCommon, gui.git, gpgHelper),
Commits: commitsHelper,
}
gui.CustomCommandsClient = custom_commands.NewClient(
@@ -81,24 +98,12 @@ func (gui *Gui) resetControllers() {
bisectController := controllers.NewBisectController(common)
getCommitMessage := func() string {
return strings.TrimSpace(gui.Views.CommitMessage.TextArea.GetContent())
}
onCommitAttempt := func(message string) {
gui.State.savedCommitMessage = message
gui.Views.CommitMessage.ClearTextArea()
}
onCommitSuccess := func() {
gui.State.savedCommitMessage = ""
}
commitMessageController := controllers.NewCommitMessageController(
common,
getCommitMessage,
onCommitAttempt,
onCommitSuccess,
)
commitDescriptionController := controllers.NewCommitDescriptionController(
common,
)
remoteBranchesController := controllers.NewRemoteBranchesController(common)
@@ -109,8 +114,7 @@ func (gui *Gui) resetControllers() {
filesController := controllers.NewFilesController(
common,
gui.enterSubmodule,
setCommitMessage,
getSavedCommitMessage,
setCommitSummary,
)
mergeConflictsController := controllers.NewMergeConflictsController(common)
remotesController := controllers.NewRemotesController(
@@ -133,7 +137,12 @@ func (gui *Gui) resetControllers() {
patchBuildingController := controllers.NewPatchBuildingController(common)
snakeController := controllers.NewSnakeController(common, func() *snake.Game { return gui.snakeGame })
setSubCommits := func(commits []*models.Commit) { gui.State.Model.SubCommits = commits }
setSubCommits := func(commits []*models.Commit) {
gui.Mutexes.SubCommitsMutex.Lock()
defer gui.Mutexes.SubCommitsMutex.Unlock()
gui.State.Model.SubCommits = commits
}
for _, context := range []controllers.CanSwitchToSubCommits{
gui.State.Contexts.Branches,
@@ -239,6 +248,10 @@ func (gui *Gui) resetControllers() {
commitMessageController,
)
controllers.AttachControllers(gui.State.Contexts.CommitDescription,
commitDescriptionController,
)
controllers.AttachControllers(gui.State.Contexts.RemoteBranches,
remoteBranchesController,
)
@@ -86,6 +86,11 @@ func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*ty
Handler: self.checkSelectedAndReal(self.fastForward),
Description: self.c.Tr.FastForward,
},
{
Key: opts.GetKey(opts.Config.Branches.CreateTag),
Handler: self.checkSelected(self.createTag),
Description: self.c.Tr.LcCreateTag,
},
{
Key: opts.GetKey(opts.Config.Commits.ViewResetOptions),
Handler: self.checkSelected(self.createResetMenu),
@@ -363,6 +368,10 @@ func (self *BranchesController) fastForward(branch *models.Branch) error {
})
}
func (self *BranchesController) createTag(branch *models.Branch) error {
return self.helpers.Tags.CreateTagMenu(branch.FullRefName(), func() {})
}
func (self *BranchesController) createResetMenu(selectedBranch *models.Branch) error {
return self.helpers.Refs.CreateGitResetMenu(selectedBranch.Name)
}
@@ -0,0 +1,60 @@
package controllers
import (
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type CommitDescriptionController struct {
baseController
*controllerCommon
}
var _ types.IController = &CommitMessageController{}
func NewCommitDescriptionController(
common *controllerCommon,
) *CommitDescriptionController {
return &CommitDescriptionController{
baseController: baseController{},
controllerCommon: common,
}
}
func (self *CommitDescriptionController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
bindings := []*types.Binding{
{
Key: opts.GetKey(opts.Config.Universal.TogglePanel),
Handler: self.switchToCommitMessage,
},
{
Key: opts.GetKey(opts.Config.Universal.Return),
Handler: self.close,
},
{
Key: opts.GetKey(opts.Config.Universal.ConfirmInEditor),
Handler: self.confirm,
},
}
return bindings
}
func (self *CommitDescriptionController) Context() types.Context {
return self.context()
}
func (self *CommitDescriptionController) context() types.Context {
return self.contexts.CommitMessage
}
func (self *CommitDescriptionController) switchToCommitMessage() error {
return self.c.PushContext(self.contexts.CommitMessage)
}
func (self *CommitDescriptionController) close() error {
return self.helpers.Commits.CloseCommitMessagePanel()
}
func (self *CommitDescriptionController) confirm() error {
return self.helpers.Commits.HandleCommitConfirm()
}
@@ -1,33 +1,24 @@
package controllers
import (
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type CommitMessageController struct {
baseController
*controllerCommon
getCommitMessage func() string
onCommitAttempt func(message string)
onCommitSuccess func()
}
var _ types.IController = &CommitMessageController{}
func NewCommitMessageController(
common *controllerCommon,
getCommitMessage func() string,
onCommitAttempt func(message string),
onCommitSuccess func(),
) *CommitMessageController {
return &CommitMessageController{
baseController: baseController{},
controllerCommon: common,
getCommitMessage: getCommitMessage,
onCommitAttempt: onCommitAttempt,
onCommitSuccess: onCommitSuccess,
}
}
@@ -41,6 +32,18 @@ func (self *CommitMessageController) GetKeybindings(opts types.KeybindingsOpts)
Key: opts.GetKey(opts.Config.Universal.Return),
Handler: self.close,
},
{
Key: opts.GetKey(opts.Config.Universal.PrevItem),
Handler: self.handlePreviousCommit,
},
{
Key: opts.GetKey(opts.Config.Universal.NextItem),
Handler: self.handleNextCommit,
},
{
Key: opts.GetKey(opts.Config.Universal.TogglePanel),
Handler: self.switchToCommitDescription,
},
}
return bindings
@@ -50,30 +53,63 @@ func (self *CommitMessageController) Context() types.Context {
return self.context()
}
// this method is pointless in this context but I'm keeping it consistent
// with other contexts so that when generics arrive it's easier to refactor
func (self *CommitMessageController) context() types.Context {
func (self *CommitMessageController) context() *context.CommitMessageContext {
return self.contexts.CommitMessage
}
func (self *CommitMessageController) confirm() error {
message := self.getCommitMessage()
self.onCommitAttempt(message)
func (self *CommitMessageController) handlePreviousCommit() error {
return self.handleCommitIndexChange(1)
}
if message == "" {
return self.c.ErrorMsg(self.c.Tr.CommitWithoutMessageErr)
func (self *CommitMessageController) handleNextCommit() error {
if self.context().GetSelectedIndex() == context.NoCommitIndex {
return nil
}
return self.handleCommitIndexChange(-1)
}
func (self *CommitMessageController) switchToCommitDescription() error {
if err := self.c.PushContext(self.contexts.CommitDescription); err != nil {
return err
}
return nil
}
func (self *CommitMessageController) handleCommitIndexChange(value int) error {
currentIndex := self.context().GetSelectedIndex()
newIndex := currentIndex + value
if newIndex == context.NoCommitIndex {
self.context().SetSelectedIndex(newIndex)
self.helpers.Commits.SetMessageAndDescriptionInView(self.context().GetHistoryMessage())
return nil
} else if currentIndex == context.NoCommitIndex {
self.context().SetHistoryMessage(self.helpers.Commits.JoinCommitMessageAndDescription())
}
cmdObj := self.git.Commit.CommitCmdObj(message)
self.c.LogAction(self.c.Tr.Actions.Commit)
validCommit, err := self.setCommitMessageAtIndex(newIndex)
if validCommit {
self.context().SetSelectedIndex(newIndex)
}
return err
}
_ = self.c.PopContext()
return self.helpers.GPG.WithGpgHandling(cmdObj, self.c.Tr.CommittingStatus, func() error {
self.onCommitSuccess()
return nil
})
// returns true if the given index is for a valid commit
func (self *CommitMessageController) setCommitMessageAtIndex(index int) (bool, error) {
commitMessage, err := self.git.Commit.GetCommitMessageFromHistory(index)
if err != nil {
if err == git_commands.ErrInvalidCommitIndex {
return false, nil
}
return false, self.c.ErrorMsg(self.c.Tr.CommitWithoutMessageErr)
}
self.helpers.Commits.UpdateCommitPanelView(commitMessage)
return true, nil
}
func (self *CommitMessageController) confirm() error {
return self.helpers.Commits.HandleCommitConfirm()
}
func (self *CommitMessageController) close() error {
return self.c.PopContext()
return self.helpers.Commits.CloseCommitMessagePanel()
}
+15 -15
View File
@@ -157,8 +157,8 @@ func (self *CommitFilesController) edit(node *filetree.CommitFileNode) error {
func (self *CommitFilesController) toggleForPatch(node *filetree.CommitFileNode) error {
toggle := func() error {
return self.c.WithWaitingStatus(self.c.Tr.LcUpdatingPatch, func() error {
if !self.git.Patch.PatchManager.Active() {
if err := self.startPatchManager(); err != nil {
if !self.git.Patch.PatchBuilder.Active() {
if err := self.startPatchBuilder(); err != nil {
return err
}
}
@@ -166,34 +166,34 @@ func (self *CommitFilesController) toggleForPatch(node *filetree.CommitFileNode)
// if there is any file that hasn't been fully added we'll fully add everything,
// otherwise we'll remove everything
adding := node.SomeFile(func(file *models.CommitFile) bool {
return self.git.Patch.PatchManager.GetFileStatus(file.Name, self.context().GetRef().RefName()) != patch.WHOLE
return self.git.Patch.PatchBuilder.GetFileStatus(file.Name, self.context().GetRef().RefName()) != patch.WHOLE
})
err := node.ForEachFile(func(file *models.CommitFile) error {
if adding {
return self.git.Patch.PatchManager.AddFileWhole(file.Name)
return self.git.Patch.PatchBuilder.AddFileWhole(file.Name)
} else {
return self.git.Patch.PatchManager.RemoveFile(file.Name)
return self.git.Patch.PatchBuilder.RemoveFile(file.Name)
}
})
if err != nil {
return self.c.Error(err)
}
if self.git.Patch.PatchManager.IsEmpty() {
self.git.Patch.PatchManager.Reset()
if self.git.Patch.PatchBuilder.IsEmpty() {
self.git.Patch.PatchBuilder.Reset()
}
return self.c.PostRefreshUpdate(self.context())
})
}
if self.git.Patch.PatchManager.Active() && self.git.Patch.PatchManager.To != self.context().GetRef().RefName() {
if self.git.Patch.PatchBuilder.Active() && self.git.Patch.PatchBuilder.To != self.context().GetRef().RefName() {
return self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.DiscardPatch,
Prompt: self.c.Tr.DiscardPatchConfirm,
HandleConfirm: func() error {
self.git.Patch.PatchManager.Reset()
self.git.Patch.PatchBuilder.Reset()
return toggle()
},
})
@@ -207,7 +207,7 @@ func (self *CommitFilesController) toggleAllForPatch(_ *filetree.CommitFileNode)
return self.toggleForPatch(root)
}
func (self *CommitFilesController) startPatchManager() error {
func (self *CommitFilesController) startPatchBuilder() error {
commitFilesContext := self.context()
canRebase := commitFilesContext.GetCanRebase()
@@ -215,7 +215,7 @@ func (self *CommitFilesController) startPatchManager() error {
to := ref.RefName()
from, reverse := self.modes.Diffing.GetFromAndReverseArgsForDiff(ref.ParentRefName())
self.git.Patch.PatchManager.Start(from, to, reverse, canRebase)
self.git.Patch.PatchBuilder.Start(from, to, reverse, canRebase)
return nil
}
@@ -229,8 +229,8 @@ func (self *CommitFilesController) enterCommitFile(node *filetree.CommitFileNode
}
enterTheFile := func() error {
if !self.git.Patch.PatchManager.Active() {
if err := self.startPatchManager(); err != nil {
if !self.git.Patch.PatchBuilder.Active() {
if err := self.startPatchBuilder(); err != nil {
return err
}
}
@@ -238,12 +238,12 @@ func (self *CommitFilesController) enterCommitFile(node *filetree.CommitFileNode
return self.c.PushContext(self.contexts.CustomPatchBuilder, opts)
}
if self.git.Patch.PatchManager.Active() && self.git.Patch.PatchManager.To != self.context().GetRef().RefName() {
if self.git.Patch.PatchBuilder.Active() && self.git.Patch.PatchBuilder.To != self.context().GetRef().RefName() {
return self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.DiscardPatch,
Prompt: self.c.Tr.DiscardPatchConfirm,
HandleConfirm: func() error {
self.git.Patch.PatchManager.Reset()
self.git.Patch.PatchBuilder.Reset()
return enterTheFile()
},
})
@@ -101,7 +101,7 @@ func (self *ContextLinesController) applyChange() error {
}
func (self *ContextLinesController) checkCanChangeContext() error {
if self.git.Patch.PatchManager.Active() {
if self.git.Patch.PatchBuilder.Active() {
return errors.New(self.c.Tr.CantChangeContextSizeError)
}
+7 -18
View File
@@ -12,12 +12,11 @@ import (
)
type FilesController struct {
baseController
baseController // nolint: unused
*controllerCommon
enterSubmodule func(submodule *models.SubmoduleConfig) error
setCommitMessage func(message string)
getSavedCommitMessage func() string
enterSubmodule func(submodule *models.SubmoduleConfig) error
setCommitMessage func(message string)
}
var _ types.IController = &FilesController{}
@@ -26,13 +25,11 @@ func NewFilesController(
common *controllerCommon,
enterSubmodule func(submodule *models.SubmoduleConfig) error,
setCommitMessage func(message string),
getSavedCommitMessage func() string,
) *FilesController {
return &FilesController{
controllerCommon: common,
enterSubmodule: enterSubmodule,
setCommitMessage: setCommitMessage,
getSavedCommitMessage: getSavedCommitMessage,
controllerCommon: common,
enterSubmodule: enterSubmodule,
setCommitMessage: setCommitMessage,
}
}
@@ -567,15 +564,7 @@ func (self *FilesController) handleAmendCommitPress() error {
return self.c.ErrorMsg(self.c.Tr.NoCommitToAmend)
}
return self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.AmendLastCommitTitle,
Prompt: self.c.Tr.SureToAmend,
HandleConfirm: func() error {
cmdObj := self.git.Commit.AmendHeadCmdObj()
self.c.LogAction(self.c.Tr.Actions.AmendCommit)
return self.helpers.GPG.WithGpgHandling(cmdObj, self.c.Tr.AmendingStatus, nil)
},
})
return self.helpers.AmendHelper.AmendHead()
}
func (self *FilesController) handleStatusFilterPressed() error {
+27 -2
View File
@@ -5,6 +5,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// splitting this action out into its own file because it's self-contained
@@ -51,7 +52,13 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error {
}
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
},
Key: 'd',
Key: 'x',
Tooltip: utils.ResolvePlaceholderString(
self.c.Tr.DiscardAllTooltip,
map[string]string{
"path": node.GetPath(),
},
),
},
}
@@ -67,6 +74,12 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error {
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
},
Key: 'u',
Tooltip: utils.ResolvePlaceholderString(
self.c.Tr.DiscardUnstagedTooltip,
map[string]string{
"path": node.GetPath(),
},
),
})
}
} else {
@@ -95,7 +108,13 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error {
}
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
},
Key: 'd',
Key: 'x',
Tooltip: utils.ResolvePlaceholderString(
self.c.Tr.DiscardAllTooltip,
map[string]string{
"path": node.GetPath(),
},
),
},
}
@@ -111,6 +130,12 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error {
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
},
Key: 'u',
Tooltip: utils.ResolvePlaceholderString(
self.c.Tr.DiscardUnstagedTooltip,
map[string]string{
"path": node.GetPath(),
},
),
})
}
}
+14 -4
View File
@@ -1,6 +1,8 @@
package controllers
import (
"strings"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
"github.com/jesseduffield/lazygit/pkg/gui/types"
@@ -37,10 +39,12 @@ func (self *GlobalController) customCommand() error {
Title: self.c.Tr.CustomCommand,
FindSuggestionsFunc: self.GetCustomCommandsHistorySuggestionsFunc(),
HandleConfirm: func(command string) error {
self.c.GetAppState().CustomCommandsHistory = utils.Limit(
lo.Uniq(append(self.c.GetAppState().CustomCommandsHistory, command)),
1000,
)
if self.shouldSaveCommand(command) {
self.c.GetAppState().CustomCommandsHistory = utils.Limit(
lo.Uniq(append(self.c.GetAppState().CustomCommandsHistory, command)),
1000,
)
}
err := self.c.SaveAppState()
if err != nil {
@@ -55,6 +59,12 @@ func (self *GlobalController) customCommand() error {
})
}
// this mimics the shell functionality `ignorespace`
// which doesn't save a command to history if it starts with a space
func (self *GlobalController) shouldSaveCommand(command string) bool {
return !strings.HasPrefix(command, " ")
}
func (self *GlobalController) GetCustomCommandsHistorySuggestionsFunc() func(string) []*types.Suggestion {
// reversing so that we display the latest command first
history := slices.Reverse(self.c.GetAppState().CustomCommandsHistory)
@@ -0,0 +1,36 @@
package helpers
import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type AmendHelper struct {
c *types.HelperCommon
git *commands.GitCommand
gpg *GpgHelper
}
func NewAmendHelper(
c *types.HelperCommon,
git *commands.GitCommand,
gpg *GpgHelper,
) *AmendHelper {
return &AmendHelper{
c: c,
git: git,
gpg: gpg,
}
}
func (self *AmendHelper) AmendHead() error {
return self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.AmendLastCommitTitle,
Prompt: self.c.Tr.SureToAmend,
HandleConfirm: func() error {
cmdObj := self.git.Commit.AmendHeadCmdObj()
self.c.LogAction(self.c.Tr.Actions.AmendCommit)
return self.gpg.WithGpgHandling(cmdObj, self.c.Tr.AmendingStatus, nil)
},
})
}
@@ -0,0 +1,166 @@
package helpers
import (
"strings"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type ICommitsHelper interface {
UpdateCommitPanelView(message string)
}
type CommitsHelper struct {
c *types.HelperCommon
model *types.Model
contexts *context.ContextTree
getCommitSummary func() string
setCommitSummary func(string)
getCommitDescription func() string
setCommitDescription func(string)
renderCommitLength func()
}
var _ ICommitsHelper = &CommitsHelper{}
func NewCommitsHelper(
c *types.HelperCommon,
model *types.Model,
contexts *context.ContextTree,
getCommitSummary func() string,
setCommitSummary func(string),
getCommitDescription func() string,
setCommitDescription func(string),
renderCommitLength func(),
) *CommitsHelper {
return &CommitsHelper{
c: c,
model: model,
contexts: contexts,
getCommitSummary: getCommitSummary,
setCommitSummary: setCommitSummary,
getCommitDescription: getCommitDescription,
setCommitDescription: setCommitDescription,
renderCommitLength: renderCommitLength,
}
}
func (self *CommitsHelper) SplitCommitMessageAndDescription(message string) (string, string) {
for _, separator := range []string{"\n\n", "\n\r\n\r", "\n", "\n\r"} {
msg, description, found := strings.Cut(message, separator)
if found {
return msg, description
}
}
return message, ""
}
func (self *CommitsHelper) SetMessageAndDescriptionInView(message string) {
summary, description := self.SplitCommitMessageAndDescription(message)
self.setCommitSummary(summary)
self.setCommitDescription(description)
self.renderCommitLength()
}
func (self *CommitsHelper) JoinCommitMessageAndDescription() string {
if len(self.getCommitDescription()) == 0 {
return self.getCommitSummary()
}
return self.getCommitSummary() + "\n" + self.getCommitDescription()
}
func (self *CommitsHelper) UpdateCommitPanelView(message string) {
// first try the passed in message, if not fallback to context -> view in that order
if message != "" {
self.SetMessageAndDescriptionInView(message)
return
}
message = self.contexts.CommitMessage.GetPreservedMessage()
if message != "" {
self.SetMessageAndDescriptionInView(message)
} else {
self.SetMessageAndDescriptionInView(self.getCommitSummary())
}
}
type OpenCommitMessagePanelOpts struct {
CommitIndex int
Title string
PreserveMessage bool
OnConfirm func(string) error
InitialMessage string
}
func (self *CommitsHelper) OpenCommitMessagePanel(opts *OpenCommitMessagePanelOpts) error {
self.contexts.CommitMessage.SetPanelState(
opts.CommitIndex,
opts.Title,
opts.PreserveMessage,
opts.OnConfirm,
)
self.UpdateCommitPanelView(opts.InitialMessage)
return self.pushCommitMessageContexts()
}
func (self *CommitsHelper) OnCommitSuccess() {
// if we have a preserved message we want to clear it on success
if self.contexts.CommitMessage.GetPreserveMessage() {
self.contexts.CommitMessage.SetPreservedMessage("")
}
self.SetMessageAndDescriptionInView("")
}
func (self *CommitsHelper) HandleCommitConfirm() error {
fullMessage := self.JoinCommitMessageAndDescription()
if fullMessage == "" {
return self.c.ErrorMsg(self.c.Tr.CommitWithoutMessageErr)
}
err := self.contexts.CommitMessage.OnConfirm(fullMessage)
if err != nil {
return err
}
return nil
}
func (self *CommitsHelper) CloseCommitMessagePanel() error {
if self.contexts.CommitMessage.GetPreserveMessage() {
message := self.JoinCommitMessageAndDescription()
self.contexts.CommitMessage.SetPreservedMessage(message)
} else {
self.SetMessageAndDescriptionInView("")
}
self.contexts.CommitMessage.SetHistoryMessage("")
return self.PopCommitMessageContexts()
}
func (self *CommitsHelper) PopCommitMessageContexts() error {
return self.c.RemoveContexts(self.commitMessageContexts())
}
func (self *CommitsHelper) pushCommitMessageContexts() error {
for _, context := range self.commitMessageContexts() {
if err := self.c.PushContext(context); err != nil {
return err
}
}
return nil
}
func (self *CommitsHelper) commitMessageContexts() []types.Context {
return []types.Context{
self.contexts.CommitDescription,
self.contexts.CommitMessage,
}
}
+22 -14
View File
@@ -10,7 +10,6 @@ type IFilesHelper interface {
EditFile(filename string) error
EditFileAtLine(filename string, lineNumber int) error
OpenFile(filename string) error
OpenFileAtLine(filename string, lineNumber int) error
}
type FilesHelper struct {
@@ -34,28 +33,37 @@ func NewFilesHelper(
var _ IFilesHelper = &FilesHelper{}
func (self *FilesHelper) EditFile(filename string) error {
return self.EditFileAtLine(filename, 1)
cmdStr, editInTerminal := self.git.File.GetEditCmdStr(filename)
return self.callEditor(cmdStr, editInTerminal)
}
func (self *FilesHelper) EditFileAtLine(filename string, lineNumber int) error {
cmdStr, err := self.git.File.GetEditCmdStr(filename, lineNumber)
if err != nil {
return self.c.Error(err)
cmdStr, editInTerminal := self.git.File.GetEditAtLineCmdStr(filename, lineNumber)
return self.callEditor(cmdStr, editInTerminal)
}
func (self *FilesHelper) EditFileAtLineAndWait(filename string, lineNumber int) error {
cmdStr := self.git.File.GetEditAtLineAndWaitCmdStr(filename, lineNumber)
// Always suspend, regardless of the value of the editInTerminal config,
// since we want to prevent interacting with the UI until the editor
// returns, even if the editor doesn't use the terminal
return self.callEditor(cmdStr, true)
}
func (self *FilesHelper) callEditor(cmdStr string, editInTerminal bool) error {
if editInTerminal {
return self.c.RunSubprocessAndRefresh(
self.os.Cmd.NewShell(cmdStr),
)
}
self.c.LogAction(self.c.Tr.Actions.EditFile)
return self.c.RunSubprocessAndRefresh(
self.os.Cmd.NewShell(cmdStr),
)
return self.os.Cmd.NewShell(cmdStr).Run()
}
func (self *FilesHelper) OpenFile(filename string) error {
return self.OpenFileAtLine(filename, 1)
}
func (self *FilesHelper) OpenFileAtLine(filename string, lineNumber int) error {
self.c.LogAction(self.c.Tr.Actions.OpenFile)
if err := self.os.OpenFileAtLine(filename, lineNumber); err != nil {
if err := self.os.OpenFile(filename); err != nil {
return self.c.Error(err)
}
return nil
+4
View File
@@ -14,6 +14,8 @@ type Helpers struct {
PatchBuilding *PatchBuildingHelper
GPG *GpgHelper
Upstream *UpstreamHelper
AmendHelper *AmendHelper
Commits *CommitsHelper
}
func NewStubHelpers() *Helpers {
@@ -31,5 +33,7 @@ func NewStubHelpers() *Helpers {
PatchBuilding: &PatchBuildingHelper{},
GPG: &GpgHelper{},
Upstream: &UpstreamHelper{},
AmendHelper: &AmendHelper{},
Commits: &CommitsHelper{},
}
}
@@ -119,6 +119,7 @@ var conflictStrings = []string{
"When you have resolved this problem",
"fix conflicts",
"Resolve all conflicts manually",
"Merge conflict in file",
}
func isMergeConflictErr(errStr string) bool {
@@ -200,22 +201,42 @@ func (self *MergeAndRebaseHelper) RebaseOntoRef(ref string) error {
if ref == checkedOutBranch {
return self.c.ErrorMsg(self.c.Tr.CantRebaseOntoSelf)
}
prompt := utils.ResolvePlaceholderString(
self.c.Tr.ConfirmRebase,
menuItems := []*types.MenuItem{
{
Label: self.c.Tr.SimpleRebase,
Key: 's',
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.RebaseBranch)
err := self.git.Rebase.RebaseBranch(ref)
return self.CheckMergeOrRebase(err)
},
},
{
Label: self.c.Tr.InteractiveRebase,
Key: 'i',
Tooltip: self.c.Tr.InteractiveRebaseTooltip,
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.RebaseBranch)
err := self.git.Rebase.EditRebase(ref)
if err = self.CheckMergeOrRebase(err); err != nil {
return err
}
return self.c.PushContext(self.contexts.LocalCommits)
},
},
}
title := utils.ResolvePlaceholderString(
self.c.Tr.RebasingTitle,
map[string]string{
"checkedOutBranch": checkedOutBranch,
"selectedBranch": ref,
"ref": ref,
},
)
return self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.RebasingTitle,
Prompt: prompt,
HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.RebaseBranch)
err := self.git.Rebase.RebaseBranch(ref)
return self.CheckMergeOrRebase(err)
},
return self.c.Menu(types.CreateMenuOptions{
Title: title,
Items: menuItems,
})
}
@@ -38,12 +38,12 @@ func (self *PatchBuildingHelper) ValidateNormalWorkingTreeState() (bool, error)
// takes us from the patch building panel back to the commit files panel
func (self *PatchBuildingHelper) Escape() error {
return self.c.PushContext(self.contexts.CommitFiles)
return self.c.PopContext()
}
// kills the custom patch and returns us back to the commit files panel if needed
func (self *PatchBuildingHelper) Reset() error {
self.git.Patch.PatchManager.Reset()
self.git.Patch.PatchBuilder.Reset()
if self.c.CurrentStaticContext().GetKind() != types.SIDE_CONTEXT {
if err := self.Escape(); err != nil {
+7 -7
View File
@@ -21,20 +21,20 @@ func NewTagsHelper(c *types.HelperCommon, git *commands.GitCommand) *TagsHelper
}
}
func (self *TagsHelper) CreateTagMenu(commitSha string, onCreate func()) error {
func (self *TagsHelper) CreateTagMenu(ref string, onCreate func()) error {
return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.TagMenuTitle,
Items: []*types.MenuItem{
{
Label: self.c.Tr.LcLightweightTag,
OnPress: func() error {
return self.handleCreateLightweightTag(commitSha, onCreate)
return self.handleCreateLightweightTag(ref, onCreate)
},
},
{
Label: self.c.Tr.LcAnnotatedTag,
OnPress: func() error {
return self.handleCreateAnnotatedTag(commitSha, onCreate)
return self.handleCreateAnnotatedTag(ref, onCreate)
},
},
},
@@ -48,7 +48,7 @@ func (self *TagsHelper) afterTagCreate(onCreate func()) error {
})
}
func (self *TagsHelper) handleCreateAnnotatedTag(commitSha string, onCreate func()) error {
func (self *TagsHelper) handleCreateAnnotatedTag(ref string, onCreate func()) error {
return self.c.Prompt(types.PromptOpts{
Title: self.c.Tr.TagNameTitle,
HandleConfirm: func(tagName string) error {
@@ -56,7 +56,7 @@ func (self *TagsHelper) handleCreateAnnotatedTag(commitSha string, onCreate func
Title: self.c.Tr.TagMessageTitle,
HandleConfirm: func(msg string) error {
self.c.LogAction(self.c.Tr.Actions.CreateAnnotatedTag)
if err := self.git.Tag.CreateAnnotated(tagName, commitSha, msg); err != nil {
if err := self.git.Tag.CreateAnnotated(tagName, ref, msg); err != nil {
return self.c.Error(err)
}
return self.afterTagCreate(onCreate)
@@ -66,12 +66,12 @@ func (self *TagsHelper) handleCreateAnnotatedTag(commitSha string, onCreate func
})
}
func (self *TagsHelper) handleCreateLightweightTag(commitSha string, onCreate func()) error {
func (self *TagsHelper) handleCreateLightweightTag(ref string, onCreate func()) error {
return self.c.Prompt(types.PromptOpts{
Title: self.c.Tr.TagNameTitle,
HandleConfirm: func(tagName string) error {
self.c.LogAction(self.c.Tr.Actions.CreateLightweightTag)
if err := self.git.Tag.CreateLightweight(tagName, commitSha); err != nil {
if err := self.git.Tag.CreateLightweight(tagName, ref); err != nil {
return self.c.Error(err)
}
return self.afterTagCreate(onCreate)
@@ -20,13 +20,14 @@ type IWorkingTreeHelper interface {
}
type WorkingTreeHelper struct {
c *types.HelperCommon
git *commands.GitCommand
contexts *context.ContextTree
refHelper *RefsHelper
model *types.Model
setCommitMessage func(message string)
getSavedCommitMessage func() string
c *types.HelperCommon
git *commands.GitCommand
contexts *context.ContextTree
refHelper *RefsHelper
model *types.Model
setCommitMessage func(message string)
commitsHelper *CommitsHelper
gpgHelper *GpgHelper
}
func NewWorkingTreeHelper(
@@ -36,16 +37,18 @@ func NewWorkingTreeHelper(
refHelper *RefsHelper,
model *types.Model,
setCommitMessage func(message string),
getSavedCommitMessage func() string,
commitsHelper *CommitsHelper,
gpgHelper *GpgHelper,
) *WorkingTreeHelper {
return &WorkingTreeHelper{
c: c,
git: git,
contexts: contexts,
refHelper: refHelper,
model: model,
setCommitMessage: setCommitMessage,
getSavedCommitMessage: getSavedCommitMessage,
c: c,
git: git,
contexts: contexts,
refHelper: refHelper,
model: model,
setCommitMessage: setCommitMessage,
commitsHelper: commitsHelper,
gpgHelper: gpgHelper,
}
}
@@ -94,7 +97,7 @@ func (self *WorkingTreeHelper) OpenMergeTool() error {
})
}
func (self *WorkingTreeHelper) HandleCommitPress() error {
func (self *WorkingTreeHelper) HandleCommitPressWithMessage(initialMessage string) error {
if err := self.prepareFilesForCommit(); err != nil {
return self.c.Error(err)
}
@@ -107,28 +110,25 @@ func (self *WorkingTreeHelper) HandleCommitPress() error {
return self.PromptToStageAllAndRetry(self.HandleCommitPress)
}
savedCommitMessage := self.getSavedCommitMessage()
if len(savedCommitMessage) > 0 {
self.setCommitMessage(savedCommitMessage)
} else {
commitPrefixConfig := self.commitPrefixConfigForRepo()
if commitPrefixConfig != nil {
prefixPattern := commitPrefixConfig.Pattern
prefixReplace := commitPrefixConfig.Replace
rgx, err := regexp.Compile(prefixPattern)
if err != nil {
return self.c.ErrorMsg(fmt.Sprintf("%s: %s", self.c.Tr.LcCommitPrefixPatternError, err.Error()))
}
prefix := rgx.ReplaceAllString(self.refHelper.GetCheckedOutRef().Name, prefixReplace)
self.setCommitMessage(prefix)
}
}
return self.commitsHelper.OpenCommitMessagePanel(
&OpenCommitMessagePanelOpts{
CommitIndex: context.NoCommitIndex,
InitialMessage: initialMessage,
Title: self.c.Tr.CommitSummary,
PreserveMessage: true,
OnConfirm: self.handleCommit,
},
)
}
if err := self.c.PushContext(self.contexts.CommitMessage); err != nil {
return err
}
return nil
func (self *WorkingTreeHelper) handleCommit(message string) error {
cmdObj := self.git.Commit.CommitCmdObj(message)
self.c.LogAction(self.c.Tr.Actions.Commit)
_ = self.commitsHelper.PopCommitMessageContexts()
return self.gpgHelper.WithGpgHandling(cmdObj, self.c.Tr.CommittingStatus, func() error {
self.commitsHelper.OnCommitSuccess()
return nil
})
}
// HandleCommitEditorPress - handle when the user wants to commit changes via
@@ -154,9 +154,27 @@ func (self *WorkingTreeHelper) HandleWIPCommitPress() error {
return self.c.ErrorMsg(self.c.Tr.SkipHookPrefixNotConfigured)
}
self.setCommitMessage(skipHookPrefix)
return self.HandleCommitPressWithMessage(skipHookPrefix)
}
return self.HandleCommitPress()
func (self *WorkingTreeHelper) HandleCommitPress() error {
message := self.contexts.CommitMessage.GetPreservedMessage()
if message != "" {
commitPrefixConfig := self.commitPrefixConfigForRepo()
if commitPrefixConfig != nil {
prefixPattern := commitPrefixConfig.Pattern
prefixReplace := commitPrefixConfig.Replace
rgx, err := regexp.Compile(prefixPattern)
if err != nil {
return self.c.ErrorMsg(fmt.Sprintf("%s: %s", self.c.Tr.LcCommitPrefixPatternError, err.Error()))
}
prefix := rgx.ReplaceAllString(self.refHelper.GetCheckedOutRef().Name, prefixReplace)
message = prefix
}
}
return self.HandleCommitPressWithMessage(message)
}
func (self *WorkingTreeHelper) PromptToStageAllAndRetry(retry func() error) error {

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