Compare commits

..
379 Commits
Author SHA1 Message Date
Jesse Duffield 14c8b80494 show loading state when amending top commit 2020-09-18 07:58:16 +10:00
Lakshay Garg 10dde518bc Fix numbering in Dutch IntroPopupMessage 2020-09-18 07:47:01 +10:00
Lakshay Garg 1e1c90c92e Fix numbering in English IntroPopupMessage 2020-09-18 07:47:01 +10:00
Jesse Duffield 4954791443 fix test 2020-09-18 07:46:12 +10:00
Jesse Duffield c471f4927a fix test 2020-09-02 20:55:53 +10:00
Jesse Duffield 9eba98302e ensure that when a branch name is ambiguous we still show the correct colours 2020-09-02 10:40:50 +00:00
Francisco Miamoto 250fe740b2 use GetBool instead of casting 2020-08-31 09:22:39 +10:00
Francisco Miamoto 70eda031dc implement config option for disabling force pushing 2020-08-31 09:22:39 +10:00
Francisco Miamoto 86f296a898 add config for disabling force pushing 2020-08-31 09:22:39 +10:00
Jesse Duffield 71ff18318d fast UI update when moving commits in rebase mode 2020-08-29 00:19:31 +00:00
Jesse Duffield 46cce28758 restore donate link 2020-08-28 09:52:56 +10:00
Jesse Duffield 5611d9a3ef gracefully fail due to git version less than 2.0 2020-08-27 12:21:37 +00:00
Jesse Duffield 40bec49de8 more efficient refreshing of rebase commits 2020-08-27 21:51:07 +10:00
Jesse Duffield f99d5f74d4 drop merge commits when interactive rebasing just like git CLI 2020-08-27 21:51:07 +10:00
Jesse Duffield 30a066aa41 remove redundant test 2020-08-27 19:29:22 +10:00
Jesse Duffield 1dcc3363d0 support branches with no upstream 2020-08-27 17:05:07 +10:00
Jesse Duffield c6948582e6 better way of knowing which commits are unpushed 2020-08-26 22:45:55 +00:00
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
Randshot fe6d8d62c5 add overrideGpg switch to Config.md
Signed-off-by: Randshot <randshot@norealm.xyz>
2020-07-12 11:50:12 +02:00
Randshot 570d27ffaa Merge branch 'master' into add-overrideGpg-switch
Signed-off-by: Randshot <randshot@norealm.xyz>
2020-07-12 11:47:35 +02:00
Pranav Shikarpur 7b69aa1fda Added ENTRYPOINT to Dockerfile to jump directly into lazy git while running the docker container 2020-07-12 14:10:04 +10:00
Randshot 21e478dd59 fix 'Amend commit using gpg' test
Signed-off-by: Randshot <randshot@norealm.xyz>
2020-07-12 14:06:53 +10:00
Randshot d14fb36cb9 fix 'Commit using gpg' test
Signed-off-by: Randshot <randshot@norealm.xyz>
2020-07-12 14:06:53 +10:00
Randshot 19a808642f fix platform specific quoting when using GPG
fixes #620

Signed-off-by: Randshot <randshot@norealm.xyz>
2020-07-12 14:06:53 +10:00
Jasper Mendiola e921ba0910 Remove getLocalGitConfig 2020-07-10 18:55:00 +10:00
Jasper Mendiola 0f5a073d57 Rename appconfig to config 2020-07-10 18:55:00 +10:00
Jasper Mendiola cb0bdd89c0 fix tests 2020-07-10 18:55:00 +10:00
Jasper Mendiola e89bf5d06b add oneline-graph 2020-07-10 18:55:00 +10:00
David Chen e82d2f37a1 Update example keybinding config for Colemak users 2020-06-03 22:14:21 +10:00
Randshot 65e955c622 add overrideGpg switch, which prevents lazygit from spawning a separate process when using GPG
Signed-off-by: Randshot <randshot@norealm.xyz>
2020-05-30 23:39:07 +02:00
Dima Kotik e73f4c6b7e Better CWD check for a git repository. 2020-05-30 00:31:58 +10:00
Jesse Duffield cf5cefb2d6 allow user to scroll themselves inside merge panel 2020-05-19 18:44:53 +10:00
Jesse Duffield 36ac764133 fix race condition when scrolling to merge conflict 2020-05-19 18:05:14 +10:00
Jesse Duffield 003e45d2f5 allow creating branches off of remote branches 2020-05-19 09:57:37 +10:00
Jesse Duffield 04e93317b8 fix https://github.com/jesseduffield/lazygit/issues/848 2020-05-19 09:57:37 +10:00
Jesse Duffield f8dedb710b additional password prompt regex 2020-05-15 22:18:07 +10:00
Jesse Duffield 1c259f69f6 check if user has configured to push to current by default 2020-05-15 21:41:23 +10:00
Jesse Duffield 913f17ee3e prevent flicker from bolding background of selected line 2020-05-15 21:12:12 +10:00
Dawid Dziurla 6291c53966 workflows: update bumping action to v3 2020-05-13 13:36:59 +02:00
Jesse Duffield 267730bc00 standardise how we handle background colours 2020-05-13 21:24:25 +10:00
Jesse Duffield d5db02a899 bump gocui to be on 'simple' branch.
The master branch of gocui contains stuff I added for lazynpm which changes how
the cursor is used. This will provide some benefits to lazygit as well but I
don't yet have the motivation to make the required changed in lazygit to support it.

So we're gonna be on the branch named 'simple' rather than master until I fix that up.
2020-05-13 21:24:25 +10:00
Gary Yendell 7ed8ee160d Add option to split patch into a new commit
Add GetHeadCommitMessage to read the subject of the HEAD commit
Create PullPatchIntoNewCommit based heavily on PullPatchIntoIndex to
  split the current patch from its commit and apply it in a separate
  commit immediately after.

WIP to Squash - Fill format string with format string

WIP
2020-05-09 11:59:37 +10:00
Josh Soref 3dd33b65a0 Minor fixes
* Windows
* Use backticks
* Italicize git config
2020-05-08 09:48:13 +10:00
Dawid Dziurla b85048f616 workflows: update CI triggers
So it would run on pull requests from forks
2020-05-04 20:03:27 +02:00
Mike Palmer 0852f53455 Add path to config file on Windows 2020-04-27 19:15:06 +10:00
Glenn Vriesman 10fa119ab3 fix: fixed readme link
Signed-off-by: Glenn Vriesman <glenn.vriesman@gmail.com>
2020-04-27 19:14:43 +10:00
Tyler Davis b5404c6159 fix issue #640 add catCmd and OS-specific values
Add a catCmd to the Platform struct and set the value to "cat" for
non-windows builds and "type" for windows builds.
2020-04-27 19:14:18 +10:00
Lars E 42d21c4bb6 Add FreeBSD installation instructions 2020-04-22 19:51:01 +10:00
Jesse Duffield cc13ae252a totally screwed up the last commit 2020-04-22 11:21:20 +10:00
Jesse Duffield b97f844a3e handle comments in todo files 2020-04-22 11:15:41 +10:00
Glenn Vriesman 1d6eb015c1 fix: fixed yaml typo
Signed-off-by: Glenn Vriesman <glenn.vriesman@gmail.com>
2020-04-22 08:52:08 +10:00
Jesse Duffield 07a8ae8c3e add handler for searching in menu 2020-04-21 19:28:31 +10:00
Jesse Duffield f05a5e531e warnings for stash actions 2020-04-20 18:57:08 +10:00
Kristijan Husak 68586ec49a Handle regex compilation errors and show them to the user. 2020-04-20 18:47:50 +10:00
Kristijan Husak 6cf75af0af Add option to set predefined commit message prefix. Fixes #760. 2020-04-20 18:47:50 +10:00
Jesse Duffield 304607ae5d support configurable merge args 2020-04-20 18:40:49 +10:00
Jesse Duffield e9f28855a2 add bugfix git flow option 2020-04-20 18:31:13 +10:00
Glenn Vriesman 66d7d5f312 fix: fixed gpg breaking terminal
Signed-off-by: Glenn Vriesman <glenn.vriesman@gmail.com>
2020-04-20 18:30:57 +10:00
Jesse Duffield 59734f1069 whoops 2020-04-17 09:27:23 +10:00
Jesse Duffield 2974a57943 support copying stuff to clipboard 2020-04-15 10:44:56 +00:00
Adwin Ying fcdcd1c335 fix config docs typo 2020-04-03 17:44:15 +11:00
Dawid Dziurla 4a35f9fcdb Merge pull request #775 from jesseduffield/dawidd6-patch-1
workflows: update homebrew bumping action
2020-04-02 23:51:22 +02:00
Dawid Dziurla 674b14802e workflows: update homebrew bumping action 2020-04-02 23:43:26 +02:00
Jesse Duffield 3e36affa69 remove trash files 2020-03-29 21:53:25 +00:00
Jesse Duffield 97d7a8ad0c add reverse patch option 2020-03-29 21:53:25 +00:00
Jesse Duffield b89ba365d0 unbold diff info 2020-03-29 18:31:19 +11:00
Jesse Duffield 47ff388549 some more UI logic 2020-03-29 18:26:24 +11:00
Jesse Duffield 647ab9bf0f better keybinding 2020-03-29 18:26:24 +11:00
Jesse Duffield 76431b4673 simplify things 2020-03-29 18:26:24 +11:00
Jesse Duffield be0dd29e3a don't support files until we understand the use case 2020-03-29 18:26:24 +11:00
Jesse Duffield 40fbce91ce add new diff mode
WIP

WIP

WIP

WIP

WIP

WIP

WIP
2020-03-29 18:26:24 +11:00
Jesse Duffield 33d287d2f0 remove old diff mode code 2020-03-29 18:26:24 +11:00
Jesse Duffield 9eb1cbc514 reset main's origin when cycling views 2020-03-29 02:36:01 +00:00
Jesse Duffield 40b173118a fix conflict race condition 2020-03-29 02:36:01 +00:00
Jesse Duffield 8822c409e2 split reflog commits into ReflogCommits and FilteredReflogCommits 2020-03-29 11:37:29 +11:00
Jesse Duffield aa750c0819 load reflog commits manually when in filter mode for branches panel 2020-03-29 11:37:29 +11:00
Jesse Duffield d90d9d7330 reset state on each Run() call 2020-03-29 11:37:29 +11:00
Jesse Duffield a8db672ffb refactor gui.go 2020-03-29 11:37:29 +11:00
Jesse Duffield 76b66ae26f properly reset gui state when restarting or coming back from a subprocess 2020-03-29 11:37:29 +11:00
Jesse Duffield a2790cfe8e rename to filtered mode 2020-03-29 11:37:29 +11:00
Jesse Duffield 624ae45ebb allow scoped mode where the commits/reflog/stash panels are scoped to a file
WIP

restrict certain actions in scoped mode

WIP
2020-03-29 11:37:29 +11:00
Jesse Duffield 2756b82f57 fix width of half screen mode 2020-03-29 11:37:29 +11:00
Jesse Duffield 52f41ab0d5 update cheatsheet 2020-03-28 03:16:44 +00:00
Jesse Duffield fbb767893e support lazyloading in commits view 2020-03-28 14:02:53 +11:00
Jesse Duffield 229f5ee48c add keybindings for paging in list panels and jumping to top/bottom 2020-03-28 14:02:53 +11:00
Jesse Duffield 96c7741ba0 add workflow for auto-merging 2020-03-28 13:22:30 +11:00
Jesse Duffield 517b7d0283 fix up some things with the patch handling stuff 2020-03-28 13:19:35 +11:00
Jesse Duffield 0c0231c3e8 autostash changes when pulling file into index 2020-03-28 13:19:35 +11:00
Jesse Duffield a9559a5c87 move working tree state function into git.go 2020-03-28 13:19:35 +11:00
Jesse Duffield 814ee24c8d better error handling 2020-03-28 11:59:45 +11:00
Jesse Duffield 7876cddf4a remove dead code 2020-03-28 11:59:45 +11:00
Jesse Duffield e9051355a1 fix test 2020-03-28 11:59:45 +11:00
Jesse Duffield 29316a528a better documentation 2020-03-28 11:59:45 +11:00
Jesse Duffield 036b53acf8 in fact we don't need any of these options 2020-03-28 11:59:45 +11:00
Jesse Duffield 919463ff02 actually don't even bother limiting 2020-03-28 11:59:45 +11:00
Jesse Duffield 3f7ec3f3b8 load reflog commits in two stages to speed up startup time 2020-03-28 11:59:45 +11:00
Jesse Duffield 19604214d7 discard old reflog commits when in new context 2020-03-28 11:59:45 +11:00
Jesse Duffield f7add8d788 smarter refreshing for tags and remotes 2020-03-28 11:59:45 +11:00
Jesse Duffield d97c230747 stop switching focus to commit files view while staging line by line 2020-03-28 11:59:45 +11:00
Jesse Duffield 906a49049e smart refreshing files 2020-03-28 11:59:45 +11:00
Jesse Duffield c1a4bd0482 more smart refreshing
WIP

WIP

WIP

WIP

WIP

fix how diff entries are handled

WIP

WIP

WIP

WIP

WIP

WIP
2020-03-28 11:59:45 +11:00
Jesse Duffield d0336fe16f better presentation of remotes 2020-03-28 11:59:45 +11:00
Jesse Duffield 61b4bbf74e clean up signature 2020-03-28 11:59:45 +11:00
Jesse Duffield 384c2e13d7 better refreshing for stash 2020-03-28 11:59:45 +11:00
Jesse Duffield 198d237679 more centralised handling of refreshing 2020-03-28 11:59:45 +11:00
Jesse Duffield 39315ca1e2 use wait groups when refreshing 2020-03-28 11:59:45 +11:00
Jesse Duffield efb51eee96 more efficient refreshing 2020-03-28 11:59:45 +11:00
Jesse Duffield fbbd16bd82 use reflogs from state to work out branch recencies 2020-03-28 11:59:45 +11:00
Jesse Duffield bd2c1eef53 remove redundant fetch of reflog 2020-03-28 11:59:45 +11:00
Jesse Duffield d1395b15bb use GIT_EDITOR 2020-03-27 19:26:14 +11:00
Máximo Cuadros 2d8ed5e274 *: update go-git import 2020-03-27 19:06:21 +11:00
Máximo Cuadros 6a5d8ba859 vendor: replace go-git package 2020-03-27 19:06:21 +11:00
Francisco Miamoto 320e2a6536 fix links on toc 2020-03-27 09:14:04 +11:00
Dawid Dziurla 3858118340 Merge pull request #751 from Semro/patch-1
Fix link in README
2020-03-26 16:59:04 +01:00
Semro 6420068569 Fix link in README 2020-03-26 18:46:26 +03:00
Jesse Duffield 95b147079f allow applying patch directly 2020-03-26 21:44:45 +11:00
Jesse Duffield 83757f1065 limit size of menu panel 2020-03-26 21:44:33 +11:00
Jesse Duffield f2036b42e5 only load new reflog entries 2020-03-26 21:44:33 +11:00
Jesse Duffield 21b7d41845 relax limit on commit list and reset on branch change 2020-03-26 21:44:33 +11:00
Jesse Duffield 91a404d033 separate commits from cherry pick state 2020-03-26 21:44:33 +11:00
Jesse Duffield d027cf969c better handling of current branch name 2020-03-26 20:37:06 +11:00
Jesse Duffield c7f68a2ef9 delete unused assets 2020-03-26 19:18:43 +11:00
Jesse Duffield 78e55a05c1 another staging gif 2020-03-26 19:15:15 +11:00
Jesse Duffield ca71555d0b Update README.md 2020-03-26 19:10:49 +11:00
Jesse Duffield 77fdac01ff better staging gif 2020-03-26 19:10:37 +11:00
Jesse Duffield 8301fae01e Update README.md 2020-03-26 19:04:48 +11:00
Jesse Duffield e9161ad702 add staging gif 2020-03-26 19:04:26 +11:00
Jesse Duffield a0a139da1f add rebasing gif 2020-03-26 18:43:10 +11:00
Francisco Miamoto 8f13d1da91 change binary releases order 2020-03-26 18:32:38 +11:00
Francisco Miamoto d5fe9ce2c7 add table of contents to readme 2020-03-26 18:32:38 +11:00
Jesse Duffield 37acc17cf3 more lenient getting of short shas 2020-03-26 18:30:02 +11:00
Jesse Duffield 569ec5919c Update README.md 2020-03-26 09:14:14 +11:00
Dawid Dziurla 19719becf5 workflows: run goreleaser as a build step for CI 2020-03-25 21:26:15 +11:00
Dawid Dziurla e64057b803 workflows: install gox in separate step in /tmp directory
avoid Go trying to add a dependency to go.mod file
2020-03-25 21:26:15 +11:00
Dawid Dziurla 672667aa3e goreleaser: stop ignoring arm64 build for freebsd 2020-03-25 21:26:15 +11:00
Dawid Dziurla 8a06b6067e go mod vendor 2020-03-25 21:26:15 +11:00
Dawid Dziurla 2dcc52abd0 go mod tidy 2020-03-25 21:26:15 +11:00
Dawid Dziurla c831ad39c9 pkg: use upstream pty package 2020-03-25 21:26:15 +11:00
Jesse Duffield 0cf78ea9ad Update Undoing.md 2020-03-25 20:35:10 +11:00
Jesse Duffield 3d51fbf354 Update README.md 2020-03-25 20:32:44 +11:00
Jesse Duffield e7a2c7cc3e update cheatsheet 2020-03-25 20:24:03 +11:00
Jesse Duffield 708a078412 document undo 2020-03-25 20:17:46 +11:00
Jesse Duffield bbcc4b7b70 just disallow undo/redo while rebasing because you need more info than just the reflog 2020-03-25 09:39:04 +11:00
Jesse Duffield 45bba0a3c5 ignore redundant actions when undoing and redoing 2020-03-25 09:39:04 +11:00
Jesse Duffield d105e2690a vastly improve the logic for undo and redo 2020-03-25 09:39:04 +11:00
Jesse Duffield 32d3e497c3 fix tests 2020-03-25 09:39:04 +11:00
Jesse Duffield 30a5d1b486 move into undoing file 2020-03-25 09:39:04 +11:00
Jesse Duffield 6b3ea56add refactor undo and redo 2020-03-25 09:39:04 +11:00
Jesse Duffield c3aefdb98e stateless undos and redos 2020-03-25 09:39:04 +11:00
Jesse Duffield 094939451d more explicit env vars 2020-03-25 09:39:04 +11:00
Jesse Duffield 0e23f44b84 support reflog action prefix 2020-03-25 09:39:04 +11:00
Jesse Duffield daecdd7c2b redoing 2020-03-25 09:39:04 +11:00
Jesse Duffield 7c8df28d01 add waiting status to checkout ref handler 2020-03-25 09:39:04 +11:00
Jesse Duffield 65917272a2 undoing status 2020-03-25 09:39:04 +11:00
Jesse Duffield 137fd80fdb note that undo functionality is experimental 2020-03-25 09:39:04 +11:00
Jesse Duffield 98fbc61221 better formatted reflog list 2020-03-25 09:39:04 +11:00
Jesse Duffield f80d15062b use reflog undo history pointer 2020-03-25 09:39:04 +11:00
Jesse Duffield b1b0219f04 autostash changes when hard resetting 2020-03-25 09:39:04 +11:00
Jesse Duffield b1941c33f7 undo via rebase 2020-03-25 09:39:04 +11:00
J. B. Rainsberger a15a7b607d Made the humor more concise and clear. 2020-03-25 09:18:25 +11:00
J. B. Rainsberger d50283f5ee improved spelling 2020-03-25 09:18:25 +11:00
J. B. Rainsberger 6508d3b872 inject more humor into the README 2020-03-25 09:18:25 +11:00
J. B. Rainsberger 65b8cef1b8 Fixed an incorrect criticism of git in the README.
It's true that parts of git are genuinely difficult to use, so
we don't need to overstate that difficulty in order to state the
value proposition of lazygit. If `git add -p` can't split a hunk
any further, one is not _completely_ stuck, but one does need to
edit the hunk in a way that, after years, I still need a few
attempts to get right. The fact that many otherwise-experienced
git users don't even know that one can do that is itself a
testament to the UX problems that lazygit is trying to address.
2020-03-25 09:18:25 +11:00
Jesse Duffield 5d460e1e5e add tab keybindings 2020-03-23 23:25:00 +11:00
Jesse Duffield 3d3e0be7bd more compatible commands 2020-03-23 22:33:17 +11:00
Dawid Dziurla c06c0b7133 workflows: git fetch --unshallow before goreleaser step 2020-03-22 21:49:41 +11:00
589 changed files with 28451 additions and 96055 deletions
-1
View File
@@ -1,5 +1,4 @@
# These are supported funding model platforms
github: [jesseduffield]
ko_fi: jesseduffield
custom: ['https://donorbox.org/lazygit']
+28
View File
@@ -0,0 +1,28 @@
name: automerge
on:
pull_request:
types:
- labeled
- unlabeled
- synchronize
- opened
- edited
- ready_for_review
- reopened
- unlocked
pull_request_review:
types:
- submitted
check_suite:
types:
- completed
status: {}
jobs:
automerge:
runs-on: ubuntu-latest
steps:
- name: automerge
uses: "pascalgn/automerge-action@135f0bdb927d9807b5446f7ca9ecc2c51de03c4a"
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
MERGE_METHOD: rebase
+3 -9
View File
@@ -11,24 +11,18 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Unshallow repo
run: git fetch --prune --unshallow
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.14.x
- name: Run goreleaser
uses: goreleaser/goreleaser-action@v1
with:
args: release --rm-dist
env:
GITHUB_TOKEN: ${{secrets.GITHUB_API_TOKEN}}
- name: Get tag
id: tag
uses: dawidd6/action-get-tag@v1
- name: Bump Homebrew
uses: dawidd6/action-homebrew-bump-formula@v1
uses: dawidd6/action-homebrew-bump-formula@v3
with:
token: ${{secrets.GITHUB_API_TOKEN}}
formula: lazygit
url: "https://github.com/${{github.repository}}/archive/${{steps.tag.outputs.tag}}.tar.gz"
+8 -5
View File
@@ -1,6 +1,10 @@
name: Continuous Integration
on: push
on:
push:
branches:
- master
pull_request:
jobs:
ci:
@@ -31,7 +35,6 @@ jobs:
run: |
./test.sh
- name: Build binaries
run: |
go get github.com/mitchellh/gox
export PATH="$(go env GOPATH)/bin:$PATH"
gox -parallel 4 -os "linux freebsd netbsd windows" -osarch "darwin/i386 darwin/amd64"
uses: goreleaser/goreleaser-action@v1
with:
args: --skip-publish --snapshot
+2 -7
View File
@@ -13,9 +13,6 @@ builds:
- arm
- arm64
- 386
ignore:
- goos: freebsd
goarch: arm64
# Default is `-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}`.
ldflags:
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.buildSource=binaryRelease
@@ -43,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
@@ -64,5 +61,3 @@ brews:
# conflicts:
# - svn
# - bash
# test comment to see if goreleaser only releases on new commits
+3 -1
View File
@@ -1,6 +1,6 @@
# run with:
# docker build -t lazygit .
# docker run -it lazygit:latest /bin/sh -l
# docker run -it lazygit:latest /bin/sh
FROM golang:1.14-alpine3.11
WORKDIR /go/src/github.com/jesseduffield/lazygit/
@@ -13,3 +13,5 @@ WORKDIR /go/src/github.com/jesseduffield/lazygit/
COPY --from=0 /go/src/github.com/jesseduffield/lazygit /go/src/github.com/jesseduffield/lazygit
COPY --from=0 /go/src/github.com/jesseduffield/lazygit/lazygit /bin/
RUN echo "alias gg=lazygit" >> ~/.profile
ENTRYPOINT [ "lazygit" ]
+84 -27
View File
@@ -1,28 +1,57 @@
# lazygit ![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)
<p align="center">
<img src="https://i.imgur.com/oYB7Cj8.png">
</p>
A simple terminal UI for git commands, written in Go with the [gocui](https://github.com/jroimartin/gocui 'gocui') library.
![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)
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 stepping through each hunk and if a hunk can't be split down any further but contains code you don't want to stage, bad luck? *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!*
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!*
If you're a mere mortal like me and you're tired of hearing how powerful git is when in your daily life it's a powerful pain in your ass, lazygit might be for you.
![Gif](/docs/resources/lazygit-example.gif)
![Gif](/docs/resources/staging.gif)
- [Installation](https://github.com/jesseduffield/lazygit#installation)
- [Usage](https://github.com/jesseduffield/lazygit#usage),
[Keybindings](/docs/keybindings)
- [Cool Features](https://github.com/jesseduffield/lazygit#cool-features)
- [Contributing](https://github.com/jesseduffield/lazygit#contributing)
- [Video Tutorial](https://youtu.be/VDXvbHZYeKY)
- [Rebase Magic Video Tutorial](https://youtu.be/4XaToVut_hs)
- [Twitch Stream](https://www.twitch.tv/jesseduffield)
## Table of contents
[<img src="https://i.imgur.com/sVEktDn.png">](https://youtu.be/CPLdltN7wgE)
- [Installation](#installation)
- [Binary releases](#binary-releases)
- [Homebrew](#homebrew)
- [MacPorts](#macports)
- [Ubuntu](#ubuntu)
- [Void Linux](#void-linux)
- [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)
- [Usage](#usage)
- [Keybindings](#keybindings)
- [Changing directory on exit](#changing-directory-on-exit)
- [Undo/Redo](#undoredo)
- [Configuration](#configuration)
- [Custom pagers](#configuration)
- [Tutorials](#tutorials)
- [Cool Features](#cool-features)
- [Contributing](#contributing)
- [Donate](#donate)
- [Alternatives](#alternatives)
Github Sponsors is matching all donations dollar-for-dollar for 12 months so if you're feeling generous consider [sponsoring me](https://github.com/sponsors/jesseduffield)
[<img src="https://i.imgur.com/sVEktDn.png">](https://youtu.be/CPLdltN7wgE)
## Installation
### Binary Releases
For Windows, Mac OS or Linux, you can download a binary release [here](../../releases).
### Homebrew
Normally the lazygit formula can be found in the Homebrew core but we suggest you tap our formula to get the frequently updated one. It works with Linux, too.
@@ -67,6 +96,7 @@ They follow upstream latest releases
```sh
sudo xbps-install -S lazygit
```
### Scoop (Windows)
You can install `lazygit` using [scoop](https://scoop.sh/). It's in the `extras` bucket:
@@ -101,6 +131,19 @@ sudo dnf copr enable atim/lazygit -y
sudo dnf install lazygit
```
### Solus Linux
```sh
sudo eopkg install lazygit
```
### FreeBSD
```sh
pkg install lazygit
```
### Conda
Released versions are available for different platforms, see <https://anaconda.org/conda-forge/lazygit>
@@ -109,10 +152,6 @@ Released versions are available for different platforms, see <https://anaconda.o
conda install -c conda-forge lazygit
```
### Binary Release (Windows/Linux/OSX)
You can download a binary release [here](https://github.com/jesseduffield/lazygit/releases).
### Go
```sh
@@ -125,18 +164,24 @@ may need to add `~/go/bin` to your \$PATH (MacOS/Linux), or `%HOME%\go\bin`
(Windows). Not to be mistaked for `C:\Go\bin` (which is for Go's own binaries,
not apps like Lazygit).
## Usage
Call `lazygit` in your terminal inside a git repository. If you want, you can
Call `lazygit` in your terminal inside a git repository.
```sh
$ lazygit
```
If you want, you can
also add an alias for this with `echo "alias lg='lazygit'" >> ~/.zshrc` (or
whichever rc file you're using).
- Basic video tutorial [here](https://youtu.be/VDXvbHZYeKY).
- Rebase Magic tutorial [here](https://youtu.be/4XaToVut_hs)
- List of keybindings
[here](/docs/keybindings).
### Keybindings
## Changing Directory On Exit
You can check out the list of keybindings [here](/docs/keybindings).
### Changing Directory On Exit
If you change repos in lazygit and want your shell to change directory into that repo on exiting lazygit, add this to your `~/.zshrc` (or other rc file):
@@ -156,14 +201,25 @@ lg()
Then `source ~/.zshrc` and from now on when you call `lg` and exit you'll switch directories to whatever you were in inside lazyigt. To override this behaviour you can exit using `shift+Q` rather than just `q`.
### Undo/Redo
See the [docs](/docs/Undoing.md)
## Configuration
Check the [configuration docs](docs/Config.md).
Check out the [configuration docs](docs/Config.md).
## Custom Pagers
### Custom Pagers
See the [docs](docs/Custom_Pagers.md)
## Tutorials
- [Video Tutorial](https://youtu.be/VDXvbHZYeKY)
- [Rebase Magic Video Tutorial](https://youtu.be/4XaToVut_hs)
- [Twitch Stream](https://www.twitch.tv/jesseduffield)
## Cool features
- Adding files easily
@@ -179,14 +235,14 @@ See the [docs](docs/Custom_Pagers.md)
### Interactive Rebasing
![Interactive Rebasing](/docs/resources/interactive-rebase.png)
![Interactive Rebasing](/docs/resources/rebase.gif)
## Contributing
We love your input! Please check out the [contributing guide](CONTRIBUTING.md).
For contributor discussion about things not better discussed here in the repo, join the slack channel
[![Slack](/docs/resources/slack_rgb.png)](https://join.slack.com/t/lazygit/shared_invite/enQtNDE3MjIwNTYyMDA0LTM3Yjk3NzdiYzhhNTA1YjM4Y2M4MWNmNDBkOTI0YTE4YjQ1ZmI2YWRhZTgwNjg2YzhhYjg3NDBlMmQyMTI5N2M)
[![Slack](/docs/resources/slack_rgb.png)](https://join.slack.com/t/lazygit/shared_invite/zt-5bo2clzo-hB8ZTVN5dWUCqj5QFiQVLA)
## Donate
@@ -209,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)
+64 -6
View File
@@ -4,6 +4,7 @@ Default path for the config file:
* Linux: `~/.config/jesseduffield/lazygit/config.yml`
* MacOS: `~/Library/Application Support/jesseduffield/lazygit/config.yml`
* Windows: `%APPDATA%\jesseduffield\lazygit\config.yml`
## Default
@@ -13,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:
@@ -23,11 +26,14 @@ Default path for the config file:
optionsTextColor:
- blue
selectedLineBgColor:
- default
selectedRangeBgColor:
- blue
commitLength:
show: true
mouseEvents: true
skipUnstageLineWarning: false
skipStashWarning: true
git:
paging:
colorArg: always
@@ -35,13 +41,22 @@ Default path for the config file:
merging:
# only applicable to unix users
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}} --"
overrideGpg: false # prevents lazygit from spawning a separate process when using GPG
disableForcePushing: false
update:
method: prompt # can be: prompt | background | never
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'
@@ -53,6 +68,10 @@ Default path for the config file:
nextItem: '<down>' # go one line down
prevItem-alt: 'k' # go one line up
nextItem-alt: 'j' # go one line down
prevPage: ',' # go to next page in list
nextPage: '.' # go to previous page in list
gotoTop: '<' # go to top of list
gotoBottom: '>' # go to bottom of list
prevBlock: '<left>' # goto the previous block / panel
nextBlock: '<right>' # goto the next block / panel
prevBlock-alt: 'h' # goto the previous block / panel
@@ -63,6 +82,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'
@@ -83,6 +104,12 @@ Default path for the config file:
prevTab: '['
nextScreenMode: '+'
prevScreenMode: '_'
undo: 'z'
redo: '<c-z>'
filteringMenu: '<c-s>'
diffingMenu: 'W'
diffingMenu-alt: '<c-e>' # deprecated
copyToClipboard: '<c-o>'
status:
checkForUpdate: 'u'
recentRepos: '<enter>'
@@ -126,7 +153,6 @@ Default path for the config file:
cherryPickCopyRange: 'C'
pasteCommits: 'v'
tagCommit: 'T'
toggleDiffCommit: 'i'
checkoutCommit: '<space>'
resetCherryPick: '<c-R>'
stash:
@@ -138,7 +164,6 @@ Default path for the config file:
toggleDragSelect-alt: 'V'
toggleSelectHunk: 'a'
pickBothHunks: 'b'
undo: 'z'
```
## Platform Defaults
@@ -170,7 +195,7 @@ for users of VSCode
```yaml
os:
openCommand: 'code -r {{filename}}'
openCommand: 'code -rg {{filename}}'
```
## Color Attributes
@@ -205,7 +230,20 @@ If you have issues with a light terminal theme where you can't read / see the te
inactiveBorderColor:
- black
selectedLineBgColor:
- blue
- default
```
## Struggling to see selected line
If you struggle to see the selected line I recomment using the reverse attribute on selected lines like so:
```yaml
gui:
theme:
selectedLineBgColor:
- reverse
selectedRangeBgColor:
- reverse
```
## Example Coloring
@@ -214,7 +252,7 @@ If you have issues with a light terminal theme where you can't read / see the te
## Keybindings
For all possible keybinding options, check [Custom_Keybinding.md](https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybinding.md)
For all possible keybinding options, check [Custom_Keybindings.md](https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md)
### Example Keybindings For Colemak Users
@@ -234,14 +272,18 @@ For all possible keybinding options, check [Custom_Keybinding.md](https://github
scrollDownMain-alt1: 'E'
scrollUpMain-alt2: '<c-u>'
scrollDownMain-alt2: '<c-e>'
undo: 'l'
redo: '<c-r>'
diffingMenu: 'M'
filteringMenu: '<c-f>'
files:
ignoreFile: 'I'
commits:
moveDownCommit: '<c-e>'
moveUpCommit: '<c-u>'
toggleDiffCommit: 'l'
branches:
viewGitFlowOptions: 'I'
setUpstream: 'U'
```
## Custom pull request URLs
@@ -260,3 +302,19 @@ Where:
- `gitDomain` stands for the domain used by git itself (i.e. the one present on clone URLs), e.g. `git.work.com`
- `provider` is one of `github`, `bitbucket` or `gitlab`
- `webDomain` is the URL where your git service exposes a web interface and APIs, e.g. `gitservice.work.com`
## Predefined commit message prefix
In situations where certain naming pattern is used for branches and commits, pattern can be used to populate
commit message with prefix that is parsed from the branch name.
Example:
* Branch name: feature/AB-123
* Commit message: [AB-123] Adding feature
```yaml
git:
commitPrefixes:
my_project: # This is repository folder name
pattern: "^\\w+\\/(\\w+-\\w+)"
replace: "[$1] "
```
+4 -4
View File
@@ -1,8 +1,8 @@
# Custom Pagers
Lazygit supports custom pagers, [configured](/docs/Config.md) in the config.yml file (which can be opened by pressing 'o' in the Status panel).
Lazygit supports custom pagers, [configured](/docs/Config.md) in the config.yml file (which can be opened by pressing `o` in the Status panel).
Support does not extend to windows users, because we're making use of a package which doesn't have windows support.
Support does not extend to Windows users, because we're making use of a package which doesn't have Windows support.
## Default:
@@ -13,7 +13,7 @@ git:
useConfig: false
```
the `colorArg` key is for whether you want the `--color=always` arg in your `git diff` command. Some pagers want it set to always, others want it set to 'never'.
the `colorArg` key is for whether you want the `--color=always` arg in your `git diff` command. Some pagers want it set to `always`, others want it set to `never`.
## Delta:
@@ -61,4 +61,4 @@ git:
useConfig: true
```
If you set `useConfig: true`, lazygit will use whatever pager is specified in $GIT_PAGER, $PAGER, or your git config. If the pager ends with something like ' | less' we will strip that part out, because less doesn't play nice with our rendering approach. If the custom pager uses less under the hood, that will also break rendering (hence the `--paging=never` flag for the `delta` pager).
If you set `useConfig: true`, lazygit will use whatever pager is specified in `$GIT_PAGER`, `$PAGER`, or your *git config*. If the pager ends with something like ` | less` we will strip that part out, because less doesn't play nice with our rendering approach. If the custom pager uses less under the hood, that will also break rendering (hence the `--paging=never` flag for the `delta` pager).
+24
View File
@@ -0,0 +1,24 @@
# Undo/Redo in lazygit
![Gif](/docs/resources/undo2.gif)
## Keybindings:
'z' to undo, 'ctrl+z' to redo
## How it works
If you're as clumsy as me you'll probably have felt the pain of botching an interactive rebase or doing a hard reset onto the wrong commit. Luckily, the reflog allows you to trace your steps and make things right again, but I personally can't stand trying to make sense of the reflog.
Lazygit can read through your reflog for you and walk back action by action so that you don't even need to read the reflog. If lazygit finds a reflog entry where you checked out a branch, we'll checkout the original branch. If the entry is from a commit being applied, we'll go back to the commit before that. If we hit an interactive rebase, we'll go back to the commit you were on just before you started it.
## You can even undo things you did outside of lazygit!
Because lazygit just uses the reflog to keep track of things, it doesn't matter whether you're trying to undo something you did in lazygit or directly on the command line. You can open lazygit for the first time and start undoing thing in your repo! Likewise, lazygit marks its undos/redos in the reflog so if you quit the application and come back, lazygit still knows where you're up to.
## Limitations
There are limitations: firstly, lazygit can only undo things that are recorded in the reflog. That means changes to your working tree or stash aren't covered. Secondly, anything permanent you do like pushing to a remote can't be undone. Thirdly, actions like creating a branch won't be undone, because they're not stored in the reflog.
If you are mid-rebase, undo/redo is not supported, because the reflog doesn't enough contain information about what specific things have happened inside that rebase. If you want to undo out of a rebase, it's best to abort the rebase (the default keybinding for bringing up rebase options is 'm').
Undo/Redo is a new feature so if you find a bug let us know. The worst case scenario is that you'll just need to look at your reflog and manually put yourself back on track.
+64 -4
View File
@@ -11,9 +11,20 @@
<kbd>p</kbd>: pull
<kbd>R</kbd>: refresh
<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>:</kbd>: execute custom command
<kbd>|</kbd>: view scoping options
<kbd>∂</kbd>: open diff menu
</pre>
## Branches Panel
<pre>
<kbd>]</kbd>: next tab
<kbd>[</kbd>: previous tab
</pre>
## Branches Panel (Branches Tab)
@@ -31,20 +42,30 @@
<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
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## 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
<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
</pre>
## Branches Panel (Remotes Tab)
@@ -54,7 +75,11 @@
<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
</pre>
## Branches Panel (Tags Tab)
@@ -65,7 +90,11 @@
<kbd>P</kbd>: push tag
<kbd>n</kbd>: create tag
<kbd>g</kbd>: view reset options
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Commit Files Panel
@@ -75,15 +104,21 @@
<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
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Commits Panel
<pre>
<kbd>/</kbd>: start search
<kbd>]</kbd>: next tab
<kbd>[</kbd>: previous tab
</pre>
## Commits Panel (Commits Tab)
@@ -104,12 +139,19 @@
<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
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Commits Panel (Reflog Tab)
@@ -117,6 +159,11 @@
<pre>
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: view reset options
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Files Panel
@@ -139,7 +186,11 @@
<kbd>enter</kbd>: stage individual hunks/lines
<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
</pre>
## Main Panel (Merging)
@@ -166,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
@@ -183,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
@@ -201,8 +254,11 @@
<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
</pre>
## Stash Panel
@@ -211,7 +267,11 @@
<kbd>space</kbd>: apply
<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
</pre>
## Status Panel
+131 -71
View File
@@ -1,22 +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>+</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 Tab)
## Branches Paneel
<pre>
<kbd>]</kbd>: volgende tab
<kbd>[</kbd>: vorige tab
</pre>
## Branches Paneel (Branches Tab)
<pre>
<kbd>space</kbd>: uitchecken
@@ -27,99 +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>: start search
<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>: start search
<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>: start search
<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>: start search
<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>: start search
<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>: start search
<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 (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>: 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
@@ -133,21 +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>: start search
<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
@@ -155,70 +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>: start search
<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>: start search
<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>
+64 -4
View File
@@ -11,9 +11,20 @@
<kbd>p</kbd>: pull
<kbd>R</kbd>: odśwież
<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>:</kbd>: execute custom command
<kbd>|</kbd>: view scoping options
<kbd>∂</kbd>: open diff menu
</pre>
## Gałęzie Panel
<pre>
<kbd>]</kbd>: next tab
<kbd>[</kbd>: previous tab
</pre>
## Gałęzie Panel (Branches Tab)
@@ -31,7 +42,12 @@
<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
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Gałęzie Panel (Remote Branches (in Remotes tab))
@@ -40,11 +56,16 @@
<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
<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
</pre>
## Gałęzie Panel (Remotes Tab)
@@ -54,7 +75,11 @@
<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
</pre>
## Gałęzie Panel (Tags Tab)
@@ -65,7 +90,11 @@
<kbd>P</kbd>: push tag
<kbd>n</kbd>: create tag
<kbd>g</kbd>: view reset options
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Commit files Panel
@@ -75,15 +104,21 @@
<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
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Commity Panel
<pre>
<kbd>/</kbd>: start search
<kbd>]</kbd>: next tab
<kbd>[</kbd>: previous tab
</pre>
## Commity Panel (Commits Tab)
@@ -104,12 +139,19 @@
<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
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Commity Panel (Reflog Tab)
@@ -117,6 +159,11 @@
<pre>
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: view reset options
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Pliki Panel
@@ -139,7 +186,11 @@
<kbd>enter</kbd>: zatwierdź pojedyncze linie
<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
</pre>
## Main Panel (Merging)
@@ -152,7 +203,7 @@
<kbd>►</kbd>: select next conflict
<kbd>▲</kbd>: select top hunk
<kbd>▼</kbd>: select bottom hunk
<kbd>z</kbd>: undo
<kbd>z</kbd>: cofnij
</pre>
## Main Panel (Normal)
@@ -166,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
@@ -183,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
@@ -201,8 +254,11 @@
<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
</pre>
## Schowek Panel
@@ -211,7 +267,11 @@
<kbd>space</kbd>: zastosuj
<kbd>g</kbd>: wyciągnij
<kbd>d</kbd>: porzuć
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Status Panel
Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 KiB

+8 -11
View File
@@ -3,24 +3,25 @@ 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.11
github.com/fatih/color v1.7.0
github.com/fsnotify/fsnotify v1.4.7
github.com/go-errors/errors v1.0.1
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.20200309001002-7765949e1c8a
github.com/jesseduffield/pty v1.2.1
github.com/jesseduffield/termbox-go v0.0.0-20200130214842-1d31d1faa3c9 // 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
github.com/mattn/go-isatty v0.0.11 // indirect
github.com/mattn/go-runewidth v0.0.8
github.com/mattn/go-runewidth v0.0.9
github.com/mgutz/str v1.2.0
github.com/mitchellh/gox v1.0.1 // indirect
github.com/nicksnyder/go-i18n/v2 v2.0.3
github.com/onsi/ginkgo v1.10.3 // indirect
github.com/onsi/gomega v1.7.1 // indirect
@@ -34,11 +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/crypto v0.0.0-20191206172530-e9b2fee46413 // indirect
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 // indirect
golang.org/x/sys v0.0.0-20200824131525-c12d262b63d8 // indirect
golang.org/x/text v0.3.2
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/src-d/go-git.v4 v4.13.1
gopkg.in/yaml.v2 v2.2.7
)
+37 -44
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=
@@ -23,7 +25,9 @@ github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
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.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/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=
@@ -40,8 +44,16 @@ 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.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=
github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-git-fixtures/v4 v4.0.1 h1:q+IFMfLx200Q3scvt2hN79JsEzy4AmBTp/pqnefH+Bc=
github.com/go-git/go-git-fixtures/v4 v4.0.1/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
github.com/go-git/go-git/v5 v5.0.0 h1:k5RWPm4iJwYtfWoxIJy4wJX9ON7ihPeZZYC1fLYDnpg=
github.com/go-git/go-git/v5 v5.0.0/go.mod h1:oYD8y9kWsGINPFJoLdaScGCN6dlKg23blmClfZwtUVA=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
@@ -68,8 +80,6 @@ github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoA
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/go-version v1.0.0 h1:21MVWPKDphxa7ineQQTrCU5brh7OuVVAzGOCnnCPtE8=
github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
@@ -78,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.20200301081700-d6e485450113 h1:jHZRVJUWsU8HaQ0crocz0i0BkpOqFLDJEO/AtBp+Ecs=
github.com/jesseduffield/gocui v0.3.1-0.20200301081700-d6e485450113/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
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/pty v1.2.1 h1:7xYBiwNH0PpWqC8JmvrPq1a/ksNqyCavzWu9pbBGYWI=
github.com/jesseduffield/pty v1.2.1/go.mod h1:7jlS40+UhOqkZJDIG1B/H21xnuET/+fvbbnHCa8wSIo=
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/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=
@@ -104,9 +110,10 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
@@ -114,29 +121,26 @@ 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=
github.com/mgutz/str v1.2.0 h1:4IzWSdIz9qPQWLfKZ0rJcV0jcUDpxvP4JVZ4GXQyvSw=
github.com/mgutz/str v1.2.0/go.mod h1:w1v0ofgLaJdoD0HpQ3fycxKD1WtxpjSo151pK/31q6w=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/gox v1.0.1 h1:x0jD3dcHk9a9xPSDN6YEL4xL6Qz0dvNYm8yZqui5chI=
github.com/mitchellh/gox v1.0.1/go.mod h1:ED6BioOGXMswlXa2zxfh/xdd5QhwYliBFn9V18Ap4z4=
github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nicksnyder/go-i18n/v2 v2.0.3 h1:ks/JkQiOEhhuF6jpNvx+Wih1NIiXzUnZeZVnJuI8R8M=
github.com/nicksnyder/go-i18n/v2 v2.0.3/go.mod h1:oDab7q8XCYMRlcrBnaY/7B1eOectbvj6B1UPBT+p5jo=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY=
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
@@ -155,8 +159,8 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 h1:Xuk8ma/ibJ1fOy4Ee11vHhUFHQNpHhrBneOCNHVXS5w=
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0/go.mod h1:7AwjWCpdPhkSmNAgUv5C7EJ4AbmjEB3r047r3DXWu3Y=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
@@ -183,13 +187,9 @@ github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk=
github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4=
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
@@ -210,9 +210,8 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -223,10 +222,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -243,10 +240,11 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 h1:gSbV7h1NRL2G1xTg/owz62CST1oJBmxy4QpMMregXVQ=
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/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=
@@ -257,7 +255,6 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@@ -267,17 +264,13 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg=
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE=
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
+4 -1
View File
@@ -25,6 +25,9 @@ func main() {
repoPath := "."
flaggy.String(&repoPath, "p", "path", "Path of git repo")
filterPath := ""
flaggy.String(&filterPath, "f", "filter", "Path to filter on in `git log -- <path>`. When in filter mode, the commits, reflog, and stash are filtered based on the given path, and some operations are restricted")
dump := ""
flaggy.AddPositionalValue(&dump, "gitargs", 1, false, "Todo file")
flaggy.DefaultParser.PositionalFlags[0].Hidden = true
@@ -61,7 +64,7 @@ func main() {
log.Fatal(err.Error())
}
app, err := app.NewApp(appConfig)
app, err := app.NewApp(appConfig, filterPath)
if err == nil {
err = app.Run()
+66 -9
View File
@@ -2,11 +2,13 @@ package app
import (
"bufio"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands"
@@ -91,7 +93,7 @@ func newLogger(config config.AppConfigurer) *logrus.Entry {
}
// NewApp bootstrap a new application
func NewApp(config config.AppConfigurer) (*App, error) {
func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
app := &App{
closers: []io.Closer{},
Config: config,
@@ -113,7 +115,8 @@ func NewApp(config config.AppConfigurer) (*App, error) {
return app, err
}
if err := app.setupRepo(); err != nil {
showRecentRepos, err := app.setupRepo()
if err != nil {
return app, err
}
@@ -121,29 +124,75 @@ func NewApp(config config.AppConfigurer) (*App, error) {
if err != nil {
return app, err
}
app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config, app.Updater)
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) validateGitVersion() error {
output, err := app.OSCommand.RunCommandWithOutput("git --version")
// if we get an error anywhere here we'll show the same status
minVersionError := errors.New(app.Tr.SLocalize("minGitVersionError"))
if err != nil {
return minVersionError
}
// output should be something like: 'git version 2.23.0'
// first number in the string should be greater than 0
split := strings.Split(output, " ")
gitVersion := split[len(split)-1]
majorVersion, err := strconv.Atoi(gitVersion[0:1])
if err != nil {
return minVersionError
}
if majorVersion < 2 {
return minVersionError
}
return nil
}
func (app *App) setupRepo() (bool, error) {
if err := app.validateGitVersion(); err != nil {
return false, err
}
// 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 {
if !strings.Contains(err.Error(), "Not a git repository") {
return err
cwd, err := os.Getwd()
if err != nil {
return false, err
}
info, _ := os.Stat(filepath.Join(cwd, ".git"))
if info != nil && info.IsDir() {
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 {
@@ -195,9 +244,17 @@ func (app *App) Close() error {
func (app *App) KnownError(err error) (string, bool) {
errorMessage := err.Error()
knownErrorMessages := []string{app.Tr.SLocalize("minGitVersionError")}
for _, message := range knownErrorMessages {
if errorMessage == message {
return message, true
}
}
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"),
},
}
+15 -1
View File
@@ -3,10 +3,24 @@ package commands
// Branch : A git branch
// duplicating this for now
type Branch struct {
Name string
Name string
// the displayname is something like '(HEAD detached at 123asdf)', whereas in that case the name would be '123asdf'
DisplayName string
Recency string
Pushables string
Pullables string
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()
}
+52 -83
View File
@@ -5,7 +5,6 @@ import (
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
@@ -22,28 +21,22 @@ import (
// BranchListBuilder returns a list of Branch objects for the current repo
type BranchListBuilder struct {
Log *logrus.Entry
GitCommand *GitCommand
Log *logrus.Entry
GitCommand *GitCommand
ReflogCommits []*Commit
}
// NewBranchListBuilder builds a new branch list builder
func NewBranchListBuilder(log *logrus.Entry, gitCommand *GitCommand) (*BranchListBuilder, error) {
func NewBranchListBuilder(log *logrus.Entry, gitCommand *GitCommand, reflogCommits []*Commit) (*BranchListBuilder, error) {
return &BranchListBuilder{
Log: log,
GitCommand: gitCommand,
Log: log,
GitCommand: gitCommand,
ReflogCommits: reflogCommits,
}, nil
}
func (b *BranchListBuilder) obtainCurrentBranchName() string {
branchName, err := b.GitCommand.CurrentBranchName()
if err != nil {
panic(err.Error())
}
return branchName
}
func (b *BranchListBuilder) obtainBranches() []*Branch {
cmdStr := `git branch --format="%(HEAD)|%(refname:short)|%(upstream:short)|%(upstream:track)"`
cmdStr := `git for-each-ref --sort=-committerdate --format="%(HEAD)|%(refname:short)|%(upstream:short)|%(upstream:track)" refs/heads`
output, err := b.GitCommand.OSCommand.RunCommandWithOutput(cmdStr)
if err != nil {
panic(err)
@@ -51,40 +44,48 @@ func (b *BranchListBuilder) obtainBranches() []*Branch {
trimmedOutput := strings.TrimSpace(output)
outputLines := strings.Split(trimmedOutput, "\n")
branches := make([]*Branch, len(outputLines))
for i, line := range outputLines {
branches := make([]*Branch, 0, len(outputLines))
for _, line := range outputLines {
if line == "" {
continue
}
split := strings.Split(line, SEPARATION_CHAR)
name := split[1]
branches[i] = &Branch{
name := strings.TrimPrefix(split[1], "heads/")
branch := &Branch{
Name: name,
Pullables: "?",
Pushables: "?",
Head: split[0] == "*",
}
upstreamName := split[2]
if upstreamName == "" {
branches = append(branches, branch)
continue
}
branches[i].UpstreamName = upstreamName
branch.UpstreamName = upstreamName
track := split[3]
re := regexp.MustCompile(`ahead (\d+)`)
match := re.FindStringSubmatch(track)
if len(match) > 1 {
branches[i].Pushables = match[1]
branch.Pushables = match[1]
} else {
branches[i].Pushables = "0"
branch.Pushables = "0"
}
re = regexp.MustCompile(`behind (\d+)`)
match = re.FindStringSubmatch(track)
if len(match) > 1 {
branches[i].Pullables = match[1]
branch.Pullables = match[1]
} else {
branches[i].Pullables = "0"
branch.Pullables = "0"
}
branches = append(branches, branch)
}
return branches
@@ -92,7 +93,6 @@ func (b *BranchListBuilder) obtainBranches() []*Branch {
// Build the list of branches for the current repo
func (b *BranchListBuilder) Build() []*Branch {
currentBranchName := b.obtainCurrentBranchName()
branches := b.obtainBranches()
reflogBranches := b.obtainReflogBranches()
@@ -116,76 +116,45 @@ outer:
branches = append(branchesWithRecency, branches...)
if len(branches) == 0 {
branches = append([]*Branch{{Name: currentBranchName}}, branches...)
}
foundHead := false
for i, branch := range branches {
if branch.Head {
branch.Name = currentBranchName
foundHead = true
branch.Recency = " *"
branches = append(branches[0:i], branches[i+1:]...)
branches = append([]*Branch{branch}, branches...)
break
}
}
if !foundHead {
currentBranchName, currentBranchDisplayName, err := b.GitCommand.CurrentBranchName()
if err != nil {
panic(err)
}
branches = append([]*Branch{{Name: currentBranchName, DisplayName: currentBranchDisplayName, Head: true, Recency: " *"}}, branches...)
}
return branches
}
// A line will have the form '10 days ago master' so we need to strip out the
// useful information from that into timeNumber, timeUnit, and branchName
func branchInfoFromLine(line string) (string, string) {
// example line: HEAD@{12 minutes ago}|checkout: moving from pulling-from-forks to tim77-patch-1
r := regexp.MustCompile(`HEAD\@\{([^\s]+) ([^\s]+) ago\}\|.*?([^\s]*)$`)
matches := r.FindStringSubmatch(strings.TrimSpace(line))
if len(matches) == 0 {
return "", ""
}
since := matches[1]
unit := matches[2]
branchName := matches[3]
return since + abbreviatedTimeUnit(unit), branchName
}
func abbreviatedTimeUnit(timeUnit string) string {
r := regexp.MustCompile("s$")
timeUnit = r.ReplaceAllString(timeUnit, "")
timeUnitMap := map[string]string{
"hour": "h",
"minute": "m",
"second": "s",
"week": "w",
"year": "y",
"day": "d",
"month": "m",
}
return timeUnitMap[timeUnit]
}
// TODO: only look at the new reflog commits, and otherwise store the recencies in
// int form against the branch to recalculate the time ago
func (b *BranchListBuilder) obtainReflogBranches() []*Branch {
branches := make([]*Branch, 0)
// if we directly put this string in RunCommandWithOutput the compiler complains because it thinks it's a format string
unescaped := "git reflog --date=relative --pretty='%gd|%gs' --grep-reflog='checkout: moving' HEAD"
rawString, err := b.GitCommand.OSCommand.RunCommandWithOutput(unescaped)
if err != nil {
return branches
}
branchNameMap := map[string]bool{}
branchLines := utils.SplitLines(rawString)
for _, line := range branchLines {
recency, branchName := branchInfoFromLine(line)
if branchName == "" {
continue
foundBranchesMap := map[string]bool{}
re := regexp.MustCompile(`checkout: moving from ([\S]+) to ([\S]+)`)
reflogBranches := make([]*Branch, 0, len(b.ReflogCommits))
for _, commit := range b.ReflogCommits {
if match := re.FindStringSubmatch(commit.Name); len(match) == 3 {
recency := utils.UnixToTimeAgo(commit.UnixTimestamp)
for _, branchName := range match[1:] {
if !foundBranchesMap[branchName] {
foundBranchesMap[branchName] = true
reflogBranches = append(reflogBranches, &Branch{
Recency: recency,
Name: branchName,
})
}
}
}
if _, ok := branchNameMap[branchName]; ok {
continue
}
branchNameMap[branchName] = true
branch := &Branch{Name: branchName, Recency: recency}
branches = append(branches, branch)
}
return branches
return reflogBranches
}
+25 -3
View File
@@ -1,15 +1,37 @@
package commands
import "fmt"
// Commit : A git commit
type Commit struct {
Sha string
Name string
Status string // one of "unpushed", "pushed", "merged", "rebasing" or "selected"
DisplayString string
Action string // one of "", "pick", "edit", "squash", "reword", "drop", "fixup"
Copied bool // to know if this commit is ready to be cherry-picked somewhere
Tags []string
ExtraInfo string // something like 'HEAD -> master, tag: v0.15.2'
Author string
Date string
UnixTimestamp int64
// IsMerge tells us whether we're dealing with a merge commit i.e. a commit with two parents
IsMerge bool
}
func (c *Commit) ShortSha() string {
if len(c.Sha) < 8 {
return c.Sha
}
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)
}
+15 -13
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
}
+138 -86
View File
@@ -4,13 +4,14 @@ import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
@@ -27,24 +28,20 @@ const SEPARATION_CHAR = "|"
// CommitListBuilder returns a list of Branch objects for the current repo
type CommitListBuilder struct {
Log *logrus.Entry
GitCommand *GitCommand
OSCommand *OSCommand
Tr *i18n.Localizer
CherryPickedCommits []*Commit
DiffEntries []*Commit
Log *logrus.Entry
GitCommand *GitCommand
OSCommand *OSCommand
Tr *i18n.Localizer
}
// NewCommitListBuilder builds a new commit list builder
func NewCommitListBuilder(log *logrus.Entry, gitCommand *GitCommand, osCommand *OSCommand, tr *i18n.Localizer, cherryPickedCommits []*Commit, diffEntries []*Commit) (*CommitListBuilder, error) {
func NewCommitListBuilder(log *logrus.Entry, gitCommand *GitCommand, osCommand *OSCommand, tr *i18n.Localizer) *CommitListBuilder {
return &CommitListBuilder{
Log: log,
GitCommand: gitCommand,
OSCommand: osCommand,
Tr: tr,
CherryPickedCommits: cherryPickedCommits,
DiffEntries: diffEntries,
}, nil
Log: log,
GitCommand: gitCommand,
OSCommand: osCommand,
Tr: tr,
}
}
// extractCommitFromLine takes a line from a git log and extracts the sha, message, date, and tag if present
@@ -55,10 +52,12 @@ func (c *CommitListBuilder) extractCommitFromLine(line string) *Commit {
split := strings.Split(line, SEPARATION_CHAR)
sha := split[0]
date := split[1]
unixTimestamp := split[1]
author := split[2]
extraInfo := strings.TrimSpace(split[3])
message := strings.Join(split[4:], SEPARATION_CHAR)
parentHashes := split[4]
message := strings.Join(split[5:], SEPARATION_CHAR)
tags := []string{}
if extraInfo != "" {
@@ -69,46 +68,103 @@ func (c *CommitListBuilder) extractCommitFromLine(line string) *Commit {
}
}
unitTimestampInt, _ := strconv.Atoi(unixTimestamp)
// Any commit with multiple parents is a merge commit.
// If there's a space then it means there must be more than one parent hash
isMerge := strings.Contains(parentHashes, " ")
return &Commit{
Sha: sha,
Name: message,
DisplayString: line,
Tags: tags,
ExtraInfo: extraInfo,
Date: date,
UnixTimestamp: int64(unitTimestampInt),
Author: author,
IsMerge: isMerge,
}
}
type GetCommitsOptions struct {
Limit bool
FilterPath string
IncludeRebaseCommits bool
RefName string // e.g. "HEAD" or "my_branch"
}
func (c *CommitListBuilder) MergeRebasingCommits(commits []*Commit) ([]*Commit, error) {
// chances are we have as many commits as last time so we'll set the capacity to be the old length
result := make([]*Commit, 0, len(commits))
for i, commit := range commits {
if commit.Status != "rebasing" { // removing the existing rebase commits so we can add the refreshed ones
result = append(result, commits[i:]...)
break
}
}
rebaseMode, err := c.GitCommand.RebaseMode()
if err != nil {
return nil, err
}
if rebaseMode == "" {
// not in rebase mode so return original commits
return result, nil
}
rebasingCommits, err := c.getRebasingCommits(rebaseMode)
if err != nil {
return nil, err
}
if len(rebasingCommits) > 0 {
result = append(rebasingCommits, result...)
}
return result, nil
}
// GetCommits obtains the commits of the current branch
func (c *CommitListBuilder) GetCommits(limit bool) ([]*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 != "" {
// here we want to also prepend the commits that we're in the process of rebasing
rebasingCommits, err = c.getRebasingCommits(rebaseMode)
if opts.IncludeRebaseCommits && opts.FilterPath == "" {
var err error
rebasingCommits, err = c.MergeRebasingCommits(commits)
if err != nil {
return nil, err
}
if len(rebasingCommits) > 0 {
commits = append(commits, rebasingCommits...)
commits = append(commits, rebasingCommits...)
}
passedFirstPushedCommit := false
firstPushedCommit, err := c.getFirstPushedCommit(opts.RefName)
if err != nil {
// must have no upstream branch so we'll consider everything as pushed
passedFirstPushedCommit = true
}
cmd := c.getLogCmd(opts)
err = RunLineOutputCmd(cmd, func(line string) (bool, error) {
if strings.Split(line, " ")[0] != "gpg:" {
commit := c.extractCommitFromLine(line)
if commit.Sha == firstPushedCommit {
passedFirstPushedCommit = true
}
commit.Status = map[bool]string{true: "unpushed", false: "pushed"}[!passedFirstPushedCommit]
commits = append(commits, commit)
}
return false, nil
})
if err != nil {
return nil, err
}
unpushedCommits := c.getUnpushedCommits()
log := c.getLog(limit)
// now we can split it up and turn it into commits
for _, line := range utils.SplitLines(log) {
commit := c.extractCommitFromLine(line)
_, unpushed := unpushedCommits[commit.Sha[:8]]
commit.Status = map[bool]string{true: "unpushed", false: "pushed"}[unpushed]
commits = append(commits, commit)
}
if rebaseMode != "" {
currentCommit := commits[len(rebasingCommits)]
blue := color.New(color.FgYellow)
@@ -116,24 +172,11 @@ func (c *CommitListBuilder) GetCommits(limit bool) ([]*Commit, error) {
currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name)
}
commits, err = c.setCommitMergedStatuses(commits)
commits, err = c.setCommitMergedStatuses(opts.RefName, commits)
if err != nil {
return nil, err
}
commits, err = c.setCommitCherryPickStatuses(commits)
if err != nil {
return nil, err
}
for _, commit := range commits {
for _, entry := range c.DiffEntries {
if entry.Sha == commit.Sha {
commit.Status = "selected"
}
}
}
return commits, nil
}
@@ -216,6 +259,9 @@ func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*Commit, error) {
if line == "" || line == "noop" {
return commits, nil
}
if strings.HasPrefix(line, "#") {
continue
}
splitLine := strings.Split(line, " ")
commits = append([]*Commit{{
Sha: splitLine[1],
@@ -225,7 +271,7 @@ func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*Commit, error) {
}}, commits...)
}
return nil, nil
return commits, nil
}
// assuming the file starts like this:
@@ -244,8 +290,8 @@ func (c *CommitListBuilder) commitFromPatch(content string) (*Commit, error) {
}, nil
}
func (c *CommitListBuilder) setCommitMergedStatuses(commits []*Commit) ([]*Commit, error) {
ancestor, err := c.getMergeBase()
func (c *CommitListBuilder) setCommitMergedStatuses(refName string, commits []*Commit) ([]*Commit, error) {
ancestor, err := c.getMergeBase(refName)
if err != nil {
return nil, err
}
@@ -267,19 +313,8 @@ func (c *CommitListBuilder) setCommitMergedStatuses(commits []*Commit) ([]*Commi
return commits, nil
}
func (c *CommitListBuilder) setCommitCherryPickStatuses(commits []*Commit) ([]*Commit, error) {
for _, commit := range commits {
for _, cherryPickedCommit := range c.CherryPickedCommits {
if commit.Sha == cherryPickedCommit.Sha {
commit.Copied = true
}
}
}
return commits, nil
}
func (c *CommitListBuilder) getMergeBase() (string, error) {
currentBranch, err := c.GitCommand.CurrentBranchName()
func (c *CommitListBuilder) getMergeBase(refName string) (string, error) {
currentBranch, _, err := c.GitCommand.CurrentBranchName()
if err != nil {
return "", err
}
@@ -290,38 +325,55 @@ func (c *CommitListBuilder) getMergeBase() (string, error) {
}
// swallowing error because it's not a big deal; probably because there are no commits yet
output, _ := c.OSCommand.RunCommandWithOutput("git merge-base HEAD %s", baseBranch)
return output, nil
output, _ := c.OSCommand.RunCommandWithOutput("git merge-base %s %s", refName, baseBranch)
return ignoringWarnings(output), nil
}
// 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 {
pushables := map[string]bool{}
o, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..HEAD --abbrev-commit --abbrev=8")
func ignoringWarnings(commandOutput string) string {
trimmedOutput := strings.TrimSpace(commandOutput)
split := strings.Split(trimmedOutput, "\n")
// need to get last line in case the first line is a warning about how the error is ambiguous.
// At some point we should find a way to make it unambiguous
lastLine := split[len(split)-1]
return lastLine
}
// getFirstPushedCommit returns the first commit SHA which has been pushed to the ref's upstream.
// all commits above this are deemed unpushed and marked as such.
func (c *CommitListBuilder) getFirstPushedCommit(refName string) (string, error) {
output, err := c.OSCommand.RunCommandWithOutput("git merge-base %s %s@{u}", refName, refName)
if err != nil {
return pushables
}
for _, p := range utils.SplitLines(o) {
pushables[p] = true
return "", err
}
return pushables
return ignoringWarnings(output), nil
}
// getLog gets the git log.
func (c *CommitListBuilder) getLog(limit bool) string {
func (c *CommitListBuilder) getLogCmd(opts GetCommitsOptions) *exec.Cmd {
limitFlag := ""
if limit {
limitFlag = "-30"
if opts.Limit {
limitFlag = "-300"
}
result, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git log --oneline --pretty=format:\"%%H%s%%ar%s%%aN%s%%d%s%%s\" %s --abbrev=%d", SEPARATION_CHAR, SEPARATION_CHAR, SEPARATION_CHAR, SEPARATION_CHAR, limitFlag, 20))
if err != nil {
// assume if there is an error there are no commits yet for this branch
return ""
filterFlag := ""
if opts.FilterPath != "" {
filterFlag = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(opts.FilterPath))
}
return result
return c.OSCommand.ExecutableFromString(
fmt.Sprintf(
"git log %s --oneline --pretty=format:\"%%H%s%%at%s%%aN%s%%d%s%%p%s%%s\" %s --abbrev=%d --date=unix %s",
opts.RefName,
SEPARATION_CHAR,
SEPARATION_CHAR,
SEPARATION_CHAR,
SEPARATION_CHAR,
SEPARATION_CHAR,
limitFlag,
20,
filterFlag,
),
)
}
+7 -47
View File
@@ -13,50 +13,10 @@ func NewDummyCommitListBuilder() *CommitListBuilder {
osCommand := NewDummyOSCommand()
return &CommitListBuilder{
Log: NewDummyLog(),
GitCommand: NewDummyGitCommandWithOSCommand(osCommand),
OSCommand: osCommand,
Tr: i18n.NewLocalizer(NewDummyLog()),
CherryPickedCommits: []*Commit{},
}
}
// TestCommitListBuilderGetUnpushedCommits is a function.
func TestCommitListBuilderGetUnpushedCommits(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(map[string]bool)
}
scenarios := []scenario{
{
"Can't retrieve pushable commits",
func(string, ...string) *exec.Cmd {
return exec.Command("test")
},
func(pushables map[string]bool) {
assert.EqualValues(t, map[string]bool{}, pushables)
},
},
{
"Retrieve pushable commits",
func(cmd string, args ...string) *exec.Cmd {
return exec.Command("echo", "8a2bb0e\n78976bc")
},
func(pushables map[string]bool) {
assert.Len(t, pushables, 2)
assert.EqualValues(t, map[string]bool{"8a2bb0e": true, "78976bc": true}, pushables)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.getUnpushedCommits())
})
Log: NewDummyLog(),
GitCommand: NewDummyGitCommandWithOSCommand(osCommand),
OSCommand: osCommand,
Tr: i18n.NewLocalizer(NewDummyLog()),
}
}
@@ -106,7 +66,7 @@ func TestCommitListBuilderGetMergeBase(t *testing.T) {
},
func(output string, err error) {
assert.NoError(t, err)
assert.Equal(t, "blah\n", output)
assert.Equal(t, "blah", output)
},
},
{
@@ -126,7 +86,7 @@ func TestCommitListBuilderGetMergeBase(t *testing.T) {
},
func(output string, err error) {
assert.NoError(t, err)
assert.Equal(t, "blah\n", output)
assert.Equal(t, "blah", output)
},
},
{
@@ -145,7 +105,7 @@ func TestCommitListBuilderGetMergeBase(t *testing.T) {
t.Run(s.testName, func(t *testing.T) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.getMergeBase())
s.test(c.getMergeBase("HEAD"))
})
}
}
+6 -1
View File
@@ -19,6 +19,11 @@ func NewDummyOSCommand() *OSCommand {
// NewDummyAppConfig creates a new dummy AppConfig for testing
func NewDummyAppConfig() *config.AppConfig {
userConfig := viper.New()
userConfig.SetConfigType("yaml")
if err := config.LoadDefaults(userConfig, config.GetDefaultConfig()); err != nil {
panic(err)
}
appConfig := &config.AppConfig{
Name: "lazygit",
Version: "unversioned",
@@ -26,7 +31,7 @@ func NewDummyAppConfig() *config.AppConfig {
BuildDate: "",
Debug: false,
BuildSource: "",
UserConfig: viper.New(),
UserConfig: userConfig,
}
_ = yaml.Unmarshal([]byte{}, appConfig.AppState)
return appConfig
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"github.com/go-errors/errors"
"github.com/jesseduffield/pty"
"github.com/creack/pty"
)
// RunCommandWithOutputLiveWrapper runs a command and return every word that gets written in stdout
+30
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
}
+360 -115
View File
@@ -15,12 +15,13 @@ 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"
"github.com/sirupsen/logrus"
gitconfig "github.com/tcnksm/go-gitconfig"
gogit "gopkg.in/src-d/go-git.v4"
)
// this takes something like:
@@ -84,7 +85,10 @@ 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
}
// NewGitCommand it runs git commands
@@ -92,6 +96,15 @@ func NewGitCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localizer,
var worktree *gogit.Worktree
var repo *gogit.Repository
// see what our default push behaviour is
output, err := osCommand.RunCommandWithOutput("git config --get push.default")
pushToCurrent := false
if err != nil {
log.Errorf("error reading git config: %v", err)
} else {
pushToCurrent = strings.TrimSpace(output) == "current"
}
fs := []func() error{
func() error {
return verifyInGitRepo(osCommand.RunCommand)
@@ -128,9 +141,10 @@ func NewGitCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localizer,
getLocalGitConfig: gitconfig.Local,
removeFile: os.RemoveAll,
DotGitDir: dotGitDir,
PushToCurrent: pushToCurrent,
}
gitCommand.PatchManager = NewPatchManager(log, gitCommand.ApplyPatch)
gitCommand.PatchManager = patch.NewPatchManager(log, gitCommand.ApplyPatch, gitCommand.ShowFileDiff)
return gitCommand, nil
}
@@ -156,9 +170,7 @@ func findDotGitDir(stat func(string) (os.FileInfo, error), readFile func(filenam
return strings.TrimSpace(strings.TrimPrefix(fileContent, "gitdir: ")), nil
}
// GetStashEntries stash entries
func (c *GitCommand) GetStashEntries() []*StashEntry {
// if we directly put this string in RunCommandWithOutput the compiler complains because it thinks it's a format string
func (c *GitCommand) getUnfilteredStashEntries() []*StashEntry {
unescaped := "git stash list --pretty='%gs'"
rawString, _ := c.OSCommand.RunCommandWithOutput(unescaped)
stashEntries := []*StashEntry{}
@@ -168,26 +180,75 @@ func (c *GitCommand) GetStashEntries() []*StashEntry {
return stashEntries
}
// GetStashEntries stash entries
func (c *GitCommand) GetStashEntries(filterPath string) []*StashEntry {
if filterPath == "" {
return c.getUnfilteredStashEntries()
}
unescaped := fmt.Sprintf("git stash list --name-only")
rawString, err := c.OSCommand.RunCommandWithOutput(unescaped)
if err != nil {
return c.getUnfilteredStashEntries()
}
stashEntries := []*StashEntry{}
var currentStashEntry *StashEntry
lines := utils.SplitLines(rawString)
isAStash := func(line string) bool { return strings.HasPrefix(line, "stash@{") }
re := regexp.MustCompile(`stash@\{(\d+)\}`)
outer:
for i := 0; i < len(lines); i++ {
if !isAStash(lines[i]) {
continue
}
match := re.FindStringSubmatch(lines[i])
idx, err := strconv.Atoi(match[1])
if err != nil {
return c.getUnfilteredStashEntries()
}
currentStashEntry = stashEntryFromLine(lines[i], idx)
for i+1 < len(lines) && !isAStash(lines[i+1]) {
i++
if lines[i] == filterPath {
stashEntries = append(stashEntries, currentStashEntry)
continue outer
}
}
}
return stashEntries
}
func stashEntryFromLine(line string, index int) *StashEntry {
return &StashEntry{
Name: line,
Index: index,
DisplayString: line,
Name: line,
Index: index,
}
}
// 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]
@@ -226,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
}
@@ -237,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
}
}
}
@@ -311,39 +377,64 @@ 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"
})
}
// ResetToCommit reset to commit
func (c *GitCommand) ResetToCommit(sha string, strength string) error {
return c.OSCommand.RunCommand("git reset --%s %s", strength, sha)
func (c *GitCommand) ResetToCommit(sha string, strength string, options RunCommandOptions) error {
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git reset --%s %s", strength, sha), options)
}
// 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 is a function.
func (c *GitCommand) CurrentBranchName() (string, error) {
// CurrentBranchName get the current branch name and displayname.
// the first returned string is the name and the second is the displayname
// e.g. name is 123asdf and displayname is '(HEAD detached at 123asdf)'
func (c *GitCommand) CurrentBranchName() (string, string, error) {
branchName, err := c.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD")
if err != nil || branchName == "HEAD\n" {
output, err := c.OSCommand.RunCommandWithOutput("git branch --contains")
if err != nil {
return "", err
}
re := regexp.MustCompile(CurrentBranchNameRegex)
match := re.FindStringSubmatch(output)
branchName = match[1]
if err == nil && branchName != "HEAD\n" {
trimmedBranchName := strings.TrimSpace(branchName)
return trimmedBranchName, trimmedBranchName, nil
}
return strings.TrimSpace(branchName), nil
output, err := c.OSCommand.RunCommandWithOutput("git branch --contains")
if err != nil {
return "", "", err
}
for _, line := range utils.SplitLines(output) {
re := regexp.MustCompile(CurrentBranchNameRegex)
match := re.FindStringSubmatch(line)
if len(match) > 0 {
branchName = match[1]
displayBranchName := match[0][2:]
return branchName, displayBranchName, nil
}
}
return "HEAD", "HEAD", nil
}
// DeleteBranch delete branch
@@ -362,9 +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 {
return c.OSCommand.RunCommand("git merge --no-edit %s", branchName)
func (c *GitCommand) Merge(branchName string, opts MergeOpts) error {
mergeArgs := c.Config.GetUserConfig().GetString("git.merging.args")
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
@@ -375,6 +477,11 @@ func (c *GitCommand) AbortMerge() error {
// usingGpg tells us whether the user has gpg enabled so that we can know
// whether we need to run a subprocess to allow them to enter their password
func (c *GitCommand) usingGpg() bool {
overrideGpg := c.Config.GetUserConfig().GetBool("git.overrideGpg")
if overrideGpg {
return false
}
gpgsign, _ := c.getLocalGitConfig("commit.gpgsign")
if gpgsign == "" {
gpgsign, _ = c.getGlobalGitConfig("commit.gpgsign")
@@ -386,36 +493,40 @@ 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.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command), nil
return c.OSCommand.ShellCommandFromString(command), nil
}
return nil, c.OSCommand.RunCommand(command)
}
// Get the subject of the HEAD commit
func (c *GitCommand) GetHeadCommitMessage() (string, error) {
cmdStr := "git log -1 --pretty=%s"
message, err := c.OSCommand.RunCommandWithOutput(cmdStr)
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.PrepareSubProcess(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"
@@ -427,17 +538,19 @@ 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
func (c *GitCommand) CatFile(fileName string) (string, error) {
return c.OSCommand.RunCommandWithOutput("cat %s", c.OSCommand.Quote(fileName))
return c.OSCommand.RunCommandWithOutput("%s %s", c.OSCommand.Platform.catCmd, c.OSCommand.Quote(fileName))
}
// 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
@@ -468,17 +581,21 @@ 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
func (c *GitCommand) IsInMergeState() (bool, error) {
output, err := c.OSCommand.RunCommandWithOutput("git status --untracked-files=all")
if err != nil {
return false, err
}
return strings.Contains(output, "conclude merge") || strings.Contains(output, "unmerged paths"), nil
return c.OSCommand.FileExists(fmt.Sprintf("%s/MERGE_HEAD", c.DotGitDir))
}
// RebaseMode returns "" for non-rebase mode, "normal" for normal rebase
@@ -499,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 {
@@ -522,12 +692,17 @@ func (c *GitCommand) DiscardUnstagedFileChanges(file *File) error {
}
// Checkout checks out a branch (or commit), with --force if you set the force arg to true
func (c *GitCommand) Checkout(branch string, force bool) error {
type CheckoutOptions struct {
Force bool
EnvVars []string
}
func (c *GitCommand) Checkout(branch string, options CheckoutOptions) error {
forceArg := ""
if force {
if options.Force {
forceArg = "--force "
}
return c.OSCommand.RunCommand("git checkout %s %s", forceArg, branch)
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git checkout %s %s", forceArg, branch), RunCommandOptions{EnvVars: options.EnvVars})
}
// PrepareCommitSubProcess prepares a subprocess for `git commit`
@@ -558,12 +733,20 @@ func (c *GitCommand) Ignore(filename string) error {
return c.OSCommand.AppendLineToFile(".gitignore", filename)
}
func (c *GitCommand) ShowCmdStr(sha string) string {
return fmt.Sprintf("git show --color=%s --no-renames --stat -p %s", c.colorArg(), sha)
func (c *GitCommand) ShowCmdStr(sha string, filterPath string) string {
filterPathArg := ""
if filterPath != "" {
filterPathArg = fmt.Sprintf(" -- %s", c.OSCommand.Quote(filterPath))
}
return fmt.Sprintf("git show --color=%s --no-renames --stat -p %s %s", c.colorArg(), sha, filterPathArg)
}
func (c *GitCommand) GetBranchGraphCmdStr(branchName string) string {
return fmt.Sprintf("git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium %s --", branchName)
branchLogCmdTemplate := c.Config.GetUserConfig().GetString("git.branchLogCmd")
templateValues := map[string]string{
"branchName": branchName,
}
return utils.ResolvePlaceholderString(branchLogCmdTemplate, templateValues)
}
// GetRemoteURL returns current repo remote url
@@ -582,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()
@@ -623,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 {
@@ -633,6 +817,7 @@ func (c *GitCommand) RunSkipEditorCommand(command string) error {
cmd.Env = append(
cmd.Env,
"LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY",
"GIT_EDITOR="+lazyGitPath,
"EDITOR="+lazyGitPath,
"VISUAL="+lazyGitPath,
)
@@ -650,7 +835,10 @@ func (c *GitCommand) GenericMerge(commandType string, command string) error {
),
)
if err != nil {
return err
if !strings.Contains(err.Error(), "no rebase in progress") {
return err
}
c.Log.Warn(err)
}
// sometimes we need to do a sequence of things in a rebase but the user needs to
@@ -722,7 +910,8 @@ func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string
debug = "TRUE"
}
splitCmd := str.ToArgv(fmt.Sprintf("git rebase --interactive --autostash --keep-empty --rebase-merges %s", baseSha))
cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty %s", baseSha)
splitCmd := str.ToArgv(cmdStr)
cmd := c.OSCommand.command(splitCmd[0], splitCmd[1:]...)
@@ -743,7 +932,7 @@ func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string
)
if overrideEditor {
cmd.Env = append(cmd.Env, "EDITOR="+ex)
cmd.Env = append(cmd.Env, "GIT_EDITOR="+ex)
}
return cmd, nil
@@ -774,11 +963,18 @@ func (c *GitCommand) GenerateGenericRebaseTodo(commits []*Commit, actionIndex in
todo := ""
for i, commit := range commits[0:baseIndex] {
a := "pick"
var commitAction string
if i == actionIndex {
a = action
commitAction = action
} else if commit.IsMerge {
// your typical interactive rebase will actually drop merge commits by default. Damn git CLI, you scary!
// doing this means we don't need to worry about rebasing over merges which always causes problems.
// you typically shouldn't be doing rebases that pass over merge commits anyway.
commitAction = "drop"
} else {
commitAction = "pick"
}
todo = a + " " + commit.Sha + " " + commit.Name + "\n" + todo
todo = commitAction + " " + commit.Sha + " " + commit.Name + "\n" + todo
}
return todo, commits[baseIndex].Sha, nil
@@ -864,45 +1060,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 show --pretty= --name-only --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
@@ -966,11 +1184,6 @@ func (c *GitCommand) ResetSoft(ref string) error {
return c.OSCommand.RunCommand("git reset --soft " + ref)
}
// DiffCommits show diff between commits
func (c *GitCommand) DiffCommits(sha1, sha2 string) (string, error) {
return c.OSCommand.RunCommandWithOutput("git diff --color=%s --stat -p %s %s", c.colorArg(), sha1, sha2)
}
// CreateFixupCommit creates a commit that fixes up a previous commit
func (c *GitCommand) CreateFixupCommit(sha string) error {
return c.OSCommand.RunCommand("git commit --fixup=%s", sha)
@@ -1013,7 +1226,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 {
@@ -1105,29 +1318,49 @@ func (c *GitCommand) FetchRemote(remoteName string) error {
return c.OSCommand.RunCommand("git fetch %s", remoteName)
}
func (c *GitCommand) GetReflogCommits() ([]*Commit, error) {
output, err := c.OSCommand.RunCommandWithOutput("git reflog --abbrev=20")
if err != nil {
return nil, err
// GetReflogCommits only returns the new reflog commits since the given lastReflogCommit
// if none is passed (i.e. it's value is nil) then we get all the reflog commits
func (c *GitCommand) GetReflogCommits(lastReflogCommit *Commit, filterPath string) ([]*Commit, bool, error) {
commits := make([]*Commit, 0)
re := regexp.MustCompile(`(\w+).*HEAD@\{([^\}]+)\}: (.*)`)
filterPathArg := ""
if filterPath != "" {
filterPathArg = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(filterPath))
}
lines := strings.Split(strings.TrimSpace(output), "\n")
commits := make([]*Commit, len(lines))
re := regexp.MustCompile(`(\w+).*HEAD@\{\d+\}: (.*)`)
for i, line := range lines {
cmd := c.OSCommand.ExecutableFromString(fmt.Sprintf("git reflog --abbrev=20 --date=unix %s", filterPathArg))
onlyObtainedNewReflogCommits := false
err := RunLineOutputCmd(cmd, func(line string) (bool, error) {
match := re.FindStringSubmatch(line)
if len(match) <= 1 {
continue
return false, nil
}
commits[i] = &Commit{
Sha: match[1],
Name: match[2],
Status: "reflog",
unixTimestamp, _ := strconv.Atoi(match[2])
commit := &Commit{
Sha: match[1],
Name: match[3],
UnixTimestamp: int64(unixTimestamp),
Status: "reflog",
}
if lastReflogCommit != nil && commit.Sha == lastReflogCommit.Sha && commit.UnixTimestamp == lastReflogCommit.UnixTimestamp {
onlyObtainedNewReflogCommits = true
// after this point we already have these reflogs loaded so we'll simply return the new ones
return true, nil
}
commits = append(commits, commit)
return false, nil
})
if err != nil {
return nil, false, err
}
return commits, nil
return commits, onlyObtainedNewReflogCommits, nil
}
func (c *GitCommand) ConfiguredPager() string {
@@ -1167,3 +1400,15 @@ func (c *GitCommand) colorArg() string {
func (c *GitCommand) RenameBranch(oldName string, newName string) error {
return c.OSCommand.RunCommand("git branch --move %s %s", oldName, newName)
}
func (c *GitCommand) WorkingTreeState() string {
rebaseMode, _ := c.RebaseMode()
if rebaseMode != "" {
return "rebasing"
}
merging, _ := c.IsInMergeState()
if merging {
return "merging"
}
return "normal"
}
+64 -174
View File
@@ -6,14 +6,15 @@ import (
"os"
"os/exec"
"regexp"
"runtime"
"testing"
"time"
"github.com/go-errors/errors"
gogit "github.com/go-git/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/test"
"github.com/stretchr/testify/assert"
gogit "gopkg.in/src-d/go-git.v4"
)
type fileInfoMock struct {
@@ -294,12 +295,10 @@ func TestGitCommandGetStashEntries(t *testing.T) {
{
0,
"WIP on add-pkg-commands-test: 55c6af2 increase parallel build",
"WIP on add-pkg-commands-test: 55c6af2 increase parallel build",
},
{
1,
"WIP on master: bb86a3f update github template",
"WIP on master: bb86a3f update github template",
},
}
@@ -314,7 +313,7 @@ func TestGitCommandGetStashEntries(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = s.command
s.test(gitCmd.GetStashEntries())
s.test(gitCmd.GetStashEntries(""))
})
}
}
@@ -421,7 +420,7 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = s.command
s.test(gitCmd.GetStatusFiles())
s.test(gitCmd.GetStatusFiles(GetStatusFileOptions{}))
})
}
}
@@ -542,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))
})
}
}
@@ -628,7 +627,7 @@ func TestGitCommandResetToCommit(t *testing.T) {
return exec.Command("echo")
}
assert.NoError(t, gitCmd.ResetToCommit("78976bc", "hard"))
assert.NoError(t, gitCmd.ResetToCommit("78976bc", "hard", RunCommandOptions{}))
}
// TestGitCommandNewBranch is a function.
@@ -704,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.
@@ -816,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")
},
@@ -1024,11 +1023,18 @@ func TestGitCommandPush(t *testing.T) {
}
}
// TestGitCommandCatFile is a function.
// TestGitCommandCatFile tests emitting a file using commands, where commands vary by OS.
func TestGitCommandCatFile(t *testing.T) {
var osCmd string
switch os := runtime.GOOS; os {
case "windows":
osCmd = "type"
default:
osCmd = "cat"
}
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "cat", cmd)
assert.EqualValues(t, osCmd, cmd)
assert.EqualValues(t, []string{"test.txt"}, args)
return exec.Command("echo", "-n", "test")
@@ -1099,75 +1105,6 @@ func TestGitCommandUnstageFile(t *testing.T) {
}
}
// TestGitCommandIsInMergeState is a function.
func TestGitCommandIsInMergeState(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(bool, error)
}
scenarios := []scenario{
{
"An error occurred when running status command",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"status", "--untracked-files=all"}, args)
return exec.Command("test")
},
func(isInMergeState bool, err error) {
assert.Error(t, err)
assert.False(t, isInMergeState)
},
},
{
"Is not in merge state",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"status", "--untracked-files=all"}, args)
return exec.Command("echo")
},
func(isInMergeState bool, err error) {
assert.False(t, isInMergeState)
assert.NoError(t, err)
},
},
{
"Command output contains conclude merge",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"status", "--untracked-files=all"}, args)
return exec.Command("echo", "'conclude merge'")
},
func(isInMergeState bool, err error) {
assert.True(t, isInMergeState)
assert.NoError(t, err)
},
},
{
"Command output contains unmerged paths",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"status", "--untracked-files=all"}, args)
return exec.Command("echo", "'unmerged paths'")
},
func(isInMergeState bool, err error) {
assert.True(t, isInMergeState)
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = s.command
s.test(gitCmd.IsInMergeState())
})
}
}
// TestGitCommandDiscardAllFileChanges is a function.
func TestGitCommandDiscardAllFileChanges(t *testing.T) {
type scenario struct {
@@ -1439,7 +1376,7 @@ func TestGitCommandCheckout(t *testing.T) {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = s.command
s.test(gitCmd.Checkout("test", s.force))
s.test(gitCmd.Checkout("test", CheckoutOptions{Force: s.force}))
})
}
}
@@ -1450,10 +1387,8 @@ func TestGitCommandGetBranchGraph(t *testing.T) {
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"log", "--graph", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium", "test", "--"}, args)
return exec.Command("echo")
}
_, err := gitCmd.GetBranchGraph("test")
assert.NoError(t, err)
}
@@ -1473,7 +1408,7 @@ func TestGitCommandDiff(t *testing.T) {
"Default case",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"diff", "--color=", "--", "test.txt"}, args)
assert.EqualValues(t, []string{"diff", "--color=always", "--", "test.txt"}, args)
return exec.Command("echo")
},
@@ -1489,7 +1424,7 @@ func TestGitCommandDiff(t *testing.T) {
"cached",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"diff", "--color=", "--cached", "--", "test.txt"}, args)
assert.EqualValues(t, []string{"diff", "--color=always", "--cached", "--", "test.txt"}, args)
return exec.Command("echo")
},
@@ -1521,7 +1456,7 @@ func TestGitCommandDiff(t *testing.T) {
"File not tracked and file has no staged changes",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"diff", "--color=", "--no-index", "/dev/null", "test.txt"}, args)
assert.EqualValues(t, []string{"diff", "--color=always", "--no-index", "/dev/null", "test.txt"}, args)
return exec.Command("echo")
},
@@ -1539,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)
})
}
}
@@ -1549,7 +1484,7 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(string, error)
test func(string, string, error)
}
scenarios := []scenario{
@@ -1559,9 +1494,10 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
assert.Equal(t, "git", cmd)
return exec.Command("echo", "master")
},
func(output string, err error) {
func(name string, displayname string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "master", output)
assert.EqualValues(t, "master", name)
assert.EqualValues(t, "master", displayname)
},
},
{
@@ -1580,9 +1516,32 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
return nil
},
func(output string, err error) {
func(name string, displayname string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "master", output)
assert.EqualValues(t, "master", name)
assert.EqualValues(t, "master", displayname)
},
},
{
"handles a detached head",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("test")
case "branch":
assert.EqualValues(t, []string{"branch", "--contains"}, args)
return exec.Command("echo", "* (HEAD detached at 123abcd)")
}
return nil
},
func(name string, displayname string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "123abcd", name)
assert.EqualValues(t, "(HEAD detached at 123abcd)", displayname)
},
},
{
@@ -1591,9 +1550,10 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
assert.Equal(t, "git", cmd)
return exec.Command("test")
},
func(output string, err error) {
func(name string, displayname string, err error) {
assert.Error(t, err)
assert.EqualValues(t, "", output)
assert.EqualValues(t, "", name)
assert.EqualValues(t, "", displayname)
},
},
}
@@ -1679,7 +1639,7 @@ func TestGitCommandRebaseBranch(t *testing.T) {
"master",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: "git rebase --interactive --autostash --keep-empty --rebase-merges master",
Expect: "git rebase --interactive --autostash --keep-empty master",
Replace: "echo",
},
}),
@@ -1692,7 +1652,7 @@ func TestGitCommandRebaseBranch(t *testing.T) {
"master",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: "git rebase --interactive --autostash --keep-empty --rebase-merges master",
Expect: "git rebase --interactive --autostash --keep-empty master",
Replace: "test",
},
}),
@@ -1815,7 +1775,7 @@ func TestGitCommandDiscardOldFileChanges(t *testing.T) {
"test999.txt",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: "git rebase --interactive --autostash --keep-empty --rebase-merges abcdef",
Expect: "git rebase --interactive --autostash --keep-empty abcdef",
Replace: "echo",
},
{
@@ -1854,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 show --pretty= --name-only --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 {
@@ -2122,6 +2005,13 @@ func TestGitCommandSkipEditorCommand(t *testing.T) {
"expected EDITOR to be set for a non-interactive external command",
)
test.AssertContainsMatch(
t,
cmd.Env,
regexp.MustCompile("^GIT_EDITOR="),
"expected GIT_EDITOR to be set for a non-interactive external command",
)
test.AssertContainsMatch(
t,
cmd.Env,
+77 -8
View File
@@ -1,17 +1,20 @@
package commands
import (
"bufio"
"fmt"
"io/ioutil"
"os"
"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"
@@ -22,6 +25,7 @@ import (
// Platform stores the os state
type Platform struct {
os string
catCmd string
shell string
shellArg string
escapedQuote string
@@ -64,6 +68,22 @@ func (c *OSCommand) SetBeforeExecuteCmd(cmd func(*exec.Cmd)) {
c.beforeExecuteCmd = cmd
}
type RunCommandOptions struct {
EnvVars []string
}
func (c *OSCommand) RunCommandWithOutputWithOptions(command string, options RunCommandOptions) (string, error) {
c.Log.WithField("command", command).Info("RunCommand")
cmd := c.ExecutableFromString(command)
cmd.Env = append(cmd.Env, options.EnvVars...)
return sanitisedCommandOutput(cmd.CombinedOutput())
}
func (c *OSCommand) RunCommandWithOptions(command string, options RunCommandOptions) error {
_, err := c.RunCommandWithOutputWithOptions(command, options)
return err
}
// RunCommandWithOutput wrapper around commands returning their output and error
// NOTE: If you don't pass any formatArgs we'll just use the command directly,
// however there's a bizarre compiler error/warning when you pass in a formatString
@@ -100,28 +120,43 @@ 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
prompts := map[string]string{
"password": `Password\s*for\s*'.+':`,
"username": `Username\s*for\s*'.+':`,
`.+'s password:`: "password",
`Password\s*for\s*'.+':`: "password",
`Username\s*for\s*'.+':`: "username",
}
for askFor, pattern := range prompts {
for pattern, askFor := range prompts {
if match, _ := regexp.MatchString(pattern, ttyText); match {
ttyText = ""
return ask(askFor)
return promptUserForCredential(askFor)
}
}
@@ -215,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
@@ -409,3 +446,35 @@ func Kill(cmd *exec.Cmd) error {
}
return cmd.Process.Kill()
}
func RunLineOutputCmd(cmd *exec.Cmd, onLine func(line string) (bool, error)) error {
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
return err
}
scanner := bufio.NewScanner(stdoutPipe)
scanner.Split(bufio.ScanLines)
if err := cmd.Start(); err != nil {
return err
}
for scanner.Scan() {
line := scanner.Text()
stop, err := onLine(line)
if err != nil {
return err
}
if stop {
cmd.Process.Kill()
break
}
}
cmd.Wait()
return nil
}
func (c *OSCommand) CopyToClipboard(str string) error {
return clipboard.WriteAll(str)
}
+1
View File
@@ -9,6 +9,7 @@ import (
func getPlatform() *Platform {
return &Platform{
os: runtime.GOOS,
catCmd: "cat",
shell: "bash",
shellArg: "-c",
escapedQuote: "'",
+1
View File
@@ -3,6 +3,7 @@ package commands
func getPlatform() *Platform {
return &Platform{
os: "windows",
catCmd: "type",
shell: "cmd",
shellArg: "/c",
escapedQuote: `\"`,
+142
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
}
@@ -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
}
+158
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
}
@@ -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)
}
})
}
}
@@ -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]
}
@@ -120,7 +120,7 @@ func coloredString(colorAttr color.Attribute, str string, selected bool, include
var cl *color.Color
attributes := []color.Attribute{colorAttr}
if selected {
attributes = append(attributes, theme.SelectedLineBgColor)
attributes = append(attributes, theme.SelectedRangeBgColor)
}
cl = color.New(attributes...)
var clIncluded *color.Color
-260
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)
}
+69 -8
View File
@@ -1,9 +1,14 @@
package commands
import "github.com/go-errors/errors"
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
}
@@ -30,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
@@ -131,14 +136,22 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*Commit, sourceCommitId
return c.GenericMerge("rebase", "continue")
}
func (c *GitCommand) PullPatchIntoIndex(commits []*Commit, commitIdx int, p *PatchManager) 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
}
}
if err := c.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil {
return err
}
if err := p.ApplyPatches(true); err != nil {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
if c.WorkingTreeState() == "rebasing" {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
}
}
return err
}
@@ -155,15 +168,63 @@ func (c *GitCommand) PullPatchIntoIndex(commits []*Commit, commitIdx int, p *Pat
c.onSuccessfulContinue = func() error {
// add patches to index
if err := p.ApplyPatches(false); err != nil {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
if c.WorkingTreeState() == "rebasing" {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
}
}
return err
}
if stash {
if err := c.StashDo(0, "apply"); err != nil {
return err
}
}
c.PatchManager.Reset()
return nil
}
return c.GenericMerge("rebase", "continue")
}
func (c *GitCommand) PullPatchIntoNewCommit(commits []*Commit, commitIdx int, p *patch.PatchManager) error {
if err := c.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil {
return err
}
if err := p.ApplyPatches(true); err != nil {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
}
return err
}
// amend the commit
if _, err := c.AmendHead(); err != nil {
return err
}
// add patches to index
if err := p.ApplyPatches(false); err != nil {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
}
return err
}
head_message, _ := c.GetHeadCommitMessage()
new_message := fmt.Sprintf("Split from \"%s\"", head_message)
_, err := c.Commit(new_message, "")
if err != nil {
return err
}
if c.onSuccessfulContinue != nil {
return errors.New("You are midway through another rebase operation. Please abort to start again")
}
c.PatchManager.Reset()
return c.GenericMerge("rebase", "continue")
}
+8 -12
View File
@@ -1,24 +1,20 @@
package commands
import (
"fmt"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// Remote : A git remote
type Remote struct {
Name string
Urls []string
Selected bool
Branches []*RemoteBranch
}
// GetDisplayStrings returns the display string of a remote
func (r *Remote) GetDisplayStrings(isFocused bool) []string {
func (r *Remote) RefName() string {
return r.Name
}
branchCount := len(r.Branches)
func (r *Remote) ID() string {
return r.RefName()
}
return []string{r.Name, utils.ColoredString(fmt.Sprintf("%d branches", branchCount), color.FgBlue)}
func (r *Remote) Description() string {
return r.RefName()
}
+16
View File
@@ -5,3 +5,19 @@ type RemoteBranch struct {
Name string
RemoteName string
}
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()
}
+14 -6
View File
@@ -1,13 +1,21 @@
package commands
import "fmt"
// StashEntry : A git stash entry
type StashEntry struct {
Index int
Name string
DisplayString string
Index int
Name string
}
// GetDisplayStrings returns the display string of branch
func (s *StashEntry) GetDisplayStrings(isFocused bool) []string {
return []string{s.DisplayString}
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
}
+12
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
}
+24 -2
View File
@@ -244,7 +244,10 @@ func GetDefaultConfig() []byte {
scrollPastBottom: true
mouseEvents: true
skipUnstageLineWarning: false
skipStashWarning: true
sidePanelWidth: 0.3333
expandFocusedSidePanel: false
mainPanelSplitMode: 'flexible' # one of 'horizontal' | 'flexible' | 'vertical'
theme:
lightTheme: false
activeBorderColor:
@@ -255,6 +258,8 @@ func GetDefaultConfig() []byte {
optionsTextColor:
- blue
selectedLineBgColor:
- default
selectedRangeBgColor:
- blue
commitLength:
show: true
@@ -264,14 +269,21 @@ git:
useConfig: false
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}} --"
overrideGpg: false # prevents lazygit from spawning a separate process when using GPG
disableForcePushing: false
update:
method: prompt # can be: prompt | background | never
days: 14 # how often a update is checked for
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
splashUpdatesIndex: 0
confirmOnQuit: false
quitOnTopLevelReturn: true
keybinding:
universal:
quit: 'q'
@@ -283,6 +295,10 @@ keybinding:
nextItem: '<down>'
prevItem-alt: 'k'
nextItem-alt: 'j'
prevPage: ','
nextPage: '.'
gotoTop: '<'
gotoBottom: '>'
prevBlock: '<left>'
nextBlock: '<right>'
prevBlock-alt: 'h'
@@ -294,6 +310,8 @@ keybinding:
optionMenu-alt1: '?'
select: '<space>'
goInto: '<enter>'
confirm: '<enter>'
confirm-alt1: 'y'
remove: 'd'
new: 'n'
edit: 'e'
@@ -314,6 +332,12 @@ keybinding:
prevTab: '['
nextScreenMode: '+'
prevScreenMode: '_'
undo: 'z'
redo: '<c-z>'
filteringMenu: <c-s>
diffingMenu: 'W'
diffingMenu-alt: '<c-e>'
copyToClipboard: '<c-o>'
status:
checkForUpdate: 'u'
recentRepos: '<enter>'
@@ -358,7 +382,6 @@ keybinding:
cherryPickCopyRange: 'C'
pasteCommits: 'v'
tagCommit: 'T'
toggleDiffCommit: 'i'
checkoutCommit: '<space>'
resetCherryPick: '<c-R>'
stash:
@@ -370,7 +393,6 @@ keybinding:
toggleDragSelect-alt: 'V'
toggleSelectHunk: 'a'
pickBothHunks: 'b'
undo: 'z'
`)
}
+2 -2
View File
@@ -66,13 +66,13 @@ func (gui *Gui) WithWaitingStatus(name string, f func() error) error {
if appStatus == "" {
return
}
gui.renderString(gui.g, "appStatus", appStatus)
gui.renderString("appStatus", appStatus)
}
}()
if err := f(); err != nil {
gui.g.Update(func(g *gocui.Gui) error {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
})
}
}()
+280
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
}
+145
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
}
+189
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))
})
}
}
+266 -250
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,92 +23,67 @@ 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),
)
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.GetBranchGraphCmdStr(branch.Name),
)
if err := gui.newCmdTask("main", cmd); err != nil {
gui.Log.Error(err)
task = gui.createRunPtyTask(cmd)
}
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(g *gocui.Gui) error {
if err := gui.refreshRemotes(); err != nil {
return err
}
if err := gui.refreshTags(); err != nil {
return err
}
g.Update(func(g *gocui.Gui) error {
builder, err := commands.NewBranchListBuilder(gui.Log, gui.GitCommand)
func (gui *Gui) refreshBranches() {
reflogCommits := gui.State.FilteredReflogCommits
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
// manually load all the reflog commits here
var err error
reflogCommits, _, err = gui.GitCommand.GetReflogCommits(nil, "")
if err != nil {
return err
}
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" {
if err := gui.renderLocalBranchesWithSelection(); err != nil {
return err
}
}
return gui.refreshStatus(g)
})
return nil
}
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.renderDisplayStrings(branchesView, displayStrings)
if gui.g.CurrentView() == branchesView {
if err := gui.handleBranchSelect(gui.g, branchesView); err != nil {
return err
gui.Log.Error(err)
}
}
return nil
builder, err := commands.NewBranchListBuilder(gui.Log, gui.GitCommand, reflogCommits)
if err != nil {
_ = gui.surfaceError(err)
}
gui.State.Branches = builder.Build()
if err := gui.postRefreshUpdate(gui.Contexts.Branches.Context); err != nil {
gui.Log.Error(err)
}
gui.refreshStatus()
}
// 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 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("AlreadyCheckedOutBranch"))
if gui.State.Panels.Branches.SelectedLineIdx == 0 {
return gui.createErrorPanel(gui.Tr.SLocalize("AlreadyCheckedOutBranch"))
}
branch := gui.getSelectedBranch()
return gui.handleCheckoutRef(branch.Name)
return gui.handleCheckoutRef(branch.Name, handleCheckoutRefOptions{})
}
func (gui *Gui) handleCreatePullRequestPress(g *gocui.Gui, v *gocui.View) error {
@@ -114,19 +91,20 @@ func (gui *Gui) handleCreatePullRequestPress(g *gocui.Gui, v *gocui.View) error
branch := gui.getSelectedBranch()
if err := pullRequest.Create(branch); err != nil {
return gui.createErrorPanel(g, err.Error())
return gui.surfaceError(err)
}
return nil
}
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
}
@@ -135,54 +113,99 @@ 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, true); err != nil {
_ = gui.createErrorPanel(g, err.Error())
}
return gui.refreshSidePanels(g)
}, 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})
},
})
}
func (gui *Gui) handleCheckoutRef(ref string) error {
if err := gui.GitCommand.Checkout(ref, false); 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
type handleCheckoutRefOptions struct {
WaitingStatus string
EnvVars []string
onRefNotFound func(ref string) error
}
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 {
if err := gui.GitCommand.StashSave(gui.Tr.SLocalize("StashPrefix") + ref); err != nil {
return gui.createErrorPanel(g, err.Error())
}
if err := gui.GitCommand.Checkout(ref, false); err != nil {
return gui.createErrorPanel(g, err.Error())
}
// checkout successful so we select the new branch
gui.State.Panels.Branches.SelectedLine = 0
if err := gui.GitCommand.StashDo(0, "pop"); err != nil {
if err := gui.refreshSidePanels(g); err != nil {
return err
}
return gui.createErrorPanel(g, err.Error())
}
return gui.refreshSidePanels(g)
}, nil)
}
if err := gui.createErrorPanel(gui.g, err.Error()); err != nil {
return err
}
func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions) error {
waitingStatus := options.WaitingStatus
if waitingStatus == "" {
waitingStatus = gui.Tr.SLocalize("CheckingOutStatus")
}
gui.State.Panels.Branches.SelectedLine = 0
gui.State.Panels.Commits.SelectedLine = 0
return gui.refreshSidePanels(gui.g)
cmdOptions := commands.CheckoutOptions{Force: false, EnvVars: options.EnvVars}
onSuccess := func() {
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
}
return gui.WithWaitingStatus(waitingStatus, func() error {
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.ask(askOpts{
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)
}
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 {
return err
}
}
onSuccess()
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI})
})
}
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))
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)
},
})
},
})
})
}
@@ -194,45 +217,37 @@ 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.createErrorPanel(g, err.Error())
}
if err := gui.refreshSidePanels(g); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return gui.handleBranchSelect(g, v)
})
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
}
checkedOutBranch := gui.getCheckedOutBranch()
if checkedOutBranch.Name == selectedBranch.Name {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantDeleteCheckOutBranch"))
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 {
@@ -246,25 +261,35 @@ 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(g, errMessage)
}
return gui.refreshSidePanels(g)
}, nil)
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{BRANCHES}})
},
})
}
func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
if gui.GitCommand.IsHeadDetached() {
return gui.createErrorPanel(gui.g, "Cannot merge branch in detached head state. You might have checked out a commit directly or a remote branch, in which case you should checkout the local branch you want to be on")
return gui.createErrorPanel("Cannot merge branch in detached head state. You might have checked out a commit directly or a remote branch, in which case you should checkout the local branch you want to be on")
}
checkedOutBranchName := gui.getCheckedOutBranch().Name
if checkedOutBranchName == branchName {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("CantMergeBranchIntoItself"))
return gui.createErrorPanel(gui.Tr.SLocalize("CantMergeBranchIntoItself"))
}
prompt := gui.Tr.TemplateLocalize(
"ConfirmMerge",
@@ -273,15 +298,23 @@ 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 {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
selectedBranchName := gui.getSelectedBranch().Name
return gui.mergeBranchIntoCheckedOutBranch(selectedBranchName)
}
@@ -292,9 +325,13 @@ func (gui *Gui) handleRebaseOntoLocalBranch(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleRebaseOntoBranch(selectedBranchName string) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
checkedOutBranch := gui.getCheckedOutBranch().Name
if selectedBranchName == checkedOutBranch {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("CantRebaseOntoSelf"))
return gui.createErrorPanel(gui.Tr.SLocalize("CantRebaseOntoSelf"))
}
prompt := gui.Tr.TemplateLocalize(
"ConfirmRebase",
@@ -303,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 {
@@ -319,15 +361,15 @@ func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
return nil
}
if branch.Pushables == "?" {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("FwdNoUpstream"))
return gui.createErrorPanel(gui.Tr.SLocalize("FwdNoUpstream"))
}
if branch.Pushables != "0" {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("FwdCommitsToPush"))
return gui.createErrorPanel(gui.Tr.SLocalize("FwdCommitsToPush"))
}
upstream, err := gui.GitCommand.GetUpstreamForBranch(branch.Name)
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
split := strings.Split(upstream, "/")
@@ -342,81 +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.createErrorPanel(gui.g, err.Error())
}
_ = gui.refreshSidePanels(gui.g)
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.createErrorPanel(gui.g, err.Error())
}
_ = gui.refreshBranches(gui.g)
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 {
@@ -426,41 +406,27 @@ 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 {
return nil
}
// TODO: find a way to not checkout the branch here if it's not the current branch (i.e. find some
// 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.createErrorPanel(gui.g, err.Error())
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, false); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
if err := gui.GitCommand.Checkout(newBranchName, commands.CheckoutOptions{Force: false}); err != nil {
return gui.surfaceError(err)
}
return gui.refreshBranches(gui.g)
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
})
}
@@ -471,11 +437,61 @@ 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 {
if len(gui.State.Branches) == 0 {
return nil
}
return gui.State.Branches[0]
}
func (gui *Gui) handleNewBranchOffCurrentItem() error {
context := gui.currentSideContext()
item, ok := context.GetSelectedItem()
if !ok {
return nil
}
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
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
}
+111 -113
View File
@@ -1,73 +1,55 @@
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(g *gocui.Gui) *commands.CommitFile {
selectedLine := gui.State.Panels.CommitFiles.SelectedLine
if selectedLine == -1 {
func (gui *Gui) getSelectedCommitFile() *commands.CommitFile {
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
func (gui *Gui) handleCommitFileSelect() error {
gui.handleEscapeLineByLinePanel()
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()
}
commitFile := gui.getSelectedCommitFile(g)
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]
if err := gui.GitCommand.CheckoutFile(file.Sha, file.Name); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
file := gui.getSelectedCommitFile()
if file == nil {
return nil
}
return gui.refreshFiles()
if err := gui.GitCommand.CheckoutFile(file.Parent, file.Name); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}
func (gui *Gui) handleDiscardOldFileChange(g *gocui.Gui, v *gocui.View) error {
@@ -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(gui.g)
})
}, 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(gui.g)
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.createErrorPanel(gui.g, err.Error())
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.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(g)
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
}
commitFile := gui.getSelectedCommitFile(g)
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(gui.g)
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,41 +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(gui.g)
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)
}, 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"),
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)
}
+10 -17
View File
@@ -15,7 +15,7 @@ import (
func (gui *Gui) runSyncOrAsyncCommand(sub *exec.Cmd, err error) (bool, error) {
if err != nil {
if err != gui.Errors.ErrSubProcess {
return false, gui.createErrorPanel(gui.g, err.Error())
return false, gui.surfaceError(err)
}
}
if sub != nil {
@@ -28,7 +28,7 @@ func (gui *Gui) runSyncOrAsyncCommand(sub *exec.Cmd, err error) (bool, error) {
func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
message := gui.trimmedContent(v)
if message == "" {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CommitWithoutMessageErr"))
return gui.createErrorPanel(gui.Tr.SLocalize("CommitWithoutMessageErr"))
}
flags := ""
skipHookPrefix := gui.Config.GetUserConfig().GetString("git.skipHookPrefix")
@@ -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())
return gui.refreshSidePanels(g)
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
}
+286 -359
View File
@@ -1,18 +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(g *gocui.Gui) *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
}
@@ -20,106 +18,130 @@ func (gui *Gui) getSelectedCommit(g *gocui.Gui) *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 > 20 && state.LimitCommits {
if state.SelectedLineIdx > 290 && state.LimitCommits {
state.LimitCommits = false
go func() {
if err := gui.refreshCommitsWithLimit(); err != nil {
_ = gui.createErrorPanel(gui.g, err.Error())
_ = gui.surfaceError(err)
}
}()
}
gui.getMainView().Title = "Patch"
gui.getSecondaryView().Title = "Custom Patch"
gui.handleEscapeLineByLinePanel()
commit := gui.getSelectedCommit(g)
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 specific diff mode is on, don't show diff
if gui.State.Panels.Commits.SpecificDiffMode {
return nil
}
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowCmdStr(commit.Sha),
)
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(),
})
}
func (gui *Gui) refreshCommits(g *gocui.Gui) error {
g.Update(func(*gocui.Gui) error {
// I think this is here for the sake of some kind of rebasing thing
_ = gui.refreshStatus(g)
if err := gui.refreshCommitsWithLimit(); err != nil {
return err
}
// doing this async because it shouldn't hold anything up
// during startup, the bottleneck is fetching the reflog entries. We need these
// on startup to sort the branches by recency. So we have two phases: INITIAL, and COMPLETE.
// In the initial phase we don't get any reflog commits, but we asynchronously get them
// and refresh the branches after that
func (gui *Gui) refreshReflogCommitsConsideringStartup() {
switch gui.State.StartupStage {
case INITIAL:
go func() {
if err := gui.refreshReflogCommits(); err != nil {
_ = gui.createErrorPanel(gui.g, err.Error())
}
_ = gui.refreshReflogCommits()
gui.refreshBranches()
gui.State.StartupStage = COMPLETE
}()
if g.CurrentView() == gui.getCommitFilesView() || (g.CurrentView() == gui.getMainView() || gui.State.MainContext == "patch-building") {
return gui.refreshCommitFilesView()
case COMPLETE:
_ = gui.refreshReflogCommits()
}
}
// whenever we change commits, we should update branches because the upstream/downstream
// counts can change. Whenever we change branches we should probably also change commits
// e.g. in the case of switching branches.
func (gui *Gui) refreshCommits() error {
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
gui.refreshReflogCommitsConsideringStartup()
gui.refreshBranches()
wg.Done()
}()
go func() {
_ = gui.refreshCommitsWithLimit()
if gui.g.CurrentView() == gui.getCommitFilesView() || (gui.currentContext().GetKey() == gui.Contexts.PatchBuilding.Context.GetKey()) {
_ = gui.refreshCommitFilesView()
}
return nil
})
wg.Done()
}()
wg.Wait()
return nil
}
func (gui *Gui) refreshCommitsWithLimit() error {
builder, err := commands.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr, gui.State.CherryPickedCommits, gui.State.DiffEntries)
if err != nil {
return err
}
gui.State.BranchCommitsMutex.Lock()
defer gui.State.BranchCommitsMutex.Unlock()
commits, err := builder.GetCommits(gui.State.Panels.Commits.LimitCommits)
builder := commands.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr)
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 gui.postRefreshUpdate(gui.Contexts.BranchCommits.Context)
}
return nil
func (gui *Gui) refreshRebaseCommits() error {
gui.State.BranchCommitsMutex.Lock()
defer gui.State.BranchCommitsMutex.Unlock()
builder := commands.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr)
updatedCommits, err := builder.MergeRebasingCommits(gui.State.Commits)
if err != nil {
return err
}
gui.State.Commits = updatedCommits
return gui.postRefreshUpdate(gui.Contexts.BranchCommits.Context)
}
// specific functions
func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
if len(gui.State.Commits) <= 1 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash"))
return gui.createErrorPanel(gui.Tr.SLocalize("YouNoCommitsToSquash"))
}
applied, err := gui.handleMidRebaseCommand("squash")
@@ -130,17 +152,25 @@ 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 {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
if len(gui.State.Commits) <= 1 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash"))
return gui.createErrorPanel(gui.Tr.SLocalize("YouNoCommitsToSquash"))
}
applied, err := gui.handleMidRebaseCommand("fixup")
@@ -151,15 +181,23 @@ 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 {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
applied, err := gui.handleMidRebaseCommand("reword")
if err != nil {
return err
@@ -168,21 +206,34 @@ func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
return nil
}
if gui.State.Panels.Commits.SelectedLine != 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlyRenameTopCommit"))
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 {
return gui.createErrorPanel(g, err.Error())
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)
}
if err := gui.refreshCommits(g); err != nil {
panic(err)
}
return gui.handleCommitSelect(g, v)
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
})
}
func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
applied, err := gui.handleMidRebaseCommand("reword")
if err != nil {
return err
@@ -191,9 +242,9 @@ 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.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
if subProcess != nil {
gui.SubProcess = subProcess
@@ -207,7 +258,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
}
@@ -217,16 +268,21 @@ func (gui *Gui) handleMidRebaseCommand(action string) (bool, error) {
// our input or we set a lazygit client as the EDITOR env variable and have it
// request us to edit the commit message when prompted.
if action == "reword" {
return true, gui.createErrorPanel(gui.g, gui.Tr.SLocalize("rewordNotSupported"))
return true, gui.createErrorPanel(gui.Tr.SLocalize("rewordNotSupported"))
}
if err := gui.GitCommand.EditRebaseTodo(gui.State.Panels.Commits.SelectedLine, action); err != nil {
return false, gui.createErrorPanel(gui.g, err.Error())
if err := gui.GitCommand.EditRebaseTodo(gui.State.Panels.Commits.SelectedLineIdx, action); err != nil {
return false, gui.surfaceError(err)
}
return true, gui.refreshCommits(gui.g)
return true, gui.refreshRebaseCommits()
}
func (gui *Gui) handleCommitDelete(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
applied, err := gui.handleMidRebaseCommand("drop")
if err != nil {
return err
@@ -235,61 +291,77 @@ 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 {
index := gui.State.Panels.Commits.SelectedLine
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
index := gui.State.Panels.Commits.SelectedLineIdx
selectedCommit := gui.State.Commits[index]
if selectedCommit.Status == "rebasing" {
if gui.State.Commits[index+1].Status != "rebasing" {
return nil
}
if err := gui.GitCommand.MoveTodoDown(index); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
gui.State.Panels.Commits.SelectedLine++
return gui.refreshCommits(gui.g)
gui.State.Panels.Commits.SelectedLineIdx++
return gui.refreshRebaseCommits()
}
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)
})
}
func (gui *Gui) handleCommitMoveUp(g *gocui.Gui, v *gocui.View) error {
index := gui.State.Panels.Commits.SelectedLine
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
index := gui.State.Panels.Commits.SelectedLineIdx
if index == 0 {
return nil
}
selectedCommit := gui.State.Commits[index]
if selectedCommit.Status == "rebasing" {
if err := gui.GitCommand.MoveTodoDown(index - 1); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
gui.State.Panels.Commits.SelectedLine--
return gui.refreshCommits(gui.g)
gui.State.Panels.Commits.SelectedLineIdx--
return gui.refreshRebaseCommits()
}
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)
})
}
func (gui *Gui) handleCommitEdit(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
applied, err := gui.handleMidRebaseCommand("edit")
if err != nil {
return err
@@ -299,21 +371,33 @@ 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)
})
}
func (gui *Gui) handleCommitAmendTo(g *gocui.Gui, v *gocui.View) error {
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)
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
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 {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
applied, err := gui.handleMidRebaseCommand("pick")
if err != nil {
return err
@@ -328,130 +412,24 @@ func (gui *Gui) handleCommitPick(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleCommitRevert(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.Revert(gui.State.Commits[gui.State.Panels.Commits.SelectedLine].Sha); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
gui.State.Panels.Commits.SelectedLine++
return gui.refreshCommits(gui.g)
}
func (gui *Gui) handleCopyCommit(g *gocui.Gui, v *gocui.View) error {
// 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.refreshCommits(gui.g)
}
}
gui.addCommitToCherryPickedCommits(gui.State.Panels.Commits.SelectedLine)
return gui.refreshCommits(gui.g)
}
func (gui *Gui) addCommitToCherryPickedCommits(index int) {
// not super happy with modifying the state of the Commits array here
// but the alternative would be very tricky
gui.State.Commits[index].Copied = true
newCommits := []*commands.Commit{}
for _, commit := range gui.State.Commits {
if commit.Copied {
// 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 {
// whenever I add a commit, I need to make sure I retain its order
// 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 commit.Copied {
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.refreshCommits(gui.g)
}
// HandlePasteCommits begins a cherry-pick rebase with the commits the user has copied
func (gui *Gui) HandlePasteCommits(g *gocui.Gui, v *gocui.View) error {
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 {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
return gui.switchFocus(g, gui.getCommitsView(), gui.getCommitFilesView())
if err := gui.GitCommand.Revert(gui.State.Commits[gui.State.Panels.Commits.SelectedLineIdx].Sha); err != nil {
return gui.surfaceError(err)
}
gui.State.Panels.Commits.SelectedLineIdx++
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI, scope: []int{COMMITS, BRANCHES}})
}
func (gui *Gui) handleToggleDiffCommit(g *gocui.Gui, v *gocui.View) error {
selectLimit := 2
// get selected commit
commit := gui.getSelectedCommit(g)
func (gui *Gui) handleViewCommitFiles() error {
commit := gui.getSelectedLocalCommit()
if commit == nil {
return gui.newStringTask("main", gui.Tr.SLocalize("NoCommitsThisBranch"))
return nil
}
// if already selected commit delete
if idx, has := gui.hasCommit(gui.State.DiffEntries, commit.Sha); has {
gui.State.DiffEntries = gui.unchooseCommit(gui.State.DiffEntries, idx)
} else {
if len(gui.State.DiffEntries) == selectLimit {
gui.State.DiffEntries = gui.unchooseCommit(gui.State.DiffEntries, 0)
}
gui.State.DiffEntries = append(gui.State.DiffEntries, commit)
}
gui.setDiffMode()
// if selected two commits, display diff between
if len(gui.State.DiffEntries) == selectLimit {
commitText, err := gui.GitCommand.DiffCommits(gui.State.DiffEntries[0].Sha, gui.State.DiffEntries[1].Sha)
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.newStringTask("main", commitText)
}
return nil
}
func (gui *Gui) setDiffMode() {
v := gui.getCommitsView()
if len(gui.State.DiffEntries) != 0 {
gui.State.Panels.Commits.SpecificDiffMode = true
v.Title = gui.Tr.SLocalize("CommitsDiffTitle")
} else {
gui.State.Panels.Commits.SpecificDiffMode = false
v.Title = gui.Tr.SLocalize("CommitsTitle")
}
_ = gui.refreshCommits(gui.g)
return gui.switchToCommitFilesContext(commit.Sha, true, gui.Contexts.BranchCommits.Context, "commits")
}
func (gui *Gui) hasCommit(commits []*commands.Commit, target string) (int, bool) {
@@ -468,49 +446,65 @@ func (gui *Gui) unchooseCommit(commits []*commands.Commit, i int) []*commands.Co
}
func (gui *Gui) handleCreateFixupCommit(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedCommit(g)
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
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.createErrorPanel(g, err.Error())
}
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(gui.g)
}, nil)
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
},
})
}
func (gui *Gui) handleSquashAllAboveFixupCommits(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedCommit(g)
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
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(g)
commit := gui.getSelectedLocalCommit()
if commit == nil {
return nil
}
@@ -519,123 +513,43 @@ 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.createErrorPanel(g, err.Error())
return gui.prompt(gui.Tr.SLocalize("TagNameTitle"), "", func(response string) error {
if err := gui.GitCommand.CreateLightweightTag(response, commitSha); err != nil {
return gui.surfaceError(err)
}
if err := gui.refreshCommits(g); err != nil {
return gui.createErrorPanel(g, err.Error())
}
if err := gui.refreshTags(); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return gui.handleCommitSelect(g, v)
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{COMMITS, TAGS}})
})
}
func (gui *Gui) handleCheckoutCommit(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedCommit(g)
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)
}, 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.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(g)
commit := gui.getSelectedLocalCommit()
if commit == nil {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NoCommitsThisBranch"))
return gui.createErrorPanel(gui.Tr.SLocalize("NoCommitsThisBranch"))
}
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 {
gui.State.Panels.Commits.LimitCommits = false
if err := gui.refreshCommits(gui.g); err != nil {
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{COMMITS}}); err != nil {
return err
}
}
@@ -643,7 +557,20 @@ 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.refreshCommits(gui.g)
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 {
gui.State.Panels.Commits.LimitCommits = false
if err := gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []int{COMMITS}}); err != nil {
return err
}
}
for _, context := range gui.getListContexts() {
if context.ViewName == "commits" {
return context.handleGotoBottom(g, v)
}
}
return nil
}
+148 -76
View File
@@ -15,32 +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.DeleteKeybindings("confirmation")
return g.DeleteView("confirmation")
gui.deleteConfirmationView()
return nil
}
func (gui *Gui) getMessageHeight(wrap bool, message string, width int) int {
@@ -57,18 +132,30 @@ 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
}
return width/2 - panelWidth/2,
height/2 - panelHeight/2 - panelHeight%2 - 1,
width/2 + panelWidth/2,
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" {
@@ -83,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)
@@ -122,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{
@@ -149,34 +211,44 @@ func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*go
"keyBindConfirm": "enter",
},
)
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
}
colorFunction := color.New(color.FgRed).SprintFunc()
coloredMessage := colorFunction(strings.TrimSpace(message))
return gui.createConfirmationPanel(gui.g, nextView, true, gui.Tr.SLocalize("Error"), coloredMessage, nil, nil)
return gui.g.SetKeybinding("confirmation", nil, gui.getKey("universal.return"), gocui.ModNone, gui.wrappedConfirmationFunction(opts.handlersManageFocus, opts.handleClose))
}
func (gui *Gui) createErrorPanel(g *gocui.Gui, message string) error {
return gui.createSpecificErrorPanel(message, g.CurrentView(), true)
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.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())
}
+710 -7
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 ""
}
+19 -47
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,70 +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
err := gui.refreshFiles()
if err != nil {
gui.clearEditorView(v)
if err := gui.returnFromContext(); err != nil {
return err
}
v.Clear()
err = v.SetCursor(0, 0)
if err != nil {
return err
}
_, err = g.SetViewOnBottom("credentials")
if err != nil {
return err
}
nextView, err := gui.g.View("confirmation")
if err != nil {
nextView = gui.getFilesView()
}
err = gui.switchFocus(g, nil, nextView)
if err != nil {
return err
}
return gui.refreshCommits(g)
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(g)
_ = gui.closeConfirmationPrompt(false)
}
}
+158
View File
@@ -0,0 +1,158 @@
package gui
import (
"fmt"
"strings"
"github.com/jesseduffield/gocui"
)
func (gui *Gui) exitDiffMode() error {
gui.State.Modes.Diffing = Diffing{}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}
func (gui *Gui) renderDiff() error {
cmd := gui.OSCommand.ExecutableFromString(
fmt.Sprintf("git diff --color %s", gui.diffStr()),
)
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.
// in the case of a branch it returns both the branch and it's upstream name,
// 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 {
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()}
}
}
func (gui *Gui) currentDiffTerminal() string {
names := gui.currentDiffTerminals()
if len(names) == 0 {
return ""
}
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.Modes.Diffing.Ref
right := gui.currentDiffTerminal()
if right != "" {
output += " " + right
}
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
}
func (gui *Gui) handleCreateDiffingMenuPanel(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
names := gui.currentDiffTerminals()
menuItems := []*menuItem{}
for _, name := range names {
name := name
menuItems = append(menuItems, []*menuItem{
{
displayString: fmt.Sprintf("%s %s", gui.Tr.SLocalize("diff"), name),
onPress: func() error {
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})
},
},
}...)
}
menuItems = append(menuItems, []*menuItem{
{
displayString: gui.Tr.SLocalize("enterRefToDiff"),
onPress: func() error {
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.State.Modes.Diffing.Active() {
menuItems = append(menuItems, []*menuItem{
{
displayString: gui.Tr.SLocalize("swapDiff"),
onPress: func() error {
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.Modes.Diffing = Diffing{}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
},
},
}...)
}
return gui.createMenu(gui.Tr.SLocalize("DiffingMenuTitle"), menuItems, createMenuOptions{showCancel: true})
}
+6 -9
View File
@@ -5,11 +5,8 @@ import (
)
func (gui *Gui) handleCreateDiscardMenu(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
}
file := gui.getSelectedFile()
if file == nil {
return nil
}
@@ -18,9 +15,9 @@ func (gui *Gui) handleCreateDiscardMenu(g *gocui.Gui, v *gocui.View) error {
displayString: gui.Tr.SLocalize("discardAllChanges"),
onPress: func() error {
if err := gui.GitCommand.DiscardAllFileChanges(file); err != nil {
return err
return gui.surfaceError(err)
}
return gui.refreshFiles()
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
},
},
}
@@ -30,10 +27,10 @@ func (gui *Gui) handleCreateDiscardMenu(g *gocui.Gui, v *gocui.View) error {
displayString: gui.Tr.SLocalize("discardUnstagedChanges"),
onPress: func() error {
if err := gui.GitCommand.DiscardUnstagedFileChanges(file); err != nil {
return err
return gui.surfaceError(err)
}
return gui.refreshFiles()
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
},
})
}
+1 -6
View File
@@ -132,12 +132,7 @@ func (gui *Gui) watchFilesForChanges() {
}
// only refresh if we're not already
if !gui.State.IsRefreshingFiles {
if err := gui.refreshFiles(); err != nil {
err = gui.createErrorPanel(gui.g, err.Error())
if err != nil {
gui.Log.Error(err)
}
}
gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
}
// watch for errors
+293 -208
View File
@@ -8,44 +8,40 @@ import (
// "strings"
"fmt"
"regexp"
"strings"
"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(g *gocui.Gui) (*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 {
file, err := gui.getSelectedFile(gui.g)
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"))
}
gui.getFilesView().FocusPoint(0, gui.State.Panels.Files.SelectedLineIdx)
gui.getFilesView().FocusPoint(0, gui.State.Panels.Files.SelectedLine)
if file.HasInlineMergeConflicts {
gui.getMainView().Title = gui.Tr.SLocalize("MergeConflictsTitle")
gui.State.SplitMainPanel = false
return gui.refreshMergePanel()
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
}
@@ -54,31 +50,31 @@ func (gui *Gui) selectFile(alreadySelected bool) error {
}
}
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")
}
if file.HasInlineMergeConflicts {
return gui.refreshMergePanel()
}
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 {
@@ -89,7 +85,7 @@ func (gui *Gui) refreshFiles() error {
gui.State.RefreshingFilesMutex.Unlock()
}()
selectedFile, _ := gui.getSelectedFile(gui.g)
selectedFile := gui.getSelectedFile()
filesView := gui.getFilesView()
if filesView == nil {
@@ -101,12 +97,13 @@ func (gui *Gui) refreshFiles() error {
}
gui.g.Update(func(g *gocui.Gui) error {
displayStrings := presentation.GetFileListDisplayStrings(gui.State.Files)
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(gui.g)
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
@@ -130,7 +127,7 @@ func (gui *Gui) stagedFiles() []*commands.File {
func (gui *Gui) trackedFiles() []*commands.File {
files := gui.State.Files
result := make([]*commands.File, 0)
result := make([]*commands.File, 0, len(files))
for _, file := range files {
if file.Tracked {
result = append(result, file)
@@ -140,10 +137,11 @@ func (gui *Gui) trackedFiles() []*commands.File {
}
func (gui *Gui) stageSelectedFile(g *gocui.Gui) error {
file, err := gui.getSelectedFile(g)
if err != nil {
return err
file := gui.getSelectedFile()
if file == nil {
return nil
}
return gui.GitCommand.StageFile(file.Name)
}
@@ -152,49 +150,43 @@ func (gui *Gui) handleEnterFile(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) enterFile(forceSecondaryFocused bool, selectedLineIdx int) error {
file, err := gui.getSelectedFile(gui.g)
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.g, gui.Tr.SLocalize("FileStagingRequirements"))
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(g)
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.createErrorPanel(gui.g, err.Error())
if err := gui.GitCommand.UnStageFile(file.Name, file.Tracked); err != nil {
return gui.surfaceError(err)
}
}
if err := gui.refreshFiles(); err != nil {
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
return err
}
@@ -210,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)
}
@@ -226,10 +214,10 @@ func (gui *Gui) handleStageAll(g *gocui.Gui, v *gocui.View) error {
err = gui.GitCommand.StageAll()
}
if err != nil {
_ = gui.createErrorPanel(g, err.Error())
_ = gui.surfaceError(err)
}
if err := gui.refreshFiles(); err != nil {
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
return err
}
@@ -237,57 +225,72 @@ 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(gui.g)
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
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
}
if err := gui.GitCommand.RemoveTrackedFiles(file.Name); err != nil {
return err
}
return gui.refreshFiles()
}, nil)
return gui.refreshSidePanels(refreshOptions{scope: []int{FILES}})
},
})
}
if err := gui.GitCommand.Ignore(file.Name); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
return gui.refreshFiles()
return gui.refreshSidePanels(refreshOptions{scope: []int{FILES}})
}
func (gui *Gui) handleWIPCommitPress(g *gocui.Gui, filesView *gocui.View) error {
skipHookPreifx := gui.Config.GetUserConfig().GetString("git.skipHookPrefix")
if skipHookPreifx == "" {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("SkipHookPrefixNotConfigured"))
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.State.WorkingTreeState == "normal" {
return gui.createErrorPanel(g, 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()
g.Update(func(g *gocui.Gui) error {
if _, err := g.SetViewOnTop("commitMessage"); err != nil {
prefixPattern := gui.Config.GetUserConfig().GetString("git.commitPrefixes." + utils.GetCurrentRepoName() + ".pattern")
prefixReplace := gui.Config.GetUserConfig().GetString("git.commitPrefixes." + utils.GetCurrentRepoName() + ".replace")
if len(prefixPattern) > 0 && len(prefixReplace) > 0 {
rgx, err := regexp.Compile(prefixPattern)
if err != nil {
return gui.createErrorPanel(fmt.Sprintf("%s: %s", gui.Tr.SLocalize("commitPrefixPatternError"), err.Error()))
}
prefix := rgx.ReplaceAllString(gui.getCheckedOutBranch().Name, prefixReplace)
gui.renderString("commitMessage", prefix)
if err := commitMessageView.SetCursor(len(prefix), 0); 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
}
@@ -297,44 +300,70 @@ 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.State.WorkingTreeState == "normal" {
return gui.createErrorPanel(g, 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(g, gui.Tr.SLocalize("NoCommitToAmend"))
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 {
return gui.WithWaitingStatus(gui.Tr.SLocalize("AmendingStatus"), 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(g)
}, 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.State.WorkingTreeState == "normal" {
return gui.createErrorPanel(g, 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
})
}
@@ -345,123 +374,176 @@ func (gui *Gui) editFile(filename string) error {
}
func (gui *Gui) handleFileEdit(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
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(g)
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
file := gui.getSelectedFile()
if file == nil {
return nil
}
return gui.openFile(file.Name)
}
func (gui *Gui) handleRefreshFiles(g *gocui.Gui, v *gocui.View) error {
return gui.refreshFiles()
return gui.refreshSidePanels(refreshOptions{scope: []int{FILES}})
}
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))
return gui.updateWorkTreeState()
}
func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) {
item, err := gui.getSelectedFile(g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
return "", err
// 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
}
}
return "", gui.newStringTask("main", gui.Tr.SLocalize("NoFilesDisplay"))
}
if item.Type != "file" {
return "", gui.newStringTask("main", 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 cat, nil
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()
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
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") {
errorMessage = fmt.Sprintf("upstream branch %s not found.\nIf you expect it to exist, you should fetch (with 'f').\nOtherwise, you should push (with 'shift+P')", upstream)
}
return gui.createErrorPanel(gui.g, errorMessage)
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") {
forcePushDisabled := gui.Config.GetUserConfig().GetBool("git.disableForcePushing")
if forcePushDisabled {
gui.createErrorPanel(gui.Tr.SLocalize("UpdatesRejectedAndForcePushDisabled"))
return
}
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()
@@ -469,46 +551,55 @@ func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error {
// see if we have this branch in our config with an upstream
conf, err := gui.GitCommand.Repo.Config()
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
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))
}
}
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), "")
})
if gui.GitCommand.PushToCurrent {
return gui.pushWithForceFlag(v, false, "", "--set-upstream")
} else {
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)
forcePushDisabled := gui.Config.GetUserConfig().GetBool("git.disableForcePushing")
if forcePushDisabled {
return gui.createErrorPanel(gui.Tr.SLocalize("ForcePushDisabled"))
}
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(g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
return gui.createErrorPanel(gui.g, err.Error())
}
func (gui *Gui) handleSwitchToMerge() error {
file := gui.getSelectedFile()
if file == nil {
return nil
}
if !file.HasInlineMergeConflicts {
return gui.createErrorPanel(g, gui.Tr.SLocalize("FileNoMergeCons"))
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 {
if err := gui.OSCommand.OpenFile(filename); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
return nil
}
@@ -523,8 +614,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
})
@@ -556,8 +646,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())
}
+21
View File
@@ -0,0 +1,21 @@
package gui
func (gui *Gui) validateNotInFilterMode() (bool, error) {
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.Modes.Filtering.Path = ""
return gui.Errors.ErrRestart
}
+62
View File
@@ -0,0 +1,62 @@
package gui
import (
"fmt"
"strings"
"github.com/jesseduffield/gocui"
)
func (gui *Gui) handleCreateFilteringMenuPanel(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
fileName := ""
switch v.Name() {
case "files":
file := gui.getSelectedFile()
if file != nil {
fileName = file.Name
}
case "commitFiles":
file := gui.getSelectedCommitFile()
if file != nil {
fileName = file.Name
}
}
menuItems := []*menuItem{}
if fileName != "" {
menuItems = append(menuItems, &menuItem{
displayString: fmt.Sprintf("%s '%s'", gui.Tr.SLocalize("filterBy"), fileName),
onPress: func() error {
gui.State.Modes.Filtering.Path = fileName
return gui.Errors.ErrRestart
},
})
}
menuItems = append(menuItems, &menuItem{
displayString: gui.Tr.SLocalize("filterPathOption"),
onPress: func() error {
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.State.Modes.Filtering.Active() {
menuItems = append(menuItems, &menuItem{
displayString: gui.Tr.SLocalize("exitFilterMode"),
onPress: func() error {
gui.State.Modes.Filtering.Path = ""
return gui.Errors.ErrRestart
},
})
}
return gui.createMenu(gui.Tr.SLocalize("FilteringMenuTitle"), menuItems, createMenuOptions{showCancel: true})
}
+7 -4
View File
@@ -28,7 +28,7 @@ func (gui *Gui) gitFlowFinishBranch(gitFlowConfig string, branchName string) err
}
if branchType == "" {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NotAGitFlowBranch"))
return gui.createErrorPanel(gui.Tr.SLocalize("NotAGitFlowBranch"))
}
subProcess := gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "finish", suffix)
@@ -45,14 +45,13 @@ func (gui *Gui) handleCreateGitFlowMenu(g *gocui.Gui, v *gocui.View) error {
// get config
gitFlowConfig, err := gui.OSCommand.RunCommandWithOutput("git config --local --get-regexp gitflow")
if err != nil {
return gui.createErrorPanel(gui.g, "You need to install git-flow and enable it in this repo to use git-flow features")
return gui.createErrorPanel("You need to install git-flow and enable it in this repo to use git-flow features")
}
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
@@ -76,6 +75,10 @@ func (gui *Gui) handleCreateGitFlowMenu(g *gocui.Gui, v *gocui.View) error {
displayString: "start hotfix",
onPress: startHandler("hotfix"),
},
{
displayString: "start bugfix",
onPress: startHandler("bugfix"),
},
{
displayString: "start release",
onPress: startHandler("release"),
+195
View File
@@ -0,0 +1,195 @@
package gui
import (
"math"
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// 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)
return gui.rerenderViewsWithScreenModeDependentContent()
}
func (gui *Gui) scrollUpView(viewName string) error {
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, err := gui.g.View(viewName)
if err != nil {
return nil
}
ox, oy := mainView.Origin()
y := oy
if !gui.Config.GetUserConfig().GetBool("gui.scrollPastBottom") {
_, sy := mainView.Size()
y += sy
}
scrollHeight := gui.Config.GetUserConfig().GetInt("gui.scrollHeight")
if y < mainView.LinesHeight() {
if err := mainView.SetOrigin(ox, oy+scrollHeight); err != nil {
return err
}
}
if manager, ok := gui.viewBufferManagerMap[viewName]; ok {
manager.ReadLines(scrollHeight)
}
return nil
}
func (gui *Gui) scrollUpMain(g *gocui.Gui, v *gocui.View) error {
if gui.canScrollMergePanel() {
gui.State.Panels.Merging.UserScrolling = true
}
return gui.scrollUpView("main")
}
func (gui *Gui) scrollDownMain(g *gocui.Gui, v *gocui.View) error {
if gui.canScrollMergePanel() {
gui.State.Panels.Merging.UserScrolling = true
}
return gui.scrollDownView("main")
}
func (gui *Gui) scrollUpSecondary(g *gocui.Gui, v *gocui.View) error {
return gui.scrollUpView("secondary")
}
func (gui *Gui) scrollDownSecondary(g *gocui.Gui, v *gocui.View) error {
return gui.scrollDownView("secondary")
}
func (gui *Gui) scrollUpConfirmationPanel(g *gocui.Gui, v *gocui.View) error {
if v.Editable {
return nil
}
return gui.scrollUpView("confirmation")
}
func (gui *Gui) scrollDownConfirmationPanel(g *gocui.Gui, v *gocui.View) error {
if v.Editable {
return nil
}
return gui.scrollDownView("confirmation")
}
func (gui *Gui) handleRefresh(g *gocui.Gui, v *gocui.View) error {
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}
func (gui *Gui) handleMouseDownMain(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
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())
}
return nil
}
func (gui *Gui) handleMouseDownSecondary(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
switch g.CurrentView().Name() {
case "files":
return gui.enterFile(true, v.SelectedLineIdx())
}
return nil
}
func (gui *Gui) handleInfoClick(g *gocui.Gui, v *gocui.View) error {
if !gui.g.Mouse {
return nil
}
cx, _ := v.Cursor()
width, _ := v.Size()
for _, mode := range gui.modeStatuses() {
if mode.isActive() {
if width-cx > len(gui.Tr.SLocalize("(reset)")) {
return nil
}
return mode.reset()
}
}
// 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(canPromptForCredentials bool) (err error) {
gui.State.FetchMutex.Lock()
defer gui.State.FetchMutex.Unlock()
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 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)
}
+296 -817
View File
File diff suppressed because it is too large Load Diff
+400 -335
View File
File diff suppressed because it is too large Load Diff
+362
View File
@@ -0,0 +1,362 @@
package gui
import (
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/theme"
)
const SEARCH_PREFIX = "search: "
const INFO_SECTION_PADDING = " "
func (gui *Gui) informationStr() string {
for _, mode := range gui.modeStatuses() {
if mode.isActive() {
return mode.description()
}
}
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()
}
}
// layout is called for every screen re-render e.g. when the screen is resized
func (gui *Gui) layout(g *gocui.Gui) error {
g.Highlight = true
width, height := g.Size()
minimumHeight := 9
minimumWidth := 10
if height < minimumHeight || width < minimumWidth {
v, err := g.SetView("limit", 0, 0, width-1, height-1, 0)
if err != nil {
if err.Error() != "unknown view" {
return err
}
v.Title = gui.Tr.SLocalize("NotEnoughSpace")
v.Wrap = true
_, _ = g.SetViewOnTop("limit")
}
return nil
}
informationStr := gui.informationStr()
appStatus := gui.statusManager.getStatusString()
viewDimensions := gui.getWindowDimensions(informationStr, appStatus)
_, _ = g.SetViewOnBottom("limit")
_ = g.DeleteView("limit")
textColor := theme.GocuiDefaultTextColor
// reading more lines into main view buffers upon resize
prevMainView, err := gui.g.View("main")
if err == nil {
_, prevMainHeight := prevMainView.Size()
newMainHeight := viewDimensions["main"].Y1 - viewDimensions["main"].Y0 - 1
heightDiff := newMainHeight - prevMainHeight
if heightDiff > 0 {
if manager, ok := gui.viewBufferManagerMap["main"]; ok {
manager.ReadLines(heightDiff)
}
if manager, ok := gui.viewBufferManagerMap["secondary"]; ok {
manager.ReadLines(heightDiff)
}
}
}
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
}
v.Title = gui.Tr.SLocalize("DiffTitle")
v.Wrap = true
v.FgColor = textColor
v.IgnoreCarriageReturns = true
}
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 = textColor
secondaryView.IgnoreCarriageReturns = true
}
hiddenViewOffset := 9999
if v, err := setViewFromDimensions("status", "status", true); err != nil {
if err.Error() != "unknown view" {
return err
}
v.Title = gui.Tr.SLocalize("StatusTitle")
v.FgColor = textColor
}
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.ContainsList = true
}
branchesView, err := setViewFromDimensions("branches", "branches", true)
if err != nil {
if err.Error() != "unknown view" {
return err
}
branchesView.Title = gui.Tr.SLocalize("BranchesTitle")
branchesView.Tabs = gui.viewTabNames("branches")
branchesView.FgColor = textColor
branchesView.ContainsList = true
}
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.ContainsList = true
}
commitsView, err := setViewFromDimensions("commits", "commits", true)
if err != nil {
if err.Error() != "unknown view" {
return err
}
commitsView.Title = gui.Tr.SLocalize("CommitsTitle")
commitsView.Tabs = gui.viewTabNames("commits")
commitsView.FgColor = textColor
commitsView.ContainsList = true
}
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.ContainsList = true
}
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 {
if err.Error() != "unknown view" {
return err
}
_, _ = g.SetViewOnBottom("commitMessage")
commitMessageView.Title = gui.Tr.SLocalize("CommitMessage")
commitMessageView.FgColor = textColor
commitMessageView.Editable = true
commitMessageView.Editor = gocui.EditorFunc(gui.commitMessageEditor)
}
}
if check, _ := g.View("credentials"); check == nil {
// doesn't matter where this view starts because it will be hidden
if credentialsView, err := g.SetView("credentials", hiddenViewOffset, hiddenViewOffset, hiddenViewOffset+10, hiddenViewOffset+10, 0); err != nil {
if err.Error() != "unknown view" {
return err
}
_, _ = g.SetViewOnBottom("credentials")
credentialsView.Title = gui.Tr.SLocalize("CredentialsUsername")
credentialsView.FgColor = textColor
credentialsView.Editable = true
}
}
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
if searchPrefixView, err := setViewFromDimensions("searchPrefix", "searchPrefix", false); err != nil {
if err.Error() != "unknown view" {
return err
}
searchPrefixView.BgColor = gocui.ColorDefault
searchPrefixView.FgColor = gocui.ColorGreen
searchPrefixView.Frame = false
gui.setViewContent(searchPrefixView, SEARCH_PREFIX)
}
if searchView, err := setViewFromDimensions("search", "search", false); err != nil {
if err.Error() != "unknown view" {
return err
}
searchView.BgColor = gocui.ColorDefault
searchView.FgColor = gocui.ColorGreen
searchView.Frame = false
searchView.Editable = true
}
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
_, _ = g.SetViewOnBottom("appStatus")
}
informationView, err := setViewFromDimensions("information", "information", false)
if err != nil {
if err.Error() != "unknown view" {
return err
}
informationView.BgColor = gocui.ColorDefault
informationView.FgColor = gocui.ColorGreen
informationView.Frame = false
gui.renderString("information", INFO_SECTION_PADDING+informationStr)
}
if gui.State.OldInformation != informationStr {
gui.setViewContent(informationView, informationStr)
gui.State.OldInformation = informationStr
}
if gui.g.CurrentView() == nil {
initialContext := gui.Contexts.Files.Context
if gui.State.Modes.Filtering.Active() {
initialContext = gui.Contexts.BranchCommits.Context
}
if err := gui.switchContext(initialContext); err != nil {
return err
}
}
type listContextState struct {
view *gocui.View
listContext *ListContext
}
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 {
listContextStates = append(listContextStates, listContextState{view: menuView, listContext: gui.menuListContext()})
}
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
listContextState.view.FocusPoint(0, listContextState.listContext.GetPanelState().GetSelectedLineIdx())
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()
if mainViewWidth != gui.State.PrevMainWidth || mainViewHeight != gui.State.PrevMainHeight {
gui.State.PrevMainWidth = mainViewWidth
gui.State.PrevMainHeight = mainViewHeight
if err := gui.onResize(); err != nil {
return err
}
}
// here is a good place log some stuff
// 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()
}
func (gui *Gui) onInitialViewsCreation() error {
gui.setInitialViewContexts()
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()
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
+54 -20
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,13 +85,13 @@ 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
}
gui.g.Update(func(*gocui.Gui) error {
gui.setViewContent(gui.g, gui.getSecondaryView(), secondaryPatchParser.Render(-1, -1, nil))
gui.setViewContent(gui.getSecondaryView(), secondaryPatchParser.Render(-1, -1, nil))
return 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)
@@ -242,7 +249,7 @@ func (gui *Gui) refreshMainView() error {
mainView.Wrap = false
gui.g.Update(func(*gocui.Gui) error {
gui.setViewContent(gui.g, gui.getMainView(), colorDiff)
gui.setViewContent(gui.getMainView(), colorDiff)
return nil
})
@@ -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
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
}
-178
View File
@@ -1,178 +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) 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.ReflogCommits) },
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
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
}
+31 -47
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,48 +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 {
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()
}
+114 -41
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) {
@@ -59,6 +65,7 @@ func (gui *Gui) coloredConflictFile(content string, conflicts []commands.Conflic
colour := color.New(colourAttr)
if hasFocus && conflictIndex < len(conflicts) && conflicts[conflictIndex] == conflict && gui.shouldHighlightLine(i, conflict, conflictTop) {
colour.Add(color.Bold)
colour.Add(theme.SelectedRangeBgColor)
}
if i == conflict.End && len(remainingConflicts) > 0 {
conflict, remainingConflicts = gui.shiftConflict(remainingConflicts)
@@ -68,17 +75,24 @@ func (gui *Gui) coloredConflictFile(content string, conflicts []commands.Conflic
return outputBuffer.String(), nil
}
func (gui *Gui) takeOverScrolling() {
gui.State.Panels.Merging.UserScrolling = false
}
func (gui *Gui) handleSelectTop(g *gocui.Gui, v *gocui.View) error {
gui.takeOverScrolling()
gui.State.Panels.Merging.ConflictTop = true
return gui.refreshMergePanel()
}
func (gui *Gui) handleSelectBottom(g *gocui.Gui, v *gocui.View) error {
gui.takeOverScrolling()
gui.State.Panels.Merging.ConflictTop = false
return gui.refreshMergePanel()
}
func (gui *Gui) handleSelectNextConflict(g *gocui.Gui, v *gocui.View) error {
gui.takeOverScrolling()
if gui.State.Panels.Merging.ConflictIndex >= len(gui.State.Panels.Merging.Conflicts)-1 {
return nil
}
@@ -87,6 +101,7 @@ func (gui *Gui) handleSelectNextConflict(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleSelectPrevConflict(g *gocui.Gui, v *gocui.View) error {
gui.takeOverScrolling()
if gui.State.Panels.Merging.ConflictIndex <= 0 {
return nil
}
@@ -103,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(g)
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 {
@@ -130,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(g)
if err != nil {
return err
gitFile := gui.getSelectedFile()
if gitFile == nil {
return nil
}
content, err := gui.GitCommand.CatFile(gitFile.Name)
if err != nil {
@@ -147,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(g)
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
@@ -159,6 +174,8 @@ func (gui *Gui) handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handlePickHunk(g *gocui.Gui, v *gocui.View) error {
gui.takeOverScrolling()
conflict := gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex]
if err := gui.pushFileSnapshot(g); err != nil {
return err
@@ -168,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)
}
@@ -183,11 +200,13 @@ func (gui *Gui) handlePickHunk(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handlePickBothHunks(g *gocui.Gui, v *gocui.View) error {
gui.takeOverScrolling()
conflict := gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex]
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)
}
@@ -198,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()
@@ -221,18 +240,42 @@ func (gui *Gui) refreshMergePanel() error {
return err
}
mainView := gui.getMainView()
mainView.Wrap = false
gui.setViewContent(gui.g, mainView, content)
gui.Log.Warn("scrolling to conflict")
if err := gui.scrollToConflict(gui.g); 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 := gui.getSelectedFile()
if item == nil {
return "", errors.New(gui.Tr.SLocalize("NoFilesDisplay"))
}
if item.Type != "file" {
return "", errors.New(gui.Tr.SLocalize("NotAFile"))
}
cat, err := gui.GitCommand.CatFile(item.Name)
if err != nil {
gui.Log.Error(err)
return "", err
}
return cat, nil
}
func (gui *Gui) scrollToConflict(g *gocui.Gui) error {
if gui.State.Panels.Merging.UserScrolling {
return nil
}
panelState := gui.State.Panels.Merging
if len(panelState.Conflicts) == 0 {
return nil
@@ -249,25 +292,27 @@ 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("main.undo"): gui.Tr.SLocalize("undo"),
})
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()
if err := gui.refreshFiles(); err != nil {
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
return err
}
// 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
}
@@ -276,24 +321,52 @@ func (gui *Gui) handleCompleteMerge() error {
if err := gui.stageSelectedFile(gui.g); err != nil {
return err
}
if err := gui.refreshFiles(); err != nil {
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
return err
}
// if we got conflicts after unstashing, we don't want to call any git
// commands to continue rebasing/merging here
if gui.State.WorkingTreeState == "normal" {
return gui.handleEscapeMerge(gui.g, gui.getMainView())
if gui.GitCommand.WorkingTreeState() == "normal" {
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 {
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)
gui.takeOverScrolling()
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 {
currentViewName := gui.currentViewName()
if currentViewName != "main" {
return false
}
file := gui.getSelectedFile()
if file == nil {
return false
}
return file.HasInlineMergeConflicts
}
+61
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,
},
}
}
+43 -26
View File
@@ -5,24 +5,38 @@ 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"
// get diff from commit file that's currently selected
commitFile := gui.getSelectedCommitFile(gui.g)
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,16 +63,18 @@ 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
}
// add range of lines to those set for the file
commitFile := gui.getSelectedCommitFile(gui.g)
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.g, 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
+96 -31
View File
@@ -7,39 +7,58 @@ import (
)
func (gui *Gui) handleCreatePatchOptionsMenu(g *gocui.Gui, v *gocui.View) error {
if !gui.GitCommand.PatchManager.CommitSelected() {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NoPatchError"))
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: "reset patch",
onPress: gui.handleResetPatch,
},
{
displayString: "apply patch",
onPress: func() error { return gui.handleApplyPatch(false) },
},
{
displayString: "apply patch in reverse",
onPress: func() error { return gui.handleApplyPatch(true) },
},
}
selectedCommit := gui.getSelectedCommit(gui.g)
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})
@@ -47,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
}
}
@@ -55,15 +74,15 @@ func (gui *Gui) getPatchCommitIndex() int {
}
func (gui *Gui) validateNormalWorkingTreeState() (bool, error) {
if gui.State.WorkingTreeState != "normal" {
return false, gui.createErrorPanel(gui.g, gui.Tr.SLocalize("CantPatchWhileRebasingError"))
if gui.GitCommand.WorkingTreeState() != "normal" {
return false, gui.createErrorPanel(gui.Tr.SLocalize("CantPatchWhileRebasingError"))
}
return true, nil
}
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
}
@@ -95,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)
})
}
@@ -109,14 +128,60 @@ func (gui *Gui) handlePullPatchIntoWorkingTree() error {
return err
}
pull := func(stash bool) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
commitIndex := gui.getPatchCommitIndex()
err := gui.GitCommand.PullPatchIntoIndex(gui.State.Commits, commitIndex, gui.GitCommand.PatchManager, stash)
return gui.handleGenericMergeCommandResult(err)
})
}
if len(gui.trackedFiles()) > 0 {
return gui.ask(askOpts{
title: gui.Tr.SLocalize("MustStashTitle"),
prompt: gui.Tr.SLocalize("MustStashWarning"),
handleConfirm: func() error {
return pull(true)
},
})
} else {
return pull(false)
}
}
func (gui *Gui) handlePullPatchIntoNewCommit() error {
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
return err
}
if err := gui.returnFocusFromLineByLinePanelIfNecessary(); err != nil {
return err
}
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
commitIndex := gui.getPatchCommitIndex()
err := gui.GitCommand.PullPatchIntoIndex(gui.State.Commits, commitIndex, gui.GitCommand.PatchManager)
err := gui.GitCommand.PullPatchIntoNewCommit(gui.State.Commits, commitIndex, gui.GitCommand.PatchManager)
return gui.handleGenericMergeCommandResult(err)
})
}
func (gui *Gui) handleApplyPatch(reverse bool) error {
if err := gui.returnFocusFromLineByLinePanelIfNecessary(); err != nil {
return err
}
if err := gui.GitCommand.PatchManager.ApplyPatches(reverse); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}
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()
}
+17 -7
View File
@@ -10,26 +10,36 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
func GetBranchListDisplayStrings(branches []*commands.Branch, fullDescription bool) [][]string {
func GetBranchListDisplayStrings(branches []*commands.Branch, fullDescription bool, diffName string) [][]string {
lines := make([][]string, len(branches))
for i := range branches {
lines[i] = getBranchDisplayStrings(branches[i], fullDescription)
diffed := branches[i].Name == diffName
lines[i] = getBranchDisplayStrings(branches[i], fullDescription, diffed)
}
return lines
}
// getBranchDisplayStrings returns the display string of branch
func getBranchDisplayStrings(b *commands.Branch, fullDescription bool) []string {
displayName := utils.ColoredString(b.Name, GetBranchColor(b.Name))
func getBranchDisplayStrings(b *commands.Branch, fullDescription bool, diffed bool) []string {
displayName := b.Name
if b.DisplayName != "" {
displayName = b.DisplayName
}
nameColorAttr := GetBranchColor(b.Name)
if diffed {
nameColorAttr = theme.DiffTerminalColor
}
coloredName := utils.ColoredString(displayName, nameColorAttr)
if b.Pushables != "" && b.Pullables != "" && b.Pushables != "?" && b.Pullables != "?" {
trackColor := color.FgYellow
if b.Pushables == "0" && b.Pullables == "0" {
trackColor = color.FgGreen
}
track := utils.ColoredString(fmt.Sprintf("↑%s↓%s", b.Pushables, b.Pullables), trackColor)
displayName = fmt.Sprintf("%s %s", displayName, track)
coloredName = fmt.Sprintf("%s %s", coloredName, track)
}
recencyColor := color.FgCyan
@@ -38,10 +48,10 @@ func getBranchDisplayStrings(b *commands.Branch, fullDescription bool) []string
}
if fullDescription {
return []string{utils.ColoredString(b.Recency, recencyColor), displayName, utils.ColoredString(b.UpstreamName, color.FgYellow)}
return []string{utils.ColoredString(b.Recency, recencyColor), coloredName, utils.ColoredString(b.UpstreamName, color.FgYellow)}
}
return []string{utils.ColoredString(b.Recency, recencyColor), displayName}
return []string{utils.ColoredString(b.Recency, recencyColor), coloredName}
}
// GetBranchColor branch color
+38 -10
View File
@@ -3,33 +3,61 @@ 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(branches []*commands.CommitFile) [][]string {
lines := make([][]string, len(branches))
func GetCommitFileListDisplayStrings(commitFiles []*commands.CommitFile, diffName string) [][]string {
if len(commitFiles) == 0 {
return [][]string{{utils.ColoredString("(none)", color.FgRed)}}
}
for i := range branches {
lines[i] = getCommitFileDisplayStrings(branches[i])
lines := make([][]string, len(commitFiles))
for i := range commitFiles {
diffed := commitFiles[i].Name == diffName
lines[i] = getCommitFileDisplayStrings(commitFiles[i], diffed)
}
return lines
}
// getCommitFileDisplayStrings returns the display string of branch
func getCommitFileDisplayStrings(f *commands.CommitFile) []string {
func getCommitFileDisplayStrings(f *commands.CommitFile, diffed bool) []string {
yellow := color.New(color.FgYellow)
green := color.New(color.FgGreen)
defaultColor := color.New(theme.DefaultTextColor)
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
}
return []string{colour.Sprint(f.DisplayString)}
if diffed {
colour = diffTerminalColor
}
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
}
}
+34 -21
View File
@@ -9,10 +9,10 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
func GetCommitListDisplayStrings(commits []*commands.Commit, fullDescription bool) [][]string {
func GetCommitListDisplayStrings(commits []*commands.Commit, fullDescription bool, cherryPickedCommitShaMap map[string]bool, diffName string) [][]string {
lines := make([][]string, len(commits))
var displayFunc func(*commands.Commit) []string
var displayFunc func(*commands.Commit, map[string]bool, bool) []string
if fullDescription {
displayFunc = getFullDescriptionDisplayStringsForCommit
} else {
@@ -20,20 +20,20 @@ func GetCommitListDisplayStrings(commits []*commands.Commit, fullDescription boo
}
for i := range commits {
lines[i] = displayFunc(commits[i])
diffed := commits[i].Sha == diffName
lines[i] = displayFunc(commits[i], cherryPickedCommitShaMap, diffed)
}
return lines
}
func getFullDescriptionDisplayStringsForCommit(c *commands.Commit) []string {
func getFullDescriptionDisplayStringsForCommit(c *commands.Commit, cherryPickedCommitShaMap map[string]bool, diffed bool) []string {
red := color.New(color.FgRed)
yellow := color.New(color.FgYellow)
green := color.New(color.FgGreen)
blue := color.New(color.FgBlue)
cyan := color.New(color.FgCyan)
defaultColor := color.New(theme.DefaultTextColor)
magenta := color.New(color.FgMagenta)
diffedColor := color.New(theme.DiffTerminalColor)
// for some reason, setting the background to blue pads out the other commits
// horizontally. For the sake of accessibility I'm considering this a feature,
@@ -52,21 +52,20 @@ func getFullDescriptionDisplayStringsForCommit(c *commands.Commit) []string {
shaColor = blue
case "reflog":
shaColor = blue
case "selected":
shaColor = magenta
default:
shaColor = defaultColor
}
if c.Copied {
if diffed {
shaColor = diffedColor
} else if cherryPickedCommitShaMap[c.Sha] {
shaColor = copied
}
tagString := ""
truncatedDate := utils.TruncateWithEllipsis(c.Date, 15)
secondColumnString := blue.Sprint(truncatedDate)
secondColumnString := blue.Sprint(utils.UnixToDate(c.UnixTimestamp))
if c.Action != "" {
secondColumnString = cyan.Sprint(c.Action)
secondColumnString = color.New(actionColorMap(c.Action)).Sprint(c.Action)
} else if c.ExtraInfo != "" {
tagColor := color.New(color.FgMagenta, color.Bold)
tagString = utils.ColoredStringDirect(c.ExtraInfo, tagColor) + " "
@@ -74,17 +73,16 @@ func getFullDescriptionDisplayStringsForCommit(c *commands.Commit) []string {
truncatedAuthor := utils.TruncateWithEllipsis(c.Author, 17)
return []string{shaColor.Sprint(c.Sha[:8]), secondColumnString, yellow.Sprint(truncatedAuthor), tagString + defaultColor.Sprint(c.Name)}
return []string{shaColor.Sprint(c.ShortSha()), secondColumnString, yellow.Sprint(truncatedAuthor), tagString + defaultColor.Sprint(c.Name)}
}
func getDisplayStringsForCommit(c *commands.Commit) []string {
func getDisplayStringsForCommit(c *commands.Commit, cherryPickedCommitShaMap map[string]bool, diffed bool) []string {
red := color.New(color.FgRed)
yellow := color.New(color.FgYellow)
green := color.New(color.FgGreen)
blue := color.New(color.FgBlue)
cyan := color.New(color.FgCyan)
defaultColor := color.New(theme.DefaultTextColor)
magenta := color.New(color.FgMagenta)
diffedColor := color.New(theme.DiffTerminalColor)
// for some reason, setting the background to blue pads out the other commits
// horizontally. For the sake of accessibility I'm considering this a feature,
@@ -103,24 +101,39 @@ func getDisplayStringsForCommit(c *commands.Commit) []string {
shaColor = blue
case "reflog":
shaColor = blue
case "selected":
shaColor = magenta
default:
shaColor = defaultColor
}
if c.Copied {
if diffed {
shaColor = diffedColor
} else if cherryPickedCommitShaMap[c.Sha] {
shaColor = copied
}
actionString := ""
tagString := ""
if c.Action != "" {
actionString = cyan.Sprint(utils.WithPadding(c.Action, 7)) + " "
actionString = color.New(actionColorMap(c.Action)).Sprint(utils.WithPadding(c.Action, 7)) + " "
} else if len(c.Tags) > 0 {
tagColor := color.New(color.FgMagenta, color.Bold)
tagString = utils.ColoredStringDirect(strings.Join(c.Tags, " "), tagColor) + " "
}
return []string{shaColor.Sprint(c.Sha[:8]), actionString + tagString + defaultColor.Sprint(c.Name)}
return []string{shaColor.Sprint(c.ShortSha()), actionString + tagString + defaultColor.Sprint(c.Name)}
}
func actionColorMap(str string) color.Attribute {
switch str {
case "pick":
return color.FgCyan
case "drop":
return color.FgRed
case "edit":
return color.FgGreen
case "fixup":
return color.FgMagenta
default:
return color.FgYellow
}
}
+29 -8
View File
@@ -3,34 +3,55 @@ package presentation
import (
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/theme"
)
func GetFileListDisplayStrings(files []*commands.File) [][]string {
func GetFileListDisplayStrings(files []*commands.File, diffName string) [][]string {
lines := make([][]string, len(files))
for i := range files {
lines[i] = getFileDisplayStrings(files[i])
diffed := files[i].Name == diffName
lines[i] = getFileDisplayStrings(files[i], diffed)
}
return lines
}
// getFileDisplayStrings returns the display string of branch
func getFileDisplayStrings(f *commands.File) []string {
func getFileDisplayStrings(f *commands.File, diffed bool) []string {
// potentially inefficient to be instantiating these color
// objects with each render
red := color.New(color.FgRed)
green := color.New(color.FgGreen)
diffColor := color.New(theme.DiffTerminalColor)
if !f.Tracked && !f.HasStagedChanges {
return []string{red.Sprint(f.DisplayString)}
}
output := green.Sprint(f.DisplayString[0:1])
output += red.Sprint(f.DisplayString[1:3])
if f.HasUnstagedChanges {
output += red.Sprint(f.Name)
var restColor *color.Color
if diffed {
restColor = diffColor
} else if f.HasUnstagedChanges {
restColor = red
} else {
output += green.Sprint(f.Name)
restColor = green
}
// this is just making things look nice when the background attribute is 'reverse'
firstChar := f.DisplayString[0:1]
firstCharCl := green
if firstChar == " " {
firstCharCl = restColor
}
secondChar := f.DisplayString[1:2]
secondCharCl := red
if secondChar == " " {
secondCharCl = restColor
}
output := firstCharCl.Sprint(firstChar)
output += secondCharCl.Sprint(secondChar)
output += restColor.Sprintf(" %s", f.Name)
return []string{output}
}
+59
View File
@@ -0,0 +1,59 @@
package presentation
import (
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func GetReflogCommitListDisplayStrings(commits []*commands.Commit, fullDescription bool, cherryPickedCommitShaMap map[string]bool, diffName string) [][]string {
lines := make([][]string, len(commits))
var displayFunc func(*commands.Commit, map[string]bool, bool) []string
if fullDescription {
displayFunc = getFullDescriptionDisplayStringsForReflogCommit
} else {
displayFunc = getDisplayStringsForReflogCommit
}
for i := range commits {
diffed := commits[i].Sha == diffName
lines[i] = displayFunc(commits[i], cherryPickedCommitShaMap, diffed)
}
return lines
}
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{
coloredReflogSha(c, cherryPickedCommitShaMap),
utils.ColoredString(utils.UnixToDate(c.UnixTimestamp), color.FgMagenta),
utils.ColoredString(c.Name, colorAttr),
}
}
func getDisplayStringsForReflogCommit(c *commands.Commit, cherryPickedCommitShaMap map[string]bool, diffed bool) []string {
defaultColor := color.New(theme.DefaultTextColor)
return []string{
coloredReflogSha(c, cherryPickedCommitShaMap),
defaultColor.Sprint(c.Name),
}
}
+11 -4
View File
@@ -2,22 +2,29 @@ package presentation
import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func GetRemoteBranchListDisplayStrings(branches []*commands.RemoteBranch) [][]string {
func GetRemoteBranchListDisplayStrings(branches []*commands.RemoteBranch, diffName string) [][]string {
lines := make([][]string, len(branches))
for i := range branches {
lines[i] = getRemoteBranchDisplayStrings(branches[i])
diffed := branches[i].FullName() == diffName
lines[i] = getRemoteBranchDisplayStrings(branches[i], diffed)
}
return lines
}
// getRemoteBranchDisplayStrings returns the display string of branch
func getRemoteBranchDisplayStrings(b *commands.RemoteBranch) []string {
displayName := utils.ColoredString(b.Name, GetBranchColor(b.Name))
func getRemoteBranchDisplayStrings(b *commands.RemoteBranch, diffed bool) []string {
nameColorAttr := GetBranchColor(b.Name)
if diffed {
nameColorAttr = theme.DiffTerminalColor
}
displayName := utils.ColoredString(b.Name, nameColorAttr)
return []string{displayName}
}
+17 -4
View File
@@ -1,20 +1,33 @@
package presentation
import (
"fmt"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func GetRemoteListDisplayStrings(remotes []*commands.Remote) [][]string {
func GetRemoteListDisplayStrings(remotes []*commands.Remote, diffName string) [][]string {
lines := make([][]string, len(remotes))
for i := range remotes {
lines[i] = getRemoteDisplayStrings(remotes[i])
diffed := remotes[i].Name == diffName
lines[i] = getRemoteDisplayStrings(remotes[i], diffed)
}
return lines
}
// getRemoteDisplayStrings returns the display string of branch
func getRemoteDisplayStrings(r *commands.Remote) []string {
return []string{r.Name}
func getRemoteDisplayStrings(r *commands.Remote, diffed bool) []string {
branchCount := len(r.Branches)
nameColorAttr := theme.DefaultTextColor
if diffed {
nameColorAttr = theme.DiffTerminalColor
}
return []string{utils.ColoredString(r.Name, nameColorAttr), utils.ColoredString(fmt.Sprintf("%d branches", branchCount), color.FgBlue)}
}
+11 -4
View File
@@ -2,19 +2,26 @@ package presentation
import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func GetStashEntryListDisplayStrings(stashEntries []*commands.StashEntry) [][]string {
func GetStashEntryListDisplayStrings(stashEntries []*commands.StashEntry, diffName string) [][]string {
lines := make([][]string, len(stashEntries))
for i := range stashEntries {
lines[i] = getStashEntryDisplayStrings(stashEntries[i])
diffed := stashEntries[i].RefName() == diffName
lines[i] = getStashEntryDisplayStrings(stashEntries[i], diffed)
}
return lines
}
// getStashEntryDisplayStrings returns the display string of branch
func getStashEntryDisplayStrings(s *commands.StashEntry) []string {
return []string{s.DisplayString}
func getStashEntryDisplayStrings(s *commands.StashEntry, diffed bool) []string {
attr := theme.DefaultTextColor
if diffed {
attr = theme.DiffTerminalColor
}
return []string{utils.ColoredString(s.Name, attr)}
}
+11 -4
View File
@@ -2,19 +2,26 @@ package presentation
import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func GetTagListDisplayStrings(tags []*commands.Tag) [][]string {
func GetTagListDisplayStrings(tags []*commands.Tag, diffName string) [][]string {
lines := make([][]string, len(tags))
for i := range tags {
lines[i] = getTagDisplayStrings(tags[i])
diffed := tags[i].Name == diffName
lines[i] = getTagDisplayStrings(tags[i], diffed)
}
return lines
}
// getTagDisplayStrings returns the display string of branch
func getTagDisplayStrings(t *commands.Tag) []string {
return []string{t.Name}
func getTagDisplayStrings(t *commands.Tag, diffed bool) []string {
attr := theme.DefaultTextColor
if diffed {
attr = theme.DiffTerminalColor
}
return []string{utils.ColoredString(t.Name, attr)}
}
+1 -1
View File
@@ -5,7 +5,7 @@ package gui
import (
"os/exec"
"github.com/jesseduffield/pty"
"github.com/creack/pty"
)
func (gui *Gui) onResize() error {
+36 -9
View File
@@ -26,22 +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 {
if gui.State.Updating {
return gui.createUpdateQuitConfirmation(gui.g, v)
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()
}
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
+23 -14
View File
@@ -3,14 +3,12 @@ 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.State.WorkingTreeState == "rebasing" {
if gui.GitCommand.WorkingTreeState() == "rebasing" {
options = append(options, "skip")
}
@@ -27,7 +25,7 @@ func (gui *Gui) handleCreateRebaseOptionsMenu(g *gocui.Gui, v *gocui.View) error
}
var title string
if gui.State.WorkingTreeState == "merging" {
if gui.GitCommand.WorkingTreeState() == "merging" {
title = gui.Tr.SLocalize("MergeOptionsTitle")
} else {
title = gui.Tr.SLocalize("RebaseOptionsTitle")
@@ -37,10 +35,10 @@ func (gui *Gui) handleCreateRebaseOptionsMenu(g *gocui.Gui, v *gocui.View) error
}
func (gui *Gui) genericMergeCommand(command string) error {
status := gui.State.WorkingTreeState
status := gui.GitCommand.WorkingTreeState()
if status != "merging" && status != "rebasing" {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NotMergingOrRebasing"))
return gui.createErrorPanel(gui.Tr.SLocalize("NotMergingOrRebasing"))
}
commandType := strings.Replace(status, "ing", "e", 1)
@@ -63,7 +61,7 @@ func (gui *Gui) genericMergeCommand(command string) error {
}
func (gui *Gui) handleGenericMergeCommandResult(result error) error {
if err := gui.refreshSidePanels(gui.g); err != nil {
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil {
return err
}
if result == nil {
@@ -74,15 +72,26 @@ func (gui *Gui) handleGenericMergeCommandResult(result error) error {
return gui.genericMergeCommand("skip")
} else if strings.Contains(result.Error(), "The previous cherry-pick is now empty") {
return gui.genericMergeCommand("continue")
} else if strings.Contains(result.Error(), "No rebase in progress?") {
// 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(gui.g, result.Error())
return gui.createErrorPanel(result.Error())
}
}
+2 -2
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,6 +31,7 @@ func (gui *Gui) handleCreateRecentReposMenu(g *gocui.Gui, v *gocui.View) error {
return err
}
gui.GitCommand = newGitCommand
gui.State.Modes.Filtering.Path = ""
return gui.Errors.ErrSwitchRepo
},
}
+75 -55
View File
@@ -3,77 +3,84 @@ 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
if selectedLine == -1 || len(gui.State.ReflogCommits) == 0 {
selectedLine := gui.State.Panels.ReflogCommits.SelectedLineIdx
reflogComits := gui.State.FilteredReflogCommits
if selectedLine == -1 || len(reflogComits) == 0 {
return nil
}
return gui.State.ReflogCommits[selectedLine]
return reflogComits[selectedLine]
}
func (gui *Gui) handleReflogCommitSelect(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
func (gui *Gui) handleReflogCommitSelect() error {
commit := gui.getSelectedReflogCommit()
var task updateTask
if commit == nil {
task = gui.createRenderStringTask("No reflog history")
} 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: "Reflog Entry",
task: task,
},
})
}
// the reflogs panel is the only panel where we cache data, in that we only
// load entries that have been created since we last ran the call. This means
// we need to be more careful with how we use this, and to ensure we're emptying
// the reflogs array when changing contexts.
// This method also manages two things: ReflogCommits and FilteredReflogCommits.
// FilteredReflogCommits are rendered in the reflogs panel, and ReflogCommits
// are used by the branches panel to obtain recency values for sorting.
func (gui *Gui) refreshReflogCommits() error {
// pulling state into its own variable incase it gets swapped out for another state
// and we get an out of bounds exception
state := gui.State
var lastReflogCommit *commands.Commit
if len(state.ReflogCommits) > 0 {
lastReflogCommit = state.ReflogCommits[0]
}
refresh := func(stateCommits *[]*commands.Commit, filterPath string) error {
commits, onlyObtainedNewReflogCommits, err := gui.GitCommand.GetReflogCommits(lastReflogCommit, filterPath)
if err != nil {
return gui.surfaceError(err)
}
if onlyObtainedNewReflogCommits {
*stateCommits = append(commits, *stateCommits...)
} else {
*stateCommits = commits
}
return nil
}
gui.State.SplitMainPanel = false
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
if err := refresh(&state.ReflogCommits, ""); err != nil {
return err
}
gui.getMainView().Title = "Reflog Entry"
commit := gui.getSelectedReflogCommit()
if commit == nil {
return gui.newStringTask("main", "No reflog history")
}
v.FocusPoint(0, gui.State.Panels.ReflogCommits.SelectedLine)
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowCmdStr(commit.Sha),
)
if err := gui.newPtyTask("main", cmd); err != nil {
gui.Log.Error(err)
}
return nil
}
func (gui *Gui) refreshReflogCommits() error {
commits, err := gui.GitCommand.GetReflogCommits()
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
gui.State.ReflogCommits = commits
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.ReflogCommits))
displayStrings := presentation.GetCommitListDisplayStrings(gui.State.ReflogCommits, gui.State.ScreenMode != SCREEN_NORMAL)
gui.renderDisplayStrings(commitsView, displayStrings)
if gui.g.CurrentView() == commitsView && commitsView.Context == "reflog-commits" {
if err := gui.handleReflogCommitSelect(gui.g, commitsView); 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
}
return nil
return gui.postRefreshUpdate(gui.Contexts.ReflogCommits.Context)
}
func (gui *Gui) handleCheckoutReflogCommit(g *gocui.Gui, v *gocui.View) error {
@@ -82,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)
}, 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
}
@@ -99,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")
}
+44 -75
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,71 +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"
remote := gui.getSelectedRemote()
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)
branchName := fmt.Sprintf("%s/%s", remote.Name, remoteBranch.Name)
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.GetBranchGraphCmdStr(branchName),
)
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.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.RemoteName + "/" + remoteBranch.Name); 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)
}
@@ -92,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.refreshRemotes()
})
}, 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)
}
@@ -117,17 +82,21 @@ func (gui *Gui) handleSetBranchUpstream(g *gocui.Gui, v *gocui.View) error {
"SetUpstreamMessage",
Teml{
"checkedOut": checkedOutBranch.Name,
"selected": selectedBranch.RemoteName + "/" + selectedBranch.Name,
"selected": selectedBranch.FullName(),
},
)
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(gui.g)
}, nil)
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
},
})
}
func (gui *Gui) handleCreateResetToRemoteBranchMenu(g *gocui.Gui, v *gocui.View) error {
@@ -136,5 +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))
return gui.createResetMenu(selectedBranch.FullName())
}
+45 -70
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,26 +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")
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")))
}
v.FocusPoint(0, gui.State.Panels.Remotes.SelectedLine)
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 {
@@ -49,7 +43,7 @@ func (gui *Gui) refreshRemotes() error {
remotes, err := gui.GitCommand.GetRemotes()
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
gui.State.Remotes = remotes
@@ -64,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.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 {
@@ -105,21 +74,18 @@ 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
}
return gui.refreshRemotes()
return gui.refreshSidePanels(refreshOptions{scope: []int{REMOTES}})
})
})
}
@@ -129,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.refreshRemotes()
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
@@ -153,12 +122,10 @@ 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.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
}
@@ -169,12 +136,17 @@ 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.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
return gui.refreshRemotes()
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
})
})
}
@@ -186,10 +158,13 @@ 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
}
return gui.refreshRemotes()
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
})
}
+24 -26
View File
@@ -4,13 +4,35 @@ import (
"fmt"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/commands"
)
func (gui *Gui) resetToRef(ref string, strength string, options commands.RunCommandOptions) error {
if err := gui.GitCommand.ResetToCommit(ref, strength, options); err != nil {
return gui.surfaceError(err)
}
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
}
return nil
}
func (gui *Gui) createResetMenu(ref string) error {
strengths := []string{"soft", "mixed", "hard"}
menuItems := make([]*menuItem, len(strengths))
for i, strength := range strengths {
innerStrength := strength
strength := strength
menuItems[i] = &menuItem{
displayStrings: []string{
fmt.Sprintf("%s reset", strength),
@@ -19,31 +41,7 @@ func (gui *Gui) createResetMenu(ref string) error {
),
},
onPress: func() error {
if err := gui.GitCommand.ResetToCommit(ref, innerStrength); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
if err := gui.switchCommitsPanelContext("branch-commits"); err != nil {
return err
}
gui.State.Panels.Commits.SelectedLine = 0
gui.State.Panels.ReflogCommits.SelectedLine = 0
if err := gui.refreshCommits(gui.g); err != nil {
return err
}
if err := gui.refreshFiles(); err != nil {
return err
}
if err := gui.refreshBranches(gui.g); err != nil {
return err
}
if err := gui.resetOrigin(gui.getCommitsView()); err != nil {
return err
}
return gui.handleCommitSelect(gui.g, gui.getCommitsView())
return gui.resetToRef(ref, strength, commands.RunCommandOptions{})
},
}
}
+17 -8
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
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)
}
}
+35 -33
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(gui.g)
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 {
@@ -113,10 +103,22 @@ func (gui *Gui) handleResetSelection(g *gocui.Gui, v *gocui.View) error {
return gui.applySelection(true)
}
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)
if !gui.Config.GetUserConfig().GetBool("gui.skipUnstageLineWarning") {
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(gui.g)
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,16 +144,16 @@ 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.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
if state.SelectMode == RANGE {
state.SelectMode = LINE
}
if err := gui.refreshFiles(); err != nil {
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
return err
}
if err := gui.refreshStagingPanel(false, -1); err != nil {
+78 -69
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(v *gocui.View) *commands.StashEntry {
selectedLine := gui.State.Panels.Stash.SelectedLine
func (gui *Gui) getSelectedStashEntry() *commands.StashEntry {
selectedLine := gui.State.Panels.Stash.SelectedLineIdx
if selectedLine == -1 {
return nil
}
@@ -17,74 +16,86 @@ func (gui *Gui) getSelectedStashEntry(v *gocui.View) *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"
stashEntry := gui.getSelectedStashEntry(v)
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)
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowStashEntryCmdStr(stashEntry.Index),
)
if err := gui.newPtyTask("main", cmd); err != nil {
gui.Log.Error(err)
task = gui.createRenderStringTask(gui.Tr.SLocalize("NoStashEntries"))
} else {
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowStashEntryCmdStr(stashEntry.Index),
)
task = gui.createRunPtyTask(cmd)
}
return nil
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: "Stash",
task: task,
},
})
}
func (gui *Gui) refreshStashEntries(g *gocui.Gui) error {
g.Update(func(g *gocui.Gui) error {
gui.State.StashEntries = gui.GitCommand.GetStashEntries()
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.renderDisplayStrings(stashView, displayStrings)
if err := gui.resetOrigin(stashView); err != nil {
return err
}
return nil
})
return nil
return gui.Contexts.Stash.Context.HandleRender()
}
// specific functions
func (gui *Gui) handleStashApply(g *gocui.Gui, v *gocui.View) error {
return gui.stashDo(g, v, "apply")
skipStashWarning := gui.Config.GetUserConfig().GetBool("gui.skipStashWarning")
apply := func() error {
return gui.stashDo("apply")
}
if skipStashWarning {
return apply()
}
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 {
return gui.stashDo(g, v, "pop")
skipStashWarning := gui.Config.GetUserConfig().GetBool("gui.skipStashWarning")
pop := func() error {
return gui.stashDo("pop")
}
if skipStashWarning {
return pop()
}
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 {
stashEntry := gui.getSelectedStashEntry(v)
func (gui *Gui) stashDo(method string) error {
stashEntry := gui.getSelectedStashEntry()
if stashEntry == nil {
errorMessage := gui.Tr.TemplateLocalize(
"NoStashTo",
@@ -92,33 +103,31 @@ func (gui *Gui) stashDo(g *gocui.Gui, v *gocui.View, method string) error {
"method": method,
},
)
return gui.createErrorPanel(g, errorMessage)
return gui.createErrorPanel(errorMessage)
}
if err := gui.GitCommand.StashDo(stashEntry.Index, method); err != nil {
return gui.createErrorPanel(g, err.Error())
return gui.surfaceError(err)
}
if err := gui.refreshStashEntries(g); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return gui.refreshFiles()
return gui.refreshSidePanels(refreshOptions{scope: []int{STASH, FILES}})
}
func (gui *Gui) handleStashSave(stashFunc func(message string) error) error {
if len(gui.trackedFiles()) == 0 && len(gui.stagedFiles()) == 0 {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NoTrackedStagedFilesStash"))
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.createErrorPanel(g, err.Error())
return gui.prompt(gui.Tr.SLocalize("StashChanges"), "", func(stashComment string) error {
if err := stashFunc(stashComment); err != nil {
return gui.surfaceError(err)
}
if err := gui.refreshStashEntries(g); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return gui.refreshFiles()
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")
}

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