Compare commits

..

216 Commits
v0.25 ... v0.27

Author SHA1 Message Date
Jesse Duffield
cd1d1996df add 0.27 release notes 2021-04-06 19:34:32 +10:00
Jesse Duffield
963fcc1444 don't kill the index.lock file until I decide whether it's actually a good idea 2021-04-06 19:34:32 +10:00
Jesse Duffield
c6825e3d0d skip some tests that are failing on CI for some reason 2021-04-06 19:34:32 +10:00
Jesse Duffield
20bdba15f6 amend reword test 2021-04-06 19:34:32 +10:00
Jesse Duffield
e636857057 prevent adding staged files when renaming top commit 2021-04-06 19:34:32 +10:00
Jesse Duffield
1ae8523098 restore contents on resume from subprocess
The proper fix will come out of this PR but it's not yet merged:
https://github.com/gdamore/tcell/pull/439/files

In the meantime I'm just going to directly edit this from my vendor
directory. If it ends up stretching a while I'll fork tcell properly
and use the fork.
2021-04-06 19:34:32 +10:00
Jesse Duffield
8eb802d3a0 fix flicker issue in main view 2021-04-06 19:34:32 +10:00
Jesse Duffield
6fc031c523 hide patch panel if we're in the commits panel and we refresh and it's now exited 2021-04-06 19:34:32 +10:00
Jesse Duffield
8c93289a72 reduce chance of deadlock by using a RW mutex on the context stack 2021-04-06 19:34:32 +10:00
Jesse Duffield
b1df0fafa2 remove junk test 2021-04-06 19:34:32 +10:00
Jesse Duffield
15046a0454 more tests for branches 2021-04-06 19:34:32 +10:00
Jesse Duffield
fb9b6314a0 ensure we're passing the right testing struct pointer around 2021-04-06 19:34:32 +10:00
Jesse Duffield
0719a3e36e stop checking out branches when doing a rename. Instead just move the cursor to the new position 2021-04-06 19:34:32 +10:00
Jesse Duffield
a3b0efb82e branch rename test 2021-04-06 19:34:32 +10:00
Jesse Duffield
bde324820d more tests 2021-04-06 19:34:32 +10:00
Jesse Duffield
bbdbbd0b1b more tests 2021-04-06 19:34:32 +10:00
Jesse Duffield
d4f3b292e6 even slower retries for CI 2021-04-06 19:34:32 +10:00
Jesse Duffield
39eb937830 update test descriptions 2021-04-06 19:34:32 +10:00
Jesse Duffield
fbab5bd444 do not refresh patch panel unless commit files panel is the current side panel 2021-04-06 19:34:32 +10:00
Jesse Duffield
12ca922a41 add tests for diffing 2021-04-06 19:34:32 +10:00
Jesse Duffield
f4e552f982 prevent deadlocks.
Hard to choose between the lock with a defer unlock in an anonymous function
vs just having an explicit unlock at the end with additional unlocks before
any early returns. The former is less error prone, but the former is much more
readable, especially if the anonymous function would have needed to return
an error value.
2021-04-06 19:34:32 +10:00
Jesse Duffield
94d26d00ba move suggestions view behind confirmation view 2021-04-06 19:34:32 +10:00
Jesse Duffield
d80d1f8493 more tests 2021-04-06 19:34:32 +10:00
Jesse Duffield
ace4350319 update snapshots to include tags comparison 2021-04-06 19:34:32 +10:00
Jesse Duffield
4441cf1045 fix bug with tags panel 2021-04-06 19:34:32 +10:00
Jesse Duffield
cf99b47ec0 another filter path test 2021-04-06 19:34:32 +10:00
Jesse Duffield
546eb50bac add another filter path test 2021-04-06 19:34:32 +10:00
Jesse Duffield
5e094c8a7c marginally better logic for searching 2021-04-06 19:34:32 +10:00
Jesse Duffield
c683f2c96c allow opening diff menu panel when other popup is open 2021-04-06 19:34:32 +10:00
Jesse Duffield
e5a372fa2d allow opening filter menu panel when other popup is open 2021-04-06 19:34:32 +10:00
Jesse Duffield
02f45b679f do not double-append contexts to the stack 2021-04-06 19:34:32 +10:00
Jesse Duffield
b1cda65dcf show error when user attempts to commit when no files are present 2021-04-06 19:34:32 +10:00
Jesse Duffield
74ce65d9ff update keybindings 2021-04-06 19:34:32 +10:00
Jesse Duffield
ccebe5e069 change language 2021-04-06 19:34:32 +10:00
Jesse Duffield
b6ec667de0 add comment 2021-04-06 19:34:32 +10:00
Jesse Duffield
390b7ddc5e change order of filtering and patch building so that esc key exits patch building mode first 2021-04-06 19:34:32 +10:00
Jesse Duffield
38739b16bc add filter path test 2021-04-06 19:34:32 +10:00
Jesse Duffield
27525f1d42 support passing extra command args in integration tests 2021-04-06 19:34:32 +10:00
Jesse Duffield
43a9dc48e0 default to not quitting when hitting esc at the top level.
I've been using this config option for years now so I don't think much of it,
but newcomers are going to find it annoying that hitting escape gets you out
of filtering/cherry-picking/patch-building mode, but also quits the app. So
if you want to exit all the modes you're in, you need to take care not to
press the key one too many times or the app will close.

We'll see if anybody gets mad about this change, but I think it's reasonable.
The only downside is that you won't be able to always quit by spamming the escape
key. If you're in a prompt panel, you'll need to hit escape to exit that, and
then 'q' at the top level. Or CTRL+C of course.
2021-04-06 19:34:32 +10:00
Jesse Duffield
440eb387d7 much cleaner integration test code 2021-04-06 19:34:32 +10:00
Jesse Duffield
28ffaf9348 tiny refactor 2021-04-06 19:34:32 +10:00
Jesse Duffield
d7da6dde0e allow decimal replay speeds for integration tests 2021-04-06 19:34:32 +10:00
Jesse Duffield
e000620cdf fix windows compilation issue 2021-04-06 19:34:32 +10:00
Jesse Duffield
f09309485a remove time limit 2021-04-06 19:34:32 +10:00
Jesse Duffield
e04e2ebab5 try better logging for CI 2021-04-06 19:34:32 +10:00
Jesse Duffield
91a107eb6f retry flakey tests 2021-04-06 19:34:32 +10:00
Jesse Duffield
5ce9e0193a add retry logic for running git commands to avoid index.lock problems 2021-04-06 19:34:32 +10:00
Jesse Duffield
4c71c26593 speed up test 2021-04-06 19:34:32 +10:00
Jesse Duffield
abdd2455bb allow playing and updating snapshots 2021-04-06 19:34:32 +10:00
Jesse Duffield
c33f8d2790 prevent git from prompting user if program is run directly 2021-04-06 19:34:32 +10:00
Jesse Duffield
8e9d08bc10 minor cleanup of integration code 2021-04-06 19:34:32 +10:00
Jesse Duffield
9593129e6a remove caching of styles in gocui 2021-04-06 19:34:32 +10:00
Jesse Duffield
267da3b4db fix issue when switching repos while files refresh 2021-04-06 19:34:32 +10:00
Jesse Duffield
c9ded489c9 bump gocui 2021-04-06 19:34:32 +10:00
Jesse Duffield
4c73d070ac ignore clicks on invisible views 2021-04-06 19:34:32 +10:00
Jesse Duffield
121b9d0715 update comment 2021-04-06 19:34:32 +10:00
Jesse Duffield
fbb33b7abc remove code that I'm pretty sure isn't needed 2021-04-06 19:34:32 +10:00
Jesse Duffield
7178bab6b4 only re-use repo state when jumping in and out of submodules 2021-04-06 19:34:32 +10:00
Jesse Duffield
2d7452bfaa Revert "see how CI goes running these tests in parallel"
This reverts commit d271cbc138.
2021-04-06 19:34:32 +10:00
Jesse Duffield
b0f3bfef27 see how CI goes running these tests in parallel 2021-04-06 19:34:32 +10:00
Jesse Duffield
7bc6dc5cf3 show branches context when starting in filtering mode 2021-04-06 19:34:32 +10:00
Jesse Duffield
ee7b634dce how about using pty 2021-04-06 19:34:32 +10:00
Jesse Duffield
b0bd752180 maybe this will fix CI 2021-04-06 19:34:32 +10:00
Jesse Duffield
4d14af5d4b more lint fixes 2021-04-06 19:34:32 +10:00
Jesse Duffield
7953e58c74 try this 2021-04-06 19:34:32 +10:00
Jesse Duffield
549d73a0b1 fix lint issues 2021-04-06 19:34:32 +10:00
Jesse Duffield
8301bba8ad make it more likely for CI to work 2021-04-06 19:34:32 +10:00
Jesse Duffield
78f17aa541 update squash integration test 2021-04-06 19:34:32 +10:00
Jesse Duffield
7578a7466f update searching tests 2021-04-06 19:34:32 +10:00
Jesse Duffield
8681a6b4e2 update patch building with filetree test 2021-04-06 19:34:32 +10:00
Jesse Duffield
efed313721 update patch building 2 test 2021-04-06 19:34:32 +10:00
Jesse Duffield
795cf39ddf update patch building test 2021-04-06 19:34:32 +10:00
Jesse Duffield
f08f248cb7 update merge conflict test 2021-04-06 19:34:32 +10:00
Jesse Duffield
3c20425649 update merge conflict undo test 2021-04-06 19:34:32 +10:00
Jesse Duffield
dfc689411b no need for debug flag because it writes to a different log anyway 2021-04-06 19:34:32 +10:00
Jesse Duffield
2295407a45 update discard file changes test 2021-04-06 19:34:32 +10:00
Jesse Duffield
828a2acd26 update branch autocomplete integration test 2021-04-06 19:34:32 +10:00
Jesse Duffield
843b8ceab0 support tcell simulation screen 2021-04-06 19:34:32 +10:00
Jesse Duffield
011451464f working on integration tests working again 2021-04-06 19:34:32 +10:00
Jesse Duffield
32d170621c remove mutex lock that caused deadlock 2021-04-06 19:34:32 +10:00
Jesse Duffield
464d022a86 minor refactor 2021-04-06 19:34:32 +10:00
Jesse Duffield
6a0066253f move recording code into gocui 2021-04-06 19:34:32 +10:00
Jesse Duffield
d627b3bfc8 more refactoring 2021-04-06 19:34:32 +10:00
Jesse Duffield
952c62df37 fix bug where searching through view got stuck if you went over the upper bound 2021-04-06 19:34:32 +10:00
Jesse Duffield
b6cc1c9492 small refactor 2021-04-06 19:34:32 +10:00
Jesse Duffield
39ae122304 more refactoring 2021-04-06 19:34:32 +10:00
Jesse Duffield
c34c6926d5 fix some things up 2021-04-06 19:34:32 +10:00
Jesse Duffield
4fe512ff3a test
type safe view access
2021-04-06 19:34:32 +10:00
Jesse Duffield
4197921465 WIP 2021-04-06 19:34:32 +10:00
Jesse Duffield
4b69ab08c1 WIP 2021-04-06 19:34:32 +10:00
Jesse Duffield
f3a0058eb9 WIP 2021-04-06 19:34:32 +10:00
Jesse Duffield
633b6f596d WIP 2021-04-06 19:34:32 +10:00
Jesse Duffield
e6274c0757 remove sentinel errors 2021-04-06 19:34:32 +10:00
Jesse Duffield
0898a7bb57 refactor 2021-04-06 19:34:32 +10:00
Jesse Duffield
fafd5234bd refactor to get view tab context map into gui state 2021-04-06 19:34:32 +10:00
Jesse Duffield
8cb10f76e4 refresh main panel when switching between tree and flat mode 2021-04-06 19:34:32 +10:00
Jesse Duffield
f1d7f59e49 switching repos without restarting the gui 2021-04-06 19:34:32 +10:00
Jesse Duffield
bc9a99387f refactor of contexts and filtering 2021-04-06 19:34:32 +10:00
Jesse Duffield
5289d49f75 more efficient gocui 2021-04-06 19:34:32 +10:00
Jesse Duffield
69e9f6d29d use suspense rather than close the gui when switching to a subprocess 2021-04-06 19:34:32 +10:00
Jesse Duffield
0b42437052 fix comment 2021-04-06 19:34:32 +10:00
Jesse Duffield
ae0f750770 fix bug where you couldn't change tabs 2021-04-06 19:34:32 +10:00
Jesse Duffield
9fe7e0d63d fix bug where we had two sets of contexts with their own state 2021-04-06 19:34:32 +10:00
Jesse Duffield
8935794e28 reset origin when clicking new item 2021-04-06 19:34:32 +10:00
Jesse Duffield
d44ff447bd fix panic 2021-04-06 19:34:32 +10:00
Jesse Duffield
798d3e2d54 get rid of these positively ghastly method signatures 2021-04-06 19:34:32 +10:00
Jesse Duffield
e8f99c3326 better scroll support 2021-04-06 19:34:32 +10:00
Jesse Duffield
1a5f380c00 support alt-enter for inserting newline when typing commit message within the app 2021-04-06 19:34:32 +10:00
Jesse Duffield
b4827a98ca fix commit message panel 2021-04-06 19:34:32 +10:00
Jesse Duffield
3ea5e4d4b2 allow scrolling when staging lines or building patch 2021-04-06 19:34:32 +10:00
Jesse Duffield
5f77ac8d6f bump gocui 2021-04-06 19:34:32 +10:00
Jesse Duffield
5d0cf3d919 prioritise keybindings on editors 2021-04-06 19:34:32 +10:00
Jesse Duffield
4b1da0cf3c bump gocui again 2021-04-06 19:34:32 +10:00
Jesse Duffield
862ced3bd0 bump gocui 2021-04-06 19:34:32 +10:00
Jesse Duffield
79b256a0fa remove 24 bit color delta arg from docs 2021-04-06 19:34:32 +10:00
Jesse Duffield
0d6ff7d1b7 support backtab key 2021-04-06 19:34:32 +10:00
Jesse Duffield
ecc5fe24a9 get tcell to cleanup the terminal if we panic 2021-04-06 19:34:32 +10:00
Jesse Duffield
1fb2317bac use true output mode 2021-04-06 19:34:32 +10:00
Jesse Duffield
6246eb9717 go mod tidy 2021-04-06 19:34:32 +10:00
Jesse Duffield
8f763c42b6 bum pgocui 2021-04-06 19:34:32 +10:00
Jesse Duffield
6472bda29e bump gocui 2021-04-06 19:34:32 +10:00
Jesse Duffield
c0cad91cb6 no more termbox 2021-04-06 19:34:32 +10:00
Jesse Duffield
1149dea4b2 stop referencing termbox 2021-04-06 19:34:32 +10:00
Jesse Duffield
6a6024e38f use tcell via porting over code from awesome-gocui 2021-04-06 19:34:32 +10:00
Jesse Duffield
8901d11674 fix merge conflict cat issue on windows 2021-04-02 13:15:07 +11:00
Jesse Duffield
8b7f7cbc30 linting 2021-04-02 11:09:12 +11:00
Jesse Duffield
b6d0bdfa2d another integration test 2021-04-02 11:09:12 +11:00
Jesse Duffield
44896bcd51 safer code 2021-04-02 11:09:12 +11:00
Jesse Duffield
bdf2b2d5c4 add merge conflict undo integration test 2021-04-02 11:09:12 +11:00
Jesse Duffield
035726f650 add integration UI to make the integration process easier 2021-04-02 11:09:12 +11:00
Jesse Duffield
1abb3cd566 more thorough merge conflict integration test 2021-04-02 11:09:12 +11:00
Jesse Duffield
f7772f00c4 do not jump cursor around when fixing merge conflicts 2021-04-02 11:09:12 +11:00
Jesse Duffield
216b5341ae better handling of scrolling for conflicted files 2021-04-02 11:09:12 +11:00
Jesse Duffield
eeeef9ca86 refactor 2021-04-02 11:09:12 +11:00
Jesse Duffield
cc9293b386 add mutex to prevent crashes with merge conflicts 2021-04-02 11:09:12 +11:00
Jesse Duffield
efe43077bc fix name 2021-04-02 11:00:15 +11:00
Jesse Duffield
949c7726d1 fix bug caused by interface 2021-04-02 11:00:15 +11:00
Jesse Duffield
0b7bda291c remove dead code 2021-04-02 11:00:15 +11:00
Jesse Duffield
872cf0d726 hide commit files view upon losing focus because you probably don't want it lingering anyway 2021-04-02 11:00:15 +11:00
Jesse Duffield
af09223dd5 refactor 2021-04-02 11:00:15 +11:00
Jesse Duffield
7d62f103e4 big refactor to give our enums actual types 2021-04-02 11:00:15 +11:00
Jesse Duffield
9e85d37fb9 refactor to no longer call these things file changes 2021-04-02 11:00:15 +11:00
Jesse Duffield
8dee06f83a allow toggling tree view for commit files panel 2021-04-02 11:00:15 +11:00
Jesse Duffield
82fe4aa6c0 disallow editing commit file directory 2021-04-02 11:00:15 +11:00
Jesse Duffield
50c169e0a3 better colouring for directories for when adding a patch 2021-04-02 11:00:15 +11:00
Jesse Duffield
7364525bf5 do not show commit files of another parent as added to the patch 2021-04-02 11:00:15 +11:00
Jesse Duffield
54910fdb76 refactor 2021-04-02 11:00:15 +11:00
Jesse Duffield
332a3c4cbf file tree for commit files 2021-04-02 11:00:15 +11:00
Jesse Duffield
ac41c41809 refactor to support commit file tree 2021-04-02 11:00:15 +11:00
Jesse Duffield
96a9df04ed Update README.md 2021-04-01 20:56:04 +11:00
Jesse Duffield
b7cc4158d5 Update Config.md 2021-04-01 20:52:56 +11:00
Jesse Duffield
2bbe6269cd Update Config.md 2021-04-01 20:44:27 +11:00
Jesse Duffield
eb54189683 support GIT_EDITOR 2021-04-01 20:40:02 +11:00
Jesse Duffield
e8e59306fc shell out custom commands 2021-04-01 20:25:30 +11:00
Jesse Duffield
8af3fe3b4a faster startup 2021-04-01 09:13:29 +11:00
Jesse Duffield
3103247e8f refactor 2021-03-30 21:57:00 +11:00
Jesse Duffield
1629a7d280 same for renames 2021-03-30 21:57:00 +11:00
Jesse Duffield
b5a5169372 expand to path when switching to tree mode 2021-03-30 21:57:00 +11:00
Jesse Duffield
4b4bfae4f4 fix background colour on selected line 2021-03-30 21:57:00 +11:00
Jesse Duffield
d5639e6e95 refactor 2021-03-30 21:57:00 +11:00
Jesse Duffield
9e67f74ca3 prevent staging directory containing files with inline merge conflicts 2021-03-30 21:57:00 +11:00
Jesse Duffield
e3ddfbf2b8 rename function 2021-03-30 21:57:00 +11:00
Jesse Duffield
1ea78c7ae7 make fields private 2021-03-30 21:57:00 +11:00
Jesse Duffield
e7af3bf55d refactor 2021-03-30 21:57:00 +11:00
Jesse Duffield
e52cec9cdf small refactor 2021-03-30 21:57:00 +11:00
Jesse Duffield
5bb48b51a0 rename 2021-03-30 21:57:00 +11:00
Jesse Duffield
d2e1b35eee small fixes 2021-03-30 21:57:00 +11:00
Jesse Duffield
ef204b0adf remove collapsed field 2021-03-30 21:57:00 +11:00
Jesse Duffield
f742434043 fix spec 2021-03-30 21:57:00 +11:00
Jesse Duffield
d3b34ce323 fix spec 2021-03-30 21:57:00 +11:00
Jesse Duffield
89c2f4f2ff fix spec 2021-03-30 21:57:00 +11:00
Jesse Duffield
5a0f23e6d6 update cheatsheets 2021-03-30 21:57:00 +11:00
Jesse Duffield
5e05e8b62b fix comment 2021-03-30 21:57:00 +11:00
Jesse Duffield
1f7273af23 better way to check if a node is a leaf 2021-03-30 21:57:00 +11:00
Jesse Duffield
2b8302bced refactor 2021-03-30 21:57:00 +11:00
Jesse Duffield
1b94462410 rename some things 2021-03-30 21:57:00 +11:00
Jesse Duffield
120bb443fe small thing 2021-03-30 21:57:00 +11:00
Jesse Duffield
6fc3c03c4b allow configuring to show file tree on startup 2021-03-30 21:57:00 +11:00
Jesse Duffield
46b79c7c61 drop Name field from status line node 2021-03-30 21:57:00 +11:00
Jesse Duffield
4782d8aa1f bring merge conflicts to top 2021-03-30 21:57:00 +11:00
Jesse Duffield
fe4e305410 safer code 2021-03-30 21:57:00 +11:00
Jesse Duffield
040c1fc302 more functional approach 2021-03-30 21:57:00 +11:00
Jesse Duffield
5edea5a8dc better handling of cursor relocation 2021-03-30 21:57:00 +11:00
Jesse Duffield
d2b65537f6 handle nothing selected 2021-03-30 21:57:00 +11:00
Jesse Duffield
1183f68e19 better handling of refreshed files 2021-03-30 21:57:00 +11:00
Jesse Duffield
da6fe01eca allow toggling on/off file tree mode 2021-03-30 21:57:00 +11:00
Jesse Duffield
c27cea6f30 more file tree improvements 2021-03-30 21:57:00 +11:00
Jesse Duffield
cd0532b4d6 allow ignoring directories 2021-03-30 21:57:00 +11:00
Jesse Duffield
c9de6c003b support some more things 2021-03-30 21:57:00 +11:00
Jesse Duffield
418621a9ff support discarding changes in dir 2021-03-30 21:57:00 +11:00
Jesse Duffield
f871724ae6 update wording 2021-03-30 21:57:00 +11:00
Jesse Duffield
def68ddc8f fix bug for combining directories with single child 2021-03-30 21:57:00 +11:00
Jesse Duffield
a31db3df9c support toggling collapsed 2021-03-30 21:57:00 +11:00
Jesse Duffield
64217a8a5b fix spacing 2021-03-30 21:57:00 +11:00
Jesse Duffield
79079b54ea combining nodes when only one child exists 2021-03-30 21:57:00 +11:00
Jesse Duffield
77a7619690 showing changes for directories 2021-03-30 21:57:00 +11:00
Jesse Duffield
9f2d7adb8e more improvements 2021-03-30 21:57:00 +11:00
Jesse Duffield
07dd9c6bc8 better tree formatting 2021-03-30 21:57:00 +11:00
Jesse Duffield
45939171ea WIP
start moving to new interface

WIP

WIP

WIP

WIP

WIP
2021-03-30 21:57:00 +11:00
Jesse Duffield
049849264e defend against race condition in editors 2021-03-30 09:26:06 +11:00
Francisco Miamoto
7e0d48c2a1 fix panic for unprintable key presses
Checking is the associated rune of a key is printable solves our problem
of having panics whenever keys like `Home` or `Ctrl-W` are pressed.

Fixes #1175
2021-03-21 11:24:36 +11:00
Jesse Duffield
ad1468f66f better handling of discarding files 2021-03-20 12:46:27 +11:00
Jesse Duffield
058bcddc53 fix renamed files looking wrong 2021-03-14 13:24:51 +11:00
Jesse Duffield
8288de0c84 update release notes 2021-03-13 11:46:48 +11:00
Ryooooooga
1da2afd450 Fix edit remote name message 2021-03-13 11:04:38 +11:00
Jesse Duffield
03de51747e remove redundant addition 2021-03-13 11:03:34 +11:00
Ryooooooga
3d698cd7c1 Fix tests 2021-03-13 11:02:31 +11:00
Ryooooooga
a48cc245e7 Support multibyte characters in pane 2021-03-13 11:02:31 +11:00
Ryooooooga
9ed3a8ee05 Fix staging/unstaging filenames that starts with - or -- 2021-03-13 11:02:31 +11:00
Ryooooooga
64daf1310d Fix staging/unstaging files containing " in paths 2021-03-13 11:02:31 +11:00
Ryooooooga
e5ba0d9d9c Support multibyte characters in Files pane 2021-03-13 11:02:31 +11:00
Ryooooooga
50e4e9d58d fix command escaping 2021-03-13 10:49:40 +11:00
István Donkó
03b9db5e0a Fix the linux config path (related: #913, #1059) 2021-03-12 12:45:48 +11:00
Jesse Duffield
043cb2ea44 reload config whenever returning to gui 2021-02-24 02:45:05 -08:00
Dawid Dziurla
a62d70fbd5 Merge pull request #1172 from jesseduffield/dawidd6-patch-1
gui: ReplaceAll -> Replace
2021-02-24 00:14:24 +01:00
Dawid Dziurla
053e80a08e gui: ReplaceAll -> Replace 2021-02-24 00:09:05 +01:00
2332 changed files with 54639 additions and 13078 deletions

View File

@@ -279,12 +279,10 @@ Run `lazygit --debug` in one terminal tab and `lazygit --logs` in another to vie
If you would like to support the development of lazygit, consider [sponsoring me](https://github.com/sponsors/jesseduffield) (github is matching all donations dollar-for-dollar for 12 months)
## Work in progress
## FAQ
This is still a work in progress so there's still bugs to iron out and as this
is my first project in Go the code could no doubt use an increase in quality,
but I'll be improving on it whenever I find the time. If you have any feedback
feel free to [raise an issue](https://github.com/jesseduffield/lazygit/issues)/[submit a PR](https://github.com/jesseduffield/lazygit/pulls).
### I'm struggling to see the selected line
see [here](https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#struggling-to-see-selected-line)
## Social

View File

@@ -2,7 +2,7 @@
Default path for the config file:
* Linux: `~/.config/jesseduffield/lazygit/config.yml`
* Linux: `~/.config/lazygit/config.yml`
* MacOS: `~/Library/Application Support/jesseduffield/lazygit/config.yml`
* Windows: `%APPDATA%\jesseduffield\lazygit\config.yml`
@@ -34,6 +34,7 @@ Default path for the config file:
mouseEvents: true
skipUnstageLineWarning: false
skipStashWarning: true
showFileTree: false # for rendering changes files in a tree format
git:
paging:
colorArg: always
@@ -60,7 +61,7 @@ Default path for the config file:
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
quitOnTopLevelReturn: false
disableStartupPopups: false
notARepository: 'prompt' # one of: 'prompt' | 'create' | 'skip'
keybinding:
@@ -133,6 +134,7 @@ Default path for the config file:
toggleStagedAll: 'a' # stage/unstage all
viewResetOptions: 'D'
fetch: 'f'
toggleTreeView: '`'
branches:
createPullRequest: 'o'
checkoutBranchByName: 'c'
@@ -248,7 +250,7 @@ If you have issues with a light terminal theme where you can't read / see the te
## Struggling to see selected line
If you struggle to see the selected line I recomment using the reverse attribute on selected lines like so:
If you struggle to see the selected line I recommend using the reverse attribute on selected lines like so:
```yaml
gui:
@@ -259,6 +261,24 @@ If you struggle to see the selected line I recomment using the reverse attribute
- reverse
```
The following has also worked for a couple of people:
```yaml
gui:
theme:
activeBorderColor:
- white
- bold
inactiveBorderColor:
- white
selectedLineBgColor:
- reverse
- blue
```
Alternatively you may have bold fonts disabled in your terminal, in which case enabling bold fonts should solve the problem.
If you're still having trouble please raise an issue.
## Example Coloring
![border example](../../assets/colored-border-example.png)

View File

@@ -21,7 +21,7 @@ the `colorArg` key is for whether you want the `--color=always` arg in your `git
git:
paging:
colorArg: always
pager: delta --dark --paging=never --24-bit-color=never
pager: delta --dark --paging=never
```
![](https://i.imgur.com/QJpQkF3.png)

View File

@@ -16,7 +16,7 @@
<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>: view filter-by-path options
<kbd>W</kbd>: open diff menu
<kbd>ctrl+e</kbd>: open diff menu
</pre>
@@ -109,7 +109,8 @@
<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>enter</kbd>: enter file to add selected lines to the patch (or toggle directory collapsed)
<kbd>`</kbd>: toggle file tree view
</pre>
## Commits Panel (Commits)
@@ -170,10 +171,11 @@
<kbd>S</kbd>: view stash options
<kbd>a</kbd>: stage/unstage all
<kbd>D</kbd>: view reset options
<kbd>enter</kbd>: stage individual hunks/lines
<kbd>enter</kbd>: stage individual hunks/lines for file, or collapse/expand for directory
<kbd>f</kbd>: fetch
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
<kbd>g</kbd>: view upstream reset options
<kbd>`</kbd>: toggle file tree view
</pre>
## Files Panel (Submodules)
@@ -205,8 +207,8 @@
## Main Panel (Normal)
<pre>
<kbd></kbd>: scroll down (fn+up)
<kbd></kbd>: scroll up (fn+down)
<kbd>Ő</kbd>: scroll down (fn+up)
<kbd>ő</kbd>: scroll up (fn+down)
</pre>
## Main Panel (Patch Building)

View File

@@ -110,6 +110,7 @@
<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>: toggle file tree view
</pre>
## Commits Paneel (Commits)
@@ -174,6 +175,7 @@
<kbd>f</kbd>: fetch
<kbd>ctrl+o</kbd>: kopieer de bestandsnaam naar het klembord
<kbd>g</kbd>: bekijk upstream reset opties
<kbd>`</kbd>: toggle file tree view
</pre>
## Bestanden Paneel (Submodules)
@@ -205,8 +207,8 @@
## Hooft Paneel (Normaal)
<pre>
<kbd></kbd>: scroll omlaag (fn+up)
<kbd></kbd>: scroll omhoog (fn+down)
<kbd>Ő</kbd>: scroll omlaag (fn+up)
<kbd>ő</kbd>: scroll omhoog (fn+down)
</pre>
## Hooft Paneel (Patch Bouwen)

View File

@@ -16,7 +16,7 @@
<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>: view filter-by-path options
<kbd>W</kbd>: open diff menu
<kbd>ctrl+e</kbd>: open diff menu
</pre>
@@ -109,7 +109,8 @@
<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>enter</kbd>: enter file to add selected lines to the patch (or toggle directory collapsed)
<kbd>`</kbd>: toggle file tree view
</pre>
## Commity Panel (Commity)
@@ -174,6 +175,7 @@
<kbd>f</kbd>: fetch
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
<kbd>g</kbd>: view upstream reset options
<kbd>`</kbd>: toggle file tree view
</pre>
## Pliki Panel (Submodules)
@@ -205,8 +207,8 @@
## Main Panel (Normal)
<pre>
<kbd></kbd>: scroll down (fn+up)
<kbd></kbd>: scroll up (fn+down)
<kbd>Ő</kbd>: scroll down (fn+up)
<kbd>ő</kbd>: scroll up (fn+down)
</pre>
## Main Panel (Patch Building)

13
go.mod
View File

@@ -11,6 +11,7 @@ require (
github.com/creack/pty v1.1.11
github.com/fatih/color v1.9.0
github.com/fsnotify/fsnotify v1.4.7
github.com/gdamore/tcell/v2 v2.2.0 // indirect
github.com/go-errors/errors v1.1.1
github.com/go-logfmt/logfmt v0.5.0 // indirect
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
@@ -19,16 +20,16 @@ require (
github.com/imdario/mergo v0.3.11
github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4
github.com/jesseduffield/gocui v0.3.1-0.20210208224444-2eecee85583d
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe
github.com/jesseduffield/gocui v0.3.1-0.20210406065942-1b0c68414064
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe // indirect
github.com/jesseduffield/yaml v2.1.0+incompatible
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.7 // indirect
github.com/mattn/go-runewidth v0.0.10
github.com/mattn/go-runewidth v0.0.12
github.com/mgutz/str v1.2.0
github.com/nsf/termbox-go v0.0.0-20210114135735-d04385b850e8 // indirect
github.com/onsi/ginkgo v1.10.3 // indirect
github.com/onsi/gomega v1.7.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
@@ -38,7 +39,9 @@ require (
github.com/stretchr/testify v1.4.0
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 // indirect
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c // indirect
golang.org/x/sys v0.0.0-20201005172224-997123666555 // indirect
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 // indirect
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 // indirect
golang.org/x/text v0.3.6 // indirect
)
replace github.com/go-git/go-git/v5 => github.com/jesseduffield/go-git/v5 v5.1.1

66
go.sum
View File

@@ -31,8 +31,17 @@ github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjr
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.0.0 h1:GRWG8aLfWAlekj9Q6W29bVvkHENc6hp79XOqG4AWDOs=
github.com/gdamore/tcell/v2 v2.0.0/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
github.com/gdamore/tcell/v2 v2.1.0 h1:UnSmozHgBkQi2PGsFr+rpdXuAPRRucMegpQp3Z3kDro=
github.com/gdamore/tcell/v2 v2.1.0/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
github.com/gdamore/tcell/v2 v2.2.0 h1:vSyEgKwraXPSOkvCk7IwOSyX+Pv3V2cV9CikJMXg4U4=
github.com/gdamore/tcell/v2 v2.2.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
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=
@@ -69,8 +78,34 @@ github.com/jesseduffield/gocui v0.3.1-0.20161105104656-d666c9f652af h1:9ZI/QyVOe
github.com/jesseduffield/gocui v0.3.1-0.20161105104656-d666c9f652af/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20201010224802-8a6768078fd7 h1:K3MGrjmpPtIhfXmKh/zsIF0CdmNKOkjpIwcUfAa/J2A=
github.com/jesseduffield/gocui v0.3.1-0.20201010224802-8a6768078fd7/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20201224041937-f5a9733d1860 h1:1xfQM6T5A4jqcVvUnYaKR6bGrOhDLWQsp79JFNJpzcQ=
github.com/jesseduffield/gocui v0.3.1-0.20201224041937-f5a9733d1860/go.mod h1:9LmtJcK+Kwiuc2huslzS37uFJPdHka2Cs/cQ06JZdbk=
github.com/jesseduffield/gocui v0.3.1-0.20210208224444-2eecee85583d h1:Jto9W9w8CFwZiAYXa7LsHDEOb5cKCA1f5LOL1A3jva4=
github.com/jesseduffield/gocui v0.3.1-0.20210208224444-2eecee85583d/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20210329125022-96b1d3106429 h1:Ih3UVczKRabZnQ7RisGi5uItC2QJxdqgef7AClJ2G9A=
github.com/jesseduffield/gocui v0.3.1-0.20210329125022-96b1d3106429/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20210329125502-e830abf4b73a h1:RVYf2MA/RJbodE+S0e2z++JmB9A7hD1lUsI0euv1fmA=
github.com/jesseduffield/gocui v0.3.1-0.20210329125502-e830abf4b73a/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20210329130738-e026850021e3 h1:UDiArPlzkg+8mmNjhUOamQoyiTSzQUGIpOsu5hCRJVI=
github.com/jesseduffield/gocui v0.3.1-0.20210329130738-e026850021e3/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20210329131148-bcc4dcd991ff h1:fTt3EzLtpsc7OA7A6Vd6JVnlxvcAy7cY9lmN9yzDwSs=
github.com/jesseduffield/gocui v0.3.1-0.20210329131148-bcc4dcd991ff/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
github.com/jesseduffield/gocui v0.3.1-0.20210402033412-1238f910f001 h1:1WH+lTSK5YMr8emISHPA+VqYDDcLei6djuSxBCLIaiI=
github.com/jesseduffield/gocui v0.3.1-0.20210402033412-1238f910f001/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
github.com/jesseduffield/gocui v0.3.1-0.20210402040718-77a1b9631715 h1:nELTdFJiZk3vv7j8nWoHvl7H2IqTr26EHKl6LaorRA8=
github.com/jesseduffield/gocui v0.3.1-0.20210402040718-77a1b9631715/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
github.com/jesseduffield/gocui v0.3.1-0.20210402113210-6fd7ef27ce76 h1:miXVlortFNTlOX+KiKW3cVxOR6+Uhl4pnQRei2X26Y4=
github.com/jesseduffield/gocui v0.3.1-0.20210402113210-6fd7ef27ce76/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
github.com/jesseduffield/gocui v0.3.1-0.20210403045716-a3be78c4ccf6 h1:nENhj0TKu+11RrPm9Ls5YtzkpbNHM0faXr9UECDhODQ=
github.com/jesseduffield/gocui v0.3.1-0.20210403045716-a3be78c4ccf6/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
github.com/jesseduffield/gocui v0.3.1-0.20210405041826-439abd8b6e07 h1:BymGR28auSeuW0QELl0JomK0iFLPS/WRjFlc1iGZiOQ=
github.com/jesseduffield/gocui v0.3.1-0.20210405041826-439abd8b6e07/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
github.com/jesseduffield/gocui v0.3.1-0.20210405093708-e79dab8f7772 h1:dg9krj10Udac4IcvlVCOAPktQkfggkgtqRmbDKk7Pzw=
github.com/jesseduffield/gocui v0.3.1-0.20210405093708-e79dab8f7772/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
github.com/jesseduffield/gocui v0.3.1-0.20210406065811-95ef6e13779b h1:3+4+muhhikpls5FePXSRNFgcdoPx8dTdqaCy3AqLz98=
github.com/jesseduffield/gocui v0.3.1-0.20210406065811-95ef6e13779b/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
github.com/jesseduffield/gocui v0.3.1-0.20210406065942-1b0c68414064 h1:Oe+QJuUIOd2TU+A3BW5sT1eXqceoBcOOfyoHlGf7F8Y=
github.com/jesseduffield/gocui v0.3.1-0.20210406065942-1b0c68414064/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
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/jesseduffield/yaml v2.1.0+incompatible h1:HWQJ1gIv2zHKbDYNp0Jwjlj24K8aqpFHnMCynY1EpmE=
@@ -94,6 +129,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
@@ -105,18 +144,19 @@ github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGe
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.7/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/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
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/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/nsf/termbox-go v0.0.0-20210114135735-d04385b850e8 h1:3vzIuru1svOK2sXlg4XcrO3KkGRneIejmfQfR+ptSW8=
github.com/nsf/termbox-go v0.0.0-20210114135735-d04385b850e8/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo=
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=
@@ -168,17 +208,35 @@ golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201005172224-997123666555 h1:fihtqzYxy4E31W1yUlyRGveTZT1JIP0bmKaDZ2ceKAw=
golang.org/x/sys v0.0.0-20201005172224-997123666555/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210217105451-b926d437f341 h1:2/QtM1mL37YmcsT8HaDNHDgTqqFVw+zr8UzMiBVLzYU=
golang.org/x/sys v0.0.0-20210217105451-b926d437f341/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54 h1:rF3Ohx8DRyl8h2zw9qojyLHLhrJpEMgyPOImREEryf0=
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210402192133-700132347e07 h1:4k6HsQjxj6hVMsI2Vf0yKlzt5lXxZsMW1q0zaq2k8zY=
golang.org/x/sys v0.0.0-20210402192133-700132347e07/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 h1:EC6+IGYTjPpRfv9a2b/6Puw0W+hLtAhkV1tPsXhutqs=
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
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=
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=

View File

@@ -97,6 +97,7 @@ func newLogger(config config.AppConfigurer) *logrus.Entry {
// NewApp bootstrap a new application
func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
app := &App{
closers: []io.Closer{},
Config: config,
@@ -182,7 +183,7 @@ func (app *App) setupRepo() (bool, error) {
}
// if we are not in a git repo, we ask if we want to `git init`
if err := app.OSCommand.RunCommand("git status"); err != nil {
if err := commands.VerifyInGitRepo(app.OSCommand); err != nil {
cwd, err := os.Getwd()
if err != nil {
return false, err
@@ -225,6 +226,7 @@ func (app *App) setupRepo() (bool, error) {
return false, err
}
}
return false, nil
}
@@ -237,7 +239,7 @@ func (app *App) Run() error {
os.Exit(0)
}
err := app.Gui.RunWithSubprocesses()
err := app.Gui.RunAndHandleError()
return err
}

View File

@@ -11,19 +11,19 @@ import (
// NewBranch create new branch
func (c *GitCommand) NewBranch(name string, base string) error {
return c.OSCommand.RunCommand("git checkout -b %s %s", name, base)
return c.RunCommand("git checkout -b %s %s", name, base)
}
// 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")
branchName, err := c.RunCommandWithOutput("git symbolic-ref --short HEAD")
if err == nil && branchName != "HEAD\n" {
trimmedBranchName := strings.TrimSpace(branchName)
return trimmedBranchName, trimmedBranchName, nil
}
output, err := c.OSCommand.RunCommandWithOutput("git branch --contains")
output, err := c.RunCommandWithOutput("git branch --contains")
if err != nil {
return "", "", err
}
@@ -73,7 +73,7 @@ func (c *GitCommand) GetBranchGraph(branchName string) (string, error) {
}
func (c *GitCommand) GetUpstreamForBranch(branchName string) (string, error) {
output, err := c.OSCommand.RunCommandWithOutput("git rev-parse --abbrev-ref --symbolic-full-name %s@{u}", branchName)
output, err := c.RunCommandWithOutput("git rev-parse --abbrev-ref --symbolic-full-name %s@{u}", branchName)
return strings.TrimSpace(output), err
}
@@ -86,11 +86,11 @@ func (c *GitCommand) GetBranchGraphCmdStr(branchName string) string {
}
func (c *GitCommand) SetUpstreamBranch(upstream string) error {
return c.OSCommand.RunCommand("git branch -u %s", upstream)
return c.RunCommand("git branch -u %s", upstream)
}
func (c *GitCommand) SetBranchUpstream(remoteName string, remoteBranchName string, branchName string) error {
return c.OSCommand.RunCommand("git branch --set-upstream-to=%s/%s %s", remoteName, remoteBranchName, branchName)
return c.RunCommand("git branch --set-upstream-to=%s/%s %s", remoteName, remoteBranchName, branchName)
}
func (c *GitCommand) GetCurrentBranchUpstreamDifferenceCount() (string, string) {
@@ -134,24 +134,24 @@ func (c *GitCommand) Merge(branchName string, opts MergeOpts) error {
// AbortMerge abort merge
func (c *GitCommand) AbortMerge() error {
return c.OSCommand.RunCommand("git merge --abort")
return c.RunCommand("git merge --abort")
}
func (c *GitCommand) IsHeadDetached() bool {
err := c.OSCommand.RunCommand("git symbolic-ref -q HEAD")
err := c.RunCommand("git symbolic-ref -q HEAD")
return err != nil
}
// ResetHardHead runs `git reset --hard`
func (c *GitCommand) ResetHard(ref string) error {
return c.OSCommand.RunCommand("git reset --hard " + ref)
return c.RunCommand("git reset --hard " + ref)
}
// ResetSoft runs `git reset --soft HEAD`
func (c *GitCommand) ResetSoft(ref string) error {
return c.OSCommand.RunCommand("git reset --soft " + ref)
return c.RunCommand("git reset --soft " + ref)
}
func (c *GitCommand) RenameBranch(oldName string, newName string) error {
return c.OSCommand.RunCommand("git branch --move %s %s", oldName, newName)
return c.RunCommand("git branch --move %s %s", oldName, newName)
}

View File

@@ -3,7 +3,6 @@ package commands
import (
"fmt"
"os/exec"
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
@@ -12,7 +11,7 @@ import (
// RenameCommit renames the topmost commit with the given name
func (c *GitCommand) RenameCommit(name string) error {
return c.OSCommand.RunCommand("git commit --allow-empty --amend -m %s", c.OSCommand.Quote(name))
return c.RunCommand("git commit --allow-empty --amend --only -m %s", c.OSCommand.Quote(name))
}
// ResetToCommit reset to commit
@@ -25,7 +24,7 @@ func (c *GitCommand) Commit(message string, flags string) (*exec.Cmd, error) {
splitMessage := strings.Split(message, "\n")
lineArgs := ""
for _, line := range splitMessage {
lineArgs += fmt.Sprintf(" -m %s", strconv.Quote(line))
lineArgs += fmt.Sprintf(" -m %s", c.OSCommand.Quote(line))
}
command := fmt.Sprintf("git commit %s%s", flags, lineArgs)
@@ -75,7 +74,7 @@ func (c *GitCommand) ShowCmdStr(sha string, filterPath string) string {
// Revert reverts the selected commit by sha
func (c *GitCommand) Revert(sha string) error {
return c.OSCommand.RunCommand("git revert %s", sha)
return c.RunCommand("git revert %s", sha)
}
// CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD
@@ -95,5 +94,5 @@ func (c *GitCommand) CherryPickCommits(commits []*models.Commit) error {
// 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)
return c.RunCommand("git commit --fixup=%s", sha)
}

View File

@@ -15,7 +15,7 @@ func (c *GitCommand) ConfiguredPager() string {
if os.Getenv("PAGER") != "" {
return os.Getenv("PAGER")
}
output, err := c.OSCommand.RunCommandWithOutput("git config --get-all core.pager")
output, err := c.RunCommandWithOutput("git config --get-all core.pager")
if err != nil {
return ""
}

View File

@@ -2,48 +2,47 @@ package commands
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/mgutz/str"
)
// CatFile obtains the content of a file
func (c *GitCommand) CatFile(fileName string) (string, error) {
return c.OSCommand.RunCommandWithOutput("%s %s", c.OSCommand.Platform.CatCmd, c.OSCommand.Quote(fileName))
return c.OSCommand.CatFile(fileName)
}
// StageFile stages a file
func (c *GitCommand) StageFile(fileName string) error {
// renamed files look like "file1 -> file2"
fileNames := strings.Split(fileName, " -> ")
return c.OSCommand.RunCommand("git add %s", c.OSCommand.Quote(fileNames[len(fileNames)-1]))
return c.RunCommand("git add -- %s", c.OSCommand.Quote(fileName))
}
// StageAll stages all files
func (c *GitCommand) StageAll() error {
return c.OSCommand.RunCommand("git add -A")
return c.RunCommand("git add -A")
}
// UnstageAll stages all files
// UnstageAll unstages all files
func (c *GitCommand) UnstageAll() error {
return c.OSCommand.RunCommand("git reset")
return c.RunCommand("git reset")
}
// UnStageFile unstages a file
func (c *GitCommand) UnStageFile(fileName string, tracked bool) error {
command := "git rm --cached --force %s"
if tracked {
command = "git reset HEAD %s"
// we accept an array of filenames for the cases where a file has been renamed i.e.
// we accept the current name and the previous name
func (c *GitCommand) UnStageFile(fileNames []string, reset bool) error {
command := "git rm --cached --force -- %s"
if reset {
command = "git reset HEAD -- %s"
}
// renamed files look like "file1 -> file2"
fileNames := strings.Split(fileName, " -> ")
for _, name := range fileNames {
if err := c.OSCommand.RunCommand(command, c.OSCommand.Quote(name)); err != nil {
return err
@@ -58,20 +57,19 @@ func (c *GitCommand) BeforeAndAfterFileForRename(file *models.File) (*models.Fil
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
// we've got a file that represents a rename from one file to another. 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
// again for the before file and after file.
split := strings.Split(file.Name, " -> ")
filesWithoutRenames := c.GetStatusFiles(GetStatusFileOptions{NoRenames: true})
var beforeFile *models.File
var afterFile *models.File
for _, f := range filesWithoutRenames {
if f.Name == split[0] {
if f.Name == file.PreviousName {
beforeFile = f
}
if f.Name == split[1] {
if f.Name == file.Name {
afterFile = f
}
}
@@ -107,24 +105,76 @@ func (c *GitCommand) DiscardAllFileChanges(file *models.File) error {
return nil
}
// if the file isn't tracked, we assume you want to delete it
quotedFileName := c.OSCommand.Quote(file.Name)
if file.ShortStatus == "AA" {
if err := c.RunCommand("git checkout --ours -- %s", quotedFileName); err != nil {
return err
}
if err := c.RunCommand("git add %s", quotedFileName); err != nil {
return err
}
return nil
}
if file.ShortStatus == "DU" {
return c.RunCommand("git rm %s", quotedFileName)
}
// if the file isn't tracked, we assume you want to delete it
if file.HasStagedChanges || file.HasMergeConflicts {
if err := c.OSCommand.RunCommand("git reset -- %s", quotedFileName); err != nil {
if err := c.RunCommand("git reset -- %s", quotedFileName); err != nil {
return err
}
}
if !file.Tracked {
if file.ShortStatus == "DD" || file.ShortStatus == "AU" {
return nil
}
if file.Added {
return c.removeFile(file.Name)
}
return c.DiscardUnstagedFileChanges(file)
}
func (c *GitCommand) DiscardAllDirChanges(node *filetree.FileNode) error {
// this could be more efficient but we would need to handle all the edge cases
return node.ForEachFile(c.DiscardAllFileChanges)
}
func (c *GitCommand) DiscardUnstagedDirChanges(node *filetree.FileNode) error {
if err := c.RemoveUntrackedDirFiles(node); err != nil {
return err
}
quotedPath := c.OSCommand.Quote(node.GetPath())
if err := c.RunCommand("git checkout -- %s", quotedPath); err != nil {
return err
}
return nil
}
func (c *GitCommand) RemoveUntrackedDirFiles(node *filetree.FileNode) error {
untrackedFilePaths := node.GetPathsMatching(
func(n *filetree.FileNode) bool { return n.File != nil && !n.File.GetIsTracked() },
)
for _, path := range untrackedFilePaths {
err := os.Remove(path)
if err != nil {
return err
}
}
return nil
}
// DiscardUnstagedFileChanges directly
func (c *GitCommand) DiscardUnstagedFileChanges(file *models.File) error {
quotedFileName := c.OSCommand.Quote(file.Name)
return c.OSCommand.RunCommand("git checkout -- %s", quotedFileName)
return c.RunCommand("git checkout -- %s", quotedFileName)
}
// Ignore adds a file to the gitignore for the repo
@@ -139,23 +189,22 @@ func (c *GitCommand) WorktreeFileDiff(file *models.File, plain bool, cached bool
return s
}
func (c *GitCommand) WorktreeFileDiffCmdStr(file *models.File, plain bool, cached bool) string {
func (c *GitCommand) WorktreeFileDiffCmdStr(node models.IFile, plain bool, cached bool) string {
cachedArg := ""
trackedArg := "--"
colorArg := c.colorArg()
split := strings.Split(file.Name, " -> ") // in case of a renamed file we get the new filename
fileName := c.OSCommand.Quote(split[len(split)-1])
path := c.OSCommand.Quote(node.GetPath())
if cached {
cachedArg = "--cached"
}
if !file.Tracked && !file.HasStagedChanges && !cached {
trackedArg = "--no-index /dev/null"
if !node.GetIsTracked() && !node.GetHasStagedChanges() && !cached {
trackedArg = "--no-index -- /dev/null"
}
if plain {
colorArg = "never"
}
return fmt.Sprintf("git diff --submodule --no-ext-diff --color=%s %s %s %s", colorArg, cachedArg, trackedArg, fileName)
return fmt.Sprintf("git diff --submodule --no-ext-diff --color=%s %s %s %s", colorArg, cachedArg, trackedArg, path)
}
func (c *GitCommand) ApplyPatch(patch string, flags ...string) error {
@@ -170,7 +219,7 @@ func (c *GitCommand) ApplyPatch(patch string, flags ...string) error {
flagStr += " --" + flag
}
return c.OSCommand.RunCommand("git apply %s %s", flagStr, c.OSCommand.Quote(filepath))
return c.RunCommand("git apply %s %s", flagStr, c.OSCommand.Quote(filepath))
}
// ShowFileDiff get the diff of specified from and to. Typically this will be used for a single commit so it'll be 123abc^..123abc
@@ -196,7 +245,7 @@ func (c *GitCommand) ShowFileDiffCmdStr(from string, to string, reverse bool, fi
// CheckoutFile checks out the file for the given commit
func (c *GitCommand) CheckoutFile(commitSha, fileName string) error {
return c.OSCommand.RunCommand("git checkout %s %s", commitSha, fileName)
return c.RunCommand("git checkout %s %s", commitSha, fileName)
}
// DiscardOldFileChanges discards changes to a file from an old commit
@@ -206,7 +255,7 @@ func (c *GitCommand) DiscardOldFileChanges(commits []*models.Commit, commitIndex
}
// check if file exists in previous commit (this command returns an error if the file doesn't exist)
if err := c.OSCommand.RunCommand("git cat-file -e HEAD^:%s", fileName); err != nil {
if err := c.RunCommand("git cat-file -e HEAD^:%s", fileName); err != nil {
if err := c.OSCommand.Remove(fileName); err != nil {
return err
}
@@ -232,17 +281,17 @@ func (c *GitCommand) DiscardOldFileChanges(commits []*models.Commit, commitIndex
// DiscardAnyUnstagedFileChanges discards any unstages file changes via `git checkout -- .`
func (c *GitCommand) DiscardAnyUnstagedFileChanges() error {
return c.OSCommand.RunCommand("git checkout -- .")
return c.RunCommand("git checkout -- .")
}
// RemoveTrackedFiles will delete the given file(s) even if they are currently tracked
func (c *GitCommand) RemoveTrackedFiles(name string) error {
return c.OSCommand.RunCommand("git rm -r --cached %s", name)
return c.RunCommand("git rm -r --cached %s", name)
}
// RemoveUntrackedFiles runs `git clean -fd`
func (c *GitCommand) RemoveUntrackedFiles() error {
return c.OSCommand.RunCommand("git clean -fd")
return c.RunCommand("git clean -fd")
}
// ResetAndClean removes all unstaged changes and removes all untracked files
@@ -266,10 +315,13 @@ func (c *GitCommand) ResetAndClean() error {
}
// EditFile opens a file in a subprocess using whatever editor is available,
// falling back to core.editor, VISUAL, EDITOR, then vi
// falling back to core.editor, GIT_EDITOR, VISUAL, EDITOR, then vi
func (c *GitCommand) EditFile(filename string) (*exec.Cmd, error) {
editor := c.GetConfigValue("core.editor")
if editor == "" {
editor = c.OSCommand.Getenv("GIT_EDITOR")
}
if editor == "" {
editor = c.OSCommand.Getenv("VISUAL")
}
@@ -282,7 +334,7 @@ func (c *GitCommand) EditFile(filename string) (*exec.Cmd, error) {
}
}
if editor == "" {
return nil, errors.New("No editor defined in $VISUAL, $EDITOR, or git config")
return nil, errors.New("No editor defined in $GIT_EDITOR, $VISUAL, $EDITOR, or git config")
}
splitCmd := str.ToArgv(fmt.Sprintf("%s %s", editor, c.OSCommand.Quote(filename)))

View File

@@ -5,6 +5,7 @@ import (
"os"
"path/filepath"
"strings"
"time"
"github.com/go-errors/errors"
@@ -54,10 +55,6 @@ func NewGitCommand(log *logrus.Entry, osCommand *oscommands.OSCommand, tr *i18n.
pushToCurrent = strings.TrimSpace(output) == "current"
}
if err := verifyInGitRepo(osCommand.RunCommand); err != nil {
return nil, err
}
if err := navigateToRepoRootDirectory(os.Stat, os.Chdir); err != nil {
return nil, err
}
@@ -88,10 +85,6 @@ func NewGitCommand(log *logrus.Entry, osCommand *oscommands.OSCommand, tr *i18n.
return gitCommand, nil
}
func verifyInGitRepo(runCmd func(string, ...interface{}) error) error {
return runCmd("git status")
}
func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir func(string) error) error {
gitDir := env.GetGitDirEnv()
if gitDir != "" {
@@ -120,6 +113,18 @@ func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir f
if err = chdir(".."); err != nil {
return utils.WrapError(err)
}
currentPath, err := os.Getwd()
if err != nil {
return err
}
atRoot := currentPath == filepath.Dir(currentPath)
if atRoot {
// we should never really land here: the code that creates GitCommand should
// verify we're in a git directory
return errors.New("Must open lazygit in a git repository")
}
}
}
@@ -189,3 +194,36 @@ func findDotGitDir(stat func(string) (os.FileInfo, error), readFile func(filenam
}
return strings.TrimSpace(strings.TrimPrefix(fileContent, "gitdir: ")), nil
}
func VerifyInGitRepo(osCommand *oscommands.OSCommand) error {
return osCommand.RunCommand("git rev-parse --git-dir")
}
func (c *GitCommand) RunCommand(formatString string, formatArgs ...interface{}) error {
_, err := c.RunCommandWithOutput(formatString, formatArgs...)
return err
}
func (c *GitCommand) RunCommandWithOutput(formatString string, formatArgs ...interface{}) (string, error) {
// TODO: have this retry logic in other places we run the command
waitTime := 50 * time.Millisecond
retryCount := 5
attempt := 0
for {
output, err := c.OSCommand.RunCommandWithOutput(formatString, formatArgs...)
if err != nil {
// if we have an error based on the index lock, we should wait a bit and then retry
if strings.Contains(output, ".git/index.lock") {
c.Log.Error(output)
c.Log.Info("index.lock prevented command from running. Retrying command after a small wait")
attempt++
time.Sleep(waitTime)
if attempt < retryCount {
continue
}
}
}
return output, err
}
}

View File

@@ -61,43 +61,6 @@ func (f fileInfoMock) Sys() interface{} {
return f.sys
}
// TestVerifyInGitRepo is a function.
func TestVerifyInGitRepo(t *testing.T) {
type scenario struct {
testName string
runCmd func(string, ...interface{}) error
test func(error)
}
scenarios := []scenario{
{
"Valid git repository",
func(string, ...interface{}) error {
return nil
},
func(err error) {
assert.NoError(t, err)
},
},
{
"Not a valid git repository",
func(string, ...interface{}) error {
return fmt.Errorf("fatal: Not a git repository (or any of the parent directories): .git")
},
func(err error) {
assert.Error(t, err)
assert.Regexp(t, `fatal: .ot a git repository \(or any of the parent directories\s?\/?\): \.git`, err.Error())
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
s.test(verifyInGitRepo(s.runCmd))
})
}
}
// TestNavigateToRepoRootDirectory is a function.
func TestNavigateToRepoRootDirectory(t *testing.T) {
type scenario struct {
@@ -233,7 +196,7 @@ func TestNewGitCommand(t *testing.T) {
},
func(gitCmd *GitCommand, err error) {
assert.Error(t, err)
assert.Regexp(t, `fatal: .ot a git repository ((\(or any of the parent directories\): \.git)|(\(or any parent up to mount point \/\)))`, err.Error())
assert.Regexp(t, `Must open lazygit in a git repository`, err.Error())
},
},
{
@@ -344,6 +307,7 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
@@ -356,6 +320,7 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
HasStagedChanges: true,
HasUnstagedChanges: false,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
@@ -368,6 +333,7 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
@@ -380,6 +346,7 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
@@ -392,6 +359,7 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: true,
HasInlineMergeConflicts: true,
@@ -456,87 +424,6 @@ func TestGitCommandCommitAmend(t *testing.T) {
assert.NoError(t, err)
}
// TestGitCommandMergeStatusFiles is a function.
func TestGitCommandMergeStatusFiles(t *testing.T) {
type scenario struct {
testName string
oldFiles []*models.File
newFiles []*models.File
test func([]*models.File)
}
scenarios := []scenario{
{
"Old file and new file are the same",
[]*models.File{},
[]*models.File{
{
Name: "new_file.txt",
},
},
func(files []*models.File) {
expected := []*models.File{
{
Name: "new_file.txt",
},
}
assert.Len(t, files, 1)
assert.EqualValues(t, expected, files)
},
},
{
"Several files to merge, with some identical",
[]*models.File{
{
Name: "new_file1.txt",
},
{
Name: "new_file2.txt",
},
{
Name: "new_file3.txt",
},
},
[]*models.File{
{
Name: "new_file4.txt",
},
{
Name: "new_file5.txt",
},
{
Name: "new_file1.txt",
},
},
func(files []*models.File) {
expected := []*models.File{
{
Name: "new_file1.txt",
},
{
Name: "new_file4.txt",
},
{
Name: "new_file5.txt",
},
}
assert.Len(t, files, 3)
assert.EqualValues(t, expected, files)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
s.test(gitCmd.MergeStatusFiles(s.oldFiles, s.newFiles, nil))
})
}
}
// TestGitCommandGetCommitDifferences is a function.
func TestGitCommandGetCommitDifferences(t *testing.T) {
type scenario struct {
@@ -600,7 +487,7 @@ func TestGitCommandRenameCommit(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"commit", "--allow-empty", "--amend", "-m", "test"}, args)
assert.EqualValues(t, []string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, args)
return secureexec.Command("echo")
}
@@ -1037,7 +924,7 @@ func TestGitCommandStageFile(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"add", "test.txt"}, args)
assert.EqualValues(t, []string{"add", "--", "test.txt"}, args)
return secureexec.Command("echo")
}
@@ -1051,7 +938,7 @@ func TestGitCommandUnstageFile(t *testing.T) {
testName string
command func(string, ...string) *exec.Cmd
test func(error)
tracked bool
reset bool
}
scenarios := []scenario{
@@ -1059,7 +946,7 @@ func TestGitCommandUnstageFile(t *testing.T) {
"Remove an untracked file from staging",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"rm", "--cached", "--force", "test.txt"}, args)
assert.EqualValues(t, []string{"rm", "--cached", "--force", "--", "test.txt"}, args)
return secureexec.Command("echo")
},
@@ -1072,7 +959,7 @@ func TestGitCommandUnstageFile(t *testing.T) {
"Remove a tracked file from staging",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"reset", "HEAD", "test.txt"}, args)
assert.EqualValues(t, []string{"reset", "HEAD", "--", "test.txt"}, args)
return secureexec.Command("echo")
},
@@ -1087,12 +974,15 @@ func TestGitCommandUnstageFile(t *testing.T) {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
s.test(gitCmd.UnStageFile("test.txt", s.tracked))
s.test(gitCmd.UnStageFile([]string{"test.txt"}, s.reset))
})
}
}
// TestGitCommandDiscardAllFileChanges is a function.
// these tests don't cover everything, in part because we already have an integration
// test which does cover everything. I don't want to unnecessarily assert on the 'how'
// when the 'what' is what matters
func TestGitCommandDiscardAllFileChanges(t *testing.T) {
type scenario struct {
testName string
@@ -1146,6 +1036,7 @@ func TestGitCommandDiscardAllFileChanges(t *testing.T) {
&models.File{
Name: "test",
Tracked: false,
Added: true,
},
func(string) error {
return fmt.Errorf("an error occurred when removing file")
@@ -1277,6 +1168,7 @@ func TestGitCommandDiscardAllFileChanges(t *testing.T) {
&models.File{
Name: "test",
Tracked: false,
Added: true,
HasStagedChanges: true,
},
func(filename string) error {
@@ -1301,6 +1193,7 @@ func TestGitCommandDiscardAllFileChanges(t *testing.T) {
&models.File{
Name: "test",
Tracked: false,
Added: true,
HasStagedChanges: false,
},
func(filename string) error {
@@ -1455,7 +1348,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", "--submodule", "--no-ext-diff", "--color=always", "--no-index", "/dev/null", "test.txt"}, args)
assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=always", "--no-index", "--", "/dev/null", "test.txt"}, args)
return secureexec.Command("echo")
},
@@ -2120,7 +2013,7 @@ func TestEditFile(t *testing.T) {
return "", nil
},
func(cmd *exec.Cmd, err error) {
assert.EqualError(t, err, "No editor defined in $VISUAL, $EDITOR, or git config")
assert.EqualError(t, err, "No editor defined in $GIT_EDITOR, $VISUAL, $EDITOR, or git config")
},
},
{

View File

@@ -4,45 +4,37 @@ import (
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
)
// GetFilesInDiff get the specified commit files
func (c *GitCommand) GetFilesInDiff(from string, to string, reverse bool, patchManager *patch.PatchManager) ([]*models.CommitFile, error) {
func (c *GitCommand) GetFilesInDiff(from string, to string, reverse bool) ([]*models.CommitFile, error) {
reverseFlag := ""
if reverse {
reverseFlag = " -R "
}
filenames, err := c.OSCommand.RunCommandWithOutput("git diff --submodule --no-ext-diff --name-status %s %s %s", reverseFlag, from, to)
filenames, err := c.RunCommandWithOutput("git diff --submodule --no-ext-diff --name-status -z --no-renames %s %s %s", reverseFlag, from, to)
if err != nil {
return nil, err
}
return c.getCommitFilesFromFilenames(filenames, to, patchManager), nil
return c.getCommitFilesFromFilenames(filenames), nil
}
// filenames string is something like "file1\nfile2\nfile3"
func (c *GitCommand) getCommitFilesFromFilenames(filenames string, parent string, patchManager *patch.PatchManager) []*models.CommitFile {
func (c *GitCommand) getCommitFilesFromFilenames(filenames string) []*models.CommitFile {
commitFiles := make([]*models.CommitFile, 0)
for _, line := range strings.Split(strings.TrimRight(filenames, "\n"), "\n") {
lines := strings.Split(strings.TrimRight(filenames, "\x00"), "\x00")
n := len(lines)
for i := 0; i < n-1; i += 2 {
// 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)
}
changeStatus := lines[i]
name := lines[i+1]
commitFiles = append(commitFiles, &models.CommitFile{
Parent: parent,
Name: name,
ChangeStatus: changeStatus,
PatchStatus: status,
})
}

View File

@@ -8,6 +8,8 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
const RENAME_SEPARATOR = " -> "
// GetStatusFiles git status files
type GetStatusFileOptions struct {
NoRenames bool
@@ -37,26 +39,35 @@ func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
change := statusString[0:2]
stagedChange := change[0:1]
unstagedChange := statusString[1:2]
filename := c.OSCommand.Unquote(statusString[3:])
name := statusString[3:]
untracked := utils.IncludesString([]string{"??", "A ", "AM"}, change)
hasNoStagedChanges := utils.IncludesString([]string{" ", "U", "?"}, stagedChange)
hasMergeConflicts := utils.IncludesString([]string{"DD", "AA", "UU", "AU", "UA", "UD", "DU"}, change)
hasInlineMergeConflicts := utils.IncludesString([]string{"UU", "AA"}, change)
previousName := ""
if strings.Contains(name, RENAME_SEPARATOR) {
split := strings.Split(name, RENAME_SEPARATOR)
name = split[1]
previousName = split[0]
}
file := &models.File{
Name: filename,
Name: name,
PreviousName: previousName,
DisplayString: statusString,
HasStagedChanges: !hasNoStagedChanges,
HasUnstagedChanges: unstagedChange != " ",
Tracked: !untracked,
Deleted: unstagedChange == "D" || stagedChange == "D",
Added: unstagedChange == "A" || untracked,
HasMergeConflicts: hasMergeConflicts,
HasInlineMergeConflicts: hasInlineMergeConflicts,
Type: c.OSCommand.FileType(filename),
Type: c.OSCommand.FileType(name),
ShortStatus: change,
}
files = append(files, file)
}
return files
}
@@ -72,40 +83,22 @@ func (c *GitCommand) GitStatus(opts GitStatusOptions) (string, error) {
noRenamesFlag = "--no-renames"
}
return c.OSCommand.RunCommandWithOutput("git status %s --porcelain %s", opts.UntrackedFilesArg, noRenamesFlag)
}
// MergeStatusFiles merge status files
func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []*models.File, selectedFile *models.File) []*models.File {
if len(oldFiles) == 0 {
return newFiles
statusLines, err := c.RunCommandWithOutput("git status %s --porcelain -z %s", opts.UntrackedFilesArg, noRenamesFlag)
if err != nil {
return "", err
}
appendedIndexes := []int{}
// retain position of files we already could see
result := []*models.File{}
for _, oldFile := range oldFiles {
for newIndex, newFile := range newFiles {
if utils.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)
}
splitLines := strings.Split(statusLines, "\x00")
// if a line starts with 'R' then the next line is the original file.
for i := 0; i < len(splitLines)-1; i++ {
original := splitLines[i]
if strings.HasPrefix(original, "R ") {
next := splitLines[i+1]
updated := "R " + next + RENAME_SEPARATOR + strings.TrimPrefix(original, "R ")
splitLines[i] = updated
splitLines = append(splitLines[0:i+1], splitLines[i+2:]...)
}
}
// append any new files to the end
for index, newFile := range newFiles {
if !utils.IncludesInt(appendedIndexes, index) {
result = append(result, newFile)
}
}
return result
return strings.Join(splitLines, "\n"), nil
}

View File

@@ -25,7 +25,7 @@ func (c *GitCommand) GetStashEntries(filterPath string) []*models.StashEntry {
return c.getUnfilteredStashEntries()
}
rawString, err := c.OSCommand.RunCommandWithOutput("git stash list --name-only")
rawString, err := c.RunCommandWithOutput("git stash list --name-only")
if err != nil {
return c.getUnfilteredStashEntries()
}

View File

@@ -2,12 +2,8 @@ package models
// CommitFile : A git commit file
type CommitFile struct {
// 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'
// TODO: rename this to Path
Name string
ChangeStatus string // e.g. 'A' for added or 'M' for modified. This is based on the result from git diff --name-status
}

View File

@@ -1,8 +1,6 @@
package models
import (
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -10,9 +8,11 @@ import (
// duplicating this for now
type File struct {
Name string
PreviousName string
HasStagedChanges bool
HasUnstagedChanges bool
Tracked bool
Added bool
Deleted bool
HasMergeConflicts bool
HasInlineMergeConflicts bool
@@ -21,15 +21,27 @@ type File struct {
ShortStatus string // e.g. 'AD', ' A', 'M ', '??'
}
// sometimes we need to deal with either a node (which contains a file) or an actual file
type IFile interface {
GetHasUnstagedChanges() bool
GetHasStagedChanges() bool
GetIsTracked() bool
GetPath() string
}
const RENAME_SEPARATOR = " -> "
func (f *File) IsRename() bool {
return strings.Contains(f.Name, RENAME_SEPARATOR)
return f.PreviousName != ""
}
// 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)
result := []string{f.Name}
if f.PreviousName != "" {
result = append(result, f.PreviousName)
}
return result
}
// returns true if the file names are the same or if a a file rename includes the filename of the other
@@ -58,3 +70,20 @@ func (f *File) SubmoduleConfig(configs []*SubmoduleConfig) *SubmoduleConfig {
return nil
}
func (f *File) GetHasUnstagedChanges() bool {
return f.HasUnstagedChanges
}
func (f *File) GetHasStagedChanges() bool {
return f.HasStagedChanges
}
func (f *File) GetIsTracked() bool {
return f.Tracked
}
func (f *File) GetPath() string {
// TODO: remove concept of name; just use path
return f.Name
}

View File

@@ -8,7 +8,6 @@ import (
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
@@ -24,14 +23,13 @@ import (
// Platform stores the os state
type Platform struct {
OS string
CatCmd string
Shell string
ShellArg string
EscapedQuote string
OpenCommand string
OpenLinkCommand string
FallbackEscapedQuote string
OS string
CatCmd []string
Shell string
ShellArg string
EscapedQuote string
OpenCommand string
OpenLinkCommand string
}
// OSCommand holds all the os commands
@@ -73,7 +71,10 @@ type RunCommandOptions struct {
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, "GIT_TERMINAL_PROMPT=0") // prevents git from prompting us for input which would freeze the program
cmd.Env = append(cmd.Env, options.EnvVars...)
return sanitisedCommandOutput(cmd.CombinedOutput())
}
@@ -97,7 +98,19 @@ func (c *OSCommand) RunCommandWithOutput(formatString string, formatArgs ...inte
cmd := c.ExecutableFromString(command)
output, err := sanitisedCommandOutput(cmd.CombinedOutput())
if err != nil {
c.Log.WithField("command", command).Error(err)
c.Log.WithField("command", command).Error(output)
}
return output, err
}
func (c *OSCommand) CatFile(filename string) (string, error) {
arr := append(c.Platform.CatCmd, filename)
cmdStr := strings.Join(arr, " ")
c.Log.WithField("command", cmdStr).Info("Cat")
cmd := c.Command(arr[0], arr[1:]...)
output, err := sanitisedCommandOutput(cmd.CombinedOutput())
if err != nil {
c.Log.WithField("command", cmdStr).Error(output)
}
return output, err
}
@@ -129,7 +142,7 @@ func (c *OSCommand) ShellCommandFromString(commandStr string) *exec.Cmd {
if c.Platform.OS == "windows" {
quotedCommand = commandStr
} else {
quotedCommand = strconv.Quote(commandStr)
quotedCommand = c.Quote(commandStr)
}
shellCommand := fmt.Sprintf("%s %s %s", c.Platform.Shell, c.Platform.ShellArg, quotedCommand)
@@ -174,6 +187,17 @@ func (c *OSCommand) RunCommand(formatString string, formatArgs ...interface{}) e
return err
}
// RunShellCommand runs shell commands i.e. 'sh -c <command>'. Good for when you
// need access to the shell
func (c *OSCommand) RunShellCommand(command string) error {
c.Log.WithField("command", command).Info("RunShellCommand")
cmd := c.Command(c.Platform.Shell, c.Platform.ShellArg, command)
_, err := sanitisedCommandOutput(cmd.CombinedOutput())
return err
}
// FileType tells us if the file is a file, directory or other
func (c *OSCommand) FileType(path string) string {
fileInfo, err := os.Stat(path)
@@ -186,16 +210,6 @@ func (c *OSCommand) FileType(path string) string {
return "file"
}
// RunDirectCommand wrapper around direct commands
func (c *OSCommand) RunDirectCommand(command string) (string, error) {
c.Log.WithField("command", command).Info("RunDirectCommand")
return sanitisedCommandOutput(
c.Command(c.Platform.Shell, c.Platform.ShellArg, command).
CombinedOutput(),
)
}
func sanitisedCommandOutput(output []byte, err error) (string, error) {
outputString := string(output)
if err != nil {
@@ -243,20 +257,24 @@ func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *ex
return cmd
}
// Quote wraps a message in platform-specific quotation marks
func (c *OSCommand) Quote(message string) string {
message = strings.Replace(message, "`", "\\`", -1)
escapedQuote := c.Platform.EscapedQuote
if strings.Contains(message, c.Platform.EscapedQuote) {
escapedQuote = c.Platform.FallbackEscapedQuote
}
return escapedQuote + message + escapedQuote
// PrepareShellSubProcess returns the pointer to a custom command
func (c *OSCommand) PrepareShellSubProcess(command string) *exec.Cmd {
return c.PrepareSubProcess(c.Platform.Shell, c.Platform.ShellArg, command)
}
// Unquote removes wrapping quotations marks if they are present
// this is needed for removing quotes from staged filenames with spaces
func (c *OSCommand) Unquote(message string) string {
return strings.Replace(message, `"`, "", -1)
// Quote wraps a message in platform-specific quotation marks
func (c *OSCommand) Quote(message string) string {
if c.Platform.OS == "windows" {
message = strings.Replace(message, `"`, `"'"'"`, -1)
message = strings.Replace(message, `\"`, `\\"`, -1)
} else {
message = strings.Replace(message, `\`, `\\`, -1)
message = strings.Replace(message, `"`, `\"`, -1)
message = strings.Replace(message, "`", "\\`", -1)
message = strings.Replace(message, "$", "\\$", -1)
}
escapedQuote := c.Platform.EscapedQuote
return escapedQuote + message + escapedQuote
}
// AppendLineToFile adds a new line in file
@@ -352,11 +370,6 @@ func (c *OSCommand) GetLazygitPath() string {
return `"` + filepath.ToSlash(ex) + `"`
}
// RunCustomCommand returns the pointer to a custom command
func (c *OSCommand) RunCustomCommand(command string) *exec.Cmd {
return c.PrepareSubProcess(c.Platform.Shell, c.Platform.ShellArg, command)
}
// PipeCommands runs a heap of commands and pipes their inputs/outputs together like A | B | C
func (c *OSCommand) PipeCommands(commandStrings ...string) error {

View File

@@ -8,13 +8,12 @@ import (
func getPlatform() *Platform {
return &Platform{
OS: runtime.GOOS,
CatCmd: "cat",
Shell: "bash",
ShellArg: "-c",
EscapedQuote: "'",
OpenCommand: "open {{filename}}",
OpenLinkCommand: "open {{link}}",
FallbackEscapedQuote: "\"",
OS: runtime.GOOS,
CatCmd: []string{"cat"},
Shell: "bash",
ShellArg: "-c",
EscapedQuote: `"`,
OpenCommand: "open {{filename}}",
OpenLinkCommand: "open {{link}}",
}
}

View File

@@ -114,6 +114,8 @@ func TestOSCommandOpenFile(t *testing.T) {
func TestOSCommandQuote(t *testing.T) {
osCommand := NewDummyOSCommand()
osCommand.Platform.OS = "linux"
actual := osCommand.Quote("hello `test`")
expected := osCommand.Platform.EscapedQuote + "hello \\`test\\`" + osCommand.Platform.EscapedQuote
@@ -129,7 +131,7 @@ func TestOSCommandQuoteSingleQuote(t *testing.T) {
actual := osCommand.Quote("hello 'test'")
expected := osCommand.Platform.FallbackEscapedQuote + "hello 'test'" + osCommand.Platform.FallbackEscapedQuote
expected := osCommand.Platform.EscapedQuote + "hello 'test'" + osCommand.Platform.EscapedQuote
assert.EqualValues(t, expected, actual)
}
@@ -142,18 +144,20 @@ func TestOSCommandQuoteDoubleQuote(t *testing.T) {
actual := osCommand.Quote(`hello "test"`)
expected := osCommand.Platform.EscapedQuote + "hello \"test\"" + osCommand.Platform.EscapedQuote
expected := osCommand.Platform.EscapedQuote + `hello \"test\"` + osCommand.Platform.EscapedQuote
assert.EqualValues(t, expected, actual)
}
// TestOSCommandUnquote is a function.
func TestOSCommandUnquote(t *testing.T) {
// TestOSCommandQuoteWindows tests the quote function for Windows
func TestOSCommandQuoteWindows(t *testing.T) {
osCommand := NewDummyOSCommand()
actual := osCommand.Unquote(`hello "test"`)
osCommand.Platform.OS = "windows"
expected := "hello test"
actual := osCommand.Quote(`hello "test"`)
expected := osCommand.Platform.EscapedQuote + `hello "'"'"test"'"'"` + osCommand.Platform.EscapedQuote
assert.EqualValues(t, expected, actual)
}

View File

@@ -2,11 +2,10 @@ package oscommands
func getPlatform() *Platform {
return &Platform{
OS: "windows",
CatCmd: "cmd /c type",
Shell: "cmd",
ShellArg: "/c",
EscapedQuote: `\"`,
FallbackEscapedQuote: "\\'",
OS: "windows",
CatCmd: []string{"cmd", "/c", "type"},
Shell: "cmd",
ShellArg: "/c",
EscapedQuote: `\"`,
}
}

View File

@@ -1,7 +1,6 @@
package patch
import (
"errors"
"sort"
"strings"
@@ -9,9 +8,11 @@ import (
"github.com/sirupsen/logrus"
)
type PatchStatus int
const (
// UNSELECTED is for when the commit file has not been added to the patch in any way
UNSELECTED = iota
UNSELECTED PatchStatus = 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
@@ -20,7 +21,7 @@ const (
)
type fileInfo struct {
mode int // one of WHOLE/PART
mode PatchStatus
includedLineIndices []int
diff string
}
@@ -81,20 +82,25 @@ func (p *PatchManager) removeFile(info *fileInfo) {
info.includedLineIndices = nil
}
func (p *PatchManager) ToggleFileWhole(filename string) error {
func (p *PatchManager) AddFileWhole(filename string) error {
info, err := p.getFileInfo(filename)
if err != nil {
return err
}
switch info.mode {
case UNSELECTED, PART:
p.addFileWhole(info)
case WHOLE:
p.removeFile(info)
default:
return errors.New("unknown file mode")
p.addFileWhole(info)
return nil
}
func (p *PatchManager) RemoveFile(filename string) error {
info, err := p.getFileInfo(filename)
if err != nil {
return err
}
p.removeFile(info)
return nil
}
@@ -216,7 +222,11 @@ func (p *PatchManager) RenderAggregatedPatchColored(plain bool) string {
return result
}
func (p *PatchManager) GetFileStatus(filename string) int {
func (p *PatchManager) GetFileStatus(filename string, parent string) PatchStatus {
if parent != p.To {
return UNSELECTED
}
info, ok := p.fileInfoMap[filename]
if !ok {
return UNSELECTED

View File

@@ -10,8 +10,10 @@ import (
"github.com/sirupsen/logrus"
)
type PatchLineKind int
const (
PATCH_HEADER = iota
PATCH_HEADER PatchLineKind = iota
COMMIT_SHA
COMMIT_DESCRIPTION
HUNK_HEADER
@@ -24,7 +26,7 @@ const (
// the job of this file is to parse a diff, find out where the hunks begin and end, which lines are stageable, and how to find the next hunk from the current position or the next stageable line from the current position.
type PatchLine struct {
Kind int
Kind PatchLineKind
Content string // something like '+ hello' (note the first character is not removed)
}
@@ -144,7 +146,7 @@ func parsePatch(patch string) ([]int, []int, []*PatchLine, error) {
pastFirstHunkHeader := false
pastCommitDescription := true
patchLines := make([]*PatchLine, len(lines))
var lineKind int
var lineKind PatchLineKind
var firstChar string
for index, line := range lines {
firstChar = " "

View File

@@ -7,19 +7,19 @@ import (
)
func (c *GitCommand) AddRemote(name string, url string) error {
return c.OSCommand.RunCommand("git remote add %s %s", name, url)
return c.RunCommand("git remote add %s %s", name, url)
}
func (c *GitCommand) RemoveRemote(name string) error {
return c.OSCommand.RunCommand("git remote remove %s", name)
return c.RunCommand("git remote remove %s", name)
}
func (c *GitCommand) RenameRemote(oldRemoteName string, newRemoteName string) error {
return c.OSCommand.RunCommand("git remote rename %s %s", oldRemoteName, newRemoteName)
return c.RunCommand("git remote rename %s %s", oldRemoteName, newRemoteName)
}
func (c *GitCommand) UpdateRemoteUrl(remoteName string, updatedUrl string) error {
return c.OSCommand.RunCommand("git remote set-url %s %s", remoteName, updatedUrl)
return c.RunCommand("git remote set-url %s %s", remoteName, updatedUrl)
}
func (c *GitCommand) DeleteRemoteBranch(remoteName string, branchName string, promptUserForCredential func(string) string) error {

View File

@@ -4,13 +4,13 @@ import "fmt"
// StashDo modify stash
func (c *GitCommand) StashDo(index int, method string) error {
return c.OSCommand.RunCommand("git stash %s stash@{%d}", method, index)
return c.RunCommand("git stash %s stash@{%d}", method, index)
}
// StashSave save stash
// TODO: before calling this, check if there is anything to save
func (c *GitCommand) StashSave(message string) error {
return c.OSCommand.RunCommand("git stash save %s", c.OSCommand.Quote(message))
return c.RunCommand("git stash save %s", c.OSCommand.Quote(message))
}
// GetStashEntryDiff stash diff
@@ -22,7 +22,7 @@ func (c *GitCommand) ShowStashEntryCmdStr(index int) string {
// shoutouts to Joe on https://stackoverflow.com/questions/14759748/stashing-only-staged-changes-in-git-is-it-possible
func (c *GitCommand) StashSaveStagedChanges(message string) error {
if err := c.OSCommand.RunCommand("git stash --keep-index"); err != nil {
if err := c.RunCommand("git stash --keep-index"); err != nil {
return err
}
@@ -30,7 +30,7 @@ func (c *GitCommand) StashSaveStagedChanges(message string) error {
return err
}
if err := c.OSCommand.RunCommand("git stash apply stash@{1}"); err != nil {
if err := c.RunCommand("git stash apply stash@{1}"); err != nil {
return err
}
@@ -38,7 +38,7 @@ func (c *GitCommand) StashSaveStagedChanges(message string) error {
return err
}
if err := c.OSCommand.RunCommand("git stash drop stash@{1}"); err != nil {
if err := c.RunCommand("git stash drop stash@{1}"); err != nil {
return err
}
@@ -48,7 +48,7 @@ func (c *GitCommand) StashSaveStagedChanges(message string) error {
files := c.GetStatusFiles(GetStatusFileOptions{})
for _, file := range files {
if file.ShortStatus == "AD" {
if err := c.UnStageFile(file.Name, false); err != nil {
if err := c.UnStageFile(file.Names(), false); err != nil {
return err
}
}

View File

@@ -69,28 +69,28 @@ func (c *GitCommand) SubmoduleStash(submodule *models.SubmoduleConfig) error {
return nil
}
return c.OSCommand.RunCommand("git -C %s stash --include-untracked", submodule.Path)
return c.RunCommand("git -C %s stash --include-untracked", submodule.Path)
}
func (c *GitCommand) SubmoduleReset(submodule *models.SubmoduleConfig) error {
return c.OSCommand.RunCommand("git submodule update --init --force %s", submodule.Path)
return c.RunCommand("git submodule update --init --force %s", submodule.Path)
}
func (c *GitCommand) SubmoduleUpdateAll() error {
// not doing an --init here because the user probably doesn't want that
return c.OSCommand.RunCommand("git submodule update --force")
return c.RunCommand("git submodule update --force")
}
func (c *GitCommand) SubmoduleDelete(submodule *models.SubmoduleConfig) error {
// based on https://gist.github.com/myusuf3/7f645819ded92bda6677
if err := c.OSCommand.RunCommand("git submodule deinit --force %s", submodule.Path); err != nil {
if err := c.RunCommand("git submodule deinit --force %s", submodule.Path); err != nil {
if strings.Contains(err.Error(), "did not match any file(s) known to git") {
if err := c.OSCommand.RunCommand("git config --file .gitmodules --remove-section submodule.%s", submodule.Name); err != nil {
if err := c.RunCommand("git config --file .gitmodules --remove-section submodule.%s", submodule.Name); err != nil {
return err
}
if err := c.OSCommand.RunCommand("git config --remove-section submodule.%s", submodule.Name); err != nil {
if err := c.RunCommand("git config --remove-section submodule.%s", submodule.Name); err != nil {
return err
}
@@ -100,7 +100,7 @@ func (c *GitCommand) SubmoduleDelete(submodule *models.SubmoduleConfig) error {
}
}
if err := c.OSCommand.RunCommand("git rm --force -r %s", submodule.Path); err != nil {
if err := c.RunCommand("git rm --force -r %s", submodule.Path); err != nil {
// if the directory isn't there then that's fine
c.Log.Error(err)
}
@@ -119,11 +119,11 @@ func (c *GitCommand) SubmoduleAdd(name string, path string, url string) error {
func (c *GitCommand) SubmoduleUpdateUrl(name string, path string, newUrl string) error {
// the set-url command is only for later git versions so we're doing it manually here
if err := c.OSCommand.RunCommand("git config --file .gitmodules submodule.%s.url %s", name, newUrl); err != nil {
if err := c.RunCommand("git config --file .gitmodules submodule.%s.url %s", name, newUrl); err != nil {
return err
}
if err := c.OSCommand.RunCommand("git submodule sync %s", path); err != nil {
if err := c.RunCommand("git submodule sync %s", path); err != nil {
return err
}
@@ -131,11 +131,11 @@ func (c *GitCommand) SubmoduleUpdateUrl(name string, path string, newUrl string)
}
func (c *GitCommand) SubmoduleInit(path string) error {
return c.OSCommand.RunCommand("git submodule init %s", path)
return c.RunCommand("git submodule init %s", path)
}
func (c *GitCommand) SubmoduleUpdate(path string) error {
return c.OSCommand.RunCommand("git submodule update --init %s", path)
return c.RunCommand("git submodule update --init %s", path)
}
func (c *GitCommand) SubmoduleBulkInitCmdStr() string {

View File

@@ -3,11 +3,11 @@ package commands
import "fmt"
func (c *GitCommand) CreateLightweightTag(tagName string, commitSha string) error {
return c.OSCommand.RunCommand("git tag %s %s", tagName, commitSha)
return c.RunCommand("git tag %s %s", tagName, commitSha)
}
func (c *GitCommand) DeleteTag(tagName string) error {
return c.OSCommand.RunCommand("git tag -d %s", tagName)
return c.RunCommand("git tag -d %s", tagName)
}
func (c *GitCommand) PushTag(remoteName string, tagName string, promptUserForCredential func(string) string) error {

View File

@@ -41,6 +41,7 @@ type AppConfigurer interface {
SaveAppState() error
SetIsNewRepo(bool)
GetIsNewRepo() bool
ReloadUserConfig() error
}
// NewAppConfig makes a new app config
@@ -203,6 +204,16 @@ func (c *AppConfig) GetUserConfigDir() string {
return c.UserConfigDir
}
func (c *AppConfig) ReloadUserConfig() error {
userConfig, err := loadUserConfigWithDefaults(c.UserConfigDir)
if err != nil {
return err
}
c.UserConfig = userConfig
return nil
}
func configFilePath(filename string) (string, error) {
folder, err := findOrCreateConfigDir()
if err != nil {

View File

@@ -35,6 +35,7 @@ type GuiConfig struct {
Theme ThemeConfig `yaml:"theme"`
CommitLength CommitLengthConfig `yaml:"commitLength"`
SkipNoStagedFilesWarning bool `yaml:"skipNoStagedFilesWarning"`
ShowFileTree bool `yaml:"showFileTree"`
}
type ThemeConfig struct {
@@ -175,6 +176,7 @@ type KeybindingFilesConfig struct {
ToggleStagedAll string `yaml:"toggleStagedAll"`
ViewResetOptions string `yaml:"viewResetOptions"`
Fetch string `yaml:"fetch"`
ToggleTreeView string `yaml:"toggleTreeView"`
}
type KeybindingBranchesConfig struct {
@@ -323,7 +325,7 @@ func GetDefaultConfig() *UserConfig {
Reporting: "undetermined",
SplashUpdatesIndex: 0,
ConfirmOnQuit: false,
QuitOnTopLevelReturn: true,
QuitOnTopLevelReturn: false,
Keybinding: KeybindingConfig{
Universal: KeybindingUniversalConfig{
Quit: "q",
@@ -379,7 +381,7 @@ func GetDefaultConfig() *UserConfig {
DiffingMenuAlt: "<c-e>",
CopyToClipboard: "<c-o>",
SubmitEditorText: "<enter>",
AppendNewline: "<tab>",
AppendNewline: "<a-enter>",
},
Status: KeybindingStatusConfig{
CheckForUpdate: "u",
@@ -398,6 +400,7 @@ func GetDefaultConfig() *UserConfig {
ToggleStagedAll: "a",
ViewResetOptions: "D",
Fetch: "f",
ToggleTreeView: "`",
},
Branches: KeybindingBranchesConfig{
CopyPullRequestURL: "<c-y>",

View File

@@ -97,10 +97,10 @@ func (gui *Gui) renderAppStatus() {
for range ticker.C {
appStatus := gui.statusManager.getStatusString()
if appStatus == "" {
gui.renderString("appStatus", "")
gui.renderString(gui.Views.AppStatus, "")
return
}
gui.renderString("appStatus", appStatus)
gui.renderString(gui.Views.AppStatus, appStatus)
}
})
}

View File

@@ -181,9 +181,12 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
// 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 {
gui.State.ContextManager.RLock()
defer gui.State.ContextManager.RUnlock()
box := &boxlayout.Box{Window: "stash"}
stashWindowAccessed := false
for _, context := range gui.State.ContextStack {
for _, context := range gui.State.ContextManager.ContextStack {
if context.GetWindowName() == "stash" {
stashWindowAccessed = true
}
@@ -278,9 +281,12 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
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]
gui.State.ContextManager.RLock()
defer gui.State.ContextManager.RUnlock()
for idx := range gui.State.ContextManager.ContextStack {
reversedIdx := len(gui.State.ContextManager.ContextStack) - 1 - idx
context := gui.State.ContextManager.ContextStack[reversedIdx]
if context.GetKind() == SIDE_CONTEXT {
return context.GetWindowName()

View File

@@ -9,8 +9,10 @@ type Dimensions struct {
Y1 int
}
type Direction int
const (
ROW = iota
ROW Direction = iota
COLUMN
)
@@ -26,10 +28,10 @@ const (
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
Direction Direction
// 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
ConditionalDirection func(width int, height int) Direction
Children []*Box
@@ -120,7 +122,7 @@ func (b *Box) isStatic() bool {
return b.Size > 0
}
func (b *Box) getDirection(width int, height int) int {
func (b *Box) getDirection(width int, height int) Direction {
if b.ConditionalDirection != nil {
return b.ConditionalDirection(width, height)
}

View File

@@ -87,7 +87,7 @@ func TestArrangeWindows(t *testing.T) {
},
{
"Box with COLUMN direction only on wide boxes with narrow box",
&Box{ConditionalDirection: func(width int, height int) int {
&Box{ConditionalDirection: func(width int, height int) Direction {
if width > 4 {
return COLUMN
} else {
@@ -111,7 +111,7 @@ func TestArrangeWindows(t *testing.T) {
},
{
"Box with COLUMN direction only on wide boxes with wide box",
&Box{ConditionalDirection: func(width int, height int) int {
&Box{ConditionalDirection: func(width int, height int) Direction {
if width > 4 {
return COLUMN
} else {

View File

@@ -4,7 +4,6 @@ import (
"fmt"
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
@@ -31,13 +30,13 @@ func (gui *Gui) handleBranchSelect() error {
var task updateTask
branch := gui.getSelectedBranch()
if branch == nil {
task = gui.createRenderStringTask(gui.Tr.NoBranchesThisRepo)
task = NewRenderStringTask(gui.Tr.NoBranchesThisRepo)
} else {
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.GetBranchGraphCmdStr(branch.Name),
)
task = gui.createRunPtyTask(cmd)
task = NewRunPtyTask(cmd)
}
return gui.refreshMainViews(refreshMainOpts{
@@ -70,7 +69,7 @@ func (gui *Gui) refreshBranches() {
}
gui.State.Branches = builder.Build()
if err := gui.postRefreshUpdate(gui.Contexts.Branches.Context); err != nil {
if err := gui.postRefreshUpdate(gui.State.Contexts.Branches); err != nil {
gui.Log.Error(err)
}
@@ -79,7 +78,7 @@ func (gui *Gui) refreshBranches() {
// specific functions
func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleBranchPress() error {
if gui.State.Panels.Branches.SelectedLineIdx == -1 {
return nil
}
@@ -90,7 +89,7 @@ func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error {
return gui.handleCheckoutRef(branch.Name, handleCheckoutRefOptions{})
}
func (gui *Gui) handleCreatePullRequestPress(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCreatePullRequestPress() error {
pullRequest := commands.NewPullRequest(gui.GitCommand)
branch := gui.getSelectedBranch()
@@ -101,7 +100,7 @@ func (gui *Gui) handleCreatePullRequestPress(g *gocui.Gui, v *gocui.View) error
return nil
}
func (gui *Gui) handleCopyPullRequestURLPress(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCopyPullRequestURLPress() error {
pullRequest := commands.NewPullRequest(gui.GitCommand)
branch := gui.getSelectedBranch()
@@ -114,7 +113,7 @@ func (gui *Gui) handleCopyPullRequestURLPress(g *gocui.Gui, v *gocui.View) error
return nil
}
func (gui *Gui) handleGitFetch(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleGitFetch() error {
if err := gui.createLoaderPanel(gui.Tr.FetchWait); err != nil {
return err
}
@@ -126,7 +125,7 @@ func (gui *Gui) handleGitFetch(g *gocui.Gui, v *gocui.View) error {
return nil
}
func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleForceCheckout() error {
branch := gui.getSelectedBranch()
message := gui.Tr.SureForceCheckout
title := gui.Tr.ForceCheckoutBranch
@@ -208,7 +207,7 @@ func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions)
})
}
func (gui *Gui) handleCheckoutByName(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCheckoutByName() error {
return gui.prompt(promptOpts{
title: gui.Tr.BranchName + ":",
findSuggestionsFunc: gui.findBranchNameSuggestions,
@@ -251,7 +250,7 @@ func (gui *Gui) createNewBranchWithName(newBranchName string) error {
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}
func (gui *Gui) handleDeleteBranch(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleDeleteBranch() error {
return gui.deleteBranch(false)
}
@@ -293,7 +292,7 @@ func (gui *Gui) deleteNamedBranch(selectedBranch *models.Branch, force bool) err
}
return gui.createErrorPanel(errMessage)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{BRANCHES}})
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{BRANCHES}})
},
})
}
@@ -328,7 +327,7 @@ func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error {
})
}
func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleMerge() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
@@ -337,7 +336,7 @@ func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error {
return gui.mergeBranchIntoCheckedOutBranch(selectedBranchName)
}
func (gui *Gui) handleRebaseOntoLocalBranch(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleRebaseOntoLocalBranch() error {
selectedBranchName := gui.getSelectedBranch().Name
return gui.handleRebaseOntoBranch(selectedBranchName)
}
@@ -369,7 +368,7 @@ func (gui *Gui) handleRebaseOntoBranch(selectedBranchName string) error {
})
}
func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleFastForward() error {
branch := gui.getSelectedBranch()
if branch == nil {
return nil
@@ -408,13 +407,13 @@ func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
} else {
err := gui.GitCommand.FastForward(branch.Name, remoteName, remoteBranchName, gui.promptUserForCredential)
gui.handleCredentialsPopup(err)
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{BRANCHES}})
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{BRANCHES}})
}
})
return nil
}
func (gui *Gui) handleCreateResetToBranchMenu(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCreateResetToBranchMenu() error {
branch := gui.getSelectedBranch()
if branch == nil {
return nil
@@ -423,15 +422,12 @@ func (gui *Gui) handleCreateResetToBranchMenu(g *gocui.Gui, v *gocui.View) error
return gui.createResetMenu(branch.Name)
}
func (gui *Gui) handleRenameBranch(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleRenameBranch() 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.prompt(promptOpts{
title: gui.Tr.NewBranchNamePrompt + " " + branch.Name + ":",
@@ -440,13 +436,21 @@ func (gui *Gui) handleRenameBranch(g *gocui.Gui, v *gocui.View) 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(newBranchName, commands.CheckoutOptions{Force: false}); err != nil {
return gui.surfaceError(err)
// need to find where the branch is now so that we can re-select it. That means we need to refetch the branches synchronously and then find our branch
gui.refreshBranches()
// now that we've got our stuff again we need to find that branch and reselect it.
for i, newBranch := range gui.State.Branches {
if newBranch.Name == newBranchName {
gui.State.Panels.Branches.SetSelectedLineIdx(i)
if err := gui.State.Contexts.Branches.HandleRender(); err != nil {
return err
}
}
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
return nil
},
})
}
@@ -474,7 +478,7 @@ func (gui *Gui) currentBranch() *models.Branch {
}
func (gui *Gui) handleNewBranchOffCurrentItem() error {
context := gui.currentSideContext()
context := gui.currentSideListContext()
item, ok := context.GetSelectedItem()
if !ok {
@@ -508,8 +512,8 @@ func (gui *Gui) handleNewBranchOffCurrentItem() error {
context.GetPanelState().SetSelectedLineIdx(0)
}
if context.GetKey() != gui.Contexts.Branches.Context.GetKey() {
if err := gui.pushContext(gui.Contexts.Branches.Context); err != nil {
if context.GetKey() != gui.State.Contexts.Branches.GetKey() {
if err := gui.pushContext(gui.State.Contexts.Branches); err != nil {
return err
}
}
@@ -550,5 +554,5 @@ func (gui *Gui) findBranchNameSuggestions(input string) []*types.Suggestion {
// sanitizedBranchName will remove all spaces in favor of a dash "-" to meet
// git's branch naming requirement.
func sanitizedBranchName(input string) string {
return strings.ReplaceAll(input, " ", "-")
return strings.Replace(input, " ", "-", -1)
}

View File

@@ -24,7 +24,7 @@ func (gui *Gui) handleCopyCommit() error {
}
// get currently selected commit, add the sha to state.
context := gui.currentSideContext()
context := gui.currentSideListContext()
if context == nil {
return nil
}
@@ -63,7 +63,7 @@ func (gui *Gui) cherryPickedCommitShaMap() map[string]bool {
}
func (gui *Gui) commitsListForContext() []*models.Commit {
context := gui.currentSideContext()
context := gui.currentSideListContext()
if context == nil {
return nil
}
@@ -104,7 +104,7 @@ func (gui *Gui) handleCopyCommitRange() error {
}
// get currently selected commit, add the sha to state.
context := gui.currentSideContext()
context := gui.currentSideListContext()
if context == nil {
return nil
}
@@ -169,7 +169,7 @@ func (gui *Gui) exitCherryPickingMode() error {
return gui.rerenderContextViewIfPresent(contextKey)
}
func (gui *Gui) rerenderContextViewIfPresent(contextKey string) error {
func (gui *Gui) rerenderContextViewIfPresent(contextKey ContextKey) error {
if contextKey == "" {
return nil
}
@@ -184,7 +184,7 @@ func (gui *Gui) rerenderContextViewIfPresent(contextKey string) error {
return nil
}
if view.Context == contextKey {
if ContextKey(view.Context) == contextKey {
if err := context.HandleRender(); err != nil {
return err
}

View File

@@ -1,34 +1,51 @@
package gui
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
)
func (gui *Gui) getSelectedCommitFile() *models.CommitFile {
func (gui *Gui) getSelectedCommitFileNode() *filetree.CommitFileNode {
selectedLine := gui.State.Panels.CommitFiles.SelectedLineIdx
if selectedLine == -1 || selectedLine > len(gui.State.CommitFiles)-1 {
if selectedLine == -1 || selectedLine > gui.State.CommitFileManager.GetItemsLength()-1 {
return nil
}
return gui.State.CommitFiles[selectedLine]
return gui.State.CommitFileManager.GetItemAtIndex(selectedLine)
}
func (gui *Gui) getSelectedCommitFile() *models.CommitFile {
node := gui.getSelectedCommitFileNode()
if node == nil {
return nil
}
return node.File
}
func (gui *Gui) getSelectedCommitFilePath() string {
node := gui.getSelectedCommitFileNode()
if node == nil {
return ""
}
return node.GetPath()
}
func (gui *Gui) handleCommitFileSelect() error {
gui.escapeLineByLinePanel()
commitFile := gui.getSelectedCommitFile()
if commitFile == nil {
node := gui.getSelectedCommitFileNode()
if node == nil {
return nil
}
to := commitFile.Parent
to := gui.State.CommitFileManager.GetParent()
from, reverse := gui.getFromAndReverseArgsForDiff(to)
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowFileDiffCmdStr(from, to, reverse, commitFile.Name, false),
gui.GitCommand.ShowFileDiffCmdStr(from, to, reverse, node.GetPath(), false),
)
task := gui.createRunPtyTask(cmd)
task := NewRunPtyTask(cmd)
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
@@ -39,25 +56,25 @@ func (gui *Gui) handleCommitFileSelect() error {
})
}
func (gui *Gui) handleCheckoutCommitFile(g *gocui.Gui, v *gocui.View) error {
file := gui.getSelectedCommitFile()
if file == nil {
func (gui *Gui) handleCheckoutCommitFile() error {
node := gui.getSelectedCommitFileNode()
if node == nil {
return nil
}
if err := gui.GitCommand.CheckoutFile(file.Parent, file.Name); err != nil {
if err := gui.GitCommand.CheckoutFile(gui.State.CommitFileManager.GetParent(), node.GetPath()); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}
func (gui *Gui) handleDiscardOldFileChange(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleDiscardOldFileChange() error {
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
return err
}
fileName := gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLineIdx].Name
fileName := gui.getSelectedCommitFileName()
return gui.ask(askOpts{
title: gui.Tr.DiscardFileChangesTitle,
@@ -77,43 +94,50 @@ func (gui *Gui) handleDiscardOldFileChange(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) refreshCommitFilesView() error {
if err := gui.handleRefreshPatchBuildingPanel(-1); err != nil {
return err
currentSideContext := gui.currentSideContext()
if currentSideContext.GetKey() == COMMIT_FILES_CONTEXT_KEY || currentSideContext.GetKey() == BRANCH_COMMITS_CONTEXT_KEY {
if err := gui.handleRefreshPatchBuildingPanel(-1); err != nil {
return err
}
}
to := gui.State.Panels.CommitFiles.refName
from, reverse := gui.getFromAndReverseArgsForDiff(to)
files, err := gui.GitCommand.GetFilesInDiff(from, to, reverse, gui.GitCommand.PatchManager)
files, err := gui.GitCommand.GetFilesInDiff(from, to, reverse)
if err != nil {
return gui.surfaceError(err)
}
gui.State.CommitFiles = files
gui.State.CommitFileManager.SetFiles(files, to)
return gui.postRefreshUpdate(gui.Contexts.CommitFiles.Context)
return gui.postRefreshUpdate(gui.State.Contexts.CommitFiles)
}
func (gui *Gui) handleOpenOldCommitFile(g *gocui.Gui, v *gocui.View) error {
file := gui.getSelectedCommitFile()
if file == nil {
func (gui *Gui) handleOpenOldCommitFile() error {
node := gui.getSelectedCommitFileNode()
if node == nil {
return nil
}
return gui.openFile(file.Name)
return gui.openFile(node.GetPath())
}
func (gui *Gui) handleEditCommitFile(g *gocui.Gui, v *gocui.View) error {
file := gui.getSelectedCommitFile()
if file == nil {
func (gui *Gui) handleEditCommitFile() error {
node := gui.getSelectedCommitFileNode()
if node == nil {
return nil
}
return gui.editFile(file.Name)
if node.File == nil {
return gui.createErrorPanel(gui.Tr.ErrCannotEditDirectory)
}
return gui.editFile(node.GetPath())
}
func (gui *Gui) handleToggleFileForPatch(g *gocui.Gui, v *gocui.View) error {
commitFile := gui.getSelectedCommitFile()
if commitFile == nil {
func (gui *Gui) handleToggleFileForPatch() error {
node := gui.getSelectedCommitFileNode()
if node == nil {
return nil
}
@@ -124,18 +148,32 @@ func (gui *Gui) handleToggleFileForPatch(g *gocui.Gui, v *gocui.View) error {
}
}
if err := gui.GitCommand.PatchManager.ToggleFileWhole(commitFile.Name); err != nil {
return err
// if there is any file that hasn't been fully added we'll fully add everything,
// otherwise we'll remove everything
adding := node.AnyFile(func(file *models.CommitFile) bool {
return gui.GitCommand.PatchManager.GetFileStatus(file.Name, gui.State.CommitFileManager.GetParent()) != patch.WHOLE
})
err := node.ForEachFile(func(file *models.CommitFile) error {
if adding {
return gui.GitCommand.PatchManager.AddFileWhole(file.Name)
} else {
return gui.GitCommand.PatchManager.RemoveFile(file.Name)
}
})
if err != nil {
return gui.surfaceError(err)
}
if gui.GitCommand.PatchManager.IsEmpty() {
gui.GitCommand.PatchManager.Reset()
}
return gui.refreshCommitFilesView()
return gui.postRefreshUpdate(gui.State.Contexts.CommitFiles)
}
if gui.GitCommand.PatchManager.Active() && gui.GitCommand.PatchManager.To != commitFile.Parent {
if gui.GitCommand.PatchManager.Active() && gui.GitCommand.PatchManager.To != gui.State.CommitFileManager.GetParent() {
return gui.ask(askOpts{
title: gui.Tr.DiscardPatch,
prompt: gui.Tr.DiscardPatchConfirm,
@@ -159,16 +197,20 @@ func (gui *Gui) startPatchManager() error {
return nil
}
func (gui *Gui) handleEnterCommitFile(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleEnterCommitFile() error {
return gui.enterCommitFile(-1)
}
func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
commitFile := gui.getSelectedCommitFile()
if commitFile == nil {
node := gui.getSelectedCommitFileNode()
if node == nil {
return nil
}
if node.File == nil {
return gui.handleToggleCommitFileDirCollapsed()
}
enterTheFile := func(selectedLineIdx int) error {
if !gui.GitCommand.PatchManager.Active() {
if err := gui.startPatchManager(); err != nil {
@@ -176,13 +218,13 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
}
}
if err := gui.pushContext(gui.Contexts.PatchBuilding.Context); err != nil {
if err := gui.pushContext(gui.State.Contexts.PatchBuilding); err != nil {
return err
}
return gui.handleRefreshPatchBuildingPanel(selectedLineIdx)
}
if gui.GitCommand.PatchManager.Active() && gui.GitCommand.PatchManager.To != commitFile.Parent {
if gui.GitCommand.PatchManager.Active() && gui.GitCommand.PatchManager.To != gui.State.CommitFileManager.GetParent() {
return gui.ask(askOpts{
title: gui.Tr.DiscardPatch,
prompt: gui.Tr.DiscardPatchConfirm,
@@ -192,7 +234,7 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
return enterTheFile(selectedLineIdx)
},
handleClose: func() error {
return gui.pushContext(gui.Contexts.CommitFiles.Context)
return gui.pushContext(gui.State.Contexts.CommitFiles)
},
})
}
@@ -200,20 +242,60 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
return enterTheFile(selectedLineIdx)
}
func (gui *Gui) handleToggleCommitFileDirCollapsed() error {
node := gui.getSelectedCommitFileNode()
if node == nil {
return nil
}
gui.State.CommitFileManager.ToggleCollapsed(node.GetPath())
if err := gui.postRefreshUpdate(gui.State.Contexts.CommitFiles); err != nil {
gui.Log.Error(err)
}
return nil
}
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.resetWindowForView(gui.Views.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)
gui.State.Contexts.CommitFiles.SetParentContext(context)
gui.State.Contexts.CommitFiles.SetWindowName(windowName)
if err := gui.refreshCommitFilesView(); err != nil {
return err
}
return gui.pushContext(gui.Contexts.CommitFiles.Context)
return gui.pushContext(gui.State.Contexts.CommitFiles)
}
// NOTE: this is very similar to handleToggleFileTreeView, could be DRY'd with generics
func (gui *Gui) handleToggleCommitFileTreeView() error {
path := gui.getSelectedCommitFilePath()
gui.State.CommitFileManager.ToggleShowTree()
// find that same node in the new format and move the cursor to it
if path != "" {
gui.State.CommitFileManager.ExpandToPath(path)
index, found := gui.State.CommitFileManager.GetIndexForPath(path)
if found {
gui.State.Contexts.CommitFiles.GetPanelState().SetSelectedLineIdx(index)
}
}
if err := gui.State.Contexts.CommitFiles.HandleRender(); err != nil {
return err
}
if err := gui.State.Contexts.CommitFiles.HandleFocus(); err != nil {
return err
}
return nil
}

View File

@@ -11,23 +11,19 @@ import (
// runSyncOrAsyncCommand takes the output of a command that may have returned
// either no error, an error, or a subprocess to execute, and if a subprocess
// needs to be set on the gui object, it does so, and then returns the error
// the bool returned tells us whether the calling code should continue
// needs to be run, it runs it
func (gui *Gui) runSyncOrAsyncCommand(sub *exec.Cmd, err error) (bool, error) {
if err != nil {
if err != gui.Errors.ErrSubProcess {
return false, gui.surfaceError(err)
}
return false, gui.surfaceError(err)
}
if sub != nil {
gui.SubProcess = sub
return false, gui.Errors.ErrSubProcess
return false, gui.runSubprocessWithSuspense(sub)
}
return true, nil
}
func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
message := gui.trimmedContent(v)
func (gui *Gui) handleCommitConfirm() error {
message := gui.trimmedContent(gui.Views.CommitMessage)
if message == "" {
return gui.createErrorPanel(gui.Tr.CommitWithoutMessageErr)
}
@@ -44,12 +40,12 @@ func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
return nil
}
gui.clearEditorView(v)
gui.clearEditorView(gui.Views.CommitMessage)
_ = gui.returnFromContext()
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}
func (gui *Gui) handleCommitClose(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCommitClose() error {
return gui.returnFromContext()
}
@@ -63,7 +59,7 @@ func (gui *Gui) handleCommitMessageFocused() error {
},
)
gui.renderString("options", message)
gui.renderString(gui.Views.Options, message)
return nil
}
@@ -76,6 +72,6 @@ func (gui *Gui) RenderCommitLength() {
if !gui.Config.GetUserConfig().Gui.CommitLength.Show {
return
}
v := gui.getCommitMessageView()
v.Subtitle = gui.getBufferLength(v)
gui.Views.CommitMessage.Subtitle = gui.getBufferLength(gui.Views.CommitMessage)
}

View File

@@ -3,7 +3,6 @@ package gui
import (
"sync"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/utils"
@@ -13,7 +12,7 @@ import (
func (gui *Gui) getSelectedLocalCommit() *models.Commit {
selectedLine := gui.State.Panels.Commits.SelectedLineIdx
if selectedLine == -1 {
if selectedLine == -1 || selectedLine > len(gui.State.Commits)-1 {
return nil
}
@@ -36,12 +35,12 @@ func (gui *Gui) handleCommitSelect() error {
var task updateTask
commit := gui.getSelectedLocalCommit()
if commit == nil {
task = gui.createRenderStringTask(gui.Tr.NoCommitsThisBranch)
task = NewRenderStringTask(gui.Tr.NoCommitsThisBranch)
} else {
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.Modes.Filtering.Path),
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.Modes.Filtering.GetPath()),
)
task = gui.createRunPtyTask(cmd)
task = NewRunPtyTask(cmd)
}
return gui.refreshMainViews(refreshMainOpts{
@@ -87,7 +86,7 @@ func (gui *Gui) refreshCommits() error {
go utils.Safe(func() {
_ = gui.refreshCommitsWithLimit()
context, ok := gui.Contexts.CommitFiles.Context.GetParentContext()
context, ok := gui.State.Contexts.CommitFiles.GetParentContext()
if ok && context.GetKey() == BRANCH_COMMITS_CONTEXT_KEY {
// This makes sense when we've e.g. just amended a commit, meaning we get a new commit SHA at the same position.
// However if we've just added a brand new commit, it pushes the list down by one and so we would end up
@@ -118,7 +117,7 @@ func (gui *Gui) refreshCommitsWithLimit() error {
commits, err := builder.GetCommits(
commands.GetCommitsOptions{
Limit: gui.State.Panels.Commits.LimitCommits,
FilterPath: gui.State.Modes.Filtering.Path,
FilterPath: gui.State.Modes.Filtering.GetPath(),
IncludeRebaseCommits: true,
RefName: "HEAD",
},
@@ -128,7 +127,7 @@ func (gui *Gui) refreshCommitsWithLimit() error {
}
gui.State.Commits = commits
return gui.postRefreshUpdate(gui.Contexts.BranchCommits.Context)
return gui.postRefreshUpdate(gui.State.Contexts.BranchCommits)
}
func (gui *Gui) refreshRebaseCommits() error {
@@ -143,12 +142,12 @@ func (gui *Gui) refreshRebaseCommits() error {
}
gui.State.Commits = updatedCommits
return gui.postRefreshUpdate(gui.Contexts.BranchCommits.Context)
return gui.postRefreshUpdate(gui.State.Contexts.BranchCommits)
}
// specific functions
func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCommitSquashDown() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
@@ -177,7 +176,7 @@ func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
})
}
func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCommitFixup() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
@@ -206,7 +205,7 @@ func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
})
}
func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleRenameCommit() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
@@ -246,7 +245,7 @@ func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
})
}
func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleRenameCommitEditor() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
@@ -264,8 +263,7 @@ func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
return gui.surfaceError(err)
}
if subProcess != nil {
gui.SubProcess = subProcess
return gui.Errors.ErrSubProcess
return gui.runSubprocessWithSuspense(subProcess)
}
return nil
@@ -295,7 +293,7 @@ func (gui *Gui) handleMidRebaseCommand(action string) (bool, error) {
return true, gui.refreshRebaseCommits()
}
func (gui *Gui) handleCommitDelete(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCommitDelete() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
@@ -320,7 +318,7 @@ func (gui *Gui) handleCommitDelete(g *gocui.Gui, v *gocui.View) error {
})
}
func (gui *Gui) handleCommitMoveDown(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCommitMoveDown() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
@@ -347,7 +345,7 @@ func (gui *Gui) handleCommitMoveDown(g *gocui.Gui, v *gocui.View) error {
})
}
func (gui *Gui) handleCommitMoveUp(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCommitMoveUp() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
@@ -374,7 +372,7 @@ func (gui *Gui) handleCommitMoveUp(g *gocui.Gui, v *gocui.View) error {
})
}
func (gui *Gui) handleCommitEdit(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCommitEdit() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
@@ -393,7 +391,7 @@ func (gui *Gui) handleCommitEdit(g *gocui.Gui, v *gocui.View) error {
})
}
func (gui *Gui) handleCommitAmendTo(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCommitAmendTo() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
@@ -410,7 +408,7 @@ func (gui *Gui) handleCommitAmendTo(g *gocui.Gui, v *gocui.View) error {
})
}
func (gui *Gui) handleCommitPick(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCommitPick() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
@@ -425,10 +423,10 @@ func (gui *Gui) handleCommitPick(g *gocui.Gui, v *gocui.View) error {
// at this point we aren't actually rebasing so we will interpret this as an
// attempt to pull. We might revoke this later after enabling configurable keybindings
return gui.handlePullFiles(g, v)
return gui.handlePullFiles()
}
func (gui *Gui) handleCommitRevert(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCommitRevert() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
@@ -437,7 +435,7 @@ func (gui *Gui) handleCommitRevert(g *gocui.Gui, v *gocui.View) error {
return gui.surfaceError(err)
}
gui.State.Panels.Commits.SelectedLineIdx++
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI, scope: []int{COMMITS, BRANCHES}})
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI, scope: []RefreshableView{COMMITS, BRANCHES}})
}
func (gui *Gui) handleViewCommitFiles() error {
@@ -446,10 +444,10 @@ func (gui *Gui) handleViewCommitFiles() error {
return nil
}
return gui.switchToCommitFilesContext(commit.Sha, true, gui.Contexts.BranchCommits.Context, "commits")
return gui.switchToCommitFilesContext(commit.Sha, true, gui.State.Contexts.BranchCommits, "commits")
}
func (gui *Gui) handleCreateFixupCommit(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCreateFixupCommit() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
@@ -479,7 +477,7 @@ func (gui *Gui) handleCreateFixupCommit(g *gocui.Gui, v *gocui.View) error {
})
}
func (gui *Gui) handleSquashAllAboveFixupCommits(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleSquashAllAboveFixupCommits() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
@@ -508,7 +506,7 @@ func (gui *Gui) handleSquashAllAboveFixupCommits(g *gocui.Gui, v *gocui.View) er
})
}
func (gui *Gui) handleTagCommit(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleTagCommit() 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
@@ -527,12 +525,12 @@ func (gui *Gui) handleCreateLightweightTag(commitSha string) error {
if err := gui.GitCommand.CreateLightweightTag(response, commitSha); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{COMMITS, TAGS}})
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{COMMITS, TAGS}})
},
})
}
func (gui *Gui) handleCheckoutCommit(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCheckoutCommit() error {
commit := gui.getSelectedLocalCommit()
if commit == nil {
return nil
@@ -547,7 +545,7 @@ func (gui *Gui) handleCheckoutCommit(g *gocui.Gui, v *gocui.View) error {
})
}
func (gui *Gui) handleCreateCommitResetMenu(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCreateCommitResetMenu() error {
commit := gui.getSelectedLocalCommit()
if commit == nil {
return gui.createErrorPanel(gui.Tr.NoCommitsThisBranch)
@@ -556,30 +554,30 @@ func (gui *Gui) handleCreateCommitResetMenu(g *gocui.Gui, v *gocui.View) error {
return gui.createResetMenu(commit.Sha)
}
func (gui *Gui) handleOpenSearchForCommitsPanel(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleOpenSearchForCommitsPanel(_viewName string) 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: ASYNC, scope: []int{COMMITS}}); err != nil {
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{COMMITS}}); err != nil {
return err
}
}
return gui.handleOpenSearch(gui.g, v)
return gui.handleOpenSearch("commits")
}
func (gui *Gui) handleGotoBottomForCommitsPanel(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleGotoBottomForCommitsPanel() 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 {
if err := gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []RefreshableView{COMMITS}}); err != nil {
return err
}
}
for _, context := range gui.getListContexts() {
if context.ViewName == "commits" {
return context.handleGotoBottom(g, v)
return context.handleGotoBottom()
}
}

View File

@@ -108,19 +108,17 @@ func (gui *Gui) wrappedPromptConfirmationFunction(handlersManageFocus bool, func
}
}
func (gui *Gui) deleteConfirmationView() {
func (gui *Gui) clearConfirmationViewKeyBindings() {
keybindingConfig := gui.Config.GetUserConfig().Keybinding
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.Confirm), gocui.ModNone)
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone)
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.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
// we've already closed it so we can just return
if !gui.Views.Confirmation.Visible {
return nil
}
if !handlersManageFocus {
@@ -129,9 +127,9 @@ func (gui *Gui) closeConfirmationPrompt(handlersManageFocus bool) error {
}
}
gui.deleteConfirmationView()
_, _ = gui.g.SetViewOnBottom("suggestions")
gui.clearConfirmationViewKeyBindings()
gui.Views.Confirmation.Visible = false
gui.Views.Suggestions.Visible = false
return nil
}
@@ -172,67 +170,64 @@ func (gui *Gui) getConfirmationPanelDimensions(wrap bool, prompt string) (int, i
height/2 + panelHeight/2
}
func (gui *Gui) prepareConfirmationPanel(title, prompt string, hasLoader bool, findSuggestionsFunc func(string) []*types.Suggestion) (*gocui.View, error) {
func (gui *Gui) prepareConfirmationPanel(title, prompt string, hasLoader bool, findSuggestionsFunc func(string) []*types.Suggestion) error {
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(true, prompt)
confirmationView, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0)
// calling SetView on an existing view returns the same view, so I'm not bothering
// to reassign to gui.Views.Confirmation
_, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0)
if err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return nil, err
}
confirmationView.HasLoader = hasLoader
if hasLoader {
gui.g.StartTicking()
}
confirmationView.Title = title
confirmationView.Wrap = true
confirmationView.FgColor = theme.GocuiDefaultTextColor
return err
}
gui.Views.Confirmation.HasLoader = hasLoader
if hasLoader {
gui.g.StartTicking()
}
gui.Views.Confirmation.Title = title
gui.Views.Confirmation.Wrap = true
gui.Views.Confirmation.FgColor = theme.GocuiDefaultTextColor
gui.findSuggestions = findSuggestionsFunc
if findSuggestionsFunc != nil {
suggestionsViewHeight := 11
suggestionsView, err := gui.g.SetView("suggestions", x0, y1, x1, y1+suggestionsViewHeight, 0)
if err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return nil, err
}
suggestionsView.Wrap = true
suggestionsView.FgColor = theme.GocuiDefaultTextColor
return err
}
suggestionsView.Wrap = true
suggestionsView.FgColor = theme.GocuiDefaultTextColor
gui.setSuggestions([]*types.Suggestion{})
_, _ = gui.g.SetViewOnTop("suggestions")
suggestionsView.Visible = true
}
gui.g.Update(func(g *gocui.Gui) error {
return gui.pushContext(gui.Contexts.Confirmation.Context)
return gui.pushContext(gui.State.Contexts.Confirmation)
})
return confirmationView, nil
return nil
}
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 {
gui.deleteConfirmationView()
}
confirmationView, err := gui.prepareConfirmationPanel(opts.title, opts.prompt, opts.hasLoader, opts.findSuggestionsFunc)
// remove any previous keybindings
gui.clearConfirmationViewKeyBindings()
err := gui.prepareConfirmationPanel(opts.title, opts.prompt, opts.hasLoader, opts.findSuggestionsFunc)
if err != nil {
return err
}
confirmationView.Editable = opts.editable
confirmationView.Editor = gocui.EditorFunc(gui.defaultEditor)
gui.Views.Confirmation.Editable = opts.editable
gui.Views.Confirmation.Editor = gocui.EditorFunc(gui.defaultEditor)
if opts.editable {
go utils.Safe(func() {
// TODO: remove this wait (right now if you remove it the EditGotoToEndOfLine method doesn't seem to work)
time.Sleep(time.Millisecond)
gui.g.Update(func(g *gocui.Gui) error {
confirmationView.EditGotoToEndOfLine()
gui.Views.Confirmation.EditGotoToEndOfLine()
return nil
})
})
}
gui.renderString("confirmation", opts.prompt)
gui.renderString(gui.Views.Confirmation, opts.prompt)
return gui.setKeyBindings(opts)
})
@@ -248,10 +243,10 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
},
)
gui.renderString("options", actions)
gui.renderString(gui.Views.Options, actions)
var onConfirm func() error
if opts.handleConfirmPrompt != nil {
onConfirm = gui.wrappedPromptConfirmationFunction(opts.handlersManageFocus, opts.handleConfirmPrompt, func() string { return gui.getConfirmationView().Buffer() })
onConfirm = gui.wrappedPromptConfirmationFunction(opts.handlersManageFocus, opts.handleConfirmPrompt, func() string { return gui.Views.Confirmation.Buffer() })
} else {
onConfirm = gui.wrappedConfirmationFunction(opts.handlersManageFocus, opts.handleConfirm)
}
@@ -284,7 +279,7 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
{
viewName: "confirmation",
key: gui.getKey(keybindingConfig.Universal.TogglePanel),
handler: func() error { return gui.replaceContext(gui.Contexts.Suggestions.Context) },
handler: func() error { return gui.replaceContext(gui.State.Contexts.Suggestions) },
},
{
viewName: "suggestions",
@@ -304,7 +299,7 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
{
viewName: "suggestions",
key: gui.getKey(keybindingConfig.Universal.TogglePanel),
handler: func() error { return gui.replaceContext(gui.Contexts.Confirmation.Context) },
handler: func() error { return gui.replaceContext(gui.State.Contexts.Confirmation) },
},
}
@@ -317,6 +312,12 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
return nil
}
func (gui *Gui) wrappedHandler(f func() error) func(g *gocui.Gui, v *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
return f()
}
}
func (gui *Gui) createErrorPanel(message string) error {
colorFunction := color.New(color.FgRed).SprintFunc()
coloredMessage := colorFunction(strings.TrimSpace(message))
@@ -335,11 +336,5 @@ func (gui *Gui) surfaceError(err error) error {
return nil
}
for _, sentinelError := range gui.sentinelErrorsArr() {
if err == sentinelError {
return err
}
}
return gui.createErrorPanel(err.Error())
}

View File

@@ -6,39 +6,43 @@ import (
"github.com/jesseduffield/gocui"
)
type ContextKind int
const (
SIDE_CONTEXT int = iota
SIDE_CONTEXT ContextKind = iota
MAIN_CONTEXT
TEMPORARY_POPUP
PERSISTENT_POPUP
)
type ContextKey string
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 = "search"
COMMIT_MESSAGE_CONTEXT_KEY = "commitMessage"
SUBMODULES_CONTEXT_KEY = "submodules"
SUGGESTIONS_CONTEXT_KEY = "suggestions"
STATUS_CONTEXT_KEY ContextKey = "status"
FILES_CONTEXT_KEY ContextKey = "files"
LOCAL_BRANCHES_CONTEXT_KEY ContextKey = "localBranches"
REMOTES_CONTEXT_KEY ContextKey = "remotes"
REMOTE_BRANCHES_CONTEXT_KEY ContextKey = "remoteBranches"
TAGS_CONTEXT_KEY ContextKey = "tags"
BRANCH_COMMITS_CONTEXT_KEY ContextKey = "commits"
REFLOG_COMMITS_CONTEXT_KEY ContextKey = "reflogCommits"
SUB_COMMITS_CONTEXT_KEY ContextKey = "subCommits"
COMMIT_FILES_CONTEXT_KEY ContextKey = "commitFiles"
STASH_CONTEXT_KEY ContextKey = "stash"
MAIN_NORMAL_CONTEXT_KEY ContextKey = "normal"
MAIN_MERGING_CONTEXT_KEY ContextKey = "merging"
MAIN_PATCH_BUILDING_CONTEXT_KEY ContextKey = "patchBuilding"
MAIN_STAGING_CONTEXT_KEY ContextKey = "staging"
MENU_CONTEXT_KEY ContextKey = "menu"
CREDENTIALS_CONTEXT_KEY ContextKey = "credentials"
CONFIRMATION_CONTEXT_KEY ContextKey = "confirmation"
SEARCH_CONTEXT_KEY ContextKey = "search"
COMMIT_MESSAGE_CONTEXT_KEY ContextKey = "commitMessage"
SUBMODULES_CONTEXT_KEY ContextKey = "submodules"
SUGGESTIONS_CONTEXT_KEY ContextKey = "suggestions"
)
var allContextKeys = []string{
var allContextKeys = []ContextKey{
STATUS_CONTEXT_KEY,
FILES_CONTEXT_KEY,
LOCAL_BRANCHES_CONTEXT_KEY,
@@ -63,62 +67,54 @@ var allContextKeys = []string{
SUGGESTIONS_CONTEXT_KEY,
}
type SimpleContextNode struct {
Context Context
}
type RemotesContextNode struct {
Context Context
Branches SimpleContextNode
}
type ContextTree struct {
Status SimpleContextNode
Files SimpleContextNode
Submodules 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
Suggestions SimpleContextNode
Status Context
Files *ListContext
Submodules *ListContext
Menu *ListContext
Branches *ListContext
Remotes *ListContext
RemoteBranches *ListContext
Tags *ListContext
BranchCommits *ListContext
CommitFiles *ListContext
ReflogCommits *ListContext
SubCommits *ListContext
Stash *ListContext
Suggestions *ListContext
Normal Context
Staging Context
PatchBuilding Context
Merging Context
Credentials Context
Confirmation Context
CommitMessage Context
Search Context
}
func (gui *Gui) allContexts() []Context {
return []Context{
gui.Contexts.Status.Context,
gui.Contexts.Files.Context,
gui.Contexts.Submodules.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,
gui.Contexts.Suggestions.Context,
gui.State.Contexts.Status,
gui.State.Contexts.Files,
gui.State.Contexts.Submodules,
gui.State.Contexts.Branches,
gui.State.Contexts.Remotes,
gui.State.Contexts.RemoteBranches,
gui.State.Contexts.Tags,
gui.State.Contexts.BranchCommits,
gui.State.Contexts.CommitFiles,
gui.State.Contexts.ReflogCommits,
gui.State.Contexts.Stash,
gui.State.Contexts.Menu,
gui.State.Contexts.Confirmation,
gui.State.Contexts.Credentials,
gui.State.Contexts.CommitMessage,
gui.State.Contexts.Normal,
gui.State.Contexts.Staging,
gui.State.Contexts.Merging,
gui.State.Contexts.PatchBuilding,
gui.State.Contexts.SubCommits,
gui.State.Contexts.Suggestions,
}
}
@@ -126,11 +122,11 @@ type Context interface {
HandleFocus() error
HandleFocusLost() error
HandleRender() error
GetKind() int
GetKind() ContextKind
GetViewName() string
GetWindowName() string
SetWindowName(string)
GetKey() string
GetKey() ContextKey
SetParentContext(Context)
// we return a bool here to tell us whether or not the returned value just wraps a nil
@@ -143,8 +139,8 @@ type BasicContext struct {
OnFocusLost func() error
OnRender func() error
OnGetOptionsMap func() map[string]string
Kind int
Key string
Kind ContextKind
Key ContextKey
ViewName string
}
@@ -194,206 +190,176 @@ func (c BasicContext) HandleFocusLost() error {
return nil
}
func (c BasicContext) GetKind() int {
func (c BasicContext) GetKind() ContextKind {
return c.Kind
}
func (c BasicContext) GetKey() string {
func (c BasicContext) GetKey() ContextKey {
return c.Key
}
func (gui *Gui) contextTree() ContextTree {
return ContextTree{
Status: SimpleContextNode{
Context: BasicContext{
OnFocus: gui.handleStatusSelect,
Kind: SIDE_CONTEXT,
ViewName: "status",
Key: STATUS_CONTEXT_KEY,
Status: BasicContext{
OnFocus: gui.handleStatusSelect,
Kind: SIDE_CONTEXT,
ViewName: "status",
Key: STATUS_CONTEXT_KEY,
},
Files: gui.filesListContext(),
Submodules: gui.submodulesListContext(),
Menu: gui.menuListContext(),
Remotes: gui.remotesListContext(),
RemoteBranches: gui.remoteBranchesListContext(),
BranchCommits: gui.branchCommitsListContext(),
CommitFiles: gui.commitFilesListContext(),
ReflogCommits: gui.reflogCommitsListContext(),
SubCommits: gui.subCommitsListContext(),
Branches: gui.branchesListContext(),
Tags: gui.tagsListContext(),
Stash: gui.stashListContext(),
Normal: 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,
},
Files: SimpleContextNode{
Context: gui.filesListContext(),
},
Submodules: SimpleContextNode{
Context: gui.submodulesListContext(),
},
Menu: SimpleContextNode{
Context: gui.menuListContext(),
},
Remotes: RemotesContextNode{
Context: gui.remotesListContext(),
Branches: SimpleContextNode{
Context: gui.remoteBranchesListContext(),
Staging: 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,
},
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,
PatchBuilding: 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,
},
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,
},
Merging: BasicContext{
OnFocus: gui.refreshMergePanelWithLock,
Kind: MAIN_CONTEXT,
ViewName: "main",
Key: MAIN_MERGING_CONTEXT_KEY,
OnGetOptionsMap: gui.getMergingOptions,
},
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,
},
Credentials: BasicContext{
OnFocus: gui.handleCredentialsViewFocused,
Kind: PERSISTENT_POPUP,
ViewName: "credentials",
Key: CREDENTIALS_CONTEXT_KEY,
},
Merging: SimpleContextNode{
Context: BasicContext{
OnFocus: gui.refreshMergePanel,
Kind: MAIN_CONTEXT,
ViewName: "main",
Key: MAIN_MERGING_CONTEXT_KEY,
OnGetOptionsMap: gui.getMergingOptions,
},
Confirmation: BasicContext{
OnFocus: func() error { return nil },
Kind: TEMPORARY_POPUP,
ViewName: "confirmation",
Key: CONFIRMATION_CONTEXT_KEY,
},
Credentials: SimpleContextNode{
Context: BasicContext{
OnFocus: gui.handleCredentialsViewFocused,
Kind: PERSISTENT_POPUP,
ViewName: "credentials",
Key: CREDENTIALS_CONTEXT_KEY,
},
Suggestions: gui.suggestionsListContext(),
CommitMessage: BasicContext{
OnFocus: gui.handleCommitMessageFocused,
Kind: PERSISTENT_POPUP,
ViewName: "commitMessage",
Key: COMMIT_MESSAGE_CONTEXT_KEY,
},
Confirmation: SimpleContextNode{
Context: BasicContext{
OnFocus: func() error { return nil },
Kind: TEMPORARY_POPUP,
ViewName: "confirmation",
Key: CONFIRMATION_CONTEXT_KEY,
},
},
Suggestions: SimpleContextNode{
Context: gui.suggestionsListContext(),
},
CommitMessage: SimpleContextNode{
Context: BasicContext{
OnFocus: 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,
},
Search: BasicContext{
OnFocus: func() error { return nil },
Kind: PERSISTENT_POPUP,
ViewName: "search",
Key: SEARCH_CONTEXT_KEY,
},
}
}
func (gui *Gui) initialViewContextMap() map[string]Context {
func (tree ContextTree) 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,
"status": tree.Status,
"files": tree.Files,
"branches": tree.Branches,
"commits": tree.BranchCommits,
"commitFiles": tree.CommitFiles,
"stash": tree.Stash,
"menu": tree.Menu,
"confirmation": tree.Confirmation,
"credentials": tree.Credentials,
"commitMessage": tree.CommitMessage,
"main": tree.Normal,
"secondary": tree.Normal,
}
}
func (gui *Gui) viewTabContextMap() map[string][]tabContext {
func (gui *Gui) popupViewNames() []string {
result := []string{}
for _, context := range gui.allContexts() {
if context.GetKind() == PERSISTENT_POPUP || context.GetKind() == TEMPORARY_POPUP {
result = append(result, context.GetViewName())
}
}
return result
}
func (tree ContextTree) initialViewTabContextMap() map[string][]tabContext {
return map[string][]tabContext{
"branches": {
{
tab: "Local Branches",
contexts: []Context{gui.Contexts.Branches.Context},
contexts: []Context{tree.Branches},
},
{
tab: "Remotes",
contexts: []Context{
gui.Contexts.Remotes.Context,
gui.Contexts.Remotes.Branches.Context,
tree.Remotes,
tree.RemoteBranches,
},
},
{
tab: "Tags",
contexts: []Context{gui.Contexts.Tags.Context},
contexts: []Context{tree.Tags},
},
},
"commits": {
{
tab: "Commits",
contexts: []Context{gui.Contexts.BranchCommits.Context},
contexts: []Context{tree.BranchCommits},
},
{
tab: "Reflog",
contexts: []Context{
gui.Contexts.ReflogCommits.Context,
tree.ReflogCommits,
},
},
},
"files": {
{
tab: "Files",
contexts: []Context{gui.Contexts.Files.Context},
contexts: []Context{tree.Files},
},
{
tab: "Submodules",
contexts: []Context{
gui.Contexts.Submodules.Context,
tree.Submodules,
},
},
},
}
}
func (gui *Gui) currentContextKeyIgnoringPopups() string {
stack := gui.State.ContextStack
func (gui *Gui) currentContextKeyIgnoringPopups() ContextKey {
gui.State.ContextManager.RLock()
defer gui.State.ContextManager.RUnlock()
stack := gui.State.ContextManager.ContextStack
for i := range stack {
reversedIndex := len(stack) - 1 - i
@@ -411,11 +377,14 @@ func (gui *Gui) currentContextKeyIgnoringPopups() string {
// hitting escape: you want to go that context's parent instead.
func (gui *Gui) replaceContext(c Context) error {
gui.g.Update(func(*gocui.Gui) error {
if len(gui.State.ContextStack) == 0 {
gui.State.ContextStack = []Context{c}
gui.State.ContextManager.Lock()
defer gui.State.ContextManager.Unlock()
if len(gui.State.ContextManager.ContextStack) == 0 {
gui.State.ContextManager.ContextStack = []Context{c}
} else {
// replace the last item with the given item
gui.State.ContextStack = append(gui.State.ContextStack[0:len(gui.State.ContextStack)-1], c)
gui.State.ContextManager.ContextStack = append(gui.State.ContextManager.ContextStack[0:len(gui.State.ContextManager.ContextStack)-1], c)
}
return gui.activateContext(c)
@@ -426,28 +395,42 @@ func (gui *Gui) replaceContext(c Context) error {
func (gui *Gui) pushContext(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 gui.pushContextDirect(c)
})
return nil
}
func (gui *Gui) pushContextDirect(c Context) error {
gui.State.ContextManager.Lock()
// 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.ContextManager.ContextStack {
if stackContext.GetKey() != c.GetKey() {
if err := gui.deactivateContext(stackContext); err != nil {
gui.State.ContextManager.Unlock()
return err
}
}
}
gui.State.ContextManager.ContextStack = []Context{c}
} else if len(gui.State.ContextManager.ContextStack) == 0 || gui.State.ContextManager.ContextStack[len(gui.State.ContextManager.ContextStack)-1].GetKey() != c.GetKey() {
// Do not append if the one at the end is the same context (e.g. opening a menu from a menu)
// In that case we'll just close the menu entirely when the user hits escape.
// TODO: think about other exceptional cases
gui.State.ContextManager.ContextStack = append(gui.State.ContextManager.ContextStack, c)
}
gui.State.ContextManager.Unlock()
return gui.activateContext(c)
}
// asynchronous code idea: functions return an error via a channel, when done
// pushContextWithView 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
@@ -457,19 +440,22 @@ func (gui *Gui) pushContextWithView(viewName string) error {
func (gui *Gui) returnFromContext() error {
gui.g.Update(func(*gocui.Gui) error {
// TODO: add mutexes
gui.State.ContextManager.Lock()
if len(gui.State.ContextStack) == 1 {
if len(gui.State.ContextManager.ContextStack) == 1 {
// cannot escape from bottommost context
gui.State.ContextManager.Unlock()
return nil
}
n := len(gui.State.ContextStack) - 1
n := len(gui.State.ContextManager.ContextStack) - 1
currentContext := gui.State.ContextStack[n]
newContext := gui.State.ContextStack[n-1]
currentContext := gui.State.ContextManager.ContextStack[n]
newContext := gui.State.ContextManager.ContextStack[n-1]
gui.State.ContextStack = gui.State.ContextStack[:n]
gui.State.ContextManager.ContextStack = gui.State.ContextManager.ContextStack[:n]
gui.State.ContextManager.Unlock()
if err := gui.deactivateContext(currentContext); err != nil {
return err
@@ -482,9 +468,17 @@ func (gui *Gui) returnFromContext() error {
}
func (gui *Gui) deactivateContext(c Context) error {
view, _ := gui.g.View(c.GetViewName())
if view != nil && view.IsSearching() {
if err := gui.onSearchEscape(); err != nil {
return err
}
}
// 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 view != nil && c.GetKind() == TEMPORARY_POPUP || c.GetKind() == PERSISTENT_POPUP || c.GetKey() == COMMIT_FILES_CONTEXT_KEY {
view.Visible = false
}
if err := c.HandleFocusLost(); err != nil {
@@ -503,7 +497,7 @@ func (gui *Gui) postRefreshUpdate(c Context) error {
return nil
}
if v.Context != c.GetKey() {
if ContextKey(v.Context) != c.GetKey() {
return nil
}
@@ -523,14 +517,13 @@ func (gui *Gui) postRefreshUpdate(c Context) error {
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()
return err
}
originalViewContextKey := v.Context
originalViewContextKey := ContextKey(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)
gui.setViewAsActiveForWindow(v)
if viewName == "main" {
gui.changeMainViewsContext(c.GetKey())
@@ -541,14 +534,10 @@ func (gui *Gui) activateContext(c Context) error {
gui.setViewTabForContext(c)
if _, err := gui.g.SetCurrentView(viewName); err != nil {
// if view no longer exists, pop again
return gui.returnFromContext()
return err
}
if _, err := gui.g.SetViewOnTop(viewName); err != nil {
// if view no longer exists, pop again
return gui.returnFromContext()
}
v.Visible = true
// if the new context's view was previously displaying another context, render the new context
if originalViewContextKey != c.GetKey() {
@@ -557,7 +546,7 @@ func (gui *Gui) activateContext(c Context) error {
}
}
v.Context = c.GetKey()
v.Context = string(c.GetKey())
gui.g.Cursor = v.Editable
@@ -578,29 +567,46 @@ func (gui *Gui) activateContext(c Context) error {
return nil
}
// currently unused
// // currently unused
// func (gui *Gui) renderContextStack() string {
// result := ""
// for _, context := range gui.State.ContextStack {
// result += context.GetKey() + "\n"
// for _, context := range gui.State.ContextManager.ContextStack {
// result += string(context.GetKey()) + "\n"
// }
// return result
// }
func (gui *Gui) currentContext() Context {
if len(gui.State.ContextStack) == 0 {
gui.State.ContextManager.RLock()
defer gui.State.ContextManager.RUnlock()
if len(gui.State.ContextManager.ContextStack) == 0 {
return gui.defaultSideContext()
}
return gui.State.ContextStack[len(gui.State.ContextStack)-1]
return gui.State.ContextManager.ContextStack[len(gui.State.ContextManager.ContextStack)-1]
}
func (gui *Gui) currentSideContext() *ListContext {
stack := gui.State.ContextStack
// the status panel is not yet a list context (and may never be), so this method is not
// quite the same as currentSideContext()
func (gui *Gui) currentSideListContext() *ListContext {
context := gui.currentSideContext()
listContext, ok := context.(*ListContext)
if !ok {
return nil
}
return listContext
}
func (gui *Gui) currentSideContext() Context {
gui.State.ContextManager.RLock()
defer gui.State.ContextManager.RUnlock()
stack := gui.State.ContextManager.ContextStack
// on startup the stack can be empty so we'll return an empty string in that case
if len(stack) == 0 {
return nil
return gui.defaultSideContext()
}
// find the first context in the stack with the type of SIDE_CONTEXT
@@ -608,22 +614,22 @@ func (gui *Gui) currentSideContext() *ListContext {
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 context
}
}
return nil
return gui.defaultSideContext()
}
func (gui *Gui) defaultSideContext() Context {
return gui.Contexts.Files.Context
if gui.State.Modes.Filtering.Active() {
return gui.State.Contexts.BranchCommits
} else {
return gui.State.Contexts.Files
}
}
// remove the need to do this: always use a mapping
func (gui *Gui) setInitialViewContexts() {
// arguably we should only have our ViewContextMap and we should do away with
// contexts on views, or vice versa
@@ -634,7 +640,7 @@ func (gui *Gui) setInitialViewContexts() {
continue
}
view.Context = context.GetKey()
view.Context = string(context.GetKey())
}
}
@@ -669,13 +675,14 @@ func (gui *Gui) onViewFocusChange() error {
return nil
}
func (gui *Gui) onViewFocusLost(v *gocui.View, newView *gocui.View) error {
if v == nil {
func (gui *Gui) onViewFocusLost(oldView *gocui.View, newView *gocui.View) error {
if oldView == nil {
return nil
}
if v.IsSearching() && newView.Name() != "search" {
if err := gui.onSearchEscape(); err != nil {
if oldView == gui.Views.CommitFiles && newView != gui.Views.Main && newView != gui.Views.Secondary && newView != gui.Views.Search {
gui.resetWindowForView(gui.Views.CommitFiles)
if err := gui.deactivateContext(gui.State.Contexts.CommitFiles); err != nil {
return err
}
}
@@ -687,15 +694,15 @@ func (gui *Gui) onViewFocusLost(v *gocui.View, newView *gocui.View) error {
// 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(contextKey string) {
func (gui *Gui) changeMainViewsContext(contextKey ContextKey) {
if gui.State.MainContext == contextKey {
return
}
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
gui.Views.Main.Context = string(contextKey)
gui.Views.Secondary.Context = string(contextKey)
default:
panic(fmt.Sprintf("unknown context for main: %s", contextKey))
}
@@ -704,7 +711,7 @@ func (gui *Gui) changeMainViewsContext(contextKey string) {
}
func (gui *Gui) viewTabNames(viewName string) []string {
tabContexts := gui.ViewTabContextMap[viewName]
tabContexts := gui.State.ViewTabContextMap[viewName]
if len(tabContexts) == 0 {
return nil
@@ -720,7 +727,7 @@ func (gui *Gui) viewTabNames(viewName string) []string {
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()]
tabContexts, ok := gui.State.ViewTabContextMap[c.GetViewName()]
if !ok {
return
}
@@ -746,7 +753,7 @@ type tabContext struct {
contexts []Context
}
func (gui *Gui) mustContextForContextKey(contextKey string) Context {
func (gui *Gui) mustContextForContextKey(contextKey ContextKey) Context {
context, ok := gui.contextForContextKey(contextKey)
if !ok {
@@ -756,7 +763,7 @@ func (gui *Gui) mustContextForContextKey(contextKey string) Context {
return context
}
func (gui *Gui) contextForContextKey(contextKey string) (Context, bool) {
func (gui *Gui) contextForContextKey(contextKey ContextKey) (Context, bool) {
for _, context := range gui.allContexts() {
if context.GetKey() == contextKey {
return context, true
@@ -766,18 +773,28 @@ func (gui *Gui) contextForContextKey(contextKey string) (Context, bool) {
return nil, false
}
func (gui *Gui) rerenderView(viewName string) error {
v, err := gui.g.View(viewName)
if err != nil {
return nil
}
contextKey := v.Context
func (gui *Gui) rerenderView(view *gocui.View) error {
contextKey := ContextKey(view.Context)
context := gui.mustContextForContextKey(contextKey)
return context.HandleRender()
}
func (gui *Gui) getSideContextSelectedItemId() string {
currentSideContext := gui.currentSideListContext()
if currentSideContext == nil {
return ""
}
item, ok := currentSideContext.GetSelectedItem()
if ok {
return item.ID()
}
return ""
}
// currently unused
// func (gui *Gui) getCurrentSideView() *gocui.View {
// currentSideContext := gui.currentSideContext()
@@ -790,17 +807,11 @@ func (gui *Gui) rerenderView(viewName string) error {
// return view
// }
func (gui *Gui) getSideContextSelectedItemId() string {
currentSideContext := gui.currentSideContext()
if currentSideContext == nil {
return ""
}
item, ok := currentSideContext.GetSelectedItem()
if ok {
return item.ID()
}
return ""
}
// currently unused
// func (gui *Gui) renderContextStack() string {
// result := ""
// for _, context := range gui.State.ContextManager.ContextStack {
// result += context.GetViewName() + "\n"
// }
// return result
// }

View File

@@ -13,7 +13,7 @@ type credentials chan string
func (gui *Gui) promptUserForCredential(passOrUname string) string {
gui.credentials = make(chan string)
gui.g.Update(func(g *gocui.Gui) error {
credentialsView, _ := g.View("credentials")
credentialsView := gui.Views.Credentials
switch passOrUname {
case "username":
credentialsView.Title = gui.Tr.CredentialsUsername
@@ -26,7 +26,7 @@ func (gui *Gui) promptUserForCredential(passOrUname string) string {
credentialsView.Mask = '*'
}
if err := gui.pushContext(gui.Contexts.Credentials.Context); err != nil {
if err := gui.pushContext(gui.State.Contexts.Credentials); err != nil {
return err
}
@@ -39,10 +39,11 @@ func (gui *Gui) promptUserForCredential(passOrUname string) string {
return userInput + "\n"
}
func (gui *Gui) handleSubmitCredential(g *gocui.Gui, v *gocui.View) error {
message := gui.trimmedContent(v)
func (gui *Gui) handleSubmitCredential() error {
credentialsView := gui.Views.Credentials
message := gui.trimmedContent(credentialsView)
gui.credentials <- message
gui.clearEditorView(v)
gui.clearEditorView(credentialsView)
if err := gui.returnFromContext(); err != nil {
return err
}
@@ -50,7 +51,7 @@ func (gui *Gui) handleSubmitCredential(g *gocui.Gui, v *gocui.View) error {
return gui.refreshSidePanels(refreshOptions{})
}
func (gui *Gui) handleCloseCredentialsView(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCloseCredentialsView() error {
gui.credentials <- ""
return gui.returnFromContext()
}
@@ -66,7 +67,7 @@ func (gui *Gui) handleCredentialsViewFocused() error {
},
)
gui.renderString("options", message)
gui.renderString(gui.Views.Options, message)
return nil
}

View File

@@ -12,34 +12,38 @@ import (
)
type CustomCommandObjects struct {
SelectedLocalCommit *models.Commit
SelectedReflogCommit *models.Commit
SelectedSubCommit *models.Commit
SelectedFile *models.File
SelectedLocalBranch *models.Branch
SelectedRemoteBranch *models.RemoteBranch
SelectedRemote *models.Remote
SelectedTag *models.Tag
SelectedStashEntry *models.StashEntry
SelectedCommitFile *models.CommitFile
CheckedOutBranch *models.Branch
PromptResponses []string
SelectedLocalCommit *models.Commit
SelectedReflogCommit *models.Commit
SelectedSubCommit *models.Commit
SelectedFile *models.File
SelectedPath string
SelectedLocalBranch *models.Branch
SelectedRemoteBranch *models.RemoteBranch
SelectedRemote *models.Remote
SelectedTag *models.Tag
SelectedStashEntry *models.StashEntry
SelectedCommitFile *models.CommitFile
SelectedCommitFilePath string
CheckedOutBranch *models.Branch
PromptResponses []string
}
func (gui *Gui) resolveTemplate(templateStr string, promptResponses []string) (string, error) {
objects := CustomCommandObjects{
SelectedFile: gui.getSelectedFile(),
SelectedLocalCommit: gui.getSelectedLocalCommit(),
SelectedReflogCommit: gui.getSelectedReflogCommit(),
SelectedLocalBranch: gui.getSelectedBranch(),
SelectedRemoteBranch: gui.getSelectedRemoteBranch(),
SelectedRemote: gui.getSelectedRemote(),
SelectedTag: gui.getSelectedTag(),
SelectedStashEntry: gui.getSelectedStashEntry(),
SelectedCommitFile: gui.getSelectedCommitFile(),
SelectedSubCommit: gui.getSelectedSubCommit(),
CheckedOutBranch: gui.currentBranch(),
PromptResponses: promptResponses,
SelectedFile: gui.getSelectedFile(),
SelectedPath: gui.getSelectedPath(),
SelectedLocalCommit: gui.getSelectedLocalCommit(),
SelectedReflogCommit: gui.getSelectedReflogCommit(),
SelectedLocalBranch: gui.getSelectedBranch(),
SelectedRemoteBranch: gui.getSelectedRemoteBranch(),
SelectedRemote: gui.getSelectedRemote(),
SelectedTag: gui.getSelectedTag(),
SelectedStashEntry: gui.getSelectedStashEntry(),
SelectedCommitFile: gui.getSelectedCommitFile(),
SelectedCommitFilePath: gui.getSelectedCommitFilePath(),
SelectedSubCommit: gui.getSelectedSubCommit(),
CheckedOutBranch: gui.currentBranch(),
PromptResponses: promptResponses,
}
return utils.ResolveTemplate(templateStr, objects)
@@ -56,8 +60,7 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
}
if customCommand.Subprocess {
gui.PrepareSubProcess(cmdStr)
return nil
return gui.runSubprocessWithSuspense(gui.OSCommand.PrepareShellSubProcess(cmdStr))
}
loadingText := customCommand.LoadingText
@@ -65,9 +68,7 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
loadingText = gui.Tr.LcRunningCustomCommandStatus
}
return gui.WithWaitingStatus(loadingText, func() error {
gui.OSCommand.PrepareSubProcess(cmdStr)
if err := gui.OSCommand.RunCommand(cmdStr); err != nil {
if err := gui.OSCommand.RunShellCommand(cmdStr); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{})
@@ -175,9 +176,14 @@ func (gui *Gui) GetCustomCommandKeybindings() []*Binding {
case "":
log.Fatalf("Error parsing custom command keybindings: context not provided (use context: 'global' for the global context). Key: %s, Command: %s", customCommand.Key, customCommand.Command)
default:
context, ok := gui.contextForContextKey(customCommand.Context)
context, ok := gui.contextForContextKey(ContextKey(customCommand.Context))
// stupid golang making me build an array of strings for this.
allContextKeyStrings := make([]string, len(allContextKeys))
for i := range allContextKeys {
allContextKeyStrings[i] = string(allContextKeys[i])
}
if !ok {
log.Fatalf("Error when setting custom command keybindings: unknown context: %s. Key: %s, Command: %s.\nPermitted contexts: %s", customCommand.Context, customCommand.Key, customCommand.Command, strings.Join(allContextKeys, ", "))
log.Fatalf("Error when setting custom command keybindings: unknown context: %s. Key: %s, Command: %s.\nPermitted contexts: %s", customCommand.Context, customCommand.Key, customCommand.Command, strings.Join(allContextKeyStrings, ", "))
}
// here we assume that a given context will always belong to the same view.
// Currently this is a safe bet but it's by no means guaranteed in the long term
@@ -196,7 +202,7 @@ func (gui *Gui) GetCustomCommandKeybindings() []*Binding {
Contexts: contexts,
Key: gui.getKey(customCommand.Key),
Modifier: gocui.ModNone,
Handler: gui.wrappedHandler(gui.handleCustomCommandKeybinding(customCommand)),
Handler: gui.handleCustomCommandKeybinding(customCommand),
Description: description,
})
}

View File

@@ -3,8 +3,6 @@ package gui
import (
"fmt"
"strings"
"github.com/jesseduffield/gocui"
)
func (gui *Gui) exitDiffMode() error {
@@ -16,7 +14,7 @@ func (gui *Gui) renderDiff() error {
cmd := gui.OSCommand.ExecutableFromString(
fmt.Sprintf("git diff --submodule --no-ext-diff --color %s", gui.diffStr()),
)
task := gui.createRunPtyTask(cmd)
task := NewRunPtyTask(cmd)
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
@@ -51,7 +49,7 @@ func (gui *Gui) currentDiffTerminals() []string {
}
return nil
default:
context := gui.currentSideContext()
context := gui.currentSideListContext()
if context == nil {
return nil
}
@@ -96,17 +94,13 @@ func (gui *Gui) diffStr() string {
if file != "" {
output += " -- " + file
} else if gui.State.Modes.Filtering.Active() {
output += " -- " + gui.State.Modes.Filtering.Path
output += " -- " + gui.State.Modes.Filtering.GetPath()
}
return output
}
func (gui *Gui) handleCreateDiffingMenuPanel(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
func (gui *Gui) handleCreateDiffingMenuPanel() error {
names := gui.currentDiffTerminals()
menuItems := []*menuItem{}

View File

@@ -1,56 +1,79 @@
package gui
import (
"github.com/jesseduffield/gocui"
)
func (gui *Gui) handleCreateDiscardMenu(g *gocui.Gui, v *gocui.View) error {
file := gui.getSelectedFile()
if file == nil {
func (gui *Gui) handleCreateDiscardMenu() error {
node := gui.getSelectedFileNode()
if node == nil {
return nil
}
var menuItems []*menuItem
submodules := gui.State.Submodules
if file.IsSubmodule(submodules) {
submodule := file.SubmoduleConfig(submodules)
menuItems = []*menuItem{
{
displayString: gui.Tr.LcSubmoduleStashAndReset,
onPress: func() error {
return gui.handleResetSubmodule(submodule)
},
},
}
} else {
if node.File == nil {
menuItems = []*menuItem{
{
displayString: gui.Tr.LcDiscardAllChanges,
onPress: func() error {
if err := gui.GitCommand.DiscardAllFileChanges(file); err != nil {
if err := gui.GitCommand.DiscardAllDirChanges(node); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}})
},
},
}
if file.HasStagedChanges && file.HasUnstagedChanges {
if node.GetHasStagedChanges() && node.GetHasUnstagedChanges() {
menuItems = append(menuItems, &menuItem{
displayString: gui.Tr.LcDiscardUnstagedChanges,
onPress: func() error {
if err := gui.GitCommand.DiscardUnstagedFileChanges(file); err != nil {
if err := gui.GitCommand.DiscardUnstagedDirChanges(node); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}})
},
})
}
} else {
file := node.File
submodules := gui.State.Submodules
if file.IsSubmodule(submodules) {
submodule := file.SubmoduleConfig(submodules)
menuItems = []*menuItem{
{
displayString: gui.Tr.LcSubmoduleStashAndReset,
onPress: func() error {
return gui.handleResetSubmodule(submodule)
},
},
}
} else {
menuItems = []*menuItem{
{
displayString: gui.Tr.LcDiscardAllChanges,
onPress: func() error {
if err := gui.GitCommand.DiscardAllFileChanges(file); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}})
},
},
}
if file.HasStagedChanges && file.HasUnstagedChanges {
menuItems = append(menuItems, &menuItem{
displayString: gui.Tr.LcDiscardUnstagedChanges,
onPress: func() error {
if err := gui.GitCommand.DiscardUnstagedFileChanges(file); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}})
},
})
}
}
}
return gui.createMenu(file.Name, menuItems, createMenuOptions{showCancel: true})
return gui.createMenu(node.GetPath(), menuItems, createMenuOptions{showCancel: true})
}

View File

@@ -1,21 +1,24 @@
package gui
import (
"unicode"
"github.com/jesseduffield/gocui"
)
// we've just copy+pasted the editor from gocui to here so that we can also re-
// render the commit message length on each keypress
func (gui *Gui) commitMessageEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
func (gui *Gui) commitMessageEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
newlineKey, ok := gui.getKey(gui.Config.GetUserConfig().Keybinding.Universal.AppendNewline).(gocui.Key)
if !ok {
newlineKey = gocui.KeyTab
newlineKey = gocui.KeyAltEnter
}
matched := true
switch {
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
v.EditDelete(true)
case key == gocui.KeyDelete:
case key == gocui.KeyCtrlD || key == gocui.KeyDelete:
v.EditDelete(false)
case key == gocui.KeyArrowDown:
v.MoveCursor(0, 1, false)
@@ -37,18 +40,25 @@ func (gui *Gui) commitMessageEditor(v *gocui.View, key gocui.Key, ch rune, mod g
v.EditGotoToStartOfLine()
case key == gocui.KeyCtrlE:
v.EditGotoToEndOfLine()
default:
// TODO: see if we need all three of these conditions: maybe the final one is sufficient
case ch != 0 && mod == 0 && unicode.IsPrint(ch):
v.EditWrite(ch)
default:
matched = false
}
gui.RenderCommitLength()
return matched
}
func (gui *Gui) defaultEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
func (gui *Gui) defaultEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
matched := true
switch {
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
v.EditDelete(true)
case key == gocui.KeyDelete:
case key == gocui.KeyCtrlD || key == gocui.KeyDelete:
v.EditDelete(false)
case key == gocui.KeyArrowDown:
v.MoveCursor(0, 1, false)
@@ -68,8 +78,12 @@ func (gui *Gui) defaultEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.M
v.EditGotoToStartOfLine()
case key == gocui.KeyCtrlE:
v.EditGotoToEndOfLine()
default:
// TODO: see if we need all three of these conditions: maybe the final one is sufficient
case ch != 0 && mod == 0 && unicode.IsPrint(ch):
v.EditWrite(ch)
default:
matched = false
}
if gui.findSuggestions != nil {
@@ -77,4 +91,6 @@ func (gui *Gui) defaultEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.M
suggestions := gui.findSuggestions(input)
gui.setSuggestions(suggestions)
}
return matched
}

3
pkg/gui/errors.go Normal file
View File

@@ -0,0 +1,3 @@
package gui
const UNKNOWN_VIEW_ERROR_MSG = "unknown view"

View File

@@ -117,7 +117,7 @@ func (gui *Gui) watchFilesForChanges() {
}
// only refresh if we're not already
if !gui.State.IsRefreshingFiles {
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}})
}
// watch for errors

View File

@@ -1,12 +1,6 @@
package gui
import (
// "io"
// "io/ioutil"
// "strings"
"fmt"
"regexp"
"strings"
@@ -15,65 +9,86 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/mgutz/str"
)
// list panel functions
func (gui *Gui) getSelectedFile() *models.File {
func (gui *Gui) getSelectedFileNode() *filetree.FileNode {
selectedLine := gui.State.Panels.Files.SelectedLineIdx
if selectedLine == -1 {
return nil
}
return gui.State.Files[selectedLine]
return gui.State.FileManager.GetItemAtIndex(selectedLine)
}
func (gui *Gui) getSelectedFile() *models.File {
node := gui.getSelectedFileNode()
if node == nil {
return nil
}
return node.File
}
func (gui *Gui) getSelectedPath() string {
node := gui.getSelectedFileNode()
if node == nil {
return ""
}
return node.GetPath()
}
func (gui *Gui) selectFile(alreadySelected bool) error {
gui.getFilesView().FocusPoint(0, gui.State.Panels.Files.SelectedLineIdx)
gui.Views.Files.FocusPoint(0, gui.State.Panels.Files.SelectedLineIdx)
file := gui.getSelectedFile()
if file == nil {
node := gui.getSelectedFileNode()
if node == nil {
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: "",
task: gui.createRenderStringTask(gui.Tr.NoChangedFiles),
task: NewRenderStringTask(gui.Tr.NoChangedFiles),
},
})
}
if !alreadySelected {
// TODO: pull into update task interface
if err := gui.resetOrigin(gui.getMainView()); err != nil {
if err := gui.resetOrigin(gui.Views.Main); err != nil {
return err
}
if err := gui.resetOrigin(gui.getSecondaryView()); err != nil {
if err := gui.resetOrigin(gui.Views.Secondary); err != nil {
return err
}
gui.takeOverMergeConflictScrolling()
}
if file.HasInlineMergeConflicts {
return gui.refreshMergePanel()
if node.File != nil && node.File.HasInlineMergeConflicts {
return gui.refreshMergePanelWithLock()
}
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(file, false, !file.HasUnstagedChanges && file.HasStagedChanges)
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(node, false, !node.GetHasUnstagedChanges() && node.GetHasStagedChanges())
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
refreshOpts := refreshMainOpts{main: &viewUpdateOpts{
title: gui.Tr.UnstagedChanges,
task: gui.createRunPtyTask(cmd),
task: NewRunPtyTask(cmd),
}}
if file.HasStagedChanges && file.HasUnstagedChanges {
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(file, false, true)
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
if node.GetHasUnstagedChanges() {
if node.GetHasStagedChanges() {
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(node, false, true)
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
refreshOpts.secondary = &viewUpdateOpts{
title: gui.Tr.StagedChanges,
task: gui.createRunPtyTask(cmd),
refreshOpts.secondary = &viewUpdateOpts{
title: gui.Tr.StagedChanges,
task: NewRunPtyTask(cmd),
}
}
} else if !file.HasUnstagedChanges {
} else {
refreshOpts.main.title = gui.Tr.StagedChanges
}
@@ -88,13 +103,8 @@ func (gui *Gui) refreshFilesAndSubmodules() error {
gui.Mutexes.RefreshingFilesMutex.Unlock()
}()
selectedFile := gui.getSelectedFile()
selectedPath := gui.getSelectedPath()
filesView := gui.getFilesView()
if filesView == nil {
// if the filesView hasn't been instantiated yet we just return
return nil
}
if err := gui.refreshStateSubmoduleConfigs(); err != nil {
return err
}
@@ -103,20 +113,20 @@ func (gui *Gui) refreshFilesAndSubmodules() error {
}
gui.g.Update(func(g *gocui.Gui) error {
if err := gui.postRefreshUpdate(gui.Contexts.Submodules.Context); err != nil {
if err := gui.postRefreshUpdate(gui.State.Contexts.Submodules); err != nil {
gui.Log.Error(err)
}
if gui.getFilesView().Context == FILES_CONTEXT_KEY {
if ContextKey(gui.Views.Files.Context) == FILES_CONTEXT_KEY {
// doing this a little custom (as opposed to using gui.postRefreshUpdate) because we handle selecting the file explicitly below
if err := gui.Contexts.Files.Context.HandleRender(); err != nil {
if err := gui.State.Contexts.Files.HandleRender(); err != nil {
return err
}
}
if gui.currentContext().GetKey() == FILES_CONTEXT_KEY || (g.CurrentView() == gui.getMainView() && g.CurrentView().Context == MAIN_MERGING_CONTEXT_KEY) {
newSelectedFile := gui.getSelectedFile()
alreadySelected := selectedFile != nil && newSelectedFile != nil && newSelectedFile.Name == selectedFile.Name
if gui.currentContext().GetKey() == FILES_CONTEXT_KEY || (g.CurrentView() == gui.Views.Main && ContextKey(g.CurrentView().Context) == MAIN_MERGING_CONTEXT_KEY) {
newSelectedPath := gui.getSelectedPath()
alreadySelected := selectedPath != "" && newSelectedPath == selectedPath
if err := gui.selectFile(alreadySelected); err != nil {
return err
}
@@ -131,7 +141,7 @@ func (gui *Gui) refreshFilesAndSubmodules() error {
// specific functions
func (gui *Gui) stagedFiles() []*models.File {
files := gui.State.Files
files := gui.State.FileManager.GetAllFiles()
result := make([]*models.File, 0)
for _, file := range files {
if file.HasStagedChanges {
@@ -142,7 +152,7 @@ func (gui *Gui) stagedFiles() []*models.File {
}
func (gui *Gui) trackedFiles() []*models.File {
files := gui.State.Files
files := gui.State.FileManager.GetAllFiles()
result := make([]*models.File, 0, len(files))
for _, file := range files {
if file.Tracked {
@@ -161,16 +171,22 @@ func (gui *Gui) stageSelectedFile() error {
return gui.GitCommand.StageFile(file.Name)
}
func (gui *Gui) handleEnterFile(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleEnterFile() error {
return gui.enterFile(false, -1)
}
func (gui *Gui) enterFile(forceSecondaryFocused bool, selectedLineIdx int) error {
file := gui.getSelectedFile()
if file == nil {
node := gui.getSelectedFileNode()
if node == nil {
return nil
}
if node.File == nil {
return gui.handleToggleDirCollapsed()
}
file := node.File
submoduleConfigs := gui.State.Submodules
if file.IsSubmodule(submoduleConfigs) {
submoduleConfig := file.SubmoduleConfig(submoduleConfigs)
@@ -183,32 +199,53 @@ func (gui *Gui) enterFile(forceSecondaryFocused bool, selectedLineIdx int) error
if file.HasMergeConflicts {
return gui.createErrorPanel(gui.Tr.FileStagingRequirements)
}
_ = gui.pushContext(gui.Contexts.Staging.Context)
_ = gui.pushContext(gui.State.Contexts.Staging)
return gui.handleRefreshStagingPanel(forceSecondaryFocused, selectedLineIdx) // TODO: check if this is broken, try moving into context code
}
func (gui *Gui) handleFilePress() error {
file := gui.getSelectedFile()
if file == nil {
node := gui.getSelectedFileNode()
if node == nil {
return nil
}
if file.HasInlineMergeConflicts {
return gui.handleSwitchToMerge()
}
if node.IsLeaf() {
file := node.File
if file.HasUnstagedChanges {
if err := gui.GitCommand.StageFile(file.Name); err != nil {
return gui.surfaceError(err)
if file.HasInlineMergeConflicts {
return gui.handleSwitchToMerge()
}
if file.HasUnstagedChanges {
if err := gui.GitCommand.StageFile(file.Name); err != nil {
return gui.surfaceError(err)
}
} else {
if err := gui.GitCommand.UnStageFile(file.Names(), file.Tracked); err != nil {
return gui.surfaceError(err)
}
}
} else {
if err := gui.GitCommand.UnStageFile(file.Name, file.Tracked); err != nil {
return gui.surfaceError(err)
// if any files within have inline merge conflicts we can't stage or unstage,
// or it'll end up with those >>>>>> lines actually staged
if node.GetHasInlineMergeConflicts() {
return gui.createErrorPanel(gui.Tr.ErrStageDirWithInlineMergeConflicts)
}
if node.GetHasUnstagedChanges() {
if err := gui.GitCommand.StageFile(node.Path); err != nil {
return gui.surfaceError(err)
}
} else {
// pretty sure it doesn't matter that we're always passing true here
if err := gui.GitCommand.UnStageFile([]string{node.Path}, true); err != nil {
return gui.surfaceError(err)
}
}
}
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
if err := gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}); err != nil {
return err
}
@@ -216,7 +253,7 @@ func (gui *Gui) handleFilePress() error {
}
func (gui *Gui) allFilesStaged() bool {
for _, file := range gui.State.Files {
for _, file := range gui.State.FileManager.GetAllFiles() {
if file.HasUnstagedChanges {
return false
}
@@ -228,7 +265,7 @@ func (gui *Gui) focusAndSelectFile() error {
return gui.selectFile(false)
}
func (gui *Gui) handleStageAll(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleStageAll() error {
var err error
if gui.allFilesStaged() {
err = gui.GitCommand.UnstageAll()
@@ -239,53 +276,76 @@ func (gui *Gui) handleStageAll(g *gocui.Gui, v *gocui.View) error {
_ = gui.surfaceError(err)
}
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
if err := gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}); err != nil {
return err
}
return gui.selectFile(false)
}
func (gui *Gui) handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
file := gui.getSelectedFile()
if file == nil {
func (gui *Gui) handleIgnoreFile() error {
node := gui.getSelectedFileNode()
if node == nil {
return nil
}
if file.Name == ".gitignore" {
if node.GetPath() == ".gitignore" {
return gui.createErrorPanel("Cannot ignore .gitignore")
}
if file.Tracked {
unstageFiles := func() error {
return node.ForEachFile(func(file *models.File) error {
if file.HasStagedChanges {
if err := gui.GitCommand.UnStageFile(file.Names(), file.Tracked); err != nil {
return err
}
}
return nil
})
}
if node.GetIsTracked() {
return gui.ask(askOpts{
title: gui.Tr.IgnoreTracked,
prompt: gui.Tr.IgnoreTrackedPrompt,
handleConfirm: func() error {
if err := gui.GitCommand.Ignore(file.Name); err != nil {
// not 100% sure if this is necessary but I'll assume it is
if err := unstageFiles(); err != nil {
return err
}
if err := gui.GitCommand.RemoveTrackedFiles(file.Name); err != nil {
if err := gui.GitCommand.RemoveTrackedFiles(node.GetPath()); err != nil {
return err
}
return gui.refreshSidePanels(refreshOptions{scope: []int{FILES}})
if err := gui.GitCommand.Ignore(node.GetPath()); err != nil {
return err
}
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}})
},
})
}
if err := gui.GitCommand.Ignore(file.Name); err != nil {
if err := unstageFiles(); err != nil {
return err
}
if err := gui.GitCommand.Ignore(node.GetPath()); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{scope: []int{FILES}})
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}})
}
func (gui *Gui) handleWIPCommitPress(g *gocui.Gui, filesView *gocui.View) error {
func (gui *Gui) handleWIPCommitPress() error {
skipHookPreifx := gui.Config.GetUserConfig().Git.SkipHookPrefix
if skipHookPreifx == "" {
return gui.createErrorPanel(gui.Tr.SkipHookPrefixNotConfigured)
}
_ = gui.renderStringSync("commitMessage", skipHookPreifx)
if err := gui.getCommitMessageView().SetCursor(len(skipHookPreifx), 0); err != nil {
_ = gui.renderStringSync(gui.Views.CommitMessage, skipHookPreifx)
if err := gui.Views.CommitMessage.SetCursor(len(skipHookPreifx), 0); err != nil {
return err
}
@@ -320,11 +380,14 @@ func (gui *Gui) handleCommitPress() error {
return gui.surfaceError(err)
}
if gui.State.FileManager.GetItemsLength() == 0 {
return gui.createErrorPanel(gui.Tr.NoFilesStagedTitle)
}
if len(gui.stagedFiles()) == 0 {
return gui.promptToStageAllAndRetry(gui.handleCommitPress)
}
commitMessageView := gui.getCommitMessageView()
commitPrefixConfig := gui.commitPrefixConfigForRepo()
if commitPrefixConfig != nil {
prefixPattern := commitPrefixConfig.Pattern
@@ -334,14 +397,14 @@ func (gui *Gui) handleCommitPress() error {
return gui.createErrorPanel(fmt.Sprintf("%s: %s", gui.Tr.LcCommitPrefixPatternError, err.Error()))
}
prefix := rgx.ReplaceAllString(gui.getCheckedOutBranch().Name, prefixReplace)
gui.renderString("commitMessage", prefix)
if err := commitMessageView.SetCursor(len(prefix), 0); err != nil {
gui.renderString(gui.Views.CommitMessage, prefix)
if err := gui.Views.CommitMessage.SetCursor(len(prefix), 0); err != nil {
return err
}
}
gui.g.Update(func(g *gocui.Gui) error {
if err := gui.pushContext(gui.Contexts.CommitMessage.Context); err != nil {
if err := gui.pushContext(gui.State.Contexts.CommitMessage); err != nil {
return err
}
@@ -369,6 +432,10 @@ func (gui *Gui) promptToStageAllAndRetry(retry func() error) error {
}
func (gui *Gui) handleAmendCommitPress() error {
if gui.State.FileManager.GetItemsLength() == 0 {
return gui.createErrorPanel(gui.Tr.NoFilesStagedTitle)
}
if len(gui.stagedFiles()) == 0 {
return gui.promptToStageAllAndRetry(gui.handleAmendCommitPress)
}
@@ -399,21 +466,17 @@ func (gui *Gui) handleAmendCommitPress() error {
// handleCommitEditorPress - handle when the user wants to commit changes via
// their editor rather than via the popup panel
func (gui *Gui) handleCommitEditorPress() error {
if gui.State.FileManager.GetItemsLength() == 0 {
return gui.createErrorPanel(gui.Tr.NoFilesStagedTitle)
}
if len(gui.stagedFiles()) == 0 {
return gui.promptToStageAllAndRetry(gui.handleCommitEditorPress)
}
gui.PrepareSubProcess("git commit")
return nil
}
// PrepareSubProcess - prepare a subprocess for execution and tell the gui to switch to it
func (gui *Gui) PrepareSubProcess(command string) {
splitCmd := str.ToArgv(command)
gui.SubProcess = gui.OSCommand.PrepareSubProcess(splitCmd[0], splitCmd[1:]...)
gui.g.Update(func(g *gocui.Gui) error {
return gui.Errors.ErrSubProcess
})
return gui.runSubprocessWithSuspense(
gui.OSCommand.PrepareSubProcess("git", "commit"),
)
}
func (gui *Gui) editFile(filename string) error {
@@ -421,58 +484,126 @@ func (gui *Gui) editFile(filename string) error {
return err
}
func (gui *Gui) handleFileEdit(g *gocui.Gui, v *gocui.View) error {
file := gui.getSelectedFile()
if file == nil {
func (gui *Gui) handleFileEdit() error {
node := gui.getSelectedFileNode()
if node == nil {
return nil
}
return gui.editFile(file.Name)
if node.File == nil {
return gui.createErrorPanel(gui.Tr.ErrCannotEditDirectory)
}
return gui.editFile(node.GetPath())
}
func (gui *Gui) handleFileOpen(g *gocui.Gui, v *gocui.View) error {
file := gui.getSelectedFile()
if file == nil {
func (gui *Gui) handleFileOpen() error {
node := gui.getSelectedFileNode()
if node == nil {
return nil
}
return gui.openFile(file.Name)
return gui.openFile(node.GetPath())
}
func (gui *Gui) handleRefreshFiles(g *gocui.Gui, v *gocui.View) error {
return gui.refreshSidePanels(refreshOptions{scope: []int{FILES}})
func (gui *Gui) handleRefreshFiles() error {
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}})
}
func (gui *Gui) refreshStateFiles() error {
state := gui.State
// 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()
selectedNode := gui.getSelectedFileNode()
prevNodes := gui.State.FileManager.GetAllItems()
prevSelectedLineIdx := gui.State.Panels.Files.SelectedLineIdx
// get files to stage
files := gui.GitCommand.GetStatusFiles(commands.GetStatusFileOptions{})
gui.State.Files = gui.GitCommand.MergeStatusFiles(gui.State.Files, files, selectedFile)
// for when you stage the old file of a rename and the new file is in a collapsed dir
state.FileManager.RWMutex.Lock()
for _, file := range files {
if selectedNode != nil && selectedNode.Path != "" && file.PreviousName == selectedNode.Path {
state.FileManager.ExpandToPath(file.Name)
}
}
state.FileManager.SetFiles(files)
state.FileManager.RWMutex.Unlock()
if err := gui.fileWatcher.addFilesToFileWatcher(files); err != nil {
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
if selectedNode != nil {
newIdx := gui.findNewSelectedIdx(prevNodes[prevSelectedLineIdx:], state.FileManager.GetAllItems())
if newIdx != -1 && newIdx != prevSelectedLineIdx {
newNode := state.FileManager.GetItemAtIndex(newIdx)
// when not in tree mode, we show merge conflict files at the top, so you
// can work through them one by one without having to sift through a large
// set of files. If you have just fixed the merge conflicts of a file, we
// actually don't want to jump to that file's new position, because that
// file will now be ages away amidst the other files without merge
// conflicts: the user in this case would rather work on the next file
// with merge conflicts, which will have moved up to fill the gap left by
// the last file, meaning the cursor doesn't need to move at all.
leaveCursor := !state.FileManager.InTreeMode() && newNode != nil &&
selectedNode.File != nil && selectedNode.File.HasMergeConflicts &&
newNode.File != nil && !newNode.File.HasMergeConflicts
if !leaveCursor {
state.Panels.Files.SelectedLineIdx = newIdx
}
}
}
gui.refreshSelectedLine(gui.State.Panels.Files, len(gui.State.Files))
gui.refreshSelectedLine(state.Panels.Files, state.FileManager.GetItemsLength())
return nil
}
func (gui *Gui) handlePullFiles(g *gocui.Gui, v *gocui.View) error {
// Let's try to find our file again and move the cursor to that.
// If we can't find our file, it was probably just removed by the user. In that
// case, we go looking for where the next file has been moved to. Given that the
// user could have removed a whole directory, we continue iterating through the old
// nodes until we find one that exists in the new set of nodes, then move the cursor
// to that.
// prevNodes starts from our previously selected node because we don't need to consider anything above that
func (gui *Gui) findNewSelectedIdx(prevNodes []*filetree.FileNode, currNodes []*filetree.FileNode) int {
getPaths := func(node *filetree.FileNode) []string {
if node == nil {
return nil
}
if node.File != nil && node.File.IsRename() {
return node.File.Names()
} else {
return []string{node.Path}
}
}
for _, prevNode := range prevNodes {
selectedPaths := getPaths(prevNode)
for idx, node := range currNodes {
paths := getPaths(node)
// If you started off with a rename selected, and now it's broken in two, we want you to jump to the new file, not the old file.
// This is because the new should be in the same position as the rename was meaning less cursor jumping
foundOldFileInRename := prevNode.File != nil && prevNode.File.IsRename() && node.Path == prevNode.File.PreviousName
foundNode := utils.StringArraysOverlap(paths, selectedPaths) && !foundOldFileInRename
if foundNode {
return idx
}
}
}
return -1
}
func (gui *Gui) handlePullFiles() error {
if gui.popupPanelFocused() {
return nil
}
@@ -563,7 +694,7 @@ func (gui *Gui) pullWithMode(mode string, opts PullFilesOptions) error {
}
}
func (gui *Gui) pushWithForceFlag(v *gocui.View, force bool, upstream string, args string) error {
func (gui *Gui) pushWithForceFlag(force bool, upstream string, args string) error {
if err := gui.createLoaderPanel(gui.Tr.PushWait); err != nil {
return err
}
@@ -580,7 +711,7 @@ func (gui *Gui) pushWithForceFlag(v *gocui.View, force bool, upstream string, ar
title: gui.Tr.ForcePush,
prompt: gui.Tr.ForcePushPrompt,
handleConfirm: func() error {
return gui.pushWithForceFlag(v, true, upstream, args)
return gui.pushWithForceFlag(true, upstream, args)
},
})
return
@@ -591,7 +722,7 @@ func (gui *Gui) pushWithForceFlag(v *gocui.View, force bool, upstream string, ar
return nil
}
func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) pushFiles() error {
if gui.popupPanelFocused() {
return nil
}
@@ -607,23 +738,23 @@ func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error {
}
for branchName, branch := range conf.Branches {
if branchName == currentBranch.Name {
return gui.pushWithForceFlag(v, false, "", fmt.Sprintf("%s %s", branch.Remote, branchName))
return gui.pushWithForceFlag(false, "", fmt.Sprintf("%s %s", branch.Remote, branchName))
}
}
if gui.GitCommand.PushToCurrent {
return gui.pushWithForceFlag(v, false, "", "--set-upstream")
return gui.pushWithForceFlag(false, "", "--set-upstream")
} else {
return gui.prompt(promptOpts{
title: gui.Tr.EnterUpstream,
initialContent: "origin " + currentBranch.Name,
handleConfirm: func(response string) error {
return gui.pushWithForceFlag(v, false, response, "")
return gui.pushWithForceFlag(false, response, "")
},
})
}
} else if currentBranch.Pullables == "0" {
return gui.pushWithForceFlag(v, false, "", "")
return gui.pushWithForceFlag(false, "", "")
}
forcePushDisabled := gui.Config.GetUserConfig().Git.DisableForcePushing
@@ -635,7 +766,7 @@ func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error {
title: gui.Tr.ForcePush,
prompt: gui.Tr.ForcePushPrompt,
handleConfirm: func() error {
return gui.pushWithForceFlag(v, true, "", "")
return gui.pushWithForceFlag(true, "", "")
},
})
}
@@ -650,7 +781,7 @@ func (gui *Gui) handleSwitchToMerge() error {
return gui.createErrorPanel(gui.Tr.FileNoMergeCons)
}
return gui.pushContext(gui.Contexts.Merging.Context)
return gui.pushContext(gui.State.Contexts.Merging)
}
func (gui *Gui) openFile(filename string) error {
@@ -661,7 +792,7 @@ func (gui *Gui) openFile(filename string) error {
}
func (gui *Gui) anyFilesWithMergeConflicts() bool {
for _, file := range gui.State.Files {
for _, file := range gui.State.FileManager.GetAllFiles() {
if file.HasMergeConflicts {
return true
}
@@ -669,17 +800,18 @@ func (gui *Gui) anyFilesWithMergeConflicts() bool {
return false
}
func (gui *Gui) handleCustomCommand(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCustomCommand() error {
return gui.prompt(promptOpts{
title: gui.Tr.CustomCommand,
handleConfirm: func(command string) error {
gui.SubProcess = gui.OSCommand.RunCustomCommand(command)
return gui.Errors.ErrSubProcess
return gui.runSubprocessWithSuspense(
gui.OSCommand.PrepareShellSubProcess(command),
)
},
})
}
func (gui *Gui) handleCreateStashMenu(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCreateStashMenu() error {
menuItems := []*menuItem{
{
displayString: gui.Tr.LcStashAllChanges,
@@ -698,10 +830,52 @@ func (gui *Gui) handleCreateStashMenu(g *gocui.Gui, v *gocui.View) error {
return gui.createMenu(gui.Tr.LcStashOptions, menuItems, createMenuOptions{showCancel: true})
}
func (gui *Gui) handleStashChanges(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleStashChanges() error {
return gui.handleStashSave(gui.GitCommand.StashSave)
}
func (gui *Gui) handleCreateResetToUpstreamMenu(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCreateResetToUpstreamMenu() error {
return gui.createResetMenu("@{upstream}")
}
func (gui *Gui) handleToggleDirCollapsed() error {
node := gui.getSelectedFileNode()
if node == nil {
return nil
}
gui.State.FileManager.ToggleCollapsed(node.GetPath())
if err := gui.postRefreshUpdate(gui.State.Contexts.Files); err != nil {
gui.Log.Error(err)
}
return nil
}
func (gui *Gui) handleToggleFileTreeView() error {
// get path of currently selected file
path := gui.getSelectedPath()
gui.State.FileManager.ToggleShowTree()
// find that same node in the new format and move the cursor to it
if path != "" {
gui.State.FileManager.ExpandToPath(path)
index, found := gui.State.FileManager.GetIndexForPath(path)
if found {
gui.filesListContext().GetPanelState().SetSelectedLineIdx(index)
}
}
if ContextKey(gui.Views.Files.Context) == FILES_CONTEXT_KEY {
if err := gui.State.Contexts.Files.HandleRender(); err != nil {
return err
}
if err := gui.State.Contexts.Files.HandleFocus(); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,110 @@
package filetree
import (
"os"
"path/filepath"
"sort"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
)
func BuildTreeFromFiles(files []*models.File) *FileNode {
root := &FileNode{}
var curr *FileNode
for _, file := range files {
split := strings.Split(file.Name, string(os.PathSeparator))
curr = root
outer:
for i := range split {
var setFile *models.File
isFile := i == len(split)-1
if isFile {
setFile = file
}
path := filepath.Join(split[:i+1]...)
for _, existingChild := range curr.Children {
if existingChild.Path == path {
curr = existingChild
continue outer
}
}
newChild := &FileNode{
Path: path,
File: setFile,
}
curr.Children = append(curr.Children, newChild)
curr = newChild
}
}
root.Sort()
root.Compress()
return root
}
func BuildFlatTreeFromCommitFiles(files []*models.CommitFile) *CommitFileNode {
rootAux := BuildTreeFromCommitFiles(files)
sortedFiles := rootAux.GetLeaves()
return &CommitFileNode{Children: sortedFiles}
}
func BuildTreeFromCommitFiles(files []*models.CommitFile) *CommitFileNode {
root := &CommitFileNode{}
var curr *CommitFileNode
for _, file := range files {
split := strings.Split(file.Name, string(os.PathSeparator))
curr = root
outer:
for i := range split {
var setFile *models.CommitFile
isFile := i == len(split)-1
if isFile {
setFile = file
}
path := filepath.Join(split[:i+1]...)
for _, existingChild := range curr.Children {
if existingChild.Path == path {
curr = existingChild
continue outer
}
}
newChild := &CommitFileNode{
Path: path,
File: setFile,
}
curr.Children = append(curr.Children, newChild)
curr = newChild
}
}
root.Sort()
root.Compress()
return root
}
func BuildFlatTreeFromFiles(files []*models.File) *FileNode {
rootAux := BuildTreeFromFiles(files)
sortedFiles := rootAux.GetLeaves()
// Move merge conflicts to top. This is the one way in which sorting
// differs between flat mode and tree mode
sort.SliceStable(sortedFiles, func(i, j int) bool {
return sortedFiles[i].File != nil && sortedFiles[i].File.HasMergeConflicts && !(sortedFiles[j].File != nil && sortedFiles[j].File.HasMergeConflicts)
})
return &FileNode{Children: sortedFiles}
}

View File

@@ -0,0 +1,25 @@
package filetree
import (
"os"
"strings"
)
type CollapsedPaths map[string]bool
func (cp CollapsedPaths) ExpandToPath(path string) {
// need every directory along the way
split := strings.Split(path, string(os.PathSeparator))
for i := range split {
dir := strings.Join(split[0:i+1], string(os.PathSeparator))
cp[dir] = false
}
}
func (cp CollapsedPaths) IsCollapsed(path string) bool {
return cp[path]
}
func (cp CollapsedPaths) ToggleCollapsed(path string) {
cp[path] = !cp[path]
}

View File

@@ -0,0 +1,119 @@
package filetree
import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/sirupsen/logrus"
)
type CommitFileManager struct {
files []*models.CommitFile
tree *CommitFileNode
showTree bool
log *logrus.Entry
collapsedPaths CollapsedPaths
// 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
}
func (m *CommitFileManager) GetParent() string {
return m.parent
}
func NewCommitFileManager(files []*models.CommitFile, log *logrus.Entry, showTree bool) *CommitFileManager {
return &CommitFileManager{
files: files,
log: log,
showTree: showTree,
collapsedPaths: CollapsedPaths{},
}
}
func (m *CommitFileManager) ExpandToPath(path string) {
m.collapsedPaths.ExpandToPath(path)
}
func (m *CommitFileManager) ToggleShowTree() {
m.showTree = !m.showTree
m.SetTree()
}
func (m *CommitFileManager) GetItemAtIndex(index int) *CommitFileNode {
// need to traverse the three depth first until we get to the index.
return m.tree.GetNodeAtIndex(index+1, m.collapsedPaths) // ignoring root
}
func (m *CommitFileManager) GetIndexForPath(path string) (int, bool) {
index, found := m.tree.GetIndexForPath(path, m.collapsedPaths)
return index - 1, found
}
func (m *CommitFileManager) GetAllItems() []*CommitFileNode {
if m.tree == nil {
return nil
}
return m.tree.Flatten(m.collapsedPaths)[1:] // ignoring root
}
func (m *CommitFileManager) GetItemsLength() int {
return m.tree.Size(m.collapsedPaths) - 1 // ignoring root
}
func (m *CommitFileManager) GetAllFiles() []*models.CommitFile {
return m.files
}
func (m *CommitFileManager) SetFiles(files []*models.CommitFile, parent string) {
m.files = files
m.parent = parent
m.SetTree()
}
func (m *CommitFileManager) SetTree() {
if m.showTree {
m.tree = BuildTreeFromCommitFiles(m.files)
} else {
m.tree = BuildFlatTreeFromCommitFiles(m.files)
}
}
func (m *CommitFileManager) IsCollapsed(path string) bool {
return m.collapsedPaths.IsCollapsed(path)
}
func (m *CommitFileManager) ToggleCollapsed(path string) {
m.collapsedPaths.ToggleCollapsed(path)
}
func (m *CommitFileManager) Render(diffName string, patchManager *patch.PatchManager) []string {
// can't rely on renderAux to check for nil because an interface won't be nil if its concrete value is nil
if m.tree == nil {
return []string{}
}
return renderAux(m.tree, m.collapsedPaths, "", -1, func(n INode, depth int) string {
castN := n.(*CommitFileNode)
// This is a little convoluted because we're dealing with either a leaf or a non-leaf.
// But this code actually applies to both. If it's a leaf, the status will just
// be whatever status it is, but if it's a non-leaf it will determine its status
// based on the leaves of that subtree
var status patch.PatchStatus
if castN.EveryFile(func(file *models.CommitFile) bool {
return patchManager.GetFileStatus(file.Name, m.parent) == patch.WHOLE
}) {
status = patch.WHOLE
} else if castN.EveryFile(func(file *models.CommitFile) bool {
return patchManager.GetFileStatus(file.Name, m.parent) == patch.UNSELECTED
}) {
status = patch.UNSELECTED
} else {
status = patch.PART
}
return presentation.GetCommitFileLine(castN.NameAtDepth(depth), diffName, castN.File, status)
})
}

View File

@@ -0,0 +1,176 @@
package filetree
import (
"os"
"path/filepath"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
)
type CommitFileNode struct {
Children []*CommitFileNode
File *models.CommitFile
Path string // e.g. '/path/to/mydir'
CompressionLevel int // equal to the number of forward slashes you'll see in the path when it's rendered in tree mode
}
// methods satisfying ListItem interface
func (s *CommitFileNode) ID() string {
return s.GetPath()
}
func (s *CommitFileNode) Description() string {
return s.GetPath()
}
// methods satisfying INode interface
func (s *CommitFileNode) IsLeaf() bool {
return s.File != nil
}
func (s *CommitFileNode) GetPath() string {
return s.Path
}
func (s *CommitFileNode) GetChildren() []INode {
result := make([]INode, len(s.Children))
for i, child := range s.Children {
result[i] = child
}
return result
}
func (s *CommitFileNode) SetChildren(children []INode) {
castChildren := make([]*CommitFileNode, len(children))
for i, child := range children {
castChildren[i] = child.(*CommitFileNode)
}
s.Children = castChildren
}
func (s *CommitFileNode) GetCompressionLevel() int {
return s.CompressionLevel
}
func (s *CommitFileNode) SetCompressionLevel(level int) {
s.CompressionLevel = level
}
// methods utilising generic functions for INodes
func (s *CommitFileNode) Sort() {
sortNode(s)
}
func (s *CommitFileNode) ForEachFile(cb func(*models.CommitFile) error) error {
return forEachLeaf(s, func(n INode) error {
castNode := n.(*CommitFileNode)
return cb(castNode.File)
})
}
func (s *CommitFileNode) Any(test func(node *CommitFileNode) bool) bool {
return any(s, func(n INode) bool {
castNode := n.(*CommitFileNode)
return test(castNode)
})
}
func (s *CommitFileNode) Every(test func(node *CommitFileNode) bool) bool {
return every(s, func(n INode) bool {
castNode := n.(*CommitFileNode)
return test(castNode)
})
}
func (s *CommitFileNode) EveryFile(test func(file *models.CommitFile) bool) bool {
return every(s, func(n INode) bool {
castNode := n.(*CommitFileNode)
return castNode.File == nil || test(castNode.File)
})
}
func (n *CommitFileNode) Flatten(collapsedPaths map[string]bool) []*CommitFileNode {
results := flatten(n, collapsedPaths)
nodes := make([]*CommitFileNode, len(results))
for i, result := range results {
nodes[i] = result.(*CommitFileNode)
}
return nodes
}
func (node *CommitFileNode) GetNodeAtIndex(index int, collapsedPaths map[string]bool) *CommitFileNode {
if node == nil {
return nil
}
result := getNodeAtIndex(node, index, collapsedPaths)
if result == nil {
// not sure how this can be nil: we probably are missing a mutex somewhere
return nil
}
return result.(*CommitFileNode)
}
func (node *CommitFileNode) GetIndexForPath(path string, collapsedPaths map[string]bool) (int, bool) {
return getIndexForPath(node, path, collapsedPaths)
}
func (node *CommitFileNode) Size(collapsedPaths map[string]bool) int {
if node == nil {
return 0
}
return size(node, collapsedPaths)
}
func (s *CommitFileNode) Compress() {
// with these functions I try to only have type conversion code on the actual struct,
// but comparing interface values to nil is fraught with danger so I'm duplicating
// that code here.
if s == nil {
return
}
compressAux(s)
}
// This ignores the root
func (node *CommitFileNode) GetPathsMatching(test func(*CommitFileNode) bool) []string {
return getPathsMatching(node, func(n INode) bool {
return test(n.(*CommitFileNode))
})
}
func (s *CommitFileNode) GetLeaves() []*CommitFileNode {
leaves := getLeaves(s)
castLeaves := make([]*CommitFileNode, len(leaves))
for i := range leaves {
castLeaves[i] = leaves[i].(*CommitFileNode)
}
return castLeaves
}
// extra methods
func (s *CommitFileNode) AnyFile(test func(file *models.CommitFile) bool) bool {
return s.Any(func(node *CommitFileNode) bool {
return node.IsLeaf() && test(node.File)
})
}
func (s *CommitFileNode) NameAtDepth(depth int) string {
splitName := strings.Split(s.Path, string(os.PathSeparator))
name := filepath.Join(splitName[depth:]...)
return name
}

View File

@@ -0,0 +1,9 @@
package filetree
const EXPANDED_ARROW = "▼"
const COLLAPSED_ARROW = "►"
const INNER_ITEM = "├─ "
const LAST_ITEM = "└─ "
const NESTED = "│ "
const NOTHING = " "

View File

@@ -0,0 +1,101 @@
package filetree
import (
"sync"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/sirupsen/logrus"
)
type FileManager struct {
files []*models.File
tree *FileNode
showTree bool
log *logrus.Entry
collapsedPaths CollapsedPaths
sync.RWMutex
}
func NewFileManager(files []*models.File, log *logrus.Entry, showTree bool) *FileManager {
return &FileManager{
files: files,
log: log,
showTree: showTree,
collapsedPaths: CollapsedPaths{},
RWMutex: sync.RWMutex{},
}
}
func (m *FileManager) InTreeMode() bool {
return m.showTree
}
func (m *FileManager) ExpandToPath(path string) {
m.collapsedPaths.ExpandToPath(path)
}
func (m *FileManager) ToggleShowTree() {
m.showTree = !m.showTree
m.SetTree()
}
func (m *FileManager) GetItemAtIndex(index int) *FileNode {
// need to traverse the three depth first until we get to the index.
return m.tree.GetNodeAtIndex(index+1, m.collapsedPaths) // ignoring root
}
func (m *FileManager) GetIndexForPath(path string) (int, bool) {
index, found := m.tree.GetIndexForPath(path, m.collapsedPaths)
return index - 1, found
}
func (m *FileManager) GetAllItems() []*FileNode {
if m.tree == nil {
return nil
}
return m.tree.Flatten(m.collapsedPaths)[1:] // ignoring root
}
func (m *FileManager) GetItemsLength() int {
return m.tree.Size(m.collapsedPaths) - 1 // ignoring root
}
func (m *FileManager) GetAllFiles() []*models.File {
return m.files
}
func (m *FileManager) SetFiles(files []*models.File) {
m.files = files
m.SetTree()
}
func (m *FileManager) SetTree() {
if m.showTree {
m.tree = BuildTreeFromFiles(m.files)
} else {
m.tree = BuildFlatTreeFromFiles(m.files)
}
}
func (m *FileManager) IsCollapsed(path string) bool {
return m.collapsedPaths.IsCollapsed(path)
}
func (m *FileManager) ToggleCollapsed(path string) {
m.collapsedPaths.ToggleCollapsed(path)
}
func (m *FileManager) Render(diffName string, submoduleConfigs []*models.SubmoduleConfig) []string {
// can't rely on renderAux to check for nil because an interface won't be nil if its concrete value is nil
if m.tree == nil {
return []string{}
}
return renderAux(m.tree, m.collapsedPaths, "", -1, func(n INode, depth int) string {
castN := n.(*FileNode)
return presentation.GetFileLine(castN.GetHasUnstagedChanges(), castN.GetHasStagedChanges(), castN.NameAtDepth(depth), diffName, submoduleConfigs, castN.File)
})
}

View File

@@ -0,0 +1,91 @@
package filetree
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/stretchr/testify/assert"
)
func TestRender(t *testing.T) {
scenarios := []struct {
name string
root *FileNode
collapsedPaths map[string]bool
expected []string
}{
{
name: "nil node",
root: nil,
expected: []string{},
},
{
name: "leaf node",
root: &FileNode{
Path: "",
Children: []*FileNode{
{File: &models.File{Name: "test", ShortStatus: " M", HasStagedChanges: true}, Path: "test"},
},
},
expected: []string{" M test"},
},
{
name: "big example",
root: &FileNode{
Path: "",
Children: []*FileNode{
{
Path: "dir1",
Children: []*FileNode{
{
File: &models.File{Name: "dir1/file2", ShortStatus: "M ", HasUnstagedChanges: true},
Path: "dir1/file2",
},
{
File: &models.File{Name: "dir1/file3", ShortStatus: "M ", HasUnstagedChanges: true},
Path: "dir1/file3",
},
},
},
{
Path: "dir2",
Children: []*FileNode{
{
Path: "dir2/dir2",
Children: []*FileNode{
{
File: &models.File{Name: "dir2/dir2/file3", ShortStatus: " M", HasStagedChanges: true},
Path: "dir2/dir2/file3",
},
{
File: &models.File{Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true},
Path: "dir2/dir2/file4",
},
},
},
{
File: &models.File{Name: "dir2/file5", ShortStatus: "M ", HasUnstagedChanges: true},
Path: "dir2/file5",
},
},
},
{
File: &models.File{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
Path: "file1",
},
},
},
expected: []string{"dir1 ►", "dir2 ▼", "├─ dir2 ▼", "│ ├─ M file3", "│ └─ M file4", "└─ M file5", "M file1"},
collapsedPaths: map[string]bool{"dir1": true},
},
}
for _, s := range scenarios {
s := s
t.Run(s.name, func(t *testing.T) {
mngr := &FileManager{tree: s.root, collapsedPaths: s.collapsedPaths}
result := mngr.Render("", nil)
assert.EqualValues(t, s.expected, result)
})
}
}

View File

@@ -0,0 +1,192 @@
package filetree
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
)
type FileNode struct {
Children []*FileNode
File *models.File
Path string // e.g. '/path/to/mydir'
CompressionLevel int // equal to the number of forward slashes you'll see in the path when it's rendered in tree mode
}
// methods satisfying ListItem interface
func (s *FileNode) ID() string {
return s.GetPath()
}
func (s *FileNode) Description() string {
return s.GetPath()
}
// methods satisfying INode interface
func (s *FileNode) IsLeaf() bool {
return s.File != nil
}
func (s *FileNode) GetPath() string {
return s.Path
}
func (s *FileNode) GetChildren() []INode {
result := make([]INode, len(s.Children))
for i, child := range s.Children {
result[i] = child
}
return result
}
func (s *FileNode) SetChildren(children []INode) {
castChildren := make([]*FileNode, len(children))
for i, child := range children {
castChildren[i] = child.(*FileNode)
}
s.Children = castChildren
}
func (s *FileNode) GetCompressionLevel() int {
return s.CompressionLevel
}
func (s *FileNode) SetCompressionLevel(level int) {
s.CompressionLevel = level
}
// methods utilising generic functions for INodes
func (s *FileNode) Sort() {
sortNode(s)
}
func (s *FileNode) ForEachFile(cb func(*models.File) error) error {
return forEachLeaf(s, func(n INode) error {
castNode := n.(*FileNode)
return cb(castNode.File)
})
}
func (s *FileNode) Any(test func(node *FileNode) bool) bool {
return any(s, func(n INode) bool {
castNode := n.(*FileNode)
return test(castNode)
})
}
func (n *FileNode) Flatten(collapsedPaths map[string]bool) []*FileNode {
results := flatten(n, collapsedPaths)
nodes := make([]*FileNode, len(results))
for i, result := range results {
nodes[i] = result.(*FileNode)
}
return nodes
}
func (node *FileNode) GetNodeAtIndex(index int, collapsedPaths map[string]bool) *FileNode {
if node == nil {
return nil
}
result := getNodeAtIndex(node, index, collapsedPaths)
if result == nil {
// not sure how this can be nil: we probably are missing a mutex somewhere
return nil
}
return result.(*FileNode)
}
func (node *FileNode) GetIndexForPath(path string, collapsedPaths map[string]bool) (int, bool) {
return getIndexForPath(node, path, collapsedPaths)
}
func (node *FileNode) Size(collapsedPaths map[string]bool) int {
if node == nil {
return 0
}
return size(node, collapsedPaths)
}
func (s *FileNode) Compress() {
// with these functions I try to only have type conversion code on the actual struct,
// but comparing interface values to nil is fraught with danger so I'm duplicating
// that code here.
if s == nil {
return
}
compressAux(s)
}
// This ignores the root
func (node *FileNode) GetPathsMatching(test func(*FileNode) bool) []string {
return getPathsMatching(node, func(n INode) bool {
return test(n.(*FileNode))
})
}
func (s *FileNode) GetLeaves() []*FileNode {
leaves := getLeaves(s)
castLeaves := make([]*FileNode, len(leaves))
for i := range leaves {
castLeaves[i] = leaves[i].(*FileNode)
}
return castLeaves
}
// extra methods
func (s *FileNode) GetHasUnstagedChanges() bool {
return s.AnyFile(func(file *models.File) bool { return file.HasUnstagedChanges })
}
func (s *FileNode) GetHasStagedChanges() bool {
return s.AnyFile(func(file *models.File) bool { return file.HasStagedChanges })
}
func (s *FileNode) GetHasInlineMergeConflicts() bool {
return s.AnyFile(func(file *models.File) bool { return file.HasInlineMergeConflicts })
}
func (s *FileNode) GetIsTracked() bool {
return s.AnyFile(func(file *models.File) bool { return file.Tracked })
}
func (s *FileNode) AnyFile(test func(file *models.File) bool) bool {
return s.Any(func(node *FileNode) bool {
return node.IsLeaf() && test(node.File)
})
}
func (s *FileNode) NameAtDepth(depth int) string {
splitName := strings.Split(s.Path, string(os.PathSeparator))
name := filepath.Join(splitName[depth:]...)
if s.File != nil && s.File.IsRename() {
splitPrevName := strings.Split(s.File.PreviousName, string(os.PathSeparator))
prevName := s.File.PreviousName
// if the file has just been renamed inside the same directory, we can shave off
// the prefix for the previous path too. Otherwise we'll keep it unchanged
sameParentDir := len(splitName) == len(splitPrevName) && filepath.Join(splitName[0:depth]...) == filepath.Join(splitPrevName[0:depth]...)
if sameParentDir {
prevName = filepath.Join(splitPrevName[depth:]...)
}
return fmt.Sprintf("%s%s%s", prevName, " → ", name)
}
return name
}

View File

@@ -0,0 +1,125 @@
package filetree
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/stretchr/testify/assert"
)
func TestCompress(t *testing.T) {
scenarios := []struct {
name string
root *FileNode
expected *FileNode
}{
{
name: "nil node",
root: nil,
expected: nil,
},
{
name: "leaf node",
root: &FileNode{
Path: "",
Children: []*FileNode{
{File: &models.File{Name: "test", ShortStatus: " M", HasStagedChanges: true}, Path: "test"},
},
},
expected: &FileNode{
Path: "",
Children: []*FileNode{
{File: &models.File{Name: "test", ShortStatus: " M", HasStagedChanges: true}, Path: "test"},
},
},
},
{
name: "big example",
root: &FileNode{
Path: "",
Children: []*FileNode{
{
Path: "dir1",
Children: []*FileNode{
{
File: &models.File{Name: "file2", ShortStatus: "M ", HasUnstagedChanges: true},
Path: "dir1/file2",
},
},
},
{
Path: "dir2",
Children: []*FileNode{
{
File: &models.File{Name: "file3", ShortStatus: " M", HasStagedChanges: true},
Path: "dir2/file3",
},
{
File: &models.File{Name: "file4", ShortStatus: "M ", HasUnstagedChanges: true},
Path: "dir2/file4",
},
},
},
{
Path: "dir3",
Children: []*FileNode{
{
Path: "dir3/dir3-1",
Children: []*FileNode{
{
File: &models.File{Name: "file5", ShortStatus: "M ", HasUnstagedChanges: true},
Path: "dir3/dir3-1/file5",
},
},
},
},
},
{
File: &models.File{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
Path: "file1",
},
},
},
expected: &FileNode{
Path: "",
Children: []*FileNode{
{
Path: "dir1/file2",
File: &models.File{Name: "file2", ShortStatus: "M ", HasUnstagedChanges: true},
CompressionLevel: 1,
},
{
Path: "dir2",
Children: []*FileNode{
{
File: &models.File{Name: "file3", ShortStatus: " M", HasStagedChanges: true},
Path: "dir2/file3",
},
{
File: &models.File{Name: "file4", ShortStatus: "M ", HasUnstagedChanges: true},
Path: "dir2/file4",
},
},
},
{
Path: "dir3/dir3-1/file5",
File: &models.File{Name: "file5", ShortStatus: "M ", HasUnstagedChanges: true},
CompressionLevel: 2,
},
{
File: &models.File{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
Path: "file1",
},
},
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.name, func(t *testing.T) {
s.root.Compress()
assert.EqualValues(t, s.expected, s.root)
})
}
}

262
pkg/gui/filetree/inode.go Normal file
View File

@@ -0,0 +1,262 @@
package filetree
import (
"fmt"
"sort"
"strings"
)
type INode interface {
IsLeaf() bool
GetPath() string
GetChildren() []INode
SetChildren([]INode)
GetCompressionLevel() int
SetCompressionLevel(int)
}
func sortNode(node INode) {
sortChildren(node)
for _, child := range node.GetChildren() {
sortNode(child)
}
}
func sortChildren(node INode) {
if node.IsLeaf() {
return
}
children := node.GetChildren()
sortedChildren := make([]INode, len(children))
copy(sortedChildren, children)
sort.Slice(sortedChildren, func(i, j int) bool {
if !sortedChildren[i].IsLeaf() && sortedChildren[j].IsLeaf() {
return true
}
if sortedChildren[i].IsLeaf() && !sortedChildren[j].IsLeaf() {
return false
}
return sortedChildren[i].GetPath() < sortedChildren[j].GetPath()
})
// TODO: think about making this in-place
node.SetChildren(sortedChildren)
}
func forEachLeaf(node INode, cb func(INode) error) error {
if node.IsLeaf() {
if err := cb(node); err != nil {
return err
}
}
for _, child := range node.GetChildren() {
if err := forEachLeaf(child, cb); err != nil {
return err
}
}
return nil
}
func any(node INode, test func(INode) bool) bool {
if test(node) {
return true
}
for _, child := range node.GetChildren() {
if any(child, test) {
return true
}
}
return false
}
func every(node INode, test func(INode) bool) bool {
if !test(node) {
return false
}
for _, child := range node.GetChildren() {
if !every(child, test) {
return false
}
}
return true
}
func flatten(node INode, collapsedPaths map[string]bool) []INode {
result := []INode{}
result = append(result, node)
if !collapsedPaths[node.GetPath()] {
for _, child := range node.GetChildren() {
result = append(result, flatten(child, collapsedPaths)...)
}
}
return result
}
func getNodeAtIndex(node INode, index int, collapsedPaths map[string]bool) INode {
foundNode, _ := getNodeAtIndexAux(node, index, collapsedPaths)
return foundNode
}
func getNodeAtIndexAux(node INode, index int, collapsedPaths map[string]bool) (INode, int) {
offset := 1
if index == 0 {
return node, offset
}
if !collapsedPaths[node.GetPath()] {
for _, child := range node.GetChildren() {
foundNode, offsetChange := getNodeAtIndexAux(child, index-offset, collapsedPaths)
offset += offsetChange
if foundNode != nil {
return foundNode, offset
}
}
}
return nil, offset
}
func getIndexForPath(node INode, path string, collapsedPaths map[string]bool) (int, bool) {
offset := 0
if node.GetPath() == path {
return offset, true
}
if !collapsedPaths[node.GetPath()] {
for _, child := range node.GetChildren() {
offsetChange, found := getIndexForPath(child, path, collapsedPaths)
offset += offsetChange + 1
if found {
return offset, true
}
}
}
return offset, false
}
func size(node INode, collapsedPaths map[string]bool) int {
output := 1
if !collapsedPaths[node.GetPath()] {
for _, child := range node.GetChildren() {
output += size(child, collapsedPaths)
}
}
return output
}
func compressAux(node INode) INode {
if node.IsLeaf() {
return node
}
children := node.GetChildren()
for i := range children {
grandchildren := children[i].GetChildren()
for len(grandchildren) == 1 {
grandchildren[0].SetCompressionLevel(children[i].GetCompressionLevel() + 1)
children[i] = grandchildren[0]
grandchildren = children[i].GetChildren()
}
}
for i := range children {
children[i] = compressAux(children[i])
}
node.SetChildren(children)
return node
}
func getPathsMatching(node INode, test func(INode) bool) []string {
paths := []string{}
if test(node) {
paths = append(paths, node.GetPath())
}
for _, child := range node.GetChildren() {
paths = append(paths, getPathsMatching(child, test)...)
}
return paths
}
func getLeaves(node INode) []INode {
if node.IsLeaf() {
return []INode{node}
}
output := []INode{}
for _, child := range node.GetChildren() {
output = append(output, getLeaves(child)...)
}
return output
}
func renderAux(s INode, collapsedPaths CollapsedPaths, prefix string, depth int, renderLine func(INode, int) string) []string {
isRoot := depth == -1
renderLineWithPrefix := func() string {
return prefix + renderLine(s, depth)
}
if s.IsLeaf() {
if isRoot {
return []string{}
}
return []string{renderLineWithPrefix()}
}
if collapsedPaths.IsCollapsed(s.GetPath()) {
return []string{fmt.Sprintf("%s %s", renderLineWithPrefix(), COLLAPSED_ARROW)}
}
arr := []string{}
if !isRoot {
arr = append(arr, fmt.Sprintf("%s %s", renderLineWithPrefix(), EXPANDED_ARROW))
}
newPrefix := prefix
if strings.HasSuffix(prefix, LAST_ITEM) {
newPrefix = strings.TrimSuffix(prefix, LAST_ITEM) + NOTHING
} else if strings.HasSuffix(prefix, INNER_ITEM) {
newPrefix = strings.TrimSuffix(prefix, INNER_ITEM) + NESTED
}
for i, child := range s.GetChildren() {
isLast := i == len(s.GetChildren())-1
var childPrefix string
if isRoot {
childPrefix = newPrefix
} else if isLast {
childPrefix = newPrefix + LAST_ITEM
} else {
childPrefix = newPrefix + INNER_ITEM
}
arr = append(arr, renderAux(child, collapsedPaths, childPrefix, depth+1+s.GetCompressionLevel(), renderLine)...)
}
return arr
}

View File

@@ -14,6 +14,29 @@ func (gui *Gui) validateNotInFilterMode() (bool, error) {
}
func (gui *Gui) exitFilterMode() error {
gui.State.Modes.Filtering.Path = ""
return gui.Errors.ErrRestart
return gui.clearFiltering()
}
func (gui *Gui) clearFiltering() error {
gui.State.Modes.Filtering.Reset()
if gui.State.ScreenMode == SCREEN_HALF {
gui.State.ScreenMode = SCREEN_NORMAL
}
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{COMMITS}})
}
func (gui *Gui) setFiltering(path string) error {
gui.State.Modes.Filtering.SetPath(path)
if gui.State.ScreenMode == SCREEN_NORMAL {
gui.State.ScreenMode = SCREEN_HALF
}
if err := gui.pushContext(gui.State.Contexts.BranchCommits); err != nil {
return err
}
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{COMMITS}, then: func() {
gui.State.Contexts.BranchCommits.GetPanelState().SetSelectedLineIdx(0)
}})
}

View File

@@ -3,26 +3,20 @@ 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
}
func (gui *Gui) handleCreateFilteringMenuPanel() error {
fileName := ""
switch v.Name() {
case "files":
file := gui.getSelectedFile()
if file != nil {
fileName = file.Name
switch gui.currentSideListContext() {
case gui.State.Contexts.Files:
node := gui.getSelectedFileNode()
if node != nil {
fileName = node.GetPath()
}
case "commitFiles":
file := gui.getSelectedCommitFile()
if file != nil {
fileName = file.Name
case gui.State.Contexts.CommitFiles:
node := gui.getSelectedCommitFileNode()
if node != nil {
fileName = node.GetPath()
}
}
@@ -32,8 +26,7 @@ func (gui *Gui) handleCreateFilteringMenuPanel(g *gocui.Gui, v *gocui.View) erro
menuItems = append(menuItems, &menuItem{
displayString: fmt.Sprintf("%s '%s'", gui.Tr.LcFilterBy, fileName),
onPress: func() error {
gui.State.Modes.Filtering.Path = fileName
return gui.Errors.ErrRestart
return gui.setFiltering(fileName)
},
})
}
@@ -44,8 +37,7 @@ func (gui *Gui) handleCreateFilteringMenuPanel(g *gocui.Gui, v *gocui.View) erro
return gui.prompt(promptOpts{
title: gui.Tr.LcEnterFileName,
handleConfirm: func(response string) error {
gui.State.Modes.Filtering.Path = strings.TrimSpace(response)
return gui.Errors.ErrRestart
return gui.setFiltering(strings.TrimSpace(response))
},
})
},
@@ -54,10 +46,7 @@ func (gui *Gui) handleCreateFilteringMenuPanel(g *gocui.Gui, v *gocui.View) erro
if gui.State.Modes.Filtering.Active() {
menuItems = append(menuItems, &menuItem{
displayString: gui.Tr.LcExitFilterMode,
onPress: func() error {
gui.State.Modes.Filtering.Path = ""
return gui.Errors.ErrRestart
},
onPress: gui.clearFiltering,
})
}

View File

@@ -5,7 +5,6 @@ import (
"regexp"
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -32,19 +31,19 @@ func (gui *Gui) gitFlowFinishBranch(gitFlowConfig string, branchName string) err
return gui.createErrorPanel(gui.Tr.NotAGitFlowBranch)
}
subProcess := gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "finish", suffix)
gui.SubProcess = subProcess
return gui.Errors.ErrSubProcess
return gui.runSubprocessWithSuspense(
gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "finish", suffix),
)
}
func (gui *Gui) handleCreateGitFlowMenu(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCreateGitFlowMenu() error {
branch := gui.getSelectedBranch()
if branch == nil {
return nil
}
// get config
gitFlowConfig, err := gui.OSCommand.RunCommandWithOutput("git config --local --get-regexp gitflow")
gitFlowConfig, err := gui.GitCommand.RunCommandWithOutput("git config --local --get-regexp gitflow")
if err != nil {
return gui.createErrorPanel("You need to install git-flow and enable it in this repo to use git-flow features")
}
@@ -56,9 +55,9 @@ func (gui *Gui) handleCreateGitFlowMenu(g *gocui.Gui, v *gocui.View) error {
return gui.prompt(promptOpts{
title: title,
handleConfirm: func(name string) error {
subProcess := gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "start", name)
gui.SubProcess = subProcess
return gui.Errors.ErrSubProcess
return gui.runSubprocessWithSuspense(
gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "start", name),
)
},
})
}

View File

@@ -13,8 +13,8 @@ import (
// 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 {
for _, view := range []*gocui.View{gui.Views.Branches, gui.Views.Commits} {
if err := gui.rerenderView(view); err != nil {
return err
}
}
@@ -22,131 +22,167 @@ func (gui *Gui) rerenderViewsWithScreenModeDependentContent() error {
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
// TODO: GENERICS
func nextIntInCycle(sl []WindowMaximisation, current WindowMaximisation) WindowMaximisation {
for i, val := range sl {
if val == current {
if i == len(sl)-1 {
return sl[0]
}
return sl[i+1]
}
}
ox, oy := mainView.Origin()
return sl[0]
}
// TODO: GENERICS
func prevIntInCycle(sl []WindowMaximisation, current WindowMaximisation) WindowMaximisation {
for i, val := range sl {
if val == current {
if i > 0 {
return sl[i-1]
}
return sl[len(sl)-1]
}
}
return sl[len(sl)-1]
}
func (gui *Gui) nextScreenMode() error {
gui.State.ScreenMode = nextIntInCycle([]WindowMaximisation{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode)
return gui.rerenderViewsWithScreenModeDependentContent()
}
func (gui *Gui) prevScreenMode() error {
gui.State.ScreenMode = prevIntInCycle([]WindowMaximisation{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode)
return gui.rerenderViewsWithScreenModeDependentContent()
}
func (gui *Gui) scrollUpView(view *gocui.View) error {
ox, oy := view.Origin()
newOy := int(math.Max(0, float64(oy-gui.Config.GetUserConfig().Gui.ScrollHeight)))
return mainView.SetOrigin(ox, newOy)
return view.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()
func (gui *Gui) scrollDownView(view *gocui.View) error {
ox, oy := view.Origin()
y := oy
if !gui.Config.GetUserConfig().Gui.ScrollPastBottom {
_, sy := mainView.Size()
canScrollPastBottom := gui.Config.GetUserConfig().Gui.ScrollPastBottom
if !canScrollPastBottom {
_, sy := view.Size()
y += sy
}
scrollHeight := gui.Config.GetUserConfig().Gui.ScrollHeight
if y < mainView.LinesHeight() {
if err := mainView.SetOrigin(ox, oy+scrollHeight); err != nil {
return err
scrollableLines := view.ViewLinesHeight() - y
if scrollableLines > 0 {
// margin is about how many lines must still appear if you scroll
// all the way down. In practice every file ends in a newline so it will really
// just show a single line
margin := 1
if canScrollPastBottom {
margin = 2
}
if scrollableLines-margin < scrollHeight {
scrollHeight = scrollableLines - margin
}
if oy+scrollHeight >= 0 {
if err := view.SetOrigin(ox, oy+scrollHeight); err != nil {
return err
}
}
}
if manager, ok := gui.viewBufferManagerMap[viewName]; ok {
if manager, ok := gui.viewBufferManagerMap[view.Name()]; ok {
manager.ReadLines(scrollHeight)
}
return nil
}
func (gui *Gui) scrollUpMain(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) scrollUpMain() error {
if gui.canScrollMergePanel() {
gui.State.Panels.Merging.UserScrolling = true
}
return gui.scrollUpView("main")
return gui.scrollUpView(gui.Views.Main)
}
func (gui *Gui) scrollDownMain(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) scrollDownMain() error {
if gui.canScrollMergePanel() {
gui.State.Panels.Merging.UserScrolling = true
}
return gui.scrollDownView("main")
return gui.scrollDownView(gui.Views.Main)
}
func (gui *Gui) scrollUpSecondary(g *gocui.Gui, v *gocui.View) error {
return gui.scrollUpView("secondary")
func (gui *Gui) scrollUpSecondary() error {
return gui.scrollUpView(gui.Views.Secondary)
}
func (gui *Gui) scrollDownSecondary(g *gocui.Gui, v *gocui.View) error {
return gui.scrollDownView("secondary")
func (gui *Gui) scrollDownSecondary() error {
return gui.scrollDownView(gui.Views.Secondary)
}
func (gui *Gui) scrollUpConfirmationPanel(g *gocui.Gui, v *gocui.View) error {
if v.Editable {
func (gui *Gui) scrollUpConfirmationPanel() error {
if gui.Views.Confirmation.Editable {
return nil
}
return gui.scrollUpView("confirmation")
return gui.scrollUpView(gui.Views.Confirmation)
}
func (gui *Gui) scrollDownConfirmationPanel(g *gocui.Gui, v *gocui.View) error {
if v.Editable {
func (gui *Gui) scrollDownConfirmationPanel() error {
if gui.Views.Confirmation.Editable {
return nil
}
return gui.scrollDownView("confirmation")
return gui.scrollDownView(gui.Views.Confirmation)
}
func (gui *Gui) handleRefresh(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleRefresh() error {
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}
func (gui *Gui) handleMouseDownMain(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleMouseDownMain() error {
if gui.popupPanelFocused() {
return nil
}
switch g.CurrentView().Name() {
case "files":
switch gui.g.CurrentView() {
case gui.Views.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 gui.enterFile(false, gui.Views.Main.SelectedLineIdx())
case gui.Views.CommitFiles:
return gui.enterCommitFile(gui.Views.Main.SelectedLineIdx())
}
return nil
}
func (gui *Gui) handleMouseDownSecondary(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleMouseDownSecondary() error {
if gui.popupPanelFocused() {
return nil
}
switch g.CurrentView().Name() {
case "files":
return gui.enterFile(true, v.SelectedLineIdx())
switch gui.g.CurrentView() {
case gui.Views.Files:
return gui.enterFile(true, gui.Views.Secondary.SelectedLineIdx())
}
return nil
}
func (gui *Gui) handleInfoClick(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleInfoClick() error {
if !gui.g.Mouse {
return nil
}
cx, _ := v.Cursor()
width, _ := v.Size()
view := gui.Views.Information
cx, _ := view.Cursor()
width, _ := view.Size()
for _, mode := range gui.modeStatuses() {
if mode.isActive() {
@@ -179,7 +215,7 @@ func (gui *Gui) fetch(canPromptForCredentials bool) (err error) {
_ = gui.createErrorPanel(gui.Tr.PassUnameWrong)
}
_ = gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, COMMITS, REMOTES, TAGS}, mode: ASYNC})
_ = gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, COMMITS, REMOTES, TAGS}, mode: ASYNC})
return err
}

View File

@@ -3,6 +3,7 @@ package gui
import (
"fmt"
"io/ioutil"
"log"
"os"
"runtime"
"sync"
@@ -11,8 +12,6 @@ import (
"strings"
"time"
"github.com/go-errors/errors"
"github.com/fatih/color"
"github.com/golang-collections/collections/stack"
"github.com/jesseduffield/gocui"
@@ -21,19 +20,26 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/modes/filtering"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/tasks"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/updates"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/jesseduffield/termbox-go"
"github.com/mattn/go-runewidth"
"github.com/sirupsen/logrus"
)
// screen sizing determines how much space your selected window takes up (window
// as in panel, not your terminal's window). Sometimes you want a bit more space
// to see the contents of a panel, and this keeps track of how much maximisation
// you've set
type WindowMaximisation int
const (
SCREEN_NORMAL int = iota
SCREEN_NORMAL WindowMaximisation = iota
SCREEN_HALF
SCREEN_FULL
)
@@ -43,56 +49,35 @@ const StartupPopupVersion = 3
// OverlappingEdges determines if panel edges overlap
var OverlappingEdges = false
// SentinelErrors are the errors that have special meaning and need to be checked
// by calling functions. The less of these, the better
type SentinelErrors struct {
ErrSubProcess error
ErrNoFiles error
ErrSwitchRepo error
ErrRestart error
type ContextManager struct {
ContextStack []Context
sync.RWMutex
}
const UNKNOWN_VIEW_ERROR_MSG = "unknown view"
// GenerateSentinelErrors makes the sentinel errors for the gui. We're defining it here
// because we can't do package-scoped errors with localization, and also because
// it seems like package-scoped variables are bad in general
// https://dave.cheney.net/2017/06/11/go-without-package-scoped-variables
// In the future it would be good to implement some of the recommendations of
// that article. For now, if we don't need an error to be a sentinel, we will just
// define it inline. This has implications for error messages that pop up everywhere
// in that we'll be duplicating the default values. We may need to look at
// having a default localisation bundle defined, and just using keys-only when
// localising things in the code.
func (gui *Gui) GenerateSentinelErrors() {
gui.Errors = SentinelErrors{
ErrSubProcess: errors.New(gui.Tr.RunningSubprocess),
ErrNoFiles: errors.New(gui.Tr.NoChangedFiles),
ErrSwitchRepo: errors.New("switching repo"),
ErrRestart: errors.New("restarting"),
func NewContextManager(initialContext Context) ContextManager {
return ContextManager{
ContextStack: []Context{initialContext},
RWMutex: sync.RWMutex{},
}
}
func (gui *Gui) sentinelErrorsArr() []error {
return []error{
gui.Errors.ErrSubProcess,
gui.Errors.ErrNoFiles,
gui.Errors.ErrSwitchRepo,
gui.Errors.ErrRestart,
}
}
type Repo string
// Gui wraps the gocui Gui object which handles rendering and events
type Gui struct {
g *gocui.Gui
Log *logrus.Entry
GitCommand *commands.GitCommand
OSCommand *oscommands.OSCommand
SubProcess *exec.Cmd
State *guiState
g *gocui.Gui
Log *logrus.Entry
GitCommand *commands.GitCommand
OSCommand *oscommands.OSCommand
// this is the state of the GUI for the current repo
State *guiState
// this is a mapping of repos to gui states, so that we can restore the original
// gui state when returning from a subrepo
RepoStateMap map[Repo]*guiState
Config config.AppConfigurer
Tr *i18n.TranslationSet
Errors SentinelErrors
Updater *updates.Updater
statusManager *statusManager
credentials credentials
@@ -103,25 +88,22 @@ type Gui struct {
// when lazygit is opened outside a git directory we want to open to the most
// recent repo with the recent repos popup showing
showRecentRepos bool
Contexts ContextTree
ViewTabContextMap map[string][]tabContext
// this array either includes the events that we're recording in this session
// or the events we've recorded in a prior session
RecordedEvents []RecordedEvent
StartTime time.Time
showRecentRepos bool
Mutexes guiStateMutexes
// findSuggestions will take a string that the user has typed into a prompt
// and return a slice of suggestions which match that string.
findSuggestions func(string) []*types.Suggestion
}
type RecordedEvent struct {
Timestamp int64
Event *termbox.Event
// when you enter into a submodule we'll append the superproject's path to this array
// so that you can return to the superproject
RepoPathStack []string
// this tells us whether our views have been initially set up
ViewsSetup bool
Views Views
}
type listPanelState struct {
@@ -136,11 +118,6 @@ func (h *listPanelState) GetSelectedLineIdx() int {
return h.SelectedLineIdx
}
type IListPanelState interface {
SetSelectedLineIdx(int)
GetSelectedLineIdx() int
}
// for now the staging panel state, unlike the other panel states, is going to be
// non-mutative, so that we don't accidentally end up
// with mismatches of data. We might change this in the future
@@ -150,15 +127,16 @@ type lBlPanelState struct {
LastLineIdx int
Diff string
PatchParser *patch.PatchParser
SelectMode int // one of LINE, HUNK, or RANGE
SelectMode SelectMode
SecondaryFocused bool // this is for if we show the left or right panel
}
type mergingPanelState struct {
ConflictIndex int
ConflictTop bool
Conflicts []commands.Conflict
EditHistory *stack.Stack
ConflictIndex int
ConflictTop bool
Conflicts []commands.Conflict
ConflictsMutex sync.Mutex
EditHistory *stack.Stack
// UserScrolling tells us if the user has started scrolling through the file themselves
// in which case we won't auto-scroll to a conflict.
@@ -247,6 +225,28 @@ type panelStates struct {
Suggestions *suggestionsPanelState
}
type Views struct {
Status *gocui.View
Files *gocui.View
Branches *gocui.View
Commits *gocui.View
Stash *gocui.View
Main *gocui.View
Secondary *gocui.View
Options *gocui.View
Confirmation *gocui.View
Menu *gocui.View
Credentials *gocui.View
CommitMessage *gocui.View
CommitFiles *gocui.View
Information *gocui.View
AppStatus *gocui.View
Search *gocui.View
SearchPrefix *gocui.View
Limit *gocui.View
Suggestions *gocui.View
}
type searchingState struct {
view *gocui.View
isSearching bool
@@ -254,8 +254,10 @@ type searchingState struct {
}
// startup stages so we don't need to load everything at once
type StartupStage int
const (
INITIAL = iota
INITIAL StartupStage = iota
COMPLETE
)
@@ -269,19 +271,11 @@ func (m *Diffing) Active() bool {
return m.Ref != ""
}
type Filtering struct {
Path string // the filename that gets passed to git log
}
func (m *Filtering) Active() bool {
return m.Path != ""
}
type CherryPicking struct {
CherryPickedCommits []*models.Commit
// we only allow cherry picking from one context at a time, so you can't copy a commit from the local commits context and then also copy a commit in the reflog context
ContextKey string
ContextKey ContextKey
}
func (m *CherryPicking) Active() bool {
@@ -289,7 +283,7 @@ func (m *CherryPicking) Active() bool {
}
type Modes struct {
Filtering Filtering
Filtering filtering.Filtering
CherryPicking CherryPicking
Diffing Diffing
}
@@ -303,12 +297,14 @@ type guiStateMutexes struct {
}
type guiState struct {
Files []*models.File
Submodules []*models.SubmoduleConfig
Branches []*models.Branch
Commits []*models.Commit
StashEntries []*models.StashEntry
CommitFiles []*models.CommitFile
// the file panels (files and commit files) can render as a tree, so we have
// managers for them which handle rendering a flat list of files in tree form
FileManager *filetree.FileManager
CommitFileManager *filetree.CommitFileManager
Submodules []*models.SubmoduleConfig
Branches []*models.Branch
Commits []*models.Commit
StashEntries []*models.StashEntry
// Suggestions will sometimes appear when typing into a prompt
Suggestions []*types.Suggestion
// FilteredReflogCommits are the ones that appear in the reflog panel.
@@ -325,60 +321,75 @@ type guiState struct {
MenuItems []*menuItem
Updating bool
Panels *panelStates
MainContext string // used to keep the main and secondary views' contexts in sync
SplitMainPanel bool
MainContext ContextKey // used to keep the main and secondary views' contexts in sync
RetainOriginalDir bool
IsRefreshingFiles bool
Searching searchingState
ScreenMode int
ScreenMode WindowMaximisation
SideView *gocui.View
Ptmx *os.File
PrevMainWidth int
PrevMainHeight int
OldInformation string
StartupStage int // one of INITIAL and COMPLETE. Allows us to not load everything at once
StartupStage StartupStage // Allows us to not load everything at once
Modes Modes
ContextStack []Context
ViewContextMap map[string]Context
ContextManager ContextManager
Contexts ContextTree
ViewContextMap map[string]Context
ViewTabContextMap map[string][]tabContext
// WindowViewNameMap is a mapping of windows to the current view of that window.
// Some views move between windows for example the commitFiles view and when cycling through
// side windows we need to know which view to give focus to for a given window
WindowViewNameMap map[string]string
// when you enter into a submodule we'll append the superproject's path to this array
// so that you can return to the superproject
RepoPathStack []string
// tells us whether we've set up our views for the current repo. We'll need to
// do this whenever we switch back and forth between repos to get the views
// back in sync with the repo state
ViewsSetup bool
}
func (gui *Gui) resetState() {
// we carry over the filter path and diff state
prevFiltering := Filtering{
Path: "",
}
prevDiff := Diffing{}
prevCherryPicking := CherryPicking{
CherryPickedCommits: make([]*models.Commit, 0),
ContextKey: "",
}
prevRepoPathStack := []string{}
if gui.State != nil {
prevFiltering = gui.State.Modes.Filtering
prevDiff = gui.State.Modes.Diffing
prevCherryPicking = gui.State.Modes.CherryPicking
prevRepoPathStack = gui.State.RepoPathStack
// reuseState determines if we pull the repo state from our repo state map or
// just re-initialize it. For now we're only re-using state when we're going
// in and out of submodules, for the sake of having the cursor back on the submodule
// when we return.
//
// I tried out always reverting to the repo's original state but found that in fact
// it gets a bit confusing to land back in the status panel when visiting a repo
// you've already switched from. There's no doubt some easy way to make the UX
// optimal for all cases but I'm too lazy to think about what that is right now
func (gui *Gui) resetState(filterPath string, reuseState bool) {
currentDir, err := os.Getwd()
if reuseState {
if err == nil {
if state := gui.RepoStateMap[Repo(currentDir)]; state != nil {
gui.State = state
gui.State.ViewsSetup = false
return
}
} else {
gui.Log.Error(err)
}
}
modes := Modes{
Filtering: prevFiltering,
CherryPicking: prevCherryPicking,
Diffing: prevDiff,
showTree := gui.Config.GetUserConfig().Gui.ShowFileTree
contexts := gui.contextTree()
screenMode := SCREEN_NORMAL
initialContext := contexts.Files
if filterPath != "" {
screenMode = SCREEN_HALF
initialContext = contexts.BranchCommits
}
gui.State = &guiState{
Files: make([]*models.File, 0),
FileManager: filetree.NewFileManager(make([]*models.File, 0), gui.Log, showTree),
CommitFileManager: filetree.NewCommitFileManager(make([]*models.CommitFile, 0), gui.Log, showTree),
Commits: make([]*models.Commit, 0),
FilteredReflogCommits: make([]*models.Commit, 0),
ReflogCommits: make([]*models.Commit, 0),
@@ -399,18 +410,32 @@ func (gui *Gui) resetState() {
Menu: &menuPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}, OnPress: nil},
Suggestions: &suggestionsPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}},
Merging: &mergingPanelState{
ConflictIndex: 0,
ConflictTop: true,
Conflicts: []commands.Conflict{},
EditHistory: stack.New(),
ConflictIndex: 0,
ConflictTop: true,
Conflicts: []commands.Conflict{},
EditHistory: stack.New(),
ConflictsMutex: sync.Mutex{},
},
},
SideView: nil,
Ptmx: nil,
Modes: modes,
ViewContextMap: gui.initialViewContextMap(),
RepoPathStack: prevRepoPathStack,
SideView: nil,
Ptmx: nil,
Modes: Modes{
Filtering: filtering.NewFiltering(filterPath),
CherryPicking: CherryPicking{
CherryPickedCommits: make([]*models.Commit, 0),
ContextKey: "",
},
Diffing: Diffing{},
},
ViewContextMap: contexts.initialViewContextMap(),
ViewTabContextMap: contexts.initialViewTabContextMap(),
ScreenMode: screenMode,
// TODO: put contexts in the context manager
ContextManager: NewContextManager(initialContext),
Contexts: contexts,
}
gui.RepoStateMap[Repo(currentDir)] = gui.State
}
// for now the split view will always be on
@@ -426,44 +451,55 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *oscom
statusManager: &statusManager{},
viewBufferManagerMap: map[string]*tasks.ViewBufferManager{},
showRecentRepos: showRecentRepos,
RecordedEvents: []RecordedEvent{},
RepoPathStack: []string{},
RepoStateMap: map[Repo]*guiState{},
}
gui.resetState()
gui.State.Modes.Filtering.Path = filterPath
gui.Contexts = gui.contextTree()
gui.ViewTabContextMap = gui.viewTabContextMap()
gui.resetState(filterPath, false)
gui.watchFilesForChanges()
gui.GenerateSentinelErrors()
return gui, nil
}
// Run setup the gui with keybindings and start the mainloop
func (gui *Gui) Run() error {
gui.resetState()
recordEvents := recordingEvents()
playMode := gocui.NORMAL
if recordEvents {
playMode = gocui.RECORDING
} else if replaying() {
playMode = gocui.REPLAYING
}
g, err := gocui.NewGui(gocui.Output256, OverlappingEdges, recordEvents)
g, err := gocui.NewGui(gocui.OutputTrue, OverlappingEdges, playMode, headless())
if err != nil {
return err
}
gui.g = g // TODO: always use gui.g rather than passing g around everywhere
defer g.Close()
if recordEvents {
go utils.Safe(gui.recordEvents)
}
if replaying() {
g.RecordingConfig = gocui.RecordingConfig{
Speed: getRecordingSpeed(),
Leeway: 100,
}
if gui.State.Modes.Filtering.Active() {
gui.State.ScreenMode = SCREEN_HALF
} else {
gui.State.ScreenMode = SCREEN_NORMAL
g.Recording, err = gui.loadRecording()
if err != nil {
return err
}
go utils.Safe(func() {
time.Sleep(time.Second * 40)
log.Fatal("40 seconds is up, lazygit recording took too long to complete")
})
}
g.OnSearchEscape = gui.onSearchEscape
if err := gui.Config.ReloadUserConfig(); err != nil {
return nil
}
userConfig := gui.Config.GetUserConfig()
g.SearchEscapeKey = gui.getKey(userConfig.Keybinding.Universal.Return)
g.NextSearchMatchKey = gui.getKey(userConfig.Keybinding.Universal.NextMatch)
@@ -475,8 +511,6 @@ func (gui *Gui) Run() error {
g.Mouse = true
}
gui.g = g // TODO: always use gui.g rather than passing g around everywhere
if err := gui.setColorScheme(); err != nil {
return err
}
@@ -505,20 +539,14 @@ func (gui *Gui) Run() error {
return err
}
// RunWithSubprocesses loops, instantiating a new gocui.Gui with each iteration
// if the error returned from a run is a ErrSubProcess, it runs the subprocess
// otherwise it handles the error, possibly by quitting the application
func (gui *Gui) RunWithSubprocesses() error {
gui.StartTime = time.Now()
go utils.Safe(gui.replayRecordedEvents)
for {
gui.stopChan = make(chan struct{})
// RunAndHandleError
func (gui *Gui) RunAndHandleError() error {
gui.stopChan = make(chan struct{})
return utils.SafeWithError(func() error {
if err := gui.Run(); err != nil {
for _, manager := range gui.viewBufferManagerMap {
manager.Close()
}
gui.viewBufferManagerMap = map[string]*tasks.ViewBufferManager{}
if !gui.fileWatcher.Disabled {
gui.fileWatcher.Watcher.Close()
@@ -534,42 +562,63 @@ func (gui *Gui) RunWithSubprocesses() error {
}
}
if err := gui.saveRecordedEvents(); err != nil {
if err := gui.saveRecording(gui.g.Recording); err != nil {
return err
}
return nil
case gui.Errors.ErrSwitchRepo, gui.Errors.ErrRestart:
continue
case gui.Errors.ErrSubProcess:
if err := gui.runCommand(); err != nil {
return err
}
default:
return err
}
}
}
return nil
})
}
func (gui *Gui) runCommand() error {
gui.SubProcess.Stdout = os.Stdout
gui.SubProcess.Stderr = os.Stdout
gui.SubProcess.Stdin = os.Stdin
func (gui *Gui) runSubprocessWithSuspense(subprocess *exec.Cmd) error {
if replaying() {
// we do not yet support running subprocesses within integration tests. So if
// we're replaying an integration test and we're inside this method, something
// has gone wrong, so we should fail
fmt.Fprintf(os.Stdout, "\n%s\n\n", utils.ColoredString("+ "+strings.Join(gui.SubProcess.Args, " "), color.FgBlue))
log.Fatal("opening subprocesses not yet supported in integration tests. Chances are that this test is running too fast and a subprocess is accidentally opened")
}
if err := gui.SubProcess.Run(); err != nil {
if err := gocui.Screen.Suspend(); err != nil {
return gui.surfaceError(err)
}
cmdErr := gui.runSubprocess(subprocess)
if err := gocui.Screen.Resume(); err != nil {
return gui.surfaceError(err)
}
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil {
return err
}
return gui.surfaceError(cmdErr)
}
func (gui *Gui) runSubprocess(subprocess *exec.Cmd) error {
subprocess.Stdout = os.Stdout
subprocess.Stderr = os.Stdout
subprocess.Stdin = os.Stdin
fmt.Fprintf(os.Stdout, "\n%s\n\n", utils.ColoredString("+ "+strings.Join(subprocess.Args, " "), color.FgBlue))
if err := subprocess.Run(); err != nil {
// not handling the error explicitly because usually we're going to see it
// in the output anyway
gui.Log.Error(err)
}
gui.SubProcess.Stdout = ioutil.Discard
gui.SubProcess.Stderr = ioutil.Discard
gui.SubProcess.Stdin = nil
gui.SubProcess = nil
subprocess.Stdout = ioutil.Discard
subprocess.Stderr = ioutil.Discard
subprocess.Stdin = nil
fmt.Fprintf(os.Stdout, "\n%s", utils.ColoredString(gui.Tr.PressEnterToReturn, color.FgGreen))
fmt.Scanln() // wait for enter press
@@ -578,11 +627,9 @@ func (gui *Gui) runCommand() error {
}
func (gui *Gui) loadNewRepo() error {
gui.Updater.CheckForNewUpdate(gui.onBackgroundUpdateCheckFinish, false)
if err := gui.updateRecentRepoList(); err != nil {
return err
}
gui.waitForIntro.Done()
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil {
return err
@@ -668,6 +715,8 @@ func (gui *Gui) setColorScheme() error {
gui.g.FgColor = theme.InactiveBorderColor
gui.g.SelFgColor = theme.ActiveBorderColor
gui.g.FrameColor = theme.InactiveBorderColor
gui.g.SelFrameColor = theme.ActiveBorderColor
return nil
}

View File

@@ -1,370 +1,80 @@
package gui
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"os/exec"
"testing"
"github.com/creack/pty"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/jesseduffield/lazygit/pkg/integration"
"github.com/stretchr/testify/assert"
)
// To run an integration test, e.g. for test 'commit', go:
// go test pkg/gui/gui_test.go -run /commit
// This file is quite similar to integration/main.go. The main difference is that this file is
// run via `go test` whereas the other is run via `test/lazyintegration/main.go` which provides
// a convenient gui wrapper around our integration tests. The `go test` approach is better
// for CI and for running locally in the background to ensure you haven't broken
// anything while making changes. If you want to visually see what's happening when a test is run,
// you'll need to take the other approach
//
// To record keypresses for an integration test, pass RECORD_EVENTS=true like so:
// RECORD_EVENTS=true go test pkg/gui/gui_test.go -run /commit
// As for this file, to run an integration test, e.g. for test 'commit', go:
// go test pkg/gui/gui_test.go -run /commit
//
// To update a snapshot for an integration test, pass UPDATE_SNAPSHOTS=true
// UPDATE_SNAPSHOTS=true go test pkg/gui/gui_test.go -run /commit
//
// When RECORD_EVENTS is true, updates will be updated automatically
//
// integration tests are run in test/integration_test and the final test does
// integration tests are run in test/integration/<test_name>/actual and the final test does
// not clean up that directory so you can cd into it to see for yourself what
// happened when a test failed.
//
// To run tests in parallel pass `PARALLEL=true` as an env var. Tests are run in parallel
// on CI, and are run in a pty so you won't be able to see the stdout of the program
// happened when a test fails.
//
// To override speed, pass e.g. `SPEED=1` as an env var. Otherwise we start each test
// at a high speed and then drop down to lower speeds upon each failure until finally
// trying at the original playback speed (speed 1). A speed of 2 represents twice the
// original playback speed. Speed must be an integer.
type integrationTest struct {
Name string `json:"name"`
Speed int `json:"speed"`
Description string `json:"description"`
}
func loadTests(t *testing.T, testDir string) []*integrationTest {
paths, err := filepath.Glob(filepath.Join(testDir, "/*/test.json"))
assert.NoError(t, err)
tests := make([]*integrationTest, len(paths))
for i, path := range paths {
data, err := ioutil.ReadFile(path)
assert.NoError(t, err)
test := &integrationTest{}
err = json.Unmarshal(data, test)
assert.NoError(t, err)
test.Name = strings.TrimPrefix(filepath.Dir(path), testDir+"/")
tests[i] = test
}
return tests
}
func generateSnapshot(t *testing.T, dir string) string {
osCommand := oscommands.NewDummyOSCommand()
_, err := os.Stat(filepath.Join(dir, ".git"))
if err != nil {
return "git directory not found"
}
snapshot := ""
statusCmd := fmt.Sprintf(`git -C %s status`, dir)
statusCmdOutput, err := osCommand.RunCommandWithOutput(statusCmd)
assert.NoError(t, err)
snapshot += statusCmdOutput + "\n"
logCmd := fmt.Sprintf(`git -C %s log --pretty=%%B -p -1`, dir)
logCmdOutput, err := osCommand.RunCommandWithOutput(logCmd)
assert.NoError(t, err)
snapshot += logCmdOutput + "\n"
err = filepath.Walk(dir, func(path string, f os.FileInfo, err error) error {
assert.NoError(t, err)
if f.IsDir() {
if f.Name() == ".git" {
return filepath.SkipDir
}
return nil
}
bytes, err := ioutil.ReadFile(path)
assert.NoError(t, err)
snapshot += string(bytes) + "\n"
return nil
})
assert.NoError(t, err)
return snapshot
}
func findOrCreateDir(path string) {
_, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
err = os.MkdirAll(path, 0777)
if err != nil {
panic(err)
}
} else {
panic(err)
}
}
}
func getTestSpeeds(testStartSpeed int, updateSnapshots bool) []int {
if updateSnapshots {
// have to go at original speed if updating snapshots in case we go to fast and create a junk snapshot
return []int{1}
}
speedEnv := os.Getenv("SPEED")
if speedEnv != "" {
speed, err := strconv.Atoi(speedEnv)
if err != nil {
panic(err)
}
return []int{speed}
}
// default is 10, 5, 1
startSpeed := 10
if testStartSpeed != 0 {
startSpeed = testStartSpeed
}
speeds := []int{startSpeed}
if startSpeed > 5 {
speeds = append(speeds, 5)
}
speeds = append(speeds, 1)
return speeds
}
func tempLazygitPath() string {
return filepath.Join("/tmp", "lazygit", "test_lazygit")
}
// original playback speed. Speed may be a decimal.
func Test(t *testing.T) {
rootDir := getRootDirectory()
err := os.Chdir(rootDir)
assert.NoError(t, err)
record := false
updateSnapshots := os.Getenv("UPDATE_SNAPSHOTS") != ""
speedEnv := os.Getenv("SPEED")
includeSkipped := os.Getenv("INCLUDE_SKIPPED") != ""
testDir := filepath.Join(rootDir, "test", "integration")
osCommand := oscommands.NewDummyOSCommand()
err = osCommand.RunCommand("go build -o %s", tempLazygitPath())
assert.NoError(t, err)
tests := loadTests(t, testDir)
record := os.Getenv("RECORD_EVENTS") != ""
updateSnapshots := record || os.Getenv("UPDATE_SNAPSHOTS") != ""
for _, test := range tests {
test := test
t.Run(test.Name, func(t *testing.T) {
if runInParallel() {
t.Parallel()
}
speeds := getTestSpeeds(test.Speed, updateSnapshots)
for i, speed := range speeds {
t.Logf("%s: attempting test at speed %d\n", test.Name, speed)
testPath := filepath.Join(testDir, test.Name)
actualDir := filepath.Join(testPath, "actual")
expectedDir := filepath.Join(testPath, "expected")
t.Logf("testPath: %s, actualDir: %s, expectedDir: %s", testPath, actualDir, expectedDir)
findOrCreateDir(testPath)
prepareIntegrationTestDir(actualDir)
err := createFixture(testPath, actualDir)
err := integration.RunTests(
t.Logf,
runCmdHeadless,
func(test *integration.Test, f func(*testing.T) error) {
t.Run(test.Name, func(t *testing.T) {
err := f(t)
assert.NoError(t, err)
})
},
updateSnapshots,
record,
speedEnv,
func(t *testing.T, expected string, actual string) {
assert.Equal(t, expected, actual, fmt.Sprintf("expected:\n%s\nactual:\n%s\n", expected, actual))
},
includeSkipped,
)
runLazygit(t, testPath, rootDir, record, speed)
if updateSnapshots {
err = oscommands.CopyDir(actualDir, expectedDir)
assert.NoError(t, err)
}
actual := generateSnapshot(t, actualDir)
expected := ""
func() {
// git refuses to track .git folders in subdirectories so we need to rename it
// to git_keep after running a test
defer func() {
err = os.Rename(
filepath.Join(expectedDir, ".git"),
filepath.Join(expectedDir, ".git_keep"),
)
assert.NoError(t, err)
}()
// ignoring this error because we might not have a .git_keep file here yet.
_ = os.Rename(
filepath.Join(expectedDir, ".git_keep"),
filepath.Join(expectedDir, ".git"),
)
expected = generateSnapshot(t, expectedDir)
}()
if expected == actual {
t.Logf("%s: success at speed %d\n", test.Name, speed)
break
}
// if the snapshots and we haven't tried all playback speeds different we'll retry at a slower speed
if i == len(speeds)-1 {
assert.Equal(t, expected, actual, fmt.Sprintf("expected:\n%s\nactual:\n%s\n", expected, actual))
}
}
})
}
assert.NoError(t, err)
}
func createFixture(testPath, actualDir string) error {
osCommand := oscommands.NewDummyOSCommand()
bashScriptPath := filepath.Join(testPath, "setup.sh")
cmd := secureexec.Command("bash", bashScriptPath, actualDir)
func runCmdHeadless(cmd *exec.Cmd) error {
cmd.Env = append(
cmd.Env,
"HEADLESS=true",
"TERM=xterm",
)
if err := osCommand.RunExecutable(cmd); err != nil {
f, err := pty.StartWithSize(cmd, &pty.Winsize{Rows: 100, Cols: 100})
if err != nil {
return err
}
return nil
}
func getRootDirectory() string {
path, err := os.Getwd()
if err != nil {
panic(err)
}
for {
_, err := os.Stat(filepath.Join(path, ".git"))
if err == nil {
return path
}
if !os.IsNotExist(err) {
panic(err)
}
path = filepath.Dir(path)
if path == "/" {
panic("must run in lazygit folder or child folder")
}
}
}
func runLazygit(t *testing.T, testPath string, rootDir string, record bool, speed int) {
osCommand := oscommands.NewDummyOSCommand()
replayPath := filepath.Join(testPath, "recording.json")
templateConfigDir := filepath.Join(rootDir, "test", "default_test_config")
actualDir := filepath.Join(testPath, "actual")
exists, err := osCommand.FileExists(filepath.Join(testPath, "config"))
assert.NoError(t, err)
if exists {
templateConfigDir = filepath.Join(testPath, "config")
}
configDir := filepath.Join(testPath, "used_config")
err = os.RemoveAll(configDir)
assert.NoError(t, err)
err = oscommands.CopyDir(templateConfigDir, configDir)
assert.NoError(t, err)
cmdStr := fmt.Sprintf("%s --use-config-dir=%s --path=%s", tempLazygitPath(), configDir, actualDir)
cmd := osCommand.ExecutableFromString(cmdStr)
cmd.Env = append(cmd.Env, fmt.Sprintf("REPLAY_SPEED=%d", speed))
if record {
cmd.Stdout = os.Stdout
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
cmd.Env = append(
cmd.Env,
fmt.Sprintf("RECORD_EVENTS_TO=%s", replayPath),
)
} else {
cmd.Env = append(
cmd.Env,
fmt.Sprintf("REPLAY_EVENTS_FROM=%s", replayPath),
)
}
// if we're on CI we'll need to use a PTY. We can work that out by seeing if the 'TERM' env is defined.
if runInPTY() {
cmd.Env = append(cmd.Env, "TERM=xterm")
f, err := pty.StartWithSize(cmd, &pty.Winsize{Rows: 100, Cols: 100})
assert.NoError(t, err)
_, _ = io.Copy(ioutil.Discard, f)
assert.NoError(t, err)
_ = f.Close()
} else {
err := cmd.Run()
assert.NoError(t, err)
}
}
func runInParallel() bool {
return os.Getenv("PARALLEL") != ""
}
func runInPTY() bool {
return runInParallel() || os.Getenv("TERM") == ""
}
func prepareIntegrationTestDir(actualDir string) {
// remove contents of integration test directory
dir, err := ioutil.ReadDir(actualDir)
if err != nil {
if os.IsNotExist(err) {
err = os.Mkdir(actualDir, 0777)
if err != nil {
panic(err)
}
} else {
panic(err)
}
}
for _, d := range dir {
os.RemoveAll(filepath.Join(actualDir, d.Name()))
}
_, _ = io.Copy(ioutil.Discard, f)
return f.Close()
}

File diff suppressed because it is too large Load Diff

View File

@@ -31,32 +31,25 @@ func (gui *Gui) layout(g *gocui.Gui) error {
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_ERROR_MSG {
return err
}
v.Title = gui.Tr.NotEnoughSpace
v.Wrap = true
_, _ = g.SetViewOnTop("limit")
var err error
gui.Views.Limit, err = g.SetView("limit", 0, 0, width-1, height-1, 0)
if err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
return nil
gui.Views.Limit.Title = gui.Tr.NotEnoughSpace
gui.Views.Limit.Wrap = true
}
gui.Views.Limit.Visible = height < minimumHeight || width < minimumWidth
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 {
prevMainView := gui.Views.Main
if prevMainView != nil {
_, prevMainHeight := prevMainView.Size()
newMainHeight := viewDimensions["main"].Y1 - viewDimensions["main"].Y0 - 1
heightDiff := newMainHeight - prevMainHeight
@@ -79,17 +72,17 @@ func (gui *Gui) layout(g *gocui.Gui) error {
// 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
if view != nil {
view.Visible = false
}
return g.SetViewOnBottom(viewName)
return view, err
}
frameOffset := 1
if frame {
frameOffset = 0
}
return g.SetView(
view, err := g.SetView(
viewName,
dimensionsObj.X0-frameOffset,
dimensionsObj.Y0-frameOffset,
@@ -97,231 +90,192 @@ func (gui *Gui) layout(g *gocui.Gui) error {
dimensionsObj.Y1+frameOffset,
0,
)
if view != nil {
view.Visible = true
}
return view, err
}
v, err := setViewFromDimensions("main", "main", true)
gui.Views.Main, err = setViewFromDimensions("main", "main", true)
if err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
v.Title = gui.Tr.DiffTitle
v.Wrap = true
v.FgColor = textColor
v.IgnoreCarriageReturns = true
gui.Views.Main.Title = gui.Tr.DiffTitle
gui.Views.Main.Wrap = true
gui.Views.Main.FgColor = theme.GocuiDefaultTextColor
gui.Views.Main.IgnoreCarriageReturns = true
}
secondaryView, err := setViewFromDimensions("secondary", "secondary", true)
gui.Views.Secondary, err = setViewFromDimensions("secondary", "secondary", true)
if err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
secondaryView.Title = gui.Tr.DiffTitle
secondaryView.Wrap = true
secondaryView.FgColor = textColor
secondaryView.IgnoreCarriageReturns = true
gui.Views.Secondary.Title = gui.Tr.DiffTitle
gui.Views.Secondary.Wrap = true
gui.Views.Secondary.FgColor = theme.GocuiDefaultTextColor
gui.Views.Secondary.IgnoreCarriageReturns = true
}
hiddenViewOffset := 9999
if v, err := setViewFromDimensions("status", "status", true); err != nil {
if gui.Views.Status, err = setViewFromDimensions("status", "status", true); err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
v.Title = gui.Tr.StatusTitle
v.FgColor = textColor
gui.Views.Status.Title = gui.Tr.StatusTitle
gui.Views.Status.FgColor = theme.GocuiDefaultTextColor
}
filesView, err := setViewFromDimensions("files", "files", true)
gui.Views.Files, err = setViewFromDimensions("files", "files", true)
if err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
filesView.Highlight = true
filesView.Title = gui.Tr.FilesTitle
filesView.FgColor = textColor
filesView.ContainsList = true
gui.Views.Files.Highlight = true
gui.Views.Files.Title = gui.Tr.FilesTitle
gui.Views.Files.FgColor = theme.GocuiDefaultTextColor
gui.Views.Files.ContainsList = true
}
branchesView, err := setViewFromDimensions("branches", "branches", true)
gui.Views.Branches, err = setViewFromDimensions("branches", "branches", true)
if err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
branchesView.Title = gui.Tr.BranchesTitle
branchesView.FgColor = textColor
branchesView.ContainsList = true
gui.Views.Branches.Title = gui.Tr.BranchesTitle
gui.Views.Branches.FgColor = theme.GocuiDefaultTextColor
gui.Views.Branches.ContainsList = true
}
commitFilesView, err := setViewFromDimensions("commitFiles", gui.Contexts.CommitFiles.Context.GetWindowName(), true)
gui.Views.CommitFiles, err = setViewFromDimensions("commitFiles", gui.State.Contexts.CommitFiles.GetWindowName(), true)
if err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
commitFilesView.Title = gui.Tr.CommitFiles
commitFilesView.FgColor = textColor
commitFilesView.ContainsList = true
_, _ = gui.g.SetViewOnBottom("commitFiles")
gui.Views.CommitFiles.Title = gui.Tr.CommitFiles
gui.Views.CommitFiles.FgColor = theme.GocuiDefaultTextColor
gui.Views.CommitFiles.ContainsList = true
}
// if the commit files view is the view to be displayed for its window, we'll display it
gui.Views.CommitFiles.Visible = gui.getViewNameForWindow(gui.State.Contexts.CommitFiles.GetWindowName()) == "commitFiles"
commitsView, err := setViewFromDimensions("commits", "commits", true)
gui.Views.Commits, err = setViewFromDimensions("commits", "commits", true)
if err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
commitsView.Title = gui.Tr.CommitsTitle
commitsView.FgColor = textColor
commitsView.ContainsList = true
gui.Views.Commits.Title = gui.Tr.CommitsTitle
gui.Views.Commits.FgColor = theme.GocuiDefaultTextColor
gui.Views.Commits.ContainsList = true
}
stashView, err := setViewFromDimensions("stash", "stash", true)
gui.Views.Stash, err = setViewFromDimensions("stash", "stash", true)
if err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
stashView.Title = gui.Tr.StashTitle
stashView.FgColor = textColor
stashView.ContainsList = true
gui.Views.Stash.Title = gui.Tr.StashTitle
gui.Views.Stash.FgColor = theme.GocuiDefaultTextColor
gui.Views.Stash.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_ERROR_MSG {
return err
}
_, _ = g.SetViewOnBottom("commitMessage")
commitMessageView.Title = gui.Tr.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_ERROR_MSG {
return err
}
_, _ = g.SetViewOnBottom("credentials")
credentialsView.Title = gui.Tr.CredentialsUsername
credentialsView.FgColor = textColor
credentialsView.Editable = true
}
}
if v, err := setViewFromDimensions("options", "options", false); err != nil {
if gui.Views.Options, err = setViewFromDimensions("options", "options", false); err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
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
}
gui.Views.Options.Frame = false
gui.Views.Options.FgColor = theme.OptionsColor
}
// 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 gui.Views.SearchPrefix, err = setViewFromDimensions("searchPrefix", "searchPrefix", false); err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
searchPrefixView.BgColor = gocui.ColorDefault
searchPrefixView.FgColor = gocui.ColorGreen
searchPrefixView.Frame = false
gui.setViewContent(searchPrefixView, SEARCH_PREFIX)
gui.Views.SearchPrefix.BgColor = gocui.ColorDefault
gui.Views.SearchPrefix.FgColor = gocui.ColorGreen
gui.Views.SearchPrefix.Frame = false
gui.setViewContent(gui.Views.SearchPrefix, SEARCH_PREFIX)
}
if searchView, err := setViewFromDimensions("search", "search", false); err != nil {
if gui.Views.Search, err = setViewFromDimensions("search", "search", false); err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
searchView.BgColor = gocui.ColorDefault
searchView.FgColor = gocui.ColorGreen
searchView.Frame = false
searchView.Editable = true
gui.Views.Search.BgColor = gocui.ColorDefault
gui.Views.Search.FgColor = gocui.ColorGreen
gui.Views.Search.Frame = false
gui.Views.Search.Editable = true
}
if appStatusView, err := setViewFromDimensions("appStatus", "appStatus", false); err != nil {
if gui.Views.AppStatus, err = setViewFromDimensions("appStatus", "appStatus", false); err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
appStatusView.BgColor = gocui.ColorDefault
appStatusView.FgColor = gocui.ColorCyan
appStatusView.Frame = false
_, _ = g.SetViewOnBottom("appStatus")
gui.Views.AppStatus.BgColor = gocui.ColorDefault
gui.Views.AppStatus.FgColor = gocui.ColorCyan
gui.Views.AppStatus.Frame = false
gui.Views.AppStatus.Visible = false
}
informationView, err := setViewFromDimensions("information", "information", false)
gui.Views.Information, err = setViewFromDimensions("information", "information", false)
if err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
informationView.BgColor = gocui.ColorDefault
informationView.FgColor = gocui.ColorGreen
informationView.Frame = false
gui.renderString("information", INFO_SECTION_PADDING+informationStr)
gui.Views.Information.BgColor = gocui.ColorDefault
gui.Views.Information.FgColor = gocui.ColorGreen
gui.Views.Information.Frame = false
gui.renderString(gui.Views.Information, INFO_SECTION_PADDING+informationStr)
}
if gui.State.OldInformation != informationStr {
gui.setViewContent(informationView, informationStr)
gui.setViewContent(gui.Views.Information, 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.pushContext(initialContext); err != nil {
if !gui.ViewsSetup {
if err := gui.onInitialViewsCreation(); err != nil {
return err
}
gui.ViewsSetup = true
}
type listContextState struct {
view *gocui.View
listContext *ListContext
if !gui.State.ViewsSetup {
if err := gui.onInitialViewsCreationForRepo(); err != nil {
return err
}
gui.State.ViewsSetup = true
}
// TODO: don't we already have the view included in the context object itself? Or might that change in a way we don't want reflected here?
listContextStates := []listContextState{
{view: filesView, listContext: gui.filesListContext()},
{view: filesView, listContext: gui.submodulesListContext()},
{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() {
for _, listContext := range gui.getListContexts() {
view, err := gui.g.View(listContext.ViewName)
if err != nil {
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
// ignore contexts whose view is owned by another context right now
if ContextKey(view.Context) != listContext.GetKey() {
continue
}
// check if the selected line is now out of view and if so refocus it
view.FocusPoint(0, listContext.GetPanelState().GetSelectedLineIdx())
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))
view.SetOnSelectItem(gui.onSelectItemWrapper(listContext.onSearchSelect))
}
gui.getMainView().SetOnSelectItem(gui.onSelectItemWrapper(gui.handlelineByLineNavigateTo))
gui.Views.Main.SetOnSelectItem(gui.onSelectItemWrapper(gui.handlelineByLineNavigateTo))
mainViewWidth, mainViewHeight := gui.getMainView().Size()
mainViewWidth, mainViewHeight := gui.Views.Main.Size()
if mainViewWidth != gui.State.PrevMainWidth || mainViewHeight != gui.State.PrevMainHeight {
gui.State.PrevMainWidth = mainViewWidth
gui.State.PrevMainHeight = mainViewHeight
@@ -337,11 +291,77 @@ func (gui *Gui) layout(g *gocui.Gui) error {
return gui.resizeCurrentPopupPanel()
}
func (gui *Gui) onInitialViewsCreation() error {
func (gui *Gui) setHiddenView(viewName string) (*gocui.View, error) {
// arbitrarily giving the view enough size so that we don't get an error, but
// it's expected that the view will be given the correct size before being shown
return gui.g.SetView(viewName, 0, 0, 10, 10, 0)
}
func (gui *Gui) onInitialViewsCreationForRepo() error {
gui.setInitialViewContexts()
// add tabs to views
// hide any popup views. This only applies when we've just switched repos
for _, viewName := range gui.popupViewNames() {
view, err := gui.g.View(viewName)
if err == nil {
view.Visible = false
}
}
initialContext := gui.currentSideContext()
if err := gui.pushContext(initialContext); err != nil {
return err
}
return gui.loadNewRepo()
}
func (gui *Gui) onInitialViewsCreation() error {
// creating some views which are hidden at the start but we need to exist so that we can set an initial ordering
if err := gui.createHiddenViews(); err != nil {
return err
}
// now we order the views (in order of bottom first)
layerOneViews := []*gocui.View{
// first layer. Ordering within this layer does not matter because there are
// no overlapping views
gui.Views.Status,
gui.Views.Files,
gui.Views.Branches,
gui.Views.Commits,
gui.Views.Stash,
gui.Views.CommitFiles,
gui.Views.Main,
gui.Views.Secondary,
// bottom line
gui.Views.Options,
gui.Views.AppStatus,
gui.Views.Information,
gui.Views.Search,
gui.Views.SearchPrefix,
// popups. Ordering within this layer does not matter because there should
// only be one popup shown at a time
gui.Views.CommitMessage,
gui.Views.Credentials,
gui.Views.Menu,
gui.Views.Suggestions,
gui.Views.Confirmation,
// this guy will cover everything else when it appears
gui.Views.Limit,
}
for _, view := range layerOneViews {
if _, err := gui.g.SetViewOnTop(view.Name()); err != nil {
return err
}
}
gui.g.Mutexes.ViewsMutex.Lock()
// add tabs to views
for _, view := range gui.g.Views() {
tabs := gui.viewTabNames(view.Name())
if len(tabs) == 0 {
@@ -351,10 +371,6 @@ func (gui *Gui) onInitialViewsCreation() error {
}
gui.g.Mutexes.ViewsMutex.Unlock()
if err := gui.pushContext(gui.defaultSideContext()); err != nil {
return err
}
if err := gui.keybindings(); err != nil {
return err
}
@@ -366,5 +382,64 @@ func (gui *Gui) onInitialViewsCreation() error {
gui.showRecentRepos = false
}
return gui.loadNewRepo()
gui.Updater.CheckForNewUpdate(gui.onBackgroundUpdateCheckFinish, false)
gui.waitForIntro.Done()
return nil
}
func (gui *Gui) createHiddenViews() error {
// doesn't matter where this view starts because it will be hidden
var err error
if gui.Views.CommitMessage, err = gui.setHiddenView("commitMessage"); err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
gui.Views.CommitMessage.Visible = false
gui.Views.CommitMessage.Title = gui.Tr.CommitMessage
gui.Views.CommitMessage.FgColor = theme.GocuiDefaultTextColor
gui.Views.CommitMessage.Editable = true
gui.Views.CommitMessage.Editor = gocui.EditorFunc(gui.commitMessageEditor)
}
// doesn't matter where this view starts because it will be hidden
if gui.Views.Credentials, err = gui.setHiddenView("credentials"); err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
gui.Views.Credentials.Visible = false
gui.Views.Credentials.Title = gui.Tr.CredentialsUsername
gui.Views.Credentials.FgColor = theme.GocuiDefaultTextColor
gui.Views.Credentials.Editable = true
}
// not worrying about setting attributes because that will be done when the view is actually shown
gui.Views.Confirmation, err = gui.setHiddenView("confirmation")
if err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
gui.Views.Confirmation.Visible = false
}
// not worrying about setting attributes because that will be done when the view is actually shown
gui.Views.Suggestions, err = gui.setHiddenView("suggestions")
if err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
gui.Views.Suggestions.Visible = false
}
// not worrying about setting attributes because that will be done when the view is actually shown
gui.Views.Menu, err = gui.setHiddenView("menu")
if err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
gui.Views.Menu.Visible = false
}
return nil
}

View File

@@ -16,8 +16,10 @@ import (
// use cases
// these represent what select mode we're in
type SelectMode int
const (
LINE = iota
LINE SelectMode = iota
RANGE
HUNK
)
@@ -25,6 +27,8 @@ const (
// returns whether the patch is empty so caller can escape if necessary
// both diffs should be non-coloured because we'll parse them and colour them here
func (gui *Gui) refreshLineByLinePanel(diff string, secondaryDiff string, secondaryFocused bool, selectedLineIdx int, state *lBlPanelState) (bool, error) {
gui.splitMainPanel(true)
patchParser, err := patch.NewPatchParser(gui.Log, diff)
if err != nil {
return false, nil
@@ -80,9 +84,8 @@ func (gui *Gui) refreshLineByLinePanel(diff string, secondaryDiff string, second
return false, err
}
secondaryView := gui.getSecondaryView()
secondaryView.Highlight = true
secondaryView.Wrap = false
gui.Views.Secondary.Highlight = true
gui.Views.Secondary.Wrap = false
secondaryPatchParser, err := patch.NewPatchParser(gui.Log, secondaryDiff)
if err != nil {
@@ -90,7 +93,7 @@ func (gui *Gui) refreshLineByLinePanel(diff string, secondaryDiff string, second
}
gui.g.Update(func(*gocui.Gui) error {
gui.setViewContent(gui.getSecondaryView(), secondaryPatchParser.Render(-1, -1, nil))
gui.setViewContent(gui.Views.Secondary, secondaryPatchParser.Render(-1, -1, nil))
return nil
})
@@ -176,13 +179,13 @@ func (gui *Gui) LBLSelectLine(newSelectedLineIdx int, state *lBlPanelState) erro
return gui.focusSelection(false, state)
}
func (gui *Gui) handleLBLMouseDown(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleLBLMouseDown() error {
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
if gui.popupPanelFocused() {
return nil
}
newSelectedLineIdx := v.SelectedLineIdx()
newSelectedLineIdx := gui.Views.Main.SelectedLineIdx()
state.FirstLineIdx = newSelectedLineIdx
state.LastLineIdx = newSelectedLineIdx
@@ -192,49 +195,27 @@ func (gui *Gui) handleLBLMouseDown(g *gocui.Gui, v *gocui.View) error {
})
}
func (gui *Gui) handleMouseDrag(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleMouseDrag() error {
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
if gui.popupPanelFocused() {
return nil
}
return gui.LBLSelectLine(v.SelectedLineIdx(), state)
})
}
func (gui *Gui) handleMouseScrollUp(g *gocui.Gui, v *gocui.View) error {
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
if gui.popupPanelFocused() {
return nil
}
state.SelectMode = LINE
return gui.LBLCycleLine(-1, state)
})
}
func (gui *Gui) handleMouseScrollDown(g *gocui.Gui, v *gocui.View) error {
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
if gui.popupPanelFocused() {
return nil
}
state.SelectMode = LINE
return gui.LBLCycleLine(1, state)
return gui.LBLSelectLine(gui.Views.Main.SelectedLineIdx(), state)
})
}
func (gui *Gui) getSelectedCommitFileName() string {
return gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLineIdx].Name
idx := gui.State.Panels.CommitFiles.SelectedLineIdx
return gui.State.CommitFileManager.GetItemAtIndex(idx).GetPath()
}
func (gui *Gui) refreshMainViewForLineByLine(state *lBlPanelState) error {
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.currentContext().GetKey() == gui.Contexts.PatchBuilding.Context.GetKey() {
if gui.currentContext().GetKey() == gui.State.Contexts.PatchBuilding.GetKey() {
filename := gui.getSelectedCommitFileName()
var err error
includedLineIndices, err = gui.GitCommand.PatchManager.GetFileIncLineIndices(filename)
@@ -244,12 +225,11 @@ func (gui *Gui) refreshMainViewForLineByLine(state *lBlPanelState) error {
}
colorDiff := state.PatchParser.Render(state.FirstLineIdx, state.LastLineIdx, includedLineIndices)
mainView := gui.getMainView()
mainView.Highlight = true
mainView.Wrap = false
gui.Views.Main.Highlight = true
gui.Views.Main.Wrap = false
gui.g.Update(func(*gocui.Gui) error {
gui.setViewContent(gui.getMainView(), colorDiff)
gui.setViewContent(gui.Views.Main, colorDiff)
return nil
})
@@ -259,7 +239,7 @@ func (gui *Gui) refreshMainViewForLineByLine(state *lBlPanelState) error {
// focusSelection works out the best focus for the staging panel given the
// selected line and size of the hunk
func (gui *Gui) focusSelection(includeCurrentHunk bool, state *lBlPanelState) error {
stagingView := gui.getMainView()
stagingView := gui.Views.Main
_, viewHeight := stagingView.Size()
bufferHeight := viewHeight - 1
@@ -337,9 +317,9 @@ 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():
case gui.State.Contexts.PatchBuilding.GetKey():
filename = gui.getSelectedCommitFileName()
case gui.Contexts.Staging.Context.GetKey():
case gui.State.Contexts.Staging.GetKey():
file := gui.getSelectedFile()
if file == nil {
return nil
@@ -363,7 +343,7 @@ func (gui *Gui) handleOpenFileAtLine() error {
func (gui *Gui) handleLineByLineNextPage() error {
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
newSelectedLineIdx := state.SelectedLineIdx + gui.pageDelta(gui.getMainView())
newSelectedLineIdx := state.SelectedLineIdx + gui.pageDelta(gui.Views.Main)
return gui.lineByLineNavigateTo(newSelectedLineIdx, state)
})
@@ -371,7 +351,7 @@ func (gui *Gui) handleLineByLineNextPage() error {
func (gui *Gui) handleLineByLinePrevPage() error {
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
newSelectedLineIdx := state.SelectedLineIdx - gui.pageDelta(gui.getMainView())
newSelectedLineIdx := state.SelectedLineIdx - gui.pageDelta(gui.Views.Main)
return gui.lineByLineNavigateTo(newSelectedLineIdx, state)
})

View File

@@ -1,13 +1,15 @@
package gui
import (
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type ListContext struct {
ViewName string
ContextKey string
ContextKey ContextKey
GetItemsLength func() int
GetDisplayStrings func() [][]string
OnFocus func() error
@@ -21,13 +23,18 @@ type ListContext struct {
Gui *Gui
ResetMainViewOriginOnFocus bool
Kind int
Kind ContextKind
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 IListPanelState interface {
SetSelectedLineIdx(int)
GetSelectedLineIdx() int
}
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
@@ -96,11 +103,11 @@ func (lc *ListContext) OnRender() error {
return nil
}
func (lc *ListContext) GetKey() string {
func (lc *ListContext) GetKey() ContextKey {
return lc.ContextKey
}
func (lc *ListContext) GetKind() int {
func (lc *ListContext) GetKind() ContextKind {
return lc.Kind
}
@@ -128,6 +135,15 @@ func (lc *ListContext) HandleFocus() error {
view.FocusPoint(0, lc.GetPanelState().GetSelectedLineIdx())
if lc.ResetMainViewOriginOnFocus {
if err := lc.Gui.resetOrigin(lc.Gui.Views.Main); err != nil {
return err
}
if err := lc.Gui.resetOrigin(lc.Gui.Views.Secondary); err != nil {
return err
}
}
if lc.Gui.State.Modes.Diffing.Active() {
return lc.Gui.renderDiff()
}
@@ -143,11 +159,11 @@ func (lc *ListContext) HandleRender() error {
return lc.OnRender()
}
func (lc *ListContext) handlePrevLine(g *gocui.Gui, v *gocui.View) error {
func (lc *ListContext) handlePrevLine() error {
return lc.handleLineChange(-1)
}
func (lc *ListContext) handleNextLine(g *gocui.Gui, v *gocui.View) error {
func (lc *ListContext) handleNextLine() error {
return lc.handleLineChange(1)
}
@@ -169,19 +185,10 @@ func (lc *ListContext) handleLineChange(change int) error {
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 {
func (lc *ListContext) handleNextPage() error {
view, err := lc.Gui.g.View(lc.ViewName)
if err != nil {
return nil
@@ -191,15 +198,15 @@ func (lc *ListContext) handleNextPage(g *gocui.Gui, v *gocui.View) error {
return lc.handleLineChange(delta)
}
func (lc *ListContext) handleGotoTop(g *gocui.Gui, v *gocui.View) error {
func (lc *ListContext) handleGotoTop() error {
return lc.handleLineChange(-lc.GetItemsLength())
}
func (lc *ListContext) handleGotoBottom(g *gocui.Gui, v *gocui.View) error {
func (lc *ListContext) handleGotoBottom() error {
return lc.handleLineChange(lc.GetItemsLength())
}
func (lc *ListContext) handlePrevPage(g *gocui.Gui, v *gocui.View) error {
func (lc *ListContext) handlePrevPage() error {
view, err := lc.Gui.g.View(lc.ViewName)
if err != nil {
return nil
@@ -210,13 +217,18 @@ func (lc *ListContext) handlePrevPage(g *gocui.Gui, v *gocui.View) error {
return lc.handleLineChange(-delta)
}
func (lc *ListContext) handleClick(g *gocui.Gui, v *gocui.View) error {
func (lc *ListContext) handleClick() error {
if !lc.Gui.isPopupPanel(lc.ViewName) && lc.Gui.popupPanelFocused() {
return nil
}
view, err := lc.Gui.g.View(lc.ViewName)
if err != nil {
return nil
}
prevSelectedLineIdx := lc.GetPanelState().GetSelectedLineIdx()
newSelectedLineIdx := v.SelectedLineIdx()
newSelectedLineIdx := view.SelectedLineIdx()
// we need to focus the view
if err := lc.Gui.pushContext(lc); err != nil {
@@ -245,7 +257,7 @@ func (gui *Gui) menuListContext() *ListContext {
return &ListContext{
ViewName: "menu",
ContextKey: "menu",
GetItemsLength: func() int { return gui.getMenuView().LinesHeight() },
GetItemsLength: func() int { return gui.Views.Menu.LinesHeight() },
GetPanelState: func() IListPanelState { return gui.State.Panels.Menu },
OnFocus: gui.handleMenuSelect,
OnClickSelectedItem: gui.onMenuPress,
@@ -262,7 +274,7 @@ func (gui *Gui) filesListContext() *ListContext {
return &ListContext{
ViewName: "files",
ContextKey: FILES_CONTEXT_KEY,
GetItemsLength: func() int { return len(gui.State.Files) },
GetItemsLength: func() int { return gui.State.FileManager.GetItemsLength() },
GetPanelState: func() IListPanelState { return gui.State.Panels.Files },
OnFocus: gui.focusAndSelectFile,
OnClickSelectedItem: gui.handleFilePress,
@@ -270,10 +282,16 @@ func (gui *Gui) filesListContext() *ListContext {
ResetMainViewOriginOnFocus: false,
Kind: SIDE_CONTEXT,
GetDisplayStrings: func() [][]string {
return presentation.GetFileListDisplayStrings(gui.State.Files, gui.State.Modes.Diffing.Ref, gui.State.Submodules)
lines := gui.State.FileManager.Render(gui.State.Modes.Diffing.Ref, gui.State.Submodules)
mappedLines := make([][]string, len(lines))
for i, line := range lines {
mappedLines[i] = []string{line}
}
return mappedLines
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedFile()
item := gui.getSelectedFileNode()
return item, item != nil
},
}
@@ -446,17 +464,27 @@ func (gui *Gui) commitFilesListContext() *ListContext {
ViewName: "commitFiles",
WindowName: "commits",
ContextKey: COMMIT_FILES_CONTEXT_KEY,
GetItemsLength: func() int { return len(gui.State.CommitFiles) },
GetItemsLength: func() int { return gui.State.CommitFileManager.GetItemsLength() },
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)
if gui.State.CommitFileManager.GetItemsLength() == 0 {
return [][]string{{utils.ColoredString("(none)", color.FgRed)}}
}
lines := gui.State.CommitFileManager.Render(gui.State.Modes.Diffing.Ref, gui.GitCommand.PatchManager)
mappedLines := make([][]string, len(lines))
for i, line := range lines {
mappedLines[i] = []string{line}
}
return mappedLines
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedCommitFile()
item := gui.getSelectedCommitFileNode()
return item, item != nil
},
}
@@ -502,19 +530,20 @@ func (gui *Gui) suggestionsListContext() *ListContext {
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(),
gui.submodulesListContext(),
gui.suggestionsListContext(),
gui.State.Contexts.Menu,
gui.State.Contexts.Files,
gui.State.Contexts.Branches,
gui.State.Contexts.Remotes,
gui.State.Contexts.RemoteBranches,
gui.State.Contexts.Tags,
gui.State.Contexts.BranchCommits,
gui.State.Contexts.BranchCommits,
gui.State.Contexts.ReflogCommits,
gui.State.Contexts.SubCommits,
gui.State.Contexts.Stash,
gui.State.Contexts.CommitFiles,
gui.State.Contexts.Submodules,
gui.State.Contexts.Suggestions,
}
}
@@ -524,17 +553,19 @@ func (gui *Gui) getListContextKeyBindings() []*Binding {
keybindingConfig := gui.Config.GetUserConfig().Keybinding
for _, listContext := range gui.getListContexts() {
listContext := listContext
bindings = append(bindings, []*Binding{
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{listContext.ContextKey}, Key: gui.getKey(keybindingConfig.Universal.PrevItemAlt), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{listContext.ContextKey}, Key: gui.getKey(keybindingConfig.Universal.PrevItem), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{listContext.ContextKey}, Key: gocui.MouseWheelUp, Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{listContext.ContextKey}, Key: gui.getKey(keybindingConfig.Universal.NextItemAlt), Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{listContext.ContextKey}, Key: gui.getKey(keybindingConfig.Universal.NextItem), Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{listContext.ContextKey}, Key: gui.getKey(keybindingConfig.Universal.PrevPage), Modifier: gocui.ModNone, Handler: listContext.handlePrevPage, Description: gui.Tr.LcPrevPage},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{listContext.ContextKey}, Key: gui.getKey(keybindingConfig.Universal.NextPage), Modifier: gocui.ModNone, Handler: listContext.handleNextPage, Description: gui.Tr.LcNextPage},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{listContext.ContextKey}, Key: gui.getKey(keybindingConfig.Universal.GotoTop), Modifier: gocui.ModNone, Handler: listContext.handleGotoTop, Description: gui.Tr.LcGotoTop},
{ViewName: listContext.ViewName, Tag: "navigation", 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},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gui.getKey(keybindingConfig.Universal.PrevItemAlt), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gui.getKey(keybindingConfig.Universal.PrevItem), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gocui.MouseWheelUp, Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gui.getKey(keybindingConfig.Universal.NextItemAlt), Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gui.getKey(keybindingConfig.Universal.NextItem), Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gui.getKey(keybindingConfig.Universal.PrevPage), Modifier: gocui.ModNone, Handler: listContext.handlePrevPage, Description: gui.Tr.LcPrevPage},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gui.getKey(keybindingConfig.Universal.NextPage), Modifier: gocui.ModNone, Handler: listContext.handleNextPage, Description: gui.Tr.LcNextPage},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gui.getKey(keybindingConfig.Universal.GotoTop), Modifier: gocui.ModNone, Handler: listContext.handleGotoTop, Description: gui.Tr.LcGotoTop},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
{ViewName: listContext.ViewName, Contexts: []string{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
@@ -548,15 +579,15 @@ func (gui *Gui) getListContextKeyBindings() []*Binding {
bindings = append(bindings, []*Binding{
{
ViewName: listContext.ViewName,
Contexts: []string{listContext.ContextKey},
Contexts: []string{string(listContext.ContextKey)},
Key: gui.getKey(keybindingConfig.Universal.StartSearch),
Handler: openSearchHandler,
Handler: func() error { return openSearchHandler(listContext.ViewName) },
Description: gui.Tr.LcStartSearch,
Tag: "navigation",
},
{
ViewName: listContext.ViewName,
Contexts: []string{listContext.ContextKey},
Contexts: []string{string(listContext.ContextKey)},
Key: gui.getKey(keybindingConfig.Universal.GotoBottom),
Handler: gotoBottomHandler,
Description: gui.Tr.LcGotoBottom,

View File

@@ -1,6 +1,10 @@
package gui
import "os/exec"
import (
"os/exec"
"github.com/jesseduffield/gocui"
)
type viewUpdateOpts struct {
title string
@@ -20,8 +24,10 @@ type refreshMainOpts struct {
}
// constants for updateTask's kind field
type TaskKind int
const (
RENDER_STRING = iota
RENDER_STRING TaskKind = iota
RENDER_STRING_WITHOUT_SCROLL
RUN_FUNCTION
RUN_COMMAND
@@ -29,18 +35,18 @@ const (
)
type updateTask interface {
GetKind() int
GetKind() TaskKind
}
type renderStringTask struct {
str string
}
func (t *renderStringTask) GetKind() int {
func (t *renderStringTask) GetKind() TaskKind {
return RENDER_STRING
}
func (gui *Gui) createRenderStringTask(str string) *renderStringTask {
func NewRenderStringTask(str string) *renderStringTask {
return &renderStringTask{str: str}
}
@@ -48,11 +54,11 @@ type renderStringWithoutScrollTask struct {
str string
}
func (t *renderStringWithoutScrollTask) GetKind() int {
func (t *renderStringWithoutScrollTask) GetKind() TaskKind {
return RENDER_STRING_WITHOUT_SCROLL
}
func (gui *Gui) createRenderStringWithoutScrollTask(str string) *renderStringWithoutScrollTask {
func NewRenderStringWithoutScrollTask(str string) *renderStringWithoutScrollTask {
return &renderStringWithoutScrollTask{str: str}
}
@@ -61,15 +67,15 @@ type runCommandTask struct {
prefix string
}
func (t *runCommandTask) GetKind() int {
func (t *runCommandTask) GetKind() TaskKind {
return RUN_COMMAND
}
func (gui *Gui) createRunCommandTask(cmd *exec.Cmd) *runCommandTask {
func NewRunCommandTask(cmd *exec.Cmd) *runCommandTask {
return &runCommandTask{cmd: cmd}
}
func (gui *Gui) createRunCommandTaskWithPrefix(cmd *exec.Cmd, prefix string) *runCommandTask {
func NewRunCommandTaskWithPrefix(cmd *exec.Cmd, prefix string) *runCommandTask {
return &runCommandTask{cmd: cmd, prefix: prefix}
}
@@ -78,11 +84,11 @@ type runPtyTask struct {
prefix string
}
func (t *runPtyTask) GetKind() int {
func (t *runPtyTask) GetKind() TaskKind {
return RUN_PTY
}
func (gui *Gui) createRunPtyTask(cmd *exec.Cmd) *runPtyTask {
func NewRunPtyTask(cmd *exec.Cmd) *runPtyTask {
return &runPtyTask{cmd: cmd}
}
@@ -95,7 +101,7 @@ type runFunctionTask struct {
f func(chan struct{}) error
}
func (t *runFunctionTask) GetKind() int {
func (t *runFunctionTask) GetKind() TaskKind {
return RUN_FUNCTION
}
@@ -104,44 +110,38 @@ func (t *runFunctionTask) GetKind() int {
// return &runFunctionTask{f: f}
// }
func (gui *Gui) runTaskForView(viewName string, task updateTask) error {
func (gui *Gui) runTaskForView(view *gocui.View, task updateTask) error {
switch task.GetKind() {
case RENDER_STRING:
specificTask := task.(*renderStringTask)
return gui.newStringTask(viewName, specificTask.str)
return gui.newStringTask(view, specificTask.str)
case RENDER_STRING_WITHOUT_SCROLL:
specificTask := task.(*renderStringWithoutScrollTask)
return gui.newStringTaskWithoutScroll(viewName, specificTask.str)
return gui.newStringTaskWithoutScroll(view, specificTask.str)
case RUN_FUNCTION:
specificTask := task.(*runFunctionTask)
return gui.newTask(viewName, specificTask.f)
return gui.newTask(view, specificTask.f)
case RUN_COMMAND:
specificTask := task.(*runCommandTask)
return gui.newCmdTask(viewName, specificTask.cmd, specificTask.prefix)
return gui.newCmdTask(view, specificTask.cmd, specificTask.prefix)
case RUN_PTY:
specificTask := task.(*runPtyTask)
return gui.newPtyTask(viewName, specificTask.cmd, specificTask.prefix)
return gui.newPtyTask(view, specificTask.cmd, specificTask.prefix)
}
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
}
func (gui *Gui) refreshMainView(opts *viewUpdateOpts, view *gocui.View) error {
view.Title = opts.title
view.Wrap = !opts.noWrap
view.Highlight = opts.highlight
if err := gui.runTaskForView(viewName, opts.task); err != nil {
if err := gui.runTaskForView(view, opts.task); err != nil {
gui.Log.Error(err)
return nil
}
@@ -151,29 +151,24 @@ func (gui *Gui) refreshMainView(opts *viewUpdateOpts, viewName string) error {
func (gui *Gui) refreshMainViews(opts refreshMainOpts) error {
if opts.main != nil {
if err := gui.refreshMainView(opts.main, "main"); err != nil {
if err := gui.refreshMainView(opts.main, gui.Views.Main); err != nil {
return err
}
}
if opts.secondary != nil {
if err := gui.refreshMainView(opts.secondary, gui.Views.Secondary); 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 {

View File

@@ -42,8 +42,7 @@ func (gui *Gui) getMenuOptions() map[string]string {
}
}
func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error {
_ = g.DeleteView("menu")
func (gui *Gui) handleMenuClose() error {
return gui.returnFromContext()
}
@@ -88,7 +87,7 @@ func (gui *Gui) createMenu(title string, items []*menuItem, createMenuOptions cr
gui.State.Panels.Menu.SelectedLineIdx = 0
gui.g.Update(func(g *gocui.Gui) error {
return gui.pushContext(gui.Contexts.Menu.Context)
return gui.pushContext(gui.State.Contexts.Menu)
})
return nil
}

View File

@@ -4,120 +4,146 @@ package gui
import (
"bufio"
"bytes"
"fmt"
"io/ioutil"
"math"
"os"
"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"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts"
)
func (gui *Gui) findConflicts(content string) []commands.Conflict {
conflicts := make([]commands.Conflict, 0)
func (gui *Gui) handleSelectTop() error {
return gui.withMergeConflictLock(func() error {
gui.takeOverMergeConflictScrolling()
gui.State.Panels.Merging.ConflictTop = true
return gui.refreshMergePanel()
})
}
if content == "" {
return conflicts
}
func (gui *Gui) handleSelectBottom() error {
return gui.withMergeConflictLock(func() error {
gui.takeOverMergeConflictScrolling()
gui.State.Panels.Merging.ConflictTop = false
return gui.refreshMergePanel()
})
}
var newConflict commands.Conflict
for i, line := range utils.SplitLines(content) {
trimmedLine := strings.TrimPrefix(line, "++")
if trimmedLine == "<<<<<<< HEAD" || trimmedLine == "<<<<<<< MERGE_HEAD" || trimmedLine == "<<<<<<< Updated upstream" || trimmedLine == "<<<<<<< ours" {
newConflict = commands.Conflict{Start: i}
} else if trimmedLine == "=======" {
newConflict.Middle = i
} else if strings.HasPrefix(trimmedLine, ">>>>>>> ") {
newConflict.End = i
conflicts = append(conflicts, newConflict)
func (gui *Gui) handleSelectNextConflict() error {
return gui.withMergeConflictLock(func() error {
gui.takeOverMergeConflictScrolling()
if gui.State.Panels.Merging.ConflictIndex >= len(gui.State.Panels.Merging.Conflicts)-1 {
return nil
}
}
return conflicts
gui.State.Panels.Merging.ConflictIndex++
return gui.refreshMergePanel()
})
}
func (gui *Gui) shiftConflict(conflicts []commands.Conflict) (commands.Conflict, []commands.Conflict) {
return conflicts[0], conflicts[1:]
}
func (gui *Gui) shouldHighlightLine(index int, conflict commands.Conflict, top bool) bool {
return (index >= conflict.Start && index <= conflict.Middle && top) || (index >= conflict.Middle && index <= conflict.End && !top)
}
func (gui *Gui) coloredConflictFile(content string, conflicts []commands.Conflict, conflictIndex int, conflictTop, hasFocus bool) string {
if len(conflicts) == 0 {
return content
}
conflict, remainingConflicts := gui.shiftConflict(conflicts)
var outputBuffer bytes.Buffer
for i, line := range utils.SplitLines(content) {
colourAttr := theme.DefaultTextColor
if i == conflict.Start || i == conflict.Middle || i == conflict.End {
colourAttr = color.FgRed
func (gui *Gui) handleSelectPrevConflict() error {
return gui.withMergeConflictLock(func() error {
gui.takeOverMergeConflictScrolling()
if gui.State.Panels.Merging.ConflictIndex <= 0 {
return nil
}
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)
}
outputBuffer.WriteString(utils.ColoredStringDirect(line, colour) + "\n")
}
return outputBuffer.String()
gui.State.Panels.Merging.ConflictIndex--
return gui.refreshMergePanel()
})
}
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 {
func (gui *Gui) pushFileSnapshot() error {
gitFile := gui.getSelectedFile()
if gitFile == nil {
return nil
}
gui.State.Panels.Merging.ConflictIndex++
return gui.refreshMergePanel()
content, err := gui.GitCommand.CatFile(gitFile.Name)
if err != nil {
return err
}
gui.State.Panels.Merging.EditHistory.Push(content)
return nil
}
func (gui *Gui) handleSelectPrevConflict(g *gocui.Gui, v *gocui.View) error {
gui.takeOverScrolling()
if gui.State.Panels.Merging.ConflictIndex <= 0 {
func (gui *Gui) handlePopFileSnapshot() error {
if gui.State.Panels.Merging.EditHistory.Len() == 0 {
return nil
}
gui.State.Panels.Merging.ConflictIndex--
prevContent := gui.State.Panels.Merging.EditHistory.Pop().(string)
gitFile := gui.getSelectedFile()
if gitFile == nil {
return nil
}
if err := ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644); err != nil {
return err
}
return gui.refreshMergePanel()
}
func (gui *Gui) isIndexToDelete(i int, conflict commands.Conflict, pick string) bool {
return i == conflict.Middle ||
i == conflict.Start ||
i == conflict.End ||
pick != "both" &&
(pick == "bottom" && i > conflict.Start && i < conflict.Middle) ||
(pick == "top" && i > conflict.Middle && i < conflict.End)
func (gui *Gui) handlePickHunk() error {
return gui.withMergeConflictLock(func() error {
conflict := gui.getCurrentConflict()
if conflict == nil {
return nil
}
gui.takeOverMergeConflictScrolling()
if err := gui.pushFileSnapshot(); err != nil {
return err
}
selection := mergeconflicts.BOTTOM
if gui.State.Panels.Merging.ConflictTop {
selection = mergeconflicts.TOP
}
err := gui.resolveConflict(*conflict, selection)
if err != nil {
panic(err)
}
// if that was the last conflict, finish the merge for this file
if len(gui.State.Panels.Merging.Conflicts) == 1 {
if err := gui.handleCompleteMerge(); err != nil {
return err
}
}
return gui.refreshMergePanel()
})
}
func (gui *Gui) resolveConflict(conflict commands.Conflict, pick string) error {
func (gui *Gui) handlePickBothHunks() error {
return gui.withMergeConflictLock(func() error {
conflict := gui.getCurrentConflict()
if conflict == nil {
return nil
}
gui.takeOverMergeConflictScrolling()
if err := gui.pushFileSnapshot(); err != nil {
return err
}
err := gui.resolveConflict(*conflict, mergeconflicts.BOTH)
if err != nil {
panic(err)
}
return gui.refreshMergePanel()
})
}
func (gui *Gui) getCurrentConflict() *commands.Conflict {
if len(gui.State.Panels.Merging.Conflicts) == 0 {
return nil
}
return &gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex]
}
func (gui *Gui) resolveConflict(conflict commands.Conflict, selection mergeconflicts.Selection) error {
gitFile := gui.getSelectedFile()
if gitFile == nil {
return nil
@@ -135,80 +161,15 @@ func (gui *Gui) resolveConflict(conflict commands.Conflict, pick string) error {
if err != nil {
break
}
if !gui.isIndexToDelete(i, conflict, pick) {
if !mergeconflicts.IsIndexToDelete(i, conflict, selection) {
output += line
}
}
return ioutil.WriteFile(gitFile.Name, []byte(output), 0644)
}
func (gui *Gui) pushFileSnapshot() error {
gitFile := gui.getSelectedFile()
if gitFile == nil {
return nil
}
content, err := gui.GitCommand.CatFile(gitFile.Name)
if err != nil {
return err
}
gui.State.Panels.Merging.EditHistory.Push(content)
return nil
}
func (gui *Gui) handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error {
if gui.State.Panels.Merging.EditHistory.Len() == 0 {
return nil
}
prevContent := gui.State.Panels.Merging.EditHistory.Pop().(string)
gitFile := gui.getSelectedFile()
if gitFile == nil {
return nil
}
if err := ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644); err != nil {
return err
}
return gui.refreshMergePanel()
}
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(); err != nil {
return err
}
pick := "bottom"
if gui.State.Panels.Merging.ConflictTop {
pick = "top"
}
err := gui.resolveConflict(conflict, pick)
if err != nil {
panic(err)
}
// if that was the last conflict, finish the merge for this file
if len(gui.State.Panels.Merging.Conflicts) == 1 {
if err := gui.handleCompleteMerge(); err != nil {
return err
}
}
return gui.refreshMergePanel()
}
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(); err != nil {
return err
}
err := gui.resolveConflict(conflict, "both")
if err != nil {
panic(err)
}
return gui.refreshMergePanel()
func (gui *Gui) refreshMergePanelWithLock() error {
return gui.withMergeConflictLock(gui.refreshMergePanel)
}
func (gui *Gui) refreshMergePanel() error {
@@ -218,12 +179,12 @@ func (gui *Gui) refreshMergePanel() error {
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: "",
task: gui.createRenderStringTask(err.Error()),
task: NewRenderStringTask(err.Error()),
},
})
}
panelState.Conflicts = gui.findConflicts(cat)
panelState.Conflicts = mergeconflicts.FindConflicts(cat)
// handle potential fixes that the user made in their editor since we last refreshed
if len(panelState.Conflicts) == 0 {
@@ -233,7 +194,7 @@ func (gui *Gui) refreshMergePanel() error {
}
hasFocus := gui.currentViewName() == "main"
content := gui.coloredConflictFile(cat, panelState.Conflicts, panelState.ConflictIndex, panelState.ConflictTop, hasFocus)
content := mergeconflicts.ColoredConflictFile(cat, panelState.Conflicts, panelState.ConflictIndex, panelState.ConflictTop, hasFocus)
if err := gui.scrollToConflict(); err != nil {
return err
@@ -242,7 +203,7 @@ func (gui *Gui) refreshMergePanel() error {
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: gui.Tr.MergeConflictsTitle,
task: gui.createRenderStringWithoutScrollTask(content),
task: NewRenderStringWithoutScrollTask(content),
noWrap: true,
},
})
@@ -275,7 +236,8 @@ func (gui *Gui) scrollToConflict() error {
if len(panelState.Conflicts) == 0 {
return nil
}
mergingView := gui.getMainView()
mergingView := gui.Views.Main
conflict := panelState.Conflicts[panelState.ConflictIndex]
ox, _ := mergingView.Origin()
_, height := mergingView.Size()
@@ -300,16 +262,16 @@ func (gui *Gui) getMergingOptions() map[string]string {
}
func (gui *Gui) handleEscapeMerge() error {
gui.takeOverScrolling()
gui.takeOverMergeConflictScrolling()
gui.State.Panels.Merging.EditHistory = stack.New()
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
if err := gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{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.pushContext(gui.Contexts.Files.Context)
if gui.g.CurrentView() == gui.Views.Main {
return gui.pushContext(gui.State.Contexts.Files)
}
return nil
}
@@ -318,7 +280,7 @@ func (gui *Gui) handleCompleteMerge() error {
if err := gui.stageSelectedFile(); err != nil {
return err
}
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
if err := gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}); err != nil {
return err
}
// if we got conflicts after unstashing, we don't want to call any git
@@ -328,35 +290,35 @@ func (gui *Gui) handleCompleteMerge() error {
}
// 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.promptToContinueRebase()
}
return gui.handleEscapeMerge()
}
// promptToContinue asks the user if they want to continue the rebase/merge that's in progress
func (gui *Gui) promptToContinue() error {
gui.takeOverScrolling()
// promptToContinueRebase asks the user if they want to continue the rebase/merge that's in progress
func (gui *Gui) promptToContinueRebase() error {
gui.takeOverMergeConflictScrolling()
return gui.ask(askOpts{
title: "continue",
prompt: gui.Tr.ConflictsResolved,
handlersManageFocus: true,
handleConfirm: func() error {
if err := gui.pushContext(gui.Contexts.Files.Context); err != nil {
if err := gui.pushContext(gui.State.Contexts.Files); err != nil {
return err
}
return gui.genericMergeCommand("continue")
},
handleClose: func() error {
return gui.pushContext(gui.Contexts.Files.Context)
return gui.pushContext(gui.State.Contexts.Files)
},
})
}
func (gui *Gui) canScrollMergePanel() bool {
currentViewName := gui.currentViewName()
if currentViewName != "main" {
currentView := gui.g.CurrentView()
if currentView != gui.Views.Main && currentView != gui.Views.Files {
return false
}
@@ -367,3 +329,14 @@ func (gui *Gui) canScrollMergePanel() bool {
return file.HasInlineMergeConflicts
}
func (gui *Gui) withMergeConflictLock(f func() error) error {
gui.State.Panels.Merging.ConflictsMutex.Lock()
defer gui.State.Panels.Merging.ConflictsMutex.Unlock()
return f()
}
func (gui *Gui) takeOverMergeConflictScrolling() {
gui.State.Panels.Merging.UserScrolling = false
}

View File

@@ -0,0 +1,86 @@
package mergeconflicts
import (
"bytes"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type Selection int
const (
TOP Selection = iota
BOTTOM
BOTH
)
func 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, "++")
switch trimmedLine {
case "<<<<<<< HEAD", "<<<<<<< MERGE_HEAD", "<<<<<<< Updated upstream", "<<<<<<< ours":
newConflict = commands.Conflict{Start: i}
case "=======":
newConflict.Middle = i
default:
if strings.HasPrefix(trimmedLine, ">>>>>>> ") {
newConflict.End = i
conflicts = append(conflicts, newConflict)
}
}
}
return conflicts
}
func ColoredConflictFile(content string, conflicts []commands.Conflict, conflictIndex int, conflictTop, hasFocus bool) string {
if len(conflicts) == 0 {
return content
}
conflict, remainingConflicts := shiftConflict(conflicts)
var outputBuffer bytes.Buffer
for i, line := range utils.SplitLines(content) {
colourAttr := theme.DefaultTextColor
if i == conflict.Start || i == conflict.Middle || i == conflict.End {
colourAttr = color.FgRed
}
colour := color.New(colourAttr)
if hasFocus && conflictIndex < len(conflicts) && conflicts[conflictIndex] == conflict && shouldHighlightLine(i, conflict, conflictTop) {
colour.Add(color.Bold)
colour.Add(theme.SelectedRangeBgColor)
}
if i == conflict.End && len(remainingConflicts) > 0 {
conflict, remainingConflicts = shiftConflict(remainingConflicts)
}
outputBuffer.WriteString(utils.ColoredStringDirect(line, colour) + "\n")
}
return outputBuffer.String()
}
func IsIndexToDelete(i int, conflict commands.Conflict, selection Selection) bool {
return i == conflict.Middle ||
i == conflict.Start ||
i == conflict.End ||
selection != BOTH &&
(selection == BOTTOM && i > conflict.Start && i < conflict.Middle) ||
(selection == TOP && i > conflict.Middle && i < conflict.End)
}
func shiftConflict(conflicts []commands.Conflict) (commands.Conflict, []commands.Conflict) {
return conflicts[0], conflicts[1:]
}
func shouldHighlightLine(index int, conflict commands.Conflict, top bool) bool {
return (index >= conflict.Start && index <= conflict.Middle && top) || (index >= conflict.Middle && index <= conflict.End && !top)
}

View File

@@ -25,17 +25,6 @@ func (gui *Gui) modeStatuses() []modeStatus {
},
reset: gui.exitDiffMode,
},
{
isActive: gui.State.Modes.Filtering.Active,
description: func() string {
return utils.ColoredString(
fmt.Sprintf("%s '%s' %s", gui.Tr.LcFilteringBy, gui.State.Modes.Filtering.Path, utils.ColoredString(gui.Tr.ResetInParentheses, color.Underline)),
color.FgRed,
color.Bold,
)
},
reset: gui.exitFilterMode,
},
{
isActive: gui.GitCommand.PatchManager.Active,
description: func() string {
@@ -47,6 +36,17 @@ func (gui *Gui) modeStatuses() []modeStatus {
},
reset: gui.handleResetPatch,
},
{
isActive: gui.State.Modes.Filtering.Active,
description: func() string {
return utils.ColoredString(
fmt.Sprintf("%s '%s' %s", gui.Tr.LcFilteringBy, gui.State.Modes.Filtering.GetPath(), utils.ColoredString(gui.Tr.ResetInParentheses, color.Underline)),
color.FgRed,
color.Bold,
)
},
reset: gui.exitFilterMode,
},
{
isActive: gui.State.Modes.CherryPicking.Active,
description: func() string {

View File

@@ -0,0 +1,25 @@
package filtering
type Filtering struct {
path string // the filename that gets passed to git log
}
func NewFiltering(path string) Filtering {
return Filtering{path: path}
}
func (m *Filtering) Active() bool {
return m.path != ""
}
func (m *Filtering) Reset() {
m.path = ""
}
func (m *Filtering) SetPath(path string) {
m.path = path
}
func (m *Filtering) GetPath() string {
return m.path
}

View File

@@ -45,8 +45,13 @@ func (gui *Gui) displayDescription(binding *Binding) string {
return commandColor.Sprint(binding.Description)
}
func (gui *Gui) handleCreateOptionsMenu(g *gocui.Gui, v *gocui.View) error {
bindings := gui.getBindings(v)
func (gui *Gui) handleCreateOptionsMenu() error {
view := gui.g.CurrentView()
if view == nil {
return nil
}
bindings := gui.getBindings(view)
menuItems := make([]*menuItem, len(bindings))
@@ -58,10 +63,10 @@ func (gui *Gui) handleCreateOptionsMenu(g *gocui.Gui, v *gocui.View) error {
if binding.Key == nil {
return nil
}
if err := gui.handleMenuClose(g, v); err != nil {
if err := gui.handleMenuClose(); err != nil {
return err
}
return binding.Handler(g, v)
return binding.Handler()
},
}
}

View File

@@ -22,25 +22,23 @@ func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int, state *lBlPanelSt
return gui.handleEscapePatchBuildingPanel()
}
gui.splitMainPanel(true)
gui.getMainView().Title = "Patch"
gui.getSecondaryView().Title = "Custom Patch"
gui.Views.Main.Title = "Patch"
gui.Views.Secondary.Title = "Custom Patch"
// get diff from commit file that's currently selected
commitFile := gui.getSelectedCommitFile()
if commitFile == nil {
node := gui.getSelectedCommitFileNode()
if node == nil {
return nil
}
to := commitFile.Parent
to := gui.State.CommitFileManager.GetParent()
from, reverse := gui.getFromAndReverseArgsForDiff(to)
diff, err := gui.GitCommand.ShowFileDiff(from, to, reverse, commitFile.Name, true)
diff, err := gui.GitCommand.ShowFileDiff(from, to, reverse, node.GetPath(), true)
if err != nil {
return err
}
secondaryDiff := gui.GitCommand.PatchManager.RenderPatchForFile(commitFile.Name, true, false, true)
secondaryDiff := gui.GitCommand.PatchManager.RenderPatchForFile(node.GetPath(), true, false, true)
if err != nil {
return err
}
@@ -78,12 +76,12 @@ func (gui *Gui) handleToggleSelectionForPatch() error {
}
// add range of lines to those set for the file
commitFile := gui.getSelectedCommitFile()
if commitFile == nil {
node := gui.getSelectedCommitFileNode()
if node == nil {
return nil
}
if err := toggleFunc(commitFile.Name, state.FirstLineIdx, state.LastLineIdx); err != nil {
if err := toggleFunc(node.GetPath(), state.FirstLineIdx, state.LastLineIdx); err != nil {
// might actually want to return an error here
gui.Log.Error(err)
}
@@ -109,8 +107,8 @@ func (gui *Gui) handleEscapePatchBuildingPanel() error {
gui.GitCommand.PatchManager.Reset()
}
if gui.currentContext().GetKey() == gui.Contexts.PatchBuilding.Context.GetKey() {
return gui.pushContext(gui.Contexts.CommitFiles.Context)
if gui.currentContext().GetKey() == gui.State.Contexts.PatchBuilding.GetKey() {
return gui.pushContext(gui.State.Contexts.CommitFiles)
} else {
// need to re-focus in case the secondary view should now be hidden
return gui.currentContext().HandleFocus()
@@ -125,7 +123,7 @@ func (gui *Gui) secondaryPatchPanelUpdateOpts() *viewUpdateOpts {
title: "Custom Patch",
noWrap: true,
highlight: true,
task: gui.createRenderStringWithoutScrollTask(patch),
task: NewRenderStringWithoutScrollTask(patch),
}
}

View File

@@ -3,11 +3,10 @@ package gui
import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
)
func (gui *Gui) handleCreatePatchOptionsMenu(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCreatePatchOptionsMenu() error {
if !gui.GitCommand.PatchManager.Active() {
return gui.createErrorPanel(gui.Tr.NoPatchError)
}
@@ -43,7 +42,7 @@ func (gui *Gui) handleCreatePatchOptionsMenu(g *gocui.Gui, v *gocui.View) error
},
}...)
if gui.currentContext().GetKey() == gui.Contexts.BranchCommits.Context.GetKey() {
if gui.currentContext().GetKey() == gui.State.Contexts.BranchCommits.GetKey() {
selectedCommit := gui.getSelectedLocalCommit()
if selectedCommit != nil && gui.GitCommand.PatchManager.To != selectedCommit.Sha {
// adding this option to index 1
@@ -180,7 +179,7 @@ func (gui *Gui) handleApplyPatch(reverse bool) error {
func (gui *Gui) handleResetPatch() error {
gui.GitCommand.PatchManager.Reset()
if gui.currentContextKeyIgnoringPopups() == MAIN_PATCH_BUILDING_CONTEXT_KEY {
if err := gui.pushContext(gui.Contexts.CommitFiles.Context); err != nil {
if err := gui.pushContext(gui.State.Contexts.CommitFiles); err != nil {
return err
}
}

View File

@@ -8,41 +8,31 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
func GetCommitFileListDisplayStrings(commitFiles []*models.CommitFile, diffName string) [][]string {
if len(commitFiles) == 0 {
return [][]string{{utils.ColoredString("(none)", color.FgRed)}}
}
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 *models.CommitFile, diffed bool) []string {
func GetCommitFileLine(name string, diffName string, commitFile *models.CommitFile, status patch.PatchStatus) 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.PatchStatus {
case patch.UNSELECTED:
colour = defaultColor
case patch.WHOLE:
colour = green
case patch.PART:
colour = yellow
}
if diffed {
colour := defaultColor
if diffName == name {
colour = diffTerminalColor
} else {
switch status {
case patch.UNSELECTED:
colour = defaultColor
case patch.WHOLE:
colour = green
case patch.PART:
colour = yellow
}
}
return []string{utils.ColoredString(f.ChangeStatus, getColorForChangeStatus(f.ChangeStatus)), colour.Sprint(f.Name)}
if commitFile == nil {
return colour.Sprint(name)
}
return utils.ColoredString(commitFile.ChangeStatus, getColorForChangeStatus(commitFile.ChangeStatus)) + " " + colour.Sprint(name)
}
func getColorForChangeStatus(changeStatus string) color.Attribute {

View File

@@ -7,57 +7,52 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
func GetFileListDisplayStrings(files []*models.File, diffName string, submoduleConfigs []*models.SubmoduleConfig) [][]string {
lines := make([][]string, len(files))
for i := range files {
diffed := files[i].Name == diffName
lines[i] = getFileDisplayStrings(files[i], diffed, submoduleConfigs)
}
return lines
}
// getFileDisplayStrings returns the display string of branch
func getFileDisplayStrings(f *models.File, diffed bool, submoduleConfigs []*models.SubmoduleConfig) []string {
func GetFileLine(hasUnstagedChanges bool, hasStagedChanges bool, name string, diffName string, submoduleConfigs []*models.SubmoduleConfig, file *models.File) 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)}
}
partiallyModifiedColor := color.New(color.FgYellow)
var restColor *color.Color
if diffed {
if name == diffName {
restColor = diffColor
} else if f.HasUnstagedChanges {
} else if file == nil && hasStagedChanges && hasUnstagedChanges {
restColor = partiallyModifiedColor
} else if hasUnstagedChanges {
restColor = red
} else {
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
output := ""
if file != nil {
// this is just making things look nice when the background attribute is 'reverse'
firstChar := file.ShortStatus[0:1]
firstCharCl := green
if firstChar == "?" {
firstCharCl = red
} else if firstChar == " " {
firstCharCl = restColor
}
secondChar := file.ShortStatus[1:2]
secondCharCl := red
if secondChar == " " {
secondCharCl = restColor
}
output = firstCharCl.Sprint(firstChar)
output += secondCharCl.Sprint(secondChar)
output += restColor.Sprint(" ")
}
secondChar := f.DisplayString[1:2]
secondCharCl := red
if secondChar == " " {
secondCharCl = restColor
}
output += restColor.Sprint(name)
output := firstCharCl.Sprint(firstChar)
output += secondCharCl.Sprint(secondChar)
output += restColor.Sprintf(" %s", f.Name)
if f.IsSubmodule(submoduleConfigs) {
if file != nil && file.IsSubmodule(submoduleConfigs) {
output += utils.ColoredString(" (submodule)", theme.DefaultTextColor)
}
return []string{output}
return output
}

View File

@@ -6,14 +6,14 @@ import (
"os/exec"
"github.com/creack/pty"
"github.com/jesseduffield/gocui"
)
func (gui *Gui) onResize() error {
if gui.State.Ptmx == nil {
return nil
}
mainView := gui.getMainView()
width, height := mainView.Size()
width, height := gui.Views.Main.Size()
if err := pty.Setsize(gui.State.Ptmx, &pty.Winsize{Cols: uint16(width), Rows: uint16(height)}); err != nil {
return err
@@ -30,22 +30,17 @@ func (gui *Gui) onResize() error {
// which is just an io.Reader. the pty package lets us wrap a command in a
// pseudo-terminal meaning we'll get the behaviour we want from the underlying
// command.
func (gui *Gui) newPtyTask(viewName string, cmd *exec.Cmd, prefix string) error {
width, _ := gui.getMainView().Size()
func (gui *Gui) newPtyTask(view *gocui.View, cmd *exec.Cmd, prefix string) error {
width, _ := gui.Views.Main.Size()
pager := gui.GitCommand.GetPager(width)
if pager == "" {
// if we're not using a custom pager we don't need to use a pty
return gui.newCmdTask(viewName, cmd, prefix)
return gui.newCmdTask(view, cmd, prefix)
}
cmd.Env = append(cmd.Env, "GIT_PAGER="+pager)
view, err := gui.g.View(viewName)
if err != nil {
return nil // swallowing for now
}
_, height := view.Size()
_, oy := view.Origin()

View File

@@ -2,12 +2,16 @@
package gui
import "os/exec"
import (
"os/exec"
"github.com/jesseduffield/gocui"
)
func (gui *Gui) onResize() error {
return nil
}
func (gui *Gui) newPtyTask(viewName string, cmd *exec.Cmd, prefix string) error {
return gui.newCmdTask(viewName, cmd, prefix)
func (gui *Gui) newPtyTask(view *gocui.View, cmd *exec.Cmd, prefix string) error {
return gui.newCmdTask(view, cmd, prefix)
}

View File

@@ -24,7 +24,7 @@ func (gui *Gui) recordCurrentDirectory() error {
return gui.OSCommand.CreateFileWithContent(os.Getenv("LAZYGIT_NEW_DIR_FILE"), dirName)
}
func (gui *Gui) handleQuitWithoutChangingDirectory(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleQuitWithoutChangingDirectory() error {
gui.State.RetainOriginalDir = true
return gui.quit()
}
@@ -34,7 +34,7 @@ func (gui *Gui) handleQuit() error {
return gui.quit()
}
func (gui *Gui) handleTopLevelReturn(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleTopLevelReturn() error {
currentContext := gui.currentContext()
parentContext, hasParent := currentContext.GetParentContext()
@@ -49,15 +49,15 @@ func (gui *Gui) handleTopLevelReturn(g *gocui.Gui, v *gocui.View) error {
}
}
repoPathStack := gui.State.RepoPathStack
repoPathStack := gui.RepoPathStack
if len(repoPathStack) > 0 {
n := len(repoPathStack) - 1
path := repoPathStack[n]
gui.State.RepoPathStack = repoPathStack[:n]
gui.RepoPathStack = repoPathStack[:n]
return gui.dispatchSwitchToRepo(path)
return gui.dispatchSwitchToRepo(path, true)
}
if gui.Config.GetUserConfig().QuitOnTopLevelReturn {

View File

@@ -50,8 +50,7 @@ func (gui *Gui) genericMergeCommand(command string) error {
if status == commands.REBASE_MODE_MERGING && command != "abort" && gui.Config.GetUserConfig().Git.Merging.ManualCommit {
sub := gui.OSCommand.PrepareSubProcess("git", commandType, fmt.Sprintf("--%s", command))
if sub != nil {
gui.SubProcess = sub
return gui.Errors.ErrSubProcess
return gui.runSubprocessWithSuspense(sub)
}
return nil
}
@@ -68,8 +67,6 @@ func (gui *Gui) handleGenericMergeCommandResult(result error) error {
}
if result == nil {
return nil
} else if result == gui.Errors.ErrSubProcess {
return result
} else if strings.Contains(result.Error(), "No changes - did you forget to use") {
return gui.genericMergeCommand("skip")
} else if strings.Contains(result.Error(), "The previous cherry-pick is now empty") {
@@ -83,7 +80,7 @@ func (gui *Gui) handleGenericMergeCommandResult(result error) error {
prompt: gui.Tr.FoundConflicts,
handlersManageFocus: true,
handleConfirm: func() error {
return gui.pushContext(gui.Contexts.Files.Context)
return gui.pushContext(gui.State.Contexts.Files)
},
handleClose: func() error {
if err := gui.returnFromContext(); err != nil {

View File

@@ -5,6 +5,7 @@ import (
"path/filepath"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/env"
"github.com/jesseduffield/lazygit/pkg/utils"
@@ -24,7 +25,10 @@ func (gui *Gui) handleCreateRecentReposMenu() error {
yellow.Sprint(path),
},
onPress: func() error {
return gui.dispatchSwitchToRepo(path)
// if we were in a submodule, we want to forget about that stack of repos
// so that hitting escape in the new repo does nothing
gui.RepoPathStack = []string{}
return gui.dispatchSwitchToRepo(path, false)
},
}
}
@@ -36,7 +40,7 @@ func (gui *Gui) handleShowAllBranchLogs() error {
cmd := gui.OSCommand.ExecutableFromString(
gui.Config.GetUserConfig().Git.AllBranchesLogCmd,
)
task := gui.createRunPtyTask(cmd)
task := NewRunPtyTask(cmd)
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
@@ -46,18 +50,49 @@ func (gui *Gui) handleShowAllBranchLogs() error {
})
}
func (gui *Gui) dispatchSwitchToRepo(path string) error {
func (gui *Gui) dispatchSwitchToRepo(path string, reuse bool) error {
env.UnsetGitDirEnvs()
originalPath, err := os.Getwd()
if err != nil {
return nil
}
if err := os.Chdir(path); err != nil {
if os.IsNotExist(err) {
return gui.createErrorPanel(gui.Tr.ErrRepositoryMovedOrDeleted)
}
return err
}
if err := commands.VerifyInGitRepo(gui.OSCommand); err != nil {
if err := os.Chdir(originalPath); err != nil {
return err
}
return err
}
newGitCommand, err := commands.NewGitCommand(gui.Log, gui.OSCommand, gui.Tr, gui.Config)
if err != nil {
return err
}
gui.GitCommand = newGitCommand
gui.State.Modes.Filtering.Path = ""
return gui.Errors.ErrSwitchRepo
gui.g.Update(func(*gocui.Gui) error {
// these two mutexes are used by our background goroutines (triggered via `gui.goEvery`. We don't want to
// switch to a repo while one of these goroutines is in the process of updating something
gui.Mutexes.FetchMutex.Lock()
defer gui.Mutexes.FetchMutex.Unlock()
gui.Mutexes.RefreshingFilesMutex.Lock()
defer gui.Mutexes.RefreshingFilesMutex.Unlock()
gui.resetState("", reuse)
return nil
})
return nil
}
// updateRecentRepoList registers the fact that we opened lazygit in this repo,

View File

@@ -6,10 +6,8 @@ import (
"log"
"os"
"strconv"
"time"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func recordingEvents() bool {
@@ -20,82 +18,29 @@ func recordEventsTo() string {
return os.Getenv("RECORD_EVENTS_TO")
}
func (gui *Gui) timeSinceStart() int64 {
return time.Since(gui.StartTime).Nanoseconds() / 1e6
func replaying() bool {
return os.Getenv("REPLAY_EVENTS_FROM") != ""
}
func (gui *Gui) replayRecordedEvents() {
if os.Getenv("REPLAY_EVENTS_FROM") == "" {
return
}
func headless() bool {
return os.Getenv("HEADLESS") != ""
}
go utils.Safe(func() {
time.Sleep(time.Second * 20)
log.Fatal("20 seconds is up, lazygit recording took too long to complete")
})
events, err := gui.loadRecordedEvents()
if err != nil {
log.Fatal(err)
}
ticker := time.NewTicker(time.Millisecond)
defer ticker.Stop()
// might need to add leeway if this ends up flakey
var leeway int64 = 0
func getRecordingSpeed() float64 {
// humans are slow so this speeds things up.
speed := 1
envReplaySpeed := os.Getenv("REPLAY_SPEED")
speed := 1.0
envReplaySpeed := os.Getenv("SPEED")
if envReplaySpeed != "" {
var err error
speed, err = strconv.Atoi(envReplaySpeed)
speed, err = strconv.ParseFloat(envReplaySpeed, 64)
if err != nil {
log.Fatal(err)
}
}
// The playback could be paused at any time because integration tests run concurrently.
// Therefore we can't just check for a given event whether we've passed its timestamp,
// or else we'll have an explosion of keypresses after the test is resumed.
// We need to check if we've waited long enough since the last event was replayed.
for i, event := range events {
var prevEventTimestamp int64 = 0
if i > 0 {
prevEventTimestamp = events[i-1].Timestamp
}
timeToWait := (event.Timestamp - prevEventTimestamp) / int64(speed)
if i == 0 {
timeToWait += leeway
}
var timeWaited int64 = 0
middle:
for {
select {
case <-ticker.C:
timeWaited += 1
if gui.g != nil && timeWaited >= timeToWait {
gui.g.ReplayedEvents <- *event.Event
break middle
}
case <-gui.stopChan:
return
}
}
}
time.Sleep(time.Second * 1)
gui.g.Update(func(*gocui.Gui) error {
return gocui.ErrQuit
})
time.Sleep(time.Second * 1)
log.Fatal("lazygit should have already exited")
return speed
}
func (gui *Gui) loadRecordedEvents() ([]RecordedEvent, error) {
func (gui *Gui) loadRecording() (*gocui.Recording, error) {
path := os.Getenv("REPLAY_EVENTS_FROM")
data, err := ioutil.ReadFile(path)
@@ -103,22 +48,22 @@ func (gui *Gui) loadRecordedEvents() ([]RecordedEvent, error) {
return nil, err
}
events := []RecordedEvent{}
recording := &gocui.Recording{}
err = json.Unmarshal(data, &events)
err = json.Unmarshal(data, &recording)
if err != nil {
return nil, err
}
return events, nil
return recording, nil
}
func (gui *Gui) saveRecordedEvents() error {
func (gui *Gui) saveRecording(recording *gocui.Recording) error {
if !recordingEvents() {
return nil
}
jsonEvents, err := json.Marshal(gui.RecordedEvents)
jsonEvents, err := json.Marshal(recording)
if err != nil {
return err
}
@@ -127,14 +72,3 @@ func (gui *Gui) saveRecordedEvents() error {
return ioutil.WriteFile(path, jsonEvents, 0600)
}
func (gui *Gui) recordEvents() {
for event := range gui.g.RecordedEvents {
recordedEvent := RecordedEvent{
Timestamp: gui.timeSinceStart(),
Event: event,
}
gui.RecordedEvents = append(gui.RecordedEvents, recordedEvent)
}
}

View File

@@ -1,7 +1,6 @@
package gui
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/models"
)
@@ -21,13 +20,13 @@ func (gui *Gui) handleReflogCommitSelect() error {
commit := gui.getSelectedReflogCommit()
var task updateTask
if commit == nil {
task = gui.createRenderStringTask("No reflog history")
task = NewRenderStringTask("No reflog history")
} else {
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.Modes.Filtering.Path),
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.Modes.Filtering.GetPath()),
)
task = gui.createRunPtyTask(cmd)
task = NewRunPtyTask(cmd)
}
return gui.refreshMainViews(refreshMainOpts{
@@ -73,17 +72,17 @@ func (gui *Gui) refreshReflogCommits() error {
}
if gui.State.Modes.Filtering.Active() {
if err := refresh(&state.FilteredReflogCommits, state.Modes.Filtering.Path); err != nil {
if err := refresh(&state.FilteredReflogCommits, state.Modes.Filtering.GetPath()); err != nil {
return err
}
} else {
state.FilteredReflogCommits = state.ReflogCommits
}
return gui.postRefreshUpdate(gui.Contexts.ReflogCommits.Context)
return gui.postRefreshUpdate(gui.State.Contexts.ReflogCommits)
}
func (gui *Gui) handleCheckoutReflogCommit(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCheckoutReflogCommit() error {
commit := gui.getSelectedReflogCommit()
if commit == nil {
return nil
@@ -105,7 +104,7 @@ func (gui *Gui) handleCheckoutReflogCommit(g *gocui.Gui, v *gocui.View) error {
return nil
}
func (gui *Gui) handleCreateReflogResetMenu(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCreateReflogResetMenu() error {
commit := gui.getSelectedReflogCommit()
return gui.createResetMenu(commit.Sha)
@@ -117,5 +116,5 @@ func (gui *Gui) handleViewReflogCommitFiles() error {
return nil
}
return gui.switchToCommitFilesContext(commit.Sha, false, gui.Contexts.ReflogCommits.Context, "commits")
return gui.switchToCommitFilesContext(commit.Sha, false, gui.State.Contexts.ReflogCommits, "commits")
}

View File

@@ -3,7 +3,6 @@ package gui
import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -23,12 +22,12 @@ func (gui *Gui) handleRemoteBranchSelect() error {
var task updateTask
remoteBranch := gui.getSelectedRemoteBranch()
if remoteBranch == nil {
task = gui.createRenderStringTask("No branches for this remote")
task = NewRenderStringTask("No branches for this remote")
} else {
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.GetBranchGraphCmdStr(remoteBranch.FullName()),
)
task = gui.createRunCommandTask(cmd)
task = NewRunCommandTask(cmd)
}
return gui.refreshMainViews(refreshMainOpts{
@@ -39,16 +38,16 @@ func (gui *Gui) handleRemoteBranchSelect() error {
})
}
func (gui *Gui) handleRemoteBranchesEscape(g *gocui.Gui, v *gocui.View) error {
return gui.pushContext(gui.Contexts.Remotes.Context)
func (gui *Gui) handleRemoteBranchesEscape() error {
return gui.pushContext(gui.State.Contexts.Remotes)
}
func (gui *Gui) handleMergeRemoteBranch(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleMergeRemoteBranch() error {
selectedBranchName := gui.getSelectedRemoteBranch().FullName()
return gui.mergeBranchIntoCheckedOutBranch(selectedBranchName)
}
func (gui *Gui) handleDeleteRemoteBranch(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleDeleteRemoteBranch() error {
remoteBranch := gui.getSelectedRemoteBranch()
if remoteBranch == nil {
return nil
@@ -63,18 +62,18 @@ func (gui *Gui) handleDeleteRemoteBranch(g *gocui.Gui, v *gocui.View) error {
err := gui.GitCommand.DeleteRemoteBranch(remoteBranch.RemoteName, remoteBranch.Name, gui.promptUserForCredential)
gui.handleCredentialsPopup(err)
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, REMOTES}})
})
},
})
}
func (gui *Gui) handleRebaseOntoRemoteBranch(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleRebaseOntoRemoteBranch() error {
selectedBranchName := gui.getSelectedRemoteBranch().FullName()
return gui.handleRebaseOntoBranch(selectedBranchName)
}
func (gui *Gui) handleSetBranchUpstream(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleSetBranchUpstream() error {
selectedBranch := gui.getSelectedRemoteBranch()
checkedOutBranch := gui.getCheckedOutBranch()
@@ -94,12 +93,12 @@ func (gui *Gui) handleSetBranchUpstream(g *gocui.Gui, v *gocui.View) error {
return err
}
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, REMOTES}})
},
})
}
func (gui *Gui) handleCreateResetToRemoteBranchMenu(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCreateResetToRemoteBranchMenu() error {
selectedBranch := gui.getSelectedRemoteBranch()
if selectedBranch == nil {
return nil

View File

@@ -5,7 +5,6 @@ import (
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -25,9 +24,9 @@ func (gui *Gui) handleRemoteSelect() error {
var task updateTask
remote := gui.getSelectedRemote()
if remote == nil {
task = gui.createRenderStringTask("No remotes")
task = NewRenderStringTask("No remotes")
} else {
task = gui.createRenderStringTask(fmt.Sprintf("%s\nUrls:\n%s", utils.ColoredString(remote.Name, color.FgGreen), strings.Join(remote.Urls, "\n")))
task = NewRenderStringTask(fmt.Sprintf("%s\nUrls:\n%s", utils.ColoredString(remote.Name, color.FgGreen), strings.Join(remote.Urls, "\n")))
}
return gui.refreshMainViews(refreshMainOpts{
@@ -58,12 +57,7 @@ func (gui *Gui) refreshRemotes() error {
}
}
branchesView := gui.getBranchesView()
if branchesView != nil {
return gui.postRefreshUpdate(gui.mustContextForContextKey(branchesView.Context))
}
return nil
return gui.postRefreshUpdate(gui.mustContextForContextKey(ContextKey(gui.Views.Branches.Context)))
}
func (gui *Gui) handleRemoteEnter() error {
@@ -81,10 +75,10 @@ func (gui *Gui) handleRemoteEnter() error {
}
gui.State.Panels.RemoteBranches.SelectedLineIdx = newSelectedLine
return gui.pushContext(gui.Contexts.Remotes.Branches.Context)
return gui.pushContext(gui.State.Contexts.RemoteBranches)
}
func (gui *Gui) handleAddRemote(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleAddRemote() error {
return gui.prompt(promptOpts{
title: gui.Tr.LcNewRemoteName,
handleConfirm: func(remoteName string) error {
@@ -94,7 +88,7 @@ func (gui *Gui) handleAddRemote(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.AddRemote(remoteName, remoteUrl); err != nil {
return err
}
return gui.refreshSidePanels(refreshOptions{scope: []int{REMOTES}})
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{REMOTES}})
},
})
},
@@ -102,7 +96,7 @@ func (gui *Gui) handleAddRemote(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleRemoveRemote(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleRemoveRemote() error {
remote := gui.getSelectedRemote()
if remote == nil {
return nil
@@ -116,12 +110,12 @@ func (gui *Gui) handleRemoveRemote(g *gocui.Gui, v *gocui.View) error {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, REMOTES}})
},
})
}
func (gui *Gui) handleEditRemote(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleEditRemote() error {
remote := gui.getSelectedRemote()
if remote == nil {
return nil
@@ -164,14 +158,14 @@ func (gui *Gui) handleEditRemote(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.UpdateRemoteUrl(updatedRemoteName, updatedRemoteUrl); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, REMOTES}})
},
})
},
})
}
func (gui *Gui) handleFetchRemote(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleFetchRemote() error {
remote := gui.getSelectedRemote()
if remote == nil {
return nil
@@ -184,6 +178,6 @@ func (gui *Gui) handleFetchRemote(g *gocui.Gui, v *gocui.View) error {
err := gui.GitCommand.FetchRemote(remote.Name, gui.promptUserForCredential)
gui.handleCredentialsPopup(err)
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, REMOTES}})
})
}

View File

@@ -17,11 +17,11 @@ func (gui *Gui) resetToRef(ref string, strength string, options oscommands.RunCo
// loading a heap of commits is slow so we limit them whenever doing a reset
gui.State.Panels.Commits.LimitCommits = true
if err := gui.pushContext(gui.Contexts.BranchCommits.Context); err != nil {
if err := gui.pushContext(gui.State.Contexts.BranchCommits); err != nil {
return err
}
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES, BRANCHES, REFLOG, COMMITS}}); err != nil {
if err := gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES, BRANCHES, REFLOG, COMMITS}}); err != nil {
return err
}

View File

@@ -3,26 +3,31 @@ package gui
import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) handleOpenSearch(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleOpenSearch(viewName string) error {
view, err := gui.g.View(viewName)
if err != nil {
return nil
}
gui.State.Searching.isSearching = true
gui.State.Searching.view = v
gui.State.Searching.view = view
gui.renderString("search", "")
gui.renderString(gui.Views.Search, "")
if err := gui.pushContext(gui.Contexts.Search.Context); err != nil {
if err := gui.pushContext(gui.State.Contexts.Search); err != nil {
return err
}
return nil
}
func (gui *Gui) handleSearch(g *gocui.Gui, v *gocui.View) error {
gui.State.Searching.searchString = gui.getSearchView().Buffer()
func (gui *Gui) handleSearch() error {
gui.State.Searching.searchString = gui.Views.Search.Buffer()
gui.Log.Warn(gui.State.Searching.searchString)
if err := gui.returnFromContext(); err != nil {
return err
}
@@ -45,7 +50,7 @@ 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(
"search",
gui.Views.Search,
fmt.Sprintf(
"no matches for '%s' %s",
gui.State.Searching.searchString,
@@ -58,7 +63,7 @@ func (gui *Gui) onSelectItemWrapper(innerFunc func(int) error) func(int, int, in
return nil
}
gui.renderString(
"search",
gui.Views.Search,
fmt.Sprintf(
"matches for '%s' (%d of %d) %s",
gui.State.Searching.searchString,
@@ -92,7 +97,7 @@ func (gui *Gui) onSearchEscape() error {
return nil
}
func (gui *Gui) handleSearchEscape(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleSearchEscape() error {
if err := gui.onSearchEscape(); err != nil {
return err
}

View File

@@ -1,7 +1,5 @@
package gui
import "github.com/jesseduffield/gocui"
func (gui *Gui) nextSideWindow() error {
windows := gui.getCyclableWindows()
currentWindow := gui.currentWindow()
@@ -19,7 +17,7 @@ func (gui *Gui) nextSideWindow() error {
}
}
}
if err := gui.resetOrigin(gui.getMainView()); err != nil {
if err := gui.resetOrigin(gui.Views.Main); err != nil {
return err
}
@@ -45,7 +43,7 @@ func (gui *Gui) previousSideWindow() error {
}
}
}
if err := gui.resetOrigin(gui.getMainView()); err != nil {
if err := gui.resetOrigin(gui.Views.Main); err != nil {
return err
}
@@ -54,8 +52,8 @@ func (gui *Gui) previousSideWindow() error {
return gui.pushContextWithView(viewName)
}
func (gui *Gui) goToSideWindow(sideViewName string) func(g *gocui.Gui, v *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) goToSideWindow(sideViewName string) func() error {
return func() error {
return gui.pushContextWithView(sideViewName)
}
}

View File

@@ -3,7 +3,6 @@ package gui
import (
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
)
@@ -27,11 +26,11 @@ func (gui *Gui) refreshStagingPanel(forceSecondaryFocused bool, selectedLineIdx
}
if secondaryFocused {
gui.getMainView().Title = gui.Tr.StagedChanges
gui.getSecondaryView().Title = gui.Tr.UnstagedChanges
gui.Views.Main.Title = gui.Tr.StagedChanges
gui.Views.Secondary.Title = gui.Tr.UnstagedChanges
} else {
gui.getMainView().Title = gui.Tr.UnstagedChanges
gui.getSecondaryView().Title = gui.Tr.StagedChanges
gui.Views.Main.Title = gui.Tr.UnstagedChanges
gui.Views.Secondary.Title = gui.Tr.StagedChanges
}
// note for custom diffs, we'll need to send a flag here saying not to use the custom diff
@@ -60,11 +59,11 @@ func (gui *Gui) refreshStagingPanel(forceSecondaryFocused bool, selectedLineIdx
return nil
}
func (gui *Gui) handleTogglePanelClick(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleTogglePanelClick() error {
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
state.SecondaryFocused = !state.SecondaryFocused
return gui.refreshStagingPanel(false, v.SelectedLineIdx(), state)
return gui.refreshStagingPanel(false, gui.Views.Secondary.SelectedLineIdx(), state)
})
}
@@ -85,7 +84,7 @@ func (gui *Gui) handleTogglePanel() error {
func (gui *Gui) handleStagingEscape() error {
gui.escapeLineByLinePanel()
return gui.pushContext(gui.Contexts.Files.Context)
return gui.pushContext(gui.State.Contexts.Files)
}
func (gui *Gui) handleToggleStagedSelection() error {
@@ -108,7 +107,7 @@ func (gui *Gui) handleResetSelection() error {
handlersManageFocus: true,
handleConfirm: func() error {
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
if err := gui.pushContext(gui.Contexts.Staging.Context); err != nil {
if err := gui.pushContext(gui.State.Contexts.Staging); err != nil {
return err
}
@@ -116,7 +115,7 @@ func (gui *Gui) handleResetSelection() error {
})
},
handleClose: func() error {
return gui.pushContext(gui.Contexts.Staging.Context)
return gui.pushContext(gui.State.Contexts.Staging)
},
})
} else {
@@ -152,7 +151,7 @@ func (gui *Gui) applySelection(reverse bool, state *lBlPanelState) error {
state.SelectMode = LINE
}
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
if err := gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}); err != nil {
return err
}
if err := gui.refreshStagingPanel(false, -1, state); err != nil {

View File

@@ -1,7 +1,6 @@
package gui
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -21,12 +20,12 @@ func (gui *Gui) handleStashEntrySelect() error {
var task updateTask
stashEntry := gui.getSelectedStashEntry()
if stashEntry == nil {
task = gui.createRenderStringTask(gui.Tr.NoStashEntries)
task = NewRenderStringTask(gui.Tr.NoStashEntries)
} else {
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowStashEntryCmdStr(stashEntry.Index),
)
task = gui.createRunPtyTask(cmd)
task = NewRunPtyTask(cmd)
}
return gui.refreshMainViews(refreshMainOpts{
@@ -38,14 +37,14 @@ func (gui *Gui) handleStashEntrySelect() error {
}
func (gui *Gui) refreshStashEntries() error {
gui.State.StashEntries = gui.GitCommand.GetStashEntries(gui.State.Modes.Filtering.Path)
gui.State.StashEntries = gui.GitCommand.GetStashEntries(gui.State.Modes.Filtering.GetPath())
return gui.Contexts.Stash.Context.HandleRender()
return gui.State.Contexts.Stash.HandleRender()
}
// specific functions
func (gui *Gui) handleStashApply(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleStashApply() error {
skipStashWarning := gui.Config.GetUserConfig().Gui.SkipStashWarning
apply := func() error {
@@ -65,7 +64,7 @@ func (gui *Gui) handleStashApply(g *gocui.Gui, v *gocui.View) error {
})
}
func (gui *Gui) handleStashPop(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleStashPop() error {
skipStashWarning := gui.Config.GetUserConfig().Gui.SkipStashWarning
pop := func() error {
@@ -85,7 +84,7 @@ func (gui *Gui) handleStashPop(g *gocui.Gui, v *gocui.View) error {
})
}
func (gui *Gui) handleStashDrop(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleStashDrop() error {
return gui.ask(askOpts{
title: gui.Tr.StashDrop,
prompt: gui.Tr.SureDropStashEntry,
@@ -110,7 +109,7 @@ func (gui *Gui) stashDo(method string) error {
if err := gui.GitCommand.StashDo(stashEntry.Index, method); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{scope: []int{STASH, FILES}})
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{STASH, FILES}})
}
func (gui *Gui) handleStashSave(stashFunc func(message string) error) error {
@@ -124,7 +123,7 @@ func (gui *Gui) handleStashSave(stashFunc func(message string) error) error {
if err := stashFunc(stashComment); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{scope: []int{STASH, FILES}})
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{STASH, FILES}})
},
})
}
@@ -135,5 +134,5 @@ func (gui *Gui) handleViewStashFiles() error {
return nil
}
return gui.switchToCommitFilesContext(stashEntry.RefName(), false, gui.Contexts.Stash.Context, "stash")
return gui.switchToCommitFilesContext(stashEntry.RefName(), false, gui.State.Contexts.Stash, "stash")
}

View File

@@ -43,7 +43,7 @@ func (gui *Gui) refreshStatus() {
status += fmt.Sprintf("%s → %s ", repoName, name)
gui.g.Update(func(*gocui.Gui) error {
gui.setViewContent(gui.getStatusView(), status)
gui.setViewContent(gui.Views.Status, status)
return nil
})
}
@@ -56,12 +56,12 @@ func cursorInSubstring(cx int, prefix string, substring string) bool {
return cx >= runeCount(prefix) && cx < runeCount(prefix+substring)
}
func (gui *Gui) handleCheckForUpdate(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCheckForUpdate() error {
gui.Updater.CheckForNewUpdate(gui.onUserUpdateCheckFinish, true)
return gui.createLoaderPanel(gui.Tr.CheckingForUpdates)
}
func (gui *Gui) handleStatusClick(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleStatusClick() error {
// TODO: move into some abstraction (status is currently not a listViewContext where a lot of this code lives)
if gui.popupPanelFocused() {
return nil
@@ -73,11 +73,11 @@ func (gui *Gui) handleStatusClick(g *gocui.Gui, v *gocui.View) error {
return nil
}
if err := gui.pushContext(gui.Contexts.Status.Context); err != nil {
if err := gui.pushContext(gui.State.Contexts.Status); err != nil {
return err
}
cx, _ := v.Cursor()
cx, _ := gui.Views.Status.Cursor()
upstreamStatus := fmt.Sprintf("↑%s↓%s", currentBranch.Pushables, currentBranch.Pullables)
repoName := utils.GetCurrentRepoName()
switch gui.GitCommand.WorkingTreeState() {
@@ -121,16 +121,16 @@ func (gui *Gui) handleStatusSelect() error {
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: "",
task: gui.createRenderStringTask(dashboardString),
task: NewRenderStringTask(dashboardString),
},
})
}
func (gui *Gui) handleOpenConfig(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleOpenConfig() error {
return gui.openFile(gui.Config.GetUserConfigPath())
}
func (gui *Gui) handleEditConfig(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleEditConfig() error {
filename := gui.Config.GetUserConfigPath()
return gui.editFile(filename)
}

View File

@@ -1,7 +1,6 @@
package gui
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
)
@@ -22,13 +21,13 @@ func (gui *Gui) handleSubCommitSelect() error {
commit := gui.getSelectedSubCommit()
var task updateTask
if commit == nil {
task = gui.createRenderStringTask("No commits")
task = NewRenderStringTask("No commits")
} else {
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.Modes.Filtering.Path),
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.Modes.Filtering.GetPath()),
)
task = gui.createRunPtyTask(cmd)
task = NewRunPtyTask(cmd)
}
return gui.refreshMainViews(refreshMainOpts{
@@ -39,7 +38,7 @@ func (gui *Gui) handleSubCommitSelect() error {
})
}
func (gui *Gui) handleCheckoutSubCommit(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCheckoutSubCommit() error {
commit := gui.getSelectedSubCommit()
if commit == nil {
return nil
@@ -73,7 +72,7 @@ func (gui *Gui) handleViewSubCommitFiles() error {
return nil
}
return gui.switchToCommitFilesContext(commit.Sha, false, gui.Contexts.SubCommits.Context, "branches")
return gui.switchToCommitFilesContext(commit.Sha, false, gui.State.Contexts.SubCommits, "branches")
}
func (gui *Gui) switchToSubCommitsContext(refName string) error {
@@ -83,7 +82,7 @@ func (gui *Gui) switchToSubCommitsContext(refName string) error {
commits, err := builder.GetCommits(
commands.GetCommitsOptions{
Limit: gui.State.Panels.Commits.LimitCommits,
FilterPath: gui.State.Modes.Filtering.Path,
FilterPath: gui.State.Modes.Filtering.GetPath(),
IncludeRebaseCommits: false,
RefName: refName,
},
@@ -95,13 +94,13 @@ func (gui *Gui) switchToSubCommitsContext(refName string) error {
gui.State.SubCommits = commits
gui.State.Panels.SubCommits.refName = refName
gui.State.Panels.SubCommits.SelectedLineIdx = 0
gui.Contexts.SubCommits.Context.SetParentContext(gui.currentSideContext())
gui.State.Contexts.SubCommits.SetParentContext(gui.currentSideListContext())
return gui.pushContext(gui.Contexts.SubCommits.Context)
return gui.pushContext(gui.State.Contexts.SubCommits)
}
func (gui *Gui) handleSwitchToSubCommits() error {
currentContext := gui.currentSideContext()
currentContext := gui.currentSideListContext()
if currentContext == nil {
return nil
}

View File

@@ -7,7 +7,6 @@ import (
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -25,7 +24,7 @@ func (gui *Gui) handleSubmoduleSelect() error {
var task updateTask
submodule := gui.getSelectedSubmodule()
if submodule == nil {
task = gui.createRenderStringTask("No submodules")
task = NewRenderStringTask("No submodules")
} else {
prefix := fmt.Sprintf(
"Name: %s\nPath: %s\nUrl: %s\n\n",
@@ -36,11 +35,11 @@ func (gui *Gui) handleSubmoduleSelect() error {
file := gui.fileForSubmodule(submodule)
if file == nil {
task = gui.createRenderStringTask(prefix)
task = NewRenderStringTask(prefix)
} else {
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(file, false, !file.HasUnstagedChanges && file.HasStagedChanges)
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
task = gui.createRunCommandTaskWithPrefix(cmd, prefix)
task = NewRunCommandTaskWithPrefix(cmd, prefix)
}
}
@@ -72,9 +71,9 @@ func (gui *Gui) enterSubmodule(submodule *models.SubmoduleConfig) error {
if err != nil {
return err
}
gui.State.RepoPathStack = append(gui.State.RepoPathStack, wd)
gui.RepoPathStack = append(gui.RepoPathStack, wd)
return gui.dispatchSwitchToRepo(submodule.Path)
return gui.dispatchSwitchToRepo(submodule.Path, true)
}
func (gui *Gui) removeSubmodule(submodule *models.SubmoduleConfig) error {
@@ -86,7 +85,7 @@ func (gui *Gui) removeSubmodule(submodule *models.SubmoduleConfig) error {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{scope: []int{SUBMODULES, FILES}})
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES, FILES}})
},
})
}
@@ -98,7 +97,7 @@ func (gui *Gui) handleResetSubmodule(submodule *models.SubmoduleConfig) error {
}
func (gui *Gui) fileForSubmodule(submodule *models.SubmoduleConfig) *models.File {
for _, file := range gui.State.Files {
for _, file := range gui.State.FileManager.GetAllFiles() {
if file.IsSubmodule([]*models.SubmoduleConfig{submodule}) {
return file
}
@@ -110,7 +109,7 @@ func (gui *Gui) fileForSubmodule(submodule *models.SubmoduleConfig) *models.File
func (gui *Gui) resetSubmodule(submodule *models.SubmoduleConfig) error {
file := gui.fileForSubmodule(submodule)
if file != nil {
if err := gui.GitCommand.UnStageFile(file.Name, file.Tracked); err != nil {
if err := gui.GitCommand.UnStageFile(file.Names(), file.Tracked); err != nil {
return gui.surfaceError(err)
}
}
@@ -122,7 +121,7 @@ func (gui *Gui) resetSubmodule(submodule *models.SubmoduleConfig) error {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES, SUBMODULES}})
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES, SUBMODULES}})
}
func (gui *Gui) handleAddSubmodule() error {
@@ -144,7 +143,7 @@ func (gui *Gui) handleAddSubmodule() error {
err := gui.GitCommand.SubmoduleAdd(submoduleName, submodulePath, submoduleUrl)
gui.handleCredentialsPopup(err)
return gui.refreshSidePanels(refreshOptions{scope: []int{SUBMODULES}})
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}})
})
},
})
@@ -164,7 +163,7 @@ func (gui *Gui) handleEditSubmoduleUrl(submodule *models.SubmoduleConfig) error
err := gui.GitCommand.SubmoduleUpdateUrl(submodule.Name, submodule.Path, newUrl)
gui.handleCredentialsPopup(err)
return gui.refreshSidePanels(refreshOptions{scope: []int{SUBMODULES}})
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}})
})
},
})
@@ -175,21 +174,19 @@ func (gui *Gui) handleSubmoduleInit(submodule *models.SubmoduleConfig) error {
err := gui.GitCommand.SubmoduleInit(submodule.Path)
gui.handleCredentialsPopup(err)
return gui.refreshSidePanels(refreshOptions{scope: []int{SUBMODULES}})
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}})
})
}
func (gui *Gui) forSubmodule(callback func(*models.SubmoduleConfig) error) func(g *gocui.Gui, v *gocui.View) error {
return gui.wrappedHandler(
func() error {
submodule := gui.getSelectedSubmodule()
if submodule == nil {
return nil
}
func (gui *Gui) forSubmodule(callback func(*models.SubmoduleConfig) error) func() error {
return func() error {
submodule := gui.getSelectedSubmodule()
if submodule == nil {
return nil
}
return callback(submodule)
},
)
return callback(submodule)
}
}
func (gui *Gui) handleResetRemoveSubmodule(submodule *models.SubmoduleConfig) error {
@@ -221,7 +218,7 @@ func (gui *Gui) handleBulkSubmoduleActionsMenu() error {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{scope: []int{SUBMODULES}})
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}})
})
},
},
@@ -233,7 +230,7 @@ func (gui *Gui) handleBulkSubmoduleActionsMenu() error {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{scope: []int{SUBMODULES}})
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}})
})
},
},
@@ -245,7 +242,7 @@ func (gui *Gui) handleBulkSubmoduleActionsMenu() error {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{scope: []int{SUBMODULES}})
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}})
})
},
},
@@ -257,7 +254,7 @@ func (gui *Gui) handleBulkSubmoduleActionsMenu() error {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{scope: []int{SUBMODULES}})
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}})
})
},
},
@@ -271,6 +268,6 @@ func (gui *Gui) handleUpdateSubmodule(submodule *models.SubmoduleConfig) error {
err := gui.GitCommand.SubmoduleUpdate(submodule.Path)
gui.handleCredentialsPopup(err)
return gui.refreshSidePanels(refreshOptions{scope: []int{SUBMODULES}})
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}})
})
}

View File

@@ -24,13 +24,8 @@ func (gui *Gui) getSelectedSuggestion() *types.Suggestion {
}
func (gui *Gui) setSuggestions(suggestions []*types.Suggestion) {
view := gui.getSuggestionsView()
if view == nil {
return
}
gui.State.Suggestions = suggestions
gui.State.Panels.Suggestions.SelectedLineIdx = 0
_ = gui.resetOrigin(view)
_ = gui.Contexts.Suggestions.Context.HandleRender()
_ = gui.resetOrigin(gui.Views.Suggestions)
_ = gui.State.Contexts.Suggestions.HandleRender()
}

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