Compare commits

...

214 Commits

Author SHA1 Message Date
Jesse Duffield
196c83d058 fix bug where cancelling search in menu caused issue 2020-08-26 09:32:57 +00:00
Jesse Duffield
806bee9646 try again 2020-08-26 08:57:32 +10:00
Jesse Duffield
45a0378c01 do not create error panel for sentinel errors 2020-08-25 22:21:15 +00:00
Jesse Duffield
afd669194a use clipboard package to handle clipboard stuff 2020-08-26 07:53:43 +10:00
Jesse Duffield
1494a3863d Remove tab keybinding for cycling tab
This keybinding has been more pain than it's worth. Having a tab keybinding
to cycle tabs implies that you can shift+tab and when you shift+tab the
application exits because termbox, our dependency, doesn't know how to
interpret the escape sequence (so it takes it for an actual ESC key which
will exit lazygit at the top level).

If people get mad at me they can set nextBlock-alt to <tab> and they'll have
the functionality back :)
2020-08-25 10:48:13 +00:00
Jesse Duffield
f5c55f066b use new branch logic when 'checking out' remote branch 2020-08-25 09:25:17 +00:00
Dawid Dziurla
bd8f198beb workflows: run CI on master or pull request only 2020-08-25 09:18:29 +02:00
Dawid Dziurla
f05adb4f99 workflows: match every branch
Previously, branches with `/` in the name wasn't being matched
2020-08-25 09:16:13 +02:00
Jesse Duffield
3ebb91c07a better keybinding ('W') for viewing diff 2020-08-24 23:08:05 +00:00
Jesse Duffield
771e87ebeb do not reset cursor unless previous file has moved position 2020-08-24 22:39:01 +00:00
Jesse Duffield
2598ce1d4b bump creack 2020-08-24 22:16:38 +00:00
Jesse Duffield
e2f3b2b41f add log when git status errors 2020-08-25 08:04:45 +10:00
Jesse Duffield
7ebb8343d1 ignore warning messages about files when obtaining file statuses 2020-08-24 11:53:17 +00:00
Jesse Duffield
42479a75af prevent moving cursor past last character in prompt modal 2020-08-24 20:11:32 +10:00
Jesse Duffield
22c7110349 prevent moving cursor past last character in prompt modal 2020-08-24 10:02:08 +00:00
Jesse Duffield
44ee28bb2e support alacritty 2020-08-24 09:19:56 +10:00
Jesse Duffield
f172f20219 Return whether the context has a parent or not along with that parent
There has got to be a better way around this but if we're returning a Context
from a function (Context is an interface, not a concrete type), even if we
return nil, on the calling end it won't be equal to nil because an interface
value is a tuple of the type and the value meaning it's never itself nil,
unless both values in the tuple are nil.

So we're explicitly returning whether or not the underlying concrete type is nil.
2020-08-23 22:30:32 +00:00
Jesse Duffield
0f7003d939 allow spamming the p key 2020-08-23 11:30:29 +00:00
Jesse Duffield
d2d88fe64e fix focus change on merge popup return 2020-08-23 19:28:59 +10:00
Jesse Duffield
fa2a385a0c when in the remote branches view, prefill name for creating branch off of remote branch 2020-08-23 19:27:34 +10:00
Jesse Duffield
bd9579983e bump gocui to ensure no crash on startup 2020-08-23 17:49:58 +10:00
Jesse Duffield
66bd86b9b7 set keybindings after initialising views 2020-08-23 17:49:58 +10:00
Jesse Duffield
364bdcf532 safer getting of branch 2020-08-23 17:49:58 +10:00
Jesse Duffield
ba7e098373 check for missing view when scrolling 2020-08-23 16:05:20 +10:00
Jesse Duffield
9f71c8d2b9 rename Status to PatchStatus 2020-08-23 15:11:06 +10:00
Jesse Duffield
fce7cdcc0a enlargen stash window when its focused 2020-08-23 15:11:06 +10:00
Jesse Duffield
4fb52ce2ab better handling of there being no commit files 2020-08-23 15:11:06 +10:00
Jesse Duffield
2915134007 show file statuses in commit files view 2020-08-23 15:11:06 +10:00
Jesse Duffield
2f893bf361 format 2020-08-23 14:29:18 +10:00
Jesse Duffield
f815c5607c prefill remote edit prompts 2020-08-23 14:29:18 +10:00
Jesse Duffield
59d61f00a6 hide secondary view when escaping patch building panel 2020-08-23 14:29:18 +10:00
Jesse Duffield
262ff24c5b always reset branch selected index when creating new branch 2020-08-23 14:29:18 +10:00
Jesse Duffield
1189c2fab7 we've now flipped the boolean 2020-08-23 14:29:18 +10:00
Jesse Duffield
3eb3de3edc allow explicitly managing focus 2020-08-23 14:29:18 +10:00
Jesse Duffield
94601b4dc9 use context to return to the correct view 2020-08-23 14:29:18 +10:00
Jesse Duffield
9ca0073cd7 attempt at fixing bad lazyloading 2020-08-23 14:29:18 +10:00
Jesse Duffield
55e6366529 run task for appropriate view 2020-08-23 14:29:18 +10:00
Jesse Duffield
bd66162972 fix up patch manager 2020-08-23 14:29:18 +10:00
Jesse Duffield
5cdfd41dca prevent spamming pull or push buttons 2020-08-23 14:29:18 +10:00
Jesse Duffield
a95fd581fd fix logic for entering merging panel 2020-08-23 14:29:18 +10:00
Jesse Duffield
fda9f4ea7a centralise logic for rendering options map 2020-08-23 14:29:18 +10:00
Jesse Duffield
f876d8fdc8 use constants 2020-08-23 14:29:18 +10:00
Jesse Duffield
4198bbae6c ensure there is always a current context 2020-08-23 14:29:18 +10:00
Jesse Duffield
ade54b38c1 cleanup 2020-08-23 14:29:18 +10:00
Jesse Duffield
0dd2c869a8 minor refactor 2020-08-23 14:29:18 +10:00
Jesse Duffield
ed85ea69bd cleanup of list context file 2020-08-23 14:29:18 +10:00
Jesse Duffield
953298de74 remove dead code 2020-08-23 14:29:18 +10:00
Jesse Duffield
628404e114 use actual keys 2020-08-23 14:29:18 +10:00
Jesse Duffield
5638a40007 carry more mode state across after returning from subprocess 2020-08-23 14:29:18 +10:00
Jesse Duffield
d6005dc0eb more accurate comment 2020-08-23 14:29:18 +10:00
Jesse Duffield
b3a7acbdad more standardising modes 2020-08-23 14:29:18 +10:00
Jesse Duffield
88ae550b93 unused method 2020-08-23 14:29:18 +10:00
Jesse Duffield
2c3f5be093 comment these things out because we're not using them yet 2020-08-23 14:29:18 +10:00
Jesse Duffield
95a4ca6f8e remove todo comment 2020-08-23 14:29:18 +10:00
Jesse Duffield
23432dd909 remove test 2020-08-23 14:29:18 +10:00
Jesse Duffield
148f601bcb cleanup now that we're always using the same diff command 2020-08-23 14:29:18 +10:00
Jesse Duffield
43d891b8d6 support creating patches from files in diff mode 2020-08-23 14:29:18 +10:00
Jesse Duffield
2eee079d3a minor rename 2020-08-23 14:29:18 +10:00
Jesse Duffield
30a555b108 don't needlessly load every file 2020-08-23 14:29:18 +10:00
Jesse Duffield
8be970e688 stop loading all the diffs at once now that we load them as we go 2020-08-23 14:29:18 +10:00
Jesse Duffield
12bf851c7d faster patch manager 2020-08-23 14:29:18 +10:00
Jesse Duffield
c837c54c39 handle diffing and filtering by file in commit files view 2020-08-23 14:29:18 +10:00
Jesse Duffield
5874529f43 deal with the fact that a nil wrapped in an interface is not equal to nil 2020-08-23 14:29:18 +10:00
Jesse Duffield
e290710f67 support drilling down into the files of a diff 2020-08-23 14:29:18 +10:00
Jesse Duffield
438abd6003 centralise code for copying to clipboard 2020-08-23 14:29:18 +10:00
Jesse Duffield
442f6cd854 more cherry picking stuff, mostly around the reflog 2020-08-23 14:29:18 +10:00
Jesse Duffield
c2b154acad better handling of our different modes and also cherry picking 2020-08-23 14:29:18 +10:00
Jesse Duffield
fbd61fcd17 refactor how we handle different modes 2020-08-23 14:29:18 +10:00
Jesse Duffield
b1529f19ad more cherry picking code into its own file 2020-08-23 14:29:18 +10:00
Jesse Duffield
134566ed49 move into more appropriate file 2020-08-23 14:29:18 +10:00
Jesse Duffield
8da93fd762 add description field to ListItem interface 2020-08-23 14:29:18 +10:00
Jesse Duffield
63209ef71e try allowing creating branches off the stash too 2020-08-23 14:29:18 +10:00
Jesse Duffield
f63ec38aae genericise creating new branches off things 2020-08-23 14:29:18 +10:00
Jesse Duffield
f858c8e750 rename to make way for a generic function name 2020-08-23 14:29:18 +10:00
Jesse Duffield
26f80087dd when toggling files reset patch manager if patch ends up empty 2020-08-23 14:29:18 +10:00
Jesse Duffield
0ac402792b allow getting the current item generically 2020-08-23 14:29:18 +10:00
Jesse Duffield
974c6510b8 add sub commit context 2020-08-23 14:29:18 +10:00
Jesse Duffield
41df63cdc4 show when building patch 2020-08-23 14:29:18 +10:00
Jesse Duffield
4080e9b501 only return focus if we already have it 2020-08-23 14:29:18 +10:00
Jesse Duffield
53da858c06 escape patch building mode on hitting escape at the top level 2020-08-23 14:29:18 +10:00
Jesse Duffield
50c9ae863a remove sdump 2020-08-23 14:29:18 +10:00
Jesse Duffield
ce20d1b482 remove clipboard option for now because we need a better way of doing it 2020-08-23 14:29:18 +10:00
Jesse Duffield
fcf916d138 don't panic 2020-08-23 14:29:18 +10:00
Jesse Duffield
f3c87bde88 more 2020-08-23 14:29:18 +10:00
Jesse Duffield
3f7136fc7d missed a spot 2020-08-23 14:29:18 +10:00
Jesse Duffield
59f5f5c1af refactor 2020-08-23 14:29:18 +10:00
Jesse Duffield
1956301b1c better menu item name 2020-08-23 14:29:18 +10:00
Jesse Duffield
1fd0f31682 only show rebasey commands on a local commit when patch building 2020-08-23 14:29:18 +10:00
Jesse Duffield
e6a1bd6566 generalise patch building stuff 2020-08-23 14:29:18 +10:00
Jesse Duffield
609f3f4bfa rename Sha to parent now that we're also considering stash entries 2020-08-23 14:29:18 +10:00
Jesse Duffield
9b42cd2214 slightly better 2020-08-23 14:29:18 +10:00
Jesse Duffield
2d90e1e8ee commit files kind of generalised 2020-08-23 14:29:18 +10:00
Jesse Duffield
ddf25e14af allowing commit files to be viewed in reflog as well 2020-08-23 14:29:18 +10:00
Jesse Duffield
48f1adad49 stop logging stack 2020-08-23 14:29:18 +10:00
Jesse Duffield
379d37a255 remove unnecessary function 2020-08-23 14:29:18 +10:00
Jesse Duffield
a59ac064d2 statically define context keys 2020-08-23 14:29:18 +10:00
Jesse Duffield
433d54fcec WIP constants for context keys 2020-08-23 14:29:18 +10:00
Jesse Duffield
146722beb8 rename to SelectedLineIdx 2020-08-23 14:29:18 +10:00
Jesse Duffield
eb5e54e9fd use interface for panel state rather than pointer 2020-08-23 14:29:18 +10:00
Jesse Duffield
99707a527d WIP 2020-08-23 14:29:18 +10:00
Jesse Duffield
9ee7793782 remove comment 2020-08-23 14:29:18 +10:00
Jesse Duffield
bc410d8e4a use camelCase 2020-08-23 14:29:18 +10:00
Jesse Duffield
7561f5aa32 some more standardisation for diffing 2020-08-23 14:29:18 +10:00
Jesse Duffield
2855b5b4d5 standardise diffmode 2020-08-23 14:29:18 +10:00
Jesse Duffield
419cb9feb8 more standardisation 2020-08-23 14:29:18 +10:00
Jesse Duffield
dbf6bb5f27 some more things 2020-08-23 14:29:18 +10:00
Jesse Duffield
f601108c5d update naming to refer to context 2020-08-23 14:29:18 +10:00
Jesse Duffield
b77abdc5e1 WIP 2020-08-23 14:29:18 +10:00
Jesse Duffield
2fac2f9f1f WIP 2020-08-23 14:29:18 +10:00
Jesse Duffield
e4beaf4de9 more stuff 2020-08-23 14:29:18 +10:00
Jesse Duffield
d4f134c6c7 WIP 2020-08-23 14:29:18 +10:00
Jesse Duffield
7ebed76d16 WIP 2020-08-23 14:29:18 +10:00
Jesse Duffield
2b812b01e9 more standardisation of rendering 2020-08-23 14:29:18 +10:00
Jesse Duffield
2f5d5034db good progress 2020-08-23 14:29:18 +10:00
Jesse Duffield
a32947e7a7 prepare for OnRender prop 2020-08-23 14:29:18 +10:00
Jesse Duffield
2fdadd383a introduce new approach to handling tab states 2020-08-23 14:29:18 +10:00
Jesse Duffield
9a2dc3fe15 stop crash due to context stack not being initialized 2020-08-23 14:29:18 +10:00
Jesse Duffield
f0c3d3fc4d centralise setting of main views context 2020-08-23 14:29:18 +10:00
Jesse Duffield
2488e0044d concurrent-safe handling of context state 2020-08-23 14:29:18 +10:00
Jesse Duffield
9c866fd49c more standardisation 2020-08-23 14:29:18 +10:00
Jesse Duffield
6c270b6e26 WIP 2020-08-23 14:29:18 +10:00
Jesse Duffield
ae1c4536e6 WIP 2020-08-23 14:29:18 +10:00
Jesse Duffield
f5b22d94d9 WIP 2020-08-23 14:29:18 +10:00
Jesse Duffield
3c87ff4eff WIP: standardising how we render to main 2020-08-23 14:29:18 +10:00
Jesse Duffield
0f7b2c45d7 centralise split main panel code 2020-08-23 14:29:18 +10:00
Jesse Duffield
a12d18146c better logic for taking focus away from popup panels 2020-08-23 14:29:18 +10:00
Jesse Duffield
119d5be1a4 move into list context file 2020-08-23 14:29:18 +10:00
Jesse Duffield
fcdc0174d9 rename context file 2020-08-23 14:29:18 +10:00
Jesse Duffield
4f4df8f9cc move context specific keybindings into context file 2020-08-23 14:29:18 +10:00
Jesse Duffield
c730271e09 minor update 2020-08-23 14:29:18 +10:00
Jesse Duffield
ac0eedda91 lots more stuff 2020-08-23 14:29:18 +10:00
Jesse Duffield
e87635295a dont check for error when sending view to bottom 2020-08-23 14:29:18 +10:00
Jesse Duffield
62a662054b hide view if not specified in dimensions object 2020-08-23 14:29:18 +10:00
Jesse Duffield
dc183c0d82 no need to set views on top anymore 2020-08-23 14:29:18 +10:00
Jesse Duffield
08e039bea9 return nil when no file selected 2020-08-23 14:29:18 +10:00
Jesse Duffield
88d329c52a WIP 2020-08-23 14:29:18 +10:00
Jesse Duffield
fd8a455aff small things
WIP
2020-08-23 14:29:18 +10:00
Jesse Duffield
ed4574bda9 standardise getting selected item 2020-08-23 14:29:18 +10:00
Jesse Duffield
c9ae54a8c8 remove previous view 2020-08-23 14:29:18 +10:00
Jesse Duffield
6fb83b740b WIP 2020-08-23 14:29:18 +10:00
Jesse Duffield
7f89113245 WIP 2020-08-23 14:29:18 +10:00
Jesse Duffield
0ea0c48631 WIP 2020-08-23 14:29:18 +10:00
Jesse Duffield
cec4cb48cb centralise some list view code 2020-08-23 14:29:18 +10:00
mjarkk
b211a14a66 Add GitUI to alternatives in readme 2020-08-23 14:05:55 +10:00
Jesse Duffield
a3d1455c83 Update README.md 2020-08-22 20:46:00 +10:00
Jesse Duffield
1716de3b59 remove space as keybinding for confirmation panel 2020-08-17 20:30:10 +10:00
Jesse Duffield
44d8b3e8f3 allow overriding default confirm/escape keybindings 2020-08-17 18:22:57 +10:00
Jesse Duffield
4f4bb40ea6 support opening lazygit outside a git directory 2020-08-16 22:59:58 +10:00
Jesse Duffield
db826b3c87 add keybinding to create new branch off of commit
retain focus in commits panel

surface prompt errors

better description
2020-08-16 22:24:54 +10:00
Jesse Duffield
be658e7d64 support multi word editor config 2020-08-16 20:37:40 +10:00
Jesse Duffield
53f06f6a4e prefill commit reword editor 2020-08-16 20:37:24 +10:00
Jesse Duffield
c8add47fe7 move cursor to right when using auto prefix 2020-08-16 18:44:39 +10:00
Jesse Duffield
28cd827cea better popups 2020-08-16 09:07:54 +10:00
Jesse Duffield
ffda2839e0 remove anonymous reporting popup cos we dont do it anymore anyway 2020-08-16 09:07:54 +10:00
Jesse Duffield
28208e8364 refactor list view 2020-08-15 18:01:43 +10:00
Jesse Duffield
9b7a6934b3 more removing of g 2020-08-15 18:01:43 +10:00
Jesse Duffield
15229bbdab more removing of g and v 2020-08-15 18:01:43 +10:00
Jesse Duffield
63e6eea9ec files view 2020-08-15 18:01:43 +10:00
Jesse Duffield
50d5b9e8e7 status view 2020-08-15 18:01:43 +10:00
Jesse Duffield
cc872b0444 menu view 2020-08-15 18:01:43 +10:00
Jesse Duffield
17b84e09c0 fix remote branches select sig 2020-08-15 18:01:43 +10:00
Jesse Duffield
43f8bae267 fix remotes select sig 2020-08-15 18:01:43 +10:00
Jesse Duffield
b0fe963f8a fix branches select sig 2020-08-15 18:01:43 +10:00
Jesse Duffield
0822a9296c rename 2020-08-15 18:01:43 +10:00
Jesse Duffield
d9fa02c53b clean up interface for popup panels 2020-08-15 18:01:43 +10:00
Jesse Duffield
c44ee71ad4 update cheatsheet 2020-08-15 11:41:37 +10:00
Jesse Duffield
826d1660c9 move patch stuff into its own package 2020-08-15 11:41:37 +10:00
Jesse Duffield
291a8e4de0 allow opening files on the selected line in the staging panel 2020-08-15 11:41:37 +10:00
Jesse Duffield
f02ccca0e0 add specs to boxlayout package 2020-08-15 09:04:40 +10:00
Jesse Duffield
1e12a60b34 move box layout stuff into its own package 2020-08-15 09:04:40 +10:00
Jesse Duffield
8430b04492 allow configurable main panel split 2020-08-13 21:50:23 +10:00
Jesse Duffield
35b72420ad support accordian mode i.e. expanding focused side panels 2020-08-13 21:50:23 +10:00
Jesse Duffield
28ba142fd6 set minimum confirmation box width 2020-08-13 21:50:23 +10:00
Jesse Duffield
b39bcd5c61 more lenient for switching into portrait mode 2020-08-13 21:50:23 +10:00
Jesse Duffield
1fd35f3824 centralise logic for information section
WIP
2020-08-13 21:50:23 +10:00
Jesse Duffield
e73937c2bd more work on new layout functionality 2020-08-13 21:50:23 +10:00
Jesse Duffield
b51ad4fcea softcode cyclable views 2020-08-13 21:50:23 +10:00
Jesse Duffield
d1a7c7283f some more changes 2020-08-13 21:50:23 +10:00
Jesse Duffield
b641ecdc74 move some things around 2020-08-13 21:50:23 +10:00
Jesse Duffield
13f567ff4c add portrait mode for when the window is really tall 2020-08-13 21:50:23 +10:00
Jesse Duffield
771d4b5811 refactor how we handle layouts 2020-08-13 21:50:23 +10:00
Jesse Duffield
3c944e0351 support force push after failure 2020-08-12 21:11:24 +10:00
Jesse Duffield
e26af258d6 allow rebasing onto remote branch 2020-08-12 20:58:34 +10:00
Jesse Duffield
76e5ec6d45 immediately quit when pressing q in diff or filter mode 2020-08-12 20:44:29 +10:00
Jesse Duffield
27cd12e2d9 accept umlaut keybindings 2020-08-12 20:07:56 +10:00
Jesse Duffield
bfaf1c4f70 use remote prefixed branch name when merging remote branch 2020-08-12 20:07:46 +10:00
Jesse Duffield
2d18d089ce allow entering a password when fast forwarding another branch 2020-08-12 18:47:16 +10:00
Jesse Duffield
9c7e40906d rename arg 2020-08-12 18:47:16 +10:00
Jesse Duffield
401f291c3b lowercase function name 2020-08-12 18:47:16 +10:00
Jesse Duffield
bea2ae5ff5 stop pulling in general 2020-08-12 18:47:16 +10:00
Jesse Duffield
f49e4946f2 minor refactor 2020-08-12 18:47:16 +10:00
Jesse Duffield
8ff74072f8 update config 2020-08-12 18:47:16 +10:00
Jesse Duffield
fcd5aea04e support multiple modes of git pull 2020-08-12 18:47:16 +10:00
Jesse Duffield
1c0da2967c update naming 2020-08-12 18:47:16 +10:00
Jesse Duffield
1b78a42b80 pass callback directly 2020-08-12 18:47:16 +10:00
Jesse Duffield
79e73d2eff minor cleanup
WIP
2020-08-12 18:47:16 +10:00
Jesse Duffield
23299f88e9 simplify patch modifier interface 2020-08-09 15:42:20 +10:00
mjarkk
ef744e45c1 Update dutch translations 2020-08-08 14:25:11 +10:00
Jesse Duffield
660cc2f3d1 follow cursor when staging and unstaging a file rename 2020-08-07 18:59:56 +10:00
Jesse Duffield
469ac116ef allow renames to be discarded 2020-08-07 18:01:26 +10:00
Jesse Duffield
a86103479b cleanup 2020-08-07 18:01:26 +10:00
Axel Navarro
d49e75bd3e Add tab keybinding in commit message 2020-07-26 16:28:01 +10:00
Jesse Duffield
f4718a9047 allow editing commit files 2020-07-21 18:24:39 +10:00
Jesse Duffield
7d5fe4b66c better logic for staging a renamed file 2020-07-19 14:11:32 +10:00
Jesse Duffield
845c80721f Decouple escaping from quitting
When a user is not entering text into a prompt, the 'q' key should immediately
quit the application. On the other hand, the 'esc' key should cancel/close/go-back
to the previous context.

If we're at the surface level (nothing to cancel/close) and the user hits the
escape key, the default behaviour is to close the app, however we now have a
`quitOnTopLevelReturn` config key to override this.

I actually think from the beginning we should have made this config option
default to false rather than true which is the default this PR gives it,
but I don't want to anger too many people familiar with the existing behaviour.
2020-07-18 20:00:48 +10:00
Gadzhi Kharkharov
0e65db10d8 add solus linux installation info 2020-07-18 19:48:05 +10:00
Jesse Duffield
a9cc321981 prompt to create new branch if branch not found 2020-07-17 09:20:50 +10:00
Jesse Duffield
6349214f00 prompt to commit all files if committing with no staged files 2020-07-17 09:01:40 +10:00
Randshot
96f821b841 fix TestGitCommandCommit test
Signed-off-by: Randshot <randshot@norealm.xyz>
2020-07-15 09:41:16 +10:00
Randshot
964e3872c1 revert changes to 'os_default_platform.go' and 'os_windows.go'
Signed-off-by: Randshot <randshot@norealm.xyz>
2020-07-15 09:41:16 +10:00
Randshot
5dfa26ea8b use strconv for quoting in 'GitCommand.Commit' and 'OSCommand.ShellCommandFromString'
use raw strings for the escaped quotes in 'os_default_platform.go' and 'os_windows.go'

Signed-off-by: Randshot <randshot@norealm.xyz>
2020-07-15 09:41:16 +10:00
Dawid Dziurla
dbf042b8ad goreleaser: fix deprecation and comment 2020-07-14 09:13:43 +02:00
Randshot
014e06eefd factor out duplicate code into 'ShellCommandFromString'
Signed-off-by: Randshot <randshot@norealm.xyz>
2020-07-14 08:26:53 +10:00
Randshot
39a2122dc0 add quotes around the git commit command on non-windows systems
Signed-off-by: Randshot <randshot@norealm.xyz>
2020-07-14 08:26:53 +10:00
252 changed files with 9547 additions and 4376 deletions

1
.github/FUNDING.yml vendored
View File

@@ -1,5 +1,4 @@
# These are supported funding model platforms
github: [jesseduffield]
ko_fi: jesseduffield
custom: ['https://donorbox.org/lazygit']

View File

@@ -3,8 +3,8 @@ name: Continuous Integration
on:
push:
branches:
- '*'
pull_request: []
- master
pull_request:
jobs:
ci:

View File

@@ -40,8 +40,8 @@ changelog:
- '^bump'
brews:
-
# Reporitory to push the tap to.
github:
# Repository to push the tap to.
tap:
owner: jesseduffield
name: homebrew-lazygit
@@ -61,5 +61,3 @@ brews:
# conflicts:
# - svn
# - bash
# test comment to see if goreleaser only releases on new commits

View File

@@ -1,7 +1,12 @@
# lazygit
<p align="center">
<img src="https://i.imgur.com/oYB7Cj8.png">
</p>
![CI](https://github.com/jesseduffield/lazygit/workflows/Continuous%20Integration/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/jesseduffield/lazygit)](https://goreportcard.com/report/github.com/jesseduffield/lazygit) [![GolangCI](https://golangci.com/badges/github.com/jesseduffield/lazygit.svg)](https://golangci.com) [![GoDoc](https://godoc.org/github.com/jesseduffield/lazygit?status.svg)](http://godoc.org/github.com/jesseduffield/lazygit) [![GitHub tag](https://img.shields.io/github/tag/jesseduffield/lazygit.svg)]() [![TODOs](https://badgen.net/https/api.tickgit.com/badgen/github.com/jesseduffield/lazygit)](https://www.tickgit.com/browse?repo=github.com/jesseduffield/lazygit)
A simple terminal UI for git commands, written in Go with the [gocui](https://github.com/jroimartin/gocui "gocui") library.
Rant time: You've heard it before, git is _powerful_, but what good is that power when everything is so damn hard to do? Interactive rebasing requires you to edit a goddamn TODO file in your editor? *Are you kidding me?* To stage part of a file you need to use a command line program to step through each hunk and if a hunk can't be split down any further but contains code you don't want to stage, you have to edit an arcane patch file _by hand_? *Are you KIDDING me?!* Sometimes you get asked to stash your changes when switching branches only to realise that after you switch and unstash that there weren't even any conflicts and it would have been fine to just checkout the branch directly? *YOU HAVE GOT TO BE KIDDING ME!*
@@ -21,6 +26,7 @@ If you're a mere mortal like me and you're tired of hearing how powerful git is
- [Scoop (Windows)](#scoop-windows)
- [Arch Linux](#arch-linux)
- [Fedora and CentOS 7](#fedora-and-centos-7)
- [Solus Linux](#solus-linux)
- [FreeBSD](#freebsd)
- [Conda](#conda)
- [Go](#go)
@@ -125,6 +131,12 @@ sudo dnf copr enable atim/lazygit -y
sudo dnf install lazygit
```
### Solus Linux
```sh
sudo eopkg install lazygit
```
### FreeBSD
```sh
@@ -253,4 +265,5 @@ If you want to see what I (Jesse) am up to in terms of development, follow me on
If you find that lazygit doesn't quite satisfy your requirements, these may be a better fit:
- [GitUI](https://github.com/Extrawurst/gitui)
- [tig](https://github.com/jonas/tig)

View File

@@ -14,6 +14,8 @@ Default path for the config file:
scrollHeight: 2 # how many lines you scroll by
scrollPastBottom: true # enable scrolling past the bottom
sidePanelWidth: 0.3333 # number from 0 to 1
expandFocusedSidePanel: false
mainPanelSplitMode: 'flexible' # one of 'horizontal' | 'flexible' | 'vertical'
theme:
lightTheme: false # For terminals with a light background
activeBorderColor:
@@ -41,6 +43,8 @@ Default path for the config file:
manualCommit: false
# extra args passed to `git merge`, e.g. --no-ff
args: ""
pull:
mode: 'merge' # one of 'merge' | 'rebase' | 'ff-only'
skipHookPrefix: WIP
autoFetch: true
branchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --"
@@ -50,6 +54,8 @@ Default path for the config file:
days: 14 # how often an update is checked for
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
confirmOnQuit: false
# determines whether hitting 'esc' will quit the application when there is nothing to cancel/close
quitOnTopLevelReturn: true
keybinding:
universal:
quit: 'q'
@@ -75,6 +81,8 @@ Default path for the config file:
optionMenu-alt1: '?' # show help menu
select: '<space>'
goInto: '<enter>'
confirm: '<enter>'
confirm-alt1: 'y'
remove: 'd'
new: 'n'
edit: 'e'
@@ -98,7 +106,8 @@ Default path for the config file:
undo: 'z'
redo: '<c-z>'
filteringMenu: '<c-s>'
diffingMenu: '<c-e>'
diffingMenu: 'W'
diffingMenu-alt: '<c-e>' # deprecated
copyToClipboard: '<c-o>'
status:
checkForUpdate: 'u'
@@ -185,7 +194,7 @@ for users of VSCode
```yaml
os:
openCommand: 'code -r {{filename}}'
openCommand: 'code -rg {{filename}}'
```
## Color Attributes

View File

@@ -16,6 +16,8 @@
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode
<kbd>:</kbd>: execute custom command
<kbd>|</kbd>: view scoping options
<kbd>∂</kbd>: open diff menu
</pre>
## Branches Panel
@@ -40,6 +42,7 @@
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>g</kbd>: view reset options
<kbd>R</kbd>: rename branch
<kbd>ctrl+o</kbd>: copy branch name to clipboard
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
@@ -50,9 +53,10 @@
## Branches Panel (Remote Branches (in Remotes tab))
<pre>
<kbd>esc</kbd>: return to remotes list
<kbd>esc</kbd>: Return to remotes list
<kbd>g</kbd>: view reset options
<kbd>space</kbd>: checkout
<kbd>n</kbd>: new branch
<kbd>M</kbd>: merge into currently checked out branch
<kbd>d</kbd>: delete branch
<kbd>r</kbd>: rebase checked-out branch onto this branch
@@ -100,6 +104,7 @@
<kbd>c</kbd>: checkout file
<kbd>d</kbd>: discard this commit's changes to this file
<kbd>o</kbd>: open file
<kbd>e</kbd>: edit file
<kbd>space</kbd>: toggle file included in patch
<kbd>enter</kbd>: enter file to add selected lines to the patch
<kbd>,</kbd>: previous page
@@ -134,11 +139,12 @@
<kbd>p</kbd>: pick commit (when mid-rebase)
<kbd>t</kbd>: revert commit
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>v</kbd>: paste commits (cherry-pick)
<kbd>enter</kbd>: view commit's files
<kbd>space</kbd>: checkout commit
<kbd>i</kbd>: select commit to diff with another commit
<kbd>n</kbd>: create new branch off of commit
<kbd>T</kbd>: tag commit
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>,</kbd>: previous page
@@ -211,6 +217,7 @@
<pre>
<kbd>esc</kbd>: exit line-by-line mode
<kbd>o</kbd>: open file
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
@@ -228,6 +235,7 @@
<kbd>space</kbd>: toggle line staged / unstaged
<kbd>d</kbd>: delete change (git reset)
<kbd>tab</kbd>: switch to other panel
<kbd>o</kbd>: open file
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
@@ -246,7 +254,6 @@
<pre>
<kbd>esc</kbd>: close menu
<kbd>q</kbd>: close menu
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top

View File

@@ -1,31 +1,33 @@
# Lazygit Keybindings
# Lazygit Sneltoetsen
## Global
## Globaale Sneltoetsen
<pre>
<kbd>pgup</kbd>: scroll up main panel (fn+up)
<kbd>pgdown</kbd>: scroll down main panel (fn+down)
<kbd>pgup</kbd>: scroll naar beneden vanaf hooft paneel (fn+up)
<kbd>pgdown</kbd>: scroll naar beneden vabaf hooft paneel (fn+down)
<kbd>m</kbd>: bekijk merge/rebase opties
<kbd>ctrl+p</kbd>: view custom patch options
<kbd>ctrl+p</kbd>: bekijk aangepaste patch opties
<kbd>P</kbd>: push
<kbd>p</kbd>: pull
<kbd>R</kbd>: verversen
<kbd>x</kbd>: open menu
<kbd>z</kbd>: undo (via reflog) (experimental)
<kbd>ctrl+z</kbd>: redo (via reflog) (experimental)
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode
<kbd>z</kbd>: ongedaan maken (via reflog) (experimenteel)
<kbd>ctrl+z</kbd>: redo (via reflog) (experimenteel)
<kbd>+</kbd>: volgende schermmode (normaal/half/groot )
<kbd>_</kbd>: vorige schermmode
<kbd>:</kbd>: voor aangepast commando uit
<kbd>|</kbd>: bekijk scoping opties
<kbd>∂</kbd>: open diff menu
</pre>
## Branches Panel
## Branches Paneel
<pre>
<kbd>]</kbd>: next tab
<kbd>[</kbd>: previous tab
<kbd>]</kbd>: volgende tab
<kbd>[</kbd>: vorige tab
</pre>
## Branches Panel (Branches Tab)
## Branches Paneel (Branches Tab)
<pre>
<kbd>space</kbd>: uitchecken
@@ -36,131 +38,135 @@
<kbd>d</kbd>: verwijder branch
<kbd>r</kbd>: rebase branch
<kbd>M</kbd>: merge in met huidige checked out branch
<kbd>i</kbd>: show git-flow options
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>i</kbd>: laat git-flow opties zien
<kbd>f</kbd>: fast-forward deze branch vanaf zijn upstream
<kbd>g</kbd>: bekijk reset opties
<kbd>R</kbd>: rename branch
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
<kbd>R</kbd>: hernoem branch
<kbd>ctrl+o</kbd>: copieer branch name naar clipboard
<kbd>,</kbd>: vorige pagina
<kbd>.</kbd>: volgende pagina
<kbd><</kbd>: scroll naar boven
<kbd>/</kbd>: start met zoekken
<kbd>></kbd>: scroll naar beneden
</pre>
## Branches Panel (Remote Branches (in Remotes tab))
## Branches Paneel (Remote Branches (in Remotes tab))
<pre>
<kbd>esc</kbd>: return to remotes list
<kbd>esc</kbd>: Ga terug naar remotes lijst
<kbd>g</kbd>: bekijk reset opties
<kbd>space</kbd>: uitchecken
<kbd>n</kbd>: nieuwe branch
<kbd>M</kbd>: merge in met huidige checked out branch
<kbd>d</kbd>: verwijder branch
<kbd>r</kbd>: rebase branch
<kbd>u</kbd>: set as upstream of checked-out branch
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
<kbd>u</kbd>: stel in als upstream van uitgecheckte branch
<kbd>,</kbd>: vorige pagina
<kbd>.</kbd>: volgende pagina
<kbd><</kbd>: scroll naar boven
<kbd>/</kbd>: start met zoekken
<kbd>></kbd>: scroll naar beneden
</pre>
## Branches Panel (Remotes Tab)
## Branches Paneel (Remotes Tab)
<pre>
<kbd>f</kbd>: fetch remote
<kbd>n</kbd>: add new remote
<kbd>d</kbd>: remove remote
<kbd>e</kbd>: edit remote
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
<kbd>n</kbd>: voeg een nieuwe remote toe
<kbd>d</kbd>: verwijder remote
<kbd>e</kbd>: wijzig remote
<kbd>,</kbd>: vorige pagina
<kbd>.</kbd>: volgende pagina
<kbd><</kbd>: scroll naar boven
<kbd>/</kbd>: start met zoekken
<kbd>></kbd>: scroll naar beneden
</pre>
## Branches Panel (Tags Tab)
## Branches Paneel (Tags Tab)
<pre>
<kbd>space</kbd>: uitchecken
<kbd>d</kbd>: delete tag
<kbd>d</kbd>: verwijder tag
<kbd>P</kbd>: push tag
<kbd>n</kbd>: create tag
<kbd>n</kbd>: creëer tag
<kbd>g</kbd>: bekijk reset opties
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
<kbd>,</kbd>: vorige pagina
<kbd>.</kbd>: volgende pagina
<kbd><</kbd>: scroll naar boven
<kbd>/</kbd>: start met zoekken
<kbd>></kbd>: scroll naar beneden
</pre>
## Commit bestanden Panel
## Commit bestanden Paneel
<pre>
<kbd>esc</kbd>: ga terug
<kbd>c</kbd>: bestand uitchecken
<kbd>d</kbd>: uitsluit deze commit zijn veranderingen aan dit bestand
<kbd>o</kbd>: open bestand
<kbd>space</kbd>: toggle file included in patch
<kbd>enter</kbd>: enter file to add selected lines to the patch
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
<kbd>e</kbd>: verander bestand
<kbd>space</kbd>: toggle bestand inbegrepen in patch
<kbd>enter</kbd>: enter bestand to add selecteered lines to the patch
<kbd>,</kbd>: vorige pagina
<kbd>.</kbd>: volgende pagina
<kbd><</kbd>: scroll naar boven
<kbd>/</kbd>: start met zoekken
<kbd>></kbd>: scroll naar beneden
</pre>
## Commits Panel
## Commits Paneel
<pre>
<kbd>]</kbd>: next tab
<kbd>[</kbd>: previous tab
<kbd>]</kbd>: volgende tab
<kbd>[</kbd>: vorige tab
</pre>
## Commits Panel (Commits Tab)
## Commits Paneel (Commits Tab)
<pre>
<kbd>s</kbd>: squash beneden
<kbd>r</kbd>: hernoem commit
<kbd>R</kbd>: rename commit with editor
<kbd>R</kbd>: hernoem commit met editor
<kbd>g</kbd>: reset naar deze commit
<kbd>f</kbd>: Fixup commit
<kbd>F</kbd>: creëer fixup commit voor deze commit
<kbd>S</kbd>: squash bovenstaande commits
<kbd>d</kbd>: verwijder commit
<kbd>ctrl+j</kbd>: verplaats commit 1 omlaag
<kbd>ctrl+k</kbd>: verplaats commit 1 omhoog
<kbd>e</kbd>: verander commit
<kbd>ctrl+j</kbd>: verplaats commit 1 naar beneden
<kbd>ctrl+k</kbd>: verplaats commit 1 naar boven
<kbd>e</kbd>: wijzig commit
<kbd>A</kbd>: wijzig commit met staged veranderingen
<kbd>p</kbd>: pick commit (when mid-rebase)
<kbd>t</kbd>: commit omgedaan maken
<kbd>p</kbd>: kies commit (wanneer midden in rebase)
<kbd>t</kbd>: commit ongedaan maken
<kbd>c</kbd>: kopiëer commit (cherry-pick)
<kbd>ctrl+o</kbd>: copieer commit SHA naar clipboard
<kbd>C</kbd>: kopiëer commit reeks (cherry-pick)
<kbd>v</kbd>: plak commits (cherry-pick)
<kbd>enter</kbd>: bekijk gecommite bestanden
<kbd>space</kbd>: checkout commit
<kbd>i</kbd>: select commit to diff with another commit
<kbd>n</kbd>: create new branch off of commit
<kbd>T</kbd>: tag commit
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
<kbd>ctrl+r</kbd>: reset cherry-picked (gecopieerde) commits selectie
<kbd>,</kbd>: vorige pagina
<kbd>.</kbd>: volgende pagina
<kbd><</kbd>: scroll naar boven
<kbd>/</kbd>: start met zoekken
<kbd>></kbd>: scroll naar beneden
</pre>
## Commits Panel (Reflog Tab)
## Commits Paneel (Reflog Tab)
<pre>
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: bekijk reset opties
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
<kbd>,</kbd>: vorige pagina
<kbd>.</kbd>: volgende pagina
<kbd><</kbd>: scroll naar boven
<kbd>/</kbd>: start met zoekken
<kbd>></kbd>: scroll naar beneden
</pre>
## Bestanden Panel
## Bestanden Paneel
<pre>
<kbd>c</kbd>: Commit veranderingen
@@ -174,25 +180,25 @@
<kbd>i</kbd>: voeg toe aan .gitignore
<kbd>r</kbd>: refresh bestanden
<kbd>s</kbd>: stash-bestanden
<kbd>S</kbd>: view stash options
<kbd>S</kbd>: bekijk stash opties
<kbd>a</kbd>: toggle staged alle
<kbd>D</kbd>: bekijk reset opties
<kbd>enter</kbd>: stage individuele hunks/lijnen
<kbd>f</kbd>: fetch
<kbd>g</kbd>: view upstream reset options
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
<kbd>g</kbd>: bekijk upstream reset opties
<kbd>,</kbd>: vorige pagina
<kbd>.</kbd>: volgende pagina
<kbd><</kbd>: scroll naar boven
<kbd>/</kbd>: start met zoekken
<kbd>></kbd>: scroll naar beneden
</pre>
## Hoofd Panel (Merging)
## Hooft Paneel (Merggen)
<pre>
<kbd>esc</kbd>: ga terug naar het bestanden paneel
<kbd>space</kbd>: pick hunk
<kbd>b</kbd>: pick beide hunks
<kbd>space</kbd>: kies hunk
<kbd>b</kbd>: kies bijde hunks
<kbd>◄</kbd>: selecteer voorgaand conflict
<kbd>►</kbd>: selecteer volgende conflict
<kbd>▲</kbd>: selecteer bovenste hunk
@@ -200,78 +206,79 @@
<kbd>z</kbd>: ongedaan maken
</pre>
## Hoofd Panel (Normaal)
## Hooft Paneel (Normaal)
<pre>
<kbd> ̄</kbd>: scroll omlaag (fn+up)
<kbd>¦</kbd>: scroll omhoog (fn+down)
</pre>
## Hoofd Panel (Patch Building)
## Hooft Paneel (Patch Bouwen)
<pre>
<kbd>esc</kbd>: exit line-by-line mode
<kbd>esc</kbd>: sluit lijn-bij-lijn mode
<kbd>o</kbd>: open bestand
<kbd>▲</kbd>: selecteer de vorige lijn
<kbd>▼</kbd>: selecteer de volgende lijn
<kbd>◄</kbd>: selecteer de vorige hunk
<kbd>►</kbd>: selecteer de volgende hunk
<kbd>space</kbd>: add/remove line(s) to patch
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
<kbd>space</kbd>: voeg toe/verwijder lijn(en) in patch
<kbd>v</kbd>: toggle drag selecteer
<kbd>V</kbd>: toggle drag selecteer
<kbd>a</kbd>: toggle selecteer hunk
</pre>
## Hoofd Panel (Stage Lines/Hunks)
## Hooft Paneel (Staging)
<pre>
<kbd>esc</kbd>: ga terug naar het bestanden paneel
<kbd>space</kbd>: toggle line staged / unstaged
<kbd>d</kbd>: delete change (git reset)
<kbd>tab</kbd>: switch to other panel
<kbd>space</kbd>: toggle lijnen staged / unstaged
<kbd>d</kbd>: verwijdert change (git reset)
<kbd>tab</kbd>: ga naar een ander paneel
<kbd>o</kbd>: open bestand
<kbd>▲</kbd>: selecteer de vorige lijn
<kbd>▼</kbd>: selecteer de volgende lijn
<kbd>◄</kbd>: selecteer de vorige hunk
<kbd>►</kbd>: selecteer de volgende hunk
<kbd>e</kbd>: verander bestand
<kbd>o</kbd>: open bestand
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
<kbd>v</kbd>: toggle drag selecteer
<kbd>V</kbd>: toggle drag selecteer
<kbd>a</kbd>: toggle selecteer hunk
<kbd>c</kbd>: Commit veranderingen
<kbd>w</kbd>: commit veranderingen zonder pre-commit hook
<kbd>C</kbd>: commit veranderingen met de git editor
</pre>
## Menu Panel
## Menu Paneel
<pre>
<kbd>esc</kbd>: close menu
<kbd>q</kbd>: close menu
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
<kbd>esc</kbd>: sluit menu
<kbd>,</kbd>: vorige pagina
<kbd>.</kbd>: volgende pagina
<kbd><</kbd>: scroll naar boven
<kbd>/</kbd>: start met zoekken
<kbd>></kbd>: scroll naar beneden
</pre>
## Stash Panel
## Stash Paneel
<pre>
<kbd>space</kbd>: toepassen
<kbd>g</kbd>: pop
<kbd>d</kbd>: drop
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
<kbd>d</kbd>: laten vallen
<kbd>,</kbd>: vorige pagina
<kbd>.</kbd>: volgende pagina
<kbd><</kbd>: scroll naar boven
<kbd>/</kbd>: start met zoekken
<kbd>></kbd>: scroll naar beneden
</pre>
## Status Panel
## Status Paneel
<pre>
<kbd>e</kbd>: verander config file
<kbd>o</kbd>: open config file
<kbd>e</kbd>: verander config bestand
<kbd>o</kbd>: open config bestand
<kbd>u</kbd>: check voor updates
<kbd>enter</kbd>: wissel naar een recente repo
</pre>

View File

@@ -16,6 +16,8 @@
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode
<kbd>:</kbd>: execute custom command
<kbd>|</kbd>: view scoping options
<kbd>∂</kbd>: open diff menu
</pre>
## Gałęzie Panel
@@ -40,6 +42,7 @@
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>g</kbd>: view reset options
<kbd>R</kbd>: rename branch
<kbd>ctrl+o</kbd>: copy branch name to clipboard
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
@@ -53,6 +56,7 @@
<kbd>esc</kbd>: return to remotes list
<kbd>g</kbd>: view reset options
<kbd>space</kbd>: przełącz
<kbd>n</kbd>: nowa gałąź
<kbd>M</kbd>: scal do obecnej gałęzi
<kbd>d</kbd>: usuń gałąź
<kbd>r</kbd>: rebase branch
@@ -100,6 +104,7 @@
<kbd>c</kbd>: checkout file
<kbd>d</kbd>: discard this commit's changes to this file
<kbd>o</kbd>: otwórz plik
<kbd>e</kbd>: edytuj plik
<kbd>space</kbd>: toggle file included in patch
<kbd>enter</kbd>: enter file to add selected lines to the patch
<kbd>,</kbd>: previous page
@@ -134,11 +139,12 @@
<kbd>p</kbd>: pick commit (when mid-rebase)
<kbd>t</kbd>: revert commit
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>v</kbd>: paste commits (cherry-pick)
<kbd>enter</kbd>: view commit's files
<kbd>space</kbd>: checkout commit
<kbd>i</kbd>: select commit to diff with another commit
<kbd>n</kbd>: create new branch off of commit
<kbd>T</kbd>: tag commit
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>,</kbd>: previous page
@@ -211,6 +217,7 @@
<pre>
<kbd>esc</kbd>: exit line-by-line mode
<kbd>o</kbd>: otwórz plik
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
@@ -228,6 +235,7 @@
<kbd>space</kbd>: toggle line staged / unstaged
<kbd>d</kbd>: delete change (git reset)
<kbd>tab</kbd>: switch to other panel
<kbd>o</kbd>: otwórz plik
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
@@ -246,7 +254,6 @@
<pre>
<kbd>esc</kbd>: close menu
<kbd>q</kbd>: close menu
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top

10
go.mod
View File

@@ -3,18 +3,19 @@ module github.com/jesseduffield/lazygit
go 1.14
require (
github.com/atotto/clipboard v0.1.2
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
github.com/creack/pty v1.1.10-0.20191209115840-8ab47f72e854
github.com/creack/pty v1.1.11
github.com/fatih/color v1.7.0
github.com/fsnotify/fsnotify v1.4.7
github.com/go-errors/errors v1.0.2
github.com/go-errors/errors v1.1.1
github.com/go-git/go-git/v5 v5.0.0
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
github.com/golang/protobuf v1.3.2 // indirect
github.com/google/go-cmp v0.3.1 // indirect
github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/gocui v0.3.1-0.20200513110002-8cde0b9be542
github.com/jesseduffield/termbox-go v0.0.0-20200405031649-4dc645f7e8ba // indirect
github.com/jesseduffield/gocui v0.3.1-0.20200824100831-1b6ec5d7d449
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe // indirect
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/mattn/go-colorable v0.1.4 // indirect
@@ -34,6 +35,7 @@ require (
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
github.com/stretchr/testify v1.4.0
github.com/tcnksm/go-gitconfig v0.1.2
golang.org/x/sys v0.0.0-20200824131525-c12d262b63d8 // indirect
golang.org/x/text v0.3.2
gopkg.in/yaml.v2 v2.2.7
)

28
go.sum
View File

@@ -12,6 +12,8 @@ github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYU
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY=
github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
@@ -24,8 +26,8 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.10-0.20191209115840-8ab47f72e854 h1:NB4neYMzyBsw52kUdkTrQm4Q05ErObCdwLvJptpfJSc=
github.com/creack/pty v1.1.10-0.20191209115840-8ab47f72e854/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -42,10 +44,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
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.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-errors/errors v1.0.2 h1:xMxH9j2fNg/L4hLn/4y3M0IUsn0M6Wbu/Uh9QlOfBh4=
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.1.1 h1:ljK/pL5ltg3qoN+OtN6yCv9HWSfMwxSx90GJCZQxYNg=
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM=
@@ -88,14 +88,10 @@ github.com/integrii/flaggy v1.4.0 h1:A1x7SYx4jqu5NSrY14z8Z+0UyX2S5ygfJJrfolWR3zM
github.com/integrii/flaggy v1.4.0/go.mod h1:tnTxHeTJbah0gQ6/K0RW0J7fMUBk9MCF5blhm43LNpI=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jesseduffield/gocui v0.3.1-0.20200309001002-7765949e1c8a h1:JSORQue6V4bMppr22dtUuYX+w79cgupo66PcGZ9ijlU=
github.com/jesseduffield/gocui v0.3.1-0.20200309001002-7765949e1c8a/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20200513110002-8cde0b9be542 h1:ezzJM/NZh5vgdHWupW4K6lWsnmVADzLqFa2E3zB2bzA=
github.com/jesseduffield/gocui v0.3.1-0.20200513110002-8cde0b9be542/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/termbox-go v0.0.0-20200130214842-1d31d1faa3c9 h1:iBBk1lhFwjwJw//J2m1yyz9S368GeXQTpMVACTyQMh0=
github.com/jesseduffield/termbox-go v0.0.0-20200130214842-1d31d1faa3c9/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4=
github.com/jesseduffield/termbox-go v0.0.0-20200405031649-4dc645f7e8ba h1:hWBdYchM9nZ2+GldroQQ627ISOC83iRDdpfwY+uyJeI=
github.com/jesseduffield/termbox-go v0.0.0-20200405031649-4dc645f7e8ba/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4=
github.com/jesseduffield/gocui v0.3.1-0.20200824100831-1b6ec5d7d449 h1:G5Cm2QuFil8fnrMqUHYFiUkVSS/SXnn3ATtU7MbMFI0=
github.com/jesseduffield/gocui v0.3.1-0.20200824100831-1b6ec5d7d449/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe h1:qsVhCf2RFyyKIUe/+gJblbCpXMUki9rZrHuEctg6M/E=
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
@@ -125,8 +121,6 @@ github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0=
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
@@ -249,6 +243,8 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200824131525-c12d262b63d8 h1:AvbQYmiaaaza3cW3QXRyPo5kYgpFIzOAfeAAN7m3qQ4=
golang.org/x/sys v0.0.0-20200824131525-c12d262b63d8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=

View File

@@ -113,7 +113,8 @@ func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
return app, err
}
if err := app.setupRepo(); err != nil {
showRecentRepos, err := app.setupRepo()
if err != nil {
return app, err
}
@@ -121,36 +122,49 @@ func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
if err != nil {
return app, err
}
app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config, app.Updater, filterPath)
app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config, app.Updater, filterPath, showRecentRepos)
if err != nil {
return app, err
}
return app, nil
}
func (app *App) setupRepo() error {
func (app *App) setupRepo() (bool, error) {
// if we are not in a git repo, we ask if we want to `git init`
if err := app.OSCommand.RunCommand("git status"); err != nil {
cwd, err := os.Getwd()
if err != nil {
return err
return false, err
}
info, _ := os.Stat(filepath.Join(cwd, ".git"))
if info != nil && info.IsDir() {
return err // Current directory appears to be a git repository.
return false, err // Current directory appears to be a git repository.
}
// Offer to initialize a new repository in current directory.
fmt.Print(app.Tr.SLocalize("CreateRepo"))
response, _ := bufio.NewReader(os.Stdin).ReadString('\n')
if strings.Trim(response, " \n") != "y" {
// check if we have a recent repo we can open
recentRepos := app.Config.GetAppState().RecentRepos
if len(recentRepos) > 0 {
var err error
// try opening each repo in turn, in case any have been deleted
for _, repoDir := range recentRepos {
if err = os.Chdir(repoDir); err == nil {
return true, nil
}
}
return false, err
}
os.Exit(1)
}
if err := app.OSCommand.RunCommand("git init"); err != nil {
return err
return false, err
}
}
return nil
return false, nil
}
func (app *App) Run() error {
@@ -204,7 +218,7 @@ func (app *App) KnownError(err error) (string, bool) {
mappings := []errorMapping{
{
originalError: "fatal: not a git repository (or any of the parent directories): .git",
originalError: "fatal: not a git repository",
newError: app.Tr.SLocalize("notARepository"),
},
}

View File

@@ -12,3 +12,15 @@ type Branch struct {
UpstreamName string
Head bool
}
func (b *Branch) RefName() string {
return b.Name
}
func (b *Branch) ID() string {
return b.RefName()
}
func (b *Branch) Description() string {
return b.RefName()
}

View File

@@ -1,5 +1,7 @@
package commands
import "fmt"
// Commit : A git commit
type Commit struct {
Sha string
@@ -18,3 +20,15 @@ func (c *Commit) ShortSha() string {
}
return c.Sha[:8]
}
func (c *Commit) RefName() string {
return c.Sha
}
func (c *Commit) ID() string {
return c.RefName()
}
func (c *Commit) Description() string {
return fmt.Sprintf("%s %s", c.Sha[:7], c.Name)
}

View File

@@ -2,18 +2,20 @@ package commands
// CommitFile : A git commit file
type CommitFile struct {
Sha string
Name string
DisplayString string
Status int // one of 'WHOLE' 'PART' 'NONE'
// Parent is the identifier of the parent object e.g. a commit SHA if this commit file is for a commit, or a stash entry ref like 'stash@{1}'
Parent string
Name string
// PatchStatus tells us whether the file has been wholly or partially added to a patch. We might want to pull this logic up into the gui package and make it a map like we do with cherry picked commits
PatchStatus int // one of 'WHOLE' 'PART' 'NONE'
ChangeStatus string // e.g. 'A' for added or 'M' for modified. This is based on the result from git diff --name-status
}
const (
// UNSELECTED is for when the commit file has not been added to the patch in any way
UNSELECTED = iota
// WHOLE is for when you want to add the whole diff of a file to the patch,
// including e.g. if it was deleted
WHOLE = iota
// PART is for when you're only talking about specific lines that have been modified
PART
)
func (f *CommitFile) ID() string {
return f.Name
}
func (f *CommitFile) Description() string {
return f.Name
}

View File

@@ -37,14 +37,14 @@ type CommitListBuilder struct {
}
// NewCommitListBuilder builds a new commit list builder
func NewCommitListBuilder(log *logrus.Entry, gitCommand *GitCommand, osCommand *OSCommand, tr *i18n.Localizer, cherryPickedCommits []*Commit) (*CommitListBuilder, error) {
func NewCommitListBuilder(log *logrus.Entry, gitCommand *GitCommand, osCommand *OSCommand, tr *i18n.Localizer, cherryPickedCommits []*Commit) *CommitListBuilder {
return &CommitListBuilder{
Log: log,
GitCommand: gitCommand,
OSCommand: osCommand,
Tr: tr,
CherryPickedCommits: cherryPickedCommits,
}, nil
}
}
// extractCommitFromLine takes a line from a git log and extracts the sha, message, date, and tag if present
@@ -82,33 +82,40 @@ func (c *CommitListBuilder) extractCommitFromLine(line string) *Commit {
}
type GetCommitsOptions struct {
Limit bool
FilterPath string
Limit bool
FilterPath string
IncludeRebaseCommits bool
RefName string // e.g. "HEAD" or "my_branch"
}
// GetCommits obtains the commits of the current branch
func (c *CommitListBuilder) GetCommits(options GetCommitsOptions) ([]*Commit, error) {
func (c *CommitListBuilder) GetCommits(opts GetCommitsOptions) ([]*Commit, error) {
commits := []*Commit{}
var rebasingCommits []*Commit
rebaseMode, err := c.GitCommand.RebaseMode()
if err != nil {
return nil, err
}
if rebaseMode != "" && options.FilterPath == "" {
// here we want to also prepend the commits that we're in the process of rebasing
rebasingCommits, err = c.getRebasingCommits(rebaseMode)
rebaseMode := ""
if opts.IncludeRebaseCommits {
var err error
rebaseMode, err = c.GitCommand.RebaseMode()
if err != nil {
return nil, err
}
if len(rebasingCommits) > 0 {
commits = append(commits, rebasingCommits...)
if rebaseMode != "" && opts.FilterPath == "" {
// here we want to also prepend the commits that we're in the process of rebasing
rebasingCommits, err = c.getRebasingCommits(rebaseMode)
if err != nil {
return nil, err
}
if len(rebasingCommits) > 0 {
commits = append(commits, rebasingCommits...)
}
}
}
unpushedCommits := c.getUnpushedCommits()
cmd := c.getLogCmd(options)
unpushedCommits := c.getUnpushedCommits(opts.RefName)
cmd := c.getLogCmd(opts)
err = RunLineOutputCmd(cmd, func(line string) (bool, error) {
err := RunLineOutputCmd(cmd, func(line string) (bool, error) {
if strings.Split(line, " ")[0] != "gpg:" {
commit := c.extractCommitFromLine(line)
_, unpushed := unpushedCommits[commit.ShortSha()]
@@ -287,9 +294,9 @@ func (c *CommitListBuilder) getMergeBase() (string, error) {
// getUnpushedCommits Returns the sha's of the commits that have not yet been pushed
// to the remote branch of the current branch, a map is returned to ease look up
func (c *CommitListBuilder) getUnpushedCommits() map[string]bool {
func (c *CommitListBuilder) getUnpushedCommits(refName string) map[string]bool {
pushables := map[string]bool{}
o, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..HEAD --abbrev-commit --abbrev=8")
o, err := c.OSCommand.RunCommandWithOutput("git rev-list %s@{u}..%s --abbrev-commit --abbrev=8", refName, refName)
if err != nil {
return pushables
}
@@ -301,16 +308,16 @@ func (c *CommitListBuilder) getUnpushedCommits() map[string]bool {
}
// getLog gets the git log.
func (c *CommitListBuilder) getLogCmd(options GetCommitsOptions) *exec.Cmd {
func (c *CommitListBuilder) getLogCmd(opts GetCommitsOptions) *exec.Cmd {
limitFlag := ""
if options.Limit {
if opts.Limit {
limitFlag = "-300"
}
filterFlag := ""
if options.FilterPath != "" {
filterFlag = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(options.FilterPath))
if opts.FilterPath != "" {
filterFlag = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(opts.FilterPath))
}
return c.OSCommand.ExecutableFromString(fmt.Sprintf("git log --oneline --pretty=format:\"%%H%s%%at%s%%aN%s%%d%s%%s\" %s --abbrev=%d --date=unix %s", SEPARATION_CHAR, SEPARATION_CHAR, SEPARATION_CHAR, SEPARATION_CHAR, limitFlag, 20, filterFlag))
return c.OSCommand.ExecutableFromString(fmt.Sprintf("git log %s --oneline --pretty=format:\"%%H%s%%at%s%%aN%s%%d%s%%s\" %s --abbrev=%d --date=unix %s", opts.RefName, SEPARATION_CHAR, SEPARATION_CHAR, SEPARATION_CHAR, SEPARATION_CHAR, limitFlag, 20, filterFlag))
}

View File

@@ -55,7 +55,7 @@ func TestCommitListBuilderGetUnpushedCommits(t *testing.T) {
t.Run(s.testName, func(t *testing.T) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.getUnpushedCommits())
s.test(c.getUnpushedCommits("HEAD"))
})
}
}

View File

@@ -1,5 +1,11 @@
package commands
import (
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// File : A file from git status
// duplicating this for now
type File struct {
@@ -14,3 +20,27 @@ type File struct {
Type string // one of 'file', 'directory', and 'other'
ShortStatus string // e.g. 'AD', ' A', 'M ', '??'
}
const RENAME_SEPARATOR = " -> "
func (f *File) IsRename() bool {
return strings.Contains(f.Name, RENAME_SEPARATOR)
}
// Names returns an array containing just the filename, or in the case of a rename, the after filename and the before filename
func (f *File) Names() []string {
return strings.Split(f.Name, RENAME_SEPARATOR)
}
// returns true if the file names are the same or if a a file rename includes the filename of the other
func (f *File) Matches(f2 *File) bool {
return utils.StringArraysOverlap(f.Names(), f2.Names())
}
func (f *File) ID() string {
return f.Name
}
func (f *File) Description() string {
return f.Name
}

View File

@@ -16,6 +16,7 @@ import (
"github.com/go-errors/errors"
gogit "github.com/go-git/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
@@ -84,7 +85,7 @@ type GitCommand struct {
removeFile func(string) error
DotGitDir string
onSuccessfulContinue func() error
PatchManager *PatchManager
PatchManager *patch.PatchManager
// Push to current determines whether the user has configured to push to the remote branch of the same name as the current or not
PushToCurrent bool
@@ -143,7 +144,7 @@ func NewGitCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localizer,
PushToCurrent: pushToCurrent,
}
gitCommand.PatchManager = NewPatchManager(log, gitCommand.ApplyPatch)
gitCommand.PatchManager = patch.NewPatchManager(log, gitCommand.ApplyPatch, gitCommand.ShowFileDiff)
return gitCommand, nil
}
@@ -227,16 +228,27 @@ func stashEntryFromLine(line string, index int) *StashEntry {
// GetStashEntryDiff stash diff
func (c *GitCommand) ShowStashEntryCmdStr(index int) string {
return fmt.Sprintf("git stash show -p --color=%s stash@{%d}", c.colorArg(), index)
return fmt.Sprintf("git stash show -p --stat --color=%s stash@{%d}", c.colorArg(), index)
}
// GetStatusFiles git status files
func (c *GitCommand) GetStatusFiles() []*File {
statusOutput, _ := c.GitStatus()
type GetStatusFileOptions struct {
NoRenames bool
}
func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*File {
statusOutput, err := c.GitStatus(GitStatusOptions{NoRenames: opts.NoRenames})
if err != nil {
c.Log.Error(err)
}
statusStrings := utils.SplitLines(statusOutput)
files := []*File{}
for _, statusString := range statusStrings {
if strings.HasPrefix(statusString, "warning") {
c.Log.Warning(statusString)
continue
}
change := statusString[0:2]
stagedChange := change[0:1]
unstagedChange := statusString[1:2]
@@ -275,7 +287,7 @@ func (c *GitCommand) StashSave(message string) error {
}
// MergeStatusFiles merge status files
func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []*File) []*File {
func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []*File, selectedFile *File) []*File {
if len(oldFiles) == 0 {
return newFiles
}
@@ -286,10 +298,15 @@ func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []*File) []*File {
result := []*File{}
for _, oldFile := range oldFiles {
for newIndex, newFile := range newFiles {
if oldFile.Name == newFile.Name {
if includesInt(appendedIndexes, newIndex) {
continue
}
// if we just staged B and in doing so created 'A -> B' and we are currently have oldFile: A and newFile: 'A -> B', we want to wait until we come across B so the our cursor isn't jumping anywhere
waitForMatchingFile := selectedFile != nil && newFile.IsRename() && !selectedFile.IsRename() && newFile.Matches(selectedFile) && !oldFile.Matches(selectedFile)
if oldFile.Matches(newFile) && !waitForMatchingFile {
result = append(result, newFile)
appendedIndexes = append(appendedIndexes, newIndex)
break
}
}
}
@@ -360,11 +377,26 @@ func (c *GitCommand) RebaseBranch(branchName string) error {
return c.OSCommand.RunPreparedCommand(cmd)
}
type FetchOptions struct {
PromptUserForCredential func(string) string
RemoteName string
BranchName string
}
// Fetch fetch git repo
func (c *GitCommand) Fetch(unamePassQuestion func(string) string, canAskForCredentials bool) error {
return c.OSCommand.DetectUnamePass("git fetch", func(question string) string {
if canAskForCredentials {
return unamePassQuestion(question)
func (c *GitCommand) Fetch(opts FetchOptions) error {
command := "git fetch"
if opts.RemoteName != "" {
command = fmt.Sprintf("%s %s", command, opts.RemoteName)
}
if opts.BranchName != "" {
command = fmt.Sprintf("%s %s", command, opts.BranchName)
}
return c.OSCommand.DetectUnamePass(command, func(question string) string {
if opts.PromptUserForCredential != nil {
return opts.PromptUserForCredential(question)
}
return "\n"
})
@@ -376,8 +408,8 @@ func (c *GitCommand) ResetToCommit(sha string, strength string, options RunComma
}
// NewBranch create new branch
func (c *GitCommand) NewBranch(name string, baseBranch string) error {
return c.OSCommand.RunCommand("git checkout -b %s %s", name, baseBranch)
func (c *GitCommand) NewBranch(name string, base string) error {
return c.OSCommand.RunCommand("git checkout -b %s %s", name, base)
}
// CurrentBranchName get the current branch name and displayname.
@@ -421,10 +453,20 @@ func (c *GitCommand) ListStash() (string, error) {
return c.OSCommand.RunCommandWithOutput("git stash list")
}
type MergeOpts struct {
FastForwardOnly bool
}
// Merge merge
func (c *GitCommand) Merge(branchName string) error {
func (c *GitCommand) Merge(branchName string, opts MergeOpts) error {
mergeArgs := c.Config.GetUserConfig().GetString("git.merging.args")
return c.OSCommand.RunCommand("git merge --no-edit %s %s", mergeArgs, branchName)
command := fmt.Sprintf("git merge --no-edit %s %s", mergeArgs, branchName)
if opts.FastForwardOnly {
command = fmt.Sprintf("%s --ff-only", command)
}
return c.OSCommand.RunCommand(command)
}
// AbortMerge abort merge
@@ -451,9 +493,9 @@ func (c *GitCommand) usingGpg() bool {
// Commit commits to git
func (c *GitCommand) Commit(message string, flags string) (*exec.Cmd, error) {
command := fmt.Sprintf("git commit %s -m %s", flags, c.OSCommand.Quote(message))
command := fmt.Sprintf("git commit %s -m %s", flags, strconv.Quote(message))
if c.usingGpg() {
return c.OSCommand.ExecutableFromString(fmt.Sprintf("%s %s %s", c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command)), nil
return c.OSCommand.ShellCommandFromString(command), nil
}
return nil, c.OSCommand.RunCommand(command)
@@ -466,28 +508,25 @@ func (c *GitCommand) GetHeadCommitMessage() (string, error) {
return strings.TrimSpace(message), err
}
func (c *GitCommand) GetCommitMessage(commitSha string) (string, error) {
cmdStr := "git rev-list --format=%B --max-count=1 " + commitSha
messageWithHeader, err := c.OSCommand.RunCommandWithOutput(cmdStr)
message := strings.Join(strings.SplitAfter(messageWithHeader, "\n")[1:], "\n")
return strings.TrimSpace(message), err
}
// AmendHead amends HEAD with whatever is staged in your working tree
func (c *GitCommand) AmendHead() (*exec.Cmd, error) {
command := "git commit --amend --no-edit --allow-empty"
if c.usingGpg() {
return c.OSCommand.ExecutableFromString(fmt.Sprintf("%s %s %s", c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command)), nil
return c.OSCommand.ShellCommandFromString(command), nil
}
return nil, c.OSCommand.RunCommand(command)
}
// Pull pulls from repo
func (c *GitCommand) Pull(args string, ask func(string) string) error {
return c.OSCommand.DetectUnamePass("git pull --no-edit "+args, ask)
}
// PullWithoutPasswordCheck assumes that the pull will not prompt the user for a password
func (c *GitCommand) PullWithoutPasswordCheck(args string) error {
return c.OSCommand.RunCommand("git pull --no-edit " + args)
}
// Push pushes to a branch
func (c *GitCommand) Push(branchName string, force bool, upstream string, args string, ask func(string) string) error {
func (c *GitCommand) Push(branchName string, force bool, upstream string, args string, promptUserForCredential func(string) string) error {
forceFlag := ""
if force {
forceFlag = "--force-with-lease"
@@ -499,7 +538,7 @@ func (c *GitCommand) Push(branchName string, force bool, upstream string, args s
}
cmd := fmt.Sprintf("git push --follow-tags %s %s %s", forceFlag, setUpstreamArg, args)
return c.OSCommand.DetectUnamePass(cmd, ask)
return c.OSCommand.DetectUnamePass(cmd, promptUserForCredential)
}
// CatFile obtains the content of a file
@@ -509,7 +548,9 @@ func (c *GitCommand) CatFile(fileName string) (string, error) {
// StageFile stages a file
func (c *GitCommand) StageFile(fileName string) error {
return c.OSCommand.RunCommand("git add %s", c.OSCommand.Quote(fileName))
// renamed files look like "file1 -> file2"
fileNames := strings.Split(fileName, " -> ")
return c.OSCommand.RunCommand("git add %s", c.OSCommand.Quote(fileNames[len(fileNames)-1]))
}
// StageAll stages all files
@@ -540,8 +581,16 @@ func (c *GitCommand) UnStageFile(fileName string, tracked bool) error {
}
// GitStatus returns the plaintext short status of the repo
func (c *GitCommand) GitStatus() (string, error) {
return c.OSCommand.RunCommandWithOutput("git status --untracked-files=all --porcelain")
type GitStatusOptions struct {
NoRenames bool
}
func (c *GitCommand) GitStatus(opts GitStatusOptions) (string, error) {
noRenamesFlag := ""
if opts.NoRenames {
noRenamesFlag = "--no-renames"
}
return c.OSCommand.RunCommandWithOutput("git status --untracked-files=all --porcelain %s", noRenamesFlag)
}
// IsInMergeState states whether we are still mid-merge
@@ -567,8 +616,61 @@ func (c *GitCommand) RebaseMode() (string, error) {
}
}
func (c *GitCommand) BeforeAndAfterFileForRename(file *File) (*File, *File, error) {
if !file.IsRename() {
return nil, nil, errors.New("Expected renamed file")
}
// we've got a file that represents a rename from one file to another. Unfortunately
// our File abstraction fails to consider this case, so here we will refetch
// all files, passing the --no-renames flag and then recursively call the function
// again for the before file and after file. At some point we should fix the abstraction itself
split := strings.Split(file.Name, " -> ")
filesWithoutRenames := c.GetStatusFiles(GetStatusFileOptions{NoRenames: true})
var beforeFile *File
var afterFile *File
for _, f := range filesWithoutRenames {
if f.Name == split[0] {
beforeFile = f
}
if f.Name == split[1] {
afterFile = f
}
}
if beforeFile == nil || afterFile == nil {
return nil, nil, errors.New("Could not find deleted file or new file for file rename")
}
if beforeFile.IsRename() || afterFile.IsRename() {
// probably won't happen but we want to ensure we don't get an infinite loop
return nil, nil, errors.New("Nested rename found")
}
return beforeFile, afterFile, nil
}
// DiscardAllFileChanges directly
func (c *GitCommand) DiscardAllFileChanges(file *File) error {
if file.IsRename() {
beforeFile, afterFile, err := c.BeforeAndAfterFileForRename(file)
if err != nil {
return err
}
if err := c.DiscardAllFileChanges(beforeFile); err != nil {
return err
}
if err := c.DiscardAllFileChanges(afterFile); err != nil {
return err
}
return nil
}
// if the file isn't tracked, we assume you want to delete it
quotedFileName := c.OSCommand.Quote(file.Name)
if file.HasStagedChanges || file.HasMergeConflicts {
@@ -663,14 +765,14 @@ func (c *GitCommand) CheckRemoteBranchExists(branch *Branch) bool {
return err == nil
}
// Diff returns the diff of a file
func (c *GitCommand) Diff(file *File, plain bool, cached bool) string {
// WorktreeFileDiff returns the diff of a file
func (c *GitCommand) WorktreeFileDiff(file *File, plain bool, cached bool) string {
// for now we assume an error means the file was deleted
s, _ := c.OSCommand.RunCommandWithOutput(c.DiffCmdStr(file, plain, cached))
s, _ := c.OSCommand.RunCommandWithOutput(c.WorktreeFileDiffCmdStr(file, plain, cached))
return s
}
func (c *GitCommand) DiffCmdStr(file *File, plain bool, cached bool) string {
func (c *GitCommand) WorktreeFileDiffCmdStr(file *File, plain bool, cached bool) string {
cachedArg := ""
trackedArg := "--"
colorArg := c.colorArg()
@@ -704,8 +806,9 @@ func (c *GitCommand) ApplyPatch(patch string, flags ...string) error {
return c.OSCommand.RunCommand("git apply %s %s", flagStr, c.OSCommand.Quote(filepath))
}
func (c *GitCommand) FastForward(branchName string, remoteName string, remoteBranchName string) error {
return c.OSCommand.RunCommand("git fetch %s %s:%s", remoteName, remoteBranchName, branchName)
func (c *GitCommand) FastForward(branchName string, remoteName string, remoteBranchName string, promptUserForCredential func(string) string) error {
command := fmt.Sprintf("git fetch %s %s:%s", remoteName, remoteBranchName, branchName)
return c.OSCommand.DetectUnamePass(command, promptUserForCredential)
}
func (c *GitCommand) RunSkipEditorCommand(command string) error {
@@ -949,45 +1052,67 @@ func (c *GitCommand) CherryPickCommits(commits []*Commit) error {
return c.OSCommand.RunPreparedCommand(cmd)
}
// GetCommitFiles get the specified commit files
func (c *GitCommand) GetCommitFiles(commitSha string, patchManager *PatchManager) ([]*CommitFile, error) {
files, err := c.OSCommand.RunCommandWithOutput("git diff-tree --no-commit-id --name-only -r --no-renames %s", commitSha)
// GetFilesInDiff get the specified commit files
func (c *GitCommand) GetFilesInDiff(from string, to string, reverse bool, patchManager *patch.PatchManager) ([]*CommitFile, error) {
reverseFlag := ""
if reverse {
reverseFlag = " -R "
}
filenames, err := c.OSCommand.RunCommandWithOutput("git diff --name-status %s %s %s", reverseFlag, from, to)
if err != nil {
return nil, err
}
return c.GetCommitFilesFromFilenames(filenames, to, patchManager), nil
}
// filenames string is something like "file1\nfile2\nfile3"
func (c *GitCommand) GetCommitFilesFromFilenames(filenames string, parent string, patchManager *patch.PatchManager) []*CommitFile {
commitFiles := make([]*CommitFile, 0)
for _, file := range strings.Split(strings.TrimRight(files, "\n"), "\n") {
status := UNSELECTED
if patchManager != nil && patchManager.CommitSha == commitSha {
status = patchManager.GetFileStatus(file)
for _, line := range strings.Split(strings.TrimRight(filenames, "\n"), "\n") {
// typical result looks like 'A my_file' meaning my_file was added
if line == "" {
continue
}
changeStatus := line[0:1]
name := line[2:]
status := patch.UNSELECTED
if patchManager != nil && patchManager.To == parent {
status = patchManager.GetFileStatus(name)
}
commitFiles = append(commitFiles, &CommitFile{
Sha: commitSha,
Name: file,
DisplayString: file,
Status: status,
Parent: parent,
Name: name,
ChangeStatus: changeStatus,
PatchStatus: status,
})
}
return commitFiles, nil
return commitFiles
}
// ShowCommitFile get the diff of specified commit file
func (c *GitCommand) ShowCommitFile(commitSha, fileName string, plain bool) (string, error) {
cmdStr := c.ShowCommitFileCmdStr(commitSha, fileName, plain)
// ShowFileDiff get the diff of specified from and to. Typically this will be used for a single commit so it'll be 123abc^..123abc
// but when we're in diff mode it could be any 'from' to any 'to'. The reverse flag is also here thanks to diff mode.
func (c *GitCommand) ShowFileDiff(from string, to string, reverse bool, fileName string, plain bool) (string, error) {
cmdStr := c.ShowFileDiffCmdStr(from, to, reverse, fileName, plain)
return c.OSCommand.RunCommandWithOutput(cmdStr)
}
func (c *GitCommand) ShowCommitFileCmdStr(commitSha, fileName string, plain bool) string {
func (c *GitCommand) ShowFileDiffCmdStr(from string, to string, reverse bool, fileName string, plain bool) string {
colorArg := c.colorArg()
if plain {
colorArg = "never"
}
return fmt.Sprintf("git show --no-renames --color=%s %s -- %s", colorArg, commitSha, fileName)
reverseFlag := ""
if reverse {
reverseFlag = " -R "
}
return fmt.Sprintf("git diff --no-renames --color=%s %s %s %s -- %s", colorArg, from, to, reverseFlag, fileName)
}
// CheckoutFile checks out the file for the given commit
@@ -1093,7 +1218,7 @@ func (c *GitCommand) StashSaveStagedChanges(message string) error {
// if you had staged an untracked file, that will now appear as 'AD' in git status
// meaning it's deleted in your working tree but added in your index. Given that it's
// now safely stashed, we need to remove it.
files := c.GetStatusFiles()
files := c.GetStatusFiles(GetStatusFileOptions{})
for _, file := range files {
if file.ShortStatus == "AD" {
if err := c.UnStageFile(file.Name, false); err != nil {

View File

@@ -420,7 +420,7 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = s.command
s.test(gitCmd.GetStatusFiles())
s.test(gitCmd.GetStatusFiles(GetStatusFileOptions{}))
})
}
}
@@ -541,7 +541,7 @@ func TestGitCommandMergeStatusFiles(t *testing.T) {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
s.test(gitCmd.MergeStatusFiles(s.oldFiles, s.newFiles))
s.test(gitCmd.MergeStatusFiles(s.oldFiles, s.newFiles, nil))
})
}
}
@@ -703,7 +703,7 @@ func TestGitCommandMerge(t *testing.T) {
return exec.Command("echo")
}
assert.NoError(t, gitCmd.Merge("test"))
assert.NoError(t, gitCmd.Merge("test", MergeOpts{}))
}
// TestGitCommandUsingGpg is a function.
@@ -815,7 +815,7 @@ func TestGitCommandCommit(t *testing.T) {
"Commit using gpg",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "bash", cmd)
assert.EqualValues(t, []string{"-c", "git", "commit", "-m", "test"}, args)
assert.EqualValues(t, []string{"-c", "git commit -m \"test\""}, args)
return exec.Command("echo")
},
@@ -905,7 +905,7 @@ func TestGitCommandAmendHead(t *testing.T) {
"Amend commit using gpg",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "bash", cmd)
assert.EqualValues(t, []string{"-c", "git", "commit", "--amend", "--no-edit", "--allow-empty"}, args)
assert.EqualValues(t, []string{"-c", "git commit --amend --no-edit --allow-empty"}, args)
return exec.Command("echo")
},
@@ -1474,7 +1474,7 @@ func TestGitCommandDiff(t *testing.T) {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = s.command
gitCmd.Diff(s.file, s.plain, s.cached)
gitCmd.WorktreeFileDiff(s.file, s.plain, s.cached)
})
}
}
@@ -1814,83 +1814,6 @@ func TestGitCommandDiscardOldFileChanges(t *testing.T) {
}
}
// TestGitCommandShowCommitFile is a function.
func TestGitCommandShowCommitFile(t *testing.T) {
type scenario struct {
testName string
commitSha string
fileName string
command func(string, ...string) *exec.Cmd
test func(string, error)
}
scenarios := []scenario{
{
"valid case",
"123456",
"hello.txt",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: "git show --no-renames --color=never 123456 -- hello.txt",
Replace: "echo -n hello",
},
}),
func(str string, err error) {
assert.NoError(t, err)
assert.Equal(t, "hello", str)
},
},
}
gitCmd := NewDummyGitCommand()
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd.OSCommand.command = s.command
s.test(gitCmd.ShowCommitFile(s.commitSha, s.fileName, true))
})
}
}
// TestGitCommandGetCommitFiles is a function.
func TestGitCommandGetCommitFiles(t *testing.T) {
type scenario struct {
testName string
commitSha string
command func(string, ...string) *exec.Cmd
test func([]*CommitFile, error)
}
scenarios := []scenario{
{
"valid case",
"123456",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: "git diff-tree --no-commit-id --name-only -r --no-renames 123456",
Replace: "echo 'hello\nworld'",
},
}),
func(commitFiles []*CommitFile, err error) {
assert.NoError(t, err)
assert.Equal(t, []*CommitFile{
{Sha: "123456", Name: "hello", DisplayString: "hello"},
{Sha: "123456", Name: "world", DisplayString: "world"},
}, commitFiles)
},
},
}
gitCmd := NewDummyGitCommand()
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd.OSCommand.command = s.command
s.test(gitCmd.GetCommitFiles(s.commitSha, nil))
})
}
}
// TestGitCommandDiscardUnstagedFileChanges is a function.
func TestGitCommandDiscardUnstagedFileChanges(t *testing.T) {
type scenario struct {

View File

@@ -8,11 +8,13 @@ import (
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"github.com/go-errors/errors"
"github.com/atotto/clipboard"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/mgutz/str"
@@ -118,15 +120,29 @@ func (c *OSCommand) ExecutableFromString(commandStr string) *exec.Cmd {
return cmd
}
// ShellCommandFromString takes a string like `git commit` and returns an executable shell command for it
func (c *OSCommand) ShellCommandFromString(commandStr string) *exec.Cmd {
quotedCommand := ""
// Windows does not seem to like quotes around the command
if c.Platform.os == "windows" {
quotedCommand = commandStr
} else {
quotedCommand = strconv.Quote(commandStr)
}
shellCommand := fmt.Sprintf("%s %s %s", c.Platform.shell, c.Platform.shellArg, quotedCommand)
return c.ExecutableFromString(shellCommand)
}
// RunCommandWithOutputLive runs RunCommandWithOutputLiveWrapper
func (c *OSCommand) RunCommandWithOutputLive(command string, output func(string) string) error {
return RunCommandWithOutputLiveWrapper(c, command, output)
}
// DetectUnamePass detect a username / password question in a command
// ask is a function that gets executen when this function detect you need to fillin a password
// The ask argument will be "username" or "password" and expects the user's password or username back
func (c *OSCommand) DetectUnamePass(command string, ask func(string) string) error {
// promptUserForCredential is a function that gets executed when this function detect you need to fillin a password
// The promptUserForCredential argument will be "username" or "password" and expects the user's password or username back
func (c *OSCommand) DetectUnamePass(command string, promptUserForCredential func(string) string) error {
ttyText := ""
errMessage := c.RunCommandWithOutputLive(command, func(word string) string {
ttyText = ttyText + " " + word
@@ -140,7 +156,7 @@ func (c *OSCommand) DetectUnamePass(command string, ask func(string) string) err
for pattern, askFor := range prompts {
if match, _ := regexp.MatchString(pattern, ttyText); match {
ttyText = ""
return ask(askFor)
return promptUserForCredential(askFor)
}
}
@@ -234,7 +250,9 @@ func (c *OSCommand) EditFile(filename string) (*exec.Cmd, error) {
return nil, errors.New("No editor defined in $VISUAL, $EDITOR, or git config")
}
return c.PrepareSubProcess(editor, filename), nil
splitCmd := str.ToArgv(fmt.Sprintf("%s %s", editor, filename))
return c.PrepareSubProcess(splitCmd[0], splitCmd[1:]...), nil
}
// PrepareSubProcess iniPrepareSubProcessrocess then tells the Gui to switch to it
@@ -458,12 +476,5 @@ func RunLineOutputCmd(cmd *exec.Cmd, onLine func(line string) (bool, error)) err
}
func (c *OSCommand) CopyToClipboard(str string) error {
commandTemplate := c.Config.GetUserConfig().GetString("os.copyToClipboardCommand")
templateValues := map[string]string{
"str": c.Quote(str),
}
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
return c.RunCommand(command)
return clipboard.WriteAll(str)
}

142
pkg/commands/patch/hunk.go Normal file
View File

@@ -0,0 +1,142 @@
package patch
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type PatchHunk struct {
FirstLineIdx int
oldStart int
newStart int
heading string
bodyLines []string
}
func (hunk *PatchHunk) LastLineIdx() int {
return hunk.FirstLineIdx + len(hunk.bodyLines)
}
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,
}
}
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
}
func (hunk *PatchHunk) updatedLines(lineIndices []int, reverse bool) []string {
skippedNewlineMessageIndex := -1
newLines := []string{}
lineIdx := hunk.FirstLineIdx
for _, line := range hunk.bodyLines {
lineIdx++ // incrementing at the start to skip the header line
if line == "" {
break
}
isLineSelected := utils.IncludesInt(lineIndices, lineIdx)
firstChar, content := line[:1], line[1:]
transformedFirstChar := transformedFirstChar(firstChar, reverse, isLineSelected)
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
}
}
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
}

View File

@@ -1,12 +1,24 @@
package commands
package patch
import (
"errors"
"sort"
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
const (
// UNSELECTED is for when the commit file has not been added to the patch in any way
UNSELECTED = iota
// WHOLE is for when you want to add the whole diff of a file to the patch,
// including e.g. if it was deleted
WHOLE
// PART is for when you're only talking about specific lines that have been modified
PART
)
type fileInfo struct {
mode int // one of WHOLE/PART
includedLineIndices []int
@@ -14,55 +26,76 @@ type fileInfo struct {
}
type applyPatchFunc func(patch string, flags ...string) error
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)
// 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 {
CommitSha string
// 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
Reverse bool
// CanRebase tells us whether we're allowed to modify our commits. CanRebase should be true for commits of the currently checked out branch and false for everything else
// TODO: move this out into a proper mode struct in the gui package: it doesn't really belong here
CanRebase bool
// fileInfoMap starts empty but you add files to it as you go along
fileInfoMap map[string]*fileInfo
Log *logrus.Entry
ApplyPatch applyPatchFunc
// LoadFileDiff loads the diff of a file, for a given to (typically a commit SHA)
LoadFileDiff loadFileDiffFunc
}
// NewPatchManager returns a new PatchModifier
func NewPatchManager(log *logrus.Entry, applyPatch applyPatchFunc) *PatchManager {
// NewPatchManager returns a new PatchManager
func NewPatchManager(log *logrus.Entry, applyPatch applyPatchFunc, loadFileDiff loadFileDiffFunc) *PatchManager {
return &PatchManager{
Log: log,
ApplyPatch: applyPatch,
Log: log,
ApplyPatch: applyPatch,
LoadFileDiff: loadFileDiff,
}
}
// NewPatchManager returns a new PatchModifier
func (p *PatchManager) Start(commitSha string, diffMap map[string]string) {
p.CommitSha = commitSha
// NewPatchManager returns a new PatchManager
func (p *PatchManager) Start(from, to string, reverse bool, canRebase bool) {
p.To = to
p.From = from
p.Reverse = reverse
p.CanRebase = canRebase
p.fileInfoMap = map[string]*fileInfo{}
for filename, diff := range diffMap {
p.fileInfoMap[filename] = &fileInfo{
mode: UNSELECTED,
diff: diff,
}
}
func (p *PatchManager) addFileWhole(info *fileInfo) {
info.mode = WHOLE
lineCount := len(strings.Split(info.diff, "\n"))
info.includedLineIndices = make([]int, lineCount)
// add every line index
for i := 0; i < lineCount; i++ {
info.includedLineIndices[i] = i
}
}
func (p *PatchManager) AddFile(filename string) {
p.fileInfoMap[filename].mode = WHOLE
p.fileInfoMap[filename].includedLineIndices = nil
func (p *PatchManager) removeFile(info *fileInfo) {
info.mode = UNSELECTED
info.includedLineIndices = nil
}
func (p *PatchManager) RemoveFile(filename string) {
p.fileInfoMap[filename].mode = UNSELECTED
p.fileInfoMap[filename].includedLineIndices = nil
}
func (p *PatchManager) ToggleFileWhole(filename string) {
info := p.fileInfoMap[filename]
func (p *PatchManager) ToggleFileWhole(filename string) error {
info, err := p.getFileInfo(filename)
if err != nil {
return err
}
switch info.mode {
case UNSELECTED:
p.AddFile(filename)
case UNSELECTED, PART:
p.addFileWhole(info)
case WHOLE:
p.RemoveFile(filename)
case PART:
p.AddFile(filename)
p.removeFile(info)
default:
return errors.New("unknown file mode")
}
return nil
}
func getIndicesForRange(first, last int) []int {
@@ -73,24 +106,55 @@ func getIndicesForRange(first, last int) []int {
return indices
}
func (p *PatchManager) AddFileLineRange(filename string, firstLineIdx, lastLineIdx int) {
info := p.fileInfoMap[filename]
info.mode = PART
info.includedLineIndices = utils.UnionInt(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx))
func (p *PatchManager) getFileInfo(filename string) (*fileInfo, error) {
info, ok := p.fileInfoMap[filename]
if ok {
return info, nil
}
diff, err := p.LoadFileDiff(p.From, p.To, p.Reverse, filename, true)
if err != nil {
return nil, err
}
info = &fileInfo{
mode: UNSELECTED,
diff: diff,
}
p.fileInfoMap[filename] = info
return info, nil
}
func (p *PatchManager) RemoveFileLineRange(filename string, firstLineIdx, lastLineIdx int) {
info := p.fileInfoMap[filename]
func (p *PatchManager) AddFileLineRange(filename string, firstLineIdx, lastLineIdx int) error {
info, err := p.getFileInfo(filename)
if err != nil {
return err
}
info.mode = PART
info.includedLineIndices = utils.UnionInt(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx))
return nil
}
func (p *PatchManager) RemoveFileLineRange(filename string, firstLineIdx, lastLineIdx int) error {
info, err := p.getFileInfo(filename)
if err != nil {
return err
}
info.mode = PART
info.includedLineIndices = utils.DifferenceInt(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx))
if len(info.includedLineIndices) == 0 {
p.RemoveFile(filename)
p.removeFile(info)
}
return nil
}
func (p *PatchManager) RenderPlainPatchForFile(filename string, reverse bool, keepOriginalHeader bool) string {
info := p.fileInfoMap[filename]
if info == nil {
func (p *PatchManager) renderPlainPatchForFile(filename string, reverse bool, keepOriginalHeader bool) string {
info, err := p.getFileInfo(filename)
if err != nil {
p.Log.Error(err)
return ""
}
@@ -101,15 +165,14 @@ func (p *PatchManager) RenderPlainPatchForFile(filename string, reverse bool, ke
return info.diff
case PART:
// generate a new diff with just the selected lines
m := NewPatchModifier(p.Log, filename, info.diff)
return m.ModifiedPatchForLines(info.includedLineIndices, reverse, keepOriginalHeader)
return ModifiedPatchForLines(p.Log, filename, info.diff, info.includedLineIndices, reverse, keepOriginalHeader)
default:
return ""
}
}
func (p *PatchManager) RenderPatchForFile(filename string, plain bool, reverse bool, keepOriginalHeader bool) string {
patch := p.RenderPlainPatchForFile(filename, reverse, keepOriginalHeader)
patch := p.renderPlainPatchForFile(filename, reverse, keepOriginalHeader)
if plain {
return patch
}
@@ -122,7 +185,7 @@ func (p *PatchManager) RenderPatchForFile(filename string, plain bool, reverse b
return parser.Render(-1, -1, nil)
}
func (p *PatchManager) RenderEachFilePatch(plain bool) []string {
func (p *PatchManager) renderEachFilePatch(plain bool) []string {
// sort files by name then iterate through and render each patch
filenames := make([]string, len(p.fileInfoMap))
index := 0
@@ -145,7 +208,7 @@ func (p *PatchManager) RenderEachFilePatch(plain bool) []string {
func (p *PatchManager) RenderAggregatedPatchColored(plain bool) string {
result := ""
for _, patch := range p.RenderEachFilePatch(plain) {
for _, patch := range p.renderEachFilePatch(plain) {
if patch != "" {
result += patch + "\n"
}
@@ -154,19 +217,20 @@ func (p *PatchManager) RenderAggregatedPatchColored(plain bool) string {
}
func (p *PatchManager) GetFileStatus(filename string) int {
info := p.fileInfoMap[filename]
if info == nil {
info, ok := p.fileInfoMap[filename]
if !ok {
return UNSELECTED
}
return info.mode
}
func (p *PatchManager) GetFileIncLineIndices(filename string) []int {
info := p.fileInfoMap[filename]
if info == nil {
return []int{}
func (p *PatchManager) GetFileIncLineIndices(filename string) ([]int, error) {
info, err := p.getFileInfo(filename)
if err != nil {
return nil, err
}
return info.includedLineIndices
return info.includedLineIndices, nil
}
func (p *PatchManager) ApplyPatches(reverse bool) error {
@@ -210,12 +274,12 @@ func (p *PatchManager) ApplyPatches(reverse bool) error {
// clears the patch
func (p *PatchManager) Reset() {
p.CommitSha = ""
p.To = ""
p.fileInfoMap = map[string]*fileInfo{}
}
func (p *PatchManager) CommitSelected() bool {
return p.CommitSha != ""
func (p *PatchManager) Active() bool {
return p.To != ""
}
func (p *PatchManager) IsEmpty() bool {
@@ -227,3 +291,8 @@ func (p *PatchManager) IsEmpty() bool {
return true
}
// 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 {
return from != p.From || to != p.To || reverse != p.Reverse
}

View File

@@ -0,0 +1,158 @@
package patch
import (
"fmt"
"regexp"
"strings"
"github.com/sirupsen/logrus"
)
var hunkHeaderRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)[^\+]+\+(\d+)[^@]+@@(.*)$`)
var 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
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 {
lines := hunk.bodyLines[0 : idx-hunk.FirstLineIdx-1]
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
}

View File

@@ -1,7 +1,8 @@
package commands
package patch
import (
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
@@ -88,6 +89,15 @@ index e69de29..c6568ea 100644
\ No newline at end of file
`
const exampleHunk = `@@ -1,5 +1,5 @@
apple
-grape
+orange
...
...
...
`
// TestModifyPatchForRange is a function.
func TestModifyPatchForRange(t *testing.T) {
type scenario struct {
@@ -509,3 +519,30 @@ func TestModifyPatchForRange(t *testing.T) {
})
}
}
func TestLineNumberOfLine(t *testing.T) {
type scenario struct {
testName string
hunk *PatchHunk
idx int
expected int
}
scenarios := []scenario{
{
testName: "nothing selected",
hunk: newHunk(strings.SplitAfter(exampleHunk, "\n"), 10),
idx: 15,
expected: 3,
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
result := s.hunk.LineNumberOfLine(s.idx)
if !assert.Equal(t, s.expected, result) {
fmt.Println(result)
}
})
}
}

View File

@@ -1,4 +1,4 @@
package commands
package patch
import (
"regexp"
@@ -63,7 +63,7 @@ func (p *PatchParser) GetHunkContainingLine(lineIndex int, offset int) *PatchHun
}
for index, hunk := range p.PatchHunks {
if lineIndex >= hunk.FirstLineIdx && lineIndex <= hunk.LastLineIdx {
if lineIndex >= hunk.FirstLineIdx && lineIndex <= hunk.LastLineIdx() {
resultIndex := index + offset
if resultIndex < 0 {
resultIndex = 0
@@ -75,7 +75,7 @@ func (p *PatchParser) GetHunkContainingLine(lineIndex int, offset int) *PatchHun
}
// if your cursor is past the last hunk, select the last hunk
if lineIndex > p.PatchHunks[len(p.PatchHunks)-1].LastLineIdx {
if lineIndex > p.PatchHunks[len(p.PatchHunks)-1].LastLineIdx() {
return p.PatchHunks[len(p.PatchHunks)-1]
}

View File

@@ -1,260 +0,0 @@
package commands
import (
"fmt"
"regexp"
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
var hunkHeaderRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)[^\+]+\+(\d+)[^@]+@@(.*)$`)
var patchHeaderRegexp = regexp.MustCompile(`(?ms)(^diff.*?)^@@`)
type PatchHunk struct {
header string
FirstLineIdx int
LastLineIdx int
bodyLines []string
}
func newHunk(header string, body string, firstLineIdx int) *PatchHunk {
bodyLines := strings.SplitAfter(header+body, "\n")[1:] // dropping the header line
return &PatchHunk{
header: header,
FirstLineIdx: firstLineIdx,
LastLineIdx: firstLineIdx + len(bodyLines),
bodyLines: bodyLines,
}
}
func (hunk *PatchHunk) updatedLines(lineIndices []int, reverse bool) []string {
skippedNewlineMessageIndex := -1
newLines := []string{}
lineIdx := hunk.FirstLineIdx
for _, line := range hunk.bodyLines {
lineIdx++ // incrementing at the start to skip the header line
if line == "" {
break
}
isLineSelected := utils.IncludesInt(lineIndices, lineIdx)
firstChar, content := line[:1], line[1:]
transformedFirstChar := transformedFirstChar(firstChar, reverse, isLineSelected)
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
}
}
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 := 0
oldLength := 0
newLength := 0
for _, line := range newBodyLines {
switch line[:1] {
case "+":
newLength++
changeCount++
case "-":
oldLength++
changeCount++
case " ":
oldLength++
newLength++
}
}
if changeCount == 0 {
// if nothing has changed we just return nothing
return startOffset, "", false
}
// get oldstart, newstart, and heading from header
match := hunkHeaderRegexp.FindStringSubmatch(hunk.header)
var oldStart int
if reverse {
oldStart = mustConvertToInt(match[2])
} else {
oldStart = mustConvertToInt(match[1])
}
heading := match[3]
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, heading)
return newStartOffset, formattedHeader, true
}
func mustConvertToInt(s string) int {
i, err := strconv.Atoi(s)
if err != nil {
panic(err)
}
return i
}
func GetHeaderFromDiff(diff string) string {
match := patchHeaderRegexp.FindStringSubmatch(diff)
if len(match) <= 1 {
return ""
}
return match[1]
}
func GetHunksFromDiff(diff string) []*PatchHunk {
headers := hunkHeaderRegexp.FindAllString(diff, -1)
bodies := hunkHeaderRegexp.Split(diff, -1)[1:] // discarding top bit
headerFirstLineIndices := []int{}
for lineIdx, line := range strings.Split(diff, "\n") {
if strings.HasPrefix(line, "@@ -") {
headerFirstLineIndices = append(headerFirstLineIndices, lineIdx)
}
}
hunks := make([]*PatchHunk, len(headers))
for index, header := range headers {
hunks[index] = newHunk(header, bodies[index], headerFirstLineIndices[index])
}
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)
}

View File

@@ -2,11 +2,13 @@ package commands
import (
"fmt"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
)
// DeletePatchesFromCommit applies a patch in reverse for a commit
func (c *GitCommand) DeletePatchesFromCommit(commits []*Commit, commitIndex int, p *PatchManager) error {
func (c *GitCommand) DeletePatchesFromCommit(commits []*Commit, commitIndex int, p *patch.PatchManager) error {
if err := c.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil {
return err
}
@@ -33,7 +35,7 @@ func (c *GitCommand) DeletePatchesFromCommit(commits []*Commit, commitIndex int,
return c.GenericMerge("rebase", "continue")
}
func (c *GitCommand) MovePatchToSelectedCommit(commits []*Commit, sourceCommitIdx int, destinationCommitIdx int, p *PatchManager) error {
func (c *GitCommand) MovePatchToSelectedCommit(commits []*Commit, sourceCommitIdx int, destinationCommitIdx int, p *patch.PatchManager) error {
if sourceCommitIdx < destinationCommitIdx {
if err := c.BeginInteractiveRebaseForCommit(commits, destinationCommitIdx); err != nil {
return err
@@ -134,7 +136,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*Commit, sourceCommitId
return c.GenericMerge("rebase", "continue")
}
func (c *GitCommand) PullPatchIntoIndex(commits []*Commit, commitIdx int, p *PatchManager, stash bool) error {
func (c *GitCommand) PullPatchIntoIndex(commits []*Commit, commitIdx int, p *patch.PatchManager, stash bool) error {
if stash {
if err := c.StashSave(c.Tr.SLocalize("StashPrefix") + commits[commitIdx].Sha); err != nil {
return err
@@ -187,7 +189,7 @@ func (c *GitCommand) PullPatchIntoIndex(commits []*Commit, commitIdx int, p *Pat
return c.GenericMerge("rebase", "continue")
}
func (c *GitCommand) PullPatchIntoNewCommit(commits []*Commit, commitIdx int, p *PatchManager) error {
func (c *GitCommand) PullPatchIntoNewCommit(commits []*Commit, commitIdx int, p *patch.PatchManager) error {
if err := c.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil {
return err
}

View File

@@ -6,3 +6,15 @@ type Remote struct {
Urls []string
Branches []*RemoteBranch
}
func (r *Remote) RefName() string {
return r.Name
}
func (r *Remote) ID() string {
return r.RefName()
}
func (r *Remote) Description() string {
return r.RefName()
}

View File

@@ -9,3 +9,15 @@ type RemoteBranch struct {
func (r *RemoteBranch) FullName() string {
return r.RemoteName + "/" + r.Name
}
func (r *RemoteBranch) RefName() string {
return r.FullName()
}
func (r *RemoteBranch) ID() string {
return r.RefName()
}
func (r *RemoteBranch) Description() string {
return r.RefName()
}

View File

@@ -11,3 +11,11 @@ type StashEntry struct {
func (s *StashEntry) RefName() string {
return fmt.Sprintf("stash@{%d}", s.Index)
}
func (s *StashEntry) ID() string {
return s.RefName()
}
func (s *StashEntry) Description() string {
return s.RefName() + ": " + s.Name
}

View File

@@ -4,3 +4,15 @@ package commands
type Tag struct {
Name string
}
func (t *Tag) RefName() string {
return t.Name
}
func (t *Tag) ID() string {
return t.RefName()
}
func (t *Tag) Description() string {
return "tag " + t.Name
}

View File

@@ -246,6 +246,8 @@ func GetDefaultConfig() []byte {
skipUnstageLineWarning: false
skipStashWarning: true
sidePanelWidth: 0.3333
expandFocusedSidePanel: false
mainPanelSplitMode: 'flexible' # one of 'horizontal' | 'flexible' | 'vertical'
theme:
lightTheme: false
activeBorderColor:
@@ -268,6 +270,8 @@ git:
merging:
manualCommit: false
args: ""
pull:
mode: 'merge' # one of 'merge' | 'rebase' | 'ff-only'
skipHookPrefix: 'WIP'
autoFetch: true
branchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --"
@@ -278,6 +282,7 @@ update:
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
splashUpdatesIndex: 0
confirmOnQuit: false
quitOnTopLevelReturn: true
keybinding:
universal:
quit: 'q'
@@ -304,6 +309,8 @@ keybinding:
optionMenu-alt1: '?'
select: '<space>'
goInto: '<enter>'
confirm: '<enter>'
confirm-alt1: 'y'
remove: 'd'
new: 'n'
edit: 'e'
@@ -327,7 +334,8 @@ keybinding:
undo: 'z'
redo: '<c-z>'
filteringMenu: <c-s>
diffingMenu: '<c-e>'
diffingMenu: 'W'
diffingMenu-alt: '<c-e>'
copyToClipboard: '<c-o>'
status:
checkForUpdate: 'u'

View File

@@ -7,6 +7,5 @@ func GetPlatformDefaultConfig() []byte {
return []byte(
`os:
openCommand: 'open {{filename}}'
openLinkCommand: 'open {{link}}'
copyToClipboardCommand: 'bash -c "echo -n {{str}} | pbcopy"'`)
openLinkCommand: 'open {{link}}'`)
}

View File

@@ -5,6 +5,5 @@ func GetPlatformDefaultConfig() []byte {
return []byte(
`os:
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'
openLinkCommand: 'sh -c "xdg-open {{link}} >/dev/null"'
copyToClipboardCommand: 'bash -c "echo -n {{str}} | xclip -selection clipboard"'`)
openLinkCommand: 'sh -c "xdg-open {{link}} >/dev/null"'`)
}

View File

@@ -5,6 +5,5 @@ func GetPlatformDefaultConfig() []byte {
return []byte(
`os:
openCommand: 'cmd /c "start "" {{filename}}"'
openLinkCommand: 'cmd /c "start "" {{link}}"'
copyToClipboardCommand: 'cmd \c "echo -n {{str}} > /dev/clipboard"'`)
openLinkCommand: 'cmd /c "start "" {{link}}"'`)
}

View File

@@ -66,7 +66,7 @@ func (gui *Gui) WithWaitingStatus(name string, f func() error) error {
if appStatus == "" {
return
}
gui.renderString(gui.g, "appStatus", appStatus)
gui.renderString("appStatus", appStatus)
}
}()

280
pkg/gui/arrangement.go Normal file
View File

@@ -0,0 +1,280 @@
package gui
import (
"github.com/jesseduffield/lazygit/pkg/gui/boxlayout"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) mainSectionChildren() []*boxlayout.Box {
currentWindow := gui.currentWindow()
// if we're not in split mode we can just show the one main panel. Likewise if
// the main panel is focused and we're in full-screen mode
if !gui.isMainPanelSplit() || (gui.State.ScreenMode == SCREEN_FULL && currentWindow == "main") {
return []*boxlayout.Box{
{
Window: "main",
Weight: 1,
},
}
}
main := "main"
secondary := "secondary"
if gui.secondaryViewFocused() {
// when you think you've focused the secondary view, we've actually just swapped them around in the layout
main, secondary = secondary, main
}
return []*boxlayout.Box{
{
Window: main,
Weight: 1,
},
{
Window: secondary,
Weight: 1,
},
}
}
func (gui *Gui) getMidSectionWeights() (int, int) {
currentWindow := gui.currentWindow()
// we originally specified this as a ratio i.e. .20 would correspond to a weight of 1 against 4
sidePanelWidthRatio := gui.Config.GetUserConfig().GetFloat64("gui.sidePanelWidth")
// we could make this better by creating ratios like 2:3 rather than always 1:something
mainSectionWeight := int(1/sidePanelWidthRatio) - 1
sideSectionWeight := 1
if gui.isMainPanelSplit() {
mainSectionWeight = 5 // need to shrink side panel to make way for main panels if side-by-side
}
if currentWindow == "main" {
if gui.State.ScreenMode == SCREEN_HALF || gui.State.ScreenMode == SCREEN_FULL {
sideSectionWeight = 0
}
} else {
if gui.State.ScreenMode == SCREEN_HALF {
mainSectionWeight = 1
} else if gui.State.ScreenMode == SCREEN_FULL {
mainSectionWeight = 0
}
}
return sideSectionWeight, mainSectionWeight
}
func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*boxlayout.Box {
if gui.State.Searching.isSearching {
return []*boxlayout.Box{
{
Window: "searchPrefix",
Size: len(SEARCH_PREFIX),
},
{
Window: "search",
Weight: 1,
},
}
}
result := []*boxlayout.Box{}
if len(appStatus) > 0 {
result = append(result,
&boxlayout.Box{
Window: "appStatus",
Size: len(appStatus) + len(INFO_SECTION_PADDING),
},
)
}
result = append(result,
[]*boxlayout.Box{
{
Window: "options",
Weight: 1,
},
{
Window: "information",
// unlike appStatus, informationStr has various colors so we need to decolorise before taking the length
Size: len(INFO_SECTION_PADDING) + len(utils.Decolorise(informationStr)),
},
}...,
)
return result
}
func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions {
width, height := gui.g.Size()
sideSectionWeight, mainSectionWeight := gui.getMidSectionWeights()
sidePanelsDirection := boxlayout.COLUMN
portraitMode := width <= 84 && height > 45
if portraitMode {
sidePanelsDirection = boxlayout.ROW
}
root := &boxlayout.Box{
Direction: boxlayout.ROW,
Children: []*boxlayout.Box{
{
Direction: sidePanelsDirection,
Weight: 1,
Children: []*boxlayout.Box{
{
Direction: boxlayout.ROW,
Weight: sideSectionWeight,
ConditionalChildren: gui.sidePanelChildren,
},
{
ConditionalDirection: func(width int, height int) int {
mainPanelSplitMode := gui.Config.GetUserConfig().GetString("gui.mainPanelSplitMode")
switch mainPanelSplitMode {
case "vertical":
return boxlayout.ROW
case "horizontal":
return boxlayout.COLUMN
default:
if width < 160 && height > 30 { // 2 80 character width panels
return boxlayout.ROW
} else {
return boxlayout.COLUMN
}
}
},
Direction: boxlayout.COLUMN,
Weight: mainSectionWeight,
Children: gui.mainSectionChildren(),
},
},
},
{
Direction: boxlayout.COLUMN,
Size: 1,
Children: gui.infoSectionChildren(informationStr, appStatus),
},
},
}
return boxlayout.ArrangeWindows(root, 0, 0, width, height)
}
// The stash window by default only contains one line so that it's not hogging
// too much space, but if you access it it should take up some space. This is
// the default behaviour when accordian mode is NOT in effect. If it is in effect
// then when it's accessed it will have weight 2, not 1.
func (gui *Gui) getDefaultStashWindowBox() *boxlayout.Box {
box := &boxlayout.Box{Window: "stash"}
stashWindowAccessed := false
for _, context := range gui.State.ContextStack {
if context.GetWindowName() == "stash" {
stashWindowAccessed = true
}
}
// if the stash window is anywhere in our stack we should enlargen it
if stashWindowAccessed {
box.Weight = 1
} else {
box.Size = 3
}
return box
}
func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
currentWindow := gui.currentSideWindowName()
if gui.State.ScreenMode == SCREEN_FULL || gui.State.ScreenMode == SCREEN_HALF {
fullHeightBox := func(window string) *boxlayout.Box {
if window == currentWindow {
return &boxlayout.Box{
Window: window,
Weight: 1,
}
} else {
return &boxlayout.Box{
Window: window,
Size: 0,
}
}
}
return []*boxlayout.Box{
fullHeightBox("status"),
fullHeightBox("files"),
fullHeightBox("branches"),
fullHeightBox("commits"),
fullHeightBox("stash"),
}
} else if height >= 28 {
accordianMode := gui.Config.GetUserConfig().GetBool("gui.expandFocusedSidePanel")
accordianBox := func(defaultBox *boxlayout.Box) *boxlayout.Box {
if accordianMode && defaultBox.Window == currentWindow {
return &boxlayout.Box{
Window: defaultBox.Window,
Weight: 2,
}
}
return defaultBox
}
return []*boxlayout.Box{
{
Window: "status",
Size: 3,
},
accordianBox(&boxlayout.Box{Window: "files", Weight: 1}),
accordianBox(&boxlayout.Box{Window: "branches", Weight: 1}),
accordianBox(&boxlayout.Box{Window: "commits", Weight: 1}),
accordianBox(gui.getDefaultStashWindowBox()),
}
} else {
squashedHeight := 1
if height >= 21 {
squashedHeight = 3
}
squashedSidePanelBox := func(window string) *boxlayout.Box {
if window == currentWindow {
return &boxlayout.Box{
Window: window,
Weight: 1,
}
} else {
return &boxlayout.Box{
Window: window,
Size: squashedHeight,
}
}
}
return []*boxlayout.Box{
squashedSidePanelBox("status"),
squashedSidePanelBox("files"),
squashedSidePanelBox("branches"),
squashedSidePanelBox("commits"),
squashedSidePanelBox("stash"),
}
}
}
func (gui *Gui) currentSideWindowName() string {
// there is always one and only one cyclable context in the context stack. We'll look from top to bottom
for idx := range gui.State.ContextStack {
reversedIdx := len(gui.State.ContextStack) - 1 - idx
context := gui.State.ContextStack[reversedIdx]
if context.GetKind() == SIDE_CONTEXT {
return context.GetWindowName()
}
}
return "files" // default
}

View File

@@ -0,0 +1,145 @@
package boxlayout
import "math"
type Dimensions struct {
X0 int
X1 int
Y0 int
Y1 int
}
const (
ROW = iota
COLUMN
)
// to give a high-level explanation of what's going on here. We layout our windows by arranging a bunch of boxes in the available space.
// If a box has children, it needs to specify how it wants to arrange those children: ROW or COLUMN.
// If a box represents a window, you can put the window name in the Window field.
// When determining how to divvy-up the available height (for row children) or width (for column children), we first
// give the boxes with a static `size` the space that they want. Then we apportion
// the remaining space based on the weights of the dynamic boxes (you can't define
// both size and weight at the same time: you gotta pick one). If there are two
// boxes, one with weight 1 and the other with weight 2, the first one gets 33%
// of the available space and the second one gets the remaining 66%
type Box struct {
// Direction decides how the children boxes are laid out. ROW means the children will each form a row i.e. that they will be stacked on top of eachother.
Direction int // ROW or COLUMN
// function which takes the width and height assigned to the box and decides which orientation it will have
ConditionalDirection func(width int, height int) int
Children []*Box
// function which takes the width and height assigned to the box and decides the layout of the children.
ConditionalChildren func(width int, height int) []*Box
// Window refers to the name of the window this box represents, if there is one
Window string
// static Size. If parent box's direction is ROW this refers to height, otherwise width
Size int
// dynamic size. Once all statically sized children have been considered, Weight decides how much of the remaining space will be taken up by the box
// TODO: consider making there be one int and a type enum so we can't have size and Weight simultaneously defined
Weight int
}
func ArrangeWindows(root *Box, x0, y0, width, height int) map[string]Dimensions {
children := root.getChildren(width, height)
if len(children) == 0 {
// leaf node
if root.Window != "" {
dimensionsForWindow := Dimensions{X0: x0, Y0: y0, X1: x0 + width - 1, Y1: y0 + height - 1}
return map[string]Dimensions{root.Window: dimensionsForWindow}
}
return map[string]Dimensions{}
}
direction := root.getDirection(width, height)
var availableSize int
if direction == COLUMN {
availableSize = width
} else {
availableSize = height
}
// work out size taken up by children
reservedSize := 0
totalWeight := 0
for _, child := range children {
// assuming either size or weight are non-zero
reservedSize += child.Size
totalWeight += child.Weight
}
remainingSize := availableSize - reservedSize
if remainingSize < 0 {
remainingSize = 0
}
unitSize := 0
extraSize := 0
if totalWeight > 0 {
unitSize = remainingSize / totalWeight
extraSize = remainingSize % totalWeight
}
result := map[string]Dimensions{}
offset := 0
for _, child := range children {
var boxSize int
if child.isStatic() {
boxSize = child.Size
} else {
// TODO: consider more evenly distributing the remainder
boxSize = unitSize * child.Weight
boxExtraSize := int(math.Min(float64(extraSize), float64(child.Weight)))
boxSize += boxExtraSize
extraSize -= boxExtraSize
}
var resultForChild map[string]Dimensions
if direction == COLUMN {
resultForChild = ArrangeWindows(child, x0+offset, y0, boxSize, height)
} else {
resultForChild = ArrangeWindows(child, x0, y0+offset, width, boxSize)
}
result = mergeDimensionMaps(result, resultForChild)
offset += boxSize
}
return result
}
func (b *Box) isStatic() bool {
return b.Size > 0
}
func (b *Box) getDirection(width int, height int) int {
if b.ConditionalDirection != nil {
return b.ConditionalDirection(width, height)
}
return b.Direction
}
func (b *Box) getChildren(width int, height int) []*Box {
if b.ConditionalChildren != nil {
return b.ConditionalChildren(width, height)
}
return b.Children
}
func mergeDimensionMaps(a map[string]Dimensions, b map[string]Dimensions) map[string]Dimensions {
result := map[string]Dimensions{}
for _, dimensionMap := range []map[string]Dimensions{a, b} {
for k, v := range dimensionMap {
result[k] = v
}
}
return result
}

View File

@@ -0,0 +1,189 @@
package boxlayout
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestArrangeWindows(t *testing.T) {
type scenario struct {
testName string
root *Box
x0 int
y0 int
width int
height int
test func(result map[string]Dimensions)
}
scenarios := []scenario{
{
"Empty box",
&Box{},
0,
0,
10,
10,
func(result map[string]Dimensions) {
assert.EqualValues(t, result, map[string]Dimensions{})
},
},
{
"Box with static and dynamic panel",
&Box{Children: []*Box{{Size: 1, Window: "static"}, {Weight: 1, Window: "dynamic"}}},
0,
0,
10,
10,
func(result map[string]Dimensions) {
assert.EqualValues(
t,
result,
map[string]Dimensions{
"dynamic": {X0: 0, X1: 9, Y0: 1, Y1: 9},
"static": {X0: 0, X1: 9, Y0: 0, Y1: 0},
},
)
},
},
{
"Box with static and two dynamic panels",
&Box{Children: []*Box{{Size: 1, Window: "static"}, {Weight: 1, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}}},
0,
0,
10,
10,
func(result map[string]Dimensions) {
assert.EqualValues(
t,
result,
map[string]Dimensions{
"static": {X0: 0, X1: 9, Y0: 0, Y1: 0},
"dynamic1": {X0: 0, X1: 9, Y0: 1, Y1: 3},
"dynamic2": {X0: 0, X1: 9, Y0: 4, Y1: 9},
},
)
},
},
{
"Box with COLUMN direction",
&Box{Direction: COLUMN, Children: []*Box{{Size: 1, Window: "static"}, {Weight: 1, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}}},
0,
0,
10,
10,
func(result map[string]Dimensions) {
assert.EqualValues(
t,
result,
map[string]Dimensions{
"static": {X0: 0, X1: 0, Y0: 0, Y1: 9},
"dynamic1": {X0: 1, X1: 3, Y0: 0, Y1: 9},
"dynamic2": {X0: 4, X1: 9, Y0: 0, Y1: 9},
},
)
},
},
{
"Box with COLUMN direction only on wide boxes with narrow box",
&Box{ConditionalDirection: func(width int, height int) int {
if width > 4 {
return COLUMN
} else {
return ROW
}
}, Children: []*Box{{Weight: 1, Window: "dynamic1"}, {Weight: 1, Window: "dynamic2"}}},
0,
0,
4,
4,
func(result map[string]Dimensions) {
assert.EqualValues(
t,
result,
map[string]Dimensions{
"dynamic1": {X0: 0, X1: 3, Y0: 0, Y1: 1},
"dynamic2": {X0: 0, X1: 3, Y0: 2, Y1: 3},
},
)
},
},
{
"Box with COLUMN direction only on wide boxes with wide box",
&Box{ConditionalDirection: func(width int, height int) int {
if width > 4 {
return COLUMN
} else {
return ROW
}
}, Children: []*Box{{Weight: 1, Window: "dynamic1"}, {Weight: 1, Window: "dynamic2"}}},
0,
0,
5,
5,
func(result map[string]Dimensions) {
assert.EqualValues(
t,
result,
map[string]Dimensions{
"dynamic1": {X0: 0, X1: 2, Y0: 0, Y1: 4},
"dynamic2": {X0: 3, X1: 4, Y0: 0, Y1: 4},
},
)
},
},
{
"Box with conditional children where box is wide",
&Box{ConditionalChildren: func(width int, height int) []*Box {
if width > 4 {
return []*Box{{Window: "wide", Weight: 1}}
} else {
return []*Box{{Window: "narrow", Weight: 1}}
}
}},
0,
0,
5,
5,
func(result map[string]Dimensions) {
assert.EqualValues(
t,
result,
map[string]Dimensions{
"wide": {X0: 0, X1: 4, Y0: 0, Y1: 4},
},
)
},
},
{
"Box with conditional children where box is narrow",
&Box{ConditionalChildren: func(width int, height int) []*Box {
if width > 4 {
return []*Box{{Window: "wide", Weight: 1}}
} else {
return []*Box{{Window: "narrow", Weight: 1}}
}
}},
0,
0,
4,
4,
func(result map[string]Dimensions) {
assert.EqualValues(
t,
result,
map[string]Dimensions{
"narrow": {X0: 0, X1: 3, Y0: 0, Y1: 3},
},
)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
s.test(ArrangeWindows(s.root, s.x0, s.y0, s.width, s.height))
})
}
}

View File

@@ -6,14 +6,16 @@ import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// list panel functions
func (gui *Gui) getSelectedBranch() *commands.Branch {
selectedLine := gui.State.Panels.Branches.SelectedLine
if len(gui.State.Branches) == 0 {
return nil
}
selectedLine := gui.State.Panels.Branches.SelectedLineIdx
if selectedLine == -1 {
return nil
}
@@ -21,45 +23,32 @@ func (gui *Gui) getSelectedBranch() *commands.Branch {
return gui.State.Branches[selectedLine]
}
// may want to standardise how these select methods work
func (gui *Gui) handleBranchSelect(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
gui.State.SplitMainPanel = false
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
gui.getMainView().Title = "Log"
// This really shouldn't happen: there should always be a master branch
if len(gui.State.Branches) == 0 {
return gui.newStringTask("main", gui.Tr.SLocalize("NoBranchesThisRepo"))
}
func (gui *Gui) handleBranchSelect() error {
var task updateTask
branch := gui.getSelectedBranch()
v.FocusPoint(0, gui.State.Panels.Branches.SelectedLine)
if branch == nil {
task = gui.createRenderStringTask(gui.Tr.SLocalize("NoBranchesThisRepo"))
} else {
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.GetBranchGraphCmdStr(branch.Name),
)
if gui.inDiffMode() {
return gui.renderDiff()
task = gui.createRunPtyTask(cmd)
}
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.GetBranchGraphCmdStr(branch.Name),
)
if err := gui.newCmdTask("main", cmd); err != nil {
gui.Log.Error(err)
}
return nil
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: "Log",
task: task,
},
})
}
// gui.refreshStatus is called at the end of this because that's when we can
// be sure there is a state.Branches array to pick the current branch from
func (gui *Gui) refreshBranches() {
reflogCommits := gui.State.FilteredReflogCommits
if gui.inFilterMode() {
if gui.State.Modes.Filtering.Active() {
// in filter mode we filter our reflog commits to just those containing the path
// however we need all the reflog entries to populate the recencies of our branches
// which allows us to order them correctly. So if we're filtering we'll just
@@ -77,36 +66,20 @@ func (gui *Gui) refreshBranches() {
}
gui.State.Branches = builder.Build()
// TODO: if we're in the remotes view and we've just deleted a remote we need to refresh accordingly
if gui.getBranchesView().Context == "local-branches" {
_ = gui.renderLocalBranchesWithSelection()
if err := gui.postRefreshUpdate(gui.Contexts.Branches.Context); err != nil {
gui.Log.Error(err)
}
gui.refreshStatus()
}
func (gui *Gui) renderLocalBranchesWithSelection() error {
branchesView := gui.getBranchesView()
gui.refreshSelectedLine(&gui.State.Panels.Branches.SelectedLine, len(gui.State.Branches))
displayStrings := presentation.GetBranchListDisplayStrings(gui.State.Branches, gui.State.ScreenMode != SCREEN_NORMAL, gui.State.Diff.Ref)
gui.renderDisplayStrings(branchesView, displayStrings)
if gui.g.CurrentView() == branchesView {
if err := gui.handleBranchSelect(gui.g, branchesView); err != nil {
return gui.surfaceError(err)
}
}
return nil
}
// specific functions
func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error {
if gui.State.Panels.Branches.SelectedLine == -1 {
if gui.State.Panels.Branches.SelectedLineIdx == -1 {
return nil
}
if gui.State.Panels.Branches.SelectedLine == 0 {
if gui.State.Panels.Branches.SelectedLineIdx == 0 {
return gui.createErrorPanel(gui.Tr.SLocalize("AlreadyCheckedOutBranch"))
}
branch := gui.getSelectedBranch()
@@ -125,12 +98,13 @@ func (gui *Gui) handleCreatePullRequestPress(g *gocui.Gui, v *gocui.View) error
}
func (gui *Gui) handleGitFetch(g *gocui.Gui, v *gocui.View) error {
if err := gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("FetchWait")); err != nil {
if err := gui.createLoaderPanel(v, gui.Tr.SLocalize("FetchWait")); err != nil {
return err
}
go func() {
unamePassOpend, err := gui.fetch(g, v, true)
gui.HandleCredentialsPopup(g, unamePassOpend, err)
err := gui.fetch(true)
gui.handleCredentialsPopup(err)
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}()
return nil
}
@@ -139,17 +113,23 @@ func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
branch := gui.getSelectedBranch()
message := gui.Tr.SLocalize("SureForceCheckout")
title := gui.Tr.SLocalize("ForceCheckoutBranch")
return gui.createConfirmationPanel(g, v, true, title, message, func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.Checkout(branch.Name, commands.CheckoutOptions{Force: true}); err != nil {
_ = gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}, nil)
return gui.ask(askOpts{
title: title,
prompt: message,
handleConfirm: func() error {
if err := gui.GitCommand.Checkout(branch.Name, commands.CheckoutOptions{Force: true}); err != nil {
_ = gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
},
})
}
type handleCheckoutRefOptions struct {
WaitingStatus string
EnvVars []string
onRefNotFound func(ref string) error
}
func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions) error {
@@ -161,8 +141,8 @@ func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions)
cmdOptions := commands.CheckoutOptions{Force: false, EnvVars: options.EnvVars}
onSuccess := func() {
gui.State.Panels.Branches.SelectedLine = 0
gui.State.Panels.Commits.SelectedLine = 0
gui.State.Panels.Branches.SelectedLineIdx = 0
gui.State.Panels.Commits.SelectedLineIdx = 0
// loading a heap of commits is slow so we limit them whenever doing a reset
gui.State.Panels.Commits.LimitCommits = true
}
@@ -171,26 +151,34 @@ func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions)
if err := gui.GitCommand.Checkout(ref, cmdOptions); err != nil {
// note, this will only work for english-language git commands. If we force git to use english, and the error isn't this one, then the user will receive an english command they may not understand. I'm not sure what the best solution to this is. Running the command once in english and a second time in the native language is one option
if options.onRefNotFound != nil && strings.Contains(err.Error(), "did not match any file(s) known to git") {
return options.onRefNotFound(ref)
}
if strings.Contains(err.Error(), "Please commit your changes or stash them before you switch branch") {
// offer to autostash changes
return gui.createConfirmationPanel(gui.g, gui.getBranchesView(), true, gui.Tr.SLocalize("AutoStashTitle"), gui.Tr.SLocalize("AutoStashPrompt"), func(g *gocui.Gui, v *gocui.View) error {
return gui.ask(askOpts{
if err := gui.GitCommand.StashSave(gui.Tr.SLocalize("StashPrefix") + ref); err != nil {
return gui.surfaceError(err)
}
if err := gui.GitCommand.Checkout(ref, cmdOptions); err != nil {
return gui.surfaceError(err)
}
onSuccess()
if err := gui.GitCommand.StashDo(0, "pop"); err != nil {
if err := gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI}); err != nil {
return err
title: gui.Tr.SLocalize("AutoStashTitle"),
prompt: gui.Tr.SLocalize("AutoStashPrompt"),
handleConfirm: func() error {
if err := gui.GitCommand.StashSave(gui.Tr.SLocalize("StashPrefix") + ref); err != nil {
return gui.surfaceError(err)
}
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI})
}, nil)
if err := gui.GitCommand.Checkout(ref, cmdOptions); err != nil {
return gui.surfaceError(err)
}
onSuccess()
if err := gui.GitCommand.StashDo(0, "pop"); err != nil {
if err := gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI}); err != nil {
return err
}
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI})
},
})
}
if err := gui.surfaceError(err); err != nil {
@@ -204,8 +192,20 @@ func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions)
}
func (gui *Gui) handleCheckoutByName(g *gocui.Gui, v *gocui.View) error {
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("BranchName")+":", "", func(g *gocui.Gui, v *gocui.View) error {
return gui.handleCheckoutRef(gui.trimmedContent(v), handleCheckoutRefOptions{})
return gui.prompt(gui.Tr.SLocalize("BranchName")+":", "", func(response string) error {
return gui.handleCheckoutRef(response, handleCheckoutRefOptions{
onRefNotFound: func(ref string) error {
return gui.ask(askOpts{
title: gui.Tr.SLocalize("BranchNotFoundTitle"),
prompt: fmt.Sprintf("%s %s%s", gui.Tr.SLocalize("BranchNotFoundPrompt"), ref, "?"),
handleConfirm: func() error {
return gui.createNewBranchWithName(ref)
},
})
},
})
})
}
@@ -217,31 +217,25 @@ func (gui *Gui) getCheckedOutBranch() *commands.Branch {
return gui.State.Branches[0]
}
func (gui *Gui) handleNewBranch(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) createNewBranchWithName(newBranchName string) error {
branch := gui.getSelectedBranch()
if branch == nil {
return nil
}
message := gui.Tr.TemplateLocalize(
"NewBranchNameBranchOff",
Teml{
"branchName": branch.Name,
},
)
return gui.createPromptPanel(g, v, message, "", func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.NewBranch(gui.trimmedContent(v), branch.Name); err != nil {
return gui.surfaceError(err)
}
gui.State.Panels.Branches.SelectedLine = 0
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
})
if err := gui.GitCommand.NewBranch(newBranchName, branch.Name); err != nil {
return gui.surfaceError(err)
}
gui.State.Panels.Branches.SelectedLineIdx = 0
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}
func (gui *Gui) handleDeleteBranch(g *gocui.Gui, v *gocui.View) error {
return gui.deleteBranch(g, v, false)
return gui.deleteBranch(false)
}
func (gui *Gui) deleteBranch(g *gocui.Gui, v *gocui.View, force bool) error {
func (gui *Gui) deleteBranch(force bool) error {
selectedBranch := gui.getSelectedBranch()
if selectedBranch == nil {
return nil
@@ -250,10 +244,10 @@ func (gui *Gui) deleteBranch(g *gocui.Gui, v *gocui.View, force bool) error {
if checkedOutBranch.Name == selectedBranch.Name {
return gui.createErrorPanel(gui.Tr.SLocalize("CantDeleteCheckOutBranch"))
}
return gui.deleteNamedBranch(g, v, selectedBranch, force)
return gui.deleteNamedBranch(selectedBranch, force)
}
func (gui *Gui) deleteNamedBranch(g *gocui.Gui, v *gocui.View, selectedBranch *commands.Branch, force bool) error {
func (gui *Gui) deleteNamedBranch(selectedBranch *commands.Branch, force bool) error {
title := gui.Tr.SLocalize("DeleteBranch")
var messageID string
if force {
@@ -267,16 +261,22 @@ func (gui *Gui) deleteNamedBranch(g *gocui.Gui, v *gocui.View, selectedBranch *c
"selectedBranchName": selectedBranch.Name,
},
)
return gui.createConfirmationPanel(g, v, true, title, message, func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.DeleteBranch(selectedBranch.Name, force); err != nil {
errMessage := err.Error()
if !force && strings.Contains(errMessage, "is not fully merged") {
return gui.deleteNamedBranch(g, v, selectedBranch, true)
return gui.ask(askOpts{
title: title,
prompt: message,
handleConfirm: func() error {
if err := gui.GitCommand.DeleteBranch(selectedBranch.Name, force); err != nil {
errMessage := err.Error()
if !force && strings.Contains(errMessage, "is not fully merged") {
return gui.deleteNamedBranch(selectedBranch, true)
}
return gui.createErrorPanel(errMessage)
}
return gui.createErrorPanel(errMessage)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{BRANCHES}})
}, nil)
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{BRANCHES}})
},
})
}
func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error {
@@ -298,12 +298,16 @@ func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error {
"selectedBranch": branchName,
},
)
return gui.createConfirmationPanel(gui.g, gui.getBranchesView(), true, gui.Tr.SLocalize("MergingTitle"), prompt,
func(g *gocui.Gui, v *gocui.View) error {
err := gui.GitCommand.Merge(branchName)
return gui.ask(askOpts{
title: gui.Tr.SLocalize("MergingTitle"),
prompt: prompt,
handleConfirm: func() error {
err := gui.GitCommand.Merge(branchName, commands.MergeOpts{})
return gui.handleGenericMergeCommandResult(err)
}, nil)
},
})
}
func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error {
@@ -336,11 +340,16 @@ func (gui *Gui) handleRebaseOntoBranch(selectedBranchName string) error {
"selectedBranch": selectedBranchName,
},
)
return gui.createConfirmationPanel(gui.g, gui.getBranchesView(), true, gui.Tr.SLocalize("RebasingTitle"), prompt,
func(g *gocui.Gui, v *gocui.View) error {
return gui.ask(askOpts{
title: gui.Tr.SLocalize("RebasingTitle"),
prompt: prompt,
handleConfirm: func() error {
err := gui.GitCommand.RebaseBranch(selectedBranchName)
return gui.handleGenericMergeCommandResult(err)
}, nil)
},
})
}
func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
@@ -375,83 +384,19 @@ func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
},
)
go func() {
_ = gui.createLoaderPanel(gui.g, v, message)
_ = gui.createLoaderPanel(v, message)
if gui.State.Panels.Branches.SelectedLine == 0 {
if err := gui.GitCommand.PullWithoutPasswordCheck("--ff-only"); err != nil {
_ = gui.surfaceError(err)
return
}
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC})
if gui.State.Panels.Branches.SelectedLineIdx == 0 {
_ = gui.pullWithMode("ff-only", PullFilesOptions{})
} else {
if err := gui.GitCommand.FastForward(branch.Name, remoteName, remoteBranchName); err != nil {
_ = gui.surfaceError(err)
return
}
err := gui.GitCommand.FastForward(branch.Name, remoteName, remoteBranchName, gui.promptUserForCredential)
gui.handleCredentialsPopup(err)
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{BRANCHES}})
}
_ = gui.closeConfirmationPrompt(gui.g, true)
}()
return nil
}
func (gui *Gui) onBranchesTabClick(tabIndex int) error {
contexts := []string{"local-branches", "remotes", "tags"}
branchesView := gui.getBranchesView()
branchesView.TabIndex = tabIndex
return gui.switchBranchesPanelContext(contexts[tabIndex])
}
func (gui *Gui) switchBranchesPanelContext(context string) error {
branchesView := gui.getBranchesView()
branchesView.Context = context
if err := gui.onSearchEscape(); err != nil {
return err
}
contextTabIndexMap := map[string]int{
"local-branches": 0,
"remotes": 1,
"remote-branches": 1,
"tags": 2,
}
branchesView.TabIndex = contextTabIndexMap[context]
return gui.refreshBranchesViewWithSelection()
}
func (gui *Gui) refreshBranchesViewWithSelection() error {
branchesView := gui.getBranchesView()
switch branchesView.Context {
case "local-branches":
return gui.renderLocalBranchesWithSelection()
case "remotes":
return gui.renderRemotesWithSelection()
case "remote-branches":
return gui.renderRemoteBranchesWithSelection()
case "tags":
return gui.renderTagsWithSelection()
}
return nil
}
func (gui *Gui) handleNextBranchesTab(g *gocui.Gui, v *gocui.View) error {
return gui.onBranchesTabClick(
utils.ModuloWithWrap(v.TabIndex+1, len(v.Tabs)),
)
}
func (gui *Gui) handlePrevBranchesTab(g *gocui.Gui, v *gocui.View) error {
return gui.onBranchesTabClick(
utils.ModuloWithWrap(v.TabIndex-1, len(v.Tabs)),
)
}
func (gui *Gui) handleCreateResetToBranchMenu(g *gocui.Gui, v *gocui.View) error {
branch := gui.getSelectedBranch()
if branch == nil {
@@ -461,22 +406,6 @@ func (gui *Gui) handleCreateResetToBranchMenu(g *gocui.Gui, v *gocui.View) error
return gui.createResetMenu(branch.Name)
}
func (gui *Gui) onBranchesPanelSearchSelect(selectedLine int) error {
branchesView := gui.getBranchesView()
switch branchesView.Context {
case "local-branches":
gui.State.Panels.Branches.SelectedLine = selectedLine
return gui.handleBranchSelect(gui.g, branchesView)
case "remotes":
gui.State.Panels.Remotes.SelectedLine = selectedLine
return gui.handleRemoteSelect(gui.g, branchesView)
case "remote-branches":
gui.State.Panels.RemoteBranches.SelectedLine = selectedLine
return gui.handleRemoteBranchSelect(gui.g, branchesView)
}
return nil
}
func (gui *Gui) handleRenameBranch(g *gocui.Gui, v *gocui.View) error {
branch := gui.getSelectedBranch()
if branch == nil {
@@ -487,14 +416,13 @@ func (gui *Gui) handleRenameBranch(g *gocui.Gui, v *gocui.View) error {
// way to get it to show up in the reflog)
promptForNewName := func() error {
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("NewBranchNamePrompt")+" "+branch.Name+":", "", func(g *gocui.Gui, v *gocui.View) error {
newName := gui.trimmedContent(v)
if err := gui.GitCommand.RenameBranch(branch.Name, newName); err != nil {
return gui.prompt(gui.Tr.SLocalize("NewBranchNamePrompt")+" "+branch.Name+":", "", func(newBranchName string) error {
if err := gui.GitCommand.RenameBranch(branch.Name, newBranchName); err != nil {
return gui.surfaceError(err)
}
// need to checkout so that the branch shows up in our reflog and therefore
// doesn't get lost among all the other branches when we switch to something else
if err := gui.GitCommand.Checkout(newName, commands.CheckoutOptions{Force: false}); err != nil {
if err := gui.GitCommand.Checkout(newBranchName, commands.CheckoutOptions{Force: false}); err != nil {
return gui.surfaceError(err)
}
@@ -509,9 +437,13 @@ func (gui *Gui) handleRenameBranch(g *gocui.Gui, v *gocui.View) error {
if notTrackingRemote {
return promptForNewName()
}
return gui.createConfirmationPanel(gui.g, v, true, gui.Tr.SLocalize("renameBranch"), gui.Tr.SLocalize("RenameBranchWarning"), func(_g *gocui.Gui, _v *gocui.View) error {
return promptForNewName()
}, nil)
return gui.ask(askOpts{
title: gui.Tr.SLocalize("renameBranch"),
prompt: gui.Tr.SLocalize("RenameBranchWarning"),
handleConfirm: promptForNewName,
})
}
func (gui *Gui) currentBranch() *commands.Branch {
@@ -521,11 +453,45 @@ func (gui *Gui) currentBranch() *commands.Branch {
return gui.State.Branches[0]
}
func (gui *Gui) handleClipboardCopyBranch(g *gocui.Gui, v *gocui.View) error {
branch := gui.getSelectedBranch()
if branch == nil {
func (gui *Gui) handleNewBranchOffCurrentItem() error {
context := gui.currentSideContext()
item, ok := context.GetSelectedItem()
if !ok {
return nil
}
return gui.OSCommand.CopyToClipboard(branch.Name)
message := gui.Tr.TemplateLocalize(
"NewBranchNameBranchOff",
Teml{
"branchName": item.Description(),
},
)
prefilledName := ""
if context.GetKey() == REMOTE_BRANCHES_CONTEXT_KEY {
// will set to the remote's existing name
prefilledName = item.ID()
}
return gui.prompt(message, prefilledName, func(response string) error {
if err := gui.GitCommand.NewBranch(response, item.ID()); err != nil {
return err
}
// if we're currently in the branch commits context then the selected commit
// is about to go to the top of the list
if context.GetKey() == BRANCH_COMMITS_CONTEXT_KEY {
context.GetPanelState().SetSelectedLineIdx(0)
}
if context.GetKey() != gui.Contexts.Branches.Context.GetKey() {
if err := gui.switchContext(gui.Contexts.Branches.Context); err != nil {
return err
}
}
gui.State.Panels.Branches.SelectedLineIdx = 0
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
})
}

196
pkg/gui/cherry_picking.go Normal file
View File

@@ -0,0 +1,196 @@
package gui
import (
"github.com/jesseduffield/lazygit/pkg/commands"
)
// you can only copy from one context at a time, because the order and position of commits matter
func (gui *Gui) resetCherryPickingIfNecessary(context Context) error {
oldContextKey := gui.State.Modes.CherryPicking.ContextKey
if oldContextKey != context.GetKey() {
// need to reset the cherry picking mode
gui.State.Modes.CherryPicking.ContextKey = context.GetKey()
gui.State.Modes.CherryPicking.CherryPickedCommits = make([]*commands.Commit, 0)
return gui.rerenderContextViewIfPresent(oldContextKey)
}
return nil
}
func (gui *Gui) handleCopyCommit() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
// get currently selected commit, add the sha to state.
context := gui.currentSideContext()
if context == nil {
return nil
}
if err := gui.resetCherryPickingIfNecessary(context); err != nil {
return err
}
item, ok := context.SelectedItem()
if !ok {
return nil
}
commit, ok := item.(*commands.Commit)
if !ok {
return nil
}
// we will un-copy it if it's already copied
for index, cherryPickedCommit := range gui.State.Modes.CherryPicking.CherryPickedCommits {
if commit.Sha == cherryPickedCommit.Sha {
gui.State.Modes.CherryPicking.CherryPickedCommits = append(gui.State.Modes.CherryPicking.CherryPickedCommits[0:index], gui.State.Modes.CherryPicking.CherryPickedCommits[index+1:]...)
return context.HandleRender()
}
}
gui.addCommitToCherryPickedCommits(context.GetPanelState().GetSelectedLineIdx())
return context.HandleRender()
}
func (gui *Gui) cherryPickedCommitShaMap() map[string]bool {
commitShaMap := map[string]bool{}
for _, commit := range gui.State.Modes.CherryPicking.CherryPickedCommits {
commitShaMap[commit.Sha] = true
}
return commitShaMap
}
func (gui *Gui) commitsListForContext() []*commands.Commit {
context := gui.currentSideContext()
if context == nil {
return nil
}
// using a switch statement, but we should use polymorphism
switch context.GetKey() {
case BRANCH_COMMITS_CONTEXT_KEY:
return gui.State.Commits
case REFLOG_COMMITS_CONTEXT_KEY:
return gui.State.FilteredReflogCommits
case SUB_COMMITS_CONTEXT_KEY:
return gui.State.SubCommits
default:
gui.Log.Errorf("no commit list for context %s", context.GetKey())
return nil
}
}
func (gui *Gui) addCommitToCherryPickedCommits(index int) {
commitShaMap := gui.cherryPickedCommitShaMap()
commitsList := gui.commitsListForContext()
commitShaMap[commitsList[index].Sha] = true
newCommits := []*commands.Commit{}
for _, commit := range commitsList {
if commitShaMap[commit.Sha] {
// duplicating just the things we need to put in the rebase TODO list
newCommits = append(newCommits, &commands.Commit{Name: commit.Name, Sha: commit.Sha})
}
}
gui.State.Modes.CherryPicking.CherryPickedCommits = newCommits
}
func (gui *Gui) handleCopyCommitRange() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
// get currently selected commit, add the sha to state.
context := gui.currentSideContext()
if context == nil {
return nil
}
if err := gui.resetCherryPickingIfNecessary(context); err != nil {
return err
}
commitShaMap := gui.cherryPickedCommitShaMap()
commitsList := gui.commitsListForContext()
selectedLineIdx := context.GetPanelState().GetSelectedLineIdx()
if selectedLineIdx > len(commitsList)-1 {
return nil
}
// find the last commit that is copied that's above our position
// if there are none, startIndex = 0
startIndex := 0
for index, commit := range commitsList[0:selectedLineIdx] {
if commitShaMap[commit.Sha] {
startIndex = index
}
}
for index := startIndex; index <= selectedLineIdx; index++ {
gui.addCommitToCherryPickedCommits(index)
}
return context.HandleRender()
}
// HandlePasteCommits begins a cherry-pick rebase with the commits the user has copied
func (gui *Gui) HandlePasteCommits() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
return gui.ask(askOpts{
title: gui.Tr.SLocalize("CherryPick"),
prompt: gui.Tr.SLocalize("SureCherryPick"),
handleConfirm: func() error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("CherryPickingStatus"), func() error {
err := gui.GitCommand.CherryPickCommits(gui.State.Modes.CherryPicking.CherryPickedCommits)
return gui.handleGenericMergeCommandResult(err)
})
},
})
}
func (gui *Gui) exitCherryPickingMode() error {
contextKey := gui.State.Modes.CherryPicking.ContextKey
gui.State.Modes.CherryPicking.ContextKey = ""
gui.State.Modes.CherryPicking.CherryPickedCommits = nil
if contextKey == "" {
gui.Log.Warn("context key blank when trying to exit cherry picking mode")
return nil
}
return gui.rerenderContextViewIfPresent(contextKey)
}
func (gui *Gui) rerenderContextViewIfPresent(contextKey string) error {
if contextKey == "" {
return nil
}
context := gui.contextForContextKey(contextKey)
viewName := context.GetViewName()
view, err := gui.g.View(viewName)
if err != nil {
gui.Log.Warn(err)
return nil
}
if view.Context == contextKey {
if err := context.HandleRender(); err != nil {
return err
}
}
return nil
}

View File

@@ -1,69 +1,51 @@
package gui
import (
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
)
func (gui *Gui) getSelectedCommitFile() *commands.CommitFile {
selectedLine := gui.State.Panels.CommitFiles.SelectedLine
if selectedLine == -1 {
selectedLine := gui.State.Panels.CommitFiles.SelectedLineIdx
if selectedLine == -1 || selectedLine > len(gui.State.CommitFiles)-1 {
return nil
}
return gui.State.CommitFiles[selectedLine]
}
func (gui *Gui) handleCommitFilesClick(g *gocui.Gui, v *gocui.View) error {
itemCount := len(gui.State.CommitFiles)
handleSelect := gui.handleCommitFileSelect
selectedLine := &gui.State.Panels.CommitFiles.SelectedLine
return gui.handleClick(v, itemCount, selectedLine, handleSelect)
}
func (gui *Gui) handleCommitFileSelect(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
gui.getMainView().Title = "Patch"
if gui.currentViewName() == "commitFiles" {
gui.handleEscapeLineByLinePanel()
}
func (gui *Gui) handleCommitFileSelect() error {
gui.handleEscapeLineByLinePanel()
commitFile := gui.getSelectedCommitFile()
if commitFile == nil {
gui.renderString(g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
return nil
}
if err := gui.refreshSecondaryPatchPanel(); err != nil {
return err
}
v.FocusPoint(0, gui.State.Panels.CommitFiles.SelectedLine)
to := commitFile.Parent
from, reverse := gui.getFromAndReverseArgsForDiff(to)
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowCommitFileCmdStr(commitFile.Sha, commitFile.Name, false),
gui.GitCommand.ShowFileDiffCmdStr(from, to, reverse, commitFile.Name, false),
)
if err := gui.newPtyTask("main", cmd); err != nil {
gui.Log.Error(err)
}
task := gui.createRunPtyTask(cmd)
return nil
}
func (gui *Gui) handleSwitchToCommitsPanel(g *gocui.Gui, v *gocui.View) error {
return gui.switchFocus(g, v, gui.getCommitsView())
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: "Patch",
task: task,
},
secondary: gui.secondaryPatchPanelUpdateOpts(),
})
}
func (gui *Gui) handleCheckoutCommitFile(g *gocui.Gui, v *gocui.View) error {
file := gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLine]
file := gui.getSelectedCommitFile()
if file == nil {
return nil
}
if err := gui.GitCommand.CheckoutFile(file.Sha, file.Name); err != nil {
if err := gui.GitCommand.CheckoutFile(file.Parent, file.Name); err != nil {
return gui.surfaceError(err)
}
@@ -75,104 +57,105 @@ func (gui *Gui) handleDiscardOldFileChange(g *gocui.Gui, v *gocui.View) error {
return err
}
fileName := gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLine].Name
fileName := gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLineIdx].Name
return gui.createConfirmationPanel(gui.g, v, true, gui.Tr.SLocalize("DiscardFileChangesTitle"), gui.Tr.SLocalize("DiscardFileChangesPrompt"), func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
if err := gui.GitCommand.DiscardOldFileChanges(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, fileName); err != nil {
if err := gui.handleGenericMergeCommandResult(err); err != nil {
return err
return gui.ask(askOpts{
title: gui.Tr.SLocalize("DiscardFileChangesTitle"),
prompt: gui.Tr.SLocalize("DiscardFileChangesPrompt"),
handleConfirm: func() error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
if err := gui.GitCommand.DiscardOldFileChanges(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, fileName); err != nil {
if err := gui.handleGenericMergeCommandResult(err); err != nil {
return err
}
}
}
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI})
})
}, nil)
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI})
})
},
})
}
func (gui *Gui) refreshCommitFilesView() error {
if err := gui.refreshSecondaryPatchPanel(); err != nil {
return err
}
if err := gui.refreshPatchBuildingPanel(-1); err != nil {
return err
}
commit := gui.getSelectedCommit()
if commit == nil {
return nil
}
to := gui.State.Panels.CommitFiles.refName
from, reverse := gui.getFromAndReverseArgsForDiff(to)
files, err := gui.GitCommand.GetCommitFiles(commit.Sha, gui.GitCommand.PatchManager)
files, err := gui.GitCommand.GetFilesInDiff(from, to, reverse, gui.GitCommand.PatchManager)
if err != nil {
return gui.surfaceError(err)
}
gui.State.CommitFiles = files
gui.refreshSelectedLine(&gui.State.Panels.CommitFiles.SelectedLine, len(gui.State.CommitFiles))
commitsFileView := gui.getCommitFilesView()
displayStrings := presentation.GetCommitFileListDisplayStrings(gui.State.CommitFiles, gui.State.Diff.Ref)
gui.renderDisplayStrings(commitsFileView, displayStrings)
return gui.handleCommitFileSelect(gui.g, commitsFileView)
return gui.postRefreshUpdate(gui.Contexts.CommitFiles.Context)
}
func (gui *Gui) handleOpenOldCommitFile(g *gocui.Gui, v *gocui.View) error {
file := gui.getSelectedCommitFile()
if file == nil {
return nil
}
return gui.openFile(file.Name)
}
func (gui *Gui) handleToggleFileForPatch(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
return err
func (gui *Gui) handleEditCommitFile(g *gocui.Gui, v *gocui.View) error {
file := gui.getSelectedCommitFile()
if file == nil {
return nil
}
return gui.editFile(file.Name)
}
func (gui *Gui) handleToggleFileForPatch(g *gocui.Gui, v *gocui.View) error {
commitFile := gui.getSelectedCommitFile()
if commitFile == nil {
gui.renderString(g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
return nil
}
toggleTheFile := func() error {
if !gui.GitCommand.PatchManager.CommitSelected() {
if !gui.GitCommand.PatchManager.Active() {
if err := gui.startPatchManager(); err != nil {
return err
}
}
gui.GitCommand.PatchManager.ToggleFileWhole(commitFile.Name)
if err := gui.GitCommand.PatchManager.ToggleFileWhole(commitFile.Name); err != nil {
return err
}
if gui.GitCommand.PatchManager.IsEmpty() {
gui.GitCommand.PatchManager.Reset()
}
return gui.refreshCommitFilesView()
}
if gui.GitCommand.PatchManager.CommitSelected() && gui.GitCommand.PatchManager.CommitSha != commitFile.Sha {
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("DiscardPatch"), gui.Tr.SLocalize("DiscardPatchConfirm"), func(g *gocui.Gui, v *gocui.View) error {
gui.GitCommand.PatchManager.Reset()
return toggleTheFile()
}, nil)
if gui.GitCommand.PatchManager.Active() && gui.GitCommand.PatchManager.To != commitFile.Parent {
return gui.ask(askOpts{
title: gui.Tr.SLocalize("DiscardPatch"),
prompt: gui.Tr.SLocalize("DiscardPatchConfirm"),
handleConfirm: func() error {
gui.GitCommand.PatchManager.Reset()
return toggleTheFile()
},
})
}
return toggleTheFile()
}
func (gui *Gui) startPatchManager() error {
diffMap := map[string]string{}
for _, commitFile := range gui.State.CommitFiles {
commitText, err := gui.GitCommand.ShowCommitFile(commitFile.Sha, commitFile.Name, true)
if err != nil {
return err
}
diffMap[commitFile.Name] = commitText
}
canRebase := gui.State.Panels.CommitFiles.canRebase
commit := gui.getSelectedCommit()
if commit == nil {
return errors.New("No commit selected")
}
to := gui.State.Panels.CommitFiles.refName
from, reverse := gui.getFromAndReverseArgsForDiff(to)
gui.GitCommand.PatchManager.Start(commit.Sha, diffMap)
gui.GitCommand.PatchManager.Start(from, to, reverse, canRebase)
return nil
}
@@ -181,43 +164,56 @@ func (gui *Gui) handleEnterCommitFile(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
return err
}
commitFile := gui.getSelectedCommitFile()
if commitFile == nil {
gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
return nil
}
enterTheFile := func(selectedLineIdx int) error {
if !gui.GitCommand.PatchManager.CommitSelected() {
if !gui.GitCommand.PatchManager.Active() {
if err := gui.startPatchManager(); err != nil {
return err
}
}
gui.changeMainViewsContext("patch-building")
if err := gui.switchFocus(gui.g, gui.getCommitFilesView(), gui.getMainView()); err != nil {
if err := gui.switchContext(gui.Contexts.PatchBuilding.Context); err != nil {
return err
}
return gui.refreshPatchBuildingPanel(selectedLineIdx)
}
if gui.GitCommand.PatchManager.CommitSelected() && gui.GitCommand.PatchManager.CommitSha != commitFile.Sha {
return gui.createConfirmationPanel(gui.g, gui.getCommitFilesView(), false, gui.Tr.SLocalize("DiscardPatch"), gui.Tr.SLocalize("DiscardPatchConfirm"), func(g *gocui.Gui, v *gocui.View) error {
gui.GitCommand.PatchManager.Reset()
return enterTheFile(selectedLineIdx)
}, func(g *gocui.Gui, v *gocui.View) error {
return gui.switchFocus(gui.g, nil, gui.getCommitFilesView())
if gui.GitCommand.PatchManager.Active() && gui.GitCommand.PatchManager.To != commitFile.Parent {
return gui.ask(askOpts{
title: gui.Tr.SLocalize("DiscardPatch"),
prompt: gui.Tr.SLocalize("DiscardPatchConfirm"),
handlersManageFocus: true,
handleConfirm: func() error {
gui.GitCommand.PatchManager.Reset()
return enterTheFile(selectedLineIdx)
},
handleClose: func() error {
return gui.switchContext(gui.Contexts.CommitFiles.Context)
},
})
}
return enterTheFile(selectedLineIdx)
}
func (gui *Gui) onCommitFilesPanelSearchSelect(selectedLine int) error {
gui.State.Panels.CommitFiles.SelectedLine = selectedLine
return gui.handleCommitFileSelect(gui.g, gui.getCommitFilesView())
func (gui *Gui) switchToCommitFilesContext(refName string, canRebase bool, context Context, windowName string) error {
// sometimes the commitFiles view is already shown in another window, so we need to ensure that window
// no longer considers the commitFiles view as its main view.
gui.resetWindowForView("commitFiles")
gui.State.Panels.CommitFiles.SelectedLineIdx = 0
gui.State.Panels.CommitFiles.refName = refName
gui.State.Panels.CommitFiles.canRebase = canRebase
gui.Contexts.CommitFiles.Context.SetParentContext(context)
gui.Contexts.CommitFiles.Context.SetWindowName(windowName)
if err := gui.refreshCommitFilesView(); err != nil {
return err
}
return gui.switchContext(gui.Contexts.CommitFiles.Context)
}

View File

@@ -43,32 +43,25 @@ func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
return nil
}
v.Clear()
_ = v.SetCursor(0, 0)
_ = v.SetOrigin(0, 0)
_, _ = g.SetViewOnBottom("commitMessage")
_ = gui.switchFocus(g, v, gui.getFilesView())
gui.clearEditorView(v)
_ = gui.returnFromContext()
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}
func (gui *Gui) handleCommitClose(g *gocui.Gui, v *gocui.View) error {
_, _ = g.SetViewOnBottom("commitMessage")
return gui.switchFocus(g, v, gui.getFilesView())
return gui.returnFromContext()
}
func (gui *Gui) handleCommitFocused(g *gocui.Gui, v *gocui.View) error {
if _, err := g.SetViewOnTop("commitMessage"); err != nil {
return err
}
func (gui *Gui) handleCommitMessageFocused() error {
message := gui.Tr.TemplateLocalize(
"CloseConfirm",
"CommitMessageConfirm",
Teml{
"keyBindClose": "esc",
"keyBindConfirm": "enter",
"keyBindNewLine": "tab",
},
)
gui.renderString(g, "options", message)
gui.renderString("options", message)
return nil
}

View File

@@ -1,19 +1,16 @@
package gui
import (
"strconv"
"sync"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// list panel functions
func (gui *Gui) getSelectedCommit() *commands.Commit {
selectedLine := gui.State.Panels.Commits.SelectedLine
func (gui *Gui) getSelectedLocalCommit() *commands.Commit {
selectedLine := gui.State.Panels.Commits.SelectedLineIdx
if selectedLine == -1 {
return nil
}
@@ -21,22 +18,9 @@ func (gui *Gui) getSelectedCommit() *commands.Commit {
return gui.State.Commits[selectedLine]
}
func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
// this probably belongs in an 'onFocus' function than a 'commit selected' function
if err := gui.refreshSecondaryPatchPanel(); err != nil {
return err
}
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
func (gui *Gui) handleCommitSelect() error {
state := gui.State.Panels.Commits
if state.SelectedLine > 290 && state.LimitCommits {
if state.SelectedLineIdx > 290 && state.LimitCommits {
state.LimitCommits = false
go func() {
if err := gui.refreshCommitsWithLimit(); err != nil {
@@ -45,29 +29,26 @@ func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
}()
}
gui.getMainView().Title = "Patch"
gui.getSecondaryView().Title = "Custom Patch"
gui.handleEscapeLineByLinePanel()
commit := gui.getSelectedCommit()
var task updateTask
commit := gui.getSelectedLocalCommit()
if commit == nil {
return gui.newStringTask("main", gui.Tr.SLocalize("NoCommitsThisBranch"))
task = gui.createRenderStringTask(gui.Tr.SLocalize("NoCommitsThisBranch"))
} else {
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.Modes.Filtering.Path),
)
task = gui.createRunPtyTask(cmd)
}
v.FocusPoint(0, gui.State.Panels.Commits.SelectedLine)
if gui.inDiffMode() {
return gui.renderDiff()
}
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.FilterPath),
)
if err := gui.newPtyTask("main", cmd); err != nil {
gui.Log.Error(err)
}
return nil
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: "Patch",
task: task,
},
secondary: gui.secondaryPatchPanelUpdateOpts(),
})
}
// during startup, the bottleneck is fetching the reflog entries. We need these
@@ -104,7 +85,7 @@ func (gui *Gui) refreshCommits() error {
go func() {
_ = gui.refreshCommitsWithLimit()
if gui.g.CurrentView() == gui.getCommitFilesView() || (gui.g.CurrentView() == gui.getMainView() && gui.State.MainContext == "patch-building") {
if gui.g.CurrentView() == gui.getCommitFilesView() || (gui.currentContext().GetKey() == gui.Contexts.PatchBuilding.Context.GetKey()) {
_ = gui.refreshCommitFilesView()
}
wg.Done()
@@ -116,24 +97,22 @@ func (gui *Gui) refreshCommits() error {
}
func (gui *Gui) refreshCommitsWithLimit() error {
builder, err := commands.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr, gui.State.CherryPickedCommits)
if err != nil {
return err
}
builder := commands.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr, gui.State.Modes.CherryPicking.CherryPickedCommits)
commits, err := builder.GetCommits(commands.GetCommitsOptions{Limit: gui.State.Panels.Commits.LimitCommits, FilterPath: gui.State.FilterPath})
commits, err := builder.GetCommits(
commands.GetCommitsOptions{
Limit: gui.State.Panels.Commits.LimitCommits,
FilterPath: gui.State.Modes.Filtering.Path,
IncludeRebaseCommits: true,
RefName: "HEAD",
},
)
if err != nil {
return err
}
gui.State.Commits = commits
if gui.getCommitsView().Context == "branch-commits" {
if err := gui.renderBranchCommitsWithSelection(); err != nil {
return err
}
}
return nil
return gui.postRefreshUpdate(gui.Contexts.BranchCommits.Context)
}
// specific functions
@@ -155,12 +134,16 @@ func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
return nil
}
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("Squash"), gui.Tr.SLocalize("SureSquashThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("SquashingStatus"), func() error {
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "squash")
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
return gui.ask(askOpts{
title: gui.Tr.SLocalize("Squash"),
prompt: gui.Tr.SLocalize("SureSquashThisCommit"),
handleConfirm: func() error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("SquashingStatus"), func() error {
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "squash")
return gui.handleGenericMergeCommandResult(err)
})
},
})
}
func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
@@ -180,12 +163,16 @@ func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
return nil
}
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("Fixup"), gui.Tr.SLocalize("SureFixupThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("FixingStatus"), func() error {
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "fixup")
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
return gui.ask(askOpts{
title: gui.Tr.SLocalize("Fixup"),
prompt: gui.Tr.SLocalize("SureFixupThisCommit"),
handleConfirm: func() error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("FixingStatus"), func() error {
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "fixup")
return gui.handleGenericMergeCommandResult(err)
})
},
})
}
func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
@@ -201,11 +188,22 @@ func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
return nil
}
if gui.State.Panels.Commits.SelectedLine != 0 {
if gui.State.Panels.Commits.SelectedLineIdx != 0 {
return gui.createErrorPanel(gui.Tr.SLocalize("OnlyRenameTopCommit"))
}
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("renameCommit"), "", func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.RenameCommit(v.Buffer()); err != nil {
commit := gui.getSelectedLocalCommit()
if commit == nil {
return nil
}
message, err := gui.GitCommand.GetCommitMessage(commit.Sha)
if err != nil {
return gui.surfaceError(err)
}
return gui.prompt(gui.Tr.SLocalize("renameCommit"), message, func(response string) error {
if err := gui.GitCommand.RenameCommit(response); err != nil {
return gui.surfaceError(err)
}
@@ -226,7 +224,7 @@ func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
return nil
}
subProcess, err := gui.GitCommand.RewordCommit(gui.State.Commits, gui.State.Panels.Commits.SelectedLine)
subProcess, err := gui.GitCommand.RewordCommit(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx)
if err != nil {
return gui.surfaceError(err)
}
@@ -242,7 +240,7 @@ func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
// commit meaning you are trying to edit the todo file rather than actually
// begin a rebase. It then updates the todo file with that action
func (gui *Gui) handleMidRebaseCommand(action string) (bool, error) {
selectedCommit := gui.State.Commits[gui.State.Panels.Commits.SelectedLine]
selectedCommit := gui.State.Commits[gui.State.Panels.Commits.SelectedLineIdx]
if selectedCommit.Status != "rebasing" {
return false, nil
}
@@ -255,7 +253,7 @@ func (gui *Gui) handleMidRebaseCommand(action string) (bool, error) {
return true, gui.createErrorPanel(gui.Tr.SLocalize("rewordNotSupported"))
}
if err := gui.GitCommand.EditRebaseTodo(gui.State.Panels.Commits.SelectedLine, action); err != nil {
if err := gui.GitCommand.EditRebaseTodo(gui.State.Panels.Commits.SelectedLineIdx, action); err != nil {
return false, gui.surfaceError(err)
}
// TODO: consider doing this in a way that is less expensive. We don't actually
@@ -276,12 +274,16 @@ func (gui *Gui) handleCommitDelete(g *gocui.Gui, v *gocui.View) error {
return nil
}
return gui.createConfirmationPanel(gui.g, v, true, gui.Tr.SLocalize("DeleteCommitTitle"), gui.Tr.SLocalize("DeleteCommitPrompt"), func(*gocui.Gui, *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("DeletingStatus"), func() error {
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "drop")
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
return gui.ask(askOpts{
title: gui.Tr.SLocalize("DeleteCommitTitle"),
prompt: gui.Tr.SLocalize("DeleteCommitPrompt"),
handleConfirm: func() error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("DeletingStatus"), func() error {
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "drop")
return gui.handleGenericMergeCommandResult(err)
})
},
})
}
func (gui *Gui) handleCommitMoveDown(g *gocui.Gui, v *gocui.View) error {
@@ -289,7 +291,7 @@ func (gui *Gui) handleCommitMoveDown(g *gocui.Gui, v *gocui.View) error {
return err
}
index := gui.State.Panels.Commits.SelectedLine
index := gui.State.Panels.Commits.SelectedLineIdx
selectedCommit := gui.State.Commits[index]
if selectedCommit.Status == "rebasing" {
if gui.State.Commits[index+1].Status != "rebasing" {
@@ -298,14 +300,14 @@ func (gui *Gui) handleCommitMoveDown(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.MoveTodoDown(index); err != nil {
return gui.surfaceError(err)
}
gui.State.Panels.Commits.SelectedLine++
gui.State.Panels.Commits.SelectedLineIdx++
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI, scope: []int{COMMITS, BRANCHES}})
}
return gui.WithWaitingStatus(gui.Tr.SLocalize("MovingStatus"), func() error {
err := gui.GitCommand.MoveCommitDown(gui.State.Commits, index)
if err == nil {
gui.State.Panels.Commits.SelectedLine++
gui.State.Panels.Commits.SelectedLineIdx++
}
return gui.handleGenericMergeCommandResult(err)
})
@@ -316,7 +318,7 @@ func (gui *Gui) handleCommitMoveUp(g *gocui.Gui, v *gocui.View) error {
return err
}
index := gui.State.Panels.Commits.SelectedLine
index := gui.State.Panels.Commits.SelectedLineIdx
if index == 0 {
return nil
}
@@ -325,14 +327,14 @@ func (gui *Gui) handleCommitMoveUp(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.MoveTodoDown(index - 1); err != nil {
return gui.surfaceError(err)
}
gui.State.Panels.Commits.SelectedLine--
gui.State.Panels.Commits.SelectedLineIdx--
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI, scope: []int{COMMITS, BRANCHES}})
}
return gui.WithWaitingStatus(gui.Tr.SLocalize("MovingStatus"), func() error {
err := gui.GitCommand.MoveCommitDown(gui.State.Commits, index-1)
if err == nil {
gui.State.Panels.Commits.SelectedLine--
gui.State.Panels.Commits.SelectedLineIdx--
}
return gui.handleGenericMergeCommandResult(err)
})
@@ -352,7 +354,7 @@ func (gui *Gui) handleCommitEdit(g *gocui.Gui, v *gocui.View) error {
}
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
err = gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "edit")
err = gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "edit")
return gui.handleGenericMergeCommandResult(err)
})
}
@@ -362,12 +364,16 @@ func (gui *Gui) handleCommitAmendTo(g *gocui.Gui, v *gocui.View) error {
return err
}
return gui.createConfirmationPanel(gui.g, v, true, gui.Tr.SLocalize("AmendCommitTitle"), gui.Tr.SLocalize("AmendCommitPrompt"), func(*gocui.Gui, *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("AmendingStatus"), func() error {
err := gui.GitCommand.AmendTo(gui.State.Commits[gui.State.Panels.Commits.SelectedLine].Sha)
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
return gui.ask(askOpts{
title: gui.Tr.SLocalize("AmendCommitTitle"),
prompt: gui.Tr.SLocalize("AmendCommitPrompt"),
handleConfirm: func() error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("AmendingStatus"), func() error {
err := gui.GitCommand.AmendTo(gui.State.Commits[gui.State.Panels.Commits.SelectedLineIdx].Sha)
return gui.handleGenericMergeCommandResult(err)
})
},
})
}
func (gui *Gui) handleCommitPick(g *gocui.Gui, v *gocui.View) error {
@@ -393,101 +399,20 @@ func (gui *Gui) handleCommitRevert(g *gocui.Gui, v *gocui.View) error {
return err
}
if err := gui.GitCommand.Revert(gui.State.Commits[gui.State.Panels.Commits.SelectedLine].Sha); err != nil {
if err := gui.GitCommand.Revert(gui.State.Commits[gui.State.Panels.Commits.SelectedLineIdx].Sha); err != nil {
return gui.surfaceError(err)
}
gui.State.Panels.Commits.SelectedLine++
gui.State.Panels.Commits.SelectedLineIdx++
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI, scope: []int{COMMITS, BRANCHES}})
}
func (gui *Gui) handleCopyCommit(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
func (gui *Gui) handleViewCommitFiles() error {
commit := gui.getSelectedLocalCommit()
if commit == nil {
return nil
}
// get currently selected commit, add the sha to state.
commit := gui.State.Commits[gui.State.Panels.Commits.SelectedLine]
// we will un-copy it if it's already copied
for index, cherryPickedCommit := range gui.State.CherryPickedCommits {
if commit.Sha == cherryPickedCommit.Sha {
gui.State.CherryPickedCommits = append(gui.State.CherryPickedCommits[0:index], gui.State.CherryPickedCommits[index+1:]...)
return gui.renderBranchCommitsWithSelection()
}
}
gui.addCommitToCherryPickedCommits(gui.State.Panels.Commits.SelectedLine)
return gui.renderBranchCommitsWithSelection()
}
func (gui *Gui) cherryPickedCommitShaMap() map[string]bool {
commitShaMap := map[string]bool{}
for _, commit := range gui.State.CherryPickedCommits {
commitShaMap[commit.Sha] = true
}
return commitShaMap
}
func (gui *Gui) addCommitToCherryPickedCommits(index int) {
commitShaMap := gui.cherryPickedCommitShaMap()
commitShaMap[gui.State.Commits[index].Sha] = true
newCommits := []*commands.Commit{}
for _, commit := range gui.State.Commits {
if commitShaMap[commit.Sha] {
// duplicating just the things we need to put in the rebase TODO list
newCommits = append(newCommits, &commands.Commit{Name: commit.Name, Sha: commit.Sha})
}
}
gui.State.CherryPickedCommits = newCommits
}
func (gui *Gui) handleCopyCommitRange(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
commitShaMap := gui.cherryPickedCommitShaMap()
// find the last commit that is copied that's above our position
// if there are none, startIndex = 0
startIndex := 0
for index, commit := range gui.State.Commits[0:gui.State.Panels.Commits.SelectedLine] {
if commitShaMap[commit.Sha] {
startIndex = index
}
}
gui.Log.Info("commit copy start index: " + strconv.Itoa(startIndex))
for index := startIndex; index <= gui.State.Panels.Commits.SelectedLine; index++ {
gui.addCommitToCherryPickedCommits(index)
}
return gui.renderBranchCommitsWithSelection()
}
// HandlePasteCommits begins a cherry-pick rebase with the commits the user has copied
func (gui *Gui) HandlePasteCommits(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("CherryPick"), gui.Tr.SLocalize("SureCherryPick"), func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("CherryPickingStatus"), func() error {
err := gui.GitCommand.CherryPickCommits(gui.State.CherryPickedCommits)
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
}
func (gui *Gui) handleSwitchToCommitFilesPanel(g *gocui.Gui, v *gocui.View) error {
if err := gui.refreshCommitFilesView(); err != nil {
return err
}
return gui.switchFocus(g, gui.getCommitsView(), gui.getCommitFilesView())
return gui.switchToCommitFilesContext(commit.Sha, true, gui.Contexts.BranchCommits.Context, "commits")
}
func (gui *Gui) hasCommit(commits []*commands.Commit, target string) (int, bool) {
@@ -508,23 +433,27 @@ func (gui *Gui) handleCreateFixupCommit(g *gocui.Gui, v *gocui.View) error {
return err
}
commit := gui.getSelectedCommit()
commit := gui.getSelectedLocalCommit()
if commit == nil {
return nil
}
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("CreateFixupCommit"), gui.Tr.TemplateLocalize(
"SureCreateFixupCommit",
Teml{
"commit": commit.Sha,
},
), func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.CreateFixupCommit(commit.Sha); err != nil {
return gui.surfaceError(err)
}
return gui.ask(askOpts{
title: gui.Tr.SLocalize("CreateFixupCommit"),
prompt: gui.Tr.TemplateLocalize(
"SureCreateFixupCommit",
Teml{
"commit": commit.Sha,
},
),
handleConfirm: func() error {
if err := gui.GitCommand.CreateFixupCommit(commit.Sha); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}, nil)
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
},
})
}
func (gui *Gui) handleSquashAllAboveFixupCommits(g *gocui.Gui, v *gocui.View) error {
@@ -532,29 +461,33 @@ func (gui *Gui) handleSquashAllAboveFixupCommits(g *gocui.Gui, v *gocui.View) er
return err
}
commit := gui.getSelectedCommit()
commit := gui.getSelectedLocalCommit()
if commit == nil {
return nil
}
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("SquashAboveCommits"), gui.Tr.TemplateLocalize(
"SureSquashAboveCommits",
Teml{
"commit": commit.Sha,
return gui.ask(askOpts{
title: gui.Tr.SLocalize("SquashAboveCommits"),
prompt: gui.Tr.TemplateLocalize(
"SureSquashAboveCommits",
Teml{
"commit": commit.Sha,
},
),
handleConfirm: func() error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("SquashingStatus"), func() error {
err := gui.GitCommand.SquashAllAboveFixupCommits(commit.Sha)
return gui.handleGenericMergeCommandResult(err)
})
},
), func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("SquashingStatus"), func() error {
err := gui.GitCommand.SquashAllAboveFixupCommits(commit.Sha)
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
})
}
func (gui *Gui) handleTagCommit(g *gocui.Gui, v *gocui.View) error {
// TODO: bring up menu asking if you want to make a lightweight or annotated tag
// if annotated, switch to a subprocess to create the message
commit := gui.getSelectedCommit()
commit := gui.getSelectedLocalCommit()
if commit == nil {
return nil
}
@@ -563,8 +496,8 @@ func (gui *Gui) handleTagCommit(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleCreateLightweightTag(commitSha string) error {
return gui.createPromptPanel(gui.g, gui.getCommitsView(), gui.Tr.SLocalize("TagNameTitle"), "", func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.CreateLightweightTag(v.Buffer(), commitSha); err != nil {
return gui.prompt(gui.Tr.SLocalize("TagNameTitle"), "", func(response string) error {
if err := gui.GitCommand.CreateLightweightTag(response, commitSha); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{COMMITS, TAGS}})
@@ -572,83 +505,22 @@ func (gui *Gui) handleCreateLightweightTag(commitSha string) error {
}
func (gui *Gui) handleCheckoutCommit(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedCommit()
commit := gui.getSelectedLocalCommit()
if commit == nil {
return nil
}
return gui.createConfirmationPanel(g, gui.getCommitsView(), true, gui.Tr.SLocalize("checkoutCommit"), gui.Tr.SLocalize("SureCheckoutThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{})
}, nil)
}
func (gui *Gui) renderBranchCommitsWithSelection() error {
commitsView := gui.getCommitsView()
gui.refreshSelectedLine(&gui.State.Panels.Commits.SelectedLine, len(gui.State.Commits))
displayStrings := presentation.GetCommitListDisplayStrings(gui.State.Commits, gui.State.ScreenMode != SCREEN_NORMAL, gui.cherryPickedCommitShaMap(), gui.State.Diff.Ref)
gui.renderDisplayStrings(commitsView, displayStrings)
if gui.g.CurrentView() == commitsView && commitsView.Context == "branch-commits" {
if err := gui.handleCommitSelect(gui.g, commitsView); err != nil {
return err
}
}
return nil
}
func (gui *Gui) onCommitsTabClick(tabIndex int) error {
contexts := []string{"branch-commits", "reflog-commits"}
commitsView := gui.getCommitsView()
commitsView.TabIndex = tabIndex
return gui.switchCommitsPanelContext(contexts[tabIndex])
}
func (gui *Gui) switchCommitsPanelContext(context string) error {
commitsView := gui.getCommitsView()
commitsView.Context = context
if err := gui.onSearchEscape(); err != nil {
return err
}
contextTabIndexMap := map[string]int{
"branch-commits": 0,
"reflog-commits": 1,
}
commitsView.TabIndex = contextTabIndexMap[context]
return gui.refreshCommitsViewWithSelection()
}
func (gui *Gui) refreshCommitsViewWithSelection() error {
commitsView := gui.getCommitsView()
switch commitsView.Context {
case "branch-commits":
return gui.renderBranchCommitsWithSelection()
case "reflog-commits":
return gui.renderReflogCommitsWithSelection()
}
return nil
}
func (gui *Gui) handleNextCommitsTab(g *gocui.Gui, v *gocui.View) error {
return gui.onCommitsTabClick(
utils.ModuloWithWrap(v.TabIndex+1, len(v.Tabs)),
)
}
func (gui *Gui) handlePrevCommitsTab(g *gocui.Gui, v *gocui.View) error {
return gui.onCommitsTabClick(
utils.ModuloWithWrap(v.TabIndex-1, len(v.Tabs)),
)
return gui.ask(askOpts{
title: gui.Tr.SLocalize("checkoutCommit"),
prompt: gui.Tr.SLocalize("SureCheckoutThisCommit"),
handleConfirm: func() error {
return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{})
},
})
}
func (gui *Gui) handleCreateCommitResetMenu(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedCommit()
commit := gui.getSelectedLocalCommit()
if commit == nil {
return gui.createErrorPanel(gui.Tr.SLocalize("NoCommitsThisBranch"))
}
@@ -656,19 +528,6 @@ func (gui *Gui) handleCreateCommitResetMenu(g *gocui.Gui, v *gocui.View) error {
return gui.createResetMenu(commit.Sha)
}
func (gui *Gui) onCommitsPanelSearchSelect(selectedLine int) error {
commitsView := gui.getCommitsView()
switch commitsView.Context {
case "branch-commits":
gui.State.Panels.Commits.SelectedLine = selectedLine
return gui.handleCommitSelect(gui.g, commitsView)
case "reflog-commits":
gui.State.Panels.ReflogCommits.SelectedLine = selectedLine
return gui.handleReflogCommitSelect(gui.g, commitsView)
}
return nil
}
func (gui *Gui) handleOpenSearchForCommitsPanel(g *gocui.Gui, v *gocui.View) error {
// we usually lazyload these commits but now that we're searching we need to load them now
if gui.State.Panels.Commits.LimitCommits {
@@ -681,11 +540,6 @@ func (gui *Gui) handleOpenSearchForCommitsPanel(g *gocui.Gui, v *gocui.View) err
return gui.handleOpenSearch(gui.g, v)
}
func (gui *Gui) handleResetCherryPick(g *gocui.Gui, v *gocui.View) error {
gui.State.CherryPickedCommits = []*commands.Commit{}
return gui.renderBranchCommitsWithSelection()
}
func (gui *Gui) handleGotoBottomForCommitsPanel(g *gocui.Gui, v *gocui.View) error {
// we usually lazyload these commits but now that we're searching we need to load them now
if gui.State.Panels.Commits.LimitCommits {
@@ -695,20 +549,11 @@ func (gui *Gui) handleGotoBottomForCommitsPanel(g *gocui.Gui, v *gocui.View) err
}
}
for _, view := range gui.getListViews() {
if view.viewName == "commits" {
return view.handleGotoBottom(g, v)
for _, context := range gui.getListContexts() {
if context.ViewName == "commits" {
return context.handleGotoBottom(g, v)
}
}
return nil
}
func (gui *Gui) handleClipboardCopyCommit(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedCommit()
if commit == nil {
return nil
}
return gui.OSCommand.CopyToClipboard(commit.Sha)
}

View File

@@ -15,33 +15,107 @@ import (
"github.com/jesseduffield/lazygit/pkg/theme"
)
func (gui *Gui) wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.View) error, returnFocusOnClose bool) func(*gocui.Gui, *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
type createPopupPanelOpts struct {
hasLoader bool
editable bool
title string
prompt string
handleConfirm func() error
handleConfirmPrompt func(string) error
handleClose func() error
// when handlersManageFocus is true, do not return from the confirmation context automatically. It's expected that the handlers will manage focus, whether that means switching to another context, or manually returning the context.
handlersManageFocus bool
}
type askOpts struct {
title string
prompt string
handleConfirm func() error
handleClose func() error
handlersManageFocus bool
}
func (gui *Gui) createLoaderPanel(currentView *gocui.View, prompt string) error {
return gui.createPopupPanel(createPopupPanelOpts{
prompt: prompt,
hasLoader: true,
})
}
func (gui *Gui) ask(opts askOpts) error {
return gui.createPopupPanel(createPopupPanelOpts{
title: opts.title,
prompt: opts.prompt,
handleConfirm: opts.handleConfirm,
handleClose: opts.handleClose,
handlersManageFocus: opts.handlersManageFocus,
})
}
func (gui *Gui) prompt(title string, initialContent string, handleConfirm func(string) error) error {
return gui.createPopupPanel(createPopupPanelOpts{
title: title,
prompt: initialContent,
editable: true,
handleConfirmPrompt: handleConfirm,
})
}
func (gui *Gui) wrappedConfirmationFunction(handlersManageFocus bool, function func() error) func(*gocui.Gui, *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
if function != nil {
if err := function(g, v); err != nil {
if err := function(); err != nil {
return err
}
}
return gui.closeConfirmationPrompt(g, returnFocusOnClose)
if err := gui.closeConfirmationPrompt(handlersManageFocus); err != nil {
return err
}
return nil
}
}
func (gui *Gui) closeConfirmationPrompt(g *gocui.Gui, returnFocusOnClose bool) error {
view, err := g.View("confirmation")
if err != nil {
func (gui *Gui) wrappedPromptConfirmationFunction(handlersManageFocus bool, function func(string) error) func(*gocui.Gui, *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
if function != nil {
if err := function(v.Buffer()); err != nil {
return gui.surfaceError(err)
}
}
if err := gui.closeConfirmationPrompt(handlersManageFocus); err != nil {
return err
}
return nil
}
}
func (gui *Gui) deleteConfirmationView() {
gui.g.DeleteKeybinding("confirmation", gui.getKey("universal.confirm"), gocui.ModNone)
gui.g.DeleteKeybinding("confirmation", gui.getKey("universal.confirm-alt1"), gocui.ModNone)
gui.g.DeleteKeybinding("confirmation", gui.getKey("universal.return"), gocui.ModNone)
_ = gui.g.DeleteView("confirmation")
}
func (gui *Gui) closeConfirmationPrompt(handlersManageFocus bool) error {
view := gui.getConfirmationView()
if view == nil {
return nil // if it's already been closed we can just return
}
view.Editable = false
if returnFocusOnClose {
if err := gui.returnFocus(g, view); err != nil {
panic(err)
if !handlersManageFocus {
if err := gui.returnFromContext(); err != nil {
return err
}
}
g.DeleteKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone)
g.DeleteKeybinding("confirmation", gocui.KeyEsc, gocui.ModNone)
return g.DeleteView("confirmation")
gui.deleteConfirmationView()
return nil
}
func (gui *Gui) getMessageHeight(wrap bool, message string, width int) int {
@@ -58,9 +132,18 @@ func (gui *Gui) getMessageHeight(wrap bool, message string, width int) int {
return lineCount
}
func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, wrap bool, prompt string) (int, int, int, int) {
width, height := g.Size()
func (gui *Gui) getConfirmationPanelDimensions(wrap bool, prompt string) (int, int, int, int) {
width, height := gui.g.Size()
// we want a minimum width up to a point, then we do it based on ratio.
panelWidth := 4 * width / 7
minWidth := 80
if panelWidth < minWidth {
if width-2 < minWidth {
panelWidth = width - 2
} else {
panelWidth = minWidth
}
}
panelHeight := gui.getMessageHeight(wrap, prompt, panelWidth)
if panelHeight > height*3/4 {
panelHeight = height * 3 / 4
@@ -71,8 +154,8 @@ func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, wrap bool, prompt s
height/2 + panelHeight/2
}
func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt string, hasLoader bool) (*gocui.View, error) {
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, true, prompt)
func (gui *Gui) prepareConfirmationPanel(title, prompt string, hasLoader bool) (*gocui.View, error) {
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(true, prompt)
confirmationView, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0)
if err != nil {
if err.Error() != "unknown view" {
@@ -87,35 +170,23 @@ func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt
confirmationView.FgColor = theme.GocuiDefaultTextColor
}
gui.g.Update(func(g *gocui.Gui) error {
return gui.switchFocus(gui.g, currentView, confirmationView)
return gui.switchContext(gui.Contexts.Confirmation.Context)
})
return confirmationView, nil
}
func (gui *Gui) onNewPopupPanel() {
viewNames := []string{"commitMessage",
"credentials",
"menu"}
for _, viewName := range viewNames {
_, _ = gui.g.SetViewOnBottom(viewName)
}
}
func (gui *Gui) createPopupPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, hasLoader bool, returnFocusOnClose bool, editable bool, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
gui.onNewPopupPanel()
g.Update(func(g *gocui.Gui) error {
func (gui *Gui) createPopupPanel(opts createPopupPanelOpts) error {
gui.g.Update(func(g *gocui.Gui) error {
// delete the existing confirmation panel if it exists
if view, _ := g.View("confirmation"); view != nil {
if err := gui.closeConfirmationPrompt(g, true); err != nil {
gui.Log.Error(err)
}
gui.deleteConfirmationView()
}
confirmationView, err := gui.prepareConfirmationPanel(currentView, title, prompt, hasLoader)
confirmationView, err := gui.prepareConfirmationPanel(opts.title, opts.prompt, opts.hasLoader)
if err != nil {
return err
}
confirmationView.Editable = editable
if editable {
confirmationView.Editable = opts.editable
if opts.editable {
go func() {
// TODO: remove this wait (right now if you remove it the EditGotoToEndOfLine method doesn't seem to work)
time.Sleep(time.Millisecond)
@@ -126,26 +197,13 @@ func (gui *Gui) createPopupPanel(g *gocui.Gui, currentView *gocui.View, title, p
}()
}
gui.renderString(g, "confirmation", prompt)
return gui.setKeyBindings(g, handleConfirm, handleClose, returnFocusOnClose)
gui.renderString("confirmation", opts.prompt)
return gui.setKeyBindings(opts)
})
return nil
}
func (gui *Gui) createLoaderPanel(g *gocui.Gui, currentView *gocui.View, prompt string) error {
return gui.createPopupPanel(g, currentView, "", prompt, true, true, false, nil, nil)
}
// it is very important that within this function we never include the original prompt in any error messages, because it may contain e.g. a user password
func (gui *Gui) createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, returnFocusOnClose bool, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
return gui.createPopupPanel(g, currentView, title, prompt, false, returnFocusOnClose, false, handleConfirm, handleClose)
}
func (gui *Gui) createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, initialContent string, handleConfirm func(*gocui.Gui, *gocui.View) error) error {
return gui.createPopupPanel(gui.g, currentView, title, initialContent, false, true, true, handleConfirm, nil)
}
func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error, returnFocusOnClose bool) error {
func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
actions := gui.Tr.TemplateLocalize(
"CloseConfirm",
Teml{
@@ -154,42 +212,43 @@ func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*go
},
)
gui.renderString(g, "options", actions)
if err := g.SetKeybinding("confirmation", nil, gocui.KeyEnter, gocui.ModNone, gui.wrappedConfirmationFunction(handleConfirm, returnFocusOnClose)); err != nil {
gui.renderString("options", actions)
var onConfirm func(*gocui.Gui, *gocui.View) error
if opts.handleConfirmPrompt != nil {
onConfirm = gui.wrappedPromptConfirmationFunction(opts.handlersManageFocus, opts.handleConfirmPrompt)
} else {
onConfirm = gui.wrappedConfirmationFunction(opts.handlersManageFocus, opts.handleConfirm)
}
if err := gui.g.SetKeybinding("confirmation", nil, gui.getKey("universal.confirm"), gocui.ModNone, onConfirm); err != nil {
return err
}
return g.SetKeybinding("confirmation", nil, gocui.KeyEsc, gocui.ModNone, gui.wrappedConfirmationFunction(handleClose, returnFocusOnClose))
}
// createSpecificErrorPanel allows you to create an error popup, specifying the
// view to be focused when the user closes the popup, and a boolean specifying
// whether we will log the error. If the message may include a user password,
// this function is to be used over the more generic createErrorPanel, with
// willLog set to false
func (gui *Gui) createSpecificErrorPanel(message string, nextView *gocui.View, willLog bool) error {
if willLog {
go func() {
// when reporting is switched on this log call sometimes introduces
// a delay on the error panel popping up. Here I'm adding a second wait
// so that the error is logged while the user is reading the error message
time.Sleep(time.Second)
gui.Log.Error(message)
}()
if err := gui.g.SetKeybinding("confirmation", nil, gui.getKey("universal.confirm-alt1"), gocui.ModNone, onConfirm); err != nil {
return err
}
return gui.g.SetKeybinding("confirmation", nil, gui.getKey("universal.return"), gocui.ModNone, gui.wrappedConfirmationFunction(opts.handlersManageFocus, opts.handleClose))
}
func (gui *Gui) createErrorPanel(message string) error {
colorFunction := color.New(color.FgRed).SprintFunc()
coloredMessage := colorFunction(strings.TrimSpace(message))
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil {
return err
}
return gui.createConfirmationPanel(gui.g, nextView, true, gui.Tr.SLocalize("Error"), coloredMessage, nil, nil)
}
func (gui *Gui) createErrorPanel(message string) error {
return gui.createSpecificErrorPanel(message, gui.g.CurrentView(), true)
return gui.ask(askOpts{
title: gui.Tr.SLocalize("Error"),
prompt: coloredMessage,
})
}
func (gui *Gui) surfaceError(err error) error {
for _, sentinelError := range gui.sentinelErrorsArr() {
if err == sentinelError {
return err
}
}
return gui.createErrorPanel(err.Error())
}

View File

@@ -1,19 +1,722 @@
package gui
import (
"fmt"
"github.com/jesseduffield/gocui"
)
const (
SIDE_CONTEXT int = iota
MAIN_CONTEXT
TEMPORARY_POPUP
PERSISTENT_POPUP
)
const (
STATUS_CONTEXT_KEY = "status"
FILES_CONTEXT_KEY = "files"
LOCAL_BRANCHES_CONTEXT_KEY = "localBranches"
REMOTES_CONTEXT_KEY = "remotes"
REMOTE_BRANCHES_CONTEXT_KEY = "remoteBranches"
TAGS_CONTEXT_KEY = "tags"
BRANCH_COMMITS_CONTEXT_KEY = "commits"
REFLOG_COMMITS_CONTEXT_KEY = "reflogCommits"
SUB_COMMITS_CONTEXT_KEY = "subCommits"
COMMIT_FILES_CONTEXT_KEY = "commitFiles"
STASH_CONTEXT_KEY = "stash"
MAIN_NORMAL_CONTEXT_KEY = "normal"
MAIN_MERGING_CONTEXT_KEY = "merging"
MAIN_PATCH_BUILDING_CONTEXT_KEY = "patchBuilding"
MAIN_STAGING_CONTEXT_KEY = "staging"
MENU_CONTEXT_KEY = "menu"
CREDENTIALS_CONTEXT_KEY = "credentials"
CONFIRMATION_CONTEXT_KEY = "confirmation"
SEARCH_CONTEXT_KEY = "confirmation"
COMMIT_MESSAGE_CONTEXT_KEY = "commitMessage"
)
type Context interface {
HandleFocus() error
HandleFocusLost() error
HandleRender() error
GetKind() int
GetViewName() string
GetWindowName() string
SetWindowName(string)
GetKey() string
SetParentContext(Context)
// we return a bool here to tell us whether or not the returned value just wraps a nil
GetParentContext() (Context, bool)
GetOptionsMap() map[string]string
}
type BasicContext struct {
OnFocus func() error
OnFocusLost func() error
OnRender func() error
OnGetOptionsMap func() map[string]string
Kind int
Key string
ViewName string
}
func (c BasicContext) GetOptionsMap() map[string]string {
if c.OnGetOptionsMap != nil {
return c.OnGetOptionsMap()
}
return nil
}
func (c BasicContext) SetWindowName(windowName string) {
panic("can't set window name on basic context")
}
func (c BasicContext) GetWindowName() string {
// TODO: fix this up
return c.GetViewName()
}
func (c BasicContext) SetParentContext(Context) {
panic("can't set parent context on basic context")
}
func (c BasicContext) GetParentContext() (Context, bool) {
return nil, false
}
func (c BasicContext) HandleRender() error {
if c.OnRender != nil {
return c.OnRender()
}
return nil
}
func (c BasicContext) GetViewName() string {
return c.ViewName
}
func (c BasicContext) HandleFocus() error {
return c.OnFocus()
}
func (c BasicContext) HandleFocusLost() error {
if c.OnFocusLost != nil {
return c.OnFocusLost()
}
return nil
}
func (c BasicContext) GetKind() int {
return c.Kind
}
func (c BasicContext) GetKey() string {
return c.Key
}
type SimpleContextNode struct {
Context Context
}
type RemotesContextNode struct {
Context Context
Branches SimpleContextNode
}
type ContextTree struct {
Status SimpleContextNode
Files SimpleContextNode
Menu SimpleContextNode
Branches SimpleContextNode
Remotes RemotesContextNode
Tags SimpleContextNode
BranchCommits SimpleContextNode
CommitFiles SimpleContextNode
ReflogCommits SimpleContextNode
SubCommits SimpleContextNode
Stash SimpleContextNode
Normal SimpleContextNode
Staging SimpleContextNode
PatchBuilding SimpleContextNode
Merging SimpleContextNode
Credentials SimpleContextNode
Confirmation SimpleContextNode
CommitMessage SimpleContextNode
Search SimpleContextNode
}
func (gui *Gui) allContexts() []Context {
return []Context{
gui.Contexts.Status.Context,
gui.Contexts.Files.Context,
gui.Contexts.Branches.Context,
gui.Contexts.Remotes.Context,
gui.Contexts.Remotes.Branches.Context,
gui.Contexts.Tags.Context,
gui.Contexts.BranchCommits.Context,
gui.Contexts.CommitFiles.Context,
gui.Contexts.ReflogCommits.Context,
gui.Contexts.Stash.Context,
gui.Contexts.Menu.Context,
gui.Contexts.Confirmation.Context,
gui.Contexts.Credentials.Context,
gui.Contexts.CommitMessage.Context,
gui.Contexts.Normal.Context,
gui.Contexts.Staging.Context,
gui.Contexts.Merging.Context,
gui.Contexts.PatchBuilding.Context,
gui.Contexts.SubCommits.Context,
}
}
func (gui *Gui) contextTree() ContextTree {
return ContextTree{
Status: SimpleContextNode{
Context: BasicContext{
OnFocus: gui.handleStatusSelect,
Kind: SIDE_CONTEXT,
ViewName: "status",
Key: STATUS_CONTEXT_KEY,
},
},
Files: SimpleContextNode{
Context: gui.filesListContext(),
},
Menu: SimpleContextNode{
Context: gui.menuListContext(),
},
Remotes: RemotesContextNode{
Context: gui.remotesListContext(),
Branches: SimpleContextNode{
Context: gui.remoteBranchesListContext(),
},
},
BranchCommits: SimpleContextNode{
Context: gui.branchCommitsListContext(),
},
CommitFiles: SimpleContextNode{
Context: gui.commitFilesListContext(),
},
ReflogCommits: SimpleContextNode{
Context: gui.reflogCommitsListContext(),
},
SubCommits: SimpleContextNode{
Context: gui.subCommitsListContext(),
},
Branches: SimpleContextNode{
Context: gui.branchesListContext(),
},
Tags: SimpleContextNode{
Context: gui.tagsListContext(),
},
Stash: SimpleContextNode{
Context: gui.stashListContext(),
},
Normal: SimpleContextNode{
Context: BasicContext{
OnFocus: func() error {
return nil // TODO: should we do something here? We should allow for scrolling the panel
},
Kind: MAIN_CONTEXT,
ViewName: "main",
Key: MAIN_NORMAL_CONTEXT_KEY,
},
},
Staging: SimpleContextNode{
Context: BasicContext{
OnFocus: func() error {
return nil
// TODO: centralise the code here
// return gui.refreshStagingPanel(false, -1)
},
Kind: MAIN_CONTEXT,
ViewName: "main",
Key: MAIN_STAGING_CONTEXT_KEY,
},
},
PatchBuilding: SimpleContextNode{
Context: BasicContext{
OnFocus: func() error {
return nil
// TODO: centralise the code here
// return gui.refreshPatchBuildingPanel(-1)
},
Kind: MAIN_CONTEXT,
ViewName: "main",
Key: MAIN_PATCH_BUILDING_CONTEXT_KEY,
},
},
Merging: SimpleContextNode{
Context: BasicContext{
OnFocus: func() error {
return gui.refreshMergePanel()
},
Kind: MAIN_CONTEXT,
ViewName: "main",
Key: MAIN_MERGING_CONTEXT_KEY,
OnGetOptionsMap: gui.getMergingOptions,
},
},
Credentials: SimpleContextNode{
Context: BasicContext{
OnFocus: func() error { return gui.handleCredentialsViewFocused() },
Kind: PERSISTENT_POPUP,
ViewName: "credentials",
Key: CREDENTIALS_CONTEXT_KEY,
},
},
Confirmation: SimpleContextNode{
Context: BasicContext{
OnFocus: func() error { return nil },
Kind: TEMPORARY_POPUP,
ViewName: "confirmation",
Key: CONFIRMATION_CONTEXT_KEY,
},
},
CommitMessage: SimpleContextNode{
Context: BasicContext{
OnFocus: func() error { return gui.handleCommitMessageFocused() },
Kind: PERSISTENT_POPUP,
ViewName: "commitMessage",
Key: COMMIT_MESSAGE_CONTEXT_KEY,
},
},
Search: SimpleContextNode{
Context: BasicContext{
OnFocus: func() error { return nil },
Kind: PERSISTENT_POPUP,
ViewName: "search",
Key: SEARCH_CONTEXT_KEY,
},
},
}
}
func (gui *Gui) initialViewContextMap() map[string]Context {
return map[string]Context{
"status": gui.Contexts.Status.Context,
"files": gui.Contexts.Files.Context,
"branches": gui.Contexts.Branches.Context,
"commits": gui.Contexts.BranchCommits.Context,
"commitFiles": gui.Contexts.CommitFiles.Context,
"stash": gui.Contexts.Stash.Context,
"menu": gui.Contexts.Menu.Context,
"confirmation": gui.Contexts.Confirmation.Context,
"credentials": gui.Contexts.Credentials.Context,
"commitMessage": gui.Contexts.CommitMessage.Context,
"main": gui.Contexts.Normal.Context,
"secondary": gui.Contexts.Normal.Context,
}
}
func (gui *Gui) viewTabContextMap() map[string][]tabContext {
return map[string][]tabContext{
"branches": {
{
tab: "Local Branches",
contexts: []Context{gui.Contexts.Branches.Context},
},
{
tab: "Remotes",
contexts: []Context{
gui.Contexts.Remotes.Context,
gui.Contexts.Remotes.Branches.Context,
},
},
{
tab: "Tags",
contexts: []Context{gui.Contexts.Tags.Context},
},
},
"commits": {
{
tab: "Commits",
contexts: []Context{gui.Contexts.BranchCommits.Context},
},
{
tab: "Reflog",
contexts: []Context{
gui.Contexts.ReflogCommits.Context,
},
},
},
}
}
func (gui *Gui) currentContextKeyIgnoringPopups() string {
stack := gui.State.ContextStack
for i := range stack {
reversedIndex := len(stack) - 1 - i
context := stack[reversedIndex]
kind := stack[reversedIndex].GetKind()
if kind != TEMPORARY_POPUP && kind != PERSISTENT_POPUP {
return context.GetKey()
}
}
return ""
}
func (gui *Gui) switchContext(c Context) error {
gui.g.Update(func(*gocui.Gui) error {
// push onto stack
// if we are switching to a side context, remove all other contexts in the stack
if c.GetKind() == SIDE_CONTEXT {
for _, stackContext := range gui.State.ContextStack {
if stackContext.GetKey() != c.GetKey() {
if err := gui.deactivateContext(stackContext); err != nil {
return err
}
}
}
gui.State.ContextStack = []Context{c}
} else {
// TODO: think about other exceptional cases
gui.State.ContextStack = append(gui.State.ContextStack, c)
}
return gui.activateContext(c)
})
return nil
}
// switchContextToView is to be used when you don't know which context you
// want to switch to: you only know the view that you want to switch to. It will
// look up the context currently active for that view and switch to that context
func (gui *Gui) switchContextToView(viewName string) error {
return gui.switchContext(gui.State.ViewContextMap[viewName])
}
func (gui *Gui) returnFromContext() error {
gui.g.Update(func(*gocui.Gui) error {
// TODO: add mutexes
if len(gui.State.ContextStack) == 1 {
// cannot escape from bottommost context
return nil
}
n := len(gui.State.ContextStack) - 1
currentContext := gui.State.ContextStack[n]
newContext := gui.State.ContextStack[n-1]
gui.State.ContextStack = gui.State.ContextStack[:n]
if err := gui.deactivateContext(currentContext); err != nil {
return err
}
return gui.activateContext(newContext)
})
return nil
}
func (gui *Gui) deactivateContext(c Context) error {
// if we are the kind of context that is sent to back upon deactivation, we should do that
if c.GetKind() == TEMPORARY_POPUP || c.GetKind() == PERSISTENT_POPUP {
_, _ = gui.g.SetViewOnBottom(c.GetViewName())
}
if err := c.HandleFocusLost(); err != nil {
return err
}
return nil
}
// postRefreshUpdate is to be called on a context after the state that it depends on has been refreshed
// if the context's view is set to another context we do nothing.
// if the context's view is the current view we trigger a focus; re-selecting the current item.
func (gui *Gui) postRefreshUpdate(c Context) error {
v, err := gui.g.View(c.GetViewName())
if err != nil {
return nil
}
if v.Context != c.GetKey() {
return nil
}
if err := c.HandleRender(); err != nil {
return err
}
if gui.currentViewName() == c.GetViewName() {
if err := c.HandleFocus(); err != nil {
return err
}
}
return nil
}
func (gui *Gui) activateContext(c Context) error {
viewName := c.GetViewName()
v, err := gui.g.View(viewName)
// if view no longer exists, pop again
if err != nil {
return gui.returnFromContext()
}
originalViewContextKey := v.Context
// ensure that any other window for which this view was active is now set to the default for that window.
gui.setViewAsActiveForWindow(viewName)
if viewName == "main" {
gui.changeMainViewsContext(c.GetKey())
} else {
gui.changeMainViewsContext("normal")
}
gui.setViewTabForContext(c)
if _, err := gui.g.SetCurrentView(viewName); err != nil {
return err
}
if _, err := gui.g.SetViewOnTop(viewName); err != nil {
return err
}
// if the new context's view was previously displaying another context, render the new context
if originalViewContextKey != c.GetKey() {
if err := c.HandleRender(); err != nil {
return err
}
}
v.Context = c.GetKey()
gui.g.Cursor = v.Editable
// render the options available for the current context at the bottom of the screen
optionsMap := c.GetOptionsMap()
if optionsMap == nil {
optionsMap = gui.globalOptionsMap()
}
gui.renderOptionsMap(optionsMap)
if err := c.HandleFocus(); err != nil {
return err
}
// TODO: consider removing this and instead depending on the .Context field of views
gui.State.ViewContextMap[c.GetViewName()] = c
return nil
}
func (gui *Gui) renderContextStack() string {
result := ""
for _, context := range gui.State.ContextStack {
result += context.GetKey() + "\n"
}
return result
}
func (gui *Gui) currentContext() Context {
if len(gui.State.ContextStack) == 0 {
return gui.defaultSideContext()
}
return gui.State.ContextStack[len(gui.State.ContextStack)-1]
}
func (gui *Gui) currentSideContext() *ListContext {
stack := gui.State.ContextStack
// on startup the stack can be empty so we'll return an empty string in that case
if len(stack) == 0 {
return nil
}
// find the first context in the stack with the type of SIDE_CONTEXT
for i := range stack {
context := stack[len(stack)-1-i]
if context.GetKind() == SIDE_CONTEXT {
// not all side contexts are list contexts (e.g. the status panel)
listContext, ok := context.(*ListContext)
if !ok {
return nil
}
return listContext
}
}
return nil
}
func (gui *Gui) defaultSideContext() Context {
return gui.Contexts.Files.Context
}
func (gui *Gui) setInitialViewContexts() {
// arguably we should only have our ViewContextMap and we should do away with
// contexts on views, or vice versa
for viewName, context := range gui.State.ViewContextMap {
// see if the view exists. If it does, set the context on it
view, err := gui.g.View(viewName)
if err != nil {
continue
}
view.Context = context.GetKey()
}
}
// getFocusLayout returns a manager function for when view gain and lose focus
func (gui *Gui) getFocusLayout() func(g *gocui.Gui) error {
var previousView *gocui.View
return func(g *gocui.Gui) error {
newView := gui.g.CurrentView()
if err := gui.onViewFocusChange(); err != nil {
return err
}
// for now we don't consider losing focus to a popup panel as actually losing focus
if newView != previousView && !gui.isPopupPanel(newView.Name()) {
if err := gui.onViewFocusLost(previousView, newView); err != nil {
return err
}
previousView = newView
}
return nil
}
}
func (gui *Gui) onViewFocusChange() error {
currentView := gui.g.CurrentView()
for _, view := range gui.g.Views() {
view.Highlight = view.Name() != "main" && view == currentView
}
return nil
}
func (gui *Gui) onViewFocusLost(v *gocui.View, newView *gocui.View) error {
if v == nil {
return nil
}
if v.IsSearching() && newView.Name() != "search" {
if err := gui.onSearchEscape(); err != nil {
return err
}
}
gui.Log.Info(v.Name() + " focus lost")
return nil
}
// changeContext is a helper function for when we want to change a 'main' context
// which currently just means a context that affects both the main and secondary views
// other views can have their context changed directly but this function helps
// keep the main and secondary views in sync
func (gui *Gui) changeMainViewsContext(context string) {
if gui.State.MainContext == context {
func (gui *Gui) changeMainViewsContext(contextKey string) {
if gui.State.MainContext == contextKey {
return
}
switch context {
case "normal", "patch-building", "staging", "merging":
gui.getMainView().Context = context
gui.getSecondaryView().Context = context
switch contextKey {
case MAIN_NORMAL_CONTEXT_KEY, MAIN_PATCH_BUILDING_CONTEXT_KEY, MAIN_STAGING_CONTEXT_KEY, MAIN_MERGING_CONTEXT_KEY:
gui.getMainView().Context = contextKey
gui.getSecondaryView().Context = contextKey
default:
panic(fmt.Sprintf("unknown context for main: %s", contextKey))
}
gui.State.MainContext = context
gui.State.MainContext = contextKey
}
func (gui *Gui) viewTabNames(viewName string) []string {
tabContexts := gui.ViewTabContextMap[viewName]
result := make([]string, len(tabContexts))
for i, tabContext := range tabContexts {
result[i] = tabContext.tab
}
return result
}
func (gui *Gui) setViewTabForContext(c Context) {
// search for the context in our map and if we find it, set the tab for the corresponding view
tabContexts, ok := gui.ViewTabContextMap[c.GetViewName()]
if !ok {
return
}
for tabIndex, tabContext := range tabContexts {
for _, context := range tabContext.contexts {
if context.GetKey() == c.GetKey() {
// get the view, set the tab
v, err := gui.g.View(c.GetViewName())
if err != nil {
gui.Log.Error(err)
return
}
v.TabIndex = tabIndex
return
}
}
}
}
type tabContext struct {
tab string
contexts []Context
}
func (gui *Gui) contextForContextKey(contextKey string) Context {
for _, context := range gui.allContexts() {
if context.GetKey() == contextKey {
return context
}
}
panic(fmt.Sprintf("context now found for key %s", contextKey))
}
func (gui *Gui) rerenderView(viewName string) error {
v, err := gui.g.View(viewName)
if err != nil {
return nil
}
contextKey := v.Context
context := gui.contextForContextKey(contextKey)
return context.HandleRender()
}
func (gui *Gui) getCurrentSideView() *gocui.View {
currentSideContext := gui.currentSideContext()
if currentSideContext == nil {
return nil
}
view, _ := gui.g.View(currentSideContext.GetViewName())
return view
}
func (gui *Gui) getSideContextSelectedItemId() string {
currentSideContext := gui.currentSideContext()
if currentSideContext == nil {
return ""
}
item, ok := currentSideContext.GetSelectedItem()
if ok {
return item.ID()
}
return ""
}

View File

@@ -8,10 +8,10 @@ import (
type credentials chan string
// waitForPassUname wait for a username or password input from the credentials popup
func (gui *Gui) waitForPassUname(g *gocui.Gui, currentView *gocui.View, passOrUname string) string {
// promptUserForCredential wait for a username or password input from the credentials popup
func (gui *Gui) promptUserForCredential(passOrUname string) string {
gui.credentials = make(chan string)
g.Update(func(g *gocui.Gui) error {
gui.g.Update(func(g *gocui.Gui) error {
credentialsView, _ := g.View("credentials")
if passOrUname == "username" {
credentialsView.Title = gui.Tr.SLocalize("CredentialsUsername")
@@ -20,10 +20,11 @@ func (gui *Gui) waitForPassUname(g *gocui.Gui, currentView *gocui.View, passOrUn
credentialsView.Title = gui.Tr.SLocalize("CredentialsPassword")
credentialsView.Mask = '*'
}
err := gui.switchFocus(g, currentView, credentialsView)
if err != nil {
if err := gui.switchContext(gui.Contexts.Credentials.Context); err != nil {
return err
}
gui.RenderCommitLength()
return nil
})
@@ -36,60 +37,41 @@ func (gui *Gui) waitForPassUname(g *gocui.Gui, currentView *gocui.View, passOrUn
func (gui *Gui) handleSubmitCredential(g *gocui.Gui, v *gocui.View) error {
message := gui.trimmedContent(v)
gui.credentials <- message
v.Clear()
_ = v.SetCursor(0, 0)
_, _ = g.SetViewOnBottom("credentials")
nextView, err := gui.g.View("confirmation")
if err != nil {
nextView = gui.getFilesView()
}
err = gui.switchFocus(g, nil, nextView)
if err != nil {
gui.clearEditorView(v)
if err := gui.returnFromContext(); err != nil {
return err
}
return gui.refreshSidePanels(refreshOptions{})
}
func (gui *Gui) handleCloseCredentialsView(g *gocui.Gui, v *gocui.View) error {
_, err := g.SetViewOnBottom("credentials")
if err != nil {
return err
}
gui.credentials <- ""
return gui.switchFocus(g, nil, gui.getFilesView())
return gui.returnFromContext()
}
func (gui *Gui) handleCredentialsViewFocused(g *gocui.Gui, v *gocui.View) error {
if _, err := g.SetViewOnTop("credentials"); err != nil {
return err
}
func (gui *Gui) handleCredentialsViewFocused() error {
message := gui.Tr.TemplateLocalize(
"CloseConfirm",
Teml{
"keyBindClose": "esc",
"keyBindConfirm": "enter",
"keyBindClose": gui.getKeyDisplay("universal.return"),
"keyBindConfirm": gui.getKeyDisplay("universal.confirm"),
},
)
gui.renderString(g, "options", message)
gui.renderString("options", message)
return nil
}
// HandleCredentialsPopup handles the views after executing a command that might ask for credentials
func (gui *Gui) HandleCredentialsPopup(g *gocui.Gui, popupOpened bool, cmdErr error) {
if popupOpened {
_, _ = gui.g.SetViewOnBottom("credentials")
}
// handleCredentialsPopup handles the views after executing a command that might ask for credentials
func (gui *Gui) handleCredentialsPopup(cmdErr error) {
if cmdErr != nil {
errMessage := cmdErr.Error()
if strings.Contains(errMessage, "Invalid username or password") {
errMessage = gui.Tr.SLocalize("PassUnameWrong")
}
// we are not logging this error because it may contain a password
_ = gui.createSpecificErrorPanel(errMessage, gui.getFilesView(), false)
gui.createErrorPanel(errMessage)
} else {
_ = gui.closeConfirmationPrompt(g, true)
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC})
_ = gui.closeConfirmationPrompt(false)
}
}

View File

@@ -5,33 +5,25 @@ import (
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
)
func (gui *Gui) inDiffMode() bool {
return gui.State.Diff.Ref != ""
}
func (gui *Gui) exitDiffMode() error {
gui.State.Diff = DiffState{}
gui.State.Modes.Diffing = Diffing{}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}
func (gui *Gui) renderDiff() error {
gui.getMainView().Title = "Diff"
gui.State.SplitMainPanel = false
filterArg := ""
if gui.inFilterMode() {
filterArg = fmt.Sprintf(" -- %s", gui.State.FilterPath)
}
cmd := gui.OSCommand.ExecutableFromString(
fmt.Sprintf("git diff --color %s %s", gui.diffStr(), filterArg),
fmt.Sprintf("git diff --color %s", gui.diffStr()),
)
if err := gui.newPtyTask("main", cmd); err != nil {
gui.Log.Error(err)
}
return nil
task := gui.createRunPtyTask(cmd)
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: "Diff",
task: task,
},
})
}
// currentDiffTerminals returns the current diff terminals of the currently selected item.
@@ -39,68 +31,35 @@ func (gui *Gui) renderDiff() error {
// which becomes an option when you bring up the diff menu, but when you're just
// flicking through branches it will be using the local branch name.
func (gui *Gui) currentDiffTerminals() []string {
currentView := gui.g.CurrentView()
if currentView == nil {
switch gui.currentContext().GetKey() {
case "":
return nil
case FILES_CONTEXT_KEY:
return []string{""}
case COMMIT_FILES_CONTEXT_KEY:
return []string{gui.State.Panels.CommitFiles.refName}
case LOCAL_BRANCHES_CONTEXT_KEY:
// for our local branches we want to include both the branch and its upstream
branch := gui.getSelectedBranch()
if branch != nil {
names := []string{branch.ID()}
if branch.UpstreamName != "" {
names = append(names, branch.UpstreamName)
}
return names
}
return nil
default:
context := gui.currentSideContext()
if context == nil {
return nil
}
item, ok := context.GetSelectedItem()
if !ok {
return nil
}
return []string{item.ID()}
}
names := []string{}
switch currentView.Name() {
case "files":
// not supporting files for now
// file, err := gui.getSelectedFile()
// if err == nil {
// names = append(names, file.Name)
// }
case "commitFiles":
// not supporting commit files for now
// file := gui.getSelectedCommitFile()
// if file != nil {
// names = append(names, file.Name)
// }
case "commits":
var commit *commands.Commit
switch gui.getCommitsView().Context {
case "reflog-commits":
commit = gui.getSelectedReflogCommit()
case "branch-commits":
commit = gui.getSelectedCommit()
}
if commit != nil {
names = append(names, commit.Sha)
}
case "stash":
entry := gui.getSelectedStashEntry()
if entry != nil {
names = append(names, entry.RefName())
}
case "branches":
switch gui.getBranchesView().Context {
case "local-branches":
branch := gui.getSelectedBranch()
if branch != nil {
names = append(names, branch.Name)
if branch.UpstreamName != "" {
names = append(names, branch.UpstreamName)
}
}
case "remotes":
remote := gui.getSelectedRemote()
if remote != nil {
names = append(names, remote.Name)
}
case "remote-branches":
remoteBranch := gui.getSelectedRemoteBranch()
if remoteBranch != nil {
names = append(names, remoteBranch.FullName())
}
case "tags":
tag := gui.getSelectedTag()
if tag != nil {
names = append(names, tag.Name)
}
}
}
return names
}
func (gui *Gui) currentDiffTerminal() string {
@@ -111,16 +70,34 @@ func (gui *Gui) currentDiffTerminal() string {
return names[0]
}
func (gui *Gui) currentlySelectedFilename() string {
switch gui.currentContext().GetKey() {
case FILES_CONTEXT_KEY, COMMIT_FILES_CONTEXT_KEY:
return gui.getSideContextSelectedItemId()
default:
return ""
}
}
func (gui *Gui) diffStr() string {
output := gui.State.Diff.Ref
output := gui.State.Modes.Diffing.Ref
right := gui.currentDiffTerminal()
if right != "" {
output += " " + right
}
if gui.State.Diff.Reverse {
if gui.State.Modes.Diffing.Reverse {
output += " -R"
}
file := gui.currentlySelectedFilename()
if file != "" {
output += " -- " + file
} else if gui.State.Modes.Filtering.Active() {
output += " -- " + gui.State.Modes.Filtering.Path
}
return output
}
@@ -138,7 +115,7 @@ func (gui *Gui) handleCreateDiffingMenuPanel(g *gocui.Gui, v *gocui.View) error
{
displayString: fmt.Sprintf("%s %s", gui.Tr.SLocalize("diff"), name),
onPress: func() error {
gui.State.Diff.Ref = name
gui.State.Modes.Diffing.Ref = name
// can scope this down based on current view but too lazy right now
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
},
@@ -150,27 +127,27 @@ func (gui *Gui) handleCreateDiffingMenuPanel(g *gocui.Gui, v *gocui.View) error
{
displayString: gui.Tr.SLocalize("enterRefToDiff"),
onPress: func() error {
return gui.createPromptPanel(gui.g, v, gui.Tr.SLocalize("enteRefName"), "", func(g *gocui.Gui, promptView *gocui.View) error {
gui.State.Diff.Ref = strings.TrimSpace(promptView.Buffer())
return gui.prompt(gui.Tr.SLocalize("enteRefName"), "", func(response string) error {
gui.State.Modes.Diffing.Ref = strings.TrimSpace(response)
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
})
},
},
}...)
if gui.inDiffMode() {
if gui.State.Modes.Diffing.Active() {
menuItems = append(menuItems, []*menuItem{
{
displayString: gui.Tr.SLocalize("swapDiff"),
onPress: func() error {
gui.State.Diff.Reverse = !gui.State.Diff.Reverse
gui.State.Modes.Diffing.Reverse = !gui.State.Modes.Diffing.Reverse
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
},
},
{
displayString: gui.Tr.SLocalize("exitDiffMode"),
onPress: func() error {
gui.State.Diff = DiffState{}
gui.State.Modes.Diffing = Diffing{}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
},
},

View File

@@ -5,11 +5,8 @@ import (
)
func (gui *Gui) handleCreateDiscardMenu(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile()
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
}
file := gui.getSelectedFile()
if file == nil {
return nil
}

View File

@@ -13,39 +13,35 @@ import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// list panel functions
func (gui *Gui) getSelectedFile() (*commands.File, error) {
selectedLine := gui.State.Panels.Files.SelectedLine
func (gui *Gui) getSelectedFile() *commands.File {
selectedLine := gui.State.Panels.Files.SelectedLineIdx
if selectedLine == -1 {
return &commands.File{}, gui.Errors.ErrNoFiles
return nil
}
return gui.State.Files[selectedLine], nil
return gui.State.Files[selectedLine]
}
func (gui *Gui) selectFile(alreadySelected bool) error {
gui.getFilesView().FocusPoint(0, gui.State.Panels.Files.SelectedLine)
gui.getFilesView().FocusPoint(0, gui.State.Panels.Files.SelectedLineIdx)
if gui.inDiffMode() {
return gui.renderDiff()
}
file, err := gui.getSelectedFile()
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
}
gui.State.SplitMainPanel = false
gui.getMainView().Title = ""
return gui.newStringTask("main", gui.Tr.SLocalize("NoChangedFiles"))
file := gui.getSelectedFile()
if file == nil {
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: "",
task: gui.createRenderStringTask(gui.Tr.SLocalize("NoChangedFiles")),
},
})
}
if !alreadySelected {
// TODO: pull into update task interface
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
@@ -55,36 +51,30 @@ func (gui *Gui) selectFile(alreadySelected bool) error {
}
if file.HasInlineMergeConflicts {
gui.getMainView().Title = gui.Tr.SLocalize("MergeConflictsTitle")
gui.State.SplitMainPanel = false
return gui.refreshMergePanel()
}
if file.HasStagedChanges && file.HasUnstagedChanges {
gui.State.SplitMainPanel = true
gui.getMainView().Title = gui.Tr.SLocalize("UnstagedChanges")
gui.getSecondaryView().Title = gui.Tr.SLocalize("StagedChanges")
cmdStr := gui.GitCommand.DiffCmdStr(file, false, true)
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
if err := gui.newPtyTask("secondary", cmd); err != nil {
return err
}
} else {
gui.State.SplitMainPanel = false
if file.HasUnstagedChanges {
gui.getMainView().Title = gui.Tr.SLocalize("UnstagedChanges")
} else {
gui.getMainView().Title = gui.Tr.SLocalize("StagedChanges")
}
}
cmdStr := gui.GitCommand.DiffCmdStr(file, false, !file.HasUnstagedChanges && file.HasStagedChanges)
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(file, false, !file.HasUnstagedChanges && file.HasStagedChanges)
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
if err := gui.newPtyTask("main", cmd); err != nil {
return err
refreshOpts := refreshMainOpts{main: &viewUpdateOpts{
title: gui.Tr.SLocalize("UnstagedChanges"),
task: gui.createRunPtyTask(cmd),
}}
if file.HasStagedChanges && file.HasUnstagedChanges {
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(file, false, true)
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
refreshOpts.secondary = &viewUpdateOpts{
title: gui.Tr.SLocalize("StagedChanges"),
task: gui.createRunPtyTask(cmd),
}
} else if !file.HasUnstagedChanges {
refreshOpts.main.title = gui.Tr.SLocalize("StagedChanges")
}
return nil
return gui.refreshMainViews(refreshOpts)
}
func (gui *Gui) refreshFiles() error {
@@ -95,7 +85,7 @@ func (gui *Gui) refreshFiles() error {
gui.State.RefreshingFilesMutex.Unlock()
}()
selectedFile, _ := gui.getSelectedFile()
selectedFile := gui.getSelectedFile()
filesView := gui.getFilesView()
if filesView == nil {
@@ -107,12 +97,13 @@ func (gui *Gui) refreshFiles() error {
}
gui.g.Update(func(g *gocui.Gui) error {
displayStrings := presentation.GetFileListDisplayStrings(gui.State.Files, gui.State.Diff.Ref)
gui.renderDisplayStrings(filesView, displayStrings)
if err := gui.Contexts.Files.Context.HandleRender(); err != nil {
return err
}
if g.CurrentView() == filesView || (g.CurrentView() == gui.getMainView() && g.CurrentView().Context == "merging") {
newSelectedFile, _ := gui.getSelectedFile()
alreadySelected := newSelectedFile.Name == selectedFile.Name
if g.CurrentView() == filesView || (g.CurrentView() == gui.getMainView() && g.CurrentView().Context == MAIN_MERGING_CONTEXT_KEY) {
newSelectedFile := gui.getSelectedFile()
alreadySelected := selectedFile != nil && newSelectedFile != nil && newSelectedFile.Name == selectedFile.Name
return gui.selectFile(alreadySelected)
}
return nil
@@ -146,10 +137,11 @@ func (gui *Gui) trackedFiles() []*commands.File {
}
func (gui *Gui) stageSelectedFile(g *gocui.Gui) error {
file, err := gui.getSelectedFile()
if err != nil {
return err
file := gui.getSelectedFile()
if file == nil {
return nil
}
return gui.GitCommand.StageFile(file.Name)
}
@@ -158,46 +150,40 @@ func (gui *Gui) handleEnterFile(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) enterFile(forceSecondaryFocused bool, selectedLineIdx int) error {
file, err := gui.getSelectedFile()
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
}
file := gui.getSelectedFile()
if file == nil {
return nil
}
if file.HasInlineMergeConflicts {
return gui.handleSwitchToMerge(gui.g, gui.getFilesView())
return gui.handleSwitchToMerge()
}
if file.HasMergeConflicts {
return gui.createErrorPanel(gui.Tr.SLocalize("FileStagingRequirements"))
}
gui.changeMainViewsContext("staging")
if err := gui.switchFocus(gui.g, gui.getFilesView(), gui.getMainView()); err != nil {
return err
}
return gui.refreshStagingPanel(forceSecondaryFocused, selectedLineIdx)
gui.switchContext(gui.Contexts.Staging.Context)
return gui.refreshStagingPanel(forceSecondaryFocused, selectedLineIdx) // TODO: check if this is broken, try moving into context code
}
func (gui *Gui) handleFilePress(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile()
if err != nil {
if err == gui.Errors.ErrNoFiles {
return nil
}
return err
func (gui *Gui) handleFilePress() error {
file := gui.getSelectedFile()
if file == nil {
return nil
}
if file.HasInlineMergeConflicts {
return gui.handleSwitchToMerge(g, v)
return gui.handleSwitchToMerge()
}
if file.HasUnstagedChanges {
err = gui.GitCommand.StageFile(file.Name)
if err := gui.GitCommand.StageFile(file.Name); err != nil {
return gui.surfaceError(err)
}
} else {
err = gui.GitCommand.UnStageFile(file.Name, file.Tracked)
}
if err != nil {
return gui.surfaceError(err)
if err := gui.GitCommand.UnStageFile(file.Name, file.Tracked); err != nil {
return gui.surfaceError(err)
}
}
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
@@ -216,11 +202,7 @@ func (gui *Gui) allFilesStaged() bool {
return true
}
func (gui *Gui) focusAndSelectFile(g *gocui.Gui, v *gocui.View) error {
if _, err := gui.g.SetCurrentView("files"); err != nil {
return err
}
func (gui *Gui) focusAndSelectFile() error {
return gui.selectFile(false)
}
@@ -243,15 +225,16 @@ func (gui *Gui) handleStageAll(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile()
if err != nil {
return gui.surfaceError(err)
file := gui.getSelectedFile()
if file == nil {
return nil
}
if file.Tracked {
return gui.createConfirmationPanel(gui.g, gui.g.CurrentView(), true, gui.Tr.SLocalize("IgnoreTracked"), gui.Tr.SLocalize("IgnoreTrackedPrompt"),
// On confirmation
func(_ *gocui.Gui, _ *gocui.View) error {
return gui.ask(askOpts{
title: gui.Tr.SLocalize("IgnoreTracked"),
prompt: gui.Tr.SLocalize("IgnoreTrackedPrompt"),
handleConfirm: func() error {
if err := gui.GitCommand.Ignore(file.Name); err != nil {
return err
}
@@ -259,7 +242,8 @@ func (gui *Gui) handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
return err
}
return gui.refreshSidePanels(refreshOptions{scope: []int{FILES}})
}, nil)
},
})
}
if err := gui.GitCommand.Ignore(file.Name); err != nil {
@@ -275,18 +259,21 @@ func (gui *Gui) handleWIPCommitPress(g *gocui.Gui, filesView *gocui.View) error
return gui.createErrorPanel(gui.Tr.SLocalize("SkipHookPrefixNotConfigured"))
}
gui.renderString(g, "commitMessage", skipHookPreifx)
gui.renderStringSync("commitMessage", skipHookPreifx)
if err := gui.getCommitMessageView().SetCursor(len(skipHookPreifx), 0); err != nil {
return err
}
return gui.handleCommitPress(g, filesView)
return gui.handleCommitPress()
}
func (gui *Gui) handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
if len(gui.stagedFiles()) == 0 && gui.GitCommand.WorkingTreeState() == "normal" {
return gui.createErrorPanel(gui.Tr.SLocalize("NoStagedFilesToCommit"))
func (gui *Gui) handleCommitPress() error {
if len(gui.stagedFiles()) == 0 {
return gui.promptToStageAllAndRetry(func() error {
return gui.handleCommitPress()
})
}
commitMessageView := gui.getCommitMessageView()
prefixPattern := gui.Config.GetUserConfig().GetString("git.commitPrefixes." + utils.GetCurrentRepoName() + ".pattern")
prefixReplace := gui.Config.GetUserConfig().GetString("git.commitPrefixes." + utils.GetCurrentRepoName() + ".replace")
@@ -296,18 +283,14 @@ func (gui *Gui) handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
return gui.createErrorPanel(fmt.Sprintf("%s: %s", gui.Tr.SLocalize("commitPrefixPatternError"), err.Error()))
}
prefix := rgx.ReplaceAllString(gui.getCheckedOutBranch().Name, prefixReplace)
gui.renderString(g, "commitMessage", prefix)
gui.renderString("commitMessage", prefix)
if err := commitMessageView.SetCursor(len(prefix), 0); err != nil {
return err
}
}
g.Update(func(g *gocui.Gui) error {
if _, err := g.SetViewOnTop("commitMessage"); err != nil {
return err
}
if err := gui.switchFocus(g, filesView, commitMessageView); err != nil {
gui.g.Update(func(g *gocui.Gui) error {
if err := gui.switchContext(gui.Contexts.CommitMessage.Context); err != nil {
return err
}
@@ -317,44 +300,68 @@ func (gui *Gui) handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
return nil
}
func (gui *Gui) handleAmendCommitPress(g *gocui.Gui, filesView *gocui.View) error {
if len(gui.stagedFiles()) == 0 && gui.GitCommand.WorkingTreeState() == "normal" {
return gui.createErrorPanel(gui.Tr.SLocalize("NoStagedFilesToCommit"))
func (gui *Gui) promptToStageAllAndRetry(retry func() error) error {
return gui.ask(askOpts{
title: gui.Tr.SLocalize("NoFilesStagedTitle"),
prompt: gui.Tr.SLocalize("NoFilesStagedPrompt"),
handleConfirm: func() error {
if err := gui.GitCommand.StageAll(); err != nil {
return gui.surfaceError(err)
}
if err := gui.refreshFiles(); err != nil {
return gui.surfaceError(err)
}
return retry()
},
})
}
func (gui *Gui) handleAmendCommitPress() error {
if len(gui.stagedFiles()) == 0 {
return gui.promptToStageAllAndRetry(func() error {
return gui.handleAmendCommitPress()
})
}
if len(gui.State.Commits) == 0 {
return gui.createErrorPanel(gui.Tr.SLocalize("NoCommitToAmend"))
}
title := strings.Title(gui.Tr.SLocalize("AmendLastCommit"))
question := gui.Tr.SLocalize("SureToAmend")
return gui.ask(askOpts{
title: strings.Title(gui.Tr.SLocalize("AmendLastCommit")),
prompt: gui.Tr.SLocalize("SureToAmend"),
handleConfirm: func() error {
ok, err := gui.runSyncOrAsyncCommand(gui.GitCommand.AmendHead())
if err != nil {
return err
}
if !ok {
return nil
}
return gui.createConfirmationPanel(g, filesView, true, title, question, func(g *gocui.Gui, v *gocui.View) error {
ok, err := gui.runSyncOrAsyncCommand(gui.GitCommand.AmendHead())
if err != nil {
return err
}
if !ok {
return nil
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}, nil)
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
},
})
}
// handleCommitEditorPress - handle when the user wants to commit changes via
// their editor rather than via the popup panel
func (gui *Gui) handleCommitEditorPress(g *gocui.Gui, filesView *gocui.View) error {
if len(gui.stagedFiles()) == 0 && gui.GitCommand.WorkingTreeState() == "normal" {
return gui.createErrorPanel(gui.Tr.SLocalize("NoStagedFilesToCommit"))
func (gui *Gui) handleCommitEditorPress() error {
if len(gui.stagedFiles()) == 0 {
return gui.promptToStageAllAndRetry(func() error {
return gui.handleCommitEditorPress()
})
}
gui.PrepareSubProcess(g, "git", "commit")
gui.PrepareSubProcess("git", "commit")
return nil
}
// PrepareSubProcess - prepare a subprocess for execution and tell the gui to switch to it
func (gui *Gui) PrepareSubProcess(g *gocui.Gui, commands ...string) {
func (gui *Gui) PrepareSubProcess(commands ...string) {
gui.SubProcess = gui.GitCommand.PrepareCommitSubProcess()
g.Update(func(g *gocui.Gui) error {
gui.g.Update(func(g *gocui.Gui) error {
return gui.Errors.ErrSubProcess
})
}
@@ -365,18 +372,18 @@ func (gui *Gui) editFile(filename string) error {
}
func (gui *Gui) handleFileEdit(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile()
if err != nil {
return gui.surfaceError(err)
file := gui.getSelectedFile()
if file == nil {
return nil
}
return gui.editFile(file.Name)
}
func (gui *Gui) handleFileOpen(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile()
if err != nil {
return gui.surfaceError(err)
file := gui.getSelectedFile()
if file == nil {
return nil
}
return gui.openFile(file.Name)
}
@@ -386,21 +393,47 @@ func (gui *Gui) handleRefreshFiles(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) refreshStateFiles() error {
// keep track of where the cursor is currently and the current file names
// when we refresh, go looking for a matching name
// move the cursor to there.
selectedFile := gui.getSelectedFile()
prevSelectedLineIdx := gui.State.Panels.Files.SelectedLineIdx
// get files to stage
files := gui.GitCommand.GetStatusFiles()
gui.State.Files = gui.GitCommand.MergeStatusFiles(gui.State.Files, files)
files := gui.GitCommand.GetStatusFiles(commands.GetStatusFileOptions{})
gui.State.Files = gui.GitCommand.MergeStatusFiles(gui.State.Files, files, selectedFile)
if err := gui.fileWatcher.addFilesToFileWatcher(files); err != nil {
return err
}
gui.refreshSelectedLine(&gui.State.Panels.Files.SelectedLine, len(gui.State.Files))
// let's try to find our file again and move the cursor to that
if selectedFile != nil {
for idx, f := range gui.State.Files {
selectedFileHasMoved := f.Matches(selectedFile) && idx != prevSelectedLineIdx
if selectedFileHasMoved {
gui.State.Panels.Files.SelectedLineIdx = idx
break
}
}
}
gui.refreshSelectedLine(gui.State.Panels.Files, len(gui.State.Files))
return nil
}
func (gui *Gui) handlePullFiles(g *gocui.Gui, v *gocui.View) error {
// if we have no upstream branch we need to set that first
if gui.popupPanelFocused() {
return nil
}
currentBranch := gui.currentBranch()
if currentBranch == nil {
// need to wait for branches to refresh
return nil
}
// if we have no upstream branch we need to set that first
if currentBranch.Pullables == "?" {
// see if we have this branch in our config with an upstream
conf, err := gui.GitCommand.Repo.Config()
@@ -409,12 +442,11 @@ func (gui *Gui) handlePullFiles(g *gocui.Gui, v *gocui.View) error {
}
for branchName, branch := range conf.Branches {
if branchName == currentBranch.Name {
return gui.pullFiles(v, fmt.Sprintf("%s %s", branch.Remote, branchName))
return gui.pullFiles(PullFilesOptions{RemoteName: branch.Remote, BranchName: branch.Name})
}
}
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("EnterUpstream"), "origin/"+currentBranch.Name, func(g *gocui.Gui, v *gocui.View) error {
upstream := gui.trimmedContent(v)
return gui.prompt(gui.Tr.SLocalize("EnterUpstream"), "origin/"+currentBranch.Name, func(upstream string) error {
if err := gui.GitCommand.SetUpstreamBranch(upstream); err != nil {
errorMessage := err.Error()
if strings.Contains(errorMessage, "does not exist") {
@@ -422,47 +454,90 @@ func (gui *Gui) handlePullFiles(g *gocui.Gui, v *gocui.View) error {
}
return gui.createErrorPanel(errorMessage)
}
return gui.pullFiles(v, "")
return gui.pullFiles(PullFilesOptions{})
})
}
return gui.pullFiles(v, "")
return gui.pullFiles(PullFilesOptions{})
}
func (gui *Gui) pullFiles(v *gocui.View, args string) error {
if err := gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("PullWait")); err != nil {
type PullFilesOptions struct {
RemoteName string
BranchName string
}
func (gui *Gui) pullFiles(opts PullFilesOptions) error {
if err := gui.createLoaderPanel(gui.g.CurrentView(), gui.Tr.SLocalize("PullWait")); err != nil {
return err
}
go func() {
unamePassOpend := false
err := gui.GitCommand.Pull(args, func(passOrUname string) string {
unamePassOpend = true
return gui.waitForPassUname(gui.g, v, passOrUname)
})
gui.HandleCredentialsPopup(gui.g, unamePassOpend, err)
}()
mode := gui.Config.GetUserConfig().GetString("git.pull.mode")
go gui.pullWithMode(mode, opts)
return nil
}
func (gui *Gui) pushWithForceFlag(g *gocui.Gui, v *gocui.View, force bool, upstream string, args string) error {
if err := gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("PushWait")); err != nil {
func (gui *Gui) pullWithMode(mode string, opts PullFilesOptions) error {
gui.State.FetchMutex.Lock()
defer gui.State.FetchMutex.Unlock()
err := gui.GitCommand.Fetch(
commands.FetchOptions{
PromptUserForCredential: gui.promptUserForCredential,
RemoteName: opts.RemoteName,
BranchName: opts.BranchName,
},
)
gui.handleCredentialsPopup(err)
if err != nil {
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}
switch mode {
case "rebase":
err := gui.GitCommand.RebaseBranch("FETCH_HEAD")
return gui.handleGenericMergeCommandResult(err)
case "merge":
err := gui.GitCommand.Merge("FETCH_HEAD", commands.MergeOpts{})
return gui.handleGenericMergeCommandResult(err)
case "ff-only":
err := gui.GitCommand.Merge("FETCH_HEAD", commands.MergeOpts{FastForwardOnly: true})
return gui.handleGenericMergeCommandResult(err)
default:
return gui.createErrorPanel(fmt.Sprintf("git pull mode '%s' unrecognised", mode))
}
}
func (gui *Gui) pushWithForceFlag(v *gocui.View, force bool, upstream string, args string) error {
if err := gui.createLoaderPanel(v, gui.Tr.SLocalize("PushWait")); err != nil {
return err
}
go func() {
unamePassOpend := false
branchName := gui.getCheckedOutBranch().Name
err := gui.GitCommand.Push(branchName, force, upstream, args, func(passOrUname string) string {
unamePassOpend = true
return gui.waitForPassUname(g, v, passOrUname)
})
gui.HandleCredentialsPopup(g, unamePassOpend, err)
err := gui.GitCommand.Push(branchName, force, upstream, args, gui.promptUserForCredential)
if err != nil && !force && strings.Contains(err.Error(), "Updates were rejected") {
gui.ask(askOpts{
title: gui.Tr.SLocalize("ForcePush"),
prompt: gui.Tr.SLocalize("ForcePushPrompt"),
handleConfirm: func() error {
return gui.pushWithForceFlag(v, true, upstream, args)
},
})
return
}
gui.handleCredentialsPopup(err)
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}()
return nil
}
func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
// if we have pullables we'll ask if the user wants to force push
currentBranch := gui.currentBranch()
@@ -474,41 +549,41 @@ func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error {
}
for branchName, branch := range conf.Branches {
if branchName == currentBranch.Name {
return gui.pushWithForceFlag(g, v, false, "", fmt.Sprintf("%s %s", branch.Remote, branchName))
return gui.pushWithForceFlag(v, false, "", fmt.Sprintf("%s %s", branch.Remote, branchName))
}
}
if gui.GitCommand.PushToCurrent {
return gui.pushWithForceFlag(g, v, false, "", "--set-upstream")
return gui.pushWithForceFlag(v, false, "", "--set-upstream")
} else {
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("EnterUpstream"), "origin "+currentBranch.Name, func(g *gocui.Gui, v *gocui.View) error {
return gui.pushWithForceFlag(g, v, false, gui.trimmedContent(v), "")
return gui.prompt(gui.Tr.SLocalize("EnterUpstream"), "origin "+currentBranch.Name, func(response string) error {
return gui.pushWithForceFlag(v, false, response, "")
})
}
} else if currentBranch.Pullables == "0" {
return gui.pushWithForceFlag(g, v, false, "", "")
return gui.pushWithForceFlag(v, false, "", "")
}
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("ForcePush"), gui.Tr.SLocalize("ForcePushPrompt"), func(g *gocui.Gui, v *gocui.View) error {
return gui.pushWithForceFlag(g, v, true, "", "")
}, nil)
return gui.ask(askOpts{
title: gui.Tr.SLocalize("ForcePush"),
prompt: gui.Tr.SLocalize("ForcePushPrompt"),
handleConfirm: func() error {
return gui.pushWithForceFlag(v, true, "", "")
},
})
}
func (gui *Gui) handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile()
if err != nil {
if err != gui.Errors.ErrNoFiles {
return gui.surfaceError(err)
}
func (gui *Gui) handleSwitchToMerge() error {
file := gui.getSelectedFile()
if file == nil {
return nil
}
if !file.HasInlineMergeConflicts {
return gui.createErrorPanel(gui.Tr.SLocalize("FileNoMergeCons"))
}
gui.changeMainViewsContext("merging")
if err := gui.switchFocus(g, v, gui.getMainView()); err != nil {
return err
}
return gui.refreshMergePanel()
return gui.switchContext(gui.Contexts.Merging.Context)
}
func (gui *Gui) openFile(filename string) error {
@@ -528,8 +603,7 @@ func (gui *Gui) anyFilesWithMergeConflicts() bool {
}
func (gui *Gui) handleCustomCommand(g *gocui.Gui, v *gocui.View) error {
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("CustomCommand"), "", func(g *gocui.Gui, v *gocui.View) error {
command := gui.trimmedContent(v)
return gui.prompt(gui.Tr.SLocalize("CustomCommand"), "", func(command string) error {
gui.SubProcess = gui.OSCommand.RunCustomCommand(command)
return gui.Errors.ErrSubProcess
})
@@ -561,8 +635,3 @@ func (gui *Gui) handleStashChanges(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCreateResetToUpstreamMenu(g *gocui.Gui, v *gocui.View) error {
return gui.createResetMenu("@{upstream}")
}
func (gui *Gui) onFilesPanelSearchSelect(selectedLine int) error {
gui.State.Panels.Files.SelectedLine = selectedLine
return gui.focusAndSelectFile(gui.g, gui.getFilesView())
}

View File

@@ -1,21 +1,21 @@
package gui
import "github.com/jesseduffield/gocui"
func (gui *Gui) inFilterMode() bool {
return gui.State.FilterPath != ""
}
func (gui *Gui) validateNotInFilterMode() (bool, error) {
if gui.inFilterMode() {
return false, gui.createConfirmationPanel(gui.g, gui.g.CurrentView(), true, gui.Tr.SLocalize("MustExitFilterModeTitle"), gui.Tr.SLocalize("MustExitFilterModePrompt"), func(*gocui.Gui, *gocui.View) error {
return gui.exitFilterMode()
}, nil)
if gui.State.Modes.Filtering.Active() {
err := gui.ask(askOpts{
title: gui.Tr.SLocalize("MustExitFilterModeTitle"),
prompt: gui.Tr.SLocalize("MustExitFilterModePrompt"),
handleConfirm: func() error {
return gui.exitFilterMode()
},
})
return false, err
}
return true, nil
}
func (gui *Gui) exitFilterMode() error {
gui.State.FilterPath = ""
gui.State.Modes.Filtering.Path = ""
return gui.Errors.ErrRestart
}

View File

@@ -15,8 +15,8 @@ func (gui *Gui) handleCreateFilteringMenuPanel(g *gocui.Gui, v *gocui.View) erro
fileName := ""
switch v.Name() {
case "files":
file, err := gui.getSelectedFile()
if err == nil {
file := gui.getSelectedFile()
if file != nil {
fileName = file.Name
}
case "commitFiles":
@@ -32,7 +32,7 @@ func (gui *Gui) handleCreateFilteringMenuPanel(g *gocui.Gui, v *gocui.View) erro
menuItems = append(menuItems, &menuItem{
displayString: fmt.Sprintf("%s '%s'", gui.Tr.SLocalize("filterBy"), fileName),
onPress: func() error {
gui.State.FilterPath = fileName
gui.State.Modes.Filtering.Path = fileName
return gui.Errors.ErrRestart
},
})
@@ -41,18 +41,18 @@ func (gui *Gui) handleCreateFilteringMenuPanel(g *gocui.Gui, v *gocui.View) erro
menuItems = append(menuItems, &menuItem{
displayString: gui.Tr.SLocalize("filterPathOption"),
onPress: func() error {
return gui.createPromptPanel(gui.g, v, gui.Tr.SLocalize("enterFileName"), "", func(g *gocui.Gui, promptView *gocui.View) error {
gui.State.FilterPath = strings.TrimSpace(promptView.Buffer())
return gui.prompt(gui.Tr.SLocalize("enterFileName"), "", func(response string) error {
gui.State.Modes.Filtering.Path = strings.TrimSpace(response)
return gui.Errors.ErrRestart
})
},
})
if gui.inFilterMode() {
if gui.State.Modes.Filtering.Active() {
menuItems = append(menuItems, &menuItem{
displayString: gui.Tr.SLocalize("exitFilterMode"),
onPress: func() error {
gui.State.FilterPath = ""
gui.State.Modes.Filtering.Path = ""
return gui.Errors.ErrRestart
},
})

View File

@@ -51,8 +51,7 @@ func (gui *Gui) handleCreateGitFlowMenu(g *gocui.Gui, v *gocui.View) error {
startHandler := func(branchType string) func() error {
return func() error {
title := gui.Tr.TemplateLocalize("NewBranchNamePrompt", map[string]interface{}{"branchType": branchType})
return gui.createPromptPanel(gui.g, gui.getMenuView(), title, "", func(g *gocui.Gui, v *gocui.View) error {
name := gui.trimmedContent(v)
return gui.prompt(title, "", func(name string) error {
subProcess := gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "start", name)
gui.SubProcess = subProcess
return gui.Errors.ErrSubProcess

View File

@@ -4,48 +4,50 @@ import (
"math"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) nextScreenMode(g *gocui.Gui, v *gocui.View) error {
gui.State.ScreenMode = utils.NextIntInCycle([]int{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode)
// commits render differently depending on whether we're in fullscreen more or not
if err := gui.refreshCommitsViewWithSelection(); err != nil {
return err
}
// same with branches
if err := gui.refreshBranchesViewWithSelection(); err != nil {
return err
// these views need to be re-rendered when the screen mode changes. The commits view,
// for example, will show authorship information in half and full screen mode.
func (gui *Gui) rerenderViewsWithScreenModeDependentContent() error {
for _, viewName := range []string{"branches", "commits"} {
if err := gui.rerenderView(viewName); err != nil {
return err
}
}
return nil
}
func (gui *Gui) nextScreenMode(g *gocui.Gui, v *gocui.View) error {
gui.State.ScreenMode = utils.NextIntInCycle([]int{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode)
return gui.rerenderViewsWithScreenModeDependentContent()
}
func (gui *Gui) prevScreenMode(g *gocui.Gui, v *gocui.View) error {
gui.State.ScreenMode = utils.PrevIntInCycle([]int{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode)
// commits render differently depending on whether we're in fullscreen more or not
if err := gui.refreshCommitsViewWithSelection(); err != nil {
return err
}
// same with branches
if err := gui.refreshBranchesViewWithSelection(); err != nil {
return err
}
return nil
return gui.rerenderViewsWithScreenModeDependentContent()
}
func (gui *Gui) scrollUpView(viewName string) error {
mainView, _ := gui.g.View(viewName)
mainView, err := gui.g.View(viewName)
if err != nil {
return nil
}
ox, oy := mainView.Origin()
newOy := int(math.Max(0, float64(oy-gui.Config.GetUserConfig().GetInt("gui.scrollHeight"))))
return mainView.SetOrigin(ox, newOy)
}
func (gui *Gui) scrollDownView(viewName string) error {
mainView, _ := gui.g.View(viewName)
mainView, err := gui.g.View(viewName)
if err != nil {
return nil
}
ox, oy := mainView.Origin()
y := oy
if !gui.Config.GetUserConfig().GetBool("gui.scrollPastBottom") {
@@ -113,6 +115,9 @@ func (gui *Gui) handleMouseDownMain(g *gocui.Gui, v *gocui.View) error {
switch g.CurrentView().Name() {
case "files":
// set filename, set primary/secondary selected, set line number, then switch context
// I'll need to know it was changed though.
// Could I pass something along to the context change?
return gui.enterFile(false, v.SelectedLineIdx())
case "commitFiles":
return gui.enterCommitFile(v.SelectedLineIdx())
@@ -142,47 +147,50 @@ func (gui *Gui) handleInfoClick(g *gocui.Gui, v *gocui.View) error {
cx, _ := v.Cursor()
width, _ := v.Size()
// if we're in the normal context there will be a donate button here
// if we have ('reset') at the end then
if gui.inFilterMode() {
if width-cx <= len(gui.Tr.SLocalize("(reset)")) {
return gui.exitFilterMode()
} else {
return nil
if width-cx > len(gui.Tr.SLocalize("(reset)")) {
return nil
}
for _, mode := range gui.modeStatuses() {
if mode.isActive() {
return mode.reset()
}
}
if gui.inDiffMode() {
if width-cx <= len(gui.Tr.SLocalize("(reset)")) {
return gui.exitDiffMode()
} else {
return nil
}
}
if cx <= len(gui.Tr.SLocalize("Donate")) {
// if we're not in an active mode we show the donate button
if cx <= len(gui.Tr.SLocalize("Donate"))+len(INFO_SECTION_PADDING) {
return gui.OSCommand.OpenLink("https://github.com/sponsors/jesseduffield")
}
return nil
}
func (gui *Gui) fetch(g *gocui.Gui, v *gocui.View, canAskForCredentials bool) (unamePassOpend bool, err error) {
unamePassOpend = false
err = gui.GitCommand.Fetch(func(passOrUname string) string {
unamePassOpend = true
return gui.waitForPassUname(gui.g, v, passOrUname)
}, canAskForCredentials)
func (gui *Gui) fetch(canPromptForCredentials bool) (err error) {
gui.State.FetchMutex.Lock()
defer gui.State.FetchMutex.Unlock()
if canAskForCredentials && err != nil && strings.Contains(err.Error(), "exit status 128") {
colorFunction := color.New(color.FgRed).SprintFunc()
coloredMessage := colorFunction(strings.TrimSpace(gui.Tr.SLocalize("PassUnameWrong")))
close := func(g *gocui.Gui, v *gocui.View) error {
return nil
}
_ = gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("Error"), coloredMessage, close, close)
fetchOpts := commands.FetchOptions{}
if canPromptForCredentials {
fetchOpts.PromptUserForCredential = gui.promptUserForCredential
}
err = gui.GitCommand.Fetch(fetchOpts)
if canPromptForCredentials && err != nil && strings.Contains(err.Error(), "exit status 128") {
gui.createErrorPanel(gui.Tr.SLocalize("PassUnameWrong"))
}
gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, COMMITS, REMOTES, TAGS}, mode: ASYNC})
return unamePassOpend, err
return err
}
func (gui *Gui) handleCopySelectedSideContextItemToClipboard() error {
// important to note that this assumes we've selected an item in a side context
itemId := gui.getSideContextSelectedItemId()
if itemId == "" {
return nil
}
return gui.OSCommand.CopyToClipboard(itemId)
}

View File

@@ -22,6 +22,7 @@ import (
"github.com/golang-collections/collections/stack"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/tasks"
@@ -71,6 +72,15 @@ func (gui *Gui) GenerateSentinelErrors() {
}
}
func (gui *Gui) sentinelErrorsArr() []error {
return []error{
gui.Errors.ErrSubProcess,
gui.Errors.ErrNoFiles,
gui.Errors.ErrSwitchRepo,
gui.Errors.ErrRestart,
}
}
// Teml is short for template used to make the required map[string]interface{} shorter when using gui.Tr.SLocalize and gui.Tr.TemplateLocalize
type Teml i18n.Teml
@@ -92,6 +102,29 @@ type Gui struct {
fileWatcher *fileWatcher
viewBufferManagerMap map[string]*tasks.ViewBufferManager
stopChan chan struct{}
// when lazygit is opened outside a git directory we want to open to the most
// recent repo with the recent repos popup showing
showRecentRepos bool
Contexts ContextTree
ViewTabContextMap map[string][]tabContext
}
type listPanelState struct {
SelectedLineIdx int
}
func (h *listPanelState) SetSelectedLineIdx(value int) {
h.SelectedLineIdx = value
}
func (h *listPanelState) GetSelectedLineIdx() int {
return h.SelectedLineIdx
}
type IListPanelState interface {
SetSelectedLineIdx(int)
GetSelectedLineIdx() int
}
// for now the staging panel state, unlike the other panel states, is going to be
@@ -102,7 +135,7 @@ type lineByLinePanelState struct {
FirstLineIdx int
LastLineIdx int
Diff string
PatchParser *commands.PatchParser
PatchParser *patch.PatchParser
SelectMode int // one of LINE, HUNK, or RANGE
SecondaryFocused bool // this is for if we show the left or right panel
}
@@ -119,46 +152,59 @@ type mergingPanelState struct {
}
type filePanelState struct {
SelectedLine int
listPanelState
}
// TODO: consider splitting this out into the window and the branches view
type branchPanelState struct {
SelectedLine int
listPanelState
}
type remotePanelState struct {
SelectedLine int
listPanelState
}
type remoteBranchesState struct {
SelectedLine int
listPanelState
}
type tagsPanelState struct {
SelectedLine int
listPanelState
}
type commitPanelState struct {
SelectedLine int
listPanelState
LimitCommits bool
}
type reflogCommitPanelState struct {
SelectedLine int
listPanelState
}
type subCommitPanelState struct {
listPanelState
// e.g. name of branch whose commits we're looking at
refName string
}
type stashPanelState struct {
SelectedLine int
listPanelState
}
type menuPanelState struct {
SelectedLine int
OnPress func(g *gocui.Gui, v *gocui.View) error
listPanelState
OnPress func() error
}
type commitFilesPanelState struct {
SelectedLine int
listPanelState
// this is the SHA of the commit or the stash index of the stash.
// Not sure if ref is actually the right word here
refName string
canRebase bool
}
type panelStates struct {
@@ -169,6 +215,7 @@ type panelStates struct {
Tags *tagsPanelState
Commits *commitPanelState
ReflogCommits *reflogCommitPanelState
SubCommits *subCommitPanelState
Stash *stashPanelState
Menu *menuPanelState
LineByLine *lineByLinePanelState
@@ -189,11 +236,40 @@ const (
)
// if ref is blank we're not diffing anything
type DiffState struct {
type Diffing struct {
Ref string
Reverse bool
}
func (m *Diffing) Active() bool {
return m.Ref != ""
}
type Filtering struct {
Path string // the filename that gets passed to git log
}
func (m *Filtering) Active() bool {
return m.Path != ""
}
type CherryPicking struct {
CherryPickedCommits []*commands.Commit
// we only allow cherry picking from one context at a time, so you can't copy a commit from the local commits context and then also copy a commit in the reflog context
ContextKey string
}
func (m *CherryPicking) Active() bool {
return len(m.CherryPickedCommits) > 0
}
type Modes struct {
Filtering Filtering
CherryPicking CherryPicking
Diffing Diffing
}
type guiState struct {
Files []*commands.File
Branches []*commands.Branch
@@ -207,20 +283,20 @@ type guiState struct {
// if we're not in filtering mode, CommitFiles and FilteredReflogCommits will be
// one and the same
ReflogCommits []*commands.Commit
SubCommits []*commands.Commit
Remotes []*commands.Remote
RemoteBranches []*commands.RemoteBranch
Tags []*commands.Tag
MenuItemCount int // can't store the actual list because it's of interface{} type
PreviousView string
MenuItems []*menuItem
Updating bool
Panels *panelStates
MainContext string // used to keep the main and secondary views' contexts in sync
CherryPickedCommits []*commands.Commit
SplitMainPanel bool
RetainOriginalDir bool
IsRefreshingFiles bool
RefreshingFilesMutex sync.Mutex
RefreshingStatusMutex sync.Mutex
FetchMutex sync.Mutex
Searching searchingState
ScreenMode int
SideView *gocui.View
@@ -228,39 +304,60 @@ type guiState struct {
PrevMainWidth int
PrevMainHeight int
OldInformation string
StartupStage int // one of INITIAL and COMPLETE. Allows us to not load everything at once
FilterPath string // the filename that gets passed to git log
Diff DiffState
StartupStage int // one of INITIAL and COMPLETE. Allows us to not load everything at once
Modes Modes
ContextStack []Context
ViewContextMap map[string]Context
// WindowViewNameMap is a mapping of windows to the current view of that window.
// Some views move between windows for example the commitFiles view and when cycling through
// side windows we need to know which view to give focus to for a given window
WindowViewNameMap map[string]string
}
func (gui *Gui) resetState() {
// we carry over the filter path and diff state
prevFilterPath := ""
prevDiff := DiffState{}
prevFiltering := Filtering{
Path: "",
}
prevDiff := Diffing{}
prevCherryPicking := CherryPicking{
CherryPickedCommits: make([]*commands.Commit, 0),
ContextKey: "",
}
if gui.State != nil {
prevFilterPath = gui.State.FilterPath
prevDiff = gui.State.Diff
prevFiltering = gui.State.Modes.Filtering
prevDiff = gui.State.Modes.Diffing
prevCherryPicking = gui.State.Modes.CherryPicking
}
modes := Modes{
Filtering: prevFiltering,
CherryPicking: prevCherryPicking,
Diffing: prevDiff,
}
gui.State = &guiState{
Files: make([]*commands.File, 0),
PreviousView: "files",
Commits: make([]*commands.Commit, 0),
FilteredReflogCommits: make([]*commands.Commit, 0),
ReflogCommits: make([]*commands.Commit, 0),
CherryPickedCommits: make([]*commands.Commit, 0),
StashEntries: make([]*commands.StashEntry, 0),
Panels: &panelStates{
Files: &filePanelState{SelectedLine: -1},
Branches: &branchPanelState{SelectedLine: 0},
Remotes: &remotePanelState{SelectedLine: 0},
RemoteBranches: &remoteBranchesState{SelectedLine: -1},
Tags: &tagsPanelState{SelectedLine: -1},
Commits: &commitPanelState{SelectedLine: -1, LimitCommits: true},
ReflogCommits: &reflogCommitPanelState{SelectedLine: 0}, // TODO: might need to make -1
CommitFiles: &commitFilesPanelState{SelectedLine: -1},
Stash: &stashPanelState{SelectedLine: -1},
Menu: &menuPanelState{SelectedLine: 0},
// TODO: work out why some of these are -1 and some are 0. Last time I checked there was a good reason but I'm less certain now
Files: &filePanelState{listPanelState{SelectedLineIdx: -1}},
Branches: &branchPanelState{listPanelState{SelectedLineIdx: 0}},
Remotes: &remotePanelState{listPanelState{SelectedLineIdx: 0}},
RemoteBranches: &remoteBranchesState{listPanelState{SelectedLineIdx: -1}},
Tags: &tagsPanelState{listPanelState{SelectedLineIdx: -1}},
Commits: &commitPanelState{listPanelState: listPanelState{SelectedLineIdx: -1}, LimitCommits: true},
ReflogCommits: &reflogCommitPanelState{listPanelState{SelectedLineIdx: 0}},
SubCommits: &subCommitPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}, refName: ""},
CommitFiles: &commitFilesPanelState{listPanelState: listPanelState{SelectedLineIdx: -1}, refName: ""},
Stash: &stashPanelState{listPanelState{SelectedLineIdx: -1}},
Menu: &menuPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}, OnPress: nil},
Merging: &mergingPanelState{
ConflictIndex: 0,
ConflictTop: true,
@@ -268,16 +365,16 @@ func (gui *Gui) resetState() {
EditHistory: stack.New(),
},
},
SideView: nil,
Ptmx: nil,
FilterPath: prevFilterPath,
Diff: prevDiff,
SideView: nil,
Ptmx: nil,
Modes: modes,
ViewContextMap: gui.initialViewContextMap(),
}
}
// for now the split view will always be on
// NewGui builds a new gui handler
func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *commands.OSCommand, tr *i18n.Localizer, config config.AppConfigurer, updater *updates.Updater, filterPath string) (*Gui, error) {
func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *commands.OSCommand, tr *i18n.Localizer, config config.AppConfigurer, updater *updates.Updater, filterPath string, showRecentRepos bool) (*Gui, error) {
gui := &Gui{
Log: log,
GitCommand: gitCommand,
@@ -287,10 +384,13 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *comma
Updater: updater,
statusManager: &statusManager{},
viewBufferManagerMap: map[string]*tasks.ViewBufferManager{},
showRecentRepos: showRecentRepos,
}
gui.resetState()
gui.State.FilterPath = filterPath
gui.State.Modes.Filtering.Path = filterPath
gui.Contexts = gui.contextTree()
gui.ViewTabContextMap = gui.viewTabContextMap()
gui.watchFilesForChanges()
@@ -309,7 +409,7 @@ func (gui *Gui) Run() error {
}
defer g.Close()
if gui.inFilterMode() {
if gui.State.Modes.Filtering.Active() {
gui.State.ScreenMode = SCREEN_HALF
} else {
gui.State.ScreenMode = SCREEN_NORMAL
@@ -320,8 +420,6 @@ func (gui *Gui) Run() error {
g.NextSearchMatchKey = gui.getKey("universal.nextMatch")
g.PrevSearchMatchKey = gui.getKey("universal.prevMatch")
gui.stopChan = make(chan struct{})
g.ASCII = runtime.GOOS == "windows" && runewidth.IsEastAsian()
if gui.Config.GetUserConfig().GetBool("gui.mouseEvents") {
@@ -335,13 +433,10 @@ func (gui *Gui) Run() error {
}
popupTasks := []func(chan struct{}) error{}
if gui.Config.GetUserConfig().GetString("reporting") == "undetermined" {
popupTasks = append(popupTasks, gui.promptAnonymousReporting)
}
configPopupVersion := gui.Config.GetUserConfig().GetInt("StartupPopupVersion")
// -1 means we've disabled these popups
if configPopupVersion != -1 && configPopupVersion < StartupPopupVersion {
popupTasks = append(popupTasks, gui.showShamelessSelfPromotionMessage)
popupTasks = append(popupTasks, gui.showIntroPopupMessage)
}
gui.showInitialPopups(popupTasks)
@@ -354,10 +449,6 @@ func (gui *Gui) Run() error {
g.SetManager(gocui.ManagerFunc(gui.layout), gocui.ManagerFunc(gui.getFocusLayout()))
if err = gui.keybindings(g); err != nil {
return err
}
gui.Log.Warn("starting main loop")
err = g.MainLoop()
@@ -369,6 +460,7 @@ func (gui *Gui) Run() error {
// otherwise it handles the error, possibly by quitting the application
func (gui *Gui) RunWithSubprocesses() error {
for {
gui.stopChan = make(chan struct{})
if err := gui.Run(); err != nil {
for _, manager := range gui.viewBufferManagerMap {
manager.Close()
@@ -381,28 +473,27 @@ func (gui *Gui) RunWithSubprocesses() error {
close(gui.stopChan)
if err == gocui.ErrQuit {
switch err {
case gocui.ErrQuit:
if !gui.State.RetainOriginalDir {
if err := gui.recordCurrentDirectory(); err != nil {
return err
}
}
break
} else if err == gui.Errors.ErrSwitchRepo {
return nil
case gui.Errors.ErrSwitchRepo, gui.Errors.ErrRestart:
continue
} else if err == gui.Errors.ErrRestart {
continue
} else if err == gui.Errors.ErrSubProcess {
case gui.Errors.ErrSubProcess:
if err := gui.runCommand(); err != nil {
return err
}
} else {
default:
return err
}
}
}
return nil
}
func (gui *Gui) runCommand() error {
@@ -461,22 +552,17 @@ func (gui *Gui) showInitialPopups(tasks []func(chan struct{}) error) {
}()
}
func (gui *Gui) showShamelessSelfPromotionMessage(done chan struct{}) error {
onConfirm := func(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) showIntroPopupMessage(done chan struct{}) error {
onConfirm := func() error {
done <- struct{}{}
return gui.Config.WriteToUserConfig("startupPopupVersion", StartupPopupVersion)
}
return gui.createConfirmationPanel(gui.g, nil, true, gui.Tr.SLocalize("ShamelessSelfPromotionTitle"), gui.Tr.SLocalize("ShamelessSelfPromotionMessage"), onConfirm, onConfirm)
}
func (gui *Gui) promptAnonymousReporting(done chan struct{}) error {
return gui.createConfirmationPanel(gui.g, nil, true, gui.Tr.SLocalize("AnonymousReportingTitle"), gui.Tr.SLocalize("AnonymousReportingPrompt"), func(g *gocui.Gui, v *gocui.View) error {
done <- struct{}{}
return gui.Config.WriteToUserConfig("reporting", "on")
}, func(g *gocui.Gui, v *gocui.View) error {
done <- struct{}{}
return gui.Config.WriteToUserConfig("reporting", "off")
return gui.ask(askOpts{
title: "",
prompt: gui.Tr.SLocalize("IntroPopupMessage"),
handleConfirm: onConfirm,
handleClose: onConfirm,
})
}
@@ -501,12 +587,15 @@ func (gui *Gui) startBackgroundFetch() {
if !isNew {
time.After(60 * time.Second)
}
_, err := gui.fetch(gui.g, gui.g.CurrentView(), false)
err := gui.fetch(false)
if err != nil && strings.Contains(err.Error(), "exit status 128") && isNew {
_ = gui.createConfirmationPanel(gui.g, gui.g.CurrentView(), true, gui.Tr.SLocalize("NoAutomaticGitFetchTitle"), gui.Tr.SLocalize("NoAutomaticGitFetchBody"), nil, nil)
_ = gui.ask(askOpts{
title: gui.Tr.SLocalize("NoAutomaticGitFetchTitle"),
prompt: gui.Tr.SLocalize("NoAutomaticGitFetchBody"),
})
} else {
gui.goEvery(time.Second*60, gui.stopChan, func() error {
_, err := gui.fetch(gui.g, gui.g.CurrentView(), false)
err := gui.fetch(false)
return err
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,143 +1,27 @@
package gui
import (
"fmt"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// getFocusLayout returns a manager function for when view gain and lose focus
func (gui *Gui) getFocusLayout() func(g *gocui.Gui) error {
var previousView *gocui.View
return func(g *gocui.Gui) error {
newView := gui.g.CurrentView()
if err := gui.onFocusChange(); err != nil {
return err
}
// for now we don't consider losing focus to a popup panel as actually losing focus
if newView != previousView && !gui.isPopupPanel(newView.Name()) {
if err := gui.onFocusLost(previousView, newView); err != nil {
return err
}
if err := gui.onFocus(newView); err != nil {
return err
}
previousView = newView
}
return nil
}
}
const SEARCH_PREFIX = "search: "
const INFO_SECTION_PADDING = " "
func (gui *Gui) onFocusChange() error {
currentView := gui.g.CurrentView()
for _, view := range gui.g.Views() {
view.Highlight = view == currentView
}
return nil
}
func (gui *Gui) onFocusLost(v *gocui.View, newView *gocui.View) error {
if v == nil {
return nil
}
if v.IsSearching() && newView.Name() != "search" {
if err := gui.onSearchEscape(); err != nil {
return err
}
}
switch v.Name() {
case "main":
// if we have lost focus to a first-class panel, we need to do some cleanup
gui.changeMainViewsContext("normal")
case "commitFiles":
if gui.State.MainContext != "patch-building" {
if _, err := gui.g.SetViewOnBottom(v.Name()); err != nil {
return err
}
}
}
gui.Log.Info(v.Name() + " focus lost")
return nil
}
func (gui *Gui) onFocus(v *gocui.View) error {
if v == nil {
return nil
}
gui.Log.Info(v.Name() + " focus gained")
return nil
}
func (gui *Gui) getViewHeights() map[string]int {
currView := gui.g.CurrentView()
currentCyclebleView := gui.State.PreviousView
if currView != nil {
viewName := currView.Name()
usePreviousView := true
for _, view := range cyclableViews {
if view == viewName {
currentCyclebleView = viewName
usePreviousView = false
break
}
}
if usePreviousView {
currentCyclebleView = gui.State.PreviousView
func (gui *Gui) informationStr() string {
for _, mode := range gui.modeStatuses() {
if mode.isActive() {
return mode.description()
}
}
// unfortunate result of the fact that these are separate views, have to map explicitly
if currentCyclebleView == "commitFiles" {
currentCyclebleView = "commits"
if gui.g.Mouse {
donate := color.New(color.FgMagenta, color.Underline).Sprint(gui.Tr.SLocalize("Donate"))
return donate + " " + gui.Config.GetVersion()
} else {
return gui.Config.GetVersion()
}
_, height := gui.g.Size()
if gui.State.ScreenMode == SCREEN_FULL || gui.State.ScreenMode == SCREEN_HALF {
vHeights := map[string]int{
"status": 0,
"files": 0,
"branches": 0,
"commits": 0,
"stash": 0,
"options": 0,
}
vHeights[currentCyclebleView] = height - 1
return vHeights
}
usableSpace := height - 7
extraSpace := usableSpace - (usableSpace/3)*3
if height >= 28 {
return map[string]int{
"status": 3,
"files": (usableSpace / 3) + extraSpace,
"branches": usableSpace / 3,
"commits": usableSpace / 3,
"stash": 3,
"options": 1,
}
}
defaultHeight := 3
if height < 21 {
defaultHeight = 1
}
vHeights := map[string]int{
"status": defaultHeight,
"files": defaultHeight,
"branches": defaultHeight,
"commits": defaultHeight,
"stash": defaultHeight,
"options": defaultHeight,
}
vHeights[currentCyclebleView] = height - defaultHeight*4 - 1
return vHeights
}
// layout is called for every screen re-render e.g. when the screen is resized
@@ -145,19 +29,6 @@ func (gui *Gui) layout(g *gocui.Gui) error {
g.Highlight = true
width, height := g.Size()
information := gui.Config.GetVersion()
if gui.g.Mouse {
donate := color.New(color.FgMagenta, color.Underline).Sprint(gui.Tr.SLocalize("Donate"))
information = donate + " " + information
}
if gui.inDiffMode() {
information = utils.ColoredString(fmt.Sprintf("%s %s %s", gui.Tr.SLocalize("showingGitDiff"), "git diff "+gui.diffStr(), utils.ColoredString(gui.Tr.SLocalize("(reset)"), color.Underline)), color.FgMagenta)
} else if gui.inFilterMode() {
information = utils.ColoredString(fmt.Sprintf("%s '%s' %s", gui.Tr.SLocalize("filteringBy"), gui.State.FilterPath, utils.ColoredString(gui.Tr.SLocalize("(reset)"), color.Underline)), color.FgRed, color.Bold)
} else if len(gui.State.CherryPickedCommits) > 0 {
information = utils.ColoredString(fmt.Sprintf("%d commits copied", len(gui.State.CherryPickedCommits)), color.FgCyan)
}
minimumHeight := 9
minimumWidth := 10
if height < minimumHeight || width < minimumWidth {
@@ -173,75 +44,22 @@ func (gui *Gui) layout(g *gocui.Gui) error {
return nil
}
vHeights := gui.getViewHeights()
optionsVersionBoundary := width - max(len(utils.Decolorise(information)), 1)
informationStr := gui.informationStr()
appStatus := gui.statusManager.getStatusString()
appStatusOptionsBoundary := 0
if appStatus != "" {
appStatusOptionsBoundary = len(appStatus) + 2
}
viewDimensions := gui.getWindowDimensions(informationStr, appStatus)
_, _ = g.SetViewOnBottom("limit")
_ = g.DeleteView("limit")
sidePanelWidthRatio := gui.Config.GetUserConfig().GetFloat64("gui.sidePanelWidth")
textColor := theme.GocuiDefaultTextColor
var leftSideWidth int
switch gui.State.ScreenMode {
case SCREEN_NORMAL:
leftSideWidth = int(float64(width) * sidePanelWidthRatio)
case SCREEN_HALF:
leftSideWidth = width/2 - 2
case SCREEN_FULL:
currentView := gui.g.CurrentView()
if currentView != nil && currentView.Name() == "main" {
leftSideWidth = 0
} else {
leftSideWidth = width - 1
}
}
mainPanelLeft := leftSideWidth + 1
mainPanelRight := width - 1
secondaryPanelLeft := width - 1
secondaryPanelTop := 0
mainPanelBottom := height - 2
if gui.State.SplitMainPanel {
if gui.State.ScreenMode == SCREEN_FULL {
mainPanelLeft = 0
panelSplitX := width/2 - 4
mainPanelRight = panelSplitX
secondaryPanelLeft = panelSplitX + 1
} else if width < 220 {
mainPanelBottom = height/2 - 1
secondaryPanelTop = mainPanelBottom + 1
secondaryPanelLeft = leftSideWidth + 1
} else {
units := 5
leftSideWidth = width / units
mainPanelLeft = leftSideWidth + 1
panelSplitX := (1 + ((units - 1) / 2)) * width / units
mainPanelRight = panelSplitX
secondaryPanelLeft = panelSplitX + 1
}
}
main := "main"
secondary := "secondary"
swappingMainPanels := gui.State.Panels.LineByLine != nil && gui.State.Panels.LineByLine.SecondaryFocused
if swappingMainPanels {
main = "secondary"
secondary = "main"
}
// reading more lines into main view buffers upon resize
prevMainView, err := gui.g.View("main")
if err == nil {
_, prevMainHeight := prevMainView.Size()
heightDiff := mainPanelBottom - prevMainHeight - 1
newMainHeight := viewDimensions["main"].Y1 - viewDimensions["main"].Y0 - 1
heightDiff := newMainHeight - prevMainHeight
if heightDiff > 0 {
if manager, ok := gui.viewBufferManagerMap["main"]; ok {
manager.ReadLines(heightDiff)
@@ -252,7 +70,36 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
}
v, err := g.SetView(main, mainPanelLeft, 0, mainPanelRight, mainPanelBottom, gocui.LEFT)
setViewFromDimensions := func(viewName string, windowName string, frame bool) (*gocui.View, error) {
dimensionsObj, ok := viewDimensions[windowName]
if !ok {
// view not specified in dimensions object: so create the view and hide it
// making the view take up the whole space in the background in case it needs
// to render content as soon as it appears, because lazyloaded content (via a pty task)
// cares about the size of the view.
view, err := g.SetView(viewName, 0, 0, width, height, 0)
if err != nil {
return view, err
}
return g.SetViewOnBottom(viewName)
}
frameOffset := 1
if frame {
frameOffset = 0
}
return g.SetView(
viewName,
dimensionsObj.X0-frameOffset,
dimensionsObj.Y0-frameOffset,
dimensionsObj.X1+frameOffset,
dimensionsObj.Y1+frameOffset,
0,
)
}
v, err := setViewFromDimensions("main", "main", true)
if err != nil {
if err.Error() != "unknown view" {
return err
@@ -263,24 +110,20 @@ func (gui *Gui) layout(g *gocui.Gui) error {
v.IgnoreCarriageReturns = true
}
hiddenViewOffset := 9999
hiddenSecondaryPanelOffset := 0
if !gui.State.SplitMainPanel {
hiddenSecondaryPanelOffset = hiddenViewOffset
}
secondaryView, err := g.SetView(secondary, secondaryPanelLeft+hiddenSecondaryPanelOffset, hiddenSecondaryPanelOffset+secondaryPanelTop, width-1+hiddenSecondaryPanelOffset, height-2+hiddenSecondaryPanelOffset, gocui.LEFT)
secondaryView, err := setViewFromDimensions("secondary", "secondary", true)
if err != nil {
if err.Error() != "unknown view" {
return err
}
secondaryView.Title = gui.Tr.SLocalize("DiffTitle")
secondaryView.Wrap = true
secondaryView.FgColor = gocui.ColorWhite
secondaryView.FgColor = textColor
secondaryView.IgnoreCarriageReturns = true
}
if v, err := g.SetView("status", 0, 0, leftSideWidth, vHeights["status"]-1, gocui.BOTTOM|gocui.RIGHT); err != nil {
hiddenViewOffset := 9999
if v, err := setViewFromDimensions("status", "status", true); err != nil {
if err.Error() != "unknown view" {
return err
}
@@ -288,71 +131,58 @@ func (gui *Gui) layout(g *gocui.Gui) error {
v.FgColor = textColor
}
filesView, err := g.SetViewBeneath("files", "status", vHeights["files"])
filesView, err := setViewFromDimensions("files", "files", true)
if err != nil {
if err.Error() != "unknown view" {
return err
}
filesView.Highlight = true
filesView.Title = gui.Tr.SLocalize("FilesTitle")
filesView.SetOnSelectItem(gui.onSelectItemWrapper(gui.onFilesPanelSearchSelect))
filesView.ContainsList = true
}
branchesView, err := g.SetViewBeneath("branches", "files", vHeights["branches"])
branchesView, err := setViewFromDimensions("branches", "branches", true)
if err != nil {
if err.Error() != "unknown view" {
return err
}
branchesView.Title = gui.Tr.SLocalize("BranchesTitle")
branchesView.Tabs = []string{"Local Branches", "Remotes", "Tags"}
branchesView.Tabs = gui.viewTabNames("branches")
branchesView.FgColor = textColor
branchesView.SetOnSelectItem(gui.onSelectItemWrapper(gui.onBranchesPanelSearchSelect))
branchesView.ContainsList = true
}
commitFilesView, err := g.SetViewBeneath("commitFiles", "branches", vHeights["commits"])
commitFilesView, err := setViewFromDimensions("commitFiles", gui.Contexts.CommitFiles.Context.GetWindowName(), true)
if err != nil {
if err.Error() != "unknown view" {
return err
}
commitFilesView.Title = gui.Tr.SLocalize("CommitFiles")
commitFilesView.FgColor = textColor
commitFilesView.SetOnSelectItem(gui.onSelectItemWrapper(gui.onCommitFilesPanelSearchSelect))
commitFilesView.ContainsList = true
}
commitsView, err := g.SetViewBeneath("commits", "branches", vHeights["commits"])
commitsView, err := setViewFromDimensions("commits", "commits", true)
if err != nil {
if err.Error() != "unknown view" {
return err
}
commitsView.Title = gui.Tr.SLocalize("CommitsTitle")
commitsView.Tabs = []string{"Commits", "Reflog"}
commitsView.Tabs = gui.viewTabNames("commits")
commitsView.FgColor = textColor
commitsView.SetOnSelectItem(gui.onSelectItemWrapper(gui.onCommitsPanelSearchSelect))
commitsView.ContainsList = true
}
stashView, err := g.SetViewBeneath("stash", "commits", vHeights["stash"])
stashView, err := setViewFromDimensions("stash", "stash", true)
if err != nil {
if err.Error() != "unknown view" {
return err
}
stashView.Title = gui.Tr.SLocalize("StashTitle")
stashView.FgColor = textColor
stashView.SetOnSelectItem(gui.onSelectItemWrapper(gui.onStashPanelSearchSelect))
stashView.ContainsList = true
}
if v, err := g.SetView("options", appStatusOptionsBoundary-1, height-2, optionsVersionBoundary-1, height, 0); err != nil {
if err.Error() != "unknown view" {
return err
}
v.Frame = false
v.FgColor = theme.OptionsColor
}
if gui.getCommitMessageView() == nil {
// doesn't matter where this view starts because it will be hidden
if commitMessageView, err := g.SetView("commitMessage", hiddenViewOffset, hiddenViewOffset, hiddenViewOffset+10, hiddenViewOffset+10, 0); err != nil {
@@ -373,24 +203,28 @@ func (gui *Gui) layout(g *gocui.Gui) error {
if err.Error() != "unknown view" {
return err
}
_, err := g.SetViewOnBottom("credentials")
if err != nil {
return err
}
_, _ = g.SetViewOnBottom("credentials")
credentialsView.Title = gui.Tr.SLocalize("CredentialsUsername")
credentialsView.FgColor = textColor
credentialsView.Editable = true
}
}
searchViewOffset := hiddenViewOffset
if gui.State.Searching.isSearching {
searchViewOffset = 0
if v, err := setViewFromDimensions("options", "options", false); err != nil {
if err.Error() != "unknown view" {
return err
}
v.Frame = false
v.FgColor = theme.OptionsColor
// doing this here because it'll only happen once
if err := gui.onInitialViewsCreation(); err != nil {
return err
}
}
// this view takes up one character. Its only purpose is to show the slash when searching
searchPrefix := "search: "
if searchPrefixView, err := g.SetView("searchPrefix", appStatusOptionsBoundary-1+searchViewOffset, height-2+searchViewOffset, len(searchPrefix)+searchViewOffset, height+searchViewOffset, 0); err != nil {
if searchPrefixView, err := setViewFromDimensions("searchPrefix", "searchPrefix", false); err != nil {
if err.Error() != "unknown view" {
return err
}
@@ -398,10 +232,10 @@ func (gui *Gui) layout(g *gocui.Gui) error {
searchPrefixView.BgColor = gocui.ColorDefault
searchPrefixView.FgColor = gocui.ColorGreen
searchPrefixView.Frame = false
gui.setViewContent(searchPrefixView, searchPrefix)
gui.setViewContent(searchPrefixView, SEARCH_PREFIX)
}
if searchView, err := g.SetView("search", appStatusOptionsBoundary-1+searchViewOffset+len(searchPrefix), height-2+searchViewOffset, optionsVersionBoundary+searchViewOffset, height+searchViewOffset, 0); err != nil {
if searchView, err := setViewFromDimensions("search", "search", false); err != nil {
if err.Error() != "unknown view" {
return err
}
@@ -412,19 +246,17 @@ func (gui *Gui) layout(g *gocui.Gui) error {
searchView.Editable = true
}
if appStatusView, err := g.SetView("appStatus", -1, height-2, width, height, 0); err != nil {
if appStatusView, err := setViewFromDimensions("appStatus", "appStatus", false); err != nil {
if err.Error() != "unknown view" {
return err
}
appStatusView.BgColor = gocui.ColorDefault
appStatusView.FgColor = gocui.ColorCyan
appStatusView.Frame = false
if _, err := g.SetViewOnBottom("appStatus"); err != nil {
return err
}
_, _ = g.SetViewOnBottom("appStatus")
}
informationView, err := g.SetView("information", optionsVersionBoundary-1, height-2, width, height, 0)
informationView, err := setViewFromDimensions("information", "information", false)
if err != nil {
if err.Error() != "unknown view" {
return err
@@ -432,63 +264,57 @@ func (gui *Gui) layout(g *gocui.Gui) error {
informationView.BgColor = gocui.ColorDefault
informationView.FgColor = gocui.ColorGreen
informationView.Frame = false
gui.renderString(g, "information", information)
// doing this here because it'll only happen once
if err := gui.onInitialViewsCreation(); err != nil {
return err
}
gui.renderString("information", INFO_SECTION_PADDING+informationStr)
}
if gui.State.OldInformation != information {
gui.setViewContent(informationView, information)
gui.State.OldInformation = information
if gui.State.OldInformation != informationStr {
gui.setViewContent(informationView, informationStr)
gui.State.OldInformation = informationStr
}
if gui.g.CurrentView() == nil {
initialView := gui.getFilesView()
if gui.inFilterMode() {
initialView = gui.getCommitsView()
}
if _, err := gui.g.SetCurrentView(initialView.Name()); err != nil {
return err
initialContext := gui.Contexts.Files.Context
if gui.State.Modes.Filtering.Active() {
initialContext = gui.Contexts.BranchCommits.Context
}
if err := gui.switchFocus(gui.g, nil, initialView); err != nil {
if err := gui.switchContext(initialContext); err != nil {
return err
}
}
type listViewState struct {
selectedLine int
lineCount int
view *gocui.View
context string
type listContextState struct {
view *gocui.View
listContext *ListContext
}
listViews := []listViewState{
{view: filesView, context: "", selectedLine: gui.State.Panels.Files.SelectedLine, lineCount: len(gui.State.Files)},
{view: branchesView, context: "local-branches", selectedLine: gui.State.Panels.Branches.SelectedLine, lineCount: len(gui.State.Branches)},
{view: branchesView, context: "remotes", selectedLine: gui.State.Panels.Remotes.SelectedLine, lineCount: len(gui.State.Remotes)},
{view: branchesView, context: "remote-branches", selectedLine: gui.State.Panels.RemoteBranches.SelectedLine, lineCount: len(gui.State.Remotes)},
{view: commitsView, context: "branch-commits", selectedLine: gui.State.Panels.Commits.SelectedLine, lineCount: len(gui.State.Commits)},
{view: commitsView, context: "reflog-commits", selectedLine: gui.State.Panels.ReflogCommits.SelectedLine, lineCount: len(gui.State.FilteredReflogCommits)},
{view: stashView, context: "", selectedLine: gui.State.Panels.Stash.SelectedLine, lineCount: len(gui.State.StashEntries)},
{view: commitFilesView, context: "", selectedLine: gui.State.Panels.CommitFiles.SelectedLine, lineCount: len(gui.State.CommitFiles)},
listContextStates := []listContextState{
{view: filesView, listContext: gui.filesListContext()},
{view: branchesView, listContext: gui.branchesListContext()},
{view: branchesView, listContext: gui.remotesListContext()},
{view: branchesView, listContext: gui.remoteBranchesListContext()},
{view: branchesView, listContext: gui.tagsListContext()},
{view: commitsView, listContext: gui.branchCommitsListContext()},
{view: commitsView, listContext: gui.reflogCommitsListContext()},
{view: stashView, listContext: gui.stashListContext()},
{view: commitFilesView, listContext: gui.commitFilesListContext()},
}
// menu view might not exist so we check to be safe
if menuView, err := gui.g.View("menu"); err == nil {
listViews = append(listViews, listViewState{view: menuView, context: "", selectedLine: gui.State.Panels.Menu.SelectedLine, lineCount: gui.State.MenuItemCount})
listContextStates = append(listContextStates, listContextState{view: menuView, listContext: gui.menuListContext()})
}
for _, listView := range listViews {
// ignore views where the context doesn't match up with the selected line we're trying to focus
if listView.context != "" && (listView.view.Context != listView.context) {
for _, listContextState := range listContextStates {
// ignore contexts whose view is owned by another context right now
if listContextState.view.Context != listContextState.listContext.GetKey() {
continue
}
// check if the selected line is now out of view and if so refocus it
listView.view.FocusPoint(0, listView.selectedLine)
listContextState.view.FocusPoint(0, listContextState.listContext.GetPanelState().GetSelectedLineIdx())
listView.view.SelBgColor = theme.GocuiSelectedLineBgColor
listContextState.view.SelBgColor = theme.GocuiSelectedLineBgColor
// I doubt this is expensive though it's admittedly redundant after the first render
listContextState.view.SetOnSelectItem(gui.onSelectItemWrapper(listContextState.listContext.onSearchSelect))
}
mainViewWidth, mainViewHeight := gui.getMainView().Size()
@@ -504,14 +330,26 @@ func (gui *Gui) layout(g *gocui.Gui) error {
// if you download humanlog and do tail -f development.log | humanlog
// this will let you see these branches as prettified json
// gui.Log.Info(utils.AsJson(gui.State.Branches[0:4]))
return gui.resizeCurrentPopupPanel(g)
return gui.resizeCurrentPopupPanel()
}
func (gui *Gui) onInitialViewsCreation() error {
gui.changeMainViewsContext("normal")
gui.setInitialViewContexts()
gui.getBranchesView().Context = "local-branches"
gui.getCommitsView().Context = "branch-commits"
if err := gui.switchContext(gui.defaultSideContext()); err != nil {
return err
}
if err := gui.keybindings(); err != nil {
return err
}
if gui.showRecentRepos {
if err := gui.handleCreateRecentReposMenu(); err != nil {
return err
}
gui.showRecentRepos = false
}
return gui.loadNewRepo()
}

View File

@@ -1,8 +1,11 @@
package gui
import (
"fmt"
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
)
// Currently there are two 'pseudo-panels' that make use of this 'pseudo-panel'.
@@ -24,7 +27,7 @@ const (
func (gui *Gui) refreshLineByLinePanel(diff string, secondaryDiff string, secondaryFocused bool, selectedLineIdx int) (bool, error) {
state := gui.State.Panels.LineByLine
patchParser, err := commands.NewPatchParser(gui.Log, diff)
patchParser, err := patch.NewPatchParser(gui.Log, diff)
if err != nil {
return false, nil
}
@@ -50,7 +53,7 @@ func (gui *Gui) refreshLineByLinePanel(diff string, secondaryDiff string, second
prevNewHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0)
selectedLineIdx = patchParser.GetNextStageableLineIndex(prevNewHunk.FirstLineIdx)
newHunk := patchParser.GetHunkContainingLine(selectedLineIdx, 0)
firstLineIdx, lastLineIdx = newHunk.FirstLineIdx, newHunk.LastLineIdx
firstLineIdx, lastLineIdx = newHunk.FirstLineIdx, newHunk.LastLineIdx()
} else {
selectedLineIdx = patchParser.GetNextStageableLineIndex(state.SelectedLineIdx)
firstLineIdx, lastLineIdx = selectedLineIdx, selectedLineIdx
@@ -70,7 +73,7 @@ func (gui *Gui) refreshLineByLinePanel(diff string, secondaryDiff string, second
SecondaryFocused: secondaryFocused,
}
if err := gui.refreshMainView(); err != nil {
if err := gui.refreshMainViewForLineByLine(); err != nil {
return false, err
}
@@ -82,7 +85,7 @@ func (gui *Gui) refreshLineByLinePanel(diff string, secondaryDiff string, second
secondaryView.Highlight = true
secondaryView.Wrap = false
secondaryPatchParser, err := commands.NewPatchParser(gui.Log, secondaryDiff)
secondaryPatchParser, err := patch.NewPatchParser(gui.Log, secondaryDiff)
if err != nil {
return false, nil
}
@@ -117,16 +120,16 @@ func (gui *Gui) handleSelectNextHunk(g *gocui.Gui, v *gocui.View) error {
return gui.selectNewHunk(newHunk)
}
func (gui *Gui) selectNewHunk(newHunk *commands.PatchHunk) error {
func (gui *Gui) selectNewHunk(newHunk *patch.PatchHunk) error {
state := gui.State.Panels.LineByLine
state.SelectedLineIdx = state.PatchParser.GetNextStageableLineIndex(newHunk.FirstLineIdx)
if state.SelectMode == HUNK {
state.FirstLineIdx, state.LastLineIdx = newHunk.FirstLineIdx, newHunk.LastLineIdx
state.FirstLineIdx, state.LastLineIdx = newHunk.FirstLineIdx, newHunk.LastLineIdx()
} else {
state.FirstLineIdx, state.LastLineIdx = state.SelectedLineIdx, state.SelectedLineIdx
}
if err := gui.refreshMainView(); err != nil {
if err := gui.refreshMainViewForLineByLine(); err != nil {
return err
}
@@ -166,7 +169,7 @@ func (gui *Gui) handleSelectNewLine(newSelectedLineIdx int) error {
state.FirstLineIdx = state.SelectedLineIdx
}
if err := gui.refreshMainView(); err != nil {
if err := gui.refreshMainViewForLineByLine(); err != nil {
return err
}
@@ -222,18 +225,22 @@ func (gui *Gui) handleMouseScrollDown(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) getSelectedCommitFileName() string {
return gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLine].Name
return gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLineIdx].Name
}
func (gui *Gui) refreshMainView() error {
func (gui *Gui) refreshMainViewForLineByLine() error {
state := gui.State.Panels.LineByLine
var includedLineIndices []int
// I'd prefer not to have knowledge of contexts using this file but I'm not sure
// how to get around this
if gui.State.MainContext == "patch-building" {
if gui.currentContext().GetKey() == gui.Contexts.PatchBuilding.Context.GetKey() {
filename := gui.getSelectedCommitFileName()
includedLineIndices = gui.GitCommand.PatchManager.GetFileIncLineIndices(filename)
var err error
includedLineIndices, err = gui.GitCommand.PatchManager.GetFileIncLineIndices(filename)
if err != nil {
return err
}
}
colorDiff := state.PatchParser.Render(state.FirstLineIdx, state.LastLineIdx, includedLineIndices)
@@ -265,7 +272,7 @@ func (gui *Gui) focusSelection(includeCurrentHunk bool) error {
if includeCurrentHunk {
hunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0)
firstLineIdx = hunk.FirstLineIdx
lastLineIdx = hunk.LastLineIdx
lastLineIdx = hunk.LastLineIdx()
}
margin := 0 // we may want to have a margin in place to show context but right now I'm thinking we keep this at zero
@@ -299,7 +306,7 @@ func (gui *Gui) handleToggleSelectRange(g *gocui.Gui, v *gocui.View) error {
}
state.FirstLineIdx, state.LastLineIdx = state.SelectedLineIdx, state.SelectedLineIdx
return gui.refreshMainView()
return gui.refreshMainViewForLineByLine()
}
func (gui *Gui) handleToggleSelectHunk(g *gocui.Gui, v *gocui.View) error {
@@ -311,10 +318,10 @@ func (gui *Gui) handleToggleSelectHunk(g *gocui.Gui, v *gocui.View) error {
} else {
state.SelectMode = HUNK
selectedHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0)
state.FirstLineIdx, state.LastLineIdx = selectedHunk.FirstLineIdx, selectedHunk.LastLineIdx
state.FirstLineIdx, state.LastLineIdx = selectedHunk.FirstLineIdx, selectedHunk.LastLineIdx()
}
if err := gui.refreshMainView(); err != nil {
if err := gui.refreshMainViewForLineByLine(); err != nil {
return err
}
@@ -322,6 +329,33 @@ func (gui *Gui) handleToggleSelectHunk(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleEscapeLineByLinePanel() {
gui.changeMainViewsContext("normal")
gui.State.Panels.LineByLine = nil
}
func (gui *Gui) handleOpenFileAtLine() error {
// again, would be good to use inheritance here (or maybe even composition)
var filename string
switch gui.State.MainContext {
case gui.Contexts.PatchBuilding.Context.GetKey():
filename = gui.getSelectedCommitFileName()
case gui.Contexts.Staging.Context.GetKey():
file := gui.getSelectedFile()
if file == nil {
return nil
}
filename = file.Name
default:
return errors.Errorf("unknown main context: %s", gui.State.MainContext)
}
state := gui.State.Panels.LineByLine
// need to look at current index, then work out what my hunk's header information is, and see how far my line is away from the hunk header
selectedHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0)
lineNumber := selectedHunk.LineNumberOfLine(state.SelectedLineIdx)
filenameWithLineNum := fmt.Sprintf("%s:%d", filename, lineNumber)
if err := gui.OSCommand.OpenFile(filenameWithLineNum); err != nil {
return err
}
return nil
}

526
pkg/gui/list_context.go Normal file
View File

@@ -0,0 +1,526 @@
package gui
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
)
type ListContext struct {
ViewName string
ContextKey string
GetItemsLength func() int
GetDisplayStrings func() [][]string
OnFocus func() error
OnFocusLost func() error
OnClickSelectedItem func() error
OnGetOptionsMap func() map[string]string
// the boolean here tells us whether the item is nil. This is needed because you can't work it out on the calling end once the pointer is wrapped in an interface (unless you want to use reflection)
SelectedItem func() (ListItem, bool)
GetPanelState func() IListPanelState
Gui *Gui
ResetMainViewOriginOnFocus bool
Kind int
ParentContext Context
// we can't know on the calling end whether a Context is actually a nil value without reflection, so we're storing this flag here to tell us. There has got to be a better way around this.
hasParent bool
WindowName string
}
type ListItem interface {
// ID is a SHA when the item is a commit, a filename when the item is a file, 'stash@{4}' when it's a stash entry, 'my_branch' when it's a branch
ID() string
// Description is something we would show in a message e.g. '123as14: push blah' for a commit
Description() string
}
func (lc *ListContext) GetSelectedItem() (ListItem, bool) {
return lc.SelectedItem()
}
func (lc *ListContext) SetWindowName(windowName string) {
lc.WindowName = windowName
}
func (lc *ListContext) GetWindowName() string {
windowName := lc.WindowName
if windowName != "" {
return windowName
}
// TODO: actually set this for everything so we don't default to the view name
return lc.ViewName
}
func (lc *ListContext) SetParentContext(c Context) {
lc.ParentContext = c
lc.hasParent = true
}
func (lc *ListContext) GetParentContext() (Context, bool) {
return lc.ParentContext, lc.hasParent
}
func (lc *ListContext) GetSelectedItemId() string {
item, ok := lc.SelectedItem()
if !ok {
return ""
}
return item.ID()
}
func (lc *ListContext) GetOptionsMap() map[string]string {
if lc.OnGetOptionsMap != nil {
return lc.OnGetOptionsMap()
}
return nil
}
// OnFocus assumes that the content of the context has already been rendered to the view. OnRender is the function which actually renders the content to the view
func (lc *ListContext) OnRender() error {
view, err := lc.Gui.g.View(lc.ViewName)
if err != nil {
return nil
}
if lc.GetDisplayStrings != nil {
lc.Gui.refreshSelectedLine(lc.GetPanelState(), lc.GetItemsLength())
lc.Gui.renderDisplayStrings(view, lc.GetDisplayStrings())
}
return nil
}
func (lc *ListContext) GetKey() string {
return lc.ContextKey
}
func (lc *ListContext) GetKind() int {
return lc.Kind
}
func (lc *ListContext) GetViewName() string {
return lc.ViewName
}
func (lc *ListContext) HandleFocusLost() error {
if lc.OnFocusLost != nil {
return lc.OnFocusLost()
}
return nil
}
func (lc *ListContext) HandleFocus() error {
if lc.Gui.popupPanelFocused() {
return nil
}
view, err := lc.Gui.g.View(lc.ViewName)
if err != nil {
return nil
}
view.FocusPoint(0, lc.GetPanelState().GetSelectedLineIdx())
if lc.Gui.State.Modes.Diffing.Active() {
return lc.Gui.renderDiff()
}
if lc.OnFocus != nil {
return lc.OnFocus()
}
return nil
}
func (lc *ListContext) HandleRender() error {
return lc.OnRender()
}
func (lc *ListContext) handlePrevLine(g *gocui.Gui, v *gocui.View) error {
return lc.handleLineChange(-1)
}
func (lc *ListContext) handleNextLine(g *gocui.Gui, v *gocui.View) error {
return lc.handleLineChange(1)
}
func (lc *ListContext) handleLineChange(change int) error {
if !lc.Gui.isPopupPanel(lc.ViewName) && lc.Gui.popupPanelFocused() {
return nil
}
view, err := lc.Gui.g.View(lc.ViewName)
if err != nil {
return err
}
lc.Gui.changeSelectedLine(lc.GetPanelState(), lc.GetItemsLength(), change)
view.FocusPoint(0, lc.GetPanelState().GetSelectedLineIdx())
if lc.ResetMainViewOriginOnFocus {
if err := lc.Gui.resetOrigin(lc.Gui.getMainView()); err != nil {
return err
}
if err := lc.Gui.resetOrigin(lc.Gui.getSecondaryView()); err != nil {
return err
}
}
return lc.HandleFocus()
}
func (lc *ListContext) handleNextPage(g *gocui.Gui, v *gocui.View) error {
view, err := lc.Gui.g.View(lc.ViewName)
if err != nil {
return nil
}
_, height := view.Size()
delta := height - 1
if delta == 0 {
delta = 1
}
return lc.handleLineChange(delta)
}
func (lc *ListContext) handleGotoTop(g *gocui.Gui, v *gocui.View) error {
return lc.handleLineChange(-lc.GetItemsLength())
}
func (lc *ListContext) handleGotoBottom(g *gocui.Gui, v *gocui.View) error {
return lc.handleLineChange(lc.GetItemsLength())
}
func (lc *ListContext) handlePrevPage(g *gocui.Gui, v *gocui.View) error {
view, err := lc.Gui.g.View(lc.ViewName)
if err != nil {
return nil
}
_, height := view.Size()
delta := height - 1
if delta == 0 {
delta = 1
}
return lc.handleLineChange(-delta)
}
func (lc *ListContext) handleClick(g *gocui.Gui, v *gocui.View) error {
if !lc.Gui.isPopupPanel(lc.ViewName) && lc.Gui.popupPanelFocused() {
return nil
}
prevSelectedLineIdx := lc.GetPanelState().GetSelectedLineIdx()
newSelectedLineIdx := v.SelectedLineIdx()
// we need to focus the view
if err := lc.Gui.switchContext(lc); err != nil {
return err
}
if newSelectedLineIdx > lc.GetItemsLength()-1 {
return nil
}
lc.GetPanelState().SetSelectedLineIdx(newSelectedLineIdx)
prevViewName := lc.Gui.currentViewName()
if prevSelectedLineIdx == newSelectedLineIdx && prevViewName == lc.ViewName && lc.OnClickSelectedItem != nil {
return lc.OnClickSelectedItem()
}
return lc.HandleFocus()
}
func (lc *ListContext) onSearchSelect(selectedLineIdx int) error {
lc.GetPanelState().SetSelectedLineIdx(selectedLineIdx)
return lc.HandleFocus()
}
func (gui *Gui) menuListContext() *ListContext {
return &ListContext{
ViewName: "menu",
ContextKey: "menu",
GetItemsLength: func() int { return gui.getMenuView().LinesHeight() },
GetPanelState: func() IListPanelState { return gui.State.Panels.Menu },
OnFocus: gui.handleMenuSelect,
OnClickSelectedItem: func() error { return gui.onMenuPress() },
Gui: gui,
ResetMainViewOriginOnFocus: false,
Kind: PERSISTENT_POPUP,
OnGetOptionsMap: gui.getMenuOptions,
// no GetDisplayStrings field because we do a custom render on menu creation
}
}
func (gui *Gui) filesListContext() *ListContext {
return &ListContext{
ViewName: "files",
ContextKey: FILES_CONTEXT_KEY,
GetItemsLength: func() int { return len(gui.State.Files) },
GetPanelState: func() IListPanelState { return gui.State.Panels.Files },
OnFocus: gui.focusAndSelectFile,
OnClickSelectedItem: gui.handleFilePress,
Gui: gui,
ResetMainViewOriginOnFocus: false,
Kind: SIDE_CONTEXT,
GetDisplayStrings: func() [][]string {
return presentation.GetFileListDisplayStrings(gui.State.Files, gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedFile()
return item, item != nil
},
}
}
func (gui *Gui) branchesListContext() *ListContext {
return &ListContext{
ViewName: "branches",
ContextKey: LOCAL_BRANCHES_CONTEXT_KEY,
GetItemsLength: func() int { return len(gui.State.Branches) },
GetPanelState: func() IListPanelState { return gui.State.Panels.Branches },
OnFocus: gui.handleBranchSelect,
Gui: gui,
ResetMainViewOriginOnFocus: true,
Kind: SIDE_CONTEXT,
GetDisplayStrings: func() [][]string {
return presentation.GetBranchListDisplayStrings(gui.State.Branches, gui.State.ScreenMode != SCREEN_NORMAL, gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedBranch()
return item, item != nil
},
}
}
func (gui *Gui) remotesListContext() *ListContext {
return &ListContext{
ViewName: "branches",
ContextKey: REMOTES_CONTEXT_KEY,
GetItemsLength: func() int { return len(gui.State.Remotes) },
GetPanelState: func() IListPanelState { return gui.State.Panels.Remotes },
OnFocus: gui.handleRemoteSelect,
OnClickSelectedItem: gui.handleRemoteEnter,
Gui: gui,
ResetMainViewOriginOnFocus: true,
Kind: SIDE_CONTEXT,
GetDisplayStrings: func() [][]string {
return presentation.GetRemoteListDisplayStrings(gui.State.Remotes, gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedRemote()
return item, item != nil
},
}
}
func (gui *Gui) remoteBranchesListContext() *ListContext {
return &ListContext{
ViewName: "branches",
ContextKey: REMOTE_BRANCHES_CONTEXT_KEY,
GetItemsLength: func() int { return len(gui.State.RemoteBranches) },
GetPanelState: func() IListPanelState { return gui.State.Panels.RemoteBranches },
OnFocus: gui.handleRemoteBranchSelect,
Gui: gui,
ResetMainViewOriginOnFocus: true,
Kind: SIDE_CONTEXT,
GetDisplayStrings: func() [][]string {
return presentation.GetRemoteBranchListDisplayStrings(gui.State.RemoteBranches, gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedRemoteBranch()
return item, item != nil
},
}
}
func (gui *Gui) tagsListContext() *ListContext {
return &ListContext{
ViewName: "branches",
ContextKey: TAGS_CONTEXT_KEY,
GetItemsLength: func() int { return len(gui.State.Tags) },
GetPanelState: func() IListPanelState { return gui.State.Panels.Tags },
OnFocus: gui.handleTagSelect,
Gui: gui,
ResetMainViewOriginOnFocus: true,
Kind: SIDE_CONTEXT,
GetDisplayStrings: func() [][]string {
return presentation.GetTagListDisplayStrings(gui.State.Tags, gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedTag()
return item, item != nil
},
}
}
func (gui *Gui) branchCommitsListContext() *ListContext {
return &ListContext{
ViewName: "commits",
ContextKey: BRANCH_COMMITS_CONTEXT_KEY,
GetItemsLength: func() int { return len(gui.State.Commits) },
GetPanelState: func() IListPanelState { return gui.State.Panels.Commits },
OnFocus: gui.handleCommitSelect,
OnClickSelectedItem: gui.handleViewCommitFiles,
Gui: gui,
ResetMainViewOriginOnFocus: true,
Kind: SIDE_CONTEXT,
GetDisplayStrings: func() [][]string {
return presentation.GetCommitListDisplayStrings(gui.State.Commits, gui.State.ScreenMode != SCREEN_NORMAL, gui.cherryPickedCommitShaMap(), gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedLocalCommit()
return item, item != nil
},
}
}
func (gui *Gui) reflogCommitsListContext() *ListContext {
return &ListContext{
ViewName: "commits",
ContextKey: REFLOG_COMMITS_CONTEXT_KEY,
GetItemsLength: func() int { return len(gui.State.FilteredReflogCommits) },
GetPanelState: func() IListPanelState { return gui.State.Panels.ReflogCommits },
OnFocus: gui.handleReflogCommitSelect,
Gui: gui,
ResetMainViewOriginOnFocus: true,
Kind: SIDE_CONTEXT,
GetDisplayStrings: func() [][]string {
return presentation.GetReflogCommitListDisplayStrings(gui.State.FilteredReflogCommits, gui.State.ScreenMode != SCREEN_NORMAL, gui.cherryPickedCommitShaMap(), gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedReflogCommit()
return item, item != nil
},
}
}
func (gui *Gui) subCommitsListContext() *ListContext {
return &ListContext{
ViewName: "branches",
ContextKey: SUB_COMMITS_CONTEXT_KEY,
GetItemsLength: func() int { return len(gui.State.SubCommits) },
GetPanelState: func() IListPanelState { return gui.State.Panels.SubCommits },
OnFocus: gui.handleSubCommitSelect,
Gui: gui,
ResetMainViewOriginOnFocus: true,
Kind: SIDE_CONTEXT,
GetDisplayStrings: func() [][]string {
gui.Log.Warn("getting display strings for sub commits")
return presentation.GetCommitListDisplayStrings(gui.State.SubCommits, gui.State.ScreenMode != SCREEN_NORMAL, gui.cherryPickedCommitShaMap(), gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedSubCommit()
return item, item != nil
},
}
}
func (gui *Gui) stashListContext() *ListContext {
return &ListContext{
ViewName: "stash",
ContextKey: STASH_CONTEXT_KEY,
GetItemsLength: func() int { return len(gui.State.StashEntries) },
GetPanelState: func() IListPanelState { return gui.State.Panels.Stash },
OnFocus: gui.handleStashEntrySelect,
Gui: gui,
ResetMainViewOriginOnFocus: true,
Kind: SIDE_CONTEXT,
GetDisplayStrings: func() [][]string {
return presentation.GetStashEntryListDisplayStrings(gui.State.StashEntries, gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedStashEntry()
return item, item != nil
},
}
}
func (gui *Gui) commitFilesListContext() *ListContext {
return &ListContext{
ViewName: "commitFiles",
WindowName: "commits",
ContextKey: COMMIT_FILES_CONTEXT_KEY,
GetItemsLength: func() int { return len(gui.State.CommitFiles) },
GetPanelState: func() IListPanelState { return gui.State.Panels.CommitFiles },
OnFocus: gui.handleCommitFileSelect,
Gui: gui,
ResetMainViewOriginOnFocus: true,
Kind: SIDE_CONTEXT,
GetDisplayStrings: func() [][]string {
return presentation.GetCommitFileListDisplayStrings(gui.State.CommitFiles, gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedCommitFile()
return item, item != nil
},
}
}
func (gui *Gui) getListContexts() []*ListContext {
return []*ListContext{
gui.menuListContext(),
gui.filesListContext(),
gui.branchesListContext(),
gui.remotesListContext(),
gui.remoteBranchesListContext(),
gui.tagsListContext(),
gui.branchCommitsListContext(),
gui.reflogCommitsListContext(),
gui.subCommitsListContext(),
gui.stashListContext(),
gui.commitFilesListContext(),
}
}
func (gui *Gui) getListContextKeyBindings() []*Binding {
bindings := make([]*Binding, 0)
for _, listContext := range gui.getListContexts() {
bindings = append(bindings, []*Binding{
{ViewName: listContext.ViewName, Contexts: []string{listContext.ContextKey}, Key: gui.getKey("universal.prevItem-alt"), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
{ViewName: listContext.ViewName, Contexts: []string{listContext.ContextKey}, Key: gui.getKey("universal.prevItem"), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
{ViewName: listContext.ViewName, Contexts: []string{listContext.ContextKey}, Key: gocui.MouseWheelUp, Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
{ViewName: listContext.ViewName, Contexts: []string{listContext.ContextKey}, Key: gui.getKey("universal.nextItem-alt"), Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
{ViewName: listContext.ViewName, Contexts: []string{listContext.ContextKey}, Key: gui.getKey("universal.nextItem"), Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
{ViewName: listContext.ViewName, Contexts: []string{listContext.ContextKey}, Key: gui.getKey("universal.prevPage"), Modifier: gocui.ModNone, Handler: listContext.handlePrevPage, Description: gui.Tr.SLocalize("prevPage")},
{ViewName: listContext.ViewName, Contexts: []string{listContext.ContextKey}, Key: gui.getKey("universal.nextPage"), Modifier: gocui.ModNone, Handler: listContext.handleNextPage, Description: gui.Tr.SLocalize("nextPage")},
{ViewName: listContext.ViewName, Contexts: []string{listContext.ContextKey}, Key: gui.getKey("universal.gotoTop"), Modifier: gocui.ModNone, Handler: listContext.handleGotoTop, Description: gui.Tr.SLocalize("gotoTop")},
{ViewName: listContext.ViewName, Contexts: []string{listContext.ContextKey}, Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
{ViewName: listContext.ViewName, Contexts: []string{listContext.ContextKey}, Key: gocui.MouseLeft, Modifier: gocui.ModNone, Handler: listContext.handleClick},
}...)
// the commits panel needs to lazyload things so it has a couple of its own handlers
openSearchHandler := gui.handleOpenSearch
gotoBottomHandler := listContext.handleGotoBottom
if listContext.ViewName == "commits" {
openSearchHandler = gui.handleOpenSearchForCommitsPanel
gotoBottomHandler = gui.handleGotoBottomForCommitsPanel
}
bindings = append(bindings, []*Binding{
{
ViewName: listContext.ViewName,
Contexts: []string{listContext.ContextKey},
Key: gui.getKey("universal.startSearch"),
Handler: openSearchHandler,
Description: gui.Tr.SLocalize("startSearch"),
},
{
ViewName: listContext.ViewName,
Contexts: []string{listContext.ContextKey},
Key: gui.getKey("universal.gotoBottom"),
Handler: gotoBottomHandler,
Description: gui.Tr.SLocalize("gotoBottom"),
},
}...)
}
return bindings
}

View File

@@ -1,212 +0,0 @@
package gui
import "github.com/jesseduffield/gocui"
type listView struct {
viewName string
context string
getItemsLength func() int
getSelectedLineIdxPtr func() *int
handleFocus func(g *gocui.Gui, v *gocui.View) error
handleItemSelect func(g *gocui.Gui, v *gocui.View) error
handleClickSelectedItem func(g *gocui.Gui, v *gocui.View) error
gui *Gui
rendersToMainView bool
}
func (lv *listView) handlePrevLine(g *gocui.Gui, v *gocui.View) error {
return lv.handleLineChange(-1)
}
func (lv *listView) handleNextLine(g *gocui.Gui, v *gocui.View) error {
return lv.handleLineChange(1)
}
func (lv *listView) handleLineChange(change int) error {
if !lv.gui.isPopupPanel(lv.viewName) && lv.gui.popupPanelFocused() {
return nil
}
lv.gui.changeSelectedLine(lv.getSelectedLineIdxPtr(), lv.getItemsLength(), change)
if lv.rendersToMainView {
if err := lv.gui.resetOrigin(lv.gui.getMainView()); err != nil {
return err
}
}
view, err := lv.gui.g.View(lv.viewName)
if err != nil {
return err
}
return lv.handleItemSelect(lv.gui.g, view)
}
func (lv *listView) handleNextPage(g *gocui.Gui, v *gocui.View) error {
view, err := lv.gui.g.View(lv.viewName)
if err != nil {
return nil
}
_, height := view.Size()
delta := height - 1
if delta == 0 {
delta = 1
}
return lv.handleLineChange(delta)
}
func (lv *listView) handleGotoTop(g *gocui.Gui, v *gocui.View) error {
return lv.handleLineChange(-lv.getItemsLength())
}
func (lv *listView) handleGotoBottom(g *gocui.Gui, v *gocui.View) error {
return lv.handleLineChange(lv.getItemsLength())
}
func (lv *listView) handlePrevPage(g *gocui.Gui, v *gocui.View) error {
view, err := lv.gui.g.View(lv.viewName)
if err != nil {
return nil
}
_, height := view.Size()
delta := height - 1
if delta == 0 {
delta = 1
}
return lv.handleLineChange(-delta)
}
func (lv *listView) handleClick(g *gocui.Gui, v *gocui.View) error {
if !lv.gui.isPopupPanel(lv.viewName) && lv.gui.popupPanelFocused() {
return nil
}
selectedLineIdxPtr := lv.getSelectedLineIdxPtr()
prevSelectedLineIdx := *selectedLineIdxPtr
newSelectedLineIdx := v.SelectedLineIdx()
if newSelectedLineIdx > lv.getItemsLength()-1 {
return lv.handleFocus(lv.gui.g, v)
}
*selectedLineIdxPtr = newSelectedLineIdx
if lv.rendersToMainView {
if err := lv.gui.resetOrigin(lv.gui.getMainView()); err != nil {
return err
}
}
prevViewName := lv.gui.currentViewName()
if prevSelectedLineIdx == newSelectedLineIdx && prevViewName == lv.viewName && lv.handleClickSelectedItem != nil {
return lv.handleClickSelectedItem(lv.gui.g, v)
}
return lv.handleItemSelect(lv.gui.g, v)
}
func (gui *Gui) getListViews() []*listView {
return []*listView{
{
viewName: "menu",
getItemsLength: func() int { return gui.getMenuView().LinesHeight() },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Menu.SelectedLine },
handleFocus: gui.handleMenuSelect,
handleItemSelect: gui.handleMenuSelect,
// need to add a layer of indirection here because the callback changes during runtime
handleClickSelectedItem: gui.wrappedHandler(func() error { return gui.State.Panels.Menu.OnPress(gui.g, nil) }),
gui: gui,
rendersToMainView: false,
},
{
viewName: "files",
getItemsLength: func() int { return len(gui.State.Files) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Files.SelectedLine },
handleFocus: gui.focusAndSelectFile,
handleItemSelect: gui.focusAndSelectFile,
handleClickSelectedItem: gui.handleFilePress,
gui: gui,
rendersToMainView: true,
},
{
viewName: "branches",
context: "local-branches",
getItemsLength: func() int { return len(gui.State.Branches) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Branches.SelectedLine },
handleFocus: gui.handleBranchSelect,
handleItemSelect: gui.handleBranchSelect,
gui: gui,
rendersToMainView: true,
},
{
viewName: "branches",
context: "remotes",
getItemsLength: func() int { return len(gui.State.Remotes) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Remotes.SelectedLine },
handleFocus: gui.wrappedHandler(gui.renderRemotesWithSelection),
handleItemSelect: gui.handleRemoteSelect,
handleClickSelectedItem: gui.handleRemoteEnter,
gui: gui,
rendersToMainView: true,
},
{
viewName: "branches",
context: "remote-branches",
getItemsLength: func() int { return len(gui.State.RemoteBranches) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.RemoteBranches.SelectedLine },
handleFocus: gui.handleRemoteBranchSelect,
handleItemSelect: gui.handleRemoteBranchSelect,
gui: gui,
rendersToMainView: true,
},
{
viewName: "branches",
context: "tags",
getItemsLength: func() int { return len(gui.State.Tags) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Tags.SelectedLine },
handleFocus: gui.handleTagSelect,
handleItemSelect: gui.handleTagSelect,
gui: gui,
rendersToMainView: true,
},
{
viewName: "commits",
context: "branch-commits",
getItemsLength: func() int { return len(gui.State.Commits) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Commits.SelectedLine },
handleFocus: gui.handleCommitSelect,
handleItemSelect: gui.handleCommitSelect,
handleClickSelectedItem: gui.handleSwitchToCommitFilesPanel,
gui: gui,
rendersToMainView: true,
},
{
viewName: "commits",
context: "reflog-commits",
getItemsLength: func() int { return len(gui.State.FilteredReflogCommits) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.ReflogCommits.SelectedLine },
handleFocus: gui.handleReflogCommitSelect,
handleItemSelect: gui.handleReflogCommitSelect,
gui: gui,
rendersToMainView: true,
},
{
viewName: "stash",
getItemsLength: func() int { return len(gui.State.StashEntries) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Stash.SelectedLine },
handleFocus: gui.handleStashEntrySelect,
handleItemSelect: gui.handleStashEntrySelect,
gui: gui,
rendersToMainView: true,
},
{
viewName: "commitFiles",
getItemsLength: func() int { return len(gui.State.CommitFiles) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.CommitFiles.SelectedLine },
handleFocus: gui.handleCommitFileSelect,
handleItemSelect: gui.handleCommitFileSelect,
gui: gui,
rendersToMainView: true,
},
}
}

174
pkg/gui/main_panels.go Normal file
View File

@@ -0,0 +1,174 @@
package gui
import "os/exec"
type viewUpdateOpts struct {
title string
// awkwardly calling this noWrap because of how hard Go makes it to have
// a boolean option that defaults to true
noWrap bool
highlight bool
task updateTask
}
type coordinates struct {
x int
y int
}
type refreshMainOpts struct {
main *viewUpdateOpts
secondary *viewUpdateOpts
}
// constants for updateTask's kind field
const (
RENDER_STRING = iota
RENDER_STRING_WITHOUT_SCROLL
RUN_FUNCTION
RUN_COMMAND
RUN_PTY
)
type updateTask interface {
GetKind() int
}
type renderStringTask struct {
str string
}
func (t *renderStringTask) GetKind() int {
return RENDER_STRING
}
func (gui *Gui) createRenderStringTask(str string) *renderStringTask {
return &renderStringTask{str: str}
}
type renderStringWithoutScrollTask struct {
str string
}
func (t *renderStringWithoutScrollTask) GetKind() int {
return RENDER_STRING_WITHOUT_SCROLL
}
func (gui *Gui) createRenderStringWithoutScrollTask(str string) *renderStringWithoutScrollTask {
return &renderStringWithoutScrollTask{str: str}
}
type runCommandTask struct {
cmd *exec.Cmd
}
func (t *runCommandTask) GetKind() int {
return RUN_COMMAND
}
func (gui *Gui) createRunCommandTask(cmd *exec.Cmd) *runCommandTask {
return &runCommandTask{cmd: cmd}
}
type runPtyTask struct {
cmd *exec.Cmd
}
func (t *runPtyTask) GetKind() int {
return RUN_PTY
}
func (gui *Gui) createRunPtyTask(cmd *exec.Cmd) *runPtyTask {
return &runPtyTask{cmd: cmd}
}
type runFunctionTask struct {
f func(chan struct{}) error
}
func (t *runFunctionTask) GetKind() int {
return RUN_FUNCTION
}
func (gui *Gui) createRunFunctionTask(f func(chan struct{}) error) *runFunctionTask {
return &runFunctionTask{f: f}
}
func (gui *Gui) runTaskForView(viewName string, task updateTask) error {
switch task.GetKind() {
case RENDER_STRING:
specificTask := task.(*renderStringTask)
return gui.newStringTask(viewName, specificTask.str)
case RENDER_STRING_WITHOUT_SCROLL:
specificTask := task.(*renderStringWithoutScrollTask)
return gui.newStringTaskWithoutScroll(viewName, specificTask.str)
case RUN_FUNCTION:
specificTask := task.(*runFunctionTask)
return gui.newTask(viewName, specificTask.f)
case RUN_COMMAND:
specificTask := task.(*runCommandTask)
return gui.newCmdTask(viewName, specificTask.cmd)
case RUN_PTY:
specificTask := task.(*runPtyTask)
return gui.newPtyTask(viewName, specificTask.cmd)
}
return nil
}
func (gui *Gui) refreshMainView(opts *viewUpdateOpts, viewName string) error {
view, err := gui.g.View(viewName)
if err != nil {
gui.Log.Error(err)
return nil
}
view.Title = opts.title
view.Wrap = !opts.noWrap
view.Highlight = opts.highlight
if err := gui.runTaskForView(viewName, opts.task); err != nil {
gui.Log.Error(err)
return nil
}
return nil
}
func (gui *Gui) refreshMainViews(opts refreshMainOpts) error {
if opts.main != nil {
if err := gui.refreshMainView(opts.main, "main"); err != nil {
return err
}
}
gui.splitMainPanel(opts.secondary != nil)
if opts.secondary != nil {
if err := gui.refreshMainView(opts.secondary, "secondary"); err != nil {
return err
}
}
return nil
}
func (gui *Gui) splitMainPanel(splitMainPanel bool) {
gui.State.SplitMainPanel = splitMainPanel
// no need to set view on bottom when splitMainPanel is false: it will have zero size anyway thanks to our view arrangement code.
if splitMainPanel {
_, _ = gui.g.SetViewOnTop("secondary")
}
}
func (gui *Gui) isMainPanelSplit() bool {
return gui.State.SplitMainPanel
}

View File

@@ -2,6 +2,7 @@ package gui
import (
"fmt"
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/theme"
@@ -14,35 +15,37 @@ type menuItem struct {
onPress func() error
}
// every item in a list context needs an ID
func (i *menuItem) ID() string {
if i.displayString != "" {
return i.displayString
}
return strings.Join(i.displayStrings, "-")
}
// list panel functions
func (gui *Gui) handleMenuSelect(g *gocui.Gui, v *gocui.View) error {
v.FocusPoint(0, gui.State.Panels.Menu.SelectedLine)
func (gui *Gui) handleMenuSelect() error {
return nil
}
// specific functions
func (gui *Gui) renderMenuOptions() error {
optionsMap := map[string]string{
fmt.Sprintf("%s/%s", gui.getKeyDisplay("universal.return"), gui.getKeyDisplay("universal.quit")): gui.Tr.SLocalize("close"),
func (gui *Gui) getMenuOptions() map[string]string {
return map[string]string{
gui.getKeyDisplay("universal.return"): gui.Tr.SLocalize("close"),
fmt.Sprintf("%s %s", gui.getKeyDisplay("universal.prevItem"), gui.getKeyDisplay("universal.nextItem")): gui.Tr.SLocalize("navigate"),
gui.getKeyDisplay("universal.select"): gui.Tr.SLocalize("execute"),
}
return gui.renderOptionsMap(optionsMap)
}
func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error {
for _, key := range []gocui.Key{gocui.KeySpace, gocui.KeyEnter} {
if err := g.DeleteKeybinding("menu", key, gocui.ModNone); err != nil {
return err
}
}
err := g.DeleteView("menu")
if err != nil {
return err
}
return gui.returnFocus(g, v)
return gui.returnFromContext()
}
type createMenuOptions struct {
@@ -60,7 +63,7 @@ func (gui *Gui) createMenu(title string, items []*menuItem, createMenuOptions cr
})
}
gui.State.MenuItemCount = len(items)
gui.State.MenuItems = items
stringArrays := make([][]string, len(items))
for i, item := range items {
@@ -73,53 +76,29 @@ func (gui *Gui) createMenu(title string, items []*menuItem, createMenuOptions cr
list := utils.RenderDisplayStrings(stringArrays)
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, false, list)
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(false, list)
menuView, _ := gui.g.SetView("menu", x0, y0, x1, y1, 0)
menuView.Title = title
menuView.FgColor = theme.GocuiDefaultTextColor
menuView.ContainsList = true
menuView.Clear()
menuView.SetOnSelectItem(gui.onSelectItemWrapper(func(selectedLine int) error {
gui.State.Panels.Menu.SelectedLine = selectedLine
menuView.FocusPoint(0, selectedLine)
return nil
}))
fmt.Fprint(menuView, list)
gui.State.Panels.Menu.SelectedLine = 0
wrappedHandlePress := func(g *gocui.Gui, v *gocui.View) error {
selectedLine := gui.State.Panels.Menu.SelectedLine
if err := items[selectedLine].onPress(); err != nil {
return err
}
if _, err := gui.g.View("menu"); err == nil {
if _, err := gui.g.SetViewOnBottom("menu"); err != nil {
return err
}
}
return gui.returnFocus(gui.g, menuView)
}
gui.State.Panels.Menu.OnPress = wrappedHandlePress
for _, key := range []gocui.Key{gocui.KeySpace, gocui.KeyEnter, 'y'} {
_ = gui.g.DeleteKeybinding("menu", key, gocui.ModNone)
if err := gui.g.SetKeybinding("menu", nil, key, gocui.ModNone, wrappedHandlePress); err != nil {
return err
}
}
gui.State.Panels.Menu.SelectedLineIdx = 0
gui.g.Update(func(g *gocui.Gui) error {
if _, err := gui.g.View("menu"); err == nil {
if _, err := g.SetViewOnTop("menu"); err != nil {
return err
}
}
currentView := gui.g.CurrentView()
return gui.switchFocus(gui.g, currentView, menuView)
return gui.switchContext(gui.Contexts.Menu.Context)
})
return nil
}
func (gui *Gui) onMenuPress() error {
selectedLine := gui.State.Panels.Menu.SelectedLineIdx
if err := gui.State.MenuItems[selectedLine].onPress(); err != nil {
return err
}
return gui.returnFromContext()
}

View File

@@ -12,6 +12,7 @@ import (
"strings"
"github.com/fatih/color"
"github.com/go-errors/errors"
"github.com/golang-collections/collections/stack"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
@@ -19,8 +20,13 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) findConflicts(content string) ([]commands.Conflict, error) {
func (gui *Gui) findConflicts(content string) []commands.Conflict {
conflicts := make([]commands.Conflict, 0)
if content == "" {
return conflicts
}
var newConflict commands.Conflict
for i, line := range utils.SplitLines(content) {
trimmedLine := strings.TrimPrefix(line, "++")
@@ -34,7 +40,7 @@ func (gui *Gui) findConflicts(content string) ([]commands.Conflict, error) {
conflicts = append(conflicts, newConflict)
}
}
return conflicts, nil
return conflicts
}
func (gui *Gui) shiftConflict(conflicts []commands.Conflict) (commands.Conflict, []commands.Conflict) {
@@ -112,10 +118,10 @@ func (gui *Gui) isIndexToDelete(i int, conflict commands.Conflict, pick string)
(pick == "top" && i > conflict.Middle && i < conflict.End)
}
func (gui *Gui) resolveConflict(g *gocui.Gui, conflict commands.Conflict, pick string) error {
gitFile, err := gui.getSelectedFile()
if err != nil {
return err
func (gui *Gui) resolveConflict(conflict commands.Conflict, pick string) error {
gitFile := gui.getSelectedFile()
if gitFile == nil {
return nil
}
file, err := os.Open(gitFile.Name)
if err != nil {
@@ -139,9 +145,9 @@ func (gui *Gui) resolveConflict(g *gocui.Gui, conflict commands.Conflict, pick s
}
func (gui *Gui) pushFileSnapshot(g *gocui.Gui) error {
gitFile, err := gui.getSelectedFile()
if err != nil {
return err
gitFile := gui.getSelectedFile()
if gitFile == nil {
return nil
}
content, err := gui.GitCommand.CatFile(gitFile.Name)
if err != nil {
@@ -156,9 +162,9 @@ func (gui *Gui) handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error {
return nil
}
prevContent := gui.State.Panels.Merging.EditHistory.Pop().(string)
gitFile, err := gui.getSelectedFile()
if err != nil {
return err
gitFile := gui.getSelectedFile()
if gitFile == nil {
return nil
}
if err := ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644); err != nil {
return err
@@ -179,7 +185,7 @@ func (gui *Gui) handlePickHunk(g *gocui.Gui, v *gocui.View) error {
if gui.State.Panels.Merging.ConflictTop {
pick = "top"
}
err := gui.resolveConflict(g, conflict, pick)
err := gui.resolveConflict(conflict, pick)
if err != nil {
panic(err)
}
@@ -200,7 +206,7 @@ func (gui *Gui) handlePickBothHunks(g *gocui.Gui, v *gocui.View) error {
if err := gui.pushFileSnapshot(g); err != nil {
return err
}
err := gui.resolveConflict(g, conflict, "both")
err := gui.resolveConflict(conflict, "both")
if err != nil {
panic(err)
}
@@ -211,16 +217,16 @@ func (gui *Gui) refreshMergePanel() error {
panelState := gui.State.Panels.Merging
cat, err := gui.catSelectedFile(gui.g)
if err != nil {
return err
}
if cat == "" {
return nil
}
panelState.Conflicts, err = gui.findConflicts(cat)
if err != nil {
return err
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: "",
task: gui.createRenderStringTask(err.Error()),
},
})
}
panelState.Conflicts = gui.findConflicts(cat)
// handle potential fixes that the user made in their editor since we last refreshed
if len(panelState.Conflicts) == 0 {
return gui.handleCompleteMerge()
@@ -238,31 +244,29 @@ func (gui *Gui) refreshMergePanel() error {
return err
}
mainView := gui.getMainView()
mainView.Wrap = false
if err := gui.newStringTaskWithoutScroll("main", content); err != nil {
return err
}
return nil
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: gui.Tr.SLocalize("MergeConflictsTitle"),
task: gui.createRenderStringWithoutScrollTask(content),
noWrap: true,
},
})
}
func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) {
item, err := gui.getSelectedFile()
if err != nil {
if err != gui.Errors.ErrNoFiles {
return "", err
}
return "", gui.newStringTask("main", gui.Tr.SLocalize("NoFilesDisplay"))
item := gui.getSelectedFile()
if item == nil {
return "", errors.New(gui.Tr.SLocalize("NoFilesDisplay"))
}
if item.Type != "file" {
return "", gui.newStringTask("main", gui.Tr.SLocalize("NotAFile"))
return "", errors.New(gui.Tr.SLocalize("NotAFile"))
}
cat, err := gui.GitCommand.CatFile(item.Name)
if err != nil {
gui.Log.Error(err)
return "", gui.newStringTask("main", err.Error())
return "", err
}
return cat, nil
}
@@ -288,17 +292,17 @@ func (gui *Gui) scrollToConflict(g *gocui.Gui) error {
return nil
}
func (gui *Gui) renderMergeOptions() error {
return gui.renderOptionsMap(map[string]string{
func (gui *Gui) getMergingOptions() map[string]string {
return map[string]string{
fmt.Sprintf("%s %s", gui.getKeyDisplay("universal.prevItem"), gui.getKeyDisplay("universal.nextItem")): gui.Tr.SLocalize("selectHunk"),
fmt.Sprintf("%s %s", gui.getKeyDisplay("universal.prevBlock"), gui.getKeyDisplay("universal.nextBlock")): gui.Tr.SLocalize("navigateConflicts"),
gui.getKeyDisplay("universal.select"): gui.Tr.SLocalize("pickHunk"),
gui.getKeyDisplay("main.pickBothHunks"): gui.Tr.SLocalize("pickBothHunks"),
gui.getKeyDisplay("universal.undo"): gui.Tr.SLocalize("undo"),
})
}
}
func (gui *Gui) handleEscapeMerge(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleEscapeMerge() error {
gui.takeOverScrolling()
gui.State.Panels.Merging.EditHistory = stack.New()
@@ -308,7 +312,7 @@ func (gui *Gui) handleEscapeMerge(g *gocui.Gui, v *gocui.View) error {
// it's possible this method won't be called from the merging view so we need to
// ensure we only 'return' focus if we already have it
if gui.g.CurrentView() == gui.getMainView() {
return gui.switchFocus(g, v, gui.getFilesView())
return gui.switchContext(gui.Contexts.Files.Context)
}
return nil
}
@@ -323,22 +327,34 @@ func (gui *Gui) handleCompleteMerge() error {
// if we got conflicts after unstashing, we don't want to call any git
// commands to continue rebasing/merging here
if gui.GitCommand.WorkingTreeState() == "normal" {
return gui.handleEscapeMerge(gui.g, gui.getMainView())
return gui.handleEscapeMerge()
}
// if there are no more files with merge conflicts, we should ask whether the user wants to continue
if !gui.anyFilesWithMergeConflicts() {
return gui.promptToContinue()
}
return gui.handleEscapeMerge(gui.g, gui.getMainView())
return gui.handleEscapeMerge()
}
// promptToContinue asks the user if they want to continue the rebase/merge that's in progress
func (gui *Gui) promptToContinue() error {
gui.takeOverScrolling()
return gui.createConfirmationPanel(gui.g, gui.getFilesView(), true, "continue", gui.Tr.SLocalize("ConflictsResolved"), func(g *gocui.Gui, v *gocui.View) error {
return gui.genericMergeCommand("continue")
}, nil)
return gui.ask(askOpts{
title: "continue",
prompt: gui.Tr.SLocalize("ConflictsResolved"),
handlersManageFocus: true,
handleConfirm: func() error {
if err := gui.switchContext(gui.Contexts.Files.Context); err != nil {
return err
}
return gui.genericMergeCommand("continue")
},
handleClose: func() error {
return gui.switchContext(gui.Contexts.Files.Context)
},
})
}
func (gui *Gui) canScrollMergePanel() bool {
@@ -347,8 +363,8 @@ func (gui *Gui) canScrollMergePanel() bool {
return false
}
file, err := gui.getSelectedFile()
if err != nil {
file := gui.getSelectedFile()
if file == nil {
return false
}

61
pkg/gui/modes.go Normal file
View File

@@ -0,0 +1,61 @@
package gui
import (
"fmt"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type modeStatus struct {
isActive func() bool
description func() string
reset func() error
}
func (gui *Gui) modeStatuses() []modeStatus {
return []modeStatus{
{
isActive: gui.State.Modes.Diffing.Active,
description: func() string {
return utils.ColoredString(
fmt.Sprintf("%s %s %s", gui.Tr.SLocalize("showingGitDiff"), "git diff "+gui.diffStr(), utils.ColoredString(gui.Tr.SLocalize("(reset)"), color.Underline)),
color.FgMagenta,
)
},
reset: gui.exitDiffMode,
},
{
isActive: gui.State.Modes.Filtering.Active,
description: func() string {
return utils.ColoredString(
fmt.Sprintf("%s '%s' %s", gui.Tr.SLocalize("filteringBy"), gui.State.Modes.Filtering.Path, utils.ColoredString(gui.Tr.SLocalize("(reset)"), color.Underline)),
color.FgRed,
color.Bold,
)
},
reset: gui.exitFilterMode,
},
{
isActive: gui.GitCommand.PatchManager.Active,
description: func() string {
return utils.ColoredString(
fmt.Sprintf("%s %s", gui.Tr.SLocalize("buildingPatch"), utils.ColoredString(gui.Tr.SLocalize("(reset)"), color.Underline)),
color.FgYellow,
color.Bold,
)
},
reset: gui.handleResetPatch,
},
{
isActive: gui.State.Modes.CherryPicking.Active,
description: func() string {
return utils.ColoredString(
fmt.Sprintf("%d commits copied %s", len(gui.State.Modes.CherryPicking.CherryPickedCommits), utils.ColoredString(gui.Tr.SLocalize("(reset)"), color.Underline)),
color.FgCyan,
)
},
reset: gui.exitCherryPickingMode,
},
}
}

View File

@@ -5,12 +5,25 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int) error {
if !gui.GitCommand.PatchManager.CommitSelected() {
return gui.handleEscapePatchBuildingPanel(gui.g, nil)
// getFromAndReverseArgsForDiff tells us the from and reverse args to be used in a diff command. If we're not in diff mode we'll end up with the equivalent of a `git show` i.e `git diff blah^..blah`.
func (gui *Gui) getFromAndReverseArgsForDiff(to string) (string, bool) {
from := to + "^"
reverse := false
if gui.State.Modes.Diffing.Active() {
reverse = gui.State.Modes.Diffing.Reverse
from = gui.State.Modes.Diffing.Ref
}
gui.State.SplitMainPanel = true
return from, reverse
}
func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int) error {
if !gui.GitCommand.PatchManager.Active() {
return gui.handleEscapePatchBuildingPanel()
}
gui.splitMainPanel(true)
gui.getMainView().Title = "Patch"
gui.getSecondaryView().Title = "Custom Patch"
@@ -18,11 +31,12 @@ func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int) error {
// get diff from commit file that's currently selected
commitFile := gui.getSelectedCommitFile()
if commitFile == nil {
gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
return nil
}
diff, err := gui.GitCommand.ShowCommitFile(commitFile.Sha, commitFile.Name, true)
to := commitFile.Parent
from, reverse := gui.getFromAndReverseArgsForDiff(to)
diff, err := gui.GitCommand.ShowFileDiff(from, to, reverse, commitFile.Name, true)
if err != nil {
return err
}
@@ -38,7 +52,7 @@ func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int) error {
}
if empty {
return gui.handleEscapePatchBuildingPanel(gui.g, nil)
return gui.handleEscapePatchBuildingPanel()
}
return nil
@@ -49,7 +63,10 @@ func (gui *Gui) handleToggleSelectionForPatch(g *gocui.Gui, v *gocui.View) error
toggleFunc := gui.GitCommand.PatchManager.AddFileLineRange
filename := gui.getSelectedCommitFileName()
includedLineIndices := gui.GitCommand.PatchManager.GetFileIncLineIndices(filename)
includedLineIndices, err := gui.GitCommand.PatchManager.GetFileIncLineIndices(filename)
if err != nil {
return err
}
currentLineIsStaged := utils.IncludesInt(includedLineIndices, state.SelectedLineIdx)
if currentLineIsStaged {
toggleFunc = gui.GitCommand.PatchManager.RemoveFileLineRange
@@ -58,7 +75,6 @@ func (gui *Gui) handleToggleSelectionForPatch(g *gocui.Gui, v *gocui.View) error
// add range of lines to those set for the file
commitFile := gui.getSelectedCommitFile()
if commitFile == nil {
gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
return nil
}
@@ -75,30 +91,31 @@ func (gui *Gui) handleToggleSelectionForPatch(g *gocui.Gui, v *gocui.View) error
return nil
}
func (gui *Gui) handleEscapePatchBuildingPanel(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleEscapePatchBuildingPanel() error {
gui.handleEscapeLineByLinePanel()
if gui.GitCommand.PatchManager.IsEmpty() {
gui.GitCommand.PatchManager.Reset()
gui.State.SplitMainPanel = false
}
return gui.switchFocus(gui.g, nil, gui.getCommitFilesView())
if gui.currentContext().GetKey() == gui.Contexts.PatchBuilding.Context.GetKey() {
return gui.switchContext(gui.Contexts.CommitFiles.Context)
} else {
// need to re-focus in case the secondary view should now be hidden
return gui.currentContext().HandleFocus()
}
}
func (gui *Gui) refreshSecondaryPatchPanel() error {
if gui.GitCommand.PatchManager.CommitSelected() {
gui.State.SplitMainPanel = true
secondaryView := gui.getSecondaryView()
secondaryView.Highlight = true
secondaryView.Wrap = false
func (gui *Gui) secondaryPatchPanelUpdateOpts() *viewUpdateOpts {
if gui.GitCommand.PatchManager.Active() {
patch := gui.GitCommand.PatchManager.RenderAggregatedPatchColored(false)
gui.g.Update(func(*gocui.Gui) error {
gui.setViewContent(gui.getSecondaryView(), gui.GitCommand.PatchManager.RenderAggregatedPatchColored(false))
return nil
})
} else {
gui.State.SplitMainPanel = false
return &viewUpdateOpts{
title: "Custom Patch",
noWrap: true,
highlight: true,
task: gui.createRenderStringWithoutScrollTask(patch),
}
}
return nil

View File

@@ -7,22 +7,14 @@ import (
)
func (gui *Gui) handleCreatePatchOptionsMenu(g *gocui.Gui, v *gocui.View) error {
if !gui.GitCommand.PatchManager.CommitSelected() {
if !gui.GitCommand.PatchManager.Active() {
return gui.createErrorPanel(gui.Tr.SLocalize("NoPatchError"))
}
menuItems := []*menuItem{
{
displayString: fmt.Sprintf("remove patch from original commit (%s)", gui.GitCommand.PatchManager.CommitSha),
onPress: gui.handleDeletePatchFromCommit,
},
{
displayString: "pull patch out into index",
onPress: gui.handlePullPatchIntoWorkingTree,
},
{
displayString: "pull patch into new commit",
onPress: gui.handlePullPatchIntoNewCommit,
displayString: "reset patch",
onPress: gui.handleResetPatch,
},
{
displayString: "apply patch",
@@ -32,26 +24,41 @@ func (gui *Gui) handleCreatePatchOptionsMenu(g *gocui.Gui, v *gocui.View) error
displayString: "apply patch in reverse",
onPress: func() error { return gui.handleApplyPatch(true) },
},
{
displayString: "reset patch",
onPress: gui.handleResetPatch,
},
}
selectedCommit := gui.getSelectedCommit()
if selectedCommit != nil && gui.GitCommand.PatchManager.CommitSha != selectedCommit.Sha {
// adding this option to index 1
menuItems = append(
menuItems[:1],
append(
[]*menuItem{
{
displayString: fmt.Sprintf("move patch to selected commit (%s)", selectedCommit.Sha),
onPress: gui.handleMovePatchToSelectedCommit,
},
}, menuItems[1:]...,
)...,
)
if gui.GitCommand.PatchManager.CanRebase && gui.workingTreeState() == "normal" {
menuItems = append(menuItems, []*menuItem{
{
displayString: fmt.Sprintf("remove patch from original commit (%s)", gui.GitCommand.PatchManager.To),
onPress: gui.handleDeletePatchFromCommit,
},
{
displayString: "pull patch out into index",
onPress: gui.handlePullPatchIntoWorkingTree,
},
{
displayString: "pull patch into new commit",
onPress: gui.handlePullPatchIntoNewCommit,
},
}...)
if gui.currentContext().GetKey() == gui.Contexts.BranchCommits.Context.GetKey() {
selectedCommit := gui.getSelectedLocalCommit()
if selectedCommit != nil && gui.GitCommand.PatchManager.To != selectedCommit.Sha {
// adding this option to index 1
menuItems = append(
menuItems[:1],
append(
[]*menuItem{
{
displayString: fmt.Sprintf("move patch to selected commit (%s)", selectedCommit.Sha),
onPress: gui.handleMovePatchToSelectedCommit,
},
}, menuItems[1:]...,
)...,
)
}
}
}
return gui.createMenu(gui.Tr.SLocalize("PatchOptionsTitle"), menuItems, createMenuOptions{showCancel: true})
@@ -59,7 +66,7 @@ func (gui *Gui) handleCreatePatchOptionsMenu(g *gocui.Gui, v *gocui.View) error
func (gui *Gui) getPatchCommitIndex() int {
for index, commit := range gui.State.Commits {
if commit.Sha == gui.GitCommand.PatchManager.CommitSha {
if commit.Sha == gui.GitCommand.PatchManager.To {
return index
}
}
@@ -67,9 +74,6 @@ func (gui *Gui) getPatchCommitIndex() int {
}
func (gui *Gui) validateNormalWorkingTreeState() (bool, error) {
if gui.GitCommand.WorkingTreeState() != "normal" {
return false, gui.createErrorPanel(gui.Tr.SLocalize("CantPatchWhileRebasingError"))
}
if gui.GitCommand.WorkingTreeState() != "normal" {
return false, gui.createErrorPanel(gui.Tr.SLocalize("CantPatchWhileRebasingError"))
}
@@ -77,8 +81,8 @@ func (gui *Gui) validateNormalWorkingTreeState() (bool, error) {
}
func (gui *Gui) returnFocusFromLineByLinePanelIfNecessary() error {
if gui.State.MainContext == "patch-building" {
return gui.handleEscapePatchBuildingPanel(gui.g, nil)
if gui.State.MainContext == MAIN_PATCH_BUILDING_CONTEXT_KEY {
return gui.handleEscapePatchBuildingPanel()
}
return nil
}
@@ -110,7 +114,7 @@ func (gui *Gui) handleMovePatchToSelectedCommit() error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
commitIndex := gui.getPatchCommitIndex()
err := gui.GitCommand.MovePatchToSelectedCommit(gui.State.Commits, commitIndex, gui.State.Panels.Commits.SelectedLine, gui.GitCommand.PatchManager)
err := gui.GitCommand.MovePatchToSelectedCommit(gui.State.Commits, commitIndex, gui.State.Panels.Commits.SelectedLineIdx, gui.GitCommand.PatchManager)
return gui.handleGenericMergeCommandResult(err)
})
}
@@ -133,9 +137,13 @@ func (gui *Gui) handlePullPatchIntoWorkingTree() error {
}
if len(gui.trackedFiles()) > 0 {
return gui.createConfirmationPanel(gui.g, gui.g.CurrentView(), true, gui.Tr.SLocalize("MustStashTitle"), gui.Tr.SLocalize("MustStashWarning"), func(*gocui.Gui, *gocui.View) error {
return pull(true)
}, nil)
return gui.ask(askOpts{
title: gui.Tr.SLocalize("MustStashTitle"),
prompt: gui.Tr.SLocalize("MustStashWarning"),
handleConfirm: func() error {
return pull(true)
},
})
} else {
return pull(false)
}
@@ -170,5 +178,10 @@ func (gui *Gui) handleApplyPatch(reverse bool) error {
func (gui *Gui) handleResetPatch() error {
gui.GitCommand.PatchManager.Reset()
if gui.currentContextKeyIgnoringPopups() == MAIN_PATCH_BUILDING_CONTEXT_KEY {
if err := gui.switchContext(gui.Contexts.CommitFiles.Context); err != nil {
return err
}
}
return gui.refreshCommitFilesView()
}

View File

@@ -3,10 +3,16 @@ package presentation
import (
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func GetCommitFileListDisplayStrings(commitFiles []*commands.CommitFile, diffName string) [][]string {
if len(commitFiles) == 0 {
return [][]string{{utils.ColoredString("(none)", color.FgRed)}}
}
lines := make([][]string, len(commitFiles))
for i := range commitFiles {
@@ -25,16 +31,33 @@ func getCommitFileDisplayStrings(f *commands.CommitFile, diffed bool) []string {
diffTerminalColor := color.New(theme.DiffTerminalColor)
var colour *color.Color
switch f.Status {
case commands.UNSELECTED:
switch f.PatchStatus {
case patch.UNSELECTED:
colour = defaultColor
case commands.WHOLE:
case patch.WHOLE:
colour = green
case commands.PART:
case patch.PART:
colour = yellow
}
if diffed {
colour = diffTerminalColor
}
return []string{colour.Sprint(f.DisplayString)}
return []string{utils.ColoredString(f.ChangeStatus, getColorForChangeStatus(f.ChangeStatus)), colour.Sprint(f.Name)}
}
func getColorForChangeStatus(changeStatus string) color.Attribute {
switch changeStatus {
case "A":
return color.FgGreen
case "M", "R":
return color.FgYellow
case "D":
return color.FgRed
case "C":
return color.FgCyan
case "T":
return color.FgMagenta
default:
return theme.DefaultTextColor
}
}

View File

@@ -7,10 +7,10 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
func GetReflogCommitListDisplayStrings(commits []*commands.Commit, fullDescription bool, diffName string) [][]string {
func GetReflogCommitListDisplayStrings(commits []*commands.Commit, fullDescription bool, cherryPickedCommitShaMap map[string]bool, diffName string) [][]string {
lines := make([][]string, len(commits))
var displayFunc func(*commands.Commit, bool) []string
var displayFunc func(*commands.Commit, map[string]bool, bool) []string
if fullDescription {
displayFunc = getFullDescriptionDisplayStringsForReflogCommit
} else {
@@ -19,27 +19,41 @@ func GetReflogCommitListDisplayStrings(commits []*commands.Commit, fullDescripti
for i := range commits {
diffed := commits[i].Sha == diffName
lines[i] = displayFunc(commits[i], diffed)
lines[i] = displayFunc(commits[i], cherryPickedCommitShaMap, diffed)
}
return lines
}
func getFullDescriptionDisplayStringsForReflogCommit(c *commands.Commit, diffed bool) []string {
func coloredReflogSha(c *commands.Commit, cherryPickedCommitShaMap map[string]bool) string {
var shaColor *color.Color
if cherryPickedCommitShaMap[c.Sha] {
shaColor = color.New(color.FgCyan, color.BgBlue)
} else {
shaColor = color.New(color.FgBlue)
}
return shaColor.Sprint(c.ShortSha())
}
func getFullDescriptionDisplayStringsForReflogCommit(c *commands.Commit, cherryPickedCommitShaMap map[string]bool, diffed bool) []string {
colorAttr := theme.DefaultTextColor
if diffed {
colorAttr = theme.DiffTerminalColor
}
return []string{
utils.ColoredString(c.ShortSha(), color.FgBlue),
coloredReflogSha(c, cherryPickedCommitShaMap),
utils.ColoredString(utils.UnixToDate(c.UnixTimestamp), color.FgMagenta),
utils.ColoredString(c.Name, colorAttr),
}
}
func getDisplayStringsForReflogCommit(c *commands.Commit, diffed bool) []string {
func getDisplayStringsForReflogCommit(c *commands.Commit, cherryPickedCommitShaMap map[string]bool, diffed bool) []string {
defaultColor := color.New(theme.DefaultTextColor)
return []string{utils.ColoredString(c.ShortSha(), color.FgBlue), defaultColor.Sprint(c.Name)}
return []string{
coloredReflogSha(c, cherryPickedCommitShaMap),
defaultColor.Sprint(c.Name),
}
}

View File

@@ -26,28 +26,49 @@ func (gui *Gui) recordCurrentDirectory() error {
func (gui *Gui) handleQuitWithoutChangingDirectory(g *gocui.Gui, v *gocui.View) error {
gui.State.RetainOriginalDir = true
return gui.quit(v)
return gui.quit()
}
func (gui *Gui) handleQuit(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleQuit() error {
gui.State.RetainOriginalDir = false
return gui.quit(v)
return gui.quit()
}
func (gui *Gui) quit(v *gocui.View) error {
func (gui *Gui) handleTopLevelReturn(g *gocui.Gui, v *gocui.View) error {
currentContext := gui.currentContext()
parentContext, hasParent := currentContext.GetParentContext()
if hasParent && currentContext != nil && parentContext != nil {
// TODO: think about whether this should be marked as a return rather than adding to the stack
return gui.switchContext(parentContext)
}
for _, mode := range gui.modeStatuses() {
if mode.isActive() {
return mode.reset()
}
}
if gui.Config.GetUserConfig().GetBool("quitOnTopLevelReturn") {
return gui.handleQuit()
}
return nil
}
func (gui *Gui) quit() error {
if gui.State.Updating {
return gui.createUpdateQuitConfirmation(gui.g, v)
}
if gui.inDiffMode() {
return gui.exitDiffMode()
}
if gui.inFilterMode() {
return gui.exitFilterMode()
return gui.createUpdateQuitConfirmation()
}
if gui.Config.GetUserConfig().GetBool("confirmOnQuit") {
return gui.createConfirmationPanel(gui.g, v, true, "", gui.Tr.SLocalize("ConfirmQuit"), func(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}, nil)
return gui.ask(askOpts{
title: "",
prompt: gui.Tr.SLocalize("ConfirmQuit"),
handleConfirm: func() error {
return gocui.ErrQuit
},
})
}
return gocui.ErrQuit

View File

@@ -3,11 +3,9 @@ package gui
import (
"fmt"
"strings"
"github.com/jesseduffield/gocui"
)
func (gui *Gui) handleCreateRebaseOptionsMenu(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCreateRebaseOptionsMenu() error {
options := []string{"continue", "abort"}
if gui.GitCommand.WorkingTreeState() == "rebasing" {
@@ -78,13 +76,21 @@ func (gui *Gui) handleGenericMergeCommandResult(result error) error {
// assume in this case that we're already done
return nil
} else if strings.Contains(result.Error(), "When you have resolved this problem") || strings.Contains(result.Error(), "fix conflicts") || strings.Contains(result.Error(), "Resolve all conflicts manually") {
return gui.createConfirmationPanel(gui.g, gui.getFilesView(), true, gui.Tr.SLocalize("FoundConflictsTitle"), gui.Tr.SLocalize("FoundConflicts"),
func(g *gocui.Gui, v *gocui.View) error {
return nil
}, func(g *gocui.Gui, v *gocui.View) error {
return gui.ask(askOpts{
title: gui.Tr.SLocalize("FoundConflictsTitle"),
prompt: gui.Tr.SLocalize("FoundConflicts"),
handlersManageFocus: true,
handleConfirm: func() error {
return gui.switchContext(gui.Contexts.Files.Context)
},
handleClose: func() error {
if err := gui.returnFromContext(); err != nil {
return err
}
return gui.genericMergeCommand("abort")
},
)
})
} else {
return gui.createErrorPanel(result.Error())
}

View File

@@ -5,12 +5,11 @@ import (
"path/filepath"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) handleCreateRecentReposMenu(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCreateRecentReposMenu() error {
recentRepoPaths := gui.Config.GetAppState().RecentRepos
reposCount := utils.Min(len(recentRepoPaths), 20)
yellow := color.New(color.FgMagenta)
@@ -32,7 +31,7 @@ func (gui *Gui) handleCreateRecentReposMenu(g *gocui.Gui, v *gocui.View) error {
return err
}
gui.GitCommand = newGitCommand
gui.State.FilterPath = ""
gui.State.Modes.Filtering.Path = ""
return gui.Errors.ErrSwitchRepo
},
}

View File

@@ -3,13 +3,12 @@ package gui
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
)
// list panel functions
func (gui *Gui) getSelectedReflogCommit() *commands.Commit {
selectedLine := gui.State.Panels.ReflogCommits.SelectedLine
selectedLine := gui.State.Panels.ReflogCommits.SelectedLineIdx
reflogComits := gui.State.FilteredReflogCommits
if selectedLine == -1 || len(reflogComits) == 0 {
return nil
@@ -18,37 +17,25 @@ func (gui *Gui) getSelectedReflogCommit() *commands.Commit {
return reflogComits[selectedLine]
}
func (gui *Gui) handleReflogCommitSelect(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
gui.State.SplitMainPanel = false
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
gui.getMainView().Title = "Reflog Entry"
func (gui *Gui) handleReflogCommitSelect() error {
commit := gui.getSelectedReflogCommit()
var task updateTask
if commit == nil {
return gui.newStringTask("main", "No reflog history")
}
v.FocusPoint(0, gui.State.Panels.ReflogCommits.SelectedLine)
task = gui.createRenderStringTask("No reflog history")
} else {
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.Modes.Filtering.Path),
)
if gui.inDiffMode() {
return gui.renderDiff()
task = gui.createRunPtyTask(cmd)
}
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.FilterPath),
)
if err := gui.newPtyTask("main", cmd); err != nil {
gui.Log.Error(err)
}
return nil
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: "Reflog Entry",
task: task,
},
})
}
// the reflogs panel is the only panel where we cache data, in that we only
@@ -85,34 +72,15 @@ func (gui *Gui) refreshReflogCommits() error {
return err
}
if gui.inFilterMode() {
if err := refresh(&state.FilteredReflogCommits, state.FilterPath); err != nil {
if gui.State.Modes.Filtering.Active() {
if err := refresh(&state.FilteredReflogCommits, state.Modes.Filtering.Path); err != nil {
return err
}
} else {
state.FilteredReflogCommits = state.ReflogCommits
}
if gui.getCommitsView().Context == "reflog-commits" {
return gui.renderReflogCommitsWithSelection()
}
return nil
}
func (gui *Gui) renderReflogCommitsWithSelection() error {
commitsView := gui.getCommitsView()
gui.refreshSelectedLine(&gui.State.Panels.ReflogCommits.SelectedLine, len(gui.State.FilteredReflogCommits))
displayStrings := presentation.GetReflogCommitListDisplayStrings(gui.State.FilteredReflogCommits, gui.State.ScreenMode != SCREEN_NORMAL, gui.State.Diff.Ref)
gui.renderDisplayStrings(commitsView, displayStrings)
if gui.g.CurrentView() == commitsView && commitsView.Context == "reflog-commits" {
if err := gui.handleReflogCommitSelect(gui.g, commitsView); err != nil {
return err
}
}
return nil
return gui.postRefreshUpdate(gui.Contexts.ReflogCommits.Context)
}
func (gui *Gui) handleCheckoutReflogCommit(g *gocui.Gui, v *gocui.View) error {
@@ -121,14 +89,18 @@ func (gui *Gui) handleCheckoutReflogCommit(g *gocui.Gui, v *gocui.View) error {
return nil
}
err := gui.createConfirmationPanel(g, gui.getCommitsView(), true, gui.Tr.SLocalize("checkoutCommit"), gui.Tr.SLocalize("SureCheckoutThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{})
}, nil)
err := gui.ask(askOpts{
title: gui.Tr.SLocalize("checkoutCommit"),
prompt: gui.Tr.SLocalize("SureCheckoutThisCommit"),
handleConfirm: func() error {
return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{})
},
})
if err != nil {
return err
}
gui.State.Panels.ReflogCommits.SelectedLine = 0
gui.State.Panels.ReflogCommits.SelectedLineIdx = 0
return nil
}
@@ -138,3 +110,12 @@ func (gui *Gui) handleCreateReflogResetMenu(g *gocui.Gui, v *gocui.View) error {
return gui.createResetMenu(commit.Sha)
}
func (gui *Gui) handleViewReflogCommitFiles() error {
commit := gui.getSelectedReflogCommit()
if commit == nil {
return nil
}
return gui.switchToCommitFilesContext(commit.Sha, false, gui.Contexts.ReflogCommits.Context, "commits")
}

View File

@@ -5,13 +5,12 @@ import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
)
// list panel functions
func (gui *Gui) getSelectedRemoteBranch() *commands.RemoteBranch {
selectedLine := gui.State.Panels.RemoteBranches.SelectedLine
selectedLine := gui.State.Panels.RemoteBranches.SelectedLineIdx
if selectedLine == -1 || len(gui.State.RemoteBranches) == 0 {
return nil
}
@@ -19,72 +18,32 @@ func (gui *Gui) getSelectedRemoteBranch() *commands.RemoteBranch {
return gui.State.RemoteBranches[selectedLine]
}
func (gui *Gui) handleRemoteBranchSelect(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
gui.State.SplitMainPanel = false
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
gui.getMainView().Title = "Remote Branch"
func (gui *Gui) handleRemoteBranchSelect() error {
var task updateTask
remoteBranch := gui.getSelectedRemoteBranch()
if remoteBranch == nil {
return gui.newStringTask("main", "No branches for this remote")
task = gui.createRenderStringTask("No branches for this remote")
} else {
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.GetBranchGraphCmdStr(remoteBranch.FullName()),
)
task = gui.createRunCommandTask(cmd)
}
v.FocusPoint(0, gui.State.Panels.RemoteBranches.SelectedLine)
if gui.inDiffMode() {
return gui.renderDiff()
}
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.GetBranchGraphCmdStr(remoteBranch.FullName()),
)
if err := gui.newCmdTask("main", cmd); err != nil {
gui.Log.Error(err)
}
return nil
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: "Remote Branch",
task: task,
},
})
}
func (gui *Gui) handleRemoteBranchesEscape(g *gocui.Gui, v *gocui.View) error {
return gui.switchBranchesPanelContext("remotes")
}
func (gui *Gui) renderRemoteBranchesWithSelection() error {
branchesView := gui.getBranchesView()
gui.refreshSelectedLine(&gui.State.Panels.RemoteBranches.SelectedLine, len(gui.State.RemoteBranches))
displayStrings := presentation.GetRemoteBranchListDisplayStrings(gui.State.RemoteBranches, gui.State.Diff.Ref)
gui.renderDisplayStrings(branchesView, displayStrings)
if gui.g.CurrentView() == branchesView && branchesView.Context == "remote-branches" {
if err := gui.handleRemoteBranchSelect(gui.g, branchesView); err != nil {
return err
}
}
return nil
}
func (gui *Gui) handleCheckoutRemoteBranch(g *gocui.Gui, v *gocui.View) error {
remoteBranch := gui.getSelectedRemoteBranch()
if remoteBranch == nil {
return nil
}
if err := gui.handleCheckoutRef(remoteBranch.FullName(), handleCheckoutRefOptions{}); err != nil {
return err
}
return gui.switchBranchesPanelContext("local-branches")
return gui.switchContext(gui.Contexts.Remotes.Context)
}
func (gui *Gui) handleMergeRemoteBranch(g *gocui.Gui, v *gocui.View) error {
selectedBranchName := gui.getSelectedRemoteBranch().Name
selectedBranchName := gui.getSelectedRemoteBranch().FullName()
return gui.mergeBranchIntoCheckedOutBranch(selectedBranchName)
}
@@ -93,20 +52,25 @@ func (gui *Gui) handleDeleteRemoteBranch(g *gocui.Gui, v *gocui.View) error {
if remoteBranch == nil {
return nil
}
message := fmt.Sprintf("%s '%s/%s'?", gui.Tr.SLocalize("DeleteRemoteBranchMessage"), remoteBranch.RemoteName, remoteBranch.Name)
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("DeleteRemoteBranch"), message, func(*gocui.Gui, *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("DeletingStatus"), func() error {
if err := gui.GitCommand.DeleteRemoteBranch(remoteBranch.RemoteName, remoteBranch.Name); err != nil {
return err
}
message := fmt.Sprintf("%s '%s'?", gui.Tr.SLocalize("DeleteRemoteBranchMessage"), remoteBranch.FullName())
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
})
}, nil)
return gui.ask(askOpts{
title: gui.Tr.SLocalize("DeleteRemoteBranch"),
prompt: message,
handleConfirm: func() error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("DeletingStatus"), func() error {
if err := gui.GitCommand.DeleteRemoteBranch(remoteBranch.RemoteName, remoteBranch.Name); err != nil {
return err
}
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
})
},
})
}
func (gui *Gui) handleRebaseOntoRemoteBranch(g *gocui.Gui, v *gocui.View) error {
selectedBranchName := gui.getSelectedRemoteBranch().Name
selectedBranchName := gui.getSelectedRemoteBranch().FullName()
return gui.handleRebaseOntoBranch(selectedBranchName)
}
@@ -122,13 +86,17 @@ func (gui *Gui) handleSetBranchUpstream(g *gocui.Gui, v *gocui.View) error {
},
)
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("SetUpstreamTitle"), message, func(*gocui.Gui, *gocui.View) error {
if err := gui.GitCommand.SetBranchUpstream(selectedBranch.RemoteName, selectedBranch.Name, checkedOutBranch.Name); err != nil {
return err
}
return gui.ask(askOpts{
title: gui.Tr.SLocalize("SetUpstreamTitle"),
prompt: message,
handleConfirm: func() error {
if err := gui.GitCommand.SetBranchUpstream(selectedBranch.RemoteName, selectedBranch.Name, checkedOutBranch.Name); err != nil {
return err
}
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
}, nil)
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
},
})
}
func (gui *Gui) handleCreateResetToRemoteBranchMenu(g *gocui.Gui, v *gocui.View) error {
@@ -137,28 +105,5 @@ func (gui *Gui) handleCreateResetToRemoteBranchMenu(g *gocui.Gui, v *gocui.View)
return nil
}
return gui.createResetMenu(fmt.Sprintf("%s/%s", selectedBranch.RemoteName, selectedBranch.Name))
}
func (gui *Gui) handleNewBranchOffRemote(g *gocui.Gui, v *gocui.View) error {
branch := gui.getSelectedRemoteBranch()
if branch == nil {
return nil
}
message := gui.Tr.TemplateLocalize(
"NewBranchNameBranchOff",
Teml{
"branchName": branch.FullName(),
},
)
return gui.createPromptPanel(g, v, message, branch.FullName(), func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.NewBranch(gui.trimmedContent(v), branch.FullName()); err != nil {
return gui.surfaceError(err)
}
gui.State.Panels.Branches.SelectedLine = 0
if err := gui.switchBranchesPanelContext("local-branches"); err != nil {
return err
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
})
return gui.createResetMenu(selectedBranch.FullName())
}

View File

@@ -7,14 +7,13 @@ import (
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// list panel functions
func (gui *Gui) getSelectedRemote() *commands.Remote {
selectedLine := gui.State.Panels.Remotes.SelectedLine
selectedLine := gui.State.Panels.Remotes.SelectedLineIdx
if selectedLine == -1 || len(gui.State.Remotes) == 0 {
return nil
}
@@ -22,30 +21,21 @@ func (gui *Gui) getSelectedRemote() *commands.Remote {
return gui.State.Remotes[selectedLine]
}
func (gui *Gui) handleRemoteSelect(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
gui.State.SplitMainPanel = false
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
gui.getMainView().Title = "Remote"
func (gui *Gui) handleRemoteSelect() error {
var task updateTask
remote := gui.getSelectedRemote()
if remote == nil {
return gui.newStringTask("main", "No remotes")
}
v.FocusPoint(0, gui.State.Panels.Remotes.SelectedLine)
if gui.inDiffMode() {
return gui.renderDiff()
task = gui.createRenderStringTask("No remotes")
} else {
task = gui.createRenderStringTask(fmt.Sprintf("%s\nUrls:\n%s", utils.ColoredString(remote.Name, color.FgGreen), strings.Join(remote.Urls, "\n")))
}
return gui.newStringTask("main", fmt.Sprintf("%s\nUrls:\n%s", utils.ColoredString(remote.Name, color.FgGreen), strings.Join(remote.Urls, "\n")))
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: "Remote",
task: task,
},
})
}
func (gui *Gui) refreshRemotes() error {
@@ -68,35 +58,10 @@ func (gui *Gui) refreshRemotes() error {
}
}
// TODO: see if this works for deleting remote branches
switch gui.getBranchesView().Context {
case "remotes":
return gui.renderRemotesWithSelection()
case "remote-branches":
return gui.renderRemoteBranchesWithSelection()
}
return nil
return gui.postRefreshUpdate(gui.contextForContextKey(gui.getBranchesView().Context))
}
func (gui *Gui) renderRemotesWithSelection() error {
branchesView := gui.getBranchesView()
gui.refreshSelectedLine(&gui.State.Panels.Remotes.SelectedLine, len(gui.State.Remotes))
displayStrings := presentation.GetRemoteListDisplayStrings(gui.State.Remotes, gui.State.Diff.Ref)
gui.renderDisplayStrings(branchesView, displayStrings)
if gui.g.CurrentView() == branchesView && branchesView.Context == "remotes" {
if err := gui.handleRemoteSelect(gui.g, branchesView); err != nil {
return err
}
}
return nil
}
func (gui *Gui) handleRemoteEnter(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleRemoteEnter() error {
// naive implementation: get the branches and render them to the list, change the context
remote := gui.getSelectedRemote()
if remote == nil {
@@ -109,17 +74,14 @@ func (gui *Gui) handleRemoteEnter(g *gocui.Gui, v *gocui.View) error {
if len(remote.Branches) == 0 {
newSelectedLine = -1
}
gui.State.Panels.RemoteBranches.SelectedLine = newSelectedLine
gui.State.Panels.RemoteBranches.SelectedLineIdx = newSelectedLine
return gui.switchBranchesPanelContext("remote-branches")
return gui.switchContext(gui.Contexts.Remotes.Branches.Context)
}
func (gui *Gui) handleAddRemote(g *gocui.Gui, v *gocui.View) error {
branchesView := gui.getBranchesView()
return gui.createPromptPanel(g, branchesView, gui.Tr.SLocalize("newRemoteName"), "", func(g *gocui.Gui, v *gocui.View) error {
remoteName := gui.trimmedContent(v)
return gui.createPromptPanel(g, branchesView, gui.Tr.SLocalize("newRemoteUrl"), "", func(g *gocui.Gui, v *gocui.View) error {
remoteUrl := gui.trimmedContent(v)
return gui.prompt(gui.Tr.SLocalize("newRemoteName"), "", func(remoteName string) error {
return gui.prompt(gui.Tr.SLocalize("newRemoteUrl"), "", func(remoteUrl string) error {
if err := gui.GitCommand.AddRemote(remoteName, remoteUrl); err != nil {
return err
}
@@ -133,18 +95,21 @@ func (gui *Gui) handleRemoveRemote(g *gocui.Gui, v *gocui.View) error {
if remote == nil {
return nil
}
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("removeRemote"), gui.Tr.SLocalize("removeRemotePrompt")+" '"+remote.Name+"'?", func(*gocui.Gui, *gocui.View) error {
if err := gui.GitCommand.RemoveRemote(remote.Name); err != nil {
return err
}
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
return gui.ask(askOpts{
title: gui.Tr.SLocalize("removeRemote"),
prompt: gui.Tr.SLocalize("removeRemotePrompt") + " '" + remote.Name + "'?",
handleConfirm: func() error {
if err := gui.GitCommand.RemoveRemote(remote.Name); err != nil {
return err
}
}, nil)
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
},
})
}
func (gui *Gui) handleEditRemote(g *gocui.Gui, v *gocui.View) error {
branchesView := gui.getBranchesView()
remote := gui.getSelectedRemote()
if remote == nil {
return nil
@@ -157,9 +122,7 @@ func (gui *Gui) handleEditRemote(g *gocui.Gui, v *gocui.View) error {
},
)
return gui.createPromptPanel(g, branchesView, editNameMessage, "", func(g *gocui.Gui, v *gocui.View) error {
updatedRemoteName := gui.trimmedContent(v)
return gui.prompt(editNameMessage, remote.Name, func(updatedRemoteName string) error {
if updatedRemoteName != remote.Name {
if err := gui.GitCommand.RenameRemote(remote.Name, updatedRemoteName); err != nil {
return gui.surfaceError(err)
@@ -173,8 +136,13 @@ func (gui *Gui) handleEditRemote(g *gocui.Gui, v *gocui.View) error {
},
)
return gui.createPromptPanel(g, branchesView, editUrlMessage, "", func(g *gocui.Gui, v *gocui.View) error {
updatedRemoteUrl := gui.trimmedContent(v)
urls := remote.Urls
url := ""
if len(urls) > 0 {
url = urls[0]
}
return gui.prompt(editUrlMessage, url, func(updatedRemoteUrl string) error {
if err := gui.GitCommand.UpdateRemoteUrl(updatedRemoteName, updatedRemoteUrl); err != nil {
return gui.surfaceError(err)
}
@@ -190,6 +158,9 @@ func (gui *Gui) handleFetchRemote(g *gocui.Gui, v *gocui.View) error {
}
return gui.WithWaitingStatus(gui.Tr.SLocalize("FetchingRemoteStatus"), func() error {
gui.State.FetchMutex.Lock()
defer gui.State.FetchMutex.Unlock()
if err := gui.GitCommand.FetchRemote(remote.Name); err != nil {
return err
}

View File

@@ -12,23 +12,20 @@ func (gui *Gui) resetToRef(ref string, strength string, options commands.RunComm
return gui.surfaceError(err)
}
if err := gui.switchCommitsPanelContext("branch-commits"); err != nil {
return err
}
gui.State.Panels.Commits.SelectedLine = 0
gui.State.Panels.ReflogCommits.SelectedLine = 0
gui.State.Panels.Commits.SelectedLineIdx = 0
gui.State.Panels.ReflogCommits.SelectedLineIdx = 0
// loading a heap of commits is slow so we limit them whenever doing a reset
gui.State.Panels.Commits.LimitCommits = true
if err := gui.switchContext(gui.Contexts.BranchCommits.Context); err != nil {
return err
}
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES, BRANCHES, REFLOG, COMMITS}}); err != nil {
return err
}
if err := gui.resetOrigin(gui.getCommitsView()); err != nil {
return err
}
return gui.handleCommitSelect(gui.g, gui.getCommitsView())
return nil
}
func (gui *Gui) createResetMenu(ref string) error {

View File

@@ -11,8 +11,10 @@ import (
func (gui *Gui) handleOpenSearch(g *gocui.Gui, v *gocui.View) error {
gui.State.Searching.isSearching = true
gui.State.Searching.view = v
gui.renderString(gui.g, "search", "")
if err := gui.switchFocus(gui.g, v, gui.getSearchView()); err != nil {
gui.renderString("search", "")
if err := gui.switchContext(gui.Contexts.Search.Context); err != nil {
return err
}
@@ -21,11 +23,16 @@ func (gui *Gui) handleOpenSearch(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleSearch(g *gocui.Gui, v *gocui.View) error {
gui.State.Searching.searchString = gui.getSearchView().Buffer()
if err := gui.switchFocus(gui.g, nil, gui.State.Searching.view); err != nil {
if err := gui.returnFromContext(); err != nil {
return err
}
if err := gui.State.Searching.view.Search(gui.State.Searching.searchString); err != nil {
view := gui.State.Searching.view
if view == nil {
return nil
}
if err := view.Search(gui.State.Searching.searchString); err != nil {
return err
}
@@ -36,7 +43,6 @@ func (gui *Gui) onSelectItemWrapper(innerFunc func(int) error) func(int, int, in
return func(y int, index int, total int) error {
if total == 0 {
gui.renderString(
gui.g,
"search",
fmt.Sprintf(
"no matches for '%s' %s",
@@ -50,7 +56,6 @@ func (gui *Gui) onSelectItemWrapper(innerFunc func(int) error) func(int, int, in
return nil
}
gui.renderString(
gui.g,
"search",
fmt.Sprintf(
"matches for '%s' (%d of %d) %s",
@@ -86,9 +91,13 @@ func (gui *Gui) onSearchEscape() error {
}
func (gui *Gui) handleSearchEscape(g *gocui.Gui, v *gocui.View) error {
if err := gui.switchFocus(gui.g, nil, gui.State.Searching.view); err != nil {
if err := gui.onSearchEscape(); err != nil {
return err
}
return gui.onSearchEscape()
if err := gui.returnFromContext(); err != nil {
return err
}
return nil
}

61
pkg/gui/side_window.go Normal file
View File

@@ -0,0 +1,61 @@
package gui
import "github.com/jesseduffield/gocui"
func (gui *Gui) nextSideWindow() error {
windows := gui.getCyclableWindows()
currentWindow := gui.currentWindow()
var newWindow string
if currentWindow == "" || currentWindow == windows[len(windows)-1] {
newWindow = windows[0]
} else {
for i := range windows {
if currentWindow == windows[i] {
newWindow = windows[i+1]
break
}
if i == len(windows)-1 {
return nil
}
}
}
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
viewName := gui.getViewNameForWindow(newWindow)
return gui.switchContextToView(viewName)
}
func (gui *Gui) previousSideWindow() error {
windows := gui.getCyclableWindows()
currentWindow := gui.currentWindow()
var newWindow string
if currentWindow == "" || currentWindow == windows[0] {
newWindow = windows[len(windows)-1]
} else {
for i := range windows {
if currentWindow == windows[i] {
newWindow = windows[i-1]
break
}
if i == len(windows)-1 {
return nil
}
}
}
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
viewName := gui.getViewNameForWindow(newWindow)
return gui.switchContextToView(viewName)
}
func (gui *Gui) goToSideWindow(sideViewName string) func(g *gocui.Gui, v *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
return gui.switchContextToView(sideViewName)
}
}

View File

@@ -4,31 +4,21 @@ import (
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
)
func (gui *Gui) refreshStagingPanel(forceSecondaryFocused bool, selectedLineIdx int) error {
gui.State.SplitMainPanel = true
gui.splitMainPanel(true)
state := gui.State.Panels.LineByLine
// We need to force focus here because the confirmation panel for safely staging lines does not return focus automatically.
// This is because if we tell it to return focus it will unconditionally return it to the main panel which may not be what we want
// e.g. in the event that there's nothing left to stage.
if err := gui.switchFocus(gui.g, nil, gui.getMainView()); err != nil {
return err
}
file, err := gui.getSelectedFile()
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
}
return gui.handleStagingEscape(gui.g, nil)
file := gui.getSelectedFile()
if file == nil {
return gui.handleStagingEscape()
}
if !file.HasUnstagedChanges && !file.HasStagedChanges {
return gui.handleStagingEscape(gui.g, nil)
return gui.handleStagingEscape()
}
secondaryFocused := false
@@ -53,14 +43,14 @@ func (gui *Gui) refreshStagingPanel(forceSecondaryFocused bool, selectedLineIdx
}
// note for custom diffs, we'll need to send a flag here saying not to use the custom diff
diff := gui.GitCommand.Diff(file, true, secondaryFocused)
secondaryDiff := gui.GitCommand.Diff(file, true, !secondaryFocused)
diff := gui.GitCommand.WorktreeFileDiff(file, true, secondaryFocused)
secondaryDiff := gui.GitCommand.WorktreeFileDiff(file, true, !secondaryFocused)
// if we have e.g. a deleted file with nothing else to the diff will have only
// 4-5 lines in which case we'll swap panels
if len(strings.Split(diff, "\n")) < 5 {
if len(strings.Split(secondaryDiff, "\n")) < 5 {
return gui.handleStagingEscape(gui.g, nil)
return gui.handleStagingEscape()
}
secondaryFocused = !secondaryFocused
diff, secondaryDiff = secondaryDiff, diff
@@ -72,7 +62,7 @@ func (gui *Gui) refreshStagingPanel(forceSecondaryFocused bool, selectedLineIdx
}
if empty {
return gui.handleStagingEscape(gui.g, nil)
return gui.handleStagingEscape()
}
return nil
@@ -93,10 +83,10 @@ func (gui *Gui) handleTogglePanel(g *gocui.Gui, v *gocui.View) error {
return gui.refreshStagingPanel(false, -1)
}
func (gui *Gui) handleStagingEscape(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleStagingEscape() error {
gui.handleEscapeLineByLinePanel()
return gui.switchFocus(gui.g, nil, gui.getFilesView())
return gui.switchContext(gui.Contexts.Files.Context)
}
func (gui *Gui) handleToggleStagedSelection(g *gocui.Gui, v *gocui.View) error {
@@ -114,9 +104,21 @@ func (gui *Gui) handleResetSelection(g *gocui.Gui, v *gocui.View) error {
}
if !gui.Config.GetUserConfig().GetBool("gui.skipUnstageLineWarning") {
return gui.createConfirmationPanel(gui.g, gui.getMainView(), false, "unstage lines", "Are you sure you want to delete the selected lines (git reset)? It is irreversible.\nTo disable this dialogue set the config key of 'gui.skipUnstageLineWarning' to true", func(*gocui.Gui, *gocui.View) error {
return gui.applySelection(true)
}, nil)
return gui.ask(askOpts{
title: gui.Tr.SLocalize("UnstageLinesTitle"),
prompt: gui.Tr.SLocalize("UnstageLinesPrompt"),
handlersManageFocus: true,
handleConfirm: func() error {
if err := gui.switchContext(gui.Contexts.Staging.Context); err != nil {
return err
}
return gui.applySelection(true)
},
handleClose: func() error {
return gui.switchContext(gui.Contexts.Staging.Context)
},
})
} else {
return gui.applySelection(true)
}
@@ -125,12 +127,12 @@ func (gui *Gui) handleResetSelection(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) applySelection(reverse bool) error {
state := gui.State.Panels.LineByLine
file, err := gui.getSelectedFile()
if err != nil {
return err
file := gui.getSelectedFile()
if file == nil {
return nil
}
patch := commands.ModifiedPatchForRange(gui.Log, file.Name, state.Diff, state.FirstLineIdx, state.LastLineIdx, reverse, false)
patch := patch.ModifiedPatchForRange(gui.Log, file.Name, state.Diff, state.FirstLineIdx, state.LastLineIdx, reverse, false)
if patch == "" {
return nil
@@ -142,7 +144,7 @@ func (gui *Gui) applySelection(reverse bool) error {
if !reverse || state.SecondaryFocused {
applyFlags = append(applyFlags, "cached")
}
err = gui.GitCommand.ApplyPatch(patch, applyFlags...)
err := gui.GitCommand.ApplyPatch(patch, applyFlags...)
if err != nil {
return gui.surfaceError(err)
}

View File

@@ -3,13 +3,12 @@ package gui
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
)
// list panel functions
func (gui *Gui) getSelectedStashEntry() *commands.StashEntry {
selectedLine := gui.State.Panels.Stash.SelectedLine
selectedLine := gui.State.Panels.Stash.SelectedLineIdx
if selectedLine == -1 {
return nil
}
@@ -17,50 +16,30 @@ func (gui *Gui) getSelectedStashEntry() *commands.StashEntry {
return gui.State.StashEntries[selectedLine]
}
func (gui *Gui) handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
gui.State.SplitMainPanel = false
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
gui.getMainView().Title = "Stash"
func (gui *Gui) handleStashEntrySelect() error {
var task updateTask
stashEntry := gui.getSelectedStashEntry()
if stashEntry == nil {
return gui.newStringTask("main", gui.Tr.SLocalize("NoStashEntries"))
}
v.FocusPoint(0, gui.State.Panels.Stash.SelectedLine)
if gui.inDiffMode() {
return gui.renderDiff()
task = gui.createRenderStringTask(gui.Tr.SLocalize("NoStashEntries"))
} else {
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowStashEntryCmdStr(stashEntry.Index),
)
task = gui.createRunPtyTask(cmd)
}
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowStashEntryCmdStr(stashEntry.Index),
)
if err := gui.newPtyTask("main", cmd); err != nil {
gui.Log.Error(err)
}
return nil
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: "Stash",
task: task,
},
})
}
func (gui *Gui) refreshStashEntries(g *gocui.Gui) error {
gui.State.StashEntries = gui.GitCommand.GetStashEntries(gui.State.FilterPath)
func (gui *Gui) refreshStashEntries() error {
gui.State.StashEntries = gui.GitCommand.GetStashEntries(gui.State.Modes.Filtering.Path)
gui.refreshSelectedLine(&gui.State.Panels.Stash.SelectedLine, len(gui.State.StashEntries))
stashView := gui.getStashView()
displayStrings := presentation.GetStashEntryListDisplayStrings(gui.State.StashEntries, gui.State.Diff.Ref)
gui.renderDisplayStrings(stashView, displayStrings)
return gui.resetOrigin(stashView)
return gui.Contexts.Stash.Context.HandleRender()
}
// specific functions
@@ -69,47 +48,53 @@ func (gui *Gui) handleStashApply(g *gocui.Gui, v *gocui.View) error {
skipStashWarning := gui.Config.GetUserConfig().GetBool("gui.skipStashWarning")
apply := func() error {
return gui.stashDo(g, v, "apply")
return gui.stashDo("apply")
}
if skipStashWarning {
return apply()
}
title := gui.Tr.SLocalize("StashApply")
message := gui.Tr.SLocalize("SureApplyStashEntry")
return gui.createConfirmationPanel(g, v, true, title, message, func(g *gocui.Gui, v *gocui.View) error {
return apply()
}, nil)
return gui.ask(askOpts{
title: gui.Tr.SLocalize("StashApply"),
prompt: gui.Tr.SLocalize("SureApplyStashEntry"),
handleConfirm: func() error {
return apply()
},
})
}
func (gui *Gui) handleStashPop(g *gocui.Gui, v *gocui.View) error {
skipStashWarning := gui.Config.GetUserConfig().GetBool("gui.skipStashWarning")
pop := func() error {
return gui.stashDo(g, v, "pop")
return gui.stashDo("pop")
}
if skipStashWarning {
return pop()
}
title := gui.Tr.SLocalize("StashPop")
message := gui.Tr.SLocalize("SurePopStashEntry")
return gui.createConfirmationPanel(g, v, true, title, message, func(g *gocui.Gui, v *gocui.View) error {
return pop()
}, nil)
return gui.ask(askOpts{
title: gui.Tr.SLocalize("StashPop"),
prompt: gui.Tr.SLocalize("SurePopStashEntry"),
handleConfirm: func() error {
return pop()
},
})
}
func (gui *Gui) handleStashDrop(g *gocui.Gui, v *gocui.View) error {
title := gui.Tr.SLocalize("StashDrop")
message := gui.Tr.SLocalize("SureDropStashEntry")
return gui.createConfirmationPanel(g, v, true, title, message, func(g *gocui.Gui, v *gocui.View) error {
return gui.stashDo(g, v, "drop")
}, nil)
return gui.ask(askOpts{
title: gui.Tr.SLocalize("StashDrop"),
prompt: gui.Tr.SLocalize("SureDropStashEntry"),
handleConfirm: func() error {
return gui.stashDo("drop")
},
})
}
func (gui *Gui) stashDo(g *gocui.Gui, v *gocui.View, method string) error {
func (gui *Gui) stashDo(method string) error {
stashEntry := gui.getSelectedStashEntry()
if stashEntry == nil {
errorMessage := gui.Tr.TemplateLocalize(
@@ -130,15 +115,19 @@ func (gui *Gui) handleStashSave(stashFunc func(message string) error) error {
if len(gui.trackedFiles()) == 0 && len(gui.stagedFiles()) == 0 {
return gui.createErrorPanel(gui.Tr.SLocalize("NoTrackedStagedFilesStash"))
}
return gui.createPromptPanel(gui.g, gui.getFilesView(), gui.Tr.SLocalize("StashChanges"), "", func(g *gocui.Gui, v *gocui.View) error {
if err := stashFunc(gui.trimmedContent(v)); err != nil {
return gui.prompt(gui.Tr.SLocalize("StashChanges"), "", func(stashComment string) error {
if err := stashFunc(stashComment); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{scope: []int{STASH, FILES}})
})
}
func (gui *Gui) onStashPanelSearchSelect(selectedLine int) error {
gui.State.Panels.Stash.SelectedLine = selectedLine
return gui.handleStashEntrySelect(gui.g, gui.getStashView())
func (gui *Gui) handleViewStashFiles() error {
stashEntry := gui.getSelectedStashEntry()
if stashEntry == nil {
return nil
}
return gui.switchToCommitFilesContext(stashEntry.RefName(), false, gui.Contexts.Stash.Context, "stash")
}

View File

@@ -57,11 +57,24 @@ func cursorInSubstring(cx int, prefix string, substring string) bool {
func (gui *Gui) handleCheckForUpdate(g *gocui.Gui, v *gocui.View) error {
gui.Updater.CheckForNewUpdate(gui.onUserUpdateCheckFinish, true)
return gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("CheckingForUpdates"))
return gui.createLoaderPanel(v, gui.Tr.SLocalize("CheckingForUpdates"))
}
func (gui *Gui) handleStatusClick(g *gocui.Gui, v *gocui.View) error {
// TODO: move into some abstraction (status is currently not a listViewContext where a lot of this code lives)
if gui.popupPanelFocused() {
return nil
}
currentBranch := gui.currentBranch()
if currentBranch == nil {
// need to wait for branches to refresh
return nil
}
if err := gui.switchContext(gui.Contexts.Status.Context); err != nil {
return err
}
cx, _ := v.Cursor()
upstreamStatus := fmt.Sprintf("↑%s↓%s", currentBranch.Pushables, currentBranch.Pullables)
@@ -70,37 +83,26 @@ func (gui *Gui) handleStatusClick(g *gocui.Gui, v *gocui.View) error {
case "rebasing", "merging":
workingTreeStatus := fmt.Sprintf("(%s)", gui.GitCommand.WorkingTreeState())
if cursorInSubstring(cx, upstreamStatus+" ", workingTreeStatus) {
return gui.handleCreateRebaseOptionsMenu(gui.g, v)
return gui.handleCreateRebaseOptionsMenu()
}
if cursorInSubstring(cx, upstreamStatus+" "+workingTreeStatus+" ", repoName) {
return gui.handleCreateRecentReposMenu(gui.g, v)
return gui.handleCreateRecentReposMenu()
}
default:
if cursorInSubstring(cx, upstreamStatus+" ", repoName) {
return gui.handleCreateRecentReposMenu(gui.g, v)
return gui.handleCreateRecentReposMenu()
}
}
return gui.handleStatusSelect(gui.g, v)
return gui.handleStatusSelect()
}
func (gui *Gui) handleStatusSelect(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleStatusSelect() error {
// TODO: move into some abstraction (status is currently not a listViewContext where a lot of this code lives)
if gui.popupPanelFocused() {
return nil
}
gui.State.SplitMainPanel = false
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
gui.getMainView().Title = ""
if gui.inDiffMode() {
return gui.renderDiff()
}
magenta := color.New(color.FgMagenta)
dashboardString := strings.Join(
@@ -114,7 +116,12 @@ func (gui *Gui) handleStatusSelect(g *gocui.Gui, v *gocui.View) error {
magenta.Sprint("Become a sponsor (github is matching all donations for 12 months): https://github.com/sponsors/jesseduffield"), // caffeine ain't free
}, "\n\n")
return gui.newStringTask("main", dashboardString)
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: "",
task: gui.createRenderStringTask(dashboardString),
},
})
}
func (gui *Gui) handleOpenConfig(g *gocui.Gui, v *gocui.View) error {

View File

@@ -0,0 +1,109 @@
package gui
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
)
// list panel functions
func (gui *Gui) getSelectedSubCommit() *commands.Commit {
selectedLine := gui.State.Panels.SubCommits.SelectedLineIdx
commits := gui.State.SubCommits
if selectedLine == -1 || len(commits) == 0 {
return nil
}
return commits[selectedLine]
}
func (gui *Gui) handleSubCommitSelect() error {
commit := gui.getSelectedSubCommit()
var task updateTask
if commit == nil {
task = gui.createRenderStringTask("No commits")
} else {
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.Modes.Filtering.Path),
)
task = gui.createRunPtyTask(cmd)
}
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: "Commit",
task: task,
},
})
}
func (gui *Gui) handleCheckoutSubCommit(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedSubCommit()
if commit == nil {
return nil
}
err := gui.ask(askOpts{
title: gui.Tr.SLocalize("checkoutCommit"),
prompt: gui.Tr.SLocalize("SureCheckoutThisCommit"),
handleConfirm: func() error {
return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{})
},
})
if err != nil {
return err
}
gui.State.Panels.SubCommits.SelectedLineIdx = 0
return nil
}
func (gui *Gui) handleCreateSubCommitResetMenu() error {
commit := gui.getSelectedSubCommit()
return gui.createResetMenu(commit.Sha)
}
func (gui *Gui) handleViewSubCommitFiles() error {
commit := gui.getSelectedSubCommit()
if commit == nil {
return nil
}
return gui.switchToCommitFilesContext(commit.Sha, false, gui.Contexts.SubCommits.Context, "branches")
}
func (gui *Gui) switchToSubCommitsContext(refName string) error {
// need to populate my sub commits
builder := commands.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr, gui.State.Modes.CherryPicking.CherryPickedCommits)
commits, err := builder.GetCommits(
commands.GetCommitsOptions{
Limit: gui.State.Panels.Commits.LimitCommits,
FilterPath: gui.State.Modes.Filtering.Path,
IncludeRebaseCommits: false,
RefName: refName,
},
)
if err != nil {
return err
}
gui.State.SubCommits = commits
gui.State.Panels.SubCommits.refName = refName
gui.State.Panels.SubCommits.SelectedLineIdx = 0
gui.Contexts.SubCommits.Context.SetParentContext(gui.currentSideContext())
return gui.switchContext(gui.Contexts.SubCommits.Context)
}
func (gui *Gui) handleSwitchToSubCommits() error {
currentContext := gui.currentSideContext()
if currentContext == nil {
return nil
}
return gui.switchToSubCommitsContext(currentContext.GetSelectedItemId())
}

View File

@@ -3,13 +3,12 @@ package gui
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
)
// list panel functions
func (gui *Gui) getSelectedTag() *commands.Tag {
selectedLine := gui.State.Panels.Tags.SelectedLine
selectedLine := gui.State.Panels.Tags.SelectedLineIdx
if selectedLine == -1 || len(gui.State.Tags) == 0 {
return nil
}
@@ -17,37 +16,24 @@ func (gui *Gui) getSelectedTag() *commands.Tag {
return gui.State.Tags[selectedLine]
}
func (gui *Gui) handleTagSelect(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
gui.State.SplitMainPanel = false
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
gui.getMainView().Title = "Tag"
func (gui *Gui) handleTagSelect() error {
var task updateTask
tag := gui.getSelectedTag()
if tag == nil {
return gui.newStringTask("main", "No tags")
}
v.FocusPoint(0, gui.State.Panels.Tags.SelectedLine)
if gui.inDiffMode() {
return gui.renderDiff()
task = gui.createRenderStringTask("No tags")
} else {
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.GetBranchGraphCmdStr(tag.Name),
)
task = gui.createRunCommandTask(cmd)
}
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.GetBranchGraphCmdStr(tag.Name),
)
if err := gui.newCmdTask("main", cmd); err != nil {
gui.Log.Error(err)
}
return nil
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: "Tag",
task: task,
},
})
}
func (gui *Gui) refreshTags() error {
@@ -58,26 +44,7 @@ func (gui *Gui) refreshTags() error {
gui.State.Tags = tags
if gui.getBranchesView().Context == "tags" {
return gui.renderTagsWithSelection()
}
return nil
}
func (gui *Gui) renderTagsWithSelection() error {
branchesView := gui.getBranchesView()
gui.refreshSelectedLine(&gui.State.Panels.Tags.SelectedLine, len(gui.State.Tags))
displayStrings := presentation.GetTagListDisplayStrings(gui.State.Tags, gui.State.Diff.Ref)
gui.renderDisplayStrings(branchesView, displayStrings)
if gui.g.CurrentView() == branchesView && branchesView.Context == "tags" {
if err := gui.handleTagSelect(gui.g, branchesView); err != nil {
return gui.surfaceError(err)
}
}
return nil
return gui.postRefreshUpdate(gui.Contexts.Tags.Context)
}
func (gui *Gui) handleCheckoutTag(g *gocui.Gui, v *gocui.View) error {
@@ -88,7 +55,7 @@ func (gui *Gui) handleCheckoutTag(g *gocui.Gui, v *gocui.View) error {
if err := gui.handleCheckoutRef(tag.Name, handleCheckoutRefOptions{}); err != nil {
return err
}
return gui.switchBranchesPanelContext("local-branches")
return gui.switchContext(gui.Contexts.Branches.Context)
}
func (gui *Gui) handleDeleteTag(g *gocui.Gui, v *gocui.View) error {
@@ -104,12 +71,16 @@ func (gui *Gui) handleDeleteTag(g *gocui.Gui, v *gocui.View) error {
},
)
return gui.createConfirmationPanel(gui.g, v, true, gui.Tr.SLocalize("DeleteTagTitle"), prompt, func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.DeleteTag(tag.Name); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{COMMITS, TAGS}})
}, nil)
return gui.ask(askOpts{
title: gui.Tr.SLocalize("DeleteTagTitle"),
prompt: prompt,
handleConfirm: func() error {
if err := gui.GitCommand.DeleteTag(tag.Name); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{COMMITS, TAGS}})
},
})
}
func (gui *Gui) handlePushTag(g *gocui.Gui, v *gocui.View) error {
@@ -125,8 +96,8 @@ func (gui *Gui) handlePushTag(g *gocui.Gui, v *gocui.View) error {
},
)
return gui.createPromptPanel(gui.g, v, title, "origin", func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.PushTag(v.Buffer(), tag.Name); err != nil {
return gui.prompt(title, "origin", func(response string) error {
if err := gui.GitCommand.PushTag(response, tag.Name); err != nil {
return gui.surfaceError(err)
}
return nil
@@ -134,9 +105,8 @@ func (gui *Gui) handlePushTag(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleCreateTag(g *gocui.Gui, v *gocui.View) error {
return gui.createPromptPanel(gui.g, v, gui.Tr.SLocalize("CreateTagTitle"), "", func(g *gocui.Gui, v *gocui.View) error {
return gui.prompt(gui.Tr.SLocalize("CreateTagTitle"), "", func(tagName string) error {
// leaving commit SHA blank so that we're just creating the tag for the current commit
tagName := v.Buffer()
if err := gui.GitCommand.CreateLightweightTag(tagName, ""); err != nil {
return gui.surfaceError(err)
}
@@ -144,8 +114,11 @@ func (gui *Gui) handleCreateTag(g *gocui.Gui, v *gocui.View) error {
// find the index of the tag and set that as the currently selected line
for i, tag := range gui.State.Tags {
if tag.Name == tagName {
gui.State.Panels.Tags.SelectedLine = i
gui.renderTagsWithSelection()
gui.State.Panels.Tags.SelectedLineIdx = i
if err := gui.Contexts.Tags.Context.HandleRender(); err != nil {
gui.Log.Error(err)
}
return
}
}

View File

@@ -59,7 +59,7 @@ func (gui *Gui) newStringTask(viewName string, str string) error {
manager := gui.getManager(view)
f := func(stop chan struct{}) error {
gui.renderString(gui.g, viewName, str)
gui.renderString(viewName, str)
return nil
}

View File

@@ -166,24 +166,28 @@ func (gui *Gui) handleHardResetWithAutoStash(commitSha string, options handleHar
dirtyWorkingTree := len(gui.trackedFiles()) > 0
if dirtyWorkingTree {
// offer to autostash changes
return gui.createConfirmationPanel(gui.g, gui.getBranchesView(), true, gui.Tr.SLocalize("AutoStashTitle"), gui.Tr.SLocalize("AutoStashPrompt"), func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(options.WaitingStatus, func() error {
if err := gui.GitCommand.StashSave(gui.Tr.SLocalize("StashPrefix") + commitSha); err != nil {
return gui.surfaceError(err)
}
if err := reset(); err != nil {
return err
}
if err := gui.GitCommand.StashDo(0, "pop"); err != nil {
if err := gui.refreshSidePanels(refreshOptions{}); err != nil {
return gui.ask(askOpts{
title: gui.Tr.SLocalize("AutoStashTitle"),
prompt: gui.Tr.SLocalize("AutoStashPrompt"),
handleConfirm: func() error {
return gui.WithWaitingStatus(options.WaitingStatus, func() error {
if err := gui.GitCommand.StashSave(gui.Tr.SLocalize("StashPrefix") + commitSha); err != nil {
return gui.surfaceError(err)
}
if err := reset(); err != nil {
return err
}
return gui.surfaceError(err)
}
return nil
})
}, nil)
if err := gui.GitCommand.StashDo(0, "pop"); err != nil {
if err := gui.refreshSidePanels(refreshOptions{}); err != nil {
return err
}
return gui.surfaceError(err)
}
return nil
})
},
})
}
return gui.WithWaitingStatus(options.WaitingStatus, func() error {

View File

@@ -3,13 +3,14 @@ package gui
import "github.com/jesseduffield/gocui"
func (gui *Gui) showUpdatePrompt(newVersion string) error {
title := "New version available!"
message := "Download latest version? (enter/esc)"
currentView := gui.g.CurrentView()
return gui.createConfirmationPanel(gui.g, currentView, true, title, message, func(g *gocui.Gui, v *gocui.View) error {
gui.startUpdating(newVersion)
return nil
}, nil)
return gui.ask(askOpts{
title: "New version available!",
prompt: "Download latest version? (enter/esc)",
handleConfirm: func() error {
gui.startUpdating(newVersion)
return nil
},
})
}
func (gui *Gui) onUserUpdateCheckFinish(newVersion string, err error) error {
@@ -47,17 +48,19 @@ func (gui *Gui) startUpdating(newVersion string) {
func (gui *Gui) onUpdateFinish(err error) error {
gui.State.Updating = false
gui.statusManager.removeStatus("updating")
gui.renderString(gui.g, "appStatus", "")
gui.renderString("appStatus", "")
if err != nil {
return gui.createErrorPanel("Update failed: " + err.Error())
}
return nil
}
func (gui *Gui) createUpdateQuitConfirmation(g *gocui.Gui, v *gocui.View) error {
title := "Currently Updating"
message := "An update is in progress. Are you sure you want to quit?"
return gui.createConfirmationPanel(gui.g, v, true, title, message, func(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}, nil)
func (gui *Gui) createUpdateQuitConfirmation() error {
return gui.ask(askOpts{
title: "Currently Updating",
prompt: "An update is in progress. Are you sure you want to quit?",
handleConfirm: func() error {
return gocui.ErrQuit
},
})
}

View File

@@ -6,13 +6,14 @@ import (
"strings"
"sync"
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/spkg/bom"
)
var cyclableViews = []string{"status", "files", "branches", "commits", "stash"}
func (gui *Gui) getCyclableWindows() []string {
return []string{"status", "files", "branches", "commits", "stash"}
}
// models/views that we can refresh
const (
@@ -85,9 +86,9 @@ func (gui *Gui) refreshSidePanels(options refreshOptions) error {
wg.Add(1)
func() {
if options.mode == ASYNC {
go gui.refreshStashEntries(gui.g)
go gui.refreshStashEntries()
} else {
gui.refreshStashEntries(gui.g)
gui.refreshStashEntries()
}
wg.Done()
}()
@@ -138,198 +139,6 @@ func (gui *Gui) refreshSidePanels(options refreshOptions) error {
return nil
}
func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error {
var focusedViewName string
if v == nil || v.Name() == cyclableViews[len(cyclableViews)-1] {
focusedViewName = cyclableViews[0]
} else {
// if we're in the commitFiles view we'll act like we're in the commits view
viewName := v.Name()
if viewName == "commitFiles" {
viewName = "commits"
}
for i := range cyclableViews {
if viewName == cyclableViews[i] {
focusedViewName = cyclableViews[i+1]
break
}
if i == len(cyclableViews)-1 {
message := gui.Tr.TemplateLocalize(
"IssntListOfViews",
Teml{
"name": viewName,
},
)
gui.Log.Info(message)
return nil
}
}
}
focusedView, err := g.View(focusedViewName)
if err != nil {
panic(err)
}
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
return gui.switchFocus(g, v, focusedView)
}
func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error {
var focusedViewName string
if v == nil || v.Name() == cyclableViews[0] {
focusedViewName = cyclableViews[len(cyclableViews)-1]
} else {
// if we're in the commitFiles view we'll act like we're in the commits view
viewName := v.Name()
if viewName == "commitFiles" {
viewName = "commits"
}
for i := range cyclableViews {
if viewName == cyclableViews[i] {
focusedViewName = cyclableViews[i-1] // TODO: make this work properly
break
}
if i == len(cyclableViews)-1 {
message := gui.Tr.TemplateLocalize(
"IssntListOfViews",
Teml{
"name": viewName,
},
)
gui.Log.Info(message)
return nil
}
}
}
focusedView, err := g.View(focusedViewName)
if err != nil {
panic(err)
}
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
return gui.switchFocus(g, v, focusedView)
}
func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error {
switch v.Name() {
case "menu":
return gui.handleMenuSelect(g, v)
case "status":
return gui.handleStatusSelect(g, v)
case "files":
return gui.focusAndSelectFile(g, v)
case "branches":
branchesView := gui.getBranchesView()
switch branchesView.Context {
case "local-branches":
return gui.handleBranchSelect(g, v)
case "remotes":
return gui.handleRemoteSelect(g, v)
case "remote-branches":
return gui.handleRemoteBranchSelect(g, v)
case "tags":
return gui.handleTagSelect(g, v)
default:
return errors.New("unknown branches panel context: " + branchesView.Context)
}
case "commits":
return gui.handleCommitSelect(g, v)
case "commitFiles":
return gui.handleCommitFileSelect(g, v)
case "stash":
return gui.handleStashEntrySelect(g, v)
case "confirmation":
return nil
case "commitMessage":
return gui.handleCommitFocused(g, v)
case "credentials":
return gui.handleCredentialsViewFocused(g, v)
case "main":
if gui.State.MainContext == "merging" {
return gui.refreshMergePanel()
}
v.Highlight = false
return nil
case "search":
return nil
default:
panic(gui.Tr.SLocalize("NoViewMachingNewLineFocusedSwitchStatement"))
}
}
func (gui *Gui) returnFocus(g *gocui.Gui, v *gocui.View) error {
previousView, err := g.View(gui.State.PreviousView)
if err != nil {
// always fall back to files view if there's no 'previous' view stored
previousView, err = g.View("files")
if err != nil {
gui.Log.Error(err)
}
}
return gui.switchFocus(g, v, previousView)
}
func (gui *Gui) goToSideView(sideViewName string) func(g *gocui.Gui, v *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
view, err := g.View(sideViewName)
if err != nil {
gui.Log.Error(err)
return nil
}
err = gui.closePopupPanels()
if err != nil {
gui.Log.Error(err)
return nil
}
return gui.switchFocus(g, nil, view)
}
}
func (gui *Gui) closePopupPanels() error {
gui.onNewPopupPanel()
err := gui.closeConfirmationPrompt(gui.g, true)
if err != nil {
gui.Log.Error(err)
return err
}
return nil
}
// pass in oldView = nil if you don't want to be able to return to your old view
// TODO: move some of this logic into our onFocusLost and onFocus hooks
func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error {
// we assume we'll never want to return focus to a popup panel i.e.
// we should never stack popup panels
if oldView != nil && !gui.isPopupPanel(oldView.Name()) {
gui.State.PreviousView = oldView.Name()
}
gui.Log.Info("setting highlight to true for view" + newView.Name())
message := gui.Tr.TemplateLocalize(
"newFocusedViewIs",
Teml{
"newFocusedView": newView.Name(),
},
)
gui.Log.Info(message)
if _, err := g.SetCurrentView(newView.Name()); err != nil {
return err
}
if _, err := g.SetViewOnTop(newView.Name()); err != nil {
return err
}
g.Cursor = newView.Editable
if err := gui.renderPanelOptions(); err != nil {
return err
}
return gui.newLineFocused(g, newView)
}
func (gui *Gui) resetOrigin(v *gocui.View) error {
_ = v.SetCursor(0, 0)
return v.SetOrigin(0, 0)
@@ -346,23 +155,27 @@ func (gui *Gui) setViewContent(v *gocui.View, s string) {
}
// renderString resets the origin of a view and sets its content
func (gui *Gui) renderString(g *gocui.Gui, viewName, s string) {
g.Update(func(*gocui.Gui) error {
v, err := g.View(viewName)
if err != nil {
return nil // return gracefully if view has been deleted
}
if err := v.SetOrigin(0, 0); err != nil {
return err
}
if err := v.SetCursor(0, 0); err != nil {
return err
}
gui.setViewContent(v, s)
return nil
func (gui *Gui) renderString(viewName, s string) {
gui.g.Update(func(*gocui.Gui) error {
return gui.renderStringSync(viewName, s)
})
}
func (gui *Gui) renderStringSync(viewName, s string) error {
v, err := gui.g.View(viewName)
if err != nil {
return nil // return gracefully if view has been deleted
}
if err := v.SetOrigin(0, 0); err != nil {
return err
}
if err := v.SetCursor(0, 0); err != nil {
return err
}
gui.setViewContent(v, s)
return nil
}
func (gui *Gui) optionsMapToString(optionsMap map[string]string) string {
optionsArray := make([]string, 0)
for key, description := range optionsMap {
@@ -372,9 +185,8 @@ func (gui *Gui) optionsMapToString(optionsMap map[string]string) string {
return strings.Join(optionsArray, ", ")
}
func (gui *Gui) renderOptionsMap(optionsMap map[string]string) error {
gui.renderString(gui.g, "options", gui.optionsMapToString(optionsMap))
return nil
func (gui *Gui) renderOptionsMap(optionsMap map[string]string) {
gui.renderString("options", gui.optionsMapToString(optionsMap))
}
// TODO: refactor properly
@@ -434,56 +246,74 @@ func (gui *Gui) getStatusView() *gocui.View {
return v
}
func (gui *Gui) getConfirmationView() *gocui.View {
v, _ := gui.g.View("confirmation")
return v
}
func (gui *Gui) trimmedContent(v *gocui.View) string {
return strings.TrimSpace(v.Buffer())
}
func (gui *Gui) currentViewName() string {
currentView := gui.g.CurrentView()
if currentView == nil {
return ""
}
return currentView.Name()
}
func (gui *Gui) resizeCurrentPopupPanel(g *gocui.Gui) error {
v := g.CurrentView()
func (gui *Gui) resizeCurrentPopupPanel() error {
v := gui.g.CurrentView()
if v == nil {
return nil
}
if gui.isPopupPanel(v.Name()) {
return gui.resizePopupPanel(g, v)
return gui.resizePopupPanel(v)
}
return nil
}
func (gui *Gui) resizePopupPanel(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) resizePopupPanel(v *gocui.View) error {
// If the confirmation panel is already displayed, just resize the width,
// otherwise continue
content := v.Buffer()
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, v.Wrap, content)
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(v.Wrap, content)
vx0, vy0, vx1, vy1 := v.Dimensions()
if vx0 == x0 && vy0 == y0 && vx1 == x1 && vy1 == y1 {
return nil
}
gui.Log.Info(gui.Tr.SLocalize("resizingPopupPanel"))
_, err := g.SetView(v.Name(), x0, y0, x1, y1, 0)
_, err := gui.g.SetView(v.Name(), x0, y0, x1, y1, 0)
return err
}
func (gui *Gui) changeSelectedLine(line *int, total int, change int) {
func (gui *Gui) changeSelectedLine(panelState IListPanelState, total int, change int) {
// TODO: find out why we're doing this
if *line == -1 {
line := panelState.GetSelectedLineIdx()
if line == -1 {
return
}
if *line+change < 0 {
*line = 0
} else if *line+change >= total {
*line = total - 1
var newLine int
if line+change < 0 {
newLine = 0
} else if line+change >= total {
newLine = total - 1
} else {
*line += change
newLine = line + change
}
panelState.SetSelectedLineIdx(newLine)
}
func (gui *Gui) refreshSelectedLine(line *int, total int) {
if *line == -1 && total > 0 {
*line = 0
} else if total-1 < *line {
*line = total - 1
func (gui *Gui) refreshSelectedLine(panelState IListPanelState, total int) {
line := panelState.GetSelectedLineIdx()
if line == -1 && total > 0 {
panelState.SetSelectedLineIdx(0)
} else if total-1 < line {
panelState.SetSelectedLineIdx(total - 1)
}
}
@@ -496,27 +326,15 @@ func (gui *Gui) renderDisplayStrings(v *gocui.View, displayStrings [][]string) {
})
}
func (gui *Gui) renderPanelOptions() error {
currentView := gui.g.CurrentView()
switch currentView.Name() {
case "menu":
return gui.renderMenuOptions()
case "main":
if gui.State.MainContext == "merging" {
return gui.renderMergeOptions()
}
}
return gui.renderGlobalOptions()
}
func (gui *Gui) renderGlobalOptions() error {
return gui.renderOptionsMap(map[string]string{
func (gui *Gui) globalOptionsMap() map[string]string {
return map[string]string{
fmt.Sprintf("%s/%s", gui.getKeyDisplay("universal.scrollUpMain"), gui.getKeyDisplay("universal.scrollDownMain")): gui.Tr.SLocalize("scroll"),
fmt.Sprintf("%s %s %s %s", gui.getKeyDisplay("universal.prevBlock"), gui.getKeyDisplay("universal.nextBlock"), gui.getKeyDisplay("universal.prevItem"), gui.getKeyDisplay("universal.nextItem")): gui.Tr.SLocalize("navigate"),
fmt.Sprintf("%s/%s", gui.getKeyDisplay("universal.return"), gui.getKeyDisplay("universal.quit")): gui.Tr.SLocalize("close"),
gui.getKeyDisplay("universal.return"): gui.Tr.SLocalize("cancel"),
gui.getKeyDisplay("universal.quit"): gui.Tr.SLocalize("quit"),
gui.getKeyDisplay("universal.optionMenu"): gui.Tr.SLocalize("menu"),
"1-5": gui.Tr.SLocalize("jump"),
})
}
}
func (gui *Gui) isPopupPanel(viewName string) bool {
@@ -527,30 +345,6 @@ func (gui *Gui) popupPanelFocused() bool {
return gui.isPopupPanel(gui.currentViewName())
}
func (gui *Gui) handleClick(v *gocui.View, itemCount int, selectedLine *int, handleSelect func(*gocui.Gui, *gocui.View) error) error {
if gui.popupPanelFocused() && v != nil && !gui.isPopupPanel(v.Name()) {
return nil
}
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
newSelectedLine := v.SelectedLineIdx()
if newSelectedLine < 0 {
newSelectedLine = 0
}
if newSelectedLine > itemCount-1 {
newSelectedLine = itemCount - 1
}
*selectedLine = newSelectedLine
return handleSelect(gui.g, v)
}
// often gocui wants functions in the form `func(g *gocui.Gui, v *gocui.View) error`
// but sometimes we just have a function that returns an error, so this is a
// convenience wrapper to give gocui what it wants.
@@ -559,3 +353,34 @@ func (gui *Gui) wrappedHandler(f func() error) func(g *gocui.Gui, v *gocui.View)
return f()
}
}
// secondaryViewFocused tells us whether it appears that the secondary view is focused. The view is actually never focused for real: we just swap the main and secondary views and then you're still focused on the main view so that we can give you access to all its keybindings for free. I will probably regret this design decision soon enough.
func (gui *Gui) secondaryViewFocused() bool {
return gui.State.Panels.LineByLine != nil && gui.State.Panels.LineByLine.SecondaryFocused
}
func (gui *Gui) clearEditorView(v *gocui.View) {
v.Clear()
_ = v.SetCursor(0, 0)
_ = v.SetOrigin(0, 0)
}
func (gui *Gui) onViewTabClick(viewName string, tabIndex int) error {
context := gui.ViewTabContextMap[viewName][tabIndex].contexts[0]
return gui.switchContext(context)
}
func (gui *Gui) handleNextTab(g *gocui.Gui, v *gocui.View) error {
return gui.onViewTabClick(
v.Name(),
utils.ModuloWithWrap(v.TabIndex+1, len(v.Tabs)),
)
}
func (gui *Gui) handlePrevTab(g *gocui.Gui, v *gocui.View) error {
return gui.onViewTabClick(
v.Name(),
utils.ModuloWithWrap(v.TabIndex-1, len(v.Tabs)),
)
}

42
pkg/gui/window.go Normal file
View File

@@ -0,0 +1,42 @@
package gui
// A window refers to a place on the screen which can hold one or more views.
// A view is a box that renders content, and within a window only one view will
// appear at a time. When a view appears within a window, it occupies the whole
// space. Right now most windows are 1:1 with views, except for commitFiles which
// is a view that moves between windows
func (gui *Gui) getViewNameForWindow(window string) string {
viewName, ok := gui.State.WindowViewNameMap[window]
if !ok {
return window
}
return viewName
}
func (gui *Gui) getWindowForViewName(viewName string) string {
if viewName == "commitFiles" {
return gui.Contexts.CommitFiles.Context.GetWindowName()
}
return viewName
}
func (gui *Gui) setViewAsActiveForWindow(viewName string) {
if gui.State.WindowViewNameMap == nil {
gui.State.WindowViewNameMap = map[string]string{}
}
gui.State.WindowViewNameMap[gui.getWindowForViewName(viewName)] = viewName
}
func (gui *Gui) currentWindow() string {
return gui.getWindowForViewName(gui.currentViewName())
}
func (gui *Gui) resetWindowForView(viewName string) {
window := gui.getWindowForViewName(viewName)
// we assume here that the window contains as its default view a view with the same name as the window
gui.State.WindowViewNameMap[window] = window
}

View File

@@ -28,24 +28,33 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "CommitsTitle",
Other: "Commits",
}, &i18n.Message{
ID: "CommitsDiffTitle",
Other: "Commits (specific diff mode)",
}, &i18n.Message{
ID: "CommitsDiff",
Other: "select commit to diff with another commit",
}, &i18n.Message{
ID: "StashTitle",
Other: "Stash",
}, &i18n.Message{
ID: "UnstagedChanges",
Other: `Unstaged Changes`,
Other: `Unstaged wijzigingen`,
}, &i18n.Message{
ID: "StagedChanges",
Other: `Staged Changes`,
Other: `Staged Wijzigingen`,
}, &i18n.Message{
ID: "PatchBuildingMainTitle",
Other: `Voeg lijnen/hunks toe aan Patch`,
}, &i18n.Message{
ID: "MergingMainTitle",
Other: "Resolve merge conflicts",
Other: "Los merge conflicten op",
}, &i18n.Message{
ID: "MainTitle",
Other: "Hooft",
}, &i18n.Message{
ID: "StagingTitle",
Other: "Staging",
}, &i18n.Message{
ID: "MergingTitle",
Other: "Merging",
}, &i18n.Message{
ID: "NormalTitle",
Other: "Normaal",
}, &i18n.Message{
ID: "CommitMessage",
Other: "Commit bericht",
@@ -78,7 +87,7 @@ func addDutch(i18nObject *i18n.Bundle) error {
Other: "Status",
}, &i18n.Message{
ID: "GlobalTitle",
Other: "Global",
Other: "Globaal",
}, &i18n.Message{
ID: "navigate",
Other: "navigeer",
@@ -124,6 +133,9 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "resolveMergeConflicts",
Other: "los merge conflicten op",
}, &i18n.Message{
ID: "MergeConflictsTitle",
Other: "Merge Conflicten",
}, &i18n.Message{
ID: "checkout",
Other: "uitchecken",
@@ -136,9 +148,6 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "CannotGitAdd",
Other: "Kan commando niet uitvoeren git add --path untracked files",
}, &i18n.Message{
ID: "NoStagedFilesToCommit",
Other: "Er zijn geen staged bestanden om te commiten",
}, &i18n.Message{
ID: "NoFilesDisplay",
Other: "Geen bestanden om te laten zien",
@@ -157,6 +166,9 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "FileNoMergeCons",
Other: "Dit bestand heeft geen merge conflicten",
}, &i18n.Message{
ID: "softReset",
Other: "zacht reset",
}, &i18n.Message{
ID: "SureTo",
Other: "Weet je het zeker dat je {{.fileName}} wilt {{.deleteVerb}} (je veranderingen zullen worden verwijderd)",
@@ -190,6 +202,9 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "rebaseBranch",
Other: "rebase branch",
}, &i18n.Message{
ID: "CantRebaseOntoSelf",
Other: "Je kan niet een branch rebasen op zichzelf",
}, &i18n.Message{
ID: "CantMergeBranchIntoItself",
Other: "Je kan niet een branch in zichzelf mergen",
@@ -217,15 +232,21 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "NoTrackingThisBranch",
Other: "deze branch wordt niet gevolgd",
}, &i18n.Message{
ID: "CommitMessageConfirm",
Other: "{{.keyBindClose}}: Sluiten, {{.keyBindNewLine}}: Nieuwe lijn, {{.keyBindConfirm}}: Bevestig",
}, &i18n.Message{
ID: "CommitWithoutMessageErr",
Other: "Je kan geen commit maken zonder commit bericht",
}, &i18n.Message{
ID: "CloseConfirm",
Other: "{{.keyBindClose}}: Sluiten, {{.keyBindConfirm}}: Bevestigen",
Other: "{{.keyBindClose}}: Sluiten, {{.keyBindConfirm}}: Bevestig",
}, &i18n.Message{
ID: "close",
Other: "sluiten",
}, &i18n.Message{
ID: "quit",
Other: "quit",
}, &i18n.Message{
ID: "SureResetThisCommit",
Other: "Weet je het zeker dat je wil resetten naar deze commit?",
@@ -262,18 +283,48 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "SureFixupThisCommit",
Other: "Weet je zeker dat je fixup wil uitvoeren op deze commit? De commit hieronder zol worden squashed in deze",
}, &i18n.Message{
ID: "SureSquashThisCommit",
Other: "Weet je zeker dat je deze commit wil samenvoegen met de commit hieronder?",
}, &i18n.Message{
ID: "Squash",
Other: "Squash",
}, &i18n.Message{
ID: "pickCommit",
Other: "kies commit (wanneer midden in rebase)",
}, &i18n.Message{
ID: "revertCommit",
Other: "commit ongedaan maken",
}, &i18n.Message{
ID: "OnlyRenameTopCommit",
Other: "Je kan alleen de bovenste commit hernoemen",
}, &i18n.Message{
ID: "renameCommit",
Other: "hernoem commit",
}, &i18n.Message{
ID: "deleteCommit",
Other: "verwijder commit",
}, &i18n.Message{
ID: "moveDownCommit",
Other: "verplaats commit 1 naar beneden",
}, &i18n.Message{
ID: "moveUpCommit",
Other: "verplaats commit 1 naar boven",
}, &i18n.Message{
ID: "editCommit",
Other: "wijzig commit",
}, &i18n.Message{
ID: "amendToCommit",
Other: "wijzig commit met staged veranderingen",
}, &i18n.Message{
ID: "renameCommitEditor",
Other: "rename commit with editor",
Other: "hernoem commit met editor",
}, &i18n.Message{
ID: "PotentialErrInGetselectedCommit",
Other: "Er is mogelijk een error in getSelected Commit (geen match tussen ui en state)",
}, &i18n.Message{
ID: "NoCommitsThisBranch",
Other: "Geen commits in deze branch",
}, &i18n.Message{
ID: "Error",
Other: "Foutmelding",
@@ -298,12 +349,18 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "undo",
Other: "ongedaan maken",
}, &i18n.Message{
ID: "undoReflog",
Other: "ongedaan maken (via reflog) (experimenteel)",
}, &i18n.Message{
ID: "redoReflog",
Other: "redo (via reflog) (experimenteel)",
}, &i18n.Message{
ID: "pop",
Other: "pop",
}, &i18n.Message{
ID: "drop",
Other: "drop",
Other: "laten vallen",
}, &i18n.Message{
ID: "apply",
Other: "toepassen",
@@ -312,10 +369,22 @@ func addDutch(i18nObject *i18n.Bundle) error {
Other: "Geen stash items",
}, &i18n.Message{
ID: "StashDrop",
Other: "Stash drop",
Other: "Stash laten vallen",
}, &i18n.Message{
ID: "SureDropStashEntry",
Other: "Weet je het zeker dat je deze stash entry wil laten vallen?",
}, &i18n.Message{
ID: "StashPop",
Other: "Stash pop",
}, &i18n.Message{
ID: "SurePopStashEntry",
Other: "Weet je zeker dat je deze stash entry wil poppen?",
}, &i18n.Message{
ID: "StashApply",
Other: "Stash toepassen",
}, &i18n.Message{
ID: "SureApplyStashEntry",
Other: "Weet je zeker dat je deze stash entry wil toepassen?",
}, &i18n.Message{
ID: "NoStashTo",
Other: "Geen stash voor {{.method}}",
@@ -328,21 +397,21 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "IssntListOfViews",
Other: "{{.name}} is niet in de lijst van weergaves",
}, &i18n.Message{
ID: "NoViewMachingNewLineFocusedSwitchStatement",
Other: "Er machen geen weergave met de newLineFocused switch declaratie",
}, &i18n.Message{
ID: "newFocusedViewIs",
Other: "nieuw gefocussed weergave is {{.newFocusedView}}",
}, &i18n.Message{
ID: "NoChangedFiles",
Other: "Geen veranderde bestanden",
}, &i18n.Message{
ID: "MergeAborted",
Other: "Merge afgebroken",
}, &i18n.Message{
ID: "OpenConfig",
Other: "open config file",
Other: "open config bestand",
}, &i18n.Message{
ID: "EditConfig",
Other: "verander config file",
Other: "verander config bestand",
}, &i18n.Message{
ID: "ForcePush",
Other: "Forceer push",
@@ -370,6 +439,15 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "AnonymousReportingPrompt",
Other: "Zou je anonieme data rapportage willen aanzetten om lazygit beter te kunnen maken? (enter/esc)",
}, &i18n.Message{
ID: "IntroPopupMessage",
Other: `Bedankt voor het gebruik maken van lazygit! 2 dingen die je moet weten:
1) Als je meer van lazygit zijn features wilt leren bekijk dan deze video:
https://youtu.be/CPLdltN7wgE
3) Als je git gebruikt, ben je een programmeur! Met jouw hulp kunnen we lazygit verbeteren, dus overweeg om een donateur te worden en mee te doen aan het plezier op
https://github.com/jesseduffield/lazygit`,
}, &i18n.Message{
ID: "GitconfigParseErr",
Other: `Gogit kon je gitconfig bestand niet goed parsen door de aanwezigheid van losstaande '\' tekens. Het weghalen van deze tekens zou het probleem moeten oplossen. `,
@@ -419,14 +497,29 @@ func addDutch(i18nObject *i18n.Bundle) error {
ID: "FileStagingRequirements",
Other: `Kan alleen individuele lijnen stagen van getrackte bestanden met onstaged veranderingen`,
}, &i18n.Message{
ID: "StagingTitle",
Other: `Stage Lines/Hunks`,
ID: "SelectHunk",
Other: `selecteer hunk`,
}, &i18n.Message{
ID: "StageHunk",
Other: `stage hunk`,
ID: "StageSelection",
Other: `toggle lijnen staged / unstaged`,
}, &i18n.Message{
ID: "StageLine",
Other: `stage lijn`,
ID: "ResetSelection",
Other: `verwijdert change (git reset)`,
}, &i18n.Message{
ID: "ToggleDragSelect",
Other: `toggle drag selecteer`,
}, &i18n.Message{
ID: "ToggleSelectHunk",
Other: `toggle selecteer hunk`,
}, &i18n.Message{
ID: "ToggleSelectionForPatch",
Other: `voeg toe/verwijder lijn(en) in patch`,
}, &i18n.Message{
ID: "TogglePanel",
Other: `ga naar een ander paneel`,
}, &i18n.Message{
ID: "CantStageStaged",
Other: `Je kan niet al gestaged verandering stagen!`,
}, &i18n.Message{
ID: "ReturnToFilesPanel",
Other: `ga terug naar het bestanden paneel`,
@@ -437,65 +530,11 @@ func addDutch(i18nObject *i18n.Bundle) error {
ID: "CantFindHunk",
Other: `Kan geen hunk vinden`,
}, &i18n.Message{
ID: "RebasingTitle",
Other: "Rebasing",
ID: "FastForward",
Other: `fast-forward deze branch vanaf zijn upstream`,
}, &i18n.Message{
ID: "MergingTitle",
Other: "Merging",
}, &i18n.Message{
ID: "ConfirmRebase",
Other: "Weet je zeker dat je {{.checkedOutBranch}} op {{.selectedBranch}} wil rebasen?",
}, &i18n.Message{
ID: "ConfirmMerge",
Other: "Weet je zeker dat je {{.selectedBranch}} in {{.checkedOutBranch}} wil mergen?",
}, &i18n.Message{
ID: "FwdNoUpstream",
Other: "Kan niet de branch vooruitspoelen zonder upstream",
}, &i18n.Message{
ID: "ErrorOccurred",
Other: "Er is iets fout gegaan! Zou je hier een issue aan willen maken: https://github.com/jesseduffield/lazygit/issues",
}, &i18n.Message{
ID: "FwdCommitsToPush",
Other: "Je kan niet vooruitspoelen als de branch geen nieuwe commits heeft",
}, &i18n.Message{
ID: "MainTitle",
Other: "Hoofd",
}, &i18n.Message{
ID: "NormalTitle",
Other: "Normaal",
}, &i18n.Message{
ID: "softReset",
Other: "zacht reset",
}, &i18n.Message{
ID: "CantRebaseOntoSelf",
Other: "Je kan niet een branch rebasen op zichzelf",
}, &i18n.Message{
ID: "SureSquashThisCommit",
Other: "Weet je zeker dat je deze commit wil samenvoegen met de commit hieronder?",
}, &i18n.Message{
ID: "Squash",
Other: "Squash",
}, &i18n.Message{
ID: "pickCommit",
Other: "pick commit (when mid-rebase)",
}, &i18n.Message{
ID: "revertCommit",
Other: "commit omgedaan maken",
}, &i18n.Message{
ID: "deleteCommit",
Other: "verwijder commit",
}, &i18n.Message{
ID: "moveDownCommit",
Other: "verplaats commit 1 omlaag",
}, &i18n.Message{
ID: "moveUpCommit",
Other: "verplaats commit 1 omhoog",
}, &i18n.Message{
ID: "editCommit",
Other: "verander commit",
}, &i18n.Message{
ID: "amendToCommit",
Other: "wijzig commit met staged veranderingen",
ID: "Fetching",
Other: "fetching en fast-forwarding {{.from}} -> {{.to}} ...",
}, &i18n.Message{
ID: "FoundConflicts",
Other: "Conflicten!, Om af te breken druk 'esc', anders druk op 'enter'",
@@ -507,10 +546,10 @@ func addDutch(i18nObject *i18n.Bundle) error {
Other: "ongedaan maken",
}, &i18n.Message{
ID: "PickHunk",
Other: "pick hunk",
Other: "kies hunk",
}, &i18n.Message{
ID: "PickBothHunks",
Other: "pick beide hunks",
Other: "kies bijde hunks",
}, &i18n.Message{
ID: "ViewMergeRebaseOptions",
Other: "bekijk merge/rebase opties",
@@ -519,16 +558,85 @@ func addDutch(i18nObject *i18n.Bundle) error {
Other: "Je bent momenteel niet aan het rebasen of mergen",
}, &i18n.Message{
ID: "RecentRepos",
Other: "recent repositories",
Other: "recente repositories",
}, &i18n.Message{
ID: "MergeOptionsTitle",
Other: "Merge Opties",
}, &i18n.Message{
ID: "RebaseOptionsTitle",
Other: "Rebase Opties",
}, &i18n.Message{
ID: "CommitMessageTitle",
Other: "Commit Bericht",
}, &i18n.Message{
ID: "Local-BranchesTitle",
Other: "Branches Tab",
}, &i18n.Message{
ID: "SearchTitle",
Other: "Zoek",
}, &i18n.Message{
ID: "TagsTitle",
Other: "Tags Tab",
}, &i18n.Message{
ID: "Branch-CommitsTitle",
Other: "Commits Tab",
}, &i18n.Message{
ID: "MenuTitle",
Other: "Menu",
}, &i18n.Message{
ID: "RemotesTitle",
Other: "Remotes Tab",
}, &i18n.Message{
ID: "CredentialsTitle",
Other: "Credentials",
}, &i18n.Message{
ID: "Remote-BranchesTitle",
Other: "Remote Branches (in Remotes tab)",
}, &i18n.Message{
ID: "Patch-BuildingTitle",
Other: "Patch Bouwen",
}, &i18n.Message{
ID: "InformationTitle",
Other: "Informatie",
}, &i18n.Message{
ID: "SecondaryTitle",
Other: "Secondary",
}, &i18n.Message{
ID: "Reflog-CommitsTitle",
Other: "Reflog Tab",
}, &i18n.Message{
ID: "Title",
Other: "Title",
}, &i18n.Message{
ID: "GlobalTitle",
Other: "Globaale Sneltoetsen",
}, &i18n.Message{
ID: "MerginTitle",
Other: "Mergen",
}, &i18n.Message{
ID: "ConflictsResolved",
Other: "alle merge conflicten zijn opgelost. Wilt je verder gaan?",
}, &i18n.Message{
ID: "RebasingTitle",
Other: "Rebasen",
}, &i18n.Message{
ID: "MergingTitle",
Other: "Merggen",
}, &i18n.Message{
ID: "ConfirmRebase",
Other: "Weet je zeker dat je {{.checkedOutBranch}} op {{.selectedBranch}} wil rebasen?",
}, &i18n.Message{
ID: "ConfirmMerge",
Other: "Weet je zeker dat je {{.selectedBranch}} in {{.checkedOutBranch}} wil mergen?",
}, &i18n.Message{
ID: "FwdNoUpstream",
Other: "Kan niet de branch vooruitspoelen zonder upstream",
}, &i18n.Message{
ID: "FwdCommitsToPush",
Other: "Je kan niet vooruitspoelen als de branch geen nieuwe commits heeft",
}, &i18n.Message{
ID: "ErrorOccurred",
Other: "Er is iets fout gegaan! Zou je hier een issue aan willen maken: https://github.com/jesseduffield/lazygit/issues",
}, &i18n.Message{
ID: "NoRoom",
Other: "Niet genoeg ruimte",
@@ -592,6 +700,12 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "ScrollUp",
Other: "scroll omhoog",
}, &i18n.Message{
ID: "scrollUpMainPanel",
Other: "scroll naar beneden vanaf hooft paneel",
}, &i18n.Message{
ID: "scrollDownMainPanel",
Other: "scroll naar beneden vabaf hooft paneel",
}, &i18n.Message{
ID: "AmendCommitTitle",
Other: "Commit wijzigen",
@@ -606,7 +720,7 @@ func addDutch(i18nObject *i18n.Bundle) error {
Other: "Weet je zeker dat je deze commit wil verwijderen?",
}, &i18n.Message{
ID: "SquashingStatus",
Other: "squashing",
Other: "squashen",
}, &i18n.Message{
ID: "FixingStatus",
Other: "fixing up",
@@ -618,13 +732,22 @@ func addDutch(i18nObject *i18n.Bundle) error {
Other: "verplaatsen",
}, &i18n.Message{
ID: "RebasingStatus",
Other: "rebasing",
Other: "rebasen",
}, &i18n.Message{
ID: "AmendingStatus",
Other: "wijzigen",
}, &i18n.Message{
ID: "CherryPickingStatus",
Other: "cherry-picking",
Other: "cherry-picken",
}, &i18n.Message{
ID: "UndoingStatus",
Other: "ongedaan maken",
}, &i18n.Message{
ID: "RedoingStatus",
Other: "redoing",
}, &i18n.Message{
ID: "CheckingOutStatus",
Other: "uitchecken",
}, &i18n.Message{
ID: "CommitFiles",
Other: "Commit bestanden",
@@ -684,7 +807,7 @@ func addDutch(i18nObject *i18n.Bundle) error {
Other: "verwijder werkende tree",
}, &i18n.Message{
ID: "discardAnyUnstagedChanges",
Other: "discard unstaged changes",
Other: "discard unstaged wijzigingen",
}, &i18n.Message{
ID: "discardUntrackedFiles",
Other: "negeer niet-gevonden bestanden",
@@ -694,6 +817,12 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "hardReset",
Other: "harde reset",
}, &i18n.Message{
ID: "hardResetUpstream",
Other: "harde naar upstream branch",
}, &i18n.Message{
ID: "viewResetOptions",
Other: `bekijk reset opties`,
}, &i18n.Message{
ID: "createFixupCommit",
Other: `creëer fixup commit voor deze commit`,
@@ -726,46 +855,304 @@ func addDutch(i18nObject *i18n.Bundle) error {
Other: "Je hebt nog niet een commit bericht voorvoegsel ingesteld voor het overslaan van hooks. Set `git.skipHookPrefix = 'WIP'` in je config",
}, &i18n.Message{
ID: "resetTo",
Other: `reset to`,
Other: `reset naar`,
}, &i18n.Message{
ID: "pressEnterToReturn",
Other: "Press enter to return to lazygit",
Other: "Press om terug te gaan naar lazygit",
}, &i18n.Message{
ID: "viewStashOptions",
Other: "view stash options",
Other: "bekijk stash opties",
}, &i18n.Message{
ID: "stashAllChanges",
Other: "stash-bestanden",
}, &i18n.Message{
ID: "stashStagedChanges",
Other: "stash staged changes",
Other: "stash staged wijzigingen",
}, &i18n.Message{
ID: "stashOptions",
Other: "Stash options",
Other: "Stash opties",
}, &i18n.Message{
ID: "notARepository",
Other: "Error: must be run inside a git repository",
Other: "Fout: must be run inside a git repository",
}, &i18n.Message{
ID: "jump",
Other: "jump to panel",
Other: "ga naar paneel",
}, &i18n.Message{
ID: "DiscardPatch",
Other: "Patch weg gooien",
}, &i18n.Message{
ID: "DiscardPatchConfirm",
Other: "Je kan alleen maar een patch bouwen van 1 commit. actueel patch weg gooien?",
}, &i18n.Message{
ID: "CantPatchWhileRebasingError",
Other: "Je kan geen patch bouwen of patch commando uitvoeren wanneer je in een merging of rebasing state zit",
}, &i18n.Message{
ID: "toggleAddToPatch",
Other: "toggle bestand inbegrepen in patch",
}, &i18n.Message{
ID: "ViewPatchOptions",
Other: "bekijk aangepaste patch opties",
}, &i18n.Message{
ID: "PatchOptionsTitle",
Other: "Patch Opties",
}, &i18n.Message{
ID: "NoPatchError",
Other: "Nog geen patch gecreëerd. Om een patch te bouwen gebruik 'space' op een commit bestand of 'enter' om een spesiefieke lijnen toe te voegen",
}, &i18n.Message{
ID: "enterFile",
Other: "enter bestand to add selecteered lines to the patch",
}, &i18n.Message{
ID: "ExitLineByLineMode",
Other: `exit line-by-line mode`,
Other: `sluit lijn-bij-lijn mode`,
}, &i18n.Message{
ID: "EnterUpstream",
Other: `Enter upstream as '<remote> <branchname>'`,
Other: `Enter upstream als '<remote> <branchnaam>'`,
}, &i18n.Message{
ID: "EnterUpstreamWithSlash",
Other: `Enter upstream als '<remote>/<branchnaam>'`,
}, &i18n.Message{
ID: "notTrackingRemote",
Other: "(nog geen remote aan het volgen)",
}, &i18n.Message{
ID: "ReturnToRemotesList",
Other: `return to remotes list`,
Other: `Ga terug naar remotes lijst`,
}, &i18n.Message{
ID: "addNewRemote",
Other: `voeg een nieuwe remote toe`,
}, &i18n.Message{
ID: "newRemoteName",
Other: `Nieuwe remote name:`,
}, &i18n.Message{
ID: "newRemoteUrl",
Other: `Nieuwe remote url:`,
}, &i18n.Message{
ID: "editRemoteName",
Other: `Enter updated remote naam voor {{ .remoteName }}:`,
}, &i18n.Message{
ID: "editRemoteUrl",
Other: `Enter updated remote url voor {{ .remoteName }}:`,
}, &i18n.Message{
ID: "removeRemote",
Other: `verwijder remote`,
}, &i18n.Message{
ID: "removeRemotePrompt",
Other: "Weet je zeker dat je deze remote wilt verwijderen",
}, &i18n.Message{
ID: "DeleteRemoteBranch",
Other: "Verwijder Remote Branch",
}, &i18n.Message{
ID: "DeleteRemoteBranchMessage",
Other: "Weet je zeker dat je deze remote branch wilt verwijderen",
}, &i18n.Message{
ID: "setUpstream",
Other: "stel in als upstream van uitgecheckte branch",
}, &i18n.Message{
ID: "SetUpstreamTitle",
Other: "Stel in als upstream branch",
}, &i18n.Message{
ID: "SetUpstreamMessage",
Other: "Weet je zeker dat je de upstream branch van '{{.checkedOut}}' naar '{{.selected}}' wilt zetten",
}, &i18n.Message{
ID: "editRemote",
Other: "wijzig remote",
}, &i18n.Message{
ID: "tagCommit",
Other: "tag commit",
}, &i18n.Message{
ID: "TagNameTitle",
Other: "Tag naam:",
}, &i18n.Message{
ID: "deleteTag",
Other: "verwijder tag",
}, &i18n.Message{
ID: "DeleteTagTitle",
Other: "Verwijder tag",
}, &i18n.Message{
ID: "DeleteTagPrompt",
Other: "Weet je zeker dat je '{{.tagName}}' wil verwijderen?",
}, &i18n.Message{
ID: "PushTagTitle",
Other: "remote om tag '{{.tagName}}' te pushen naar:",
}, &i18n.Message{
ID: "pushTag",
Other: "push tag",
}, &i18n.Message{
ID: "createTag",
Other: "creëer tag",
}, &i18n.Message{
ID: "CreateTagTitle",
Other: "Tag naam:",
}, &i18n.Message{
ID: "fetchRemote",
Other: "fetch remote",
}, &i18n.Message{
ID: "FetchingRemoteStatus",
Other: "remote fetchen",
}, &i18n.Message{
ID: "checkoutCommit",
Other: "checkout commit",
}, &i18n.Message{
ID: "SureCheckoutThisCommit",
Other: "Weet je zeker dat je deze commit wil uitchecken?",
}, &i18n.Message{
ID: "gitFlowOptions",
Other: "laat git-flow opties zien",
}, &i18n.Message{
ID: "NotAGitFlowBranch",
Other: "Dit lijkt geen git flow branch te zijn",
}, &i18n.Message{
ID: "NewBranchNamePrompt",
Other: "nieuwe {{.branchType}} naam:",
}, &i18n.Message{
ID: "IgnoreTracked",
Other: "Ignore tracked file",
Other: "Negeer tracked bestand",
}, &i18n.Message{
ID: "IgnoreTrackedPrompt",
Other: "Are you sure you want to ignore a tracked file?",
Other: "weet je zeker dat je een getracked bestand wil negeeren?",
}, &i18n.Message{
ID: "viewResetToUpstreamOptions",
Other: "bekijk upstream reset opties",
}, &i18n.Message{
ID: "nextScreenMode",
Other: "volgende schermmode (normaal/half/groot )",
}, &i18n.Message{
ID: "prevScreenMode",
Other: "vorige schermmode",
}, &i18n.Message{
ID: "startSearch",
Other: "start met zoekken",
}, &i18n.Message{
ID: "Panel",
Other: "Paneel",
}, &i18n.Message{
ID: "Keybindings",
Other: "Sneltoetsen",
}, &i18n.Message{
ID: "renameBranch",
Other: "hernoem branch",
}, &i18n.Message{
ID: "NewBranchNamePrompt",
Other: "Noem een nieuwe branch naam",
}, &i18n.Message{
ID: "RenameBranchWarning",
Other: "Deze branch volgt een remote. Deze actie zal alleen de locale branch name wijzigen niet de naam van de remote branch. Verder gaan?",
}, &i18n.Message{
ID: "openMenu",
Other: "open menu",
}, &i18n.Message{
ID: "closeMenu",
Other: "sluit menu",
}, &i18n.Message{
ID: "resetCherryPick",
Other: "reset cherry-picked (gecopieerde) commits selectie",
}, &i18n.Message{
ID: "nextTab",
Other: "volgende tab",
}, &i18n.Message{
ID: "prevTab",
Other: "vorige tab",
}, &i18n.Message{
ID: "cantUndoWhileRebasing",
Other: "Kan niet ongedaan maken terwijl je aan het rebasen bent",
}, &i18n.Message{
ID: "cantRedoWhileRebasing",
Other: "Kan niet opnieuw doen (redo) terwijl je aan het rebasen bent",
}, &i18n.Message{
ID: "MustStashWarning",
Other: "Een patch in de index stoppen verijst stashen en onstashen van je wijzigingen. Als iets verkeert gaat kan je je bestanden terug vinden in de stash. Verder gaan?",
}, &i18n.Message{
ID: "MustStashTitle",
Other: "Moet stashen",
}, &i18n.Message{
ID: "ConfirmationTitle",
Other: "Bevestigings Paneel",
}, &i18n.Message{
ID: "prevPage",
Other: "vorige pagina",
}, &i18n.Message{
ID: "nextPage",
Other: "volgende pagina",
}, &i18n.Message{
ID: "gotoTop",
Other: "scroll naar boven",
}, &i18n.Message{
ID: "gotoBottom",
Other: "scroll naar beneden",
}, &i18n.Message{
ID: "filteringBy",
Other: "filteren bij",
}, &i18n.Message{
ID: "(reset)",
Other: "(reset)",
}, &i18n.Message{
ID: "openScopingMenu",
Other: "bekijk scoping opties",
}, &i18n.Message{
ID: "filterBy",
Other: "filter bij",
}, &i18n.Message{
ID: "exitFilterMode",
Other: "stop met filteren bij pad",
}, &i18n.Message{
ID: "filterPathOption",
Other: "vulin pad om op te filteren",
}, &i18n.Message{
ID: "enterFileName",
Other: "vulin path:",
}, &i18n.Message{
ID: "FilteringMenuTitle",
Other: "Filteren",
}, &i18n.Message{
ID: "MustExitFilterModeTitle",
Other: "Command niet beschikbaar",
}, &i18n.Message{
ID: "MustExitFilterModePrompt",
Other: "Command niet beschikbaar in filter mode. Sluit filter mode?",
}, &i18n.Message{
ID: "diff",
Other: "diff",
}, &i18n.Message{
ID: "enterRefToDiff",
Other: "vulin ref to diff",
}, &i18n.Message{
ID: "enteRefName",
Other: "vulin ref:",
}, &i18n.Message{
ID: "exitDiffMode",
Other: "sluit diff mode",
}, &i18n.Message{
ID: "DiffingMenuTitle",
Other: "Diffen",
}, &i18n.Message{
ID: "swapDiff",
Other: "keer diff richting om",
}, &i18n.Message{
ID: "openDiffingMenu",
Other: "open diff menu",
}, &i18n.Message{
ID: "showingGitDiff",
Other: "laat output zien voor:",
}, &i18n.Message{
ID: "copyCommitShaToClipboard",
Other: "copieer commit SHA naar clipboard",
}, &i18n.Message{
ID: "copyBranchNameToClipboard",
Other: "copieer branch name naar clipboard",
}, &i18n.Message{
ID: "commitPrefixPatternError",
Other: "Error in commitPrefix pattern",
Other: "Fout in commitPrefix patroon",
}, &i18n.Message{
ID: "NoFilesStagedTitle",
Other: "geen bestanden gestaged",
}, &i18n.Message{
ID: "NoFilesStagedPrompt",
Other: "Je hebt geen bestanden gestaged. Commit alle bestanden?",
}, &i18n.Message{
ID: "BranchNotFoundTitle",
Other: "Branch niet gevonden",
}, &i18n.Message{
ID: "BranchNotFoundPrompt",
Other: "Branch niet gevonden. Creëer een nieuwe branch genaamd",
},
)
}

View File

@@ -156,9 +156,6 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "CannotGitAdd",
Other: "Cannot git add --patch untracked files",
}, &i18n.Message{
ID: "NoStagedFilesToCommit",
Other: "There are no staged files to commit",
}, &i18n.Message{
ID: "NoFilesDisplay",
Other: "No file to display",
@@ -243,6 +240,9 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "NoTrackingThisBranch",
Other: "There is no tracking for this branch",
}, &i18n.Message{
ID: "CommitMessageConfirm",
Other: "{{.keyBindClose}}: close, {{.keyBindNewLine}}: new line, {{.keyBindConfirm}}: confirm",
}, &i18n.Message{
ID: "CommitWithoutMessageErr",
Other: "You cannot commit without a commit message",
@@ -252,6 +252,9 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "close",
Other: "close",
}, &i18n.Message{
ID: "quit",
Other: "quit",
}, &i18n.Message{
ID: "SureResetThisCommit",
Other: "Are you sure you want to reset to this commit?",
@@ -402,9 +405,6 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "IssntListOfViews",
Other: "{{.name}} is not in the list of views",
}, &i18n.Message{
ID: "NoViewMachingNewLineFocusedSwitchStatement",
Other: "No view matching newLineFocused switch statement",
}, &i18n.Message{
ID: "newFocusedViewIs",
Other: "new focused view is {{.newFocusedView}}",
@@ -448,18 +448,14 @@ func addEnglish(i18nObject *i18n.Bundle) error {
ID: "AnonymousReportingPrompt",
Other: "Would you like to enable anonymous reporting data to help improve lazygit? (enter/esc)",
}, &i18n.Message{
ID: "ShamelessSelfPromotionTitle",
Other: "Shameless Self Promotion",
}, &i18n.Message{
ID: "ShamelessSelfPromotionMessage",
Other: `Thanks for using lazygit! Three things to share with you:
ID: "IntroPopupMessage",
Other: `Thanks for using lazygit! Two things to share with you:
1) lazygit now has basic mouse support!
2) If you want to learn about lazygit's features, watch this vid:
1) If you want to learn about lazygit's features, watch this vid:
https://youtu.be/CPLdltN7wgE
3) Github are now matching any donations dollar-for-dollar for the next 12 months, so if you've been tossing up over whether to click the donate link in the bottom right corner, now is the time!`,
3) If you're using git, that makes you a programmer! With your help we can make lazygit better, so consider becoming a contributor and joining the fun at
https://github.com/jesseduffield/lazygit`,
}, &i18n.Message{
ID: "GitconfigParseErr",
Other: `Gogit failed to parse your gitconfig file due to the presence of unquoted '\' characters. Removing these should fix the issue.`,
@@ -526,12 +522,10 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "ToggleSelectionForPatch",
Other: `add/remove line(s) to patch`,
},
&i18n.Message{
}, &i18n.Message{
ID: "TogglePanel",
Other: `switch to other panel`,
},
&i18n.Message{
}, &i18n.Message{
ID: "CantStageStaged",
Other: `You can't stage an already staged change!`,
}, &i18n.Message{
@@ -893,7 +887,7 @@ func addEnglish(i18nObject *i18n.Bundle) error {
Other: "Discard Patch",
}, &i18n.Message{
ID: "DiscardPatchConfirm",
Other: "You can only build a patch from one commit at a time. Discard current patch?",
Other: "You can only build a patch from one commit/stash-entry at a time. Discard current patch?",
}, &i18n.Message{
ID: "CantPatchWhileRebasingError",
Other: "You cannot build a patch or run patch commands while in a merging or rebasing state",
@@ -926,7 +920,7 @@ func addEnglish(i18nObject *i18n.Bundle) error {
Other: "(not tracking any remote)",
}, &i18n.Message{
ID: "ReturnToRemotesList",
Other: `return to remotes list`,
Other: `Return to remotes list`,
}, &i18n.Message{
ID: "addNewRemote",
Other: `add new remote`,
@@ -1152,6 +1146,36 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "commitPrefixPatternError",
Other: "Error in commitPrefix pattern",
}, &i18n.Message{
ID: "NoFilesStagedTitle",
Other: "No files staged",
}, &i18n.Message{
ID: "NoFilesStagedPrompt",
Other: "You have not staged any files. Commit all files?",
}, &i18n.Message{
ID: "BranchNotFoundTitle",
Other: "Branch not found",
}, &i18n.Message{
ID: "BranchNotFoundPrompt",
Other: "Branch not found. Create a new branch named",
}, &i18n.Message{
ID: "UnstageLinesTitle",
Other: "Unstage lines",
}, &i18n.Message{
ID: "UnstageLinesPrompt",
Other: "Are you sure you want to delete the selected lines (git reset)? It is irreversible.\nTo disable this dialogue set the config key of 'gui.skipUnstageLineWarning' to true",
}, &i18n.Message{
ID: "createNewBranchFromCommit",
Other: "create new branch off of commit",
}, &i18n.Message{
ID: "viewStashFiles",
Other: "view stash entry's files",
}, &i18n.Message{
ID: "buildingPatch",
Other: "building patch",
}, &i18n.Message{
ID: "viewCommits",
Other: "view commits",
},
)
}

View File

@@ -128,9 +128,6 @@ func addPolish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "CannotGitAdd",
Other: "Nie można git add --patch nieśledzonych plików",
}, &i18n.Message{
ID: "NoStagedFilesToCommit",
Other: "Brak zatwierdzonych plików do commita",
}, &i18n.Message{
ID: "NoFilesDisplay",
Other: "Brak pliku do wyświetlenia",
@@ -209,6 +206,9 @@ func addPolish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "NoTrackingThisBranch",
Other: "Brak śledzenia dla tej gałęzi",
}, &i18n.Message{
ID: "CommitMessageConfirm",
Other: "{{.keyBindClose}}: zamknij, {{.keyBindNewLine}}: new line, {{.keyBindConfirm}}: potwierdź",
}, &i18n.Message{
ID: "CommitWithoutMessageErr",
Other: "Nie możesz commitować bez podania wiadomości",
@@ -320,9 +320,6 @@ func addPolish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "IssntListOfViews",
Other: "{{.name}} nie jest na liście widoków",
}, &i18n.Message{
ID: "NoViewMachingNewLineFocusedSwitchStatement",
Other: "Brak widoku pasującego do instrukcji przełączania newLineFocused",
}, &i18n.Message{
ID: "newFocusedViewIs",
Other: "nowy skupiony widok to {{.newFocusedView}}",
@@ -749,6 +746,18 @@ func addPolish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "commitPrefixPatternError",
Other: "Error in commitPrefix pattern",
}, &i18n.Message{
ID: "NoFilesStagedTitle",
Other: "No files staged",
}, &i18n.Message{
ID: "NoFilesStagedPrompt",
Other: "You have not staged any files. Commit all files?",
}, &i18n.Message{
ID: "BranchNotFoundTitle",
Other: "Branch not found",
}, &i18n.Message{
ID: "BranchNotFoundPrompt",
Other: "Branch not found. Create a new branch named",
},
)
}

View File

@@ -7,6 +7,7 @@ import (
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
@@ -322,3 +323,23 @@ func FindStringSubmatch(str string, regexpStr string) (bool, []string) {
match := re.FindStringSubmatch(str)
return len(match) > 0, match
}
func StringArraysOverlap(strArrA []string, strArrB []string) bool {
for _, first := range strArrA {
for _, second := range strArrB {
if first == second {
return true
}
}
}
return false
}
func MustConvertToInt(s string) int {
i, err := strconv.Atoi(s)
if err != nil {
panic(err)
}
return i
}

View File

@@ -31,7 +31,7 @@ func main() {
for _, lang := range langs {
os.Setenv("LC_ALL", lang)
mApp, _ := app.NewApp(mConfig)
mApp, _ := app.NewApp(mConfig, "")
file, err := os.Create(getProjectRoot() + "/docs/keybindings/Keybindings_" + lang + ".md")
if err != nil {
panic(err)

27
vendor/github.com/atotto/clipboard/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,27 @@
Copyright (c) 2013 Ato Araki. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of @atotto. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

48
vendor/github.com/atotto/clipboard/README.md generated vendored Normal file
View File

@@ -0,0 +1,48 @@
[![Build Status](https://travis-ci.org/atotto/clipboard.svg?branch=master)](https://travis-ci.org/atotto/clipboard)
[![GoDoc](https://godoc.org/github.com/atotto/clipboard?status.svg)](http://godoc.org/github.com/atotto/clipboard)
# Clipboard for Go
Provide copying and pasting to the Clipboard for Go.
Build:
$ go get github.com/atotto/clipboard
Platforms:
* OSX
* Windows 7 (probably work on other Windows)
* Linux, Unix (requires 'xclip' or 'xsel' command to be installed)
Document:
* http://godoc.org/github.com/atotto/clipboard
Notes:
* Text string only
* UTF-8 text encoding only (no conversion)
TODO:
* Clipboard watcher(?)
## Commands:
paste shell command:
$ go get github.com/atotto/clipboard/cmd/gopaste
$ # example:
$ gopaste > document.txt
copy shell command:
$ go get github.com/atotto/clipboard/cmd/gocopy
$ # example:
$ cat document.txt | gocopy

20
vendor/github.com/atotto/clipboard/clipboard.go generated vendored Normal file
View File

@@ -0,0 +1,20 @@
// Copyright 2013 @atotto. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package clipboard read/write on clipboard
package clipboard
// ReadAll read string from clipboard
func ReadAll() (string, error) {
return readAll()
}
// WriteAll write string to clipboard
func WriteAll(text string) error {
return writeAll(text)
}
// Unsupported might be set true during clipboard init, to help callers decide
// whether or not to offer clipboard options.
var Unsupported bool

52
vendor/github.com/atotto/clipboard/clipboard_darwin.go generated vendored Normal file
View File

@@ -0,0 +1,52 @@
// Copyright 2013 @atotto. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build darwin
package clipboard
import (
"os/exec"
)
var (
pasteCmdArgs = "pbpaste"
copyCmdArgs = "pbcopy"
)
func getPasteCommand() *exec.Cmd {
return exec.Command(pasteCmdArgs)
}
func getCopyCommand() *exec.Cmd {
return exec.Command(copyCmdArgs)
}
func readAll() (string, error) {
pasteCmd := getPasteCommand()
out, err := pasteCmd.Output()
if err != nil {
return "", err
}
return string(out), nil
}
func writeAll(text string) error {
copyCmd := getCopyCommand()
in, err := copyCmd.StdinPipe()
if err != nil {
return err
}
if err := copyCmd.Start(); err != nil {
return err
}
if _, err := in.Write([]byte(text)); err != nil {
return err
}
if err := in.Close(); err != nil {
return err
}
return copyCmd.Wait()
}

129
vendor/github.com/atotto/clipboard/clipboard_unix.go generated vendored Normal file
View File

@@ -0,0 +1,129 @@
// Copyright 2013 @atotto. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build freebsd linux netbsd openbsd solaris dragonfly
package clipboard
import (
"errors"
"os"
"os/exec"
)
const (
xsel = "xsel"
xclip = "xclip"
wlcopy = "wl-copy"
wlpaste = "wl-paste"
termuxClipboardGet = "termux-clipboard-get"
termuxClipboardSet = "termux-clipboard-set"
)
var (
Primary bool
pasteCmdArgs []string
copyCmdArgs []string
xselPasteArgs = []string{xsel, "--output", "--clipboard"}
xselCopyArgs = []string{xsel, "--input", "--clipboard"}
xclipPasteArgs = []string{xclip, "-out", "-selection", "clipboard"}
xclipCopyArgs = []string{xclip, "-in", "-selection", "clipboard"}
wlpasteArgs = []string{wlpaste, "--no-newline"}
wlcopyArgs = []string{wlcopy}
termuxPasteArgs = []string{termuxClipboardGet}
termuxCopyArgs = []string{termuxClipboardSet}
missingCommands = errors.New("No clipboard utilities available. Please install xsel, xclip, wl-clipboard or Termux:API add-on for termux-clipboard-get/set.")
)
func init() {
if os.Getenv("WAYLAND_DISPLAY") != "" {
pasteCmdArgs = wlpasteArgs;
copyCmdArgs = wlcopyArgs;
if _, err := exec.LookPath(wlcopy); err == nil {
if _, err := exec.LookPath(wlpaste); err == nil {
return
}
}
}
pasteCmdArgs = xclipPasteArgs
copyCmdArgs = xclipCopyArgs
if _, err := exec.LookPath(xclip); err == nil {
return
}
pasteCmdArgs = xselPasteArgs
copyCmdArgs = xselCopyArgs
if _, err := exec.LookPath(xsel); err == nil {
return
}
pasteCmdArgs = termuxPasteArgs
copyCmdArgs = termuxCopyArgs
if _, err := exec.LookPath(termuxClipboardSet); err == nil {
if _, err := exec.LookPath(termuxClipboardGet); err == nil {
return
}
}
Unsupported = true
}
func getPasteCommand() *exec.Cmd {
if Primary {
pasteCmdArgs = pasteCmdArgs[:1]
}
return exec.Command(pasteCmdArgs[0], pasteCmdArgs[1:]...)
}
func getCopyCommand() *exec.Cmd {
if Primary {
copyCmdArgs = copyCmdArgs[:1]
}
return exec.Command(copyCmdArgs[0], copyCmdArgs[1:]...)
}
func readAll() (string, error) {
if Unsupported {
return "", missingCommands
}
pasteCmd := getPasteCommand()
out, err := pasteCmd.Output()
if err != nil {
return "", err
}
return string(out), nil
}
func writeAll(text string) error {
if Unsupported {
return missingCommands
}
copyCmd := getCopyCommand()
in, err := copyCmd.StdinPipe()
if err != nil {
return err
}
if err := copyCmd.Start(); err != nil {
return err
}
if _, err := in.Write([]byte(text)); err != nil {
return err
}
if err := in.Close(); err != nil {
return err
}
return copyCmd.Wait()
}

128
vendor/github.com/atotto/clipboard/clipboard_windows.go generated vendored Normal file
View File

@@ -0,0 +1,128 @@
// Copyright 2013 @atotto. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build windows
package clipboard
import (
"syscall"
"time"
"unsafe"
)
const (
cfUnicodetext = 13
gmemMoveable = 0x0002
)
var (
user32 = syscall.MustLoadDLL("user32")
openClipboard = user32.MustFindProc("OpenClipboard")
closeClipboard = user32.MustFindProc("CloseClipboard")
emptyClipboard = user32.MustFindProc("EmptyClipboard")
getClipboardData = user32.MustFindProc("GetClipboardData")
setClipboardData = user32.MustFindProc("SetClipboardData")
kernel32 = syscall.NewLazyDLL("kernel32")
globalAlloc = kernel32.NewProc("GlobalAlloc")
globalFree = kernel32.NewProc("GlobalFree")
globalLock = kernel32.NewProc("GlobalLock")
globalUnlock = kernel32.NewProc("GlobalUnlock")
lstrcpy = kernel32.NewProc("lstrcpyW")
)
// waitOpenClipboard opens the clipboard, waiting for up to a second to do so.
func waitOpenClipboard() error {
started := time.Now()
limit := started.Add(time.Second)
var r uintptr
var err error
for time.Now().Before(limit) {
r, _, err = openClipboard.Call(0)
if r != 0 {
return nil
}
time.Sleep(time.Millisecond)
}
return err
}
func readAll() (string, error) {
err := waitOpenClipboard()
if err != nil {
return "", err
}
defer closeClipboard.Call()
h, _, err := getClipboardData.Call(cfUnicodetext)
if h == 0 {
return "", err
}
l, _, err := globalLock.Call(h)
if l == 0 {
return "", err
}
text := syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(l))[:])
r, _, err := globalUnlock.Call(h)
if r == 0 {
return "", err
}
return text, nil
}
func writeAll(text string) error {
err := waitOpenClipboard()
if err != nil {
return err
}
defer closeClipboard.Call()
r, _, err := emptyClipboard.Call(0)
if r == 0 {
return err
}
data := syscall.StringToUTF16(text)
// "If the hMem parameter identifies a memory object, the object must have
// been allocated using the function with the GMEM_MOVEABLE flag."
h, _, err := globalAlloc.Call(gmemMoveable, uintptr(len(data)*int(unsafe.Sizeof(data[0]))))
if h == 0 {
return err
}
defer func() {
if h != 0 {
globalFree.Call(h)
}
}()
l, _, err := globalLock.Call(h)
if l == 0 {
return err
}
r, _, err = lstrcpy.Call(l, uintptr(unsafe.Pointer(&data[0])))
if r == 0 {
return err
}
r, _, err = globalUnlock.Call(h)
if r == 0 {
if err.(syscall.Errno) != 0 {
return err
}
}
r, _, err = setClipboardData.Call(cfUnicodetext, h)
if r == 0 {
return err
}
h = 0 // suppress deferred cleanup
return nil
}

1
vendor/github.com/atotto/clipboard/go.mod generated vendored Normal file
View File

@@ -0,0 +1 @@
module github.com/atotto/clipboard

41
vendor/github.com/creack/pty/run.go generated vendored
View File

@@ -11,6 +11,8 @@ import (
// Start assigns a pseudo-terminal tty os.File to c.Stdin, c.Stdout,
// and c.Stderr, calls c.Start, and returns the File of the tty's
// corresponding pty.
//
// Starts the process in a new session and sets the controlling terminal.
func Start(c *exec.Cmd) (pty *os.File, err error) {
return StartWithSize(c, nil)
}
@@ -19,16 +21,35 @@ func Start(c *exec.Cmd) (pty *os.File, err error) {
// and c.Stderr, calls c.Start, and returns the File of the tty's
// corresponding pty.
//
// This will resize the pty to the specified size before starting the command
// This will resize the pty to the specified size before starting the command.
// Starts the process in a new session and sets the controlling terminal.
func StartWithSize(c *exec.Cmd, sz *Winsize) (pty *os.File, err error) {
if c.SysProcAttr == nil {
c.SysProcAttr = &syscall.SysProcAttr{}
}
c.SysProcAttr.Setsid = true
c.SysProcAttr.Setctty = true
return StartWithAttrs(c, sz, c.SysProcAttr)
}
// StartWithAttrs assigns a pseudo-terminal tty os.File to c.Stdin, c.Stdout,
// and c.Stderr, calls c.Start, and returns the File of the tty's
// corresponding pty.
//
// This will resize the pty to the specified size before starting the command if a size is provided.
// The `attrs` parameter overrides the one set in c.SysProcAttr.
//
// This should generally not be needed. Used in some edge cases where it is needed to create a pty
// without a controlling terminal.
func StartWithAttrs(c *exec.Cmd, sz *Winsize, attrs *syscall.SysProcAttr) (pty *os.File, err error) {
pty, tty, err := Open()
if err != nil {
return nil, err
}
defer tty.Close()
if sz != nil {
err = Setsize(pty, sz)
if err != nil {
if err := Setsize(pty, sz); err != nil {
pty.Close()
return nil, err
}
@@ -42,15 +63,11 @@ func StartWithSize(c *exec.Cmd, sz *Winsize) (pty *os.File, err error) {
if c.Stdin == nil {
c.Stdin = tty
}
if c.SysProcAttr == nil {
c.SysProcAttr = &syscall.SysProcAttr{}
}
c.SysProcAttr.Setctty = true
c.SysProcAttr.Setsid = true
c.SysProcAttr.Ctty = int(tty.Fd())
err = c.Start()
if err != nil {
pty.Close()
c.SysProcAttr = attrs
if err := c.Start(); err != nil {
_ = pty.Close()
return nil, err
}
return pty, err

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