Compare commits

...

221 Commits
v0.3.0 ... v0.6

Author SHA1 Message Date
Jesse Duffield
3a607061a2 Only reset origin in main view when handling the selection of a new file 2019-01-18 09:32:15 +11:00
Jesse Duffield
695b092c41 Directly send wrap argument rather than the view 2019-01-17 10:29:52 +11:00
Jesse Duffield
a38d1a3b68 Explicitly refer confirmation panel view
as @jesseduffield pointed in #358, need to refer
confirmation panel view explicitly in case something
else has focus

Co-Authored-By: KOREAN139 <korean139@gmail.com>
2019-01-17 10:29:52 +11:00
KOREAN139
2dc5e6d503 Fix recent repo view size issue
getMessageHeight() calculates height under assumption that given view's
wrap option (view.Wrap) is true, and createMenu() does not set wrap
option as true. this causes gocui set improper view's height when lines
in view needs to be wrapped.
add *gocui.View as parameter in getMessageHeight(), and calculates
view's height depend on its wrap option.

resolve issue #354
2019-01-17 10:29:52 +11:00
Dawid Dziurla
0dcfa09ff2 run go fmt against generator 2019-01-17 10:11:17 +11:00
Dawid Dziurla
d5401ab200 add script generating keybindings cheatsheet 2019-01-17 10:11:17 +11:00
Dawid Dziurla
b6f8ebc0ca delete KeyReadable field from Binding struct
also rewrite GetKey function
2019-01-17 10:11:17 +11:00
KimMachineGune
3e24069722 pkg: Fix typo 2019-01-16 18:06:11 +11:00
Jesse Duffield
c722ea5afc log in config directory rather than wherever you opened lazygit 2019-01-16 08:54:12 +11:00
Jesse Duffield
c759c7ac65 bump gocui to prevent truncating the contents of wrapped views 2019-01-15 19:56:32 +11:00
Jesse Duffield
e50bd812fc fix checksum mismatches 2019-01-14 22:12:43 +11:00
KimMachineGune
7ff022f1e7 pkg/updates: Refactoring 2019-01-14 21:52:03 +11:00
Mark Kopenga
1db8801771 Update dutch.go 2018-12-20 09:17:10 +11:00
Mark Kopenga
666ea3a4a0 Fixed suggestion from glvr182 2018-12-20 09:17:10 +11:00
mjarkk
47d50989c4 Added dutch translations 2018-12-20 09:17:10 +11:00
Dawid Dziurla
e4f70278dd i18n: pl translation 2018-12-20 09:13:41 +11:00
Jesse Duffield
0afffd03ca remove comment 2018-12-19 21:46:48 +11:00
Jesse Duffield
6c5e409ffa send direct error message 2018-12-19 20:12:35 +11:00
mjarkk
800b40ecc4 Translated credentials error with git push/pull/fetch 2018-12-19 10:06:58 +01:00
mjarkk
097f687efe hopefully fixed circleci 2018-12-19 08:56:59 +01:00
Mark Kopenga
aa30e00643 Merge branch 'master' into https-ask-for-username-password 2018-12-19 08:44:14 +01:00
Jesse Duffield
cf56dcf9ff refresh files every 10 seconds rather than 2 so that we're not getting a heap of index lock errors 2018-12-18 23:04:32 +11:00
Jesse Duffield
c14a4eed0e bump modules and add bump_modules script 2018-12-18 23:03:26 +11:00
Jesse Duffield
a1b688f070 bump gocui fork to convert tabs to spaces 2018-12-18 23:03:26 +11:00
Cameron Nemo
4793232a35 Update gomodule checksums for go 1.11.4 2018-12-18 22:49:12 +11:00
Jesse Duffield
7835fce708 fix tests 2018-12-18 22:40:36 +11:00
Jesse Duffield
535152e15e bump go.mod 2018-12-18 22:37:10 +11:00
Jesse Duffield
160af3bb99 fix typo 2018-12-18 22:29:07 +11:00
Jesse Duffield
328b57e2cf no longer checking for 'exit status 128' because we're directly returning stderr 2018-12-18 22:27:50 +11:00
Jesse Duffield
20a94447d7 explicitly return newlines to our live command stdin 2018-12-18 22:23:17 +11:00
Jesse Duffield
865c7c2332 minor refactor of credentials panel into its own file 2018-12-18 22:19:32 +11:00
Jesse Duffield
11c7cbe3ac bump pty fork 2018-12-18 22:18:48 +11:00
Jesse Duffield
276ac3a92e decrease frequency of refreshing files because it's causing index lock errors 2018-12-18 21:28:09 +11:00
Jesse Duffield
a4beabf4b9 improved pre-push test script 2018-12-18 21:27:39 +11:00
Jesse Duffield
c35255b7a9 set stderr ourselves so that we only read the error output if there is any 2018-12-18 21:25:49 +11:00
Jesse Duffield
319064f040 switch to our own fork of pty which lets us set our own stdout and stderr 2018-12-18 21:25:28 +11:00
mjarkk
f5f726e9c4 A try to hide the password from the error output 2018-12-17 08:58:09 +01:00
Jesse Duffield
c56b303b29 add pre-hook to test credentials panel 2018-12-16 17:55:37 +11:00
Jesse Duffield
4886b8350e always hide rather than delete the credentials view, and don't log on error in case there is a user password in the error 2018-12-16 17:28:04 +11:00
mjarkk
af26b5f3e0 Tried to fix circleci 2018-12-14 13:45:43 +01:00
mjarkk
70cd6700e7 Tried to fix circleci 2018-12-14 13:43:13 +01:00
mjarkk
d11f8989d9 Merge branch 'https-ask-for-username-password' of https://github.com/mjarkk/lazygit into https-ask-for-username-password 2018-12-14 13:40:40 +01:00
mjarkk
0fca27d022 Tried to fix circleci 2018-12-14 13:40:29 +01:00
Mark Kopenga
255319e597 Merge branch 'master' into https-ask-for-username-password 2018-12-14 11:13:29 +01:00
mjarkk
5d038dfd33 Removed the wired error handling 2018-12-12 22:11:31 +01:00
mjarkk
0577d3b97f Removed the username / password savety check
This check is not realy needed because the change that it will show up a second time is so low that this is more work to check than the change it actualy might happen
2018-12-12 21:08:53 +01:00
Jesse Duffield
a26c15dafa some fixes for issues around the credentials panel 2018-12-12 22:34:20 +11:00
Jesse Duffield
c71bcc64ed move fetch keybinding to files view to make way for branch-specific fetched 2018-12-12 22:33:42 +11:00
Glenn Vriesman
822dc5dada Moved push-pullables status to the end 2018-12-11 09:38:33 +11:00
mjarkk
e20d8366e1 Made gobot happy 2018-12-10 14:21:00 +01:00
mjarkk
76e9582739 Not always git fetch 2018-12-10 13:45:03 +01:00
mjarkk
50f20de8f3 Removed a lot of duplicated code 2018-12-10 08:22:52 +01:00
mjarkk
8e3f5e19e0 Changed some other names 2018-12-10 08:04:22 +01:00
mjarkk
61c2778de1 Changed pushPassUname name to credentials 2018-12-10 07:51:06 +01:00
mjarkk
3c17bf761a Better name for this function 2018-12-10 07:46:26 +01:00
mjarkk
696d6dc20c Fixed loop before error check 2018-12-10 07:43:32 +01:00
mjarkk
f14effe5f5 Worked and fixed a view comments 2018-12-09 13:04:19 +01:00
Tommy Nguyen
b95abd95ef Dockerfile: add alias 2018-12-09 16:36:12 +11:00
Tommy Nguyen
ea6712dec8 Remove -a -installsuffix; copy to /bin/ 2018-12-09 16:36:12 +11:00
Tommy Nguyen
de37a66ef3 Dockerfile improvements 2018-12-09 16:36:12 +11:00
mjarkk
efb82a58ae Tried to fix circleci error 2018-12-08 16:54:00 +01:00
Mark Kopenga
19a6a32625 Merge branch 'master' into https-ask-for-username-password 2018-12-08 16:41:39 +01:00
mjarkk
270658fc00 Made code ready to merge to master's latest commit 2018-12-08 16:40:22 +01:00
Jesse Duffield
ff856b7630 fetching branches without checking out 2018-12-08 11:51:47 +11:00
Jesse Duffield
ca3afa2a39 standardising how list panels deal with cursor movement 2018-12-08 11:51:47 +11:00
Jesse Duffield
99a8b1ae8b making a start on unidirectional data binding to fix these UI bugs 2018-12-08 11:51:47 +11:00
Jesse Duffield
ccc771d8b1 Merge pull request #342 from jesseduffield/hotfix/faster-reset-hard-head
Faster reset --hard head and clean as well
2018-12-08 10:04:27 +11:00
mjarkk
cf5a85b80f Intro message stays on screen now 2018-12-07 19:22:22 +01:00
mjarkk
2f7bd2896c Fixed error when there is no state.yml 2018-12-07 15:46:49 +01:00
mjarkk
8f904ffd72 Working popup 2018-12-07 14:56:29 +01:00
mjarkk
ced81e11f0 Only show private repo popup when opening repo for first time 2018-12-06 22:05:16 +01:00
mjarkk
6d0fa8bc29 Made some small inprovements 2018-12-06 09:05:51 +01:00
mjarkk
21a808a52b Renamed branch to branches 2018-12-06 08:39:49 +01:00
mjarkk
89c272eed5 Removed the tabs for spaces 2018-12-06 08:33:04 +01:00
Mark Kopenga
1b6d34e76a Merge branch 'master' into https-ask-for-username-password 2018-12-06 08:31:12 +01:00
mjarkk
6711543634 Made the gobot happy again 2018-12-06 08:28:56 +01:00
mjarkk
f6e83cdbdf Started working on the popup 2018-12-06 08:26:05 +01:00
Jesse Duffield
3b51d7cd00 clean as well as reset (I'm hoping this is a good design decision) 2018-12-05 20:06:47 +11:00
Jesse Duffield
66512ca253 use porcelain git rather than go-git for reset --hard HEAD because go-git takes over 5 seconds 2018-12-05 19:49:07 +11:00
Jesse Duffield
1a6a69a8f1 You can now stage changes one line at a time
Staging Lines
2018-12-05 19:40:22 +11:00
Jesse Duffield
933874fb25 dutch and polish translations to be updated 2018-12-05 19:33:54 +11:00
Jesse Duffield
c0f9795910 staging lines and hunks 2018-12-05 19:33:46 +11:00
Jesse Duffield
658e5a9faf initial support for staging individual lines 2018-12-04 22:11:48 +11:00
Jesse Duffield
99824c8a7b add patch modifier struct 2018-12-04 22:11:48 +11:00
Jesse Duffield
60060551bf Merge pull request #336 from BlakeMScurr/auto-fix-function-comments
Fix function comments with CodeLingo.
2018-12-03 09:33:42 +11:00
mjarkk
c269ad1370 Made the bot happy 2018-12-02 15:06:51 +01:00
mjarkk
2edd2b74ff Removed a lot of useless code 2018-12-02 14:58:18 +01:00
BlakeMScurr
181f91d2ef Add full stops to new comments. 2018-11-30 13:47:14 +13:00
BlakeMScurr
643cdd3461 Add simple comments to uncommented functions. 2018-11-30 11:04:08 +13:00
BlakeMScurr
5c70d2724b Fix function comments with CodeLingo. 2018-11-28 15:31:22 +13:00
Jesse Duffield
55712f509c Merge pull request #333 from jirfag/master
fix 'main' redefinition in scripts/ dir
2018-11-26 19:54:08 +11:00
mjarkk
d91493b587 Forgot to set the git fetch timeout back to 60s 2018-11-25 13:23:32 +01:00
mjarkk
9da1382e09 Added credentials popup 2018-11-25 13:15:36 +01:00
Denis Isaev
4e8e4612bd fix 'main' redefinition in scripts/ dir 2018-11-24 12:23:46 +03:00
mjarkk
adfc00bcdc Changed the waitForGroup to a channel 2018-11-23 13:58:30 +01:00
Mark Kopenga
b0eaf507a5 Merge branch 'master' into https-ask-for-username-password 2018-11-14 13:40:17 +01:00
Jesse Duffield
b9ecb82cb7 Merge pull request #328 from jesseduffield/feature/detached-heads
support Detached heads
2018-11-14 22:07:33 +11:00
mjarkk
448d9caf1b Fixed typo 2018-11-14 11:40:32 +01:00
Mark Kopenga
6d2bf0b0b5 Merge branch 'master' into https-ask-for-username-password 2018-11-14 11:34:39 +01:00
Jesse Duffield
5160668efd Merge branch 'master' into feature/detached-heads 2018-11-14 21:23:44 +11:00
Jesse Duffield
0eb1e4a86b change how we build our list of branches to support detached heads 2018-11-14 21:19:12 +11:00
mjarkk
0c4c00c1bf Removed useless channel read 2018-11-14 11:14:31 +01:00
Jesse Duffield
cc7d78f1ee Merge pull request #315 from glvr182/feature/translations
Fixed and added some dutch translations
2018-11-14 20:50:08 +11:00
Jesse Duffield
b8d5adcb84 Merge branch 'master' into feature/translations 2018-11-14 19:35:51 +11:00
Jesse Duffield
a5f483fae9 refactor obtaining current branch name 2018-11-14 19:08:42 +11:00
Jesse Duffield
775d910bdc Merge pull request #326 from KOREAN139/master
add scroll-past-bottom configuration option
2018-11-14 18:49:21 +11:00
mjarkk
18a1070c2c Trying to fix circleci 2018-11-10 18:24:37 +01:00
mjarkk
9fafd7ebc1 Fixed case that a commit message will break git push 2018-11-10 18:10:53 +01:00
mjarkk
bc14b01d03 just a test :) 2018-11-10 17:25:35 +01:00
mjarkk
80c6e0a8c4 Fixed pushing forever 2018-11-10 17:02:39 +01:00
mjarkk
8742c4c110 Removed some variables and placed them inside the gui struct 2018-11-10 09:27:03 +01:00
mjarkk
32ecc6d745 Removed getPushPassUnameView function 2018-11-10 09:09:18 +01:00
mjarkk
834e42897d Switched back to github.com/mgutz/str instaid of a copy of ToArgv 2018-11-10 08:57:02 +01:00
mjarkk
500267417b Removed some duplicated code 2018-11-10 08:46:42 +01:00
mjarkk
18bcc0df4d Fixed no error text on windows
when executing a command live on windows it detects errors but did just show a empty string
2018-11-10 08:43:02 +01:00
mjarkk
5ae0e75e5e Switched to channels instaid of a mutex 2018-11-10 08:39:09 +01:00
mjarkk
1fd8cadd9e Replaced regex with trim 2018-11-10 08:14:35 +01:00
KOREAN139
9d79d32c94 add scroll-past-bottom configuration option
with gui.scrollPastBottom option true, lazygit let user scroll past the
bottom - which is default
if option is false, user cannot scroll further when bottom of file has
appeared in mainView
2018-11-08 19:35:05 +09:00
Mark Kopenga
17b4b4cb33 Merge branch 'master' into https-ask-for-username-password 2018-11-08 10:31:10 +01:00
Jesse Duffield
79ef98739d Merge branch 'master' into feature/translations 2018-11-08 17:57:01 +11:00
Jesse Duffield
c2eaeab1f0 Merge pull request #314 from glvr182/feature/fix-keybind-crash
Fix bug related to limit view
2018-11-08 17:55:54 +11:00
Jesse Duffield
32d1289af7 Merge branch 'master' into feature/fix-keybind-crash 2018-11-08 17:49:21 +11:00
Jesse Duffield
ea55643cb2 Merge branch 'master' into feature/translations 2018-11-08 17:47:00 +11:00
Jesse Duffield
dcb6216713 Merge pull request #319 from mjarkk/fix-yt-link
Change the youtube video tutorial link to a short youtu.be link
2018-11-08 17:46:15 +11:00
mjarkk
9c8b241292 Removed some useless comments 2018-11-06 20:37:59 +01:00
mjarkk
7c4d360645 Better error for code 128 2018-11-06 20:25:11 +01:00
mjarkk
ad77ac639e Working new lines in live pty output 2018-11-06 20:24:10 +01:00
mjarkk
cf1e9f79b1 hopefully fixed the test now 2018-11-03 09:36:38 +01:00
mjarkk
8f0741a458 Changed the youtube link to a youtu.be short link 2018-11-03 09:19:47 +01:00
Mark Kopenga
6f2b62f729 Merge pull request #15 from jesseduffield/master
Update to latest master
2018-11-03 09:17:43 +01:00
mjarkk
8469239d84 Fixed test 2018-11-03 09:12:45 +01:00
mjarkk
af54d7f015 Fixed view not defined error with git push and pull 2018-11-02 15:07:10 +01:00
mjarkk
cb9ad5bc73 Fixed golangcibot surgestion 2018-11-02 10:06:10 +01:00
mjarkk
5470bb4121 Added username password detect to git pull 2018-11-02 09:54:54 +01:00
mjarkk
0e53a26d6f Maybe fixed the test this time 2018-11-01 07:06:34 +01:00
mjarkk
3938138ebc Hopefully fixed circleci 2018-10-31 19:25:52 +01:00
mjarkk
05f0e5120a Fixed one text 2018-10-31 17:55:02 +01:00
mjarkk
5532289086 Fixed some tests 2018-10-31 17:36:20 +01:00
mjarkk
78b2bc4f60 Made a better way of test pushing 2018-10-31 16:23:58 +01:00
Glenn Vriesman
d33f89fd60 Merge branch 'master' into feature/fix-keybind-crash 2018-10-31 15:04:34 +01:00
Glenn Vriesman
c0da212f54 Merge branch 'master' into feature/translations 2018-10-31 15:04:17 +01:00
mjarkk
9585f49490 Made error handling better 2018-10-29 08:23:56 +01:00
Mark Kopenga
1e0310a86d Merge pull request #14 from jesseduffield/master
Merged master into https-ask-for-username-password
2018-10-29 07:50:44 +01:00
Jesse Duffield
bf45e5b0e3 Merge pull request #313 from dbast/master
Add conda to readme as possibility to install on different platforms
2018-10-29 16:11:27 +11:00
Glenn Vriesman
9a0f094f58 Changed some things after marks feedback 2018-10-28 20:33:42 +01:00
Glenn Vriesman
ee89ad6ae7 Fixed test 2018-10-28 20:13:55 +01:00
Glenn Vriesman
22e5aafd59 Fixed and added some dutch translations 2018-10-28 19:58:20 +01:00
Glenn Vriesman
abd0803ef4 Set current view to limit view 2018-10-28 19:15:34 +01:00
mjarkk
372b333662 Added copy note at top of string-to-args.go 2018-10-27 17:08:25 +02:00
mjarkk
18f09a14e6 Removed package github.com/mgutz/str for better code coverage 2018-10-27 17:06:33 +02:00
mjarkk
ed564adb4a Removed github.com/ionrock/procs for better code coverage 2018-10-27 16:57:34 +02:00
mjarkk
9a99748d3b Fixed bug where username input didn't go away 2018-10-27 16:29:16 +02:00
mjarkk
9163110640 Removed error check for regex
Regexp only returns an error when regex string is in-corret
2018-10-27 15:52:12 +02:00
mjarkk
6c1c110ce0 Made tests pass
Git constandly exits with error code 1 for some reason it might be because of the wrong username and password but i don't think error 1 is for wrong credentials
2018-10-27 15:32:12 +02:00
Mark Kopenga
45c249acca Merge branch 'master' into https-ask-for-username-password 2018-10-27 15:06:51 +02:00
mjarkk
1df1053947 Fixed test 2018-10-27 15:01:16 +02:00
mjarkk
14cff0bd07 Fixed circleci build 2018-10-27 14:57:44 +02:00
mjarkk
87d1b9a547 Fixed circleci build 2018-10-27 14:56:15 +02:00
mjarkk
959d6fa2ca Made it possible to build for windows again 2018-10-27 14:37:31 +02:00
mjarkk
e47c597b3a Made it possible to build again for windows 2018-10-27 14:35:07 +02:00
Daniel Bast
bfcb348923 Add conda to readme as possibility to install on different platforms 2018-10-25 09:54:01 +02:00
Jesse Duffield
c6da4c8a47 Merge pull request #301 from rozuur/master
Initial version of delete named branch added
2018-10-24 09:24:16 +11:00
Mark Kopenga
1fedda6a75 Merge branch 'master' into https-ask-for-username-password 2018-10-23 13:52:50 +02:00
Jesse Duffield
467775fc1c Merge branch 'master' into master 2018-10-23 09:43:25 +11:00
Jesse Duffield
3b6b68a2a1 Merge pull request #305 from kristijanhusak/feature/create-pull-request
Add option to create pull request from branches panel.
2018-10-22 11:51:16 +11:00
Naveen Vardhi
3a23cb87b7 Remove force delete keybinding
And force delete messages gives feedback about merge status
2018-10-21 21:13:24 +05:30
mjarkk
ac03665df3 Handled some errors 2018-10-20 19:44:56 +02:00
mjarkk
1be44eae84 Fixed surgestion from golangcibot 2018-10-20 19:03:51 +02:00
mjarkk
b72841ca0c Fixed golangcibot surgestions 2018-10-20 18:58:37 +02:00
mjarkk
12425f0aa7 First good success 2018-10-20 17:37:55 +02:00
mjarkk
727ba9f42e test 2018-10-20 17:21:17 +02:00
mjarkk
73a0a65ee1 test 2018-10-20 17:20:52 +02:00
mjarkk
ac5696574c added some translations 2018-10-20 17:18:42 +02:00
mjarkk
1a43d64de3 Added extra translations 2018-10-20 16:25:48 +02:00
Kristijan Husak
990dc8c4ea Add separate open command for links and check if branch exists on remote before opening pull request link. 2018-10-20 11:58:08 +02:00
Jesse Duffield
59cdd7d46e Merge branch 'master' into master 2018-10-20 10:53:02 +11:00
mjarkk
4451cbc50b handled golangcibot 2018-10-17 21:14:24 +02:00
mjarkk
01fa106de3 Added files to commit pannelsidjfjlisdjfilisldjfiljisldjilfjisdjilfjlsidMoved some code around 2018-10-17 21:12:33 +02:00
mjarkk
9fc4262887 small code addition 2018-10-17 20:41:22 +02:00
mjarkk
cecd5733a8 Basic file for getting the fix working 2018-10-17 20:38:13 +02:00
Mark Kopenga
1d733f3adc Merge pull request #13 from jesseduffield/master
Updated to latest master
2018-10-17 20:22:41 +02:00
Kristijan Husak
c69fce2e9d Remove unnecessary nil error in NewPullRequest. 2018-10-15 11:00:19 +02:00
Kristijan Husak
df0e3e52fe Add option to create pull request form branches panel. 2018-10-13 22:54:51 +02:00
Jesse Duffield
d5f64602a8 Merge pull request #283 from kristijanhusak/feature/commit-amend
Add action for amending a commit
2018-10-10 17:50:55 +11:00
Kristijan Husak
4287f8ae90 Fix tests and add test scenarios for amend. 2018-10-08 22:19:42 +02:00
Kristijan Husak
190309e5c1 Check if there is any commit to amend and use 'A' instead of 'M' as shortcut. 2018-10-08 21:19:45 +02:00
Jesse Duffield
ac65586bd5 Merge branch 'master' into feature/commit-amend 2018-10-07 21:08:46 +11:00
Naveen Vardhi
af8d362caa Initial version of delete named branch added 2018-10-06 17:04:33 +05:30
Kristijan Husak
5f7ac97a39 Refresh side panels and use uppercase HEAD in all git commands that requires it. 2018-10-06 09:53:54 +02:00
Jesse Duffield
b8b59baa27 Merge pull request #290 from jesseduffield/feature/informative-commit-colors
Color merged and unmerged commits differently
2018-10-05 09:37:55 +10:00
Jesse Duffield
2be613679e more test coverage 2018-10-05 09:11:19 +10:00
Kristijan Husak
28fe3d6cf9 Use confirmation popup for amending last commit. 2018-09-25 22:11:51 +02:00
Kristijan Husak
b6b21bc98e Merge branch 'master' of git://github.com/jesseduffield/lazygit into feature/commit-amend 2018-09-25 21:17:53 +02:00
Jesse Duffield
eb69d98f99 add test for CurrentBranchName 2018-09-25 20:31:19 +10:00
Jesse Duffield
fb9596a3ff add test for getMergeBase 2018-09-25 20:25:04 +10:00
Jesse Duffield
0d33a746ba Merge branch 'feature/informative-commit-colors' of https://github.com/jesseduffield/lazygit into feature/informative-commit-colors 2018-09-25 20:11:36 +10:00
Jesse Duffield
f3fc98a3d0 support git flow when colouring commits 2018-09-25 20:11:33 +10:00
Jesse Duffield
17d7bcdeaf Merge branch 'master' into feature/informative-commit-colors 2018-09-25 20:10:12 +10:00
Jesse Duffield
d0a3f1eecf Merge pull request #291 from antham/add-tests-part-7
Add tests to pkg/commands/git - Part 7
2018-09-25 19:13:35 +10:00
Jesse Duffield
7164f37266 Merge branch 'master' into feature/commit-amend 2018-09-25 19:05:24 +10:00
Jesse Duffield
e9245cd53b Merge branch 'master' into add-tests-part-7 2018-09-25 19:03:29 +10:00
Jesse Duffield
80d6bbef86 Merge branch 'master' of https://github.com/jesseduffield/lazygit 2018-09-23 14:13:14 +10:00
Jesse Duffield
3d751c03fe add donation link to status panel 2018-09-23 14:13:10 +10:00
Jesse Duffield
4e0d0f7d75 Update README.md 2018-09-23 13:09:54 +10:00
Jesse Duffield
b8b3eee961 Merge pull request #300 from jesseduffield/hotfix/273-cursor-scrolling
Fix slow scrolling
2018-09-22 13:59:49 +10:00
Jesse Duffield
7947668e18 Merge branch 'hotfix/273-cursor-scrolling' of https://github.com/jesseduffield/lazygit into hotfix/273-cursor-scrolling 2018-09-22 13:50:01 +10:00
Jesse Duffield
619c28ce56 use lineheight rather than buffer length 2018-09-22 13:49:58 +10:00
Jesse Duffield
53aef7846a Merge branch 'master' into hotfix/273-cursor-scrolling 2018-09-22 13:47:38 +10:00
Jesse Duffield
227067fdd1 use lineheight rather than buffer length 2018-09-22 13:44:48 +10:00
Jesse Duffield
3df4a9484f Merge pull request #295 from jesseduffield/hotfix/commit-message-panel-focus
Fix issues with commit message panel losing focus
2018-09-22 13:42:27 +10:00
Jesse Duffield
2229a6e133 Merge branch 'master' into hotfix/commit-message-panel-focus 2018-09-21 16:43:37 +10:00
Anthony HAMON
360b7c1def commands/git : refactor test to Diff, refactor function 2018-09-20 09:11:47 +02:00
Anthony HAMON
bdeb78c9a0 commands/git : returns an error instead of panicing 2018-09-20 09:09:37 +02:00
Anthony HAMON
9481920101 commands/git : add test to GetLog 2018-09-20 09:09:37 +02:00
Jesse Duffield
a2b3cd0823 add removeAll to git 2018-09-20 09:48:56 +10:00
Jesse Duffield
8fac19c175 Merge branch 'master' into feature/informative-commit-colors 2018-09-20 09:41:29 +10:00
Jesse Duffield
b9708c9f88 fix issues with commit message panel losing focus 2018-09-19 20:36:40 +10:00
Jesse Duffield
c789bba673 color merged and unmerged commits differently 2018-09-18 21:45:35 +10:00
Mark Kopenga
c64fb87b2b Merge pull request #12 from jesseduffield/master
Update to latest master
2018-09-14 11:50:49 +02:00
Kristijan Husak
61f0801bd3 Add ammend commit action. 2018-09-13 12:44:32 +02:00
Jesse Duffield
7fb2cafd0c Merge pull request #279 from jesseduffield/hotfix/file-ordering
Restore old file merging algorithm
2018-09-12 15:20:20 +02:00
93 changed files with 4376 additions and 759 deletions

View File

@@ -1,13 +1,14 @@
# run with:
# docker build -t lazygit .
# docker run -it lazygit:latest
# docker run -it lazygit:latest /bin/sh -l
FROM golang:alpine
WORKDIR /go/src/github.com/jesseduffield/lazygit/
COPY ./ .
RUN CGO_ENABLED=0 GOOS=linux go build -o lazygit .
FROM alpine:latest
RUN apk add -U git xdg-utils
ADD . /go/src/github.com/jesseduffield/lazygit
RUN go install github.com/jesseduffield/lazygit
WORKDIR /go/src/github.com/jesseduffield/lazygit
WORKDIR /go/src/github.com/jesseduffield/lazygit/
COPY --from=0 /go/src/github.com/jesseduffield/lazygit/lazygit /bin/
RUN echo "alias gg=lazygit" >> ~/.profile

13
Gopkg.lock generated
View File

@@ -189,11 +189,19 @@
[[projects]]
branch = "master"
digest = "1:d6df25dee1274e63c25f84c257bc504359f975dedb791eec7a5b51992146bb76"
digest = "1:9b266d7748a5d94985fd9e323494f5b8ae1ab3e910418e898dfe7f03339ddbcd"
name = "github.com/jesseduffield/gocui"
packages = ["."]
pruneopts = "NUT"
revision = "4fca348422d8b6136e801b222858204a35ee369a"
revision = "cfa9e452ba5ebf014041846851152d64a59dce14"
[[projects]]
branch = "master"
digest = "1:a46c2f4863e5284ddb255c28750298e04bc8c0fc896bed6056e947673168b7be"
name = "github.com/jesseduffield/pty"
packages = ["."]
pruneopts = "NUT"
revision = "02db52c7e406c7abec44c717a173c7715e4c1b62"
[[projects]]
branch = "master"
@@ -616,6 +624,7 @@
"github.com/heroku/rollrus",
"github.com/jesseduffield/go-getter",
"github.com/jesseduffield/gocui",
"github.com/jesseduffield/pty",
"github.com/kardianos/osext",
"github.com/mgutz/str",
"github.com/nicksnyder/go-i18n/v2/i18n",

View File

@@ -37,6 +37,10 @@
branch = "master"
name = "github.com/jesseduffield/gocui"
[[constraint]]
branch = "master"
name = "github.com/jesseduffield/pty"
[[constraint]]
name = "gopkg.in/src-d/go-git.v4"
revision = "43d17e14b714665ab5bc2ecc220b6740779d733f"

View File

@@ -14,7 +14,7 @@ Jira? This is the app for you!
[Keybindings](https://github.com/jesseduffield/lazygit/blob/master/docs/Keybindings.md)
* [Cool Features](https://github.com/jesseduffield/lazygit#cool-features)
* [Contributing](https://github.com/jesseduffield/lazygit#contributing)
* [Video Tutorial](https://www.youtube.com/watch?v=VDXvbHZYeKY)
* [Video Tutorial](https://youtu.be/VDXvbHZYeKY)
* [Twitch Stream](https://www.twitch.tv/jesseduffield)
## Installation
@@ -69,6 +69,12 @@ and the git version which builds from the most recent commit.
Instruction of how to install AUR content can be found here:
https://wiki.archlinux.org/index.php/Arch_User_Repository
### Conda
Released versions are available for different platforms, see https://anaconda.org/conda-forge/lazygit
```sh
conda install -c conda-forge lazygit
```
### Binary Release (Windows/Linux/OSX)
You can download a binary release [here](https://github.com/jesseduffield/lazygit/releases).
@@ -88,7 +94,7 @@ Call `lazygit` in your terminal inside a git repository. If you want, you can
also add an alias for this with `echo "alias lg='lazygit'" >> ~/.zshrc` (or
whichever rc file you're using).
* Basic video tutorial [here](https://www.youtube.com/watch?v=VDXvbHZYeKY).
* Basic video tutorial [here](https://youtu.be/VDXvbHZYeKY).
* List of keybindings
[here](/docs/Keybindings.md).
@@ -121,6 +127,11 @@ For contributor discussion about things not better discussed here in the repo, j
[![Slack](/docs/resources/slack_rgb.png)](https://join.slack.com/t/lazygit/shared_invite/enQtNDE3MjIwNTYyMDA0LTM3Yjk3NzdiYzhhNTA1YjM4Y2M4MWNmNDBkOTI0YTE4YjQ1ZmI2YWRhZTgwNjg2YzhhYjg3NDBlMmQyMTI5N2M)
## Donate
If you would like to support the development of lazygit, please donate
[![Donate](https://d1iczxrky3cnb2.cloudfront.net/button-medium-blue.png)](https://donorbox.org/lazygit)
## Work in progress
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,

View File

@@ -6,6 +6,7 @@
gui:
# stuff relating to the UI
scrollHeight: 2 # how many lines you scroll by
scrollPastBottom: true # enable scrolling past the bottom
theme:
activeBorderColor:
- white

3
go.mod
View File

@@ -18,7 +18,8 @@ require (
github.com/heroku/rollrus v0.0.0-20180515183152-fc0cef2ff331
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99
github.com/jesseduffield/go-getter v0.0.0-20180822080847-906e15686e63
github.com/jesseduffield/gocui v0.0.0-20180919095827-4fca348422d8
github.com/jesseduffield/gocui v0.0.0-20190115084758-cfa9e452ba5e
github.com/jesseduffield/pty v0.0.0-20181218102224-02db52c7e406
github.com/jesseduffield/termbox-go v0.0.0-20180919093808-1e272ff78dcb
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1

10
go.sum
View File

@@ -33,8 +33,10 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jesseduffield/go-getter v0.0.0-20180822080847-906e15686e63 h1:Nrr/yUxNjXWYK0B3IqcFlYh1ICnesJDB4ogcfOVc5Ns=
github.com/jesseduffield/go-getter v0.0.0-20180822080847-906e15686e63/go.mod h1:fNqjRf+4XnTo2PrGN1JRb79b/BeoHwP4lU00f39SQY0=
github.com/jesseduffield/gocui v0.0.0-20180919095827-4fca348422d8 h1:XxX+IqNOFDh1PnU4eZDzUomoKbuKCvwyEm5an/IxLQU=
github.com/jesseduffield/gocui v0.0.0-20180919095827-4fca348422d8/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.0.0-20181209104758-fe55a32c8a4c h1:jEfh/vAtfF3pQ8xFhpYR/0S4iHo11VfaYelJmzZJm94=
github.com/jesseduffield/gocui v0.0.0-20181209104758-fe55a32c8a4c/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/pty v0.0.0-20181218102224-02db52c7e406 h1:iYMH6h6SuWuBkIzRtymosE8NpSgTK0oRMfyTdVWgxzc=
github.com/jesseduffield/pty v0.0.0-20181218102224-02db52c7e406/go.mod h1:7jlS40+UhOqkZJDIG1B/H21xnuET/+fvbbnHCa8wSIo=
github.com/jesseduffield/termbox-go v0.0.0-20180919093808-1e272ff78dcb h1:cFHYEWpQEfzFZVKiKZytCUX4UwQixKSw0kd3WhluPsY=
github.com/jesseduffield/termbox-go v0.0.0-20180919093808-1e272ff78dcb/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4=
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 h1:12VvqtR6Aowv3l/EQUlocDHW2Cp4G9WJVH7uyH8QFJE=
@@ -61,8 +63,8 @@ github.com/mitchellh/mapstructure v0.0.0-20180715050151-f15292f7a699 h1:KXZJFdun
github.com/mitchellh/mapstructure v0.0.0-20180715050151-f15292f7a699/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/nicksnyder/go-i18n v0.0.0-20180803040939-a16b91a3ba80 h1:7ory6RlsEkeK89iyV7Imz3sVz8YHeSw29w3PehpCWC0=
github.com/nicksnyder/go-i18n v0.0.0-20180803040939-a16b91a3ba80/go.mod h1:e4Di5xjP9oTVrC6y3C7C0HoSYXjSbhh/dU0eUV32nB4=
github.com/nicksnyder/go-i18n/v2 v2.0.0-beta.5 h1:/TjjTS4kg7vC+05gD0LE4+97f/+PRFICnK/7wJPk7kE=
github.com/nicksnyder/go-i18n/v2 v2.0.0-beta.5/go.mod h1:4Opqa6/HIv0lhG3WRAkqzO0afezkRhxXI0P8EJkqeRU=
github.com/nicksnyder/go-i18n/v2 v2.0.0-beta.6 h1:SooTCzUOOs899x/M7gmSS+dgL+AUfSWqAcHLN3auCic=
github.com/nicksnyder/go-i18n/v2 v2.0.0-beta.6/go.mod h1:4Opqa6/HIv0lhG3WRAkqzO0afezkRhxXI0P8EJkqeRU=
github.com/pelletier/go-buffruneio v0.2.0 h1:U4t4R6YkofJ5xHm3dJzuRpPZ0mr5MMCoAWooScCR7aA=
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=

View File

@@ -4,6 +4,7 @@ import (
"io"
"io/ioutil"
"os"
"path/filepath"
"github.com/heroku/rollrus"
"github.com/jesseduffield/lazygit/pkg/commands"
@@ -11,6 +12,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/updates"
"github.com/shibukawa/configdir"
"github.com/sirupsen/logrus"
)
@@ -33,9 +35,15 @@ func newProductionLogger(config config.AppConfigurer) *logrus.Logger {
return log
}
func newDevelopmentLogger() *logrus.Logger {
func globalConfigDir() string {
configDirs := configdir.New("jesseduffield", "lazygit")
configDir := configDirs.QueryFolders(configdir.Global)[0]
return configDir.Path
}
func newDevelopmentLogger(config config.AppConfigurer) *logrus.Logger {
log := logrus.New()
file, err := os.OpenFile("development.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
file, err := os.OpenFile(filepath.Join(globalConfigDir(), "development.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
panic("unable to log to file") // TODO: don't panic (also, remove this call to the `panic` function)
}
@@ -48,10 +56,15 @@ func newLogger(config config.AppConfigurer) *logrus.Entry {
environment := "production"
if config.GetDebug() {
environment = "development"
log = newDevelopmentLogger()
log = newDevelopmentLogger(config)
} else {
log = newProductionLogger(config)
}
// highly recommended: tail -f development.log | humanlog
// https://github.com/aybabtme/humanlog
log.Formatter = &logrus.JSONFormatter{}
if config.GetUserConfig().GetString("reporting") == "on" {
// this isn't really a secret token: it only has permission to push new rollbar items
hook := rollrus.NewHook("23432119147a4367abf7c0de2aa99a2d", environment)

View File

@@ -1,6 +1,7 @@
package commands
import (
"fmt"
"strings"
"github.com/fatih/color"
@@ -10,13 +11,21 @@ import (
// Branch : A git branch
// duplicating this for now
type Branch struct {
Name string
Recency string
Name string
Recency string
Pushables string
Pullables string
Selected bool
}
// GetDisplayStrings returns the dispaly string of branch
func (b *Branch) GetDisplayStrings() []string {
return []string{b.Recency, utils.ColoredString(b.Name, b.GetColor())}
displayName := utils.ColoredString(b.Name, b.GetColor())
if b.Selected && b.Pushables != "" && b.Pullables != "" {
displayName = fmt.Sprintf("%s ↑%s↓%s", displayName, b.Pushables, b.Pullables)
}
return []string{b.Recency, displayName}
}
// GetColor branch color

View File

@@ -9,17 +9,22 @@ type Commit struct {
Sha string
Name string
Pushed bool
Merged bool
DisplayString string
}
// GetDisplayStrings is a function.
func (c *Commit) GetDisplayStrings() []string {
red := color.New(color.FgRed)
yellow := color.New(color.FgYellow)
yellow := color.New(color.FgGreen)
green := color.New(color.FgYellow)
white := color.New(color.FgWhite)
shaColor := yellow
if c.Pushed {
shaColor = red
} else if !c.Merged {
shaColor = green
}
return []string{shaColor.Sprint(c.Sha), white.Sprint(c.Name)}

View File

@@ -0,0 +1,100 @@
// +build !windows
package commands
import (
"bufio"
"bytes"
"errors"
"os"
"os/exec"
"strings"
"unicode/utf8"
"github.com/jesseduffield/pty"
"github.com/mgutz/str"
)
// RunCommandWithOutputLiveWrapper runs a command and return every word that gets written in stdout
// Output is a function that executes by every word that gets read by bufio
// As return of output you need to give a string that will be written to stdin
// NOTE: If the return data is empty it won't written anything to stdin
func RunCommandWithOutputLiveWrapper(c *OSCommand, command string, output func(string) string) error {
splitCmd := str.ToArgv(command)
cmd := exec.Command(splitCmd[0], splitCmd[1:]...)
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, "LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8")
var stderr bytes.Buffer
cmd.Stderr = &stderr
ptmx, err := pty.Start(cmd)
if err != nil {
return err
}
go func() {
scanner := bufio.NewScanner(ptmx)
scanner.Split(scanWordsWithNewLines)
for scanner.Scan() {
toOutput := strings.Trim(scanner.Text(), " ")
_, _ = ptmx.WriteString(output(toOutput))
}
}()
err = cmd.Wait()
ptmx.Close()
if err != nil {
return errors.New(stderr.String())
}
return nil
}
// scanWordsWithNewLines is a copy of bufio.ScanWords but this also captures new lines
// For specific comments about this function take a look at: bufio.ScanWords
func scanWordsWithNewLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
start := 0
for width := 0; start < len(data); start += width {
var r rune
r, width = utf8.DecodeRune(data[start:])
if !isSpace(r) {
break
}
}
for width, i := 0, start; i < len(data); i += width {
var r rune
r, width = utf8.DecodeRune(data[i:])
if isSpace(r) {
return i + width, data[start:i], nil
}
}
if atEOF && len(data) > start {
return len(data), data[start:], nil
}
return start, nil, nil
}
// isSpace is also copied from the bufio package and has been modified to also captures new lines
// For specific comments about this function take a look at: bufio.isSpace
func isSpace(r rune) bool {
if r <= '\u00FF' {
switch r {
case ' ', '\t', '\v', '\f':
return true
case '\u0085', '\u00A0':
return true
}
return false
}
if '\u2000' <= r && r <= '\u200a' {
return true
}
switch r {
case '\u1680', '\u2028', '\u2029', '\u202f', '\u205f', '\u3000':
return true
}
return false
}

View File

@@ -0,0 +1,9 @@
// +build windows
package commands
// RunCommandWithOutputLiveWrapper runs a command live but because of windows compatibility this command can't be ran there
// TODO: Remove this hack and replace it with a proper way to run commands live on windows
func RunCommandWithOutputLiveWrapper(c *OSCommand, command string, output func(string) string) error {
return c.RunCommand(command)
}

View File

@@ -154,7 +154,6 @@ func (c *GitCommand) GetStatusFiles() []*File {
}
files = append(files, file)
}
c.Log.Info(files) // TODO: use a dumper-esque log here
return files
}
@@ -208,24 +207,33 @@ func includesInt(list []int, a int) bool {
return false
}
// GetBranchName branch name
func (c *GitCommand) GetBranchName() (string, error) {
return c.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD")
// ResetAndClean removes all unstaged changes and removes all untracked files
func (c *GitCommand) ResetAndClean() error {
if err := c.OSCommand.RunCommand("git reset --hard HEAD"); err != nil {
return err
}
return c.OSCommand.RunCommand("git clean -fd")
}
// ResetHard does the equivalent of `git reset --hard HEAD`
func (c *GitCommand) ResetHard() error {
return c.Worktree.Reset(&gogit.ResetOptions{Mode: gogit.HardReset})
func (c *GitCommand) GetCurrentBranchUpstreamDifferenceCount() (string, string) {
return c.GetCommitDifferences("HEAD", "@{u}")
}
// UpstreamDifferenceCount checks how many pushables/pullables there are for the
func (c *GitCommand) GetBranchUpstreamDifferenceCount(branchName string) (string, string) {
upstream := "origin" // hardcoded for now
return c.GetCommitDifferences(branchName, fmt.Sprintf("%s/%s", upstream, branchName))
}
// GetCommitDifferences checks how many pushables/pullables there are for the
// current branch
func (c *GitCommand) UpstreamDifferenceCount() (string, string) {
pushableCount, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..head --count")
func (c *GitCommand) GetCommitDifferences(from, to string) (string, string) {
command := "git rev-list %s..%s --count"
pushableCount, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf(command, to, from))
if err != nil {
return "?", "?"
}
pullableCount, err := c.OSCommand.RunCommandWithOutput("git rev-list head..@{u} --count")
pullableCount, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf(command, from, to))
if err != nil {
return "?", "?"
}
@@ -236,7 +244,7 @@ func (c *GitCommand) UpstreamDifferenceCount() (string, string) {
// to the remote branch of the current branch, a map is returned to ease look up
func (c *GitCommand) GetCommitsToPush() map[string]bool {
pushables := map[string]bool{}
o, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..head --abbrev-commit")
o, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..HEAD --abbrev-commit")
if err != nil {
return pushables
}
@@ -253,8 +261,13 @@ func (c *GitCommand) RenameCommit(name string) error {
}
// Fetch fetch git repo
func (c *GitCommand) Fetch() error {
return c.OSCommand.RunCommand("git fetch")
func (c *GitCommand) Fetch(unamePassQuestion func(string) string, canAskForCredentials bool) error {
return c.OSCommand.DetectUnamePass("git fetch", func(question string) string {
if canAskForCredentials {
return unamePassQuestion(question)
}
return "\n"
})
}
// ResetToCommit reset to commit
@@ -267,6 +280,18 @@ func (c *GitCommand) NewBranch(name string) error {
return c.OSCommand.RunCommand(fmt.Sprintf("git checkout -b %s", name))
}
// CurrentBranchName is a function.
func (c *GitCommand) CurrentBranchName() (string, error) {
branchName, err := c.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD")
if err != nil {
branchName, err = c.OSCommand.RunCommandWithOutput("git rev-parse --short HEAD")
if err != nil {
return "", err
}
}
return utils.TrimTrailingNewline(branchName), nil
}
// DeleteBranch delete branch
func (c *GitCommand) DeleteBranch(branch string, force bool) error {
command := "git branch -d"
@@ -306,8 +331,12 @@ func (c *GitCommand) usingGpg() bool {
}
// Commit commits to git
func (c *GitCommand) Commit(message string) (*exec.Cmd, error) {
command := fmt.Sprintf("git commit -m %s", c.OSCommand.Quote(message))
func (c *GitCommand) Commit(message string, amend bool) (*exec.Cmd, error) {
amendParam := ""
if amend {
amendParam = " --amend"
}
command := fmt.Sprintf("git commit%s -m %s", amendParam, c.OSCommand.Quote(message))
if c.usingGpg() {
return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command), nil
}
@@ -316,18 +345,19 @@ func (c *GitCommand) Commit(message string) (*exec.Cmd, error) {
}
// Pull pulls from repo
func (c *GitCommand) Pull() error {
return c.OSCommand.RunCommand("git pull --no-edit")
func (c *GitCommand) Pull(ask func(string) string) error {
return c.OSCommand.DetectUnamePass("git pull --no-edit", ask)
}
// Push pushes to a branch
func (c *GitCommand) Push(branchName string, force bool) error {
func (c *GitCommand) Push(branchName string, force bool, ask func(string) string) error {
forceFlag := ""
if force {
forceFlag = "--force-with-lease "
}
return c.OSCommand.RunCommand(fmt.Sprintf("git push %s -u origin %s", forceFlag, branchName))
cmd := fmt.Sprintf("git push %s -u origin %s", forceFlag, branchName)
return c.OSCommand.DetectUnamePass(cmd, ask)
}
// SquashPreviousTwoCommits squashes a commit down to the one below it
@@ -460,24 +490,62 @@ func (c *GitCommand) GetBranchGraph(branchName string) (string, error) {
return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium -100 %s", branchName))
}
func (c *GitCommand) getMergeBase() (string, error) {
currentBranch, err := c.CurrentBranchName()
if err != nil {
return "", err
}
baseBranch := "master"
if strings.HasPrefix(currentBranch, "feature/") {
baseBranch = "develop"
}
output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git merge-base HEAD %s", baseBranch))
if err != nil {
// swallowing error because it's not a big deal; probably because there are no commits yet
}
return output, nil
}
// GetCommits obtains the commits of the current branch
func (c *GitCommand) GetCommits() []*Commit {
func (c *GitCommand) GetCommits() ([]*Commit, error) {
pushables := c.GetCommitsToPush()
log := c.GetLog()
commits := []*Commit{}
lines := utils.SplitLines(log)
commits := make([]*Commit, len(lines))
// now we can split it up and turn it into commits
for _, line := range utils.SplitLines(log) {
for i, line := range lines {
splitLine := strings.Split(line, " ")
sha := splitLine[0]
_, pushed := pushables[sha]
commits = append(commits, &Commit{
commits[i] = &Commit{
Sha: sha,
Name: strings.Join(splitLine[1:], " "),
Pushed: pushed,
DisplayString: strings.Join(splitLine, " "),
})
}
}
return commits
return c.setCommitMergedStatuses(commits)
}
func (c *GitCommand) setCommitMergedStatuses(commits []*Commit) ([]*Commit, error) {
ancestor, err := c.getMergeBase()
if err != nil {
return nil, err
}
if ancestor == "" {
return commits, nil
}
passedAncestor := false
for i, commit := range commits {
if strings.HasPrefix(ancestor, commit.Sha) {
passedAncestor = true
}
commits[i].Merged = passedAncestor
}
return commits, nil
}
// GetLog gets the git log (currently limited to 30 commits for performance
@@ -500,28 +568,62 @@ func (c *GitCommand) Ignore(filename string) error {
}
// Show shows the diff of a commit
func (c *GitCommand) Show(sha string) string {
result, err := c.OSCommand.RunCommandWithOutput("git show --color " + sha)
if err != nil {
panic(err)
}
return result
func (c *GitCommand) Show(sha string) (string, error) {
return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git show --color %s", sha))
}
// GetRemoteURL returns current repo remote url
func (c *GitCommand) GetRemoteURL() string {
url, _ := c.OSCommand.RunCommandWithOutput("git config --get remote.origin.url")
return utils.TrimTrailingNewline(url)
}
// CheckRemoteBranchExists Returns remote branch
func (c *GitCommand) CheckRemoteBranchExists(branch *Branch) bool {
_, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf(
"git show-ref --verify -- refs/remotes/origin/%s",
branch.Name,
))
return err == nil
}
// Diff returns the diff of a file
func (c *GitCommand) Diff(file *File) string {
func (c *GitCommand) Diff(file *File, plain bool) string {
cachedArg := ""
trackedArg := "--"
colorArg := "--color"
fileName := c.OSCommand.Quote(file.Name)
if file.HasStagedChanges && !file.HasUnstagedChanges {
cachedArg = "--cached"
}
trackedArg := "--"
if !file.Tracked && !file.HasStagedChanges {
trackedArg = "--no-index /dev/null"
}
command := fmt.Sprintf("%s %s %s %s", "git diff --color ", cachedArg, trackedArg, fileName)
if plain {
colorArg = ""
}
command := fmt.Sprintf("git diff %s %s %s %s", colorArg, cachedArg, trackedArg, fileName)
// for now we assume an error means the file was deleted
s, _ := c.OSCommand.RunCommandWithOutput(command)
return s
}
func (c *GitCommand) ApplyPatch(patch string) (string, error) {
filename, err := c.OSCommand.CreateTempFile("patch", patch)
if err != nil {
c.Log.Error(err)
return "", err
}
defer func() { _ = c.OSCommand.RemoveFile(filename) }()
return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git apply --cached %s", filename))
}
func (c *GitCommand) FastForward(branchName string) error {
upstream := "origin" // hardcoding for now
return c.OSCommand.RunCommand(fmt.Sprintf("git fetch %s %s:%s", upstream, branchName, branchName))
}

View File

@@ -9,7 +9,6 @@ import (
"time"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/test"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
gogit "gopkg.in/src-d/go-git.v4"
@@ -24,26 +23,32 @@ type fileInfoMock struct {
sys interface{}
}
// Name is a function.
func (f fileInfoMock) Name() string {
return f.name
}
// Size is a function.
func (f fileInfoMock) Size() int64 {
return f.size
}
// Mode is a function.
func (f fileInfoMock) Mode() os.FileMode {
return f.fileMode
}
// ModTime is a function.
func (f fileInfoMock) ModTime() time.Time {
return f.fileModTime
}
// IsDir is a function.
func (f fileInfoMock) IsDir() bool {
return f.isDir
}
// Sys is a function.
func (f fileInfoMock) Sys() interface{} {
return f.sys
}
@@ -65,6 +70,7 @@ func newDummyGitCommand() *GitCommand {
}
}
// TestVerifyInGitRepo is a function.
func TestVerifyInGitRepo(t *testing.T) {
type scenario struct {
testName string
@@ -89,7 +95,7 @@ func TestVerifyInGitRepo(t *testing.T) {
},
func(err error) {
assert.Error(t, err)
assert.Regexp(t, "fatal: .ot a git repository \\(or any of the parent directories\\): \\.git", err.Error())
assert.Regexp(t, `fatal: .ot a git repository \(or any of the parent directories\s?\/?\): \.git`, err.Error())
},
},
}
@@ -101,6 +107,7 @@ func TestVerifyInGitRepo(t *testing.T) {
}
}
// TestNavigateToRepoRootDirectory is a function.
func TestNavigateToRepoRootDirectory(t *testing.T) {
type scenario struct {
testName string
@@ -157,6 +164,7 @@ func TestNavigateToRepoRootDirectory(t *testing.T) {
}
}
// TestSetupRepositoryAndWorktree is a function.
func TestSetupRepositoryAndWorktree(t *testing.T) {
type scenario struct {
testName string
@@ -225,6 +233,7 @@ func TestSetupRepositoryAndWorktree(t *testing.T) {
}
}
// TestNewGitCommand is a function.
func TestNewGitCommand(t *testing.T) {
actual, err := os.Getwd()
assert.NoError(t, err)
@@ -247,7 +256,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", err.Error())
assert.Regexp(t, `fatal: .ot a git repository ((\(or any of the parent directories\): \.git)|(\(or any parent up to mount point \/\)))`, err.Error())
},
},
{
@@ -272,6 +281,7 @@ func TestNewGitCommand(t *testing.T) {
}
}
// TestGitCommandGetStashEntries is a function.
func TestGitCommandGetStashEntries(t *testing.T) {
type scenario struct {
testName string
@@ -324,6 +334,7 @@ func TestGitCommandGetStashEntries(t *testing.T) {
}
}
// TestGitCommandGetStashEntryDiff is a function.
func TestGitCommandGetStashEntryDiff(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -338,6 +349,7 @@ func TestGitCommandGetStashEntryDiff(t *testing.T) {
assert.NoError(t, err)
}
// TestGitCommandGetStatusFiles is a function.
func TestGitCommandGetStatusFiles(t *testing.T) {
type scenario struct {
testName string
@@ -424,6 +436,7 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
}
}
// TestGitCommandStashDo is a function.
func TestGitCommandStashDo(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -436,6 +449,7 @@ func TestGitCommandStashDo(t *testing.T) {
assert.NoError(t, gitCmd.StashDo(1, "drop"))
}
// TestGitCommandStashSave is a function.
func TestGitCommandStashSave(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -448,6 +462,7 @@ func TestGitCommandStashSave(t *testing.T) {
assert.NoError(t, gitCmd.StashSave("A stash message"))
}
// TestGitCommandCommitAmend is a function.
func TestGitCommandCommitAmend(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -461,6 +476,7 @@ func TestGitCommandCommitAmend(t *testing.T) {
assert.NoError(t, err)
}
// TestGitCommandMergeStatusFiles is a function.
func TestGitCommandMergeStatusFiles(t *testing.T) {
type scenario struct {
testName string
@@ -541,7 +557,8 @@ func TestGitCommandMergeStatusFiles(t *testing.T) {
}
}
func TestGitCommandUpstreamDifferentCount(t *testing.T) {
// TestGitCommandGetCommitDifferences is a function.
func TestGitCommandGetCommitDifferences(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
@@ -562,7 +579,7 @@ func TestGitCommandUpstreamDifferentCount(t *testing.T) {
{
"Can't retrieve pullable count",
func(cmd string, args ...string) *exec.Cmd {
if args[1] == "head..@{u}" {
if args[1] == "HEAD..@{u}" {
return exec.Command("test")
}
@@ -576,7 +593,7 @@ func TestGitCommandUpstreamDifferentCount(t *testing.T) {
{
"Retrieve pullable and pushable count",
func(cmd string, args ...string) *exec.Cmd {
if args[1] == "head..@{u}" {
if args[1] == "HEAD..@{u}" {
return exec.Command("echo", "10")
}
@@ -593,11 +610,12 @@ func TestGitCommandUpstreamDifferentCount(t *testing.T) {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.OSCommand.command = s.command
s.test(gitCmd.UpstreamDifferenceCount())
s.test(gitCmd.GetCommitDifferences("HEAD", "@{u}"))
})
}
}
// TestGitCommandGetCommitsToPush is a function.
func TestGitCommandGetCommitsToPush(t *testing.T) {
type scenario struct {
testName string
@@ -636,6 +654,7 @@ func TestGitCommandGetCommitsToPush(t *testing.T) {
}
}
// TestGitCommandRenameCommit is a function.
func TestGitCommandRenameCommit(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -648,6 +667,7 @@ func TestGitCommandRenameCommit(t *testing.T) {
assert.NoError(t, gitCmd.RenameCommit("test"))
}
// TestGitCommandResetToCommit is a function.
func TestGitCommandResetToCommit(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -660,6 +680,7 @@ func TestGitCommandResetToCommit(t *testing.T) {
assert.NoError(t, gitCmd.ResetToCommit("78976bc"))
}
// TestGitCommandNewBranch is a function.
func TestGitCommandNewBranch(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -672,6 +693,7 @@ func TestGitCommandNewBranch(t *testing.T) {
assert.NoError(t, gitCmd.NewBranch("test"))
}
// TestGitCommandDeleteBranch is a function.
func TestGitCommandDeleteBranch(t *testing.T) {
type scenario struct {
testName string
@@ -721,6 +743,7 @@ func TestGitCommandDeleteBranch(t *testing.T) {
}
}
// TestGitCommandMerge is a function.
func TestGitCommandMerge(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -733,6 +756,7 @@ func TestGitCommandMerge(t *testing.T) {
assert.NoError(t, gitCmd.Merge("test"))
}
// TestGitCommandUsingGpg is a function.
func TestGitCommandUsingGpg(t *testing.T) {
type scenario struct {
testName string
@@ -826,6 +850,7 @@ func TestGitCommandUsingGpg(t *testing.T) {
}
}
// TestGitCommandCommit is a function.
func TestGitCommandCommit(t *testing.T) {
type scenario struct {
testName string
@@ -890,11 +915,82 @@ func TestGitCommandCommit(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.getGlobalGitConfig = s.getGlobalGitConfig
gitCmd.OSCommand.command = s.command
s.test(gitCmd.Commit("test"))
s.test(gitCmd.Commit("test", false))
})
}
}
// TestGitCommandCommitAmendFromFiles is a function.
func TestGitCommandCommitAmendFromFiles(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
getGlobalGitConfig func(string) (string, error)
test func(*exec.Cmd, error)
}
scenarios := []scenario{
{
"Amend commit using gpg",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "bash", cmd)
assert.EqualValues(t, []string{"-c", `git commit --amend -m 'test'`}, args)
return exec.Command("echo")
},
func(string) (string, error) {
return "true", nil
},
func(cmd *exec.Cmd, err error) {
assert.NotNil(t, cmd)
assert.Nil(t, err)
},
},
{
"Amend commit without using gpg",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"commit", "--amend", "-m", "test"}, args)
return exec.Command("echo")
},
func(string) (string, error) {
return "false", nil
},
func(cmd *exec.Cmd, err error) {
assert.Nil(t, cmd)
assert.Nil(t, err)
},
},
{
"Amend commit without using gpg with an error",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"commit", "--amend", "-m", "test"}, args)
return exec.Command("test")
},
func(string) (string, error) {
return "false", nil
},
func(cmd *exec.Cmd, err error) {
assert.Nil(t, cmd)
assert.Error(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.getGlobalGitConfig = s.getGlobalGitConfig
gitCmd.OSCommand.command = s.command
s.test(gitCmd.Commit("test", true))
})
}
}
// TestGitCommandPush is a function.
func TestGitCommandPush(t *testing.T) {
type scenario struct {
testName string
@@ -914,7 +1010,7 @@ func TestGitCommandPush(t *testing.T) {
},
false,
func(err error) {
assert.Nil(t, err)
assert.Contains(t, err.Error(), "error: failed to push some refs")
},
},
{
@@ -927,7 +1023,7 @@ func TestGitCommandPush(t *testing.T) {
},
true,
func(err error) {
assert.Nil(t, err)
assert.Contains(t, err.Error(), "error: failed to push some refs")
},
},
{
@@ -935,12 +1031,11 @@ func TestGitCommandPush(t *testing.T) {
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push", "-u", "origin", "test"}, args)
return exec.Command("test")
},
false,
func(err error) {
assert.Error(t, err)
assert.Contains(t, err.Error(), "error: failed to push some refs")
},
},
}
@@ -949,11 +1044,15 @@ func TestGitCommandPush(t *testing.T) {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.OSCommand.command = s.command
s.test(gitCmd.Push("test", s.forcePush))
err := gitCmd.Push("test", s.forcePush, func(passOrUname string) string {
return "\n"
})
s.test(err)
})
}
}
// TestGitCommandSquashPreviousTwoCommits is a function.
func TestGitCommandSquashPreviousTwoCommits(t *testing.T) {
type scenario struct {
testName string
@@ -1017,6 +1116,7 @@ func TestGitCommandSquashPreviousTwoCommits(t *testing.T) {
}
}
// TestGitCommandSquashFixupCommit is a function.
func TestGitCommandSquashFixupCommit(t *testing.T) {
type scenario struct {
testName string
@@ -1080,6 +1180,7 @@ func TestGitCommandSquashFixupCommit(t *testing.T) {
}
}
// TestGitCommandCatFile is a function.
func TestGitCommandCatFile(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -1094,6 +1195,7 @@ func TestGitCommandCatFile(t *testing.T) {
assert.Equal(t, "test", o)
}
// TestGitCommandStageFile is a function.
func TestGitCommandStageFile(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -1106,6 +1208,7 @@ func TestGitCommandStageFile(t *testing.T) {
assert.NoError(t, gitCmd.StageFile("test.txt"))
}
// TestGitCommandUnstageFile is a function.
func TestGitCommandUnstageFile(t *testing.T) {
type scenario struct {
testName string
@@ -1152,6 +1255,7 @@ func TestGitCommandUnstageFile(t *testing.T) {
}
}
// TestGitCommandIsInMergeState is a function.
func TestGitCommandIsInMergeState(t *testing.T) {
type scenario struct {
testName string
@@ -1220,6 +1324,7 @@ func TestGitCommandIsInMergeState(t *testing.T) {
}
}
// TestGitCommandRemoveFile is a function.
func TestGitCommandRemoveFile(t *testing.T) {
type scenario struct {
testName string
@@ -1421,6 +1526,21 @@ func TestGitCommandRemoveFile(t *testing.T) {
}
}
// TestGitCommandShow is a function.
func TestGitCommandShow(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"show", "--color", "456abcde"}, args)
return exec.Command("echo")
}
_, err := gitCmd.Show("456abcde")
assert.NoError(t, err)
}
// TestGitCommandCheckout is a function.
func TestGitCommandCheckout(t *testing.T) {
type scenario struct {
testName string
@@ -1467,6 +1587,7 @@ func TestGitCommandCheckout(t *testing.T) {
}
}
// TestGitCommandGetBranchGraph is a function.
func TestGitCommandGetBranchGraph(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
@@ -1480,11 +1601,12 @@ func TestGitCommandGetBranchGraph(t *testing.T) {
assert.NoError(t, err)
}
// TestGitCommandGetCommits is a function.
func TestGitCommandGetCommits(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func([]*Commit)
test func([]*Commit, error)
}
scenarios := []scenario{
@@ -1495,16 +1617,23 @@ func TestGitCommandGetCommits(t *testing.T) {
switch args[0] {
case "rev-list":
assert.EqualValues(t, []string{"rev-list", "@{u}..head", "--abbrev-commit"}, args)
assert.EqualValues(t, []string{"rev-list", "@{u}..HEAD", "--abbrev-commit"}, args)
return exec.Command("echo")
case "log":
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
return exec.Command("echo")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("test")
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
}
return nil
},
func(commits []*Commit) {
func(commits []*Commit, err error) {
assert.NoError(t, err)
assert.Len(t, commits, 0)
},
},
@@ -1515,33 +1644,74 @@ func TestGitCommandGetCommits(t *testing.T) {
switch args[0] {
case "rev-list":
assert.EqualValues(t, []string{"rev-list", "@{u}..head", "--abbrev-commit"}, args)
assert.EqualValues(t, []string{"rev-list", "@{u}..HEAD", "--abbrev-commit"}, args)
return exec.Command("echo", "8a2bb0e")
case "log":
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
return exec.Command("echo", "8a2bb0e commit 1\n78976bc commit 2")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("echo", "78976bc")
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
}
return nil
},
func(commits []*Commit) {
func(commits []*Commit, err error) {
assert.NoError(t, err)
assert.Len(t, commits, 2)
assert.EqualValues(t, []*Commit{
{
Sha: "8a2bb0e",
Name: "commit 1",
Pushed: true,
Merged: false,
DisplayString: "8a2bb0e commit 1",
},
{
Sha: "78976bc",
Name: "commit 2",
Pushed: false,
Merged: true,
DisplayString: "78976bc commit 2",
},
}, commits)
},
},
{
"GetCommits bubbles up an error from setCommitMergedStatuses",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "rev-list":
assert.EqualValues(t, []string{"rev-list", "@{u}..HEAD", "--abbrev-commit"}, args)
return exec.Command("echo", "8a2bb0e")
case "log":
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
return exec.Command("echo", "8a2bb0e commit 1\n78976bc commit 2")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("echo", "78976bc")
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
// here's where we are returning the error
return exec.Command("test")
case "rev-parse":
assert.EqualValues(t, []string{"rev-parse", "--short", "HEAD"}, args)
// here too
return exec.Command("test")
}
return nil
},
func(commits []*Commit, err error) {
assert.Error(t, err)
assert.Len(t, commits, 0)
},
},
}
for _, s := range scenarios {
@@ -1553,97 +1723,338 @@ func TestGitCommandGetCommits(t *testing.T) {
}
}
func TestGitCommandDiff(t *testing.T) {
gitCommand := newDummyGitCommand()
assert.NoError(t, test.GenerateRepo("lots_of_diffs.sh"))
// TestGitCommandGetLog is a function.
func TestGitCommandGetLog(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(string)
}
files := []*File{
scenarios := []scenario{
{
Name: "deleted_staged",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: true,
Deleted: true,
HasMergeConflicts: false,
DisplayString: " D deleted_staged",
"Retrieves logs",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
return exec.Command("echo", "6f0b32f commands/git : add GetCommits tests refactor\n9d9d775 circle : remove new line")
},
func(output string) {
assert.EqualValues(t, "6f0b32f commands/git : add GetCommits tests refactor\n9d9d775 circle : remove new line\n", output)
},
},
{
Name: "file with space staged",
HasStagedChanges: true,
HasUnstagedChanges: false,
Tracked: false,
Deleted: false,
HasMergeConflicts: false,
DisplayString: "A \"file with space staged\"",
},
{
Name: "file with space unstaged",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: false,
Deleted: false,
HasMergeConflicts: false,
DisplayString: "?? file with space unstaged",
},
{
Name: "modified_unstaged",
HasStagedChanges: true,
HasUnstagedChanges: false,
Tracked: true,
Deleted: false,
HasMergeConflicts: false,
DisplayString: "M modified_unstaged",
},
{
Name: "modified_staged",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: true,
Deleted: false,
HasMergeConflicts: false,
DisplayString: " M modified_staged",
},
{
Name: "renamed_before -> renamed_after",
HasStagedChanges: true,
HasUnstagedChanges: false,
Tracked: true,
Deleted: false,
HasMergeConflicts: false,
DisplayString: "R renamed_before -> renamed_after",
},
{
Name: "untracked_unstaged",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: false,
Deleted: false,
HasMergeConflicts: false,
DisplayString: "?? untracked_unstaged",
},
{
Name: "untracked_staged",
HasStagedChanges: true,
HasUnstagedChanges: false,
Tracked: false,
Deleted: false,
HasMergeConflicts: false,
DisplayString: "A untracked_staged",
},
{
Name: "master",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: false,
Deleted: false,
HasMergeConflicts: false,
DisplayString: "?? master",
"An error occurred when retrieving logs",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
return exec.Command("test")
},
func(output string) {
assert.Empty(t, output)
},
},
}
for _, file := range files {
t.Run(file.Name, func(t *testing.T) {
assert.NotContains(t, gitCommand.Diff(file), "error")
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.OSCommand.command = s.command
s.test(gitCmd.GetLog())
})
}
}
// TestGitCommandDiff is a function.
func TestGitCommandDiff(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
file *File
plain bool
}
scenarios := []scenario{
{
"Default case",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"diff", "--color", "--", "test.txt"}, args)
return exec.Command("echo")
},
&File{
Name: "test.txt",
HasStagedChanges: false,
Tracked: true,
},
false,
},
{
"Default case",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"diff", "--", "test.txt"}, args)
return exec.Command("echo")
},
&File{
Name: "test.txt",
HasStagedChanges: false,
Tracked: true,
},
true,
},
{
"All changes staged",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"diff", "--color", "--cached", "--", "test.txt"}, args)
return exec.Command("echo")
},
&File{
Name: "test.txt",
HasStagedChanges: true,
HasUnstagedChanges: false,
Tracked: true,
},
false,
},
{
"File not tracked and file has no staged changes",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"diff", "--color", "--no-index", "/dev/null", "test.txt"}, args)
return exec.Command("echo")
},
&File{
Name: "test.txt",
HasStagedChanges: false,
Tracked: false,
},
false,
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.OSCommand.command = s.command
gitCmd.Diff(s.file, s.plain)
})
}
}
// TestGitCommandGetMergeBase is a function.
func TestGitCommandGetMergeBase(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(string, error)
}
scenarios := []scenario{
{
"swallows an error if the call to merge-base returns an error",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("test")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "", output)
},
},
{
"returns the commit when master",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("echo", "blah")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.Equal(t, "blah\n", output)
},
},
{
"checks against develop when a feature branch",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "feature/test")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "develop"}, args)
return exec.Command("echo", "blah")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.Equal(t, "blah\n", output)
},
},
{
"bubbles up error if there is one",
func(cmd string, args ...string) *exec.Cmd {
return exec.Command("test")
},
func(output string, err error) {
assert.Error(t, err)
assert.Equal(t, "", output)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.OSCommand.command = s.command
s.test(gitCmd.getMergeBase())
})
}
}
// TestGitCommandCurrentBranchName is a function.
func TestGitCommandCurrentBranchName(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(string, error)
}
scenarios := []scenario{
{
"says we are on the master branch if we are",
func(cmd string, args ...string) *exec.Cmd {
assert.Equal(t, "git", cmd)
return exec.Command("echo", "master")
},
func(output string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "master", output)
},
},
{
"falls back to git rev-parse if symbolic-ref fails",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("test")
case "rev-parse":
assert.EqualValues(t, []string{"rev-parse", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "master", output)
},
},
{
"bubbles up error if there is one",
func(cmd string, args ...string) *exec.Cmd {
assert.Equal(t, "git", cmd)
return exec.Command("test")
},
func(output string, err error) {
assert.Error(t, err)
assert.EqualValues(t, "", output)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.OSCommand.command = s.command
s.test(gitCmd.CurrentBranchName())
})
}
}
func TestGitCommandApplyPatch(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(string, error)
}
scenarios := []scenario{
{
"valid case",
func(cmd string, args ...string) *exec.Cmd {
assert.Equal(t, "git", cmd)
assert.EqualValues(t, []string{"apply", "--cached"}, args[0:2])
filename := args[2]
content, err := ioutil.ReadFile(filename)
assert.NoError(t, err)
assert.Equal(t, "test", string(content))
return exec.Command("echo", "done")
},
func(output string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "done\n", output)
},
},
{
"command returns error",
func(cmd string, args ...string) *exec.Cmd {
assert.Equal(t, "git", cmd)
assert.EqualValues(t, []string{"apply", "--cached"}, args[0:2])
filename := args[2]
// TODO: Ideally we want to mock out OSCommand here so that we're not
// double handling testing it's CreateTempFile functionality,
// but it is going to take a bit of work to make a proper mock for it
// so I'm leaving it for another PR
content, err := ioutil.ReadFile(filename)
assert.NoError(t, err)
assert.Equal(t, "test", string(content))
return exec.Command("test")
},
func(output string, err error) {
assert.Error(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := newDummyGitCommand()
gitCmd.OSCommand.command = s.command
s.test(gitCmd.ApplyPatch("test"))
})
}
}

View File

@@ -2,15 +2,15 @@ package commands
import (
"errors"
"io/ioutil"
"os"
"os/exec"
"regexp"
"strings"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/mgutz/str"
"github.com/sirupsen/logrus"
gitconfig "github.com/tcnksm/go-gitconfig"
)
@@ -22,6 +22,7 @@ type Platform struct {
shellArg string
escapedQuote string
openCommand string
openLinkCommand string
fallbackEscapedQuote string
}
@@ -57,6 +58,36 @@ func (c *OSCommand) RunCommandWithOutput(command string) (string, error) {
)
}
// RunCommandWithOutputLive runs RunCommandWithOutputLiveWrapper
func (c *OSCommand) RunCommandWithOutputLive(command string, output func(string) string) error {
return RunCommandWithOutputLiveWrapper(c, command, output)
}
// DetectUnamePass detect a username / password question in a command
// ask is a function that gets executen when this function detect you need to fillin a password
// The ask argument will be "username" or "password" and expects the user's password or username back
func (c *OSCommand) DetectUnamePass(command string, ask func(string) string) error {
ttyText := ""
errMessage := c.RunCommandWithOutputLive(command, func(word string) string {
ttyText = ttyText + " " + word
prompts := map[string]string{
"password": `Password\s*for\s*'.+':`,
"username": `Username\s*for\s*'.+':`,
}
for askFor, pattern := range prompts {
if match, _ := regexp.MatchString(pattern, ttyText); match {
ttyText = ""
return ask(askFor)
}
}
return ""
})
return errMessage
}
// RunCommand runs a command and just returns the error
func (c *OSCommand) RunCommand(command string) error {
_, err := c.RunCommandWithOutput(command)
@@ -110,6 +141,18 @@ func (c *OSCommand) OpenFile(filename string) error {
return err
}
// OpenLink opens a file with the given
func (c *OSCommand) OpenLink(link string) error {
commandTemplate := c.Config.GetUserConfig().GetString("os.openLinkCommand")
templateValues := map[string]string{
"link": c.Quote(link),
}
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
err := c.RunCommand(command)
return err
}
// EditFile opens a file in a subprocess using whatever editor is available,
// falling back to core.editor, VISUAL, EDITOR, then vi
func (c *OSCommand) EditFile(filename string) (*exec.Cmd, error) {
@@ -165,3 +208,28 @@ func (c *OSCommand) AppendLineToFile(filename, line string) error {
_, err = f.WriteString("\n" + line)
return err
}
// CreateTempFile writes a string to a new temp file and returns the file's name
func (c *OSCommand) CreateTempFile(filename, content string) (string, error) {
tmpfile, err := ioutil.TempFile("", filename)
if err != nil {
c.Log.Error(err)
return "", err
}
if _, err := tmpfile.WriteString(content); err != nil {
c.Log.Error(err)
return "", err
}
if err := tmpfile.Close(); err != nil {
c.Log.Error(err)
return "", err
}
return tmpfile.Name(), nil
}
// RemoveFile removes a file at the specified path
func (c *OSCommand) RemoveFile(filename string) error {
return os.Remove(filename)
}

View File

@@ -13,6 +13,7 @@ func getPlatform() *Platform {
shellArg: "-c",
escapedQuote: "'",
openCommand: "open {{filename}}",
openLinkCommand: "open {{link}}",
fallbackEscapedQuote: "\"",
}
}

View File

@@ -1,6 +1,7 @@
package commands
import (
"io/ioutil"
"os"
"os/exec"
"testing"
@@ -29,6 +30,7 @@ func newDummyAppConfig() *config.AppConfig {
return appConfig
}
// TestOSCommandRunCommandWithOutput is a function.
func TestOSCommandRunCommandWithOutput(t *testing.T) {
type scenario struct {
command string
@@ -56,6 +58,7 @@ func TestOSCommandRunCommandWithOutput(t *testing.T) {
}
}
// TestOSCommandRunCommand is a function.
func TestOSCommandRunCommand(t *testing.T) {
type scenario struct {
command string
@@ -76,6 +79,7 @@ func TestOSCommandRunCommand(t *testing.T) {
}
}
// TestOSCommandOpenFile is a function.
func TestOSCommandOpenFile(t *testing.T) {
type scenario struct {
filename string
@@ -126,6 +130,7 @@ func TestOSCommandOpenFile(t *testing.T) {
}
}
// TestOSCommandEditFile is a function.
func TestOSCommandEditFile(t *testing.T) {
type scenario struct {
filename string
@@ -255,6 +260,7 @@ func TestOSCommandEditFile(t *testing.T) {
}
}
// TestOSCommandQuote is a function.
func TestOSCommandQuote(t *testing.T) {
osCommand := newDummyOSCommand()
@@ -278,7 +284,7 @@ func TestOSCommandQuoteSingleQuote(t *testing.T) {
assert.EqualValues(t, expected, actual)
}
// TestOSCommandQuoteSingleQuote tests the quote function with " quotes explicitly for Linux
// TestOSCommandQuoteDoubleQuote tests the quote function with " quotes explicitly for Linux
func TestOSCommandQuoteDoubleQuote(t *testing.T) {
osCommand := newDummyOSCommand()
@@ -291,6 +297,7 @@ func TestOSCommandQuoteDoubleQuote(t *testing.T) {
assert.EqualValues(t, expected, actual)
}
// TestOSCommandUnquote is a function.
func TestOSCommandUnquote(t *testing.T) {
osCommand := newDummyOSCommand()
@@ -301,6 +308,7 @@ func TestOSCommandUnquote(t *testing.T) {
assert.EqualValues(t, expected, actual)
}
// TestOSCommandFileType is a function.
func TestOSCommandFileType(t *testing.T) {
type scenario struct {
path string
@@ -357,3 +365,34 @@ func TestOSCommandFileType(t *testing.T) {
_ = os.RemoveAll(s.path)
}
}
func TestOSCommandCreateTempFile(t *testing.T) {
type scenario struct {
testName string
filename string
content string
test func(string, error)
}
scenarios := []scenario{
{
"valid case",
"filename",
"content",
func(path string, err error) {
assert.NoError(t, err)
content, err := ioutil.ReadFile(path)
assert.NoError(t, err)
assert.Equal(t, "content", string(content))
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
s.test(newDummyOSCommand().CreateTempFile(s.filename, s.content))
})
}
}

View File

@@ -0,0 +1,105 @@
package commands
import (
"errors"
"fmt"
"strings"
)
// Service is a service that repository is on (Github, Bitbucket, ...)
type Service struct {
Name string
PullRequestURL string
}
// PullRequest opens a link in browser to create new pull request
// with selected branch
type PullRequest struct {
GitServices []*Service
GitCommand *GitCommand
}
// RepoInformation holds some basic information about the repo
type RepoInformation struct {
Owner string
Repository string
}
func getServices() []*Service {
return []*Service{
{
Name: "github.com",
PullRequestURL: "https://github.com/%s/%s/compare/%s?expand=1",
},
{
Name: "bitbucket.org",
PullRequestURL: "https://bitbucket.org/%s/%s/pull-requests/new?t=%s",
},
{
Name: "gitlab.com",
PullRequestURL: "https://gitlab.com/%s/%s/merge_requests/new?merge_request[source_branch]=%s",
},
}
}
// NewPullRequest creates new instance of PullRequest
func NewPullRequest(gitCommand *GitCommand) *PullRequest {
return &PullRequest{
GitServices: getServices(),
GitCommand: gitCommand,
}
}
// Create opens link to new pull request in browser
func (pr *PullRequest) Create(branch *Branch) error {
branchExistsOnRemote := pr.GitCommand.CheckRemoteBranchExists(branch)
if !branchExistsOnRemote {
return errors.New(pr.GitCommand.Tr.SLocalize("NoBranchOnRemote"))
}
repoURL := pr.GitCommand.GetRemoteURL()
var gitService *Service
for _, service := range pr.GitServices {
if strings.Contains(repoURL, service.Name) {
gitService = service
break
}
}
if gitService == nil {
return errors.New(pr.GitCommand.Tr.SLocalize("UnsupportedGitService"))
}
repoInfo := getRepoInfoFromURL(repoURL)
return pr.GitCommand.OSCommand.OpenLink(fmt.Sprintf(
gitService.PullRequestURL, repoInfo.Owner, repoInfo.Repository, branch.Name,
))
}
func getRepoInfoFromURL(url string) *RepoInformation {
isHTTP := strings.HasPrefix(url, "http")
if isHTTP {
splits := strings.Split(url, "/")
owner := splits[len(splits)-2]
repo := strings.TrimSuffix(splits[len(splits)-1], ".git")
return &RepoInformation{
Owner: owner,
Repository: repo,
}
}
tmpSplit := strings.Split(url, ":")
splits := strings.Split(tmpSplit[1], "/")
owner := splits[0]
repo := strings.TrimSuffix(splits[1], ".git")
return &RepoInformation{
Owner: owner,
Repository: repo,
}
}

View File

@@ -0,0 +1,154 @@
package commands
import (
"os/exec"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
// TestGetRepoInfoFromURL is a function.
func TestGetRepoInfoFromURL(t *testing.T) {
type scenario struct {
testName string
repoURL string
test func(*RepoInformation)
}
scenarios := []scenario{
{
"Returns repository information for git remote url",
"git@github.com:petersmith/super_calculator",
func(repoInfo *RepoInformation) {
assert.EqualValues(t, repoInfo.Owner, "petersmith")
assert.EqualValues(t, repoInfo.Repository, "super_calculator")
},
},
{
"Returns repository information for http remote url",
"https://my_username@bitbucket.org/johndoe/social_network.git",
func(repoInfo *RepoInformation) {
assert.EqualValues(t, repoInfo.Owner, "johndoe")
assert.EqualValues(t, repoInfo.Repository, "social_network")
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
s.test(getRepoInfoFromURL(s.repoURL))
})
}
}
// TestCreatePullRequest is a function.
func TestCreatePullRequest(t *testing.T) {
type scenario struct {
testName string
branch *Branch
command func(string, ...string) *exec.Cmd
test func(err error)
}
scenarios := []scenario{
{
"Opens a link to new pull request on bitbucket",
&Branch{
Name: "feature/profile-page",
},
func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return exec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
}
assert.Equal(t, cmd, "open")
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?t=feature/profile-page"})
return exec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
},
{
"Opens a link to new pull request on bitbucket with http remote url",
&Branch{
Name: "feature/events",
},
func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return exec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
}
assert.Equal(t, cmd, "open")
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?t=feature/events"})
return exec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
},
{
"Opens a link to new pull request on github",
&Branch{
Name: "feature/sum-operation",
},
func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return exec.Command("echo", "git@github.com:peter/calculator.git")
}
assert.Equal(t, cmd, "open")
assert.Equal(t, args, []string{"https://github.com/peter/calculator/compare/feature/sum-operation?expand=1"})
return exec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
},
{
"Opens a link to new pull request on gitlab",
&Branch{
Name: "feature/ui",
},
func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return exec.Command("echo", "git@gitlab.com:peter/calculator.git")
}
assert.Equal(t, cmd, "open")
assert.Equal(t, args, []string{"https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"})
return exec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
},
{
"Throws an error if git service is unsupported",
&Branch{
Name: "feature/divide-operation",
},
func(cmd string, args ...string) *exec.Cmd {
return exec.Command("echo", "git@something.com:peter/calculator.git")
},
func(err error) {
assert.Error(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCommand := newDummyGitCommand()
gitCommand.OSCommand.command = s.command
gitCommand.OSCommand.Config.GetUserConfig().Set("os.openLinkCommand", "open {{link}}")
dummyPullRequest := NewPullRequest(gitCommand)
s.test(dummyPullRequest.Create(s.branch))
})
}
}

View File

@@ -20,6 +20,7 @@ type AppConfig struct {
BuildSource string `long:"build-source" env:"BUILD_SOURCE" default:""`
UserConfig *viper.Viper
AppState *AppState
IsNewRepo bool
}
// AppConfigurer interface allows individual app config structs to inherit Fields
@@ -36,6 +37,8 @@ type AppConfigurer interface {
WriteToUserConfig(string, string) error
SaveAppState() error
LoadAppState() error
SetIsNewRepo(bool)
GetIsNewRepo() bool
}
// NewAppConfig makes a new app config
@@ -54,6 +57,7 @@ func NewAppConfig(name, version, commit, date string, buildSource string, debugg
BuildSource: buildSource,
UserConfig: userConfig,
AppState: &AppState{},
IsNewRepo: false,
}
if err := appConfig.LoadAppState(); err != nil {
@@ -63,6 +67,16 @@ func NewAppConfig(name, version, commit, date string, buildSource string, debugg
return appConfig, nil
}
// GetIsNewRepo returns known repo boolean
func (c *AppConfig) GetIsNewRepo() bool {
return c.IsNewRepo
}
// SetIsNewRepo set if the current repo is known
func (c *AppConfig) SetIsNewRepo(toSet bool) {
c.IsNewRepo = toSet
}
// GetDebug returns debug flag
func (c *AppConfig) GetDebug() bool {
return c.Debug
@@ -153,7 +167,7 @@ func prepareConfigFile(filename string) (string, error) {
}
// LoadAndMergeFile Loads the config/state file, creating
// the file as an empty one if it does not exist
// the file has an empty one if it does not exist
func LoadAndMergeFile(v *viper.Viper, filename string) error {
configPath, err := prepareConfigFile(filename)
if err != nil {
@@ -214,6 +228,7 @@ func GetDefaultConfig() []byte {
`gui:
## stuff relating to the UI
scrollHeight: 2
scrollPastBottom: true
theme:
activeBorderColor:
- white
@@ -241,9 +256,9 @@ type AppState struct {
func getDefaultAppState() []byte {
return []byte(`
lastUpdateCheck: 0
recentRepos: []
`)
lastUpdateCheck: 0
recentRepos: []
`)
}
// // commenting this out until we use it again

View File

@@ -6,5 +6,6 @@ package config
func GetPlatformDefaultConfig() []byte {
return []byte(
`os:
openCommand: 'open {{filename}}'`)
openCommand: 'open {{filename}}'
openLinkCommand: 'open {{link}}'`)
}

View File

@@ -4,5 +4,6 @@ package config
func GetPlatformDefaultConfig() []byte {
return []byte(
`os:
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'`)
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'
openLinkCommand: 'sh -c "xdg-open {{link}} >/dev/null"'`)
}

View File

@@ -4,5 +4,6 @@ package config
func GetPlatformDefaultConfig() []byte {
return []byte(
`os:
openCommand: 'cmd /c "start "" {{filename}}"'`)
openCommand: 'cmd /c "start "" {{filename}}"'
openLinkCommand: 'cmd /c "start "" {{link}}"'`)
}

View File

@@ -35,16 +35,12 @@ func NewBranchListBuilder(log *logrus.Entry, gitCommand *commands.GitCommand) (*
}
func (b *BranchListBuilder) obtainCurrentBranch() *commands.Branch {
// I used go-git for this, but that breaks if you've just done a git init,
// even though you're on 'master'
branchName, err := b.GitCommand.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD")
branchName, err := b.GitCommand.CurrentBranchName()
if err != nil {
branchName, err = b.GitCommand.OSCommand.RunCommandWithOutput("git rev-parse --short HEAD")
if err != nil {
panic(err.Error())
}
panic(err.Error())
}
return &commands.Branch{Name: strings.TrimSpace(branchName), Recency: " *"}
return &commands.Branch{Name: strings.TrimSpace(branchName)}
}
func (b *BranchListBuilder) obtainReflogBranches() []*commands.Branch {
@@ -61,7 +57,7 @@ func (b *BranchListBuilder) obtainReflogBranches() []*commands.Branch {
branch := &commands.Branch{Name: branchName, Recency: timeNumber + timeUnit}
branches = append(branches, branch)
}
return branches
return uniqueByName(branches)
}
func (b *BranchListBuilder) obtainSafeBranches() []*commands.Branch {
@@ -103,11 +99,8 @@ func (b *BranchListBuilder) Build() []*commands.Branch {
branches := make([]*commands.Branch, 0)
head := b.obtainCurrentBranch()
safeBranches := b.obtainSafeBranches()
if len(safeBranches) == 0 {
return append(branches, head)
}
reflogBranches := b.obtainReflogBranches()
reflogBranches = uniqueByName(append([]*commands.Branch{head}, reflogBranches...))
for i, reflogBranch := range reflogBranches {
reflogBranches[i].Name = sanitisedReflogName(reflogBranch, safeBranches)
}
@@ -115,6 +108,12 @@ func (b *BranchListBuilder) Build() []*commands.Branch {
branches = b.appendNewBranches(branches, reflogBranches, safeBranches, true)
branches = b.appendNewBranches(branches, safeBranches, branches, false)
if len(branches) == 0 || branches[0].Name != head.Name {
branches = append([]*commands.Branch{head}, branches...)
}
branches[0].Recency = " *"
return branches
}

156
pkg/git/patch_modifier.go Normal file
View File

@@ -0,0 +1,156 @@
package git
import (
"errors"
"regexp"
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
type PatchModifier struct {
Log *logrus.Entry
Tr *i18n.Localizer
}
// NewPatchModifier builds a new branch list builder
func NewPatchModifier(log *logrus.Entry) (*PatchModifier, error) {
return &PatchModifier{
Log: log,
}, nil
}
// ModifyPatchForHunk takes the original patch, which may contain several hunks,
// and removes any hunks that aren't the selected hunk
func (p *PatchModifier) ModifyPatchForHunk(patch string, hunkStarts []int, currentLine int) (string, error) {
// get hunk start and end
lines := strings.Split(patch, "\n")
hunkStartIndex := utils.PrevIndex(hunkStarts, currentLine)
hunkStart := hunkStarts[hunkStartIndex]
nextHunkStartIndex := utils.NextIndex(hunkStarts, currentLine)
var hunkEnd int
if nextHunkStartIndex == 0 {
hunkEnd = len(lines) - 1
} else {
hunkEnd = hunkStarts[nextHunkStartIndex]
}
headerLength, err := p.getHeaderLength(lines)
if err != nil {
return "", err
}
output := strings.Join(lines[0:headerLength], "\n") + "\n"
output += strings.Join(lines[hunkStart:hunkEnd], "\n") + "\n"
return output, nil
}
func (p *PatchModifier) getHeaderLength(patchLines []string) (int, error) {
for index, line := range patchLines {
if strings.HasPrefix(line, "@@") {
return index, nil
}
}
return 0, errors.New(p.Tr.SLocalize("CantFindHunks"))
}
// ModifyPatchForLine takes the original patch, which may contain several hunks,
// and the line number of the line we want to stage
func (p *PatchModifier) ModifyPatchForLine(patch string, lineNumber int) (string, error) {
lines := strings.Split(patch, "\n")
headerLength, err := p.getHeaderLength(lines)
if err != nil {
return "", err
}
output := strings.Join(lines[0:headerLength], "\n") + "\n"
hunkStart, err := p.getHunkStart(lines, lineNumber)
if err != nil {
return "", err
}
hunk, err := p.getModifiedHunk(lines, hunkStart, lineNumber)
if err != nil {
return "", err
}
output += strings.Join(hunk, "\n")
return output, nil
}
// getHunkStart returns the line number of the hunk we're going to be modifying
// in order to stage our line
func (p *PatchModifier) getHunkStart(patchLines []string, lineNumber int) (int, error) {
// find the hunk that we're modifying
hunkStart := 0
for index, line := range patchLines {
if strings.HasPrefix(line, "@@") {
hunkStart = index
}
if index == lineNumber {
return hunkStart, nil
}
}
return 0, errors.New(p.Tr.SLocalize("CantFindHunk"))
}
func (p *PatchModifier) getModifiedHunk(patchLines []string, hunkStart int, lineNumber int) ([]string, error) {
lineChanges := 0
// strip the hunk down to just the line we want to stage
newHunk := []string{patchLines[hunkStart]}
for offsetIndex, line := range patchLines[hunkStart+1:] {
index := offsetIndex + hunkStart + 1
if strings.HasPrefix(line, "@@") {
newHunk = append(newHunk, "\n")
break
}
if index != lineNumber {
// we include other removals but treat them like context
if strings.HasPrefix(line, "-") {
newHunk = append(newHunk, " "+line[1:])
lineChanges += 1
continue
}
// we don't include other additions
if strings.HasPrefix(line, "+") {
lineChanges -= 1
continue
}
}
newHunk = append(newHunk, line)
}
var err error
newHunk[0], err = p.updatedHeader(newHunk[0], lineChanges)
if err != nil {
return nil, err
}
return newHunk, nil
}
// updatedHeader returns the hunk header with the updated line range
// we need to update the hunk length to reflect the changes we made
// if the hunk has three additions but we're only staging one, then
// @@ -14,8 +14,11 @@ import (
// becomes
// @@ -14,8 +14,9 @@ import (
func (p *PatchModifier) updatedHeader(currentHeader string, lineChanges int) (string, error) {
// current counter is the number after the second comma
re := regexp.MustCompile(`(\d+) @@`)
prevLengthString := re.FindStringSubmatch(currentHeader)[1]
prevLength, err := strconv.Atoi(prevLengthString)
if err != nil {
return "", err
}
re = regexp.MustCompile(`\d+ @@`)
newLength := strconv.Itoa(prevLength + lineChanges)
return re.ReplaceAllString(currentHeader, newLength+" @@"), nil
}

View File

@@ -0,0 +1,89 @@
package git
import (
"io/ioutil"
"testing"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
func newDummyLog() *logrus.Entry {
log := logrus.New()
log.Out = ioutil.Discard
return log.WithField("test", "test")
}
func newDummyPatchModifier() *PatchModifier {
return &PatchModifier{
Log: newDummyLog(),
}
}
func TestModifyPatchForLine(t *testing.T) {
type scenario struct {
testName string
patchFilename string
lineNumber int
shouldError bool
expectedPatchFilename string
}
scenarios := []scenario{
{
"Removing one line",
"testdata/testPatchBefore.diff",
8,
false,
"testdata/testPatchAfter1.diff",
},
{
"Adding one line",
"testdata/testPatchBefore.diff",
10,
false,
"testdata/testPatchAfter2.diff",
},
{
"Adding one line in top hunk in diff with multiple hunks",
"testdata/testPatchBefore2.diff",
20,
false,
"testdata/testPatchAfter3.diff",
},
{
"Adding one line in top hunk in diff with multiple hunks",
"testdata/testPatchBefore2.diff",
53,
false,
"testdata/testPatchAfter4.diff",
},
{
"adding unstaged file with a single line",
"testdata/addedFile.diff",
6,
false,
"testdata/addedFile.diff",
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
p := newDummyPatchModifier()
beforePatch, err := ioutil.ReadFile(s.patchFilename)
if err != nil {
panic("Cannot open file at " + s.patchFilename)
}
afterPatch, err := p.ModifyPatchForLine(string(beforePatch), s.lineNumber)
if s.shouldError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
expected, err := ioutil.ReadFile(s.expectedPatchFilename)
if err != nil {
panic("Cannot open file at " + s.expectedPatchFilename)
}
assert.Equal(t, string(expected), afterPatch)
}
})
}
}

36
pkg/git/patch_parser.go Normal file
View File

@@ -0,0 +1,36 @@
package git
import (
"strings"
"github.com/sirupsen/logrus"
)
type PatchParser struct {
Log *logrus.Entry
}
// NewPatchParser builds a new branch list builder
func NewPatchParser(log *logrus.Entry) (*PatchParser, error) {
return &PatchParser{
Log: log,
}, nil
}
func (p *PatchParser) ParsePatch(patch string) ([]int, []int, error) {
lines := strings.Split(patch, "\n")
hunkStarts := []int{}
stageableLines := []int{}
pastHeader := false
for index, line := range lines {
if strings.HasPrefix(line, "@@") {
pastHeader = true
hunkStarts = append(hunkStarts, index)
}
if pastHeader && (strings.HasPrefix(line, "-") || strings.HasPrefix(line, "+")) {
stageableLines = append(stageableLines, index)
}
}
p.Log.WithField("staging", "staging").Info(stageableLines)
return hunkStarts, stageableLines, nil
}

View File

@@ -0,0 +1,65 @@
package git
import (
"io/ioutil"
"testing"
"github.com/stretchr/testify/assert"
)
func newDummyPatchParser() *PatchParser {
return &PatchParser{
Log: newDummyLog(),
}
}
func TestParsePatch(t *testing.T) {
type scenario struct {
testName string
patchFilename string
shouldError bool
expectedStageableLines []int
expectedHunkStarts []int
}
scenarios := []scenario{
{
"Diff with one hunk",
"testdata/testPatchBefore.diff",
false,
[]int{8, 9, 10, 11},
[]int{4},
},
{
"Diff with two hunks",
"testdata/testPatchBefore2.diff",
false,
[]int{8, 9, 10, 11, 12, 13, 20, 21, 22, 23, 24, 25, 26, 27, 28, 33, 34, 35, 36, 37, 45, 46, 47, 48, 49, 50, 51, 52, 53},
[]int{4, 41},
},
{
"Unstaged file",
"testdata/addedFile.diff",
false,
[]int{6},
[]int{5},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
p := newDummyPatchParser()
beforePatch, err := ioutil.ReadFile(s.patchFilename)
if err != nil {
panic("Cannot open file at " + s.patchFilename)
}
hunkStarts, stageableLines, err := p.ParsePatch(string(beforePatch))
if s.shouldError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, s.expectedStageableLines, stageableLines)
assert.Equal(t, s.expectedHunkStarts, hunkStarts)
}
})
}
}

7
pkg/git/testdata/addedFile.diff vendored Normal file
View File

@@ -0,0 +1,7 @@
diff --git a/blah b/blah
new file mode 100644
index 0000000..907b308
--- /dev/null
+++ b/blah
@@ -0,0 +1 @@
+blah

13
pkg/git/testdata/testPatchAfter1.diff vendored Normal file
View File

@@ -0,0 +1,13 @@
diff --git a/pkg/git/branch_list_builder.go b/pkg/git/branch_list_builder.go
index 60ec4e0..db4485d 100644
--- a/pkg/git/branch_list_builder.go
+++ b/pkg/git/branch_list_builder.go
@@ -14,8 +14,7 @@ import (
// context:
// we want to only show 'safe' branches (ones that haven't e.g. been deleted)
-// which `git branch -a` gives us, but we also want the recency data that
// git reflog gives us.
// So we get the HEAD, then append get the reflog branches that intersect with
// our safe branches, then add the remaining safe branches, ensuring uniqueness
// along the way

14
pkg/git/testdata/testPatchAfter2.diff vendored Normal file
View File

@@ -0,0 +1,14 @@
diff --git a/pkg/git/branch_list_builder.go b/pkg/git/branch_list_builder.go
index 60ec4e0..db4485d 100644
--- a/pkg/git/branch_list_builder.go
+++ b/pkg/git/branch_list_builder.go
@@ -14,8 +14,9 @@ import (
// context:
// we want to only show 'safe' branches (ones that haven't e.g. been deleted)
// which `git branch -a` gives us, but we also want the recency data that
// git reflog gives us.
+// test 2 - if I remove this, I decrement the end counter
// So we get the HEAD, then append get the reflog branches that intersect with
// our safe branches, then add the remaining safe branches, ensuring uniqueness
// along the way

25
pkg/git/testdata/testPatchAfter3.diff vendored Normal file
View File

@@ -0,0 +1,25 @@
diff --git a/pkg/git/patch_modifier.go b/pkg/git/patch_modifier.go
index a8fc600..6d8f7d7 100644
--- a/pkg/git/patch_modifier.go
+++ b/pkg/git/patch_modifier.go
@@ -36,18 +36,19 @@ func (p *PatchModifier) ModifyPatchForHunk(patch string, hunkStarts []int, curre
hunkEnd = hunkStarts[nextHunkStartIndex]
}
headerLength := 4
output := strings.Join(lines[0:headerLength], "\n") + "\n"
output += strings.Join(lines[hunkStart:hunkEnd], "\n") + "\n"
return output, nil
}
+func getHeaderLength(patchLines []string) (int, error) {
// ModifyPatchForLine takes the original patch, which may contain several hunks,
// and the line number of the line we want to stage
func (p *PatchModifier) ModifyPatchForLine(patch string, lineNumber int) (string, error) {
lines := strings.Split(patch, "\n")
headerLength := 4
output := strings.Join(lines[0:headerLength], "\n") + "\n"
hunkStart, err := p.getHunkStart(lines, lineNumber)

19
pkg/git/testdata/testPatchAfter4.diff vendored Normal file
View File

@@ -0,0 +1,19 @@
diff --git a/pkg/git/patch_modifier.go b/pkg/git/patch_modifier.go
index a8fc600..6d8f7d7 100644
--- a/pkg/git/patch_modifier.go
+++ b/pkg/git/patch_modifier.go
@@ -124,13 +140,14 @@ func (p *PatchModifier) getModifiedHunk(patchLines []string, hunkStart int, line
// @@ -14,8 +14,9 @@ import (
func (p *PatchModifier) updatedHeader(currentHeader string, lineChanges int) (string, error) {
// current counter is the number after the second comma
re := regexp.MustCompile(`^[^,]+,[^,]+,(\d+)`)
matches := re.FindStringSubmatch(currentHeader)
if len(matches) < 2 {
re = regexp.MustCompile(`^[^,]+,[^+]+\+(\d+)`)
matches = re.FindStringSubmatch(currentHeader)
}
prevLengthString := matches[1]
+ prevLengthString := re.FindStringSubmatch(currentHeader)[1]
prevLength, err := strconv.Atoi(prevLengthString)
if err != nil {

15
pkg/git/testdata/testPatchBefore.diff vendored Normal file
View File

@@ -0,0 +1,15 @@
diff --git a/pkg/git/branch_list_builder.go b/pkg/git/branch_list_builder.go
index 60ec4e0..db4485d 100644
--- a/pkg/git/branch_list_builder.go
+++ b/pkg/git/branch_list_builder.go
@@ -14,8 +14,8 @@ import (
// context:
// we want to only show 'safe' branches (ones that haven't e.g. been deleted)
-// which `git branch -a` gives us, but we also want the recency data that
-// git reflog gives us.
+// test 2 - if I remove this, I decrement the end counter
+// test
// So we get the HEAD, then append get the reflog branches that intersect with
// our safe branches, then add the remaining safe branches, ensuring uniqueness
// along the way

57
pkg/git/testdata/testPatchBefore2.diff vendored Normal file
View File

@@ -0,0 +1,57 @@
diff --git a/pkg/git/patch_modifier.go b/pkg/git/patch_modifier.go
index a8fc600..6d8f7d7 100644
--- a/pkg/git/patch_modifier.go
+++ b/pkg/git/patch_modifier.go
@@ -36,18 +36,34 @@ func (p *PatchModifier) ModifyPatchForHunk(patch string, hunkStarts []int, curre
hunkEnd = hunkStarts[nextHunkStartIndex]
}
- headerLength := 4
+ headerLength, err := getHeaderLength(lines)
+ if err != nil {
+ return "", err
+ }
+
output := strings.Join(lines[0:headerLength], "\n") + "\n"
output += strings.Join(lines[hunkStart:hunkEnd], "\n") + "\n"
return output, nil
}
+func getHeaderLength(patchLines []string) (int, error) {
+ for index, line := range patchLines {
+ if strings.HasPrefix(line, "@@") {
+ return index, nil
+ }
+ }
+ return 0, errors.New("Could not find any hunks in this patch")
+}
+
// ModifyPatchForLine takes the original patch, which may contain several hunks,
// and the line number of the line we want to stage
func (p *PatchModifier) ModifyPatchForLine(patch string, lineNumber int) (string, error) {
lines := strings.Split(patch, "\n")
- headerLength := 4
+ headerLength, err := getHeaderLength(lines)
+ if err != nil {
+ return "", err
+ }
output := strings.Join(lines[0:headerLength], "\n") + "\n"
hunkStart, err := p.getHunkStart(lines, lineNumber)
@@ -124,13 +140,8 @@ func (p *PatchModifier) getModifiedHunk(patchLines []string, hunkStart int, line
// @@ -14,8 +14,9 @@ import (
func (p *PatchModifier) updatedHeader(currentHeader string, lineChanges int) (string, error) {
// current counter is the number after the second comma
- re := regexp.MustCompile(`^[^,]+,[^,]+,(\d+)`)
- matches := re.FindStringSubmatch(currentHeader)
- if len(matches) < 2 {
- re = regexp.MustCompile(`^[^,]+,[^+]+\+(\d+)`)
- matches = re.FindStringSubmatch(currentHeader)
- }
- prevLengthString := matches[1]
+ re := regexp.MustCompile(`(\d+) @@`)
+ prevLengthString := re.FindStringSubmatch(currentHeader)[1]
prevLength, err := strconv.Atoi(prevLengthString)
if err != nil {

View File

@@ -7,23 +7,136 @@ import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/git"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// list panel functions
func (gui *Gui) getSelectedBranch() *commands.Branch {
selectedLine := gui.State.Panels.Branches.SelectedLine
if selectedLine == -1 {
return nil
}
return gui.State.Branches[selectedLine]
}
// may want to standardise how these select methods work
func (gui *Gui) handleBranchSelect(g *gocui.Gui, v *gocui.View) error {
// This really shouldn't happen: there should always be a master branch
if len(gui.State.Branches) == 0 {
return gui.renderString(g, "main", gui.Tr.SLocalize("NoBranchesThisRepo"))
}
branch := gui.getSelectedBranch()
if err := gui.focusPoint(0, gui.State.Panels.Branches.SelectedLine, v); err != nil {
return err
}
go func() {
_ = gui.RenderSelectedBranchUpstreamDifferences()
}()
go func() {
graph, err := gui.GitCommand.GetBranchGraph(branch.Name)
if err != nil && strings.HasPrefix(graph, "fatal: ambiguous argument") {
graph = gui.Tr.SLocalize("NoTrackingThisBranch")
}
_ = gui.renderString(g, "main", graph)
}()
return nil
}
func (gui *Gui) RenderSelectedBranchUpstreamDifferences() error {
// here we tell the selected branch that it is selected.
// this is necessary for showing stats on a branch that is selected, because
// the displaystring function doesn't have access to gui state to tell if it's selected
for i, branch := range gui.State.Branches {
branch.Selected = i == gui.State.Panels.Branches.SelectedLine
}
branch := gui.getSelectedBranch()
branch.Pushables, branch.Pullables = gui.GitCommand.GetBranchUpstreamDifferenceCount(branch.Name)
return gui.renderListPanel(gui.getBranchesView(gui.g), gui.State.Branches)
}
// gui.refreshStatus is called at the end of this because that's when we can
// be sure there is a state.Branches array to pick the current branch from
func (gui *Gui) refreshBranches(g *gocui.Gui) error {
g.Update(func(g *gocui.Gui) error {
builder, err := git.NewBranchListBuilder(gui.Log, gui.GitCommand)
if err != nil {
return err
}
gui.State.Branches = builder.Build()
gui.refreshSelectedLine(&gui.State.Panels.Branches.SelectedLine, len(gui.State.Branches))
if err := gui.resetOrigin(gui.getBranchesView(gui.g)); err != nil {
return err
}
if err := gui.RenderSelectedBranchUpstreamDifferences(); err != nil {
return err
}
return gui.refreshStatus(g)
})
return nil
}
func (gui *Gui) handleBranchesNextLine(g *gocui.Gui, v *gocui.View) error {
panelState := gui.State.Panels.Branches
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Branches), false)
return gui.handleBranchSelect(gui.g, v)
}
func (gui *Gui) handleBranchesPrevLine(g *gocui.Gui, v *gocui.View) error {
panelState := gui.State.Panels.Branches
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Branches), true)
return gui.handleBranchSelect(gui.g, v)
}
// specific functions
func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error {
index := gui.getItemPosition(gui.getBranchesView(g))
if index == 0 {
if gui.State.Panels.Branches.SelectedLine == -1 {
return nil
}
if gui.State.Panels.Branches.SelectedLine == 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("AlreadyCheckedOutBranch"))
}
branch := gui.getSelectedBranch(gui.getBranchesView(g))
branch := gui.getSelectedBranch()
if err := gui.GitCommand.Checkout(branch.Name, false); err != nil {
gui.createErrorPanel(g, err.Error())
if err := gui.createErrorPanel(g, err.Error()); err != nil {
return err
}
} else {
gui.State.Panels.Branches.SelectedLine = 0
}
return gui.refreshSidePanels(g)
}
func (gui *Gui) handleCreatePullRequestPress(g *gocui.Gui, v *gocui.View) error {
pullRequest := commands.NewPullRequest(gui.GitCommand)
branch := gui.getSelectedBranch()
if err := pullRequest.Create(branch); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return nil
}
func (gui *Gui) handleGitFetch(g *gocui.Gui, v *gocui.View) error {
if err := gui.createMessagePanel(g, v, "", gui.Tr.SLocalize("FetchWait")); err != nil {
return err
}
go func() {
unamePassOpend, err := gui.fetch(g, v, true)
gui.HandleCredentialsPopup(g, unamePassOpend, err)
}()
return nil
}
func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
branch := gui.getSelectedBranch(v)
branch := gui.getSelectedBranch()
message := gui.Tr.SLocalize("SureForceCheckout")
title := gui.Tr.SLocalize("ForceCheckoutBranch")
return gui.createConfirmationPanel(g, v, title, message, func(g *gocui.Gui, v *gocui.View) error {
@@ -71,27 +184,38 @@ func (gui *Gui) handleForceDeleteBranch(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) deleteBranch(g *gocui.Gui, v *gocui.View, force bool) error {
selectedBranch := gui.getSelectedBranch()
if selectedBranch == nil {
return nil
}
checkedOutBranch := gui.State.Branches[0]
selectedBranch := gui.getSelectedBranch(v)
if checkedOutBranch.Name == selectedBranch.Name {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantDeleteCheckOutBranch"))
}
return gui.deleteNamedBranch(g, v, selectedBranch, force)
}
func (gui *Gui) deleteNamedBranch(g *gocui.Gui, v *gocui.View, selectedBranch *commands.Branch, force bool) error {
title := gui.Tr.SLocalize("DeleteBranch")
var messageId string
var messageID string
if force {
messageId = "ForceDeleteBranchMessage"
messageID = "ForceDeleteBranchMessage"
} else {
messageId = "DeleteBranchMessage"
messageID = "DeleteBranchMessage"
}
message := gui.Tr.TemplateLocalize(
messageId,
messageID,
Teml{
"selectedBranchName": selectedBranch.Name,
},
)
return gui.createConfirmationPanel(g, v, title, message, func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.DeleteBranch(selectedBranch.Name, force); err != nil {
return gui.createErrorPanel(g, err.Error())
errMessage := err.Error()
if !force && strings.Contains(errMessage, "is not fully merged") {
return gui.deleteNamedBranch(g, v, selectedBranch, true)
}
return gui.createErrorPanel(g, errMessage)
}
return gui.refreshSidePanels(g)
}, nil)
@@ -99,7 +223,7 @@ func (gui *Gui) deleteBranch(g *gocui.Gui, v *gocui.View, force bool) error {
func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error {
checkedOutBranch := gui.State.Branches[0]
selectedBranch := gui.getSelectedBranch(v)
selectedBranch := gui.getSelectedBranch()
defer gui.refreshSidePanels(g)
if checkedOutBranch.Name == selectedBranch.Name {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantMergeBranchIntoItself"))
@@ -110,59 +234,36 @@ func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error {
return nil
}
func (gui *Gui) getSelectedBranch(v *gocui.View) *commands.Branch {
lineNumber := gui.getItemPosition(v)
return gui.State.Branches[lineNumber]
}
func (gui *Gui) renderBranchesOptions(g *gocui.Gui) error {
return gui.renderGlobalOptions(g)
}
// may want to standardise how these select methods work
func (gui *Gui) handleBranchSelect(g *gocui.Gui, v *gocui.View) error {
if err := gui.renderBranchesOptions(g); err != nil {
return err
func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
branch := gui.getSelectedBranch()
if branch == nil {
return nil
}
// This really shouldn't happen: there should always be a master branch
if len(gui.State.Branches) == 0 {
return gui.renderString(g, "main", gui.Tr.SLocalize("NoBranchesThisRepo"))
if branch.Pushables == "" {
return nil
}
if branch.Pushables == "?" {
return gui.createErrorPanel(gui.g, "Cannot fast-forward a branch with no upstream")
}
if branch.Pushables != "0" {
return gui.createErrorPanel(gui.g, "Cannot fast-forward a branch with commits to push")
}
upstream := "origin" // hardcoding for now
message := gui.Tr.TemplateLocalize(
"Fetching",
Teml{
"from": fmt.Sprintf("%s/%s", upstream, branch.Name),
"to": branch.Name,
},
)
go func() {
branch := gui.getSelectedBranch(v)
diff, err := gui.GitCommand.GetBranchGraph(branch.Name)
if err != nil && strings.HasPrefix(diff, "fatal: ambiguous argument") {
diff = gui.Tr.SLocalize("NoTrackingThisBranch")
_ = gui.createMessagePanel(gui.g, v, "", message)
if err := gui.GitCommand.FastForward(branch.Name); err != nil {
_ = gui.createErrorPanel(gui.g, err.Error())
} else {
_ = gui.closeConfirmationPrompt(gui.g)
_ = gui.RenderSelectedBranchUpstreamDifferences()
}
gui.renderString(g, "main", diff)
}()
return nil
}
// gui.refreshStatus is called at the end of this because that's when we can
// be sure there is a state.Branches array to pick the current branch from
func (gui *Gui) refreshBranches(g *gocui.Gui) error {
g.Update(func(g *gocui.Gui) error {
v, err := g.View("branches")
if err != nil {
panic(err)
}
builder, err := git.NewBranchListBuilder(gui.Log, gui.GitCommand)
if err != nil {
return err
}
gui.State.Branches = builder.Build()
v.Clear()
list, err := utils.RenderList(gui.State.Branches)
if err != nil {
return err
}
fmt.Fprint(v, list)
gui.resetOrigin(v)
return gui.refreshStatus(g)
})
return nil
}

View File

@@ -12,7 +12,7 @@ func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
if message == "" {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CommitWithoutMessageErr"))
}
sub, err := gui.GitCommand.Commit(message)
sub, err := gui.GitCommand.Commit(message, false)
if err != nil {
// TODO need to find a way to send through this error
if err != gui.Errors.ErrSubProcess {
@@ -23,12 +23,12 @@ func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
gui.SubProcess = sub
return gui.Errors.ErrSubProcess
}
gui.refreshFiles(g)
v.Clear()
v.SetCursor(0, 0)
g.SetViewOnBottom("commitMessage")
gui.switchFocus(g, v, gui.getFilesView(g))
return gui.refreshCommits(g)
_ = v.SetCursor(0, 0)
_ = v.SetOrigin(0, 0)
_, _ = g.SetViewOnBottom("commitMessage")
_ = gui.switchFocus(g, v, gui.getFilesView(g))
return gui.refreshSidePanels(g)
}
func (gui *Gui) handleCommitClose(g *gocui.Gui, v *gocui.View) error {
@@ -37,6 +37,10 @@ func (gui *Gui) handleCommitClose(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleCommitFocused(g *gocui.Gui, v *gocui.View) error {
if _, err := g.SetViewOnTop("commitMessage"); err != nil {
return err
}
message := gui.Tr.TemplateLocalize(
"CloseConfirm",
Teml{
@@ -78,6 +82,7 @@ func (gui *Gui) getBufferLength(view *gocui.View) string {
return " " + strconv.Itoa(strings.Count(view.Buffer(), "")-1) + " "
}
// RenderCommitLength is a function.
func (gui *Gui) RenderCommitLength() {
if !gui.Config.GetUserConfig().GetBool("gui.commitLength.show") {
return

View File

@@ -9,23 +9,54 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
// list panel functions
func (gui *Gui) getSelectedCommit(g *gocui.Gui) *commands.Commit {
selectedLine := gui.State.Panels.Commits.SelectedLine
if selectedLine == -1 {
return nil
}
return gui.State.Commits[selectedLine]
}
func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedCommit(g)
if commit == nil {
return gui.renderString(g, "main", gui.Tr.SLocalize("NoCommitsThisBranch"))
}
if err := gui.focusPoint(0, gui.State.Panels.Commits.SelectedLine, v); err != nil {
return err
}
commitText, err := gui.GitCommand.Show(commit.Sha)
if err != nil {
return err
}
return gui.renderString(g, "main", commitText)
}
func (gui *Gui) refreshCommits(g *gocui.Gui) error {
g.Update(func(*gocui.Gui) error {
gui.State.Commits = gui.GitCommand.GetCommits()
v, err := g.View("commits")
commits, err := gui.GitCommand.GetCommits()
if err != nil {
panic(err)
return err
}
gui.State.Commits = commits
gui.refreshSelectedLine(&gui.State.Panels.Commits.SelectedLine, len(gui.State.Commits))
v.Clear()
list, err := utils.RenderList(gui.State.Commits)
if err != nil {
return err
}
v := gui.getCommitsView(gui.g)
v.Clear()
fmt.Fprint(v, list)
gui.refreshStatus(g)
if g.CurrentView().Name() == "commits" {
if v == g.CurrentView() {
gui.handleCommitSelect(g, v)
}
return nil
@@ -33,11 +64,27 @@ func (gui *Gui) refreshCommits(g *gocui.Gui) error {
return nil
}
func (gui *Gui) handleCommitsNextLine(g *gocui.Gui, v *gocui.View) error {
panelState := gui.State.Panels.Commits
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Commits), false)
return gui.handleCommitSelect(gui.g, v)
}
func (gui *Gui) handleCommitsPrevLine(g *gocui.Gui, v *gocui.View) error {
panelState := gui.State.Panels.Commits
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Commits), true)
return gui.handleCommitSelect(gui.g, v)
}
// specific functions
func (gui *Gui) handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error {
return gui.createConfirmationPanel(g, commitView, gui.Tr.SLocalize("ResetToCommit"), gui.Tr.SLocalize("SureResetThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
commit, err := gui.getSelectedCommit(g)
if err != nil {
panic(err)
commit := gui.getSelectedCommit(g)
if commit == nil {
panic(errors.New(gui.Tr.SLocalize("NoCommitsThisBranch")))
}
if err := gui.GitCommand.ResetToCommit(commit.Sha); err != nil {
return gui.createErrorPanel(g, err.Error())
@@ -49,39 +96,21 @@ func (gui *Gui) handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error
panic(err)
}
gui.resetOrigin(commitView)
return gui.handleCommitSelect(g, nil)
gui.State.Panels.Commits.SelectedLine = 0
return gui.handleCommitSelect(g, commitView)
}, nil)
}
func (gui *Gui) renderCommitsOptions(g *gocui.Gui) error {
return gui.renderGlobalOptions(g)
}
func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
if err := gui.renderCommitsOptions(g); err != nil {
return err
}
commit, err := gui.getSelectedCommit(g)
if err != nil {
if err.Error() != gui.Tr.SLocalize("NoCommitsThisBranch") {
return err
}
return gui.renderString(g, "main", gui.Tr.SLocalize("NoCommitsThisBranch"))
}
commitText := gui.GitCommand.Show(commit.Sha)
return gui.renderString(g, "main", commitText)
}
func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
if gui.getItemPosition(v) != 0 {
if gui.State.Panels.Commits.SelectedLine != 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlySquashTopmostCommit"))
}
if len(gui.State.Commits) == 1 {
if len(gui.State.Commits) <= 1 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash"))
}
commit, err := gui.getSelectedCommit(g)
if err != nil {
return err
commit := gui.getSelectedCommit(g)
if commit == nil {
return errors.New(gui.Tr.SLocalize("NoCommitsThisBranch"))
}
if err := gui.GitCommand.SquashPreviousTwoCommits(commit.Name); err != nil {
return gui.createErrorPanel(g, err.Error())
@@ -104,16 +133,16 @@ func (gui *Gui) anyUnStagedChanges(files []*commands.File) bool {
}
func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
if len(gui.State.Commits) == 1 {
if len(gui.State.Commits) <= 1 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash"))
}
if gui.anyUnStagedChanges(gui.State.Files) {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantFixupWhileUnstagedChanges"))
}
branch := gui.State.Branches[0]
commit, err := gui.getSelectedCommit(g)
if err != nil {
return err
commit := gui.getSelectedCommit(g)
if commit == nil {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoCommitsThisBranch"))
}
message := gui.Tr.SLocalize("SureFixupThisCommit")
gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("Fixup"), message, func(g *gocui.Gui, v *gocui.View) error {
@@ -129,7 +158,7 @@ func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
if gui.getItemPosition(gui.getCommitsView(g)) != 0 {
if gui.State.Panels.Commits.SelectedLine != 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlyRenameTopCommit"))
}
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("renameCommit"), func(g *gocui.Gui, v *gocui.View) error {
@@ -144,7 +173,7 @@ func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
if gui.getItemPosition(gui.getCommitsView(g)) != 0 {
if gui.State.Panels.Commits.SelectedLine != 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlyRenameTopCommit"))
}
@@ -155,19 +184,3 @@ func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
return nil
}
func (gui *Gui) getSelectedCommit(g *gocui.Gui) (*commands.Commit, error) {
v, err := g.View("commits")
if err != nil {
panic(err)
}
if len(gui.State.Commits) == 0 {
return &commands.Commit{}, errors.New(gui.Tr.SLocalize("NoCommitsThisBranch"))
}
lineNumber := gui.getItemPosition(v)
if lineNumber > len(gui.State.Commits)-1 {
gui.Log.Info(gui.Tr.SLocalize("PotentialErrInGetselectedCommit"), gui.State.Commits, lineNumber)
return gui.State.Commits[len(gui.State.Commits)-1], nil
}
return gui.State.Commits[lineNumber], nil
}

View File

@@ -8,6 +8,7 @@ package gui
import (
"strings"
"time"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
@@ -27,7 +28,7 @@ func (gui *Gui) wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.Vie
func (gui *Gui) closeConfirmationPrompt(g *gocui.Gui) error {
view, err := g.View("confirmation")
if err != nil {
panic(err)
return nil // if it's already been closed we can just return
}
if err := gui.returnFocus(g, view); err != nil {
panic(err)
@@ -36,19 +37,24 @@ func (gui *Gui) closeConfirmationPrompt(g *gocui.Gui) error {
return g.DeleteView("confirmation")
}
func (gui *Gui) getMessageHeight(message string, width int) int {
func (gui *Gui) getMessageHeight(wrap bool, message string, width int) int {
lines := strings.Split(message, "\n")
lineCount := 0
for _, line := range lines {
lineCount += len(line)/width + 1
// if we need to wrap, calculate height to fit content within view's width
if wrap {
for _, line := range lines {
lineCount += len(line)/width + 1
}
} else {
lineCount = len(lines)
}
return lineCount
}
func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, prompt string) (int, int, int, int) {
func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, wrap bool, prompt string) (int, int, int, int) {
width, height := g.Size()
panelWidth := width / 2
panelHeight := gui.getMessageHeight(prompt, panelWidth)
panelHeight := gui.getMessageHeight(wrap, prompt, panelWidth)
return width/2 - panelWidth/2,
height/2 - panelHeight/2 - panelHeight%2 - 1,
width/2 + panelWidth/2,
@@ -66,27 +72,29 @@ func (gui *Gui) createPromptPanel(g *gocui.Gui, currentView *gocui.View, title s
}
func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt string) (*gocui.View, error) {
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, prompt)
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, true, prompt)
confirmationView, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0)
if err != nil {
if err != gocui.ErrUnknownView {
return nil, err
}
confirmationView.Title = title
confirmationView.Wrap = true
confirmationView.FgColor = gocui.ColorWhite
}
confirmationView.Clear()
if err := gui.switchFocus(gui.g, currentView, confirmationView); err != nil {
return nil, err
}
gui.g.Update(func(g *gocui.Gui) error {
confirmationView.Clear()
return gui.switchFocus(gui.g, currentView, confirmationView)
})
return confirmationView, nil
}
func (gui *Gui) onNewPopupPanel() {
gui.g.SetViewOnBottom("commitMessage")
_, _ = gui.g.SetViewOnBottom("commitMessage")
_, _ = gui.g.SetViewOnBottom("credentials")
}
// it is very important that within this function we never include the original prompt in any error messages, because it may contain e.g. a user password
func (gui *Gui) createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
gui.onNewPopupPanel()
g.Update(func(g *gocui.Gui) error {
@@ -136,10 +144,27 @@ func (gui *Gui) createMessagePanel(g *gocui.Gui, currentView *gocui.View, title,
return gui.createConfirmationPanel(g, currentView, title, prompt, nil, nil)
}
func (gui *Gui) createErrorPanel(g *gocui.Gui, message string) error {
gui.Log.Error(message)
currentView := g.CurrentView()
// createSpecificErrorPanel allows you to create an error popup, specifying the
// view to be focused when the user closes the popup, and a boolean specifying
// whether we will log the error. If the message may include a user password,
// this function is to be used over the more generic createErrorPanel, with
// willLog set to false
func (gui *Gui) createSpecificErrorPanel(message string, nextView *gocui.View, willLog bool) error {
if willLog {
go func() {
// when reporting is switched on this log call sometimes introduces
// a delay on the error panel popping up. Here I'm adding a second wait
// so that the error is logged while the user is reading the error message
time.Sleep(time.Second)
gui.Log.Error(message)
}()
}
colorFunction := color.New(color.FgRed).SprintFunc()
coloredMessage := colorFunction(strings.TrimSpace(message))
return gui.createConfirmationPanel(g, currentView, gui.Tr.SLocalize("Error"), coloredMessage, nil, nil)
return gui.createConfirmationPanel(gui.g, nextView, gui.Tr.SLocalize("Error"), coloredMessage, nil, nil)
}
func (gui *Gui) createErrorPanel(g *gocui.Gui, message string) error {
return gui.createSpecificErrorPanel(message, g.CurrentView(), true)
}

View File

@@ -0,0 +1,104 @@
package gui
import (
"strings"
"github.com/jesseduffield/gocui"
)
type credentials chan string
// waitForPassUname wait for a username or password input from the credentials popup
func (gui *Gui) waitForPassUname(g *gocui.Gui, currentView *gocui.View, passOrUname string) string {
gui.credentials = make(chan string)
g.Update(func(g *gocui.Gui) error {
credentialsView, _ := g.View("credentials")
if passOrUname == "username" {
credentialsView.Title = gui.Tr.SLocalize("CredentialsUsername")
credentialsView.Mask = 0
} else {
credentialsView.Title = gui.Tr.SLocalize("CredentialsPassword")
credentialsView.Mask = '*'
}
err := gui.switchFocus(g, currentView, credentialsView)
if err != nil {
return err
}
gui.RenderCommitLength()
return nil
})
// wait for username/passwords input
userInput := <-gui.credentials
return userInput + "\n"
}
func (gui *Gui) handleSubmitCredential(g *gocui.Gui, v *gocui.View) error {
message := gui.trimmedContent(v)
gui.credentials <- message
err := gui.refreshFiles(g)
if err != nil {
return err
}
v.Clear()
err = v.SetCursor(0, 0)
if err != nil {
return err
}
_, err = g.SetViewOnBottom("credentials")
if err != nil {
return err
}
nextView, err := gui.g.View("confirmation")
if err != nil {
nextView = gui.getFilesView(g)
}
err = gui.switchFocus(g, nil, nextView)
if err != nil {
return err
}
return gui.refreshCommits(g)
}
func (gui *Gui) handleCloseCredentialsView(g *gocui.Gui, v *gocui.View) error {
_, err := g.SetViewOnBottom("credentials")
if err != nil {
return err
}
gui.credentials <- ""
return gui.switchFocus(g, nil, gui.getFilesView(g))
}
func (gui *Gui) handleCredentialsViewFocused(g *gocui.Gui, v *gocui.View) error {
if _, err := g.SetViewOnTop("credentials"); err != nil {
return err
}
message := gui.Tr.TemplateLocalize(
"CloseConfirm",
Teml{
"keyBindClose": "esc",
"keyBindConfirm": "enter",
},
)
return gui.renderString(g, "options", message)
}
// HandleCredentialsPopup handles the views after executing a command that might ask for credentials
func (gui *Gui) HandleCredentialsPopup(g *gocui.Gui, popupOpened bool, cmdErr error) {
if popupOpened {
_, _ = gui.g.SetViewOnBottom("credentials")
}
if cmdErr != nil {
errMessage := cmdErr.Error()
if strings.Contains(errMessage, "Invalid username or password") {
errMessage = gui.Tr.SLocalize("PassUnameWrong")
}
// we are not logging this error because it may contain a password
_ = gui.createSpecificErrorPanel(errMessage, gui.getFilesView(gui.g), false)
} else {
_ = gui.closeConfirmationPrompt(g)
_ = gui.refreshSidePanels(g)
}
}

View File

@@ -15,6 +15,89 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
// list panel functions
func (gui *Gui) getSelectedFile(g *gocui.Gui) (*commands.File, error) {
selectedLine := gui.State.Panels.Files.SelectedLine
if selectedLine == -1 {
return &commands.File{}, gui.Errors.ErrNoFiles
}
return gui.State.Files[selectedLine], nil
}
func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View, alreadySelected bool) error {
file, err := gui.getSelectedFile(g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
}
return gui.renderString(g, "main", gui.Tr.SLocalize("NoChangedFiles"))
}
if file.HasMergeConflicts {
return gui.refreshMergePanel(g)
}
if err := gui.focusPoint(0, gui.State.Panels.Files.SelectedLine, v); err != nil {
return err
}
content := gui.GitCommand.Diff(file, false)
if alreadySelected {
g.Update(func(*gocui.Gui) error {
return gui.setViewContent(gui.g, gui.getMainView(gui.g), content)
})
return nil
}
return gui.renderString(g, "main", content)
}
func (gui *Gui) refreshFiles(g *gocui.Gui) error {
selectedFile, _ := gui.getSelectedFile(gui.g)
filesView, err := g.View("files")
if err != nil {
return err
}
gui.refreshStateFiles()
gui.g.Update(func(g *gocui.Gui) error {
filesView.Clear()
list, err := utils.RenderList(gui.State.Files)
if err != nil {
return err
}
fmt.Fprint(filesView, list)
if filesView == g.CurrentView() {
newSelectedFile, _ := gui.getSelectedFile(gui.g)
alreadySelected := newSelectedFile.Name == selectedFile.Name
return gui.handleFileSelect(g, filesView, alreadySelected)
}
return nil
})
return nil
}
func (gui *Gui) handleFilesNextLine(g *gocui.Gui, v *gocui.View) error {
panelState := gui.State.Panels.Files
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Files), false)
return gui.handleFileSelect(gui.g, v, false)
}
func (gui *Gui) handleFilesPrevLine(g *gocui.Gui, v *gocui.View) error {
panelState := gui.State.Panels.Files
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Files), true)
return gui.handleFileSelect(gui.g, v, false)
}
// specific functions
func (gui *Gui) stagedFiles() []*commands.File {
files := gui.State.Files
result := make([]*commands.File, 0)
@@ -45,6 +128,28 @@ func (gui *Gui) stageSelectedFile(g *gocui.Gui) error {
return gui.GitCommand.StageFile(file.Name)
}
func (gui *Gui) handleSwitchToStagingPanel(g *gocui.Gui, v *gocui.View) error {
stagingView, err := g.View("staging")
if err != nil {
return err
}
file, err := gui.getSelectedFile(g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
}
return nil
}
if !file.HasUnstagedChanges {
gui.Log.WithField("staging", "staging").Info("making error panel")
return gui.createErrorPanel(g, gui.Tr.SLocalize("FileStagingRequirements"))
}
if err := gui.switchFocus(g, v, stagingView); err != nil {
return err
}
return gui.refreshStagingPanel()
}
func (gui *Gui) handleFilePress(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
@@ -68,7 +173,7 @@ func (gui *Gui) handleFilePress(g *gocui.Gui, v *gocui.View) error {
return err
}
return gui.handleFileSelect(g, v)
return gui.handleFileSelect(g, v, true)
}
func (gui *Gui) allFilesStaged() bool {
@@ -91,11 +196,7 @@ func (gui *Gui) handleStageAll(g *gocui.Gui, v *gocui.View) error {
_ = gui.createErrorPanel(g, err.Error())
}
if err := gui.refreshFiles(g); err != nil {
return err
}
return gui.handleFileSelect(g, v)
return gui.refreshFiles(g)
}
func (gui *Gui) handleAddPatch(g *gocui.Gui, v *gocui.View) error {
@@ -117,18 +218,6 @@ func (gui *Gui) handleAddPatch(g *gocui.Gui, v *gocui.View) error {
return gui.Errors.ErrSubProcess
}
func (gui *Gui) getSelectedFile(g *gocui.Gui) (*commands.File, error) {
if len(gui.State.Files) == 0 {
return &commands.File{}, gui.Errors.ErrNoFiles
}
filesView, err := g.View("files")
if err != nil {
panic(err)
}
lineNumber := gui.getItemPosition(filesView)
return gui.State.Files[lineNumber], nil
}
func (gui *Gui) handleFileRemove(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
@@ -172,31 +261,6 @@ func (gui *Gui) handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
return gui.refreshFiles(g)
}
func (gui *Gui) renderfilesOptions(g *gocui.Gui, file *commands.File) error {
return gui.renderGlobalOptions(g)
}
func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
}
gui.renderString(g, "main", gui.Tr.SLocalize("NoChangedFiles"))
return gui.renderfilesOptions(g, nil)
}
if err := gui.renderfilesOptions(g, file); err != nil {
return err
}
var content string
if file.HasMergeConflicts {
return gui.refreshMergePanel(g)
}
content = gui.GitCommand.Diff(file)
return gui.renderString(g, "main", content)
}
func (gui *Gui) handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
if len(gui.stagedFiles()) == 0 && !gui.State.HasMergeConflicts {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit"))
@@ -211,6 +275,28 @@ func (gui *Gui) handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
return nil
}
func (gui *Gui) handleAmendCommitPress(g *gocui.Gui, filesView *gocui.View) error {
if len(gui.stagedFiles()) == 0 && !gui.State.HasMergeConflicts {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit"))
}
title := strings.Title(gui.Tr.SLocalize("AmendLastCommit"))
question := gui.Tr.SLocalize("SureToAmend")
if len(gui.State.Commits) == 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoCommitToAmend"))
}
return gui.createConfirmationPanel(g, filesView, title, question, func(g *gocui.Gui, v *gocui.View) error {
lastCommitMsg := gui.State.Commits[0].Name
_, err := gui.GitCommand.Commit(lastCommitMsg, true)
if err != nil {
return gui.createErrorPanel(g, err.Error())
}
return gui.refreshSidePanels(g)
}, nil)
}
// handleCommitEditorPress - handle when the user wants to commit changes via
// their editor rather than via the popup panel
func (gui *Gui) handleCommitEditorPress(g *gocui.Gui, filesView *gocui.View) error {
@@ -266,6 +352,7 @@ func (gui *Gui) refreshStateFiles() {
// get files to stage
files := gui.GitCommand.GetStatusFiles()
gui.State.Files = gui.GitCommand.MergeStatusFiles(gui.State.Files, files)
gui.refreshSelectedLine(&gui.State.Panels.Files.SelectedLine, len(gui.State.Files))
gui.updateHasMergeConflictStatus()
}
@@ -297,67 +384,45 @@ func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) {
return cat, nil
}
func (gui *Gui) refreshFiles(g *gocui.Gui) error {
filesView, err := g.View("files")
if err != nil {
return err
}
gui.refreshStateFiles()
filesView.Clear()
list, err := utils.RenderList(gui.State.Files)
if err != nil {
return err
}
fmt.Fprint(filesView, list)
gui.correctCursor(filesView)
if filesView == g.CurrentView() {
gui.handleFileSelect(g, filesView)
}
return nil
}
func (gui *Gui) pullFiles(g *gocui.Gui, v *gocui.View) error {
gui.createMessagePanel(g, v, "", gui.Tr.SLocalize("PullWait"))
if err := gui.createMessagePanel(gui.g, v, "", gui.Tr.SLocalize("PullWait")); err != nil {
return err
}
go func() {
if err := gui.GitCommand.Pull(); err != nil {
gui.createErrorPanel(g, err.Error())
} else {
gui.closeConfirmationPrompt(g)
gui.refreshCommits(g)
gui.refreshStatus(g)
}
gui.refreshFiles(g)
unamePassOpend := false
err := gui.GitCommand.Pull(func(passOrUname string) string {
unamePassOpend = true
return gui.waitForPassUname(g, v, passOrUname)
})
gui.HandleCredentialsPopup(g, unamePassOpend, err)
}()
return nil
}
func (gui *Gui) pushWithForceFlag(currentView *gocui.View, force bool) error {
if err := gui.createMessagePanel(gui.g, currentView, "", gui.Tr.SLocalize("PushWait")); err != nil {
func (gui *Gui) pushWithForceFlag(g *gocui.Gui, v *gocui.View, force bool) error {
if err := gui.createMessagePanel(g, v, "", gui.Tr.SLocalize("PushWait")); err != nil {
return err
}
go func() {
unamePassOpend := false
branchName := gui.State.Branches[0].Name
if err := gui.GitCommand.Push(branchName, force); err != nil {
_ = gui.createErrorPanel(gui.g, err.Error())
} else {
_ = gui.closeConfirmationPrompt(gui.g)
_ = gui.refreshCommits(gui.g)
_ = gui.refreshStatus(gui.g)
}
err := gui.GitCommand.Push(branchName, force, func(passOrUname string) string {
unamePassOpend = true
return gui.waitForPassUname(g, v, passOrUname)
})
gui.HandleCredentialsPopup(g, unamePassOpend, err)
}()
return nil
}
func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error {
// if we have pullables we'll ask if the user wants to force push
_, pullables := gui.GitCommand.UpstreamDifferenceCount()
_, pullables := gui.GitCommand.GetCurrentBranchUpstreamDifferenceCount()
if pullables == "?" || pullables == "0" {
return gui.pushWithForceFlag(v, false)
return gui.pushWithForceFlag(g, v, false)
}
err := gui.createConfirmationPanel(g, nil, gui.Tr.SLocalize("ForcePush"), gui.Tr.SLocalize("ForcePushPrompt"), func(g *gocui.Gui, v *gocui.View) error {
return gui.pushWithForceFlag(v, true)
return gui.pushWithForceFlag(g, v, true)
}, nil)
return err
}
@@ -390,9 +455,9 @@ func (gui *Gui) handleAbortMerge(g *gocui.Gui, v *gocui.View) error {
return gui.refreshFiles(g)
}
func (gui *Gui) handleResetHard(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleResetAndClean(g *gocui.Gui, v *gocui.View) error {
return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("ClearFilePanel"), gui.Tr.SLocalize("SureResetHardHead"), func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.ResetHard(); err != nil {
if err := gui.GitCommand.ResetAndClean(); err != nil {
gui.createErrorPanel(g, err.Error())
}
return gui.refreshFiles(g)

View File

@@ -1,6 +1,7 @@
package gui
import (
"sync"
// "io"
// "io/ioutil"
@@ -15,6 +16,7 @@ import (
// "strings"
"github.com/fatih/color"
"github.com/golang-collections/collections/stack"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
@@ -70,6 +72,47 @@ type Gui struct {
Errors SentinelErrors
Updater *updates.Updater
statusManager *statusManager
credentials credentials
waitForIntro sync.WaitGroup
}
// 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
type stagingPanelState struct {
SelectedLine int
StageableLines []int
HunkStarts []int
Diff string
}
type filePanelState struct {
SelectedLine int
}
type branchPanelState struct {
SelectedLine int
}
type commitPanelState struct {
SelectedLine int
}
type stashPanelState struct {
SelectedLine int
}
type menuPanelState struct {
SelectedLine int
}
type panelStates struct {
Files *filePanelState
Staging *stagingPanelState
Branches *branchPanelState
Commits *commitPanelState
Stash *stashPanelState
Menu *menuPanelState
}
type guiState struct {
@@ -85,6 +128,7 @@ type guiState struct {
EditHistory *stack.Stack
Platform commands.Platform
Updating bool
Panels *panelStates
}
// NewGui builds a new gui handler
@@ -100,6 +144,13 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *comma
Conflicts: make([]commands.Conflict, 0),
EditHistory: stack.New(),
Platform: *oSCommand.Platform,
Panels: &panelStates{
Files: &filePanelState{SelectedLine: -1},
Branches: &branchPanelState{SelectedLine: 0},
Commits: &commitPanelState{SelectedLine: -1},
Stash: &stashPanelState{SelectedLine: -1},
Menu: &menuPanelState{SelectedLine: 0},
},
}
gui := &Gui{
@@ -130,7 +181,12 @@ func (gui *Gui) scrollUpMain(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) scrollDownMain(g *gocui.Gui, v *gocui.View) error {
mainView, _ := g.View("main")
ox, oy := mainView.Origin()
if oy < len(mainView.BufferLines()) {
y := oy
if !gui.Config.GetUserConfig().GetBool("gui.scrollPastBottom") {
_, sy := mainView.Size()
y += sy
}
if y < len(mainView.BufferLines()) {
return mainView.SetOrigin(ox, oy+gui.Config.GetUserConfig().GetInt("gui.scrollHeight"))
}
return nil
@@ -180,8 +236,11 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
v.Title = gui.Tr.SLocalize("NotEnoughSpace")
v.Wrap = true
g.SetViewOnTop("limit")
}
return nil
} else {
_, _ = g.SetViewOnBottom("limit")
}
g.DeleteView("limit")
@@ -202,6 +261,19 @@ func (gui *Gui) layout(g *gocui.Gui) error {
v.FgColor = gocui.ColorWhite
}
v, err = g.SetView("staging", leftSideWidth+panelSpacing, 0, width-1, optionsTop, gocui.LEFT)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = gui.Tr.SLocalize("StagingTitle")
v.Highlight = true
v.FgColor = gocui.ColorWhite
if _, err := g.SetViewOnBottom("staging"); err != nil {
return err
}
}
if v, err := g.SetView("status", 0, 0, leftSideWidth, statusFilesBoundary, gocui.BOTTOM|gocui.RIGHT); err != nil {
if err != gocui.ErrUnknownView {
return err
@@ -220,12 +292,13 @@ func (gui *Gui) layout(g *gocui.Gui) error {
v.FgColor = gocui.ColorWhite
}
if v, err := g.SetView("branches", 0, filesBranchesBoundary+panelSpacing, leftSideWidth, commitsBranchesBoundary, gocui.TOP|gocui.BOTTOM); err != nil {
branchesView, err := g.SetView("branches", 0, filesBranchesBoundary+panelSpacing, leftSideWidth, commitsBranchesBoundary, gocui.TOP|gocui.BOTTOM)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = gui.Tr.SLocalize("BranchesTitle")
v.FgColor = gocui.ColorWhite
branchesView.Title = gui.Tr.SLocalize("BranchesTitle")
branchesView.FgColor = gocui.ColorWhite
}
if v, err := g.SetView("commits", 0, commitsBranchesBoundary+panelSpacing, leftSideWidth, commitsStashBoundary, gocui.TOP|gocui.BOTTOM); err != nil {
@@ -268,6 +341,23 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
}
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", 0, 0, width/2, height/2, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
_, err := g.SetViewOnBottom("credentials")
if err != nil {
return err
}
credentialsView.Title = gui.Tr.SLocalize("CredentialsUsername")
credentialsView.FgColor = gocui.ColorWhite
credentialsView.Editable = true
credentialsView.Editor = gocui.EditorFunc(gui.simpleEditor)
}
}
if appStatusView, err := g.SetView("appStatus", -1, optionsTop, width, optionsTop+2, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
@@ -297,12 +387,16 @@ func (gui *Gui) layout(g *gocui.Gui) error {
if err := gui.updateRecentRepoList(); err != nil {
return err
}
gui.waitForIntro.Done()
if _, err := gui.g.SetCurrentView(filesView.Name()); err != nil {
return err
}
if err := gui.refreshSidePanels(gui.g); err != nil {
return err
}
gui.handleFileSelect(g, filesView)
gui.refreshFiles(g)
gui.refreshBranches(g)
gui.refreshCommits(g)
gui.refreshStashEntries(g)
if err := gui.switchFocus(g, nil, filesView); err != nil {
return err
}
@@ -314,33 +408,68 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
}
listViews := map[*gocui.View]int{
filesView: gui.State.Panels.Files.SelectedLine,
branchesView: gui.State.Panels.Branches.SelectedLine,
}
for view, selectedLine := range listViews {
// check if the selected line is now out of view and if so refocus it
if err := gui.focusPoint(0, selectedLine, view); err != nil {
return err
}
}
// here is a good place log some stuff
// if you download humanlog and do tail -f development.log | humanlog
// this will let you see these branches as prettified json
// gui.Log.Info(utils.AsJson(gui.State.Branches[0:4]))
return gui.resizeCurrentPopupPanel(g)
}
func (gui *Gui) promptAnonymousReporting() error {
return gui.createConfirmationPanel(gui.g, nil, gui.Tr.SLocalize("AnonymousReportingTitle"), gui.Tr.SLocalize("AnonymousReportingPrompt"), func(g *gocui.Gui, v *gocui.View) error {
gui.waitForIntro.Done()
return gui.Config.WriteToUserConfig("reporting", "on")
}, func(g *gocui.Gui, v *gocui.View) error {
gui.waitForIntro.Done()
return gui.Config.WriteToUserConfig("reporting", "off")
})
}
func (gui *Gui) fetch(g *gocui.Gui) error {
gui.GitCommand.Fetch()
func (gui *Gui) fetch(g *gocui.Gui, v *gocui.View, canAskForCredentials bool) (unamePassOpend bool, err error) {
unamePassOpend = false
err = gui.GitCommand.Fetch(func(passOrUname string) string {
unamePassOpend = true
return gui.waitForPassUname(gui.g, v, passOrUname)
}, canAskForCredentials)
if canAskForCredentials && err != nil && strings.Contains(err.Error(), "exit status 128") {
colorFunction := color.New(color.FgRed).SprintFunc()
coloredMessage := colorFunction(strings.TrimSpace(gui.Tr.SLocalize("PassUnameWrong")))
close := func(g *gocui.Gui, v *gocui.View) error {
return nil
}
_ = gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("Error"), coloredMessage, close, close)
}
gui.refreshStatus(g)
return nil
return unamePassOpend, err
}
func (gui *Gui) updateLoader(g *gocui.Gui) error {
if view, _ := g.View("confirmation"); view != nil {
content := gui.trimmedContent(view)
if strings.Contains(content, "...") {
staticContent := strings.Split(content, "...")[0] + "..."
if err := gui.renderString(g, "confirmation", staticContent+" "+utils.Loader()); err != nil {
return err
gui.g.Update(func(g *gocui.Gui) error {
if view, _ := g.View("confirmation"); view != nil {
content := gui.trimmedContent(view)
if strings.Contains(content, "...") {
staticContent := strings.Split(content, "...")[0] + "..."
if err := gui.setViewContent(g, view, staticContent+" "+utils.Loader()); err != nil {
return err
}
}
}
}
return nil
})
return nil
}
@@ -352,8 +481,8 @@ func (gui *Gui) renderAppStatus(g *gocui.Gui) error {
return nil
}
func (gui *Gui) renderGlobalOptions(g *gocui.Gui) error {
return gui.renderOptionsMap(g, map[string]string{
func (gui *Gui) renderGlobalOptions() error {
return gui.renderOptionsMap(map[string]string{
"PgUp/PgDn": gui.Tr.SLocalize("scroll"),
"← → ↑ ↓": gui.Tr.SLocalize("navigate"),
"esc/q": gui.Tr.SLocalize("close"),
@@ -383,7 +512,28 @@ func (gui *Gui) Run() error {
return err
}
gui.goEvery(g, time.Second*60, gui.fetch)
if gui.Config.GetUserConfig().GetString("reporting") == "undetermined" {
gui.waitForIntro.Add(2)
} else {
gui.waitForIntro.Add(1)
}
go func() {
gui.waitForIntro.Wait()
isNew := gui.Config.GetIsNewRepo()
if !isNew {
time.After(60 * time.Second)
}
_, err := gui.fetch(g, g.CurrentView(), false)
if err != nil && strings.Contains(err.Error(), "exit status 128") && isNew {
_ = gui.createConfirmationPanel(g, g.CurrentView(), gui.Tr.SLocalize("NoAutomaticGitFetchTitle"), gui.Tr.SLocalize("NoAutomaticGitFetchBody"), nil, nil)
} else {
gui.goEvery(g, time.Second*60, func(g *gocui.Gui) error {
_, err := gui.fetch(g, g.CurrentView(), false)
return err
})
}
}()
gui.goEvery(g, time.Second*10, gui.refreshFiles)
gui.goEvery(g, time.Millisecond*50, gui.updateLoader)
gui.goEvery(g, time.Millisecond*50, gui.renderAppStatus)

View File

@@ -12,7 +12,6 @@ type Binding struct {
Handler func(*gocui.Gui, *gocui.View) error
Key interface{} // FIXME: find out how to get `gocui.Key | rune`
Modifier gocui.Modifier
KeyReadable string
Description string
}
@@ -21,19 +20,31 @@ func (b *Binding) GetDisplayStrings() []string {
return []string{b.GetKey(), b.Description}
}
// GetKey is a function.
func (b *Binding) GetKey() string {
r, ok := b.Key.(rune)
key := ""
key := 0
if ok {
key = string(r)
} else if b.KeyReadable != "" {
key = b.KeyReadable
switch b.Key.(type) {
case rune:
key = int(b.Key.(rune))
case gocui.Key:
key = int(b.Key.(gocui.Key))
}
return key
// special keys
switch key {
case 27:
return "esc"
case 13:
return "enter"
case 32:
return "space"
}
return string(key)
}
// GetKeybindings is a function.
func (gui *Gui) GetKeybindings() []*Binding {
bindings := []*Binding{
{
@@ -125,6 +136,12 @@ func (gui *Gui) GetKeybindings() []*Binding {
Modifier: gocui.ModNone,
Handler: gui.handleCommitPress,
Description: gui.Tr.SLocalize("CommitChanges"),
}, {
ViewName: "files",
Key: 'A',
Modifier: gocui.ModNone,
Handler: gui.handleAmendCommitPress,
Description: gui.Tr.SLocalize("AmendLastCommit"),
}, {
ViewName: "files",
Key: 'C',
@@ -136,7 +153,6 @@ func (gui *Gui) GetKeybindings() []*Binding {
Key: gocui.KeySpace,
Modifier: gocui.ModNone,
Handler: gui.handleFilePress,
KeyReadable: "space",
Description: gui.Tr.SLocalize("toggleStaged"),
}, {
ViewName: "files",
@@ -182,7 +198,7 @@ func (gui *Gui) GetKeybindings() []*Binding {
Description: gui.Tr.SLocalize("stashFiles"),
}, {
ViewName: "files",
Key: 'A',
Key: 'M',
Modifier: gocui.ModNone,
Handler: gui.handleAbortMerge,
Description: gui.Tr.SLocalize("abortMerge"),
@@ -202,8 +218,20 @@ func (gui *Gui) GetKeybindings() []*Binding {
ViewName: "files",
Key: 'D',
Modifier: gocui.ModNone,
Handler: gui.handleResetHard,
Handler: gui.handleResetAndClean,
Description: gui.Tr.SLocalize("resetHard"),
}, {
ViewName: "files",
Key: gocui.KeyEnter,
Modifier: gocui.ModNone,
Handler: gui.handleSwitchToStagingPanel,
Description: gui.Tr.SLocalize("StageLines"),
}, {
ViewName: "files",
Key: 'f',
Modifier: gocui.ModNone,
Handler: gui.handleGitFetch,
Description: gui.Tr.SLocalize("fetch"),
}, {
ViewName: "main",
Key: gocui.KeyEsc,
@@ -269,8 +297,13 @@ func (gui *Gui) GetKeybindings() []*Binding {
Key: gocui.KeySpace,
Modifier: gocui.ModNone,
Handler: gui.handleBranchPress,
KeyReadable: "space",
Description: gui.Tr.SLocalize("checkout"),
}, {
ViewName: "branches",
Key: 'o',
Modifier: gocui.ModNone,
Handler: gui.handleCreatePullRequestPress,
Description: gui.Tr.SLocalize("createPullRequest"),
}, {
ViewName: "branches",
Key: 'c',
@@ -295,18 +328,18 @@ func (gui *Gui) GetKeybindings() []*Binding {
Modifier: gocui.ModNone,
Handler: gui.handleDeleteBranch,
Description: gui.Tr.SLocalize("deleteBranch"),
}, {
ViewName: "branches",
Key: 'D',
Modifier: gocui.ModNone,
Handler: gui.handleForceDeleteBranch,
Description: gui.Tr.SLocalize("forceDeleteBranch"),
}, {
ViewName: "branches",
Key: 'm',
Modifier: gocui.ModNone,
Handler: gui.handleMerge,
Description: gui.Tr.SLocalize("mergeIntoCurrentBranch"),
}, {
ViewName: "branches",
Key: 'f',
Modifier: gocui.ModNone,
Handler: gui.handleFastForward,
Description: gui.Tr.SLocalize("FastForward"),
}, {
ViewName: "commits",
Key: 's',
@@ -342,7 +375,6 @@ func (gui *Gui) GetKeybindings() []*Binding {
Key: gocui.KeySpace,
Modifier: gocui.ModNone,
Handler: gui.handleStashApply,
KeyReadable: "space",
Description: gui.Tr.SLocalize("apply"),
}, {
ViewName: "stash",
@@ -366,6 +398,16 @@ func (gui *Gui) GetKeybindings() []*Binding {
Key: gocui.KeyEsc,
Modifier: gocui.ModNone,
Handler: gui.handleCommitClose,
}, {
ViewName: "credentials",
Key: gocui.KeyEnter,
Modifier: gocui.ModNone,
Handler: gui.handleSubmitCredential,
}, {
ViewName: "credentials",
Key: gocui.KeyEsc,
Modifier: gocui.ModNone,
Handler: gui.handleCloseCredentialsView,
}, {
ViewName: "menu",
Key: gocui.KeyEsc,
@@ -376,22 +418,94 @@ func (gui *Gui) GetKeybindings() []*Binding {
Key: 'q',
Modifier: gocui.ModNone,
Handler: gui.handleMenuClose,
}, {
ViewName: "staging",
Key: gocui.KeyEsc,
Modifier: gocui.ModNone,
Handler: gui.handleStagingEscape,
Description: gui.Tr.SLocalize("EscapeStaging"),
}, {
ViewName: "staging",
Key: gocui.KeyArrowUp,
Modifier: gocui.ModNone,
Handler: gui.handleStagingPrevLine,
}, {
ViewName: "staging",
Key: gocui.KeyArrowDown,
Modifier: gocui.ModNone,
Handler: gui.handleStagingNextLine,
}, {
ViewName: "staging",
Key: 'k',
Modifier: gocui.ModNone,
Handler: gui.handleStagingPrevLine,
}, {
ViewName: "staging",
Key: 'j',
Modifier: gocui.ModNone,
Handler: gui.handleStagingNextLine,
}, {
ViewName: "staging",
Key: gocui.KeyArrowLeft,
Modifier: gocui.ModNone,
Handler: gui.handleStagingPrevHunk,
}, {
ViewName: "staging",
Key: gocui.KeyArrowRight,
Modifier: gocui.ModNone,
Handler: gui.handleStagingNextHunk,
}, {
ViewName: "staging",
Key: 'h',
Modifier: gocui.ModNone,
Handler: gui.handleStagingPrevHunk,
}, {
ViewName: "staging",
Key: 'l',
Modifier: gocui.ModNone,
Handler: gui.handleStagingNextHunk,
}, {
ViewName: "staging",
Key: gocui.KeySpace,
Modifier: gocui.ModNone,
Handler: gui.handleStageLine,
Description: gui.Tr.SLocalize("StageLine"),
}, {
ViewName: "staging",
Key: 'a',
Modifier: gocui.ModNone,
Handler: gui.handleStageHunk,
Description: gui.Tr.SLocalize("StageHunk"),
},
}
// Would make these keybindings global but that interferes with editing
// input in the confirmation panel
for _, viewName := range []string{"status", "files", "branches", "commits", "stash", "menu"} {
for _, viewName := range []string{"status", "branches", "files", "commits", "stash", "menu"} {
bindings = append(bindings, []*Binding{
{ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.nextView},
{ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.previousView},
{ViewName: viewName, Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: gui.nextView},
{ViewName: viewName, Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: gui.cursorUp},
{ViewName: viewName, Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: gui.cursorDown},
{ViewName: viewName, Key: 'h', Modifier: gocui.ModNone, Handler: gui.previousView},
{ViewName: viewName, Key: 'l', Modifier: gocui.ModNone, Handler: gui.nextView},
{ViewName: viewName, Key: 'k', Modifier: gocui.ModNone, Handler: gui.cursorUp},
{ViewName: viewName, Key: 'j', Modifier: gocui.ModNone, Handler: gui.cursorDown},
}...)
}
listPanelMap := map[string]struct {
prevLine func(*gocui.Gui, *gocui.View) error
nextLine func(*gocui.Gui, *gocui.View) error
}{
"menu": {prevLine: gui.handleMenuPrevLine, nextLine: gui.handleMenuNextLine},
"files": {prevLine: gui.handleFilesPrevLine, nextLine: gui.handleFilesNextLine},
"branches": {prevLine: gui.handleBranchesPrevLine, nextLine: gui.handleBranchesNextLine},
"commits": {prevLine: gui.handleCommitsPrevLine, nextLine: gui.handleCommitsNextLine},
"stash": {prevLine: gui.handleStashPrevLine, nextLine: gui.handleStashNextLine},
}
for viewName, functions := range listPanelMap {
bindings = append(bindings, []*Binding{
{ViewName: viewName, Key: 'k', Modifier: gocui.ModNone, Handler: functions.prevLine},
{ViewName: viewName, Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: functions.prevLine},
{ViewName: viewName, Key: 'j', Modifier: gocui.ModNone, Handler: functions.nextLine},
{ViewName: viewName, Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: functions.nextLine},
}...)
}

View File

@@ -8,19 +8,35 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
// list panel functions
func (gui *Gui) handleMenuSelect(g *gocui.Gui, v *gocui.View) error {
// doing nothing for now
// but it is needed for switch in newLineFocused
return nil
return gui.focusPoint(0, gui.State.Panels.Menu.SelectedLine, v)
}
func (gui *Gui) renderMenuOptions(g *gocui.Gui) error {
func (gui *Gui) handleMenuNextLine(g *gocui.Gui, v *gocui.View) error {
panelState := gui.State.Panels.Menu
gui.changeSelectedLine(&panelState.SelectedLine, v.LinesHeight(), false)
return gui.handleMenuSelect(g, v)
}
func (gui *Gui) handleMenuPrevLine(g *gocui.Gui, v *gocui.View) error {
panelState := gui.State.Panels.Menu
gui.changeSelectedLine(&panelState.SelectedLine, v.LinesHeight(), true)
return gui.handleMenuSelect(g, v)
}
// specific functions
func (gui *Gui) renderMenuOptions() error {
optionsMap := map[string]string{
"esc/q": gui.Tr.SLocalize("close"),
"↑ ↓": gui.Tr.SLocalize("navigate"),
"space": gui.Tr.SLocalize("execute"),
}
return gui.renderOptionsMap(g, optionsMap)
return gui.renderOptionsMap(optionsMap)
}
func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error {
@@ -40,20 +56,17 @@ func (gui *Gui) createMenu(items interface{}, handlePress func(int) error) error
return err
}
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, list)
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, false, list)
menuView, _ := gui.g.SetView("menu", x0, y0, x1, y1, 0)
menuView.Title = strings.Title(gui.Tr.SLocalize("menu"))
menuView.FgColor = gocui.ColorWhite
menuView.Clear()
fmt.Fprint(menuView, list)
if err := gui.renderMenuOptions(gui.g); err != nil {
return err
}
gui.State.Panels.Menu.SelectedLine = 0
wrappedHandlePress := func(g *gocui.Gui, v *gocui.View) error {
lineNumber := gui.getItemPosition(v)
return handlePress(lineNumber)
selectedLine := gui.State.Panels.Menu.SelectedLine
return handlePress(selectedLine)
}
if err := gui.g.SetKeybinding("menu", gocui.KeySpace, gocui.ModNone, wrappedHandlePress); err != nil {

View File

@@ -194,9 +194,6 @@ func (gui *Gui) refreshMergePanel(g *gocui.Gui) error {
gui.State.ConflictIndex = len(gui.State.Conflicts) - 1
}
hasFocus := gui.currentViewName(g) == "main"
if hasFocus {
gui.renderMergeOptions(g)
}
content, err := gui.coloredConflictFile(cat, gui.State.Conflicts, gui.State.ConflictIndex, gui.State.ConflictTop, hasFocus)
if err != nil {
return err
@@ -233,8 +230,8 @@ func (gui *Gui) switchToMerging(g *gocui.Gui) error {
return gui.refreshMergePanel(g)
}
func (gui *Gui) renderMergeOptions(g *gocui.Gui) error {
return gui.renderOptionsMap(g, map[string]string{
func (gui *Gui) renderMergeOptions() error {
return gui.renderOptionsMap(map[string]string{
"↑ ↓": gui.Tr.SLocalize("selectHunk"),
"← →": gui.Tr.SLocalize("navigateConflicts"),
"space": gui.Tr.SLocalize("pickHunk"),

View File

@@ -14,6 +14,7 @@ type recentRepo struct {
path string
}
// GetDisplayStrings returns the path from a recent repo.
func (r *recentRepo) GetDisplayStrings() []string {
yellow := color.New(color.FgMagenta)
base := filepath.Base(r.path)
@@ -54,16 +55,22 @@ func (gui *Gui) updateRecentRepoList() error {
if err != nil {
return err
}
gui.Config.GetAppState().RecentRepos = newRecentReposList(recentRepos, currentRepo)
known, recentRepos := newRecentReposList(recentRepos, currentRepo)
gui.Config.SetIsNewRepo(known)
gui.Config.GetAppState().RecentRepos = recentRepos
return gui.Config.SaveAppState()
}
func newRecentReposList(recentRepos []string, currentRepo string) []string {
// newRecentReposList returns a new repo list with a new entry but only when it doesn't exist yet
func newRecentReposList(recentRepos []string, currentRepo string) (bool, []string) {
isNew := true
newRepos := []string{currentRepo}
for _, repo := range recentRepos {
if repo != currentRepo {
newRepos = append(newRepos, repo)
} else {
isNew = false
}
}
return newRepos
return isNew, newRepos
}

219
pkg/gui/staging_panel.go Normal file
View File

@@ -0,0 +1,219 @@
package gui
import (
"errors"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/git"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) refreshStagingPanel() error {
file, err := gui.getSelectedFile(gui.g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
}
return gui.handleStagingEscape(gui.g, nil)
}
if !file.HasUnstagedChanges {
return gui.handleStagingEscape(gui.g, nil)
}
// note for custom diffs, we'll need to send a flag here saying not to use the custom diff
diff := gui.GitCommand.Diff(file, true)
colorDiff := gui.GitCommand.Diff(file, false)
if len(diff) < 2 {
return gui.handleStagingEscape(gui.g, nil)
}
// parse the diff and store the line numbers of hunks and stageable lines
// TODO: maybe instantiate this at application start
p, err := git.NewPatchParser(gui.Log)
if err != nil {
return nil
}
hunkStarts, stageableLines, err := p.ParsePatch(diff)
if err != nil {
return nil
}
var selectedLine int
if gui.State.Panels.Staging != nil {
end := len(stageableLines) - 1
if end < gui.State.Panels.Staging.SelectedLine {
selectedLine = end
} else {
selectedLine = gui.State.Panels.Staging.SelectedLine
}
} else {
selectedLine = 0
}
gui.State.Panels.Staging = &stagingPanelState{
StageableLines: stageableLines,
HunkStarts: hunkStarts,
SelectedLine: selectedLine,
Diff: diff,
}
if len(stageableLines) == 0 {
return errors.New("No lines to stage")
}
if err := gui.focusLineAndHunk(); err != nil {
return err
}
return gui.renderString(gui.g, "staging", colorDiff)
}
func (gui *Gui) handleStagingEscape(g *gocui.Gui, v *gocui.View) error {
if _, err := gui.g.SetViewOnBottom("staging"); err != nil {
return err
}
gui.State.Panels.Staging = nil
return gui.switchFocus(gui.g, nil, gui.getFilesView(gui.g))
}
func (gui *Gui) handleStagingPrevLine(g *gocui.Gui, v *gocui.View) error {
return gui.handleCycleLine(true)
}
func (gui *Gui) handleStagingNextLine(g *gocui.Gui, v *gocui.View) error {
return gui.handleCycleLine(false)
}
func (gui *Gui) handleStagingPrevHunk(g *gocui.Gui, v *gocui.View) error {
return gui.handleCycleHunk(true)
}
func (gui *Gui) handleStagingNextHunk(g *gocui.Gui, v *gocui.View) error {
return gui.handleCycleHunk(false)
}
func (gui *Gui) handleCycleHunk(prev bool) error {
state := gui.State.Panels.Staging
lineNumbers := state.StageableLines
currentLine := lineNumbers[state.SelectedLine]
currentHunkIndex := utils.PrevIndex(state.HunkStarts, currentLine)
var newHunkIndex int
if prev {
if currentHunkIndex == 0 {
newHunkIndex = len(state.HunkStarts) - 1
} else {
newHunkIndex = currentHunkIndex - 1
}
} else {
if currentHunkIndex == len(state.HunkStarts)-1 {
newHunkIndex = 0
} else {
newHunkIndex = currentHunkIndex + 1
}
}
state.SelectedLine = utils.NextIndex(lineNumbers, state.HunkStarts[newHunkIndex])
return gui.focusLineAndHunk()
}
func (gui *Gui) handleCycleLine(prev bool) error {
state := gui.State.Panels.Staging
lineNumbers := state.StageableLines
currentLine := lineNumbers[state.SelectedLine]
var newIndex int
if prev {
newIndex = utils.PrevIndex(lineNumbers, currentLine)
} else {
newIndex = utils.NextIndex(lineNumbers, currentLine)
}
state.SelectedLine = newIndex
return gui.focusLineAndHunk()
}
// focusLineAndHunk works out the best focus for the staging panel given the
// selected line and size of the hunk
func (gui *Gui) focusLineAndHunk() error {
stagingView := gui.getStagingView(gui.g)
state := gui.State.Panels.Staging
lineNumber := state.StageableLines[state.SelectedLine]
// we want the bottom line of the view buffer to ideally be the bottom line
// of the hunk, but if the hunk is too big we'll just go three lines beyond
// the currently selected line so that the user can see the context
var bottomLine int
nextHunkStartIndex := utils.NextIndex(state.HunkStarts, lineNumber)
if nextHunkStartIndex == 0 {
// for now linesHeight is an efficient means of getting the number of lines
// in the patch. However if we introduce word wrap we'll need to update this
bottomLine = stagingView.LinesHeight() - 1
} else {
bottomLine = state.HunkStarts[nextHunkStartIndex] - 1
}
hunkStartIndex := utils.PrevIndex(state.HunkStarts, lineNumber)
hunkStart := state.HunkStarts[hunkStartIndex]
// if it's the first hunk we'll also show the diff header
if hunkStartIndex == 0 {
hunkStart = 0
}
_, height := stagingView.Size()
// if this hunk is too big, we will just ensure that the user can at least
// see three lines of context below the cursor
if bottomLine-hunkStart > height {
bottomLine = lineNumber + 3
}
return gui.generalFocusLine(lineNumber, bottomLine, stagingView)
}
func (gui *Gui) handleStageHunk(g *gocui.Gui, v *gocui.View) error {
return gui.handleStageLineOrHunk(true)
}
func (gui *Gui) handleStageLine(g *gocui.Gui, v *gocui.View) error {
return gui.handleStageLineOrHunk(false)
}
func (gui *Gui) handleStageLineOrHunk(hunk bool) error {
state := gui.State.Panels.Staging
p, err := git.NewPatchModifier(gui.Log)
if err != nil {
return err
}
currentLine := state.StageableLines[state.SelectedLine]
var patch string
if hunk {
patch, err = p.ModifyPatchForHunk(state.Diff, state.HunkStarts, currentLine)
} else {
patch, err = p.ModifyPatchForLine(state.Diff, currentLine)
}
if err != nil {
return err
}
// for logging purposes
// ioutil.WriteFile("patch.diff", []byte(patch), 0600)
// apply the patch then refresh this panel
// create a new temp file with the patch, then call git apply with that patch
_, err = gui.GitCommand.ApplyPatch(patch)
if err != nil {
return err
}
if err := gui.refreshFiles(gui.g); err != nil {
return err
}
if err := gui.refreshStagingPanel(); err != nil {
return err
}
return nil
}

View File

@@ -8,19 +8,46 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
// list panel functions
func (gui *Gui) getSelectedStashEntry(v *gocui.View) *commands.StashEntry {
selectedLine := gui.State.Panels.Stash.SelectedLine
if selectedLine == -1 {
return nil
}
return gui.State.StashEntries[selectedLine]
}
func (gui *Gui) handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error {
stashEntry := gui.getSelectedStashEntry(v)
if stashEntry == nil {
return gui.renderString(g, "main", gui.Tr.SLocalize("NoStashEntries"))
}
if err := gui.focusPoint(0, gui.State.Panels.Stash.SelectedLine, v); err != nil {
return err
}
go func() {
// doing this asynchronously cos it can take time
diff, _ := gui.GitCommand.GetStashEntryDiff(stashEntry.Index)
_ = gui.renderString(g, "main", diff)
}()
return nil
}
func (gui *Gui) refreshStashEntries(g *gocui.Gui) error {
g.Update(func(g *gocui.Gui) error {
v, err := g.View("stash")
if err != nil {
panic(err)
}
gui.State.StashEntries = gui.GitCommand.GetStashEntries()
v.Clear()
gui.refreshSelectedLine(&gui.State.Panels.Stash.SelectedLine, len(gui.State.StashEntries))
list, err := utils.RenderList(gui.State.StashEntries)
if err != nil {
return err
}
v := gui.getStashView(gui.g)
v.Clear()
fmt.Fprint(v, list)
return gui.resetOrigin(v)
@@ -28,34 +55,21 @@ func (gui *Gui) refreshStashEntries(g *gocui.Gui) error {
return nil
}
func (gui *Gui) getSelectedStashEntry(v *gocui.View) *commands.StashEntry {
if len(gui.State.StashEntries) == 0 {
return nil
}
stashView, _ := gui.g.View("stash")
lineNumber := gui.getItemPosition(stashView)
return gui.State.StashEntries[lineNumber]
func (gui *Gui) handleStashNextLine(g *gocui.Gui, v *gocui.View) error {
panelState := gui.State.Panels.Stash
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.StashEntries), false)
return gui.handleStashEntrySelect(gui.g, v)
}
func (gui *Gui) renderStashOptions(g *gocui.Gui) error {
return gui.renderGlobalOptions(g)
func (gui *Gui) handleStashPrevLine(g *gocui.Gui, v *gocui.View) error {
panelState := gui.State.Panels.Stash
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.StashEntries), true)
return gui.handleStashEntrySelect(gui.g, v)
}
func (gui *Gui) handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error {
if err := gui.renderStashOptions(g); err != nil {
return err
}
go func() {
stashEntry := gui.getSelectedStashEntry(v)
if stashEntry == nil {
gui.renderString(g, "main", gui.Tr.SLocalize("NoStashEntries"))
return
}
diff, _ := gui.GitCommand.GetStashEntryDiff(stashEntry.Index)
gui.renderString(g, "main", diff)
}()
return nil
}
// specific functions
func (gui *Gui) handleStashApply(g *gocui.Gui, v *gocui.View) error {
return gui.stashDo(g, v, "apply")

View File

@@ -2,6 +2,7 @@ package gui
import (
"fmt"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
@@ -18,7 +19,7 @@ func (gui *Gui) refreshStatus(g *gocui.Gui) error {
// contents end up cleared
g.Update(func(*gocui.Gui) error {
v.Clear()
pushables, pullables := gui.GitCommand.UpstreamDifferenceCount()
pushables, pullables := gui.GitCommand.GetCurrentBranchUpstreamDifferenceCount()
fmt.Fprint(v, "↑"+pushables+"↓"+pullables)
branches := gui.State.Branches
if err := gui.updateHasMergeConflictStatus(); err != nil {
@@ -41,29 +42,26 @@ func (gui *Gui) refreshStatus(g *gocui.Gui) error {
return nil
}
func (gui *Gui) renderStatusOptions(g *gocui.Gui) error {
return gui.renderGlobalOptions(g)
}
func (gui *Gui) handleCheckForUpdate(g *gocui.Gui, v *gocui.View) error {
gui.Updater.CheckForNewUpdate(gui.onUserUpdateCheckFinish, true)
return gui.createMessagePanel(gui.g, v, "", gui.Tr.SLocalize("CheckingForUpdates"))
}
func (gui *Gui) handleStatusSelect(g *gocui.Gui, v *gocui.View) error {
dashboardString := fmt.Sprintf(
"%s\n\n%s\n\n%s\n\n%s\n\n%s",
lazygitTitle(),
"Keybindings: https://github.com/jesseduffield/lazygit/blob/master/docs/Keybindings.md",
"Config Options: https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md",
"Tutorial: https://www.youtube.com/watch?v=VDXvbHZYeKY",
"Raise an Issue: https://github.com/jesseduffield/lazygit/issues",
)
blue := color.New(color.FgBlue)
if err := gui.renderString(g, "main", dashboardString); err != nil {
return err
}
return gui.renderStatusOptions(g)
dashboardString := strings.Join(
[]string{
lazygitTitle(),
"Copyright (c) 2018 Jesse Duffield",
"Keybindings: https://github.com/jesseduffield/lazygit/blob/master/docs/Keybindings.md",
"Config Options: https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md",
"Tutorial: https://youtu.be/VDXvbHZYeKY",
"Raise an Issue: https://github.com/jesseduffield/lazygit/issues",
blue.Sprint("Buy Jesse a coffee: https://donorbox.org/lazygit"), // caffeine ain't free
}, "\n\n")
return gui.renderString(g, "main", dashboardString)
}
func (gui *Gui) handleOpenConfig(g *gocui.Gui, v *gocui.View) error {

View File

@@ -13,10 +13,16 @@ import (
var cyclableViews = []string{"status", "files", "branches", "commits", "stash"}
func (gui *Gui) refreshSidePanels(g *gocui.Gui) error {
gui.refreshBranches(g)
gui.refreshFiles(g)
gui.refreshCommits(g)
return nil
if err := gui.refreshBranches(g); err != nil {
return err
}
if err := gui.refreshFiles(g); err != nil {
return err
}
if err := gui.refreshCommits(g); err != nil {
return err
}
return gui.refreshStashEntries(g)
}
func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error {
@@ -78,31 +84,33 @@ func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error {
mainView, _ := g.View("main")
mainView.SetOrigin(0, 0)
switch v.Name() {
case "menu":
return gui.handleMenuSelect(g, v)
case "status":
return gui.handleStatusSelect(g, v)
case "files":
return gui.handleFileSelect(g, v)
return gui.handleFileSelect(g, v, false)
case "branches":
return gui.handleBranchSelect(g, v)
case "commits":
return gui.handleCommitSelect(g, v)
case "stash":
return gui.handleStashEntrySelect(g, v)
case "confirmation":
return nil
case "commitMessage":
return gui.handleCommitFocused(g, v)
case "credentials":
return gui.handleCredentialsViewFocused(g, v)
case "main":
// TODO: pull this out into a 'view focused' function
gui.refreshMergePanel(g)
v.Highlight = false
return nil
case "commits":
return gui.handleCommitSelect(g, v)
case "stash":
return gui.handleStashEntrySelect(g, v)
case "staging":
return nil
// return gui.handleStagingSelect(g, v)
default:
panic(gui.Tr.SLocalize("NoViewMachingNewLineFocusedSwitchStatement"))
}
@@ -133,8 +141,15 @@ func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error {
},
)
gui.Log.Info(message)
gui.State.PreviousView = oldView.Name()
// second class panels should never have focus restored to them because
// once they lose focus they are effectively 'destroyed'
secondClassPanels := []string{"confirmation", "menu"}
if !utils.IncludesString(secondClassPanels, oldView.Name()) {
gui.State.PreviousView = oldView.Name()
}
}
newView.Highlight = true
message := gui.Tr.TemplateLocalize(
"newFocusedViewIs",
@@ -146,64 +161,17 @@ func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error {
if _, err := g.SetCurrentView(newView.Name()); err != nil {
return err
}
g.Cursor = newView.Editable
return gui.newLineFocused(g, newView)
}
func (gui *Gui) getItemPosition(v *gocui.View) int {
gui.correctCursor(v)
_, cy := v.Cursor()
_, oy := v.Origin()
return oy + cy
}
func (gui *Gui) cursorUp(g *gocui.Gui, v *gocui.View) error {
// swallowing cursor movements in main
if v == nil || v.Name() == "main" {
return nil
}
ox, oy := v.Origin()
cx, cy := v.Cursor()
if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 {
if err := v.SetOrigin(ox, oy-1); err != nil {
return err
}
}
gui.newLineFocused(g, v)
return nil
}
func (gui *Gui) cursorDown(g *gocui.Gui, v *gocui.View) error {
// swallowing cursor movements in main
if v == nil || v.Name() == "main" {
return nil
}
cx, cy := v.Cursor()
ox, oy := v.Origin()
ly := len(v.BufferLines()) - 1
_, height := v.Size()
maxY := height - 1
// if we are at the end we just return
if cy+oy == ly {
return nil
}
var err error
if cy < maxY {
err = v.SetCursor(cx, cy+1)
} else {
err = v.SetOrigin(ox, oy+1)
}
if err != nil {
if _, err := g.SetViewOnTop(newView.Name()); err != nil {
return err
}
gui.newLineFocused(g, v)
return nil
g.Cursor = newView.Editable
if err := gui.renderPanelOptions(); err != nil {
return err
}
return gui.newLineFocused(g, newView)
}
func (gui *Gui) resetOrigin(v *gocui.View) error {
@@ -214,39 +182,68 @@ func (gui *Gui) resetOrigin(v *gocui.View) error {
}
// if the cursor down past the last item, move it to the last line
func (gui *Gui) correctCursor(v *gocui.View) error {
cx, cy := v.Cursor()
ox, oy := v.Origin()
_, height := v.Size()
maxY := height - 1
ly := len(v.BufferLines()) - 1
if oy+cy <= ly {
func (gui *Gui) focusPoint(cx int, cy int, v *gocui.View) error {
if cy < 0 {
return nil
}
newCy := utils.Min(ly, maxY)
if err := v.SetCursor(cx, newCy); err != nil {
return err
}
if err := v.SetOrigin(ox, ly-newCy); err != nil {
return err
ox, oy := v.Origin()
_, height := v.Size()
ly := height - 1
// if line is above origin, move origin and set cursor to zero
// if line is below origin + height, move origin and set cursor to max
// otherwise set cursor to value - origin
if ly > v.LinesHeight() {
if err := v.SetCursor(cx, cy); err != nil {
return err
}
if err := v.SetOrigin(ox, 0); err != nil {
return err
}
} else if cy < oy {
if err := v.SetCursor(cx, 0); err != nil {
return err
}
if err := v.SetOrigin(ox, cy); err != nil {
return err
}
} else if cy > oy+ly {
if err := v.SetCursor(cx, ly); err != nil {
return err
}
if err := v.SetOrigin(ox, cy-ly); err != nil {
return err
}
} else {
if err := v.SetCursor(cx, cy-oy); err != nil {
return err
}
}
return nil
}
func (gui *Gui) cleanString(s string) string {
output := string(bom.Clean([]byte(s)))
return utils.NormalizeLinefeeds(output)
}
func (gui *Gui) setViewContent(g *gocui.Gui, v *gocui.View, s string) error {
v.Clear()
fmt.Fprint(v, gui.cleanString(s))
return nil
}
// renderString resets the origin of a view and sets its content
func (gui *Gui) renderString(g *gocui.Gui, viewName, s string) error {
g.Update(func(*gocui.Gui) error {
v, err := g.View(viewName)
// just in case the view disappeared as this function was called, we'll
// silently return if it's not found
if err != nil {
return nil
return nil // return gracefully if view has been deleted
}
v.Clear()
output := string(bom.Clean([]byte(s)))
output = utils.NormalizeLinefeeds(output)
fmt.Fprint(v, output)
v.Wrap = true
return nil
if err := v.SetOrigin(0, 0); err != nil {
return err
}
return gui.setViewContent(gui.g, v, s)
})
return nil
}
@@ -260,8 +257,8 @@ func (gui *Gui) optionsMapToString(optionsMap map[string]string) string {
return strings.Join(optionsArray, ", ")
}
func (gui *Gui) renderOptionsMap(g *gocui.Gui, optionsMap map[string]string) error {
return gui.renderString(g, "options", gui.optionsMapToString(optionsMap))
func (gui *Gui) renderOptionsMap(optionsMap map[string]string) error {
return gui.renderString(gui.g, "options", gui.optionsMapToString(optionsMap))
}
// TODO: refactor properly
@@ -286,6 +283,21 @@ func (gui *Gui) getBranchesView(g *gocui.Gui) *gocui.View {
return v
}
func (gui *Gui) getStagingView(g *gocui.Gui) *gocui.View {
v, _ := g.View("staging")
return v
}
func (gui *Gui) getMainView(g *gocui.Gui) *gocui.View {
v, _ := g.View("main")
return v
}
func (gui *Gui) getStashView(g *gocui.Gui) *gocui.View {
v, _ := g.View("stash")
return v
}
func (gui *Gui) trimmedContent(v *gocui.View) string {
return strings.TrimSpace(v.Buffer())
}
@@ -297,7 +309,7 @@ func (gui *Gui) currentViewName(g *gocui.Gui) string {
func (gui *Gui) resizeCurrentPopupPanel(g *gocui.Gui) error {
v := g.CurrentView()
if v.Name() == "commitMessage" || v.Name() == "confirmation" {
if v.Name() == "commitMessage" || v.Name() == "credentials" || v.Name() == "confirmation" {
return gui.resizePopupPanel(g, v)
}
return nil
@@ -307,7 +319,7 @@ func (gui *Gui) resizePopupPanel(g *gocui.Gui, v *gocui.View) error {
// If the confirmation panel is already displayed, just resize the width,
// otherwise continue
content := v.Buffer()
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, content)
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, v.Wrap, content)
vx0, vy0, vx1, vy1 := v.Dimensions()
if vx0 == x0 && vy0 == y0 && vx1 == x1 && vy1 == y1 {
return nil
@@ -316,3 +328,68 @@ func (gui *Gui) resizePopupPanel(g *gocui.Gui, v *gocui.View) error {
_, err := g.SetView(v.Name(), x0, y0, x1, y1, 0)
return err
}
// generalFocusLine takes a lineNumber to focus, and a bottomLine to ensure we can see
func (gui *Gui) generalFocusLine(lineNumber int, bottomLine int, v *gocui.View) error {
_, height := v.Size()
overScroll := bottomLine - height + 1
if overScroll < 0 {
overScroll = 0
}
if err := v.SetOrigin(0, overScroll); err != nil {
return err
}
if err := v.SetCursor(0, lineNumber-overScroll); err != nil {
return err
}
return nil
}
func (gui *Gui) changeSelectedLine(line *int, total int, up bool) {
if up {
if *line == -1 || *line == 0 {
return
}
*line -= 1
} else {
if *line == -1 || *line == total-1 {
return
}
*line += 1
}
}
func (gui *Gui) refreshSelectedLine(line *int, total int) {
if *line == -1 && total > 0 {
*line = 0
} else if total-1 < *line {
*line = total - 1
}
}
func (gui *Gui) renderListPanel(v *gocui.View, items interface{}) error {
gui.g.Update(func(g *gocui.Gui) error {
list, err := utils.RenderList(items)
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
v.Clear()
fmt.Fprint(v, list)
return nil
})
return nil
}
func (gui *Gui) renderPanelOptions() error {
currentView := gui.g.CurrentView()
switch currentView.Name() {
case "menu":
return gui.renderMenuOptions()
case "main":
return gui.renderMergeOptions()
default:
return gui.renderGlobalOptions()
}
}

View File

@@ -30,13 +30,31 @@ func addDutch(i18nObject *i18n.Bundle) error {
Other: "Stash",
}, &i18n.Message{
ID: "CommitMessage",
Other: "Commit Bericht",
Other: "Commit bericht",
}, &i18n.Message{
ID: "CredentialsUsername",
Other: "Gebruikersnaam",
}, &i18n.Message{
ID: "CredentialsPassword",
Other: "Wachtwoord",
}, &i18n.Message{
ID: "PassUnameWrong",
Other: "Wachtwoord en/of gebruikersnaam verkeert",
}, &i18n.Message{
ID: "CommitChanges",
Other: "Commit Veranderingen",
Other: "Commit veranderingen",
}, &i18n.Message{
ID: "AmendLastCommit",
Other: "wijzig laatste commit",
}, &i18n.Message{
ID: "SureToAmend",
Other: "Weet je zeker dat je de laatste commit wilt wijzigen? U kunt het commit-bericht wijzigen vanuit het commits-paneel.",
}, &i18n.Message{
ID: "NoCommitToAmend",
Other: "Er is geen commits om te wijzigen.",
}, &i18n.Message{
ID: "CommitChangesWithEditor",
Other: "commit Veranderingen met de git editor",
Other: "commit veranderingen met de git editor",
}, &i18n.Message{
ID: "StatusTitle",
Other: "Status",
@@ -73,12 +91,18 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "refresh",
Other: "verversen",
}, &i18n.Message{
ID: "push",
Other: "push",
}, &i18n.Message{
ID: "pull",
Other: "pull",
}, &i18n.Message{
ID: "addPatch",
Other: "verandering toevoegen",
Other: "bewerkingen toevoegen",
}, &i18n.Message{
ID: "edit",
Other: "verander",
Other: "bewerken",
}, &i18n.Message{
ID: "scroll",
Other: "scroll",
@@ -87,13 +111,13 @@ func addDutch(i18nObject *i18n.Bundle) error {
Other: "samenvoegen afbreken",
}, &i18n.Message{
ID: "resolveMergeConflicts",
Other: "verhelp samenvoegen fouten",
Other: "los merge conflicten op",
}, &i18n.Message{
ID: "checkout",
Other: "uitchecken",
}, &i18n.Message{
ID: "NoChangedFiles",
Other: "Geen Bestanden verandert",
Other: "Geen bestanden veranderd",
}, &i18n.Message{
ID: "FileHasNoUnstagedChanges",
Other: "Het bestand heeft geen unstaged veranderingen om toe te voegen",
@@ -109,27 +133,33 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "NoFilesDisplay",
Other: "Geen bestanden om te laten zien",
}, &i18n.Message{
ID: "NotAFile",
Other: "Dit is geen bestand",
}, &i18n.Message{
ID: "PullWait",
Other: "Pulling...",
Other: "Pullen...",
}, &i18n.Message{
ID: "PushWait",
Other: "Pushing...",
Other: "Pushen...",
}, &i18n.Message{
ID: "FetchWait",
Other: "Fetchen...",
}, &i18n.Message{
ID: "FileNoMergeCons",
Other: "Dit bestand heeft geen merge conflicten",
}, &i18n.Message{
ID: "SureResetHardHead",
Other: "Weet je het zeker dat je `reset --hard HEAD` wil uitvoeren? het kan dat je hierdoor bestanden verliest",
Other: "Weet je het zeker dat je `reset --hard HEAD` en `clean -fd` wil uitvoeren? Het kan dat je hierdoor bestanden verliest",
}, &i18n.Message{
ID: "SureTo",
Other: "Weet je het zeker dat je {{.fileName}} wilt {{.deleteVerb}} (je veranderingen zullen worden verwijdert)",
Other: "Weet je het zeker dat je {{.fileName}} wilt {{.deleteVerb}} (je veranderingen zullen worden verwijderd)",
}, &i18n.Message{
ID: "AlreadyCheckedOutBranch",
Other: "Je hebt uitgecheckt op deze branch",
Other: "Je hebt deze branch al uitgecheckt",
}, &i18n.Message{
ID: "SureForceCheckout",
Other: "Weet je zeker dat je het uitchecken wil forceren? al je locale verandering zullen worden verwijdert",
Other: "Weet je zeker dat je het uitchecken wil forceren? Al je lokale verandering zullen worden verwijdert",
}, &i18n.Message{
ID: "ForceCheckoutBranch",
Other: "Forceer uitchecken op deze branch",
@@ -147,7 +177,7 @@ func addDutch(i18nObject *i18n.Bundle) error {
Other: "Verwijder branch",
}, &i18n.Message{
ID: "DeleteBranchMessage",
Other: "Weet je zeker dat je branch {{.selectedBranchName}} wil verwijderen?",
Other: "Weet je zeker dat je branch {{.selectedBranchName}} wilt verwijderen?",
}, &i18n.Message{
ID: "ForceDeleteBranchMessage",
Other: "Weet je zeker dat je branch {{.selectedBranchName}} geforceerd wil verwijderen?",
@@ -159,7 +189,7 @@ func addDutch(i18nObject *i18n.Bundle) error {
Other: "forceer checkout",
}, &i18n.Message{
ID: "merge",
Other: "merge",
Other: "samenvoegen",
}, &i18n.Message{
ID: "checkoutByName",
Other: "uitchecken bij naam",
@@ -198,7 +228,7 @@ func addDutch(i18nObject *i18n.Bundle) error {
Other: "squash beneden",
}, &i18n.Message{
ID: "rename",
Other: "hernoem",
Other: "hernoemen",
}, &i18n.Message{
ID: "resetToThisCommit",
Other: "reset naar deze commit",
@@ -240,7 +270,7 @@ func addDutch(i18nObject *i18n.Bundle) error {
Other: "Geen commits voor deze branch",
}, &i18n.Message{
ID: "Error",
Other: "Fout",
Other: "Foutmelding",
}, &i18n.Message{
ID: "resizingPopupPanel",
Other: "resizen popup paneel",
@@ -249,16 +279,16 @@ func addDutch(i18nObject *i18n.Bundle) error {
Other: "subprocess lopend",
}, &i18n.Message{
ID: "selectHunk",
Other: "selecteer Hunk",
Other: "selecteer stuk",
}, &i18n.Message{
ID: "navigateConflicts",
Other: "navigeer conflicts",
}, &i18n.Message{
ID: "pickHunk",
Other: "kies Hunk",
Other: "kies stuk",
}, &i18n.Message{
ID: "pickBothHunks",
Other: "kies bijde hunks",
Other: "kies beide stukken",
}, &i18n.Message{
ID: "undo",
Other: "ongedaan maken",
@@ -324,28 +354,31 @@ func addDutch(i18nObject *i18n.Bundle) error {
Other: "Forceer push",
}, &i18n.Message{
ID: "ForcePushPrompt",
Other: "Jou branch is afgeweken van de remote branch. Druk 'esc' om te anuleren, of 'enter' om geforceert te pushen.",
Other: "Jouw branch is afgeweken van de remote branch. Druk 'esc' om te annuleren, of 'enter' om geforceert te pushen.",
}, &i18n.Message{
ID: "checkForUpdate",
Other: "check voor updates",
}, &i18n.Message{
ID: "CheckingForUpdates",
Other: "checken voor updates...",
Other: "zoeken naar updates...",
}, &i18n.Message{
ID: "OnLatestVersionErr",
Other: "Je hebt al de laatste versie",
}, &i18n.Message{
ID: "MajorVersionErr",
Other: "Nieuwe versie ({{.newVersion}}) is niet teruggaand compatibele vergeleken met de huidige versie ({{.currentVersion}})",
Other: "Nieuwe versie ({{.newVersion}}) is niet backwards compatibele vergeleken met de huidige versie ({{.currentVersion}})",
}, &i18n.Message{
ID: "CouldNotFindBinaryErr",
Other: "Kon geen binary vinden op {{.url}}",
}, &i18n.Message{
ID: "AnonymousReportingTitle",
Other: "Help maak lazygit beter",
Other: "Help lazygit te verbeteren",
}, &i18n.Message{
ID: "AnonymousReportingPrompt",
Other: "Zou je anonieme data rapportage willen aanzetten om lazygit beter te kunnen maken? (enter/esc)",
}, &i18n.Message{
ID: "GitconfigParseErr",
Other: `Gogit kon je gitconfig bestand niet goed parsen door de aanwezigheid van losstaande '\' tekens. Het weghalen van deze tekens zou het probleem moeten oplossen. `,
}, &i18n.Message{
ID: "removeFile",
Other: `Verwijder als untracked / uitchecken wordt gevolgd (ga weg)`,
@@ -363,13 +396,58 @@ func addDutch(i18nObject *i18n.Bundle) error {
Other: `refresh bestanden`,
}, &i18n.Message{
ID: "resetHard",
Other: `harde reset`,
Other: `harde reset and verwijderen ongevolgde bestanden`,
}, &i18n.Message{
ID: "mergeIntoCurrentBranch",
Other: `merge in met huidige checked out branch`,
}, &i18n.Message{
ID: "ConfirmQuit",
Other: `Weet je zeker dat je dit programma wil sluiten?`,
}, &i18n.Message{
ID: "SwitchRepo",
Other: "wissel naar een recente repo",
}, &i18n.Message{
ID: "UnsupportedGitService",
Other: `Niet-ondersteunde git-service`,
}, &i18n.Message{
ID: "createPullRequest",
Other: `maak een pull-aanvraag`,
}, &i18n.Message{
ID: "NoBranchOnRemote",
Other: `Deze branch bestaat niet op de remote. U moet het eerst naar de remote pushen.`,
}, &i18n.Message{
ID: "fetch",
Other: `fetch`,
}, &i18n.Message{
ID: "NoAutomaticGitFetchTitle",
Other: `Geen automatiese git fetch`,
}, &i18n.Message{
ID: "NoAutomaticGitFetchBody",
Other: `Lazygit kan niet "git fetch" uitvoeren in een privé repository, gebruik f in het branches paneel om "git fetch" manueel uit te voeren`,
}, &i18n.Message{
ID: "StageLines",
Other: `stage individuele hunks/lijnen`,
}, &i18n.Message{
ID: "FileStagingRequirements",
Other: `Kan alleen individuele lijnen stagen van getrackte bestanden met onstaged veranderingen`,
}, &i18n.Message{
ID: "StagingTitle",
Other: `Staging`,
}, &i18n.Message{
ID: "StageHunk",
Other: `stage hunk`,
}, &i18n.Message{
ID: "StageLine",
Other: `stage lijn`,
}, &i18n.Message{
ID: "EscapeStaging",
Other: `ga terug naar het bestanden paneel`,
}, &i18n.Message{
ID: "CantFindHunks",
Other: `Kan geen hunks vinden in deze patch`,
}, &i18n.Message{
ID: "CantFindHunk",
Other: `Kan geen hunk vinden`,
},
)
}

View File

@@ -39,9 +39,27 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "CommitMessage",
Other: "Commit message",
}, &i18n.Message{
ID: "CredentialsUsername",
Other: "Username",
}, &i18n.Message{
ID: "CredentialsPassword",
Other: "Password",
}, &i18n.Message{
ID: "PassUnameWrong",
Other: "Password and/or username wrong",
}, &i18n.Message{
ID: "CommitChanges",
Other: "commit changes",
}, &i18n.Message{
ID: "AmendLastCommit",
Other: "amend last commit",
}, &i18n.Message{
ID: "SureToAmend",
Other: "Are you sure you want to amend last commit? You can change commit message from commits panel.",
}, &i18n.Message{
ID: "NoCommitToAmend",
Other: "There's no commit to amend.",
}, &i18n.Message{
ID: "CommitChangesWithEditor",
Other: "commit changes using git editor",
@@ -132,12 +150,15 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "PushWait",
Other: "Pushing...",
}, &i18n.Message{
ID: "FetchWait",
Other: "Fetching...",
}, &i18n.Message{
ID: "FileNoMergeCons",
Other: "This file has no merge conflicts",
}, &i18n.Message{
ID: "SureResetHardHead",
Other: "Are you sure you want `reset --hard HEAD`? You may lose changes",
Other: "Are you sure you want `reset --hard HEAD` and `clean -fd`? You may lose changes",
}, &i18n.Message{
ID: "SureTo",
Other: "Are you sure you want to {{.deleteVerb}} {{.fileName}} (you will lose your changes)?",
@@ -167,7 +188,7 @@ func addEnglish(i18nObject *i18n.Bundle) error {
Other: "Are you sure you want to delete the branch {{.selectedBranchName}}?",
}, &i18n.Message{
ID: "ForceDeleteBranchMessage",
Other: "Are you sure you want to force delete the branch {{.selectedBranchName}}?",
Other: "{{.selectedBranchName}} is not fully merged. Are you sure you want to delete it?",
}, &i18n.Message{
ID: "CantMergeBranchIntoItself",
Other: "You cannot merge a branch into itself",
@@ -383,7 +404,7 @@ func addEnglish(i18nObject *i18n.Bundle) error {
Other: `refresh files`,
}, &i18n.Message{
ID: "resetHard",
Other: `reset hard`,
Other: `reset hard and remove untracked files`,
}, &i18n.Message{
ID: "mergeIntoCurrentBranch",
Other: `merge into currently checked out branch`,
@@ -393,6 +414,54 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "SwitchRepo",
Other: `switch to a recent repo`,
}, &i18n.Message{
ID: "UnsupportedGitService",
Other: `Unsupported git service`,
}, &i18n.Message{
ID: "createPullRequest",
Other: `create pull request`,
}, &i18n.Message{
ID: "NoBranchOnRemote",
Other: `This branch doesn't exist on remote. You need to push it to remote first.`,
}, &i18n.Message{
ID: "fetch",
Other: `fetch`,
}, &i18n.Message{
ID: "NoAutomaticGitFetchTitle",
Other: `No automatic git fetch`,
}, &i18n.Message{
ID: "NoAutomaticGitFetchBody",
Other: `Lazygit can't use "git fetch" in a private repo; use 'f' in the files panel to run "git fetch" manually`,
}, &i18n.Message{
ID: "StageLines",
Other: `stage individual hunks/lines`,
}, &i18n.Message{
ID: "FileStagingRequirements",
Other: `Can only stage individual lines for tracked files with unstaged changes`,
}, &i18n.Message{
ID: "StagingTitle",
Other: `Staging`,
}, &i18n.Message{
ID: "StageHunk",
Other: `stage hunk`,
}, &i18n.Message{
ID: "StageLine",
Other: `stage line`,
}, &i18n.Message{
ID: "EscapeStaging",
Other: `return to files panel`,
}, &i18n.Message{
ID: "CantFindHunks",
Other: `Could not find any hunks in this patch`,
}, &i18n.Message{
ID: "CantFindHunk",
Other: `Could not find hunk`,
}, &i18n.Message{
ID: "FastForward",
Other: `fast-forward this branch from its upstream`,
}, &i18n.Message{
ID: "Fetching",
Other: "fetching and fast-forwarding {{.from}} -> {{.to}} ...",
},
)
}

View File

@@ -16,10 +16,13 @@ func getDummyLog() *logrus.Entry {
log.Out = ioutil.Discard
return log.WithField("test", "test")
}
// TestNewLocalizer is a function.
func TestNewLocalizer(t *testing.T) {
assert.NotNil(t, NewLocalizer(getDummyLog()))
}
// TestDetectLanguage is a function.
func TestDetectLanguage(t *testing.T) {
type scenario struct {
langDetector func() (string, error)
@@ -46,6 +49,7 @@ func TestDetectLanguage(t *testing.T) {
}
}
// TestLocalizer is a function.
func TestLocalizer(t *testing.T) {
type scenario struct {
userLang string
@@ -76,7 +80,7 @@ func TestLocalizer(t *testing.T) {
},
}))
assert.Equal(t, "Diff", l.SLocalize("DiffTitle"))
assert.Equal(t, "Weet je zeker dat je branch test wil verwijderen?", l.TemplateLocalize("DeleteBranchMessage", Teml{"selectedBranchName": "test"}))
assert.Equal(t, "Weet je zeker dat je branch test wilt verwijderen?", l.TemplateLocalize("DeleteBranchMessage", Teml{"selectedBranchName": "test"}))
},
},
}

View File

@@ -29,9 +29,27 @@ func addPolish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "CommitMessage",
Other: "Wiadomość commita",
}, &i18n.Message{
ID: "CredentialsUsername",
Other: "Username",
}, &i18n.Message{
ID: "CredentialsPassword",
Other: "Password",
}, &i18n.Message{
ID: "PassUnameWrong",
Other: "Password and/or username wrong",
}, &i18n.Message{
ID: "CommitChanges",
Other: "commituj zmiany",
}, &i18n.Message{
ID: "AmendLastCommit",
Other: "zmień ostatnie zatwierdzenie",
}, &i18n.Message{
ID: "SureToAmend",
Other: "Czy na pewno chcesz zmienić ostatnie zatwierdzenie? Możesz zmienić komunikat zatwierdzenia z panelu zatwierdzeń.",
}, &i18n.Message{
ID: "NoCommitToAmend",
Other: "Nie ma zobowiązania do zmiany.",
}, &i18n.Message{
ID: "CommitChangesWithEditor",
Other: "commituj zmiany używając edytora z gita",
@@ -113,12 +131,15 @@ func addPolish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "PushWait",
Other: "Wypychanie zmian...",
}, &i18n.Message{
ID: "FetchWait",
Other: "Fetching...",
}, &i18n.Message{
ID: "FileNoMergeCons",
Other: "Ten plik nie powoduje konfliktów scalania",
}, &i18n.Message{
ID: "SureResetHardHead",
Other: "Jesteś pewny, że chcesz wykonać `reset --hard HEAD`? Możesz stracić wprowadzone zmiany",
Other: "Jesteś pewny, że chcesz wykonać `reset --hard HEAD` i `clean -fd`? Możesz stracić wprowadzone zmiany",
}, &i18n.Message{
ID: "SureTo",
Other: "Jesteś pewny, że chcesz {{.deleteVerb}} {{.fileName}} (stracisz swoje wprowadzone zmiany)?",
@@ -361,13 +382,55 @@ func addPolish(i18nObject *i18n.Bundle) error {
Other: `odśwież pliki`,
}, &i18n.Message{
ID: "resetHard",
Other: `zresetuj twardo`,
Other: `zresetuj twardo i usuń niepotwierdzone pliki`,
}, &i18n.Message{
ID: "mergeIntoCurrentBranch",
Other: `scal do obecnej gałęzi`,
}, &i18n.Message{
ID: "ConfirmQuit",
Other: `Na pewno chcesz wyjść z programu?`,
}, &i18n.Message{
ID: "UnsupportedGitService",
Other: `Nieobsługiwana usługa git`,
}, &i18n.Message{
ID: "createPullRequest",
Other: `utwórz żądanie wyciągnięcia`,
}, &i18n.Message{
ID: "NoBranchOnRemote",
Other: `Ta gałąź nie istnieje na zdalnym. Najpierw musisz go odepchnąć na odległość.`,
}, &i18n.Message{
ID: "fetch",
Other: `fetch`,
}, &i18n.Message{
ID: "NoAutomaticGitFetchTitle",
Other: `No automatic git fetch`,
}, &i18n.Message{
ID: "NoAutomaticGitFetchBody",
Other: `Lazygit can't use "git fetch" in a private repo use f in the branches panel to run "git fetch" manually`,
}, &i18n.Message{
ID: "StageLines",
Other: `zatwierdź pojedyncze linie`,
}, &i18n.Message{
ID: "FileStagingRequirements",
Other: `Można tylko zatwierdzić pojedyncze linie dla śledzonych plików z niezatwierdzonymi zmianami`,
}, &i18n.Message{
ID: "StagingTitle",
Other: `Zatwierdzanie`,
}, &i18n.Message{
ID: "StageHunk",
Other: `zatwierdź kawałek`,
}, &i18n.Message{
ID: "StageLine",
Other: `zatwierdź linię`,
}, &i18n.Message{
ID: "EscapeStaging",
Other: `wróć do panelu plików`,
}, &i18n.Message{
ID: "CantFindHunks",
Other: `Nie można znaleźć żadnych kawałków w tej łatce`,
}, &i18n.Message{
ID: "CantFindHunk",
Other: `Nie można znaleźć kawałka`,
},
)
}

View File

@@ -36,25 +36,24 @@ type Updaterer interface {
Update()
}
var (
projectUrl = "https://github.com/jesseduffield/lazygit"
const (
PROJECT_URL = "https://github.com/jesseduffield/lazygit"
)
// NewUpdater creates a new updater
func NewUpdater(log *logrus.Entry, config config.AppConfigurer, osCommand *commands.OSCommand, tr *i18n.Localizer) (*Updater, error) {
contextLogger := log.WithField("context", "updates")
updater := &Updater{
return &Updater{
Log: contextLogger,
Config: config,
OSCommand: osCommand,
Tr: tr,
}
return updater, nil
}, nil
}
func (u *Updater) getLatestVersionNumber() (string, error) {
req, err := http.NewRequest("GET", projectUrl+"/releases/latest", nil)
req, err := http.NewRequest("GET", PROJECT_URL+"/releases/latest", nil)
if err != nil {
return "", err
}
@@ -65,17 +64,16 @@ func (u *Updater) getLatestVersionNumber() (string, error) {
return "", err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
dec := json.NewDecoder(resp.Body)
data := struct {
TagName string `json:"tag_name"`
}{}
if err := dec.Decode(&data); err != nil {
return "", err
}
byt := []byte(body)
var dat map[string]interface{}
if err := json.Unmarshal(byt, &dat); err != nil {
return "", err
}
return dat["tag_name"].(string), nil
return data.TagName, nil
}
// RecordLastUpdateCheck records last time an update check was performed
@@ -225,14 +223,14 @@ func (u *Updater) getBinaryUrl(newVersion string) (string, error) {
}
url := fmt.Sprintf(
"%s/releases/download/%s/lazygit_%s_%s_%s.%s",
projectUrl,
PROJECT_URL,
newVersion,
newVersion[1:],
u.mappedOs(runtime.GOOS),
u.mappedArch(runtime.GOARCH),
extension,
)
u.Log.Info("url for latest release is " + url)
u.Log.Info("Url for latest release is " + url)
return url, nil
}
@@ -251,7 +249,7 @@ func (u *Updater) update(newVersion string) error {
if err != nil {
return err
}
u.Log.Info("updating with url " + rawUrl)
u.Log.Info("Updating with url " + rawUrl)
return u.downloadAndInstall(rawUrl)
}
@@ -267,7 +265,7 @@ func (u *Updater) downloadAndInstall(rawUrl string) error {
return err
}
defer os.RemoveAll(tempDir)
u.Log.Info("temp directory is " + tempDir)
u.Log.Info("Temp directory is " + tempDir)
// Get it!
if err := g.Get(tempDir, url); err != nil {
@@ -279,14 +277,14 @@ func (u *Updater) downloadAndInstall(rawUrl string) error {
if err != nil {
return err
}
u.Log.Info("binary path is " + binaryPath)
u.Log.Info("Binary path is " + binaryPath)
binaryName := filepath.Base(binaryPath)
u.Log.Info("binary name is " + binaryName)
u.Log.Info("Binary name is " + binaryName)
// Verify the main file exists
tempPath := filepath.Join(tempDir, binaryName)
u.Log.Info("temp path to binary is " + tempPath)
u.Log.Info("Temp path to binary is " + tempPath)
if _, err := os.Stat(tempPath); err != nil {
return err
}
@@ -296,7 +294,7 @@ func (u *Updater) downloadAndInstall(rawUrl string) error {
if err != nil {
return err
}
u.Log.Info("update complete!")
u.Log.Info("Update complete!")
return nil
}

View File

@@ -1,6 +1,7 @@
package utils
import (
"encoding/json"
"errors"
"fmt"
"log"
@@ -204,3 +205,39 @@ func getDisplayStringArrays(displayables []Displayable) [][]string {
}
return stringArrays
}
// IncludesString if the list contains the string
func IncludesString(list []string, a string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
// NextIndex returns the index of the element that comes after the given number
func NextIndex(numbers []int, currentNumber int) int {
for index, number := range numbers {
if number > currentNumber {
return index
}
}
return 0
}
// PrevIndex returns the index that comes before the given number, cycling if we reach the end
func PrevIndex(numbers []int, currentNumber int) int {
end := len(numbers) - 1
for i := end; i >= 0; i -= 1 {
if numbers[i] < currentNumber {
return i
}
}
return end
}
func AsJson(i interface{}) string {
bytes, _ := json.MarshalIndent(i, "", " ")
return string(bytes)
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/assert"
)
// TestSplitLines is a function.
func TestSplitLines(t *testing.T) {
type scenario struct {
multilineString string
@@ -36,6 +37,7 @@ func TestSplitLines(t *testing.T) {
}
}
// TestWithPadding is a function.
func TestWithPadding(t *testing.T) {
type scenario struct {
str string
@@ -61,6 +63,7 @@ func TestWithPadding(t *testing.T) {
}
}
// TestTrimTrailingNewline is a function.
func TestTrimTrailingNewline(t *testing.T) {
type scenario struct {
str string
@@ -83,6 +86,7 @@ func TestTrimTrailingNewline(t *testing.T) {
}
}
// TestNormalizeLinefeeds is a function.
func TestNormalizeLinefeeds(t *testing.T) {
type scenario struct {
byteArray []byte
@@ -116,6 +120,7 @@ func TestNormalizeLinefeeds(t *testing.T) {
}
}
// TestResolvePlaceholderString is a function.
func TestResolvePlaceholderString(t *testing.T) {
type scenario struct {
templateString string
@@ -169,6 +174,7 @@ func TestResolvePlaceholderString(t *testing.T) {
}
}
// TestDisplayArraysAligned is a function.
func TestDisplayArraysAligned(t *testing.T) {
type scenario struct {
input [][]string
@@ -197,10 +203,12 @@ type myDisplayable struct {
type myStruct struct{}
// GetDisplayStrings is a function.
func (d *myDisplayable) GetDisplayStrings() []string {
return d.strings
}
// TestGetDisplayStringArrays is a function.
func TestGetDisplayStringArrays(t *testing.T) {
type scenario struct {
input []Displayable
@@ -222,6 +230,7 @@ func TestGetDisplayStringArrays(t *testing.T) {
}
}
// TestRenderDisplayableList is a function.
func TestRenderDisplayableList(t *testing.T) {
type scenario struct {
input []Displayable
@@ -263,6 +272,7 @@ func TestRenderDisplayableList(t *testing.T) {
}
}
// TestRenderList is a function.
func TestRenderList(t *testing.T) {
type scenario struct {
input interface{}
@@ -301,6 +311,7 @@ func TestRenderList(t *testing.T) {
}
}
// TestGetPaddedDisplayStrings is a function.
func TestGetPaddedDisplayStrings(t *testing.T) {
type scenario struct {
stringArrays [][]string
@@ -321,6 +332,7 @@ func TestGetPaddedDisplayStrings(t *testing.T) {
}
}
// TestGetPadWidths is a function.
func TestGetPadWidths(t *testing.T) {
type scenario struct {
stringArrays [][]string
@@ -347,6 +359,7 @@ func TestGetPadWidths(t *testing.T) {
}
}
// TestMin is a function.
func TestMin(t *testing.T) {
type scenario struct {
a int
@@ -376,3 +389,142 @@ func TestMin(t *testing.T) {
assert.EqualValues(t, s.expected, Min(s.a, s.b))
}
}
// TestIncludesString is a function.
func TestIncludesString(t *testing.T) {
type scenario struct {
list []string
element string
expected bool
}
scenarios := []scenario{
{
[]string{"a", "b"},
"a",
true,
},
{
[]string{"a", "b"},
"c",
false,
},
{
[]string{"a", "b"},
"",
false,
},
{
[]string{""},
"",
true,
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, IncludesString(s.list, s.element))
}
}
func TestNextIndex(t *testing.T) {
type scenario struct {
testName string
list []int
element int
expected int
}
scenarios := []scenario{
{
// I'm not really fussed about how it behaves here
"no elements",
[]int{},
1,
0,
},
{
"one element",
[]int{1},
1,
0,
},
{
"two elements",
[]int{1, 2},
1,
1,
},
{
"two elements, giving second one",
[]int{1, 2},
2,
0,
},
{
"three elements, giving second one",
[]int{1, 2, 3},
2,
2,
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
assert.EqualValues(t, s.expected, NextIndex(s.list, s.element))
})
}
}
func TestPrevIndex(t *testing.T) {
type scenario struct {
testName string
list []int
element int
expected int
}
scenarios := []scenario{
{
// I'm not really fussed about how it behaves here
"no elements",
[]int{},
1,
-1,
},
{
"one element",
[]int{1},
1,
0,
},
{
"two elements",
[]int{1, 2},
1,
1,
},
{
"three elements, giving second one",
[]int{1, 2, 3},
2,
0,
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
assert.EqualValues(t, s.expected, PrevIndex(s.list, s.element))
})
}
}
func TestAsJson(t *testing.T) {
type myStruct struct {
a string
}
output := AsJson(&myStruct{a: "foo"})
// no idea why this is returning empty hashes but it's works in the app ¯\_(ツ)_/¯
assert.EqualValues(t, "{}", output)
}

5
scripts/bump_modules.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
GO111MODULE=on
mv go.mod /tmp/
go mod init

View File

@@ -1,54 +1,63 @@
// run:
// LANG=en go run generate_cheatsheet.go
// to generate Keybindings_en.md file in current directory
// change LANG to generate cheatsheet in different language (if supported)
// This "script" generates a file called Keybindings_{{.LANG}}.md
// in current working directory.
//
// The content of this generated file is a keybindings cheatsheet.
//
// To generate cheatsheet in english run:
// LANG=en go run scripts/generate_cheatsheet.go
package main
import (
"fmt"
"os"
"strings"
"github.com/jesseduffield/lazygit/pkg/app"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/utils"
"log"
"os"
"strings"
)
func main() {
appConfig, _ := config.NewAppConfig("", "", "", "", "", new(bool))
a, _ := app.NewApp(appConfig)
lang := a.Tr.GetLanguage()
name := "Keybindings_" + lang + ".md"
bindings := a.Gui.GetKeybindings()
padWidth := a.Gui.GetMaxKeyLength(bindings)
file, _ := os.Create(name)
current := "v"
content := ""
title := ""
file.WriteString("# Lazygit " + a.Tr.SLocalize("menu"))
for _, binding := range bindings {
if key := a.Gui.GetKey(binding); key != "" && (binding.Description != "" || key == "x") {
if binding.ViewName != current {
current = binding.ViewName
if current == "" {
title = a.Tr.SLocalize("GlobalTitle")
} else {
title = a.Tr.SLocalize(strings.Title(current) + "Title")
}
content = fmt.Sprintf("</pre>\n\n## %s\n<pre>\n", title)
file.WriteString(content)
}
// workaround to include menu keybinding in cheatsheet
// could not add this Description field directly to keybindings.go,
// because then menu key would be displayed in menu itself and that is undesirable
if key == "x" {
binding.Description = a.Tr.SLocalize("menu")
}
content = fmt.Sprintf("\t<kbd>%s</kbd>%s %s\n", key, strings.TrimPrefix(utils.WithPadding(key, padWidth), key), binding.Description)
file.WriteString(content)
}
func writeString(file *os.File, str string) {
_, err := file.WriteString(str)
if err != nil {
log.Fatal(err)
}
}
func getTitle(mApp *app.App, viewName string) string {
viewTitle := strings.Title(viewName) + "Title"
translatedTitle := mApp.Tr.SLocalize(viewTitle)
formattedTitle := fmt.Sprintf("\n## %s\n\n", translatedTitle)
return formattedTitle
}
func main() {
mConfig, _ := config.NewAppConfig("", "", "", "", "", new(bool))
mApp, _ := app.Setup(mConfig)
lang := mApp.Tr.GetLanguage()
file, _ := os.Create("Keybindings_" + lang + ".md")
current := ""
writeString(file, fmt.Sprintf("# Lazygit %s\n", mApp.Tr.SLocalize("menu")))
writeString(file, getTitle(mApp, "global"))
writeString(file, "<pre>\n")
for _, binding := range mApp.Gui.GetKeybindings() {
if binding.Description == "" {
continue
}
if binding.ViewName != current {
current = binding.ViewName
writeString(file, "</pre>\n")
writeString(file, getTitle(mApp, current))
writeString(file, "<pre>\n")
}
info := fmt.Sprintf(" <kbd>%s</kbd>: %s\n", binding.GetKey(), binding.Description)
writeString(file, info)
}
writeString(file, "</pre>\n")
}

View File

@@ -1,5 +1,5 @@
// call from project root with
// go run bin/push_new_patch.go
// go run scripts/push_new_patch/main.go
// goreleaser expects a $GITHUB_TOKEN env variable to be defined
// in order to push the release got github

25
test/hooks/pre-push Normal file
View File

@@ -0,0 +1,25 @@
#!/bin/bash
# test pre-push hook for testing the lazygit credentials view
#
# to enable, use:
# chmod +x .git/hooks/pre-push
#
# this will hang if you're using git from the command line, so only enable this
# when you are testing the credentials view in lazygit
exec < /dev/tty
echo -n "Username for 'github': "
read username
echo -n "Password for 'github': "
read password
if [ "$username" = "username" -a "$password" = "password" ]; then
echo "success"
exit 0
fi
>&2 echo "incorrect username/password"
exit 1

View File

@@ -1,35 +0,0 @@
#!/bin/bash
set -ex; rm -rf repo; mkdir repo; cd repo
git init
git config user.email "test@example.com"
git config user.name "Lazygit Tester"
echo "deleted" > deleted_staged
echo "deleted_unstaged" > deleted_unstaged
echo "modified_staged" > modified_staged
echo "modified_unstaged" > modified_unstaged
echo "renamed" > renamed_before
git add .
git commit -m "files to delete"
rm deleted_staged
rm deleted_unstaged
rm renamed_before
echo "renamed" > renamed_after
echo "more" >> modified_staged
echo "more" >> modified_unstaged
echo "untracked_staged" > untracked_staged
echo "untracked_unstaged" > untracked_unstaged
echo "blah" > "file with space staged"
echo "blah" > "file with space unstaged"
echo "same name as branch" > master
git add deleted_staged
git add modified_staged
git add untracked_staged
git add "file with space staged"
git add renamed_before
git add renamed_after

View File

@@ -148,7 +148,6 @@ func (v *View) setRune(x, y int, ch rune, fgColor, bgColor Attribute) error {
if x < 0 || x >= maxX || y < 0 || y >= maxY {
return errors.New("invalid point")
}
var (
ry, rcy int
err error
@@ -270,12 +269,19 @@ func (v *View) parseInput(ch rune) []cell {
if isEscape {
return nil
}
c := cell{
fgColor: v.ei.curFgColor,
bgColor: v.ei.curBgColor,
chr: ch,
repeatCount := 1
if ch == '\t' {
ch = ' '
repeatCount = 4
}
for i := 0; i < repeatCount; i++ {
c := cell{
fgColor: v.ei.curFgColor,
bgColor: v.ei.curBgColor,
chr: ch,
}
cells = append(cells, c)
}
cells = append(cells, c)
}
return cells
@@ -447,8 +453,8 @@ func (v *View) ViewBufferLines() []string {
return lines
}
func (v *View) ViewLinesHeight() int {
return len(v.viewLines)
func (v *View) LinesHeight() int {
return len(v.lines)
}
// ViewBuffer returns a string with the contents of the view's buffer that is
@@ -533,7 +539,7 @@ func lineWrap(line []cell, columns int) [][]cell {
n += rw
if n > columns {
n = rw
lines = append(lines, line[offset:i-1])
lines = append(lines, line[offset:i])
offset = i
}
}

23
vendor/github.com/jesseduffield/pty/License generated vendored Normal file
View File

@@ -0,0 +1,23 @@
Copyright (c) 2011 Keith Rarick
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall
be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

16
vendor/github.com/jesseduffield/pty/doc.go generated vendored Normal file
View File

@@ -0,0 +1,16 @@
// Package pty provides functions for working with Unix terminals.
package pty
import (
"errors"
"os"
)
// ErrUnsupported is returned if a function is not
// available on the current platform.
var ErrUnsupported = errors.New("unsupported")
// Opens a pty and its corresponding tty.
func Open() (pty, tty *os.File, err error) {
return open()
}

13
vendor/github.com/jesseduffield/pty/ioctl.go generated vendored Normal file
View File

@@ -0,0 +1,13 @@
// +build !windows
package pty
import "syscall"
func ioctl(fd, cmd, ptr uintptr) error {
_, _, e := syscall.Syscall(syscall.SYS_IOCTL, fd, cmd, ptr)
if e != 0 {
return e
}
return nil
}

39
vendor/github.com/jesseduffield/pty/ioctl_bsd.go generated vendored Normal file
View File

@@ -0,0 +1,39 @@
// +build darwin dragonfly freebsd netbsd openbsd
package pty
// from <sys/ioccom.h>
const (
_IOC_VOID uintptr = 0x20000000
_IOC_OUT uintptr = 0x40000000
_IOC_IN uintptr = 0x80000000
_IOC_IN_OUT uintptr = _IOC_OUT | _IOC_IN
_IOC_DIRMASK = _IOC_VOID | _IOC_OUT | _IOC_IN
_IOC_PARAM_SHIFT = 13
_IOC_PARAM_MASK = (1 << _IOC_PARAM_SHIFT) - 1
)
func _IOC_PARM_LEN(ioctl uintptr) uintptr {
return (ioctl >> 16) & _IOC_PARAM_MASK
}
func _IOC(inout uintptr, group byte, ioctl_num uintptr, param_len uintptr) uintptr {
return inout | (param_len&_IOC_PARAM_MASK)<<16 | uintptr(group)<<8 | ioctl_num
}
func _IO(group byte, ioctl_num uintptr) uintptr {
return _IOC(_IOC_VOID, group, ioctl_num, 0)
}
func _IOR(group byte, ioctl_num uintptr, param_len uintptr) uintptr {
return _IOC(_IOC_OUT, group, ioctl_num, param_len)
}
func _IOW(group byte, ioctl_num uintptr, param_len uintptr) uintptr {
return _IOC(_IOC_IN, group, ioctl_num, param_len)
}
func _IOWR(group byte, ioctl_num uintptr, param_len uintptr) uintptr {
return _IOC(_IOC_IN_OUT, group, ioctl_num, param_len)
}

65
vendor/github.com/jesseduffield/pty/pty_darwin.go generated vendored Normal file
View File

@@ -0,0 +1,65 @@
package pty
import (
"errors"
"os"
"syscall"
"unsafe"
)
func open() (pty, tty *os.File, err error) {
pFD, err := syscall.Open("/dev/ptmx", syscall.O_RDWR|syscall.O_CLOEXEC, 0)
if err != nil {
return nil, nil, err
}
p := os.NewFile(uintptr(pFD), "/dev/ptmx")
// In case of error after this point, make sure we close the ptmx fd.
defer func() {
if err != nil {
_ = p.Close() // Best effort.
}
}()
sname, err := ptsname(p)
if err != nil {
return nil, nil, err
}
if err := grantpt(p); err != nil {
return nil, nil, err
}
if err := unlockpt(p); err != nil {
return nil, nil, err
}
t, err := os.OpenFile(sname, os.O_RDWR, 0)
if err != nil {
return nil, nil, err
}
return p, t, nil
}
func ptsname(f *os.File) (string, error) {
n := make([]byte, _IOC_PARM_LEN(syscall.TIOCPTYGNAME))
err := ioctl(f.Fd(), syscall.TIOCPTYGNAME, uintptr(unsafe.Pointer(&n[0])))
if err != nil {
return "", err
}
for i, c := range n {
if c == 0 {
return string(n[:i]), nil
}
}
return "", errors.New("TIOCPTYGNAME string not NUL-terminated")
}
func grantpt(f *os.File) error {
return ioctl(f.Fd(), syscall.TIOCPTYGRANT, 0)
}
func unlockpt(f *os.File) error {
return ioctl(f.Fd(), syscall.TIOCPTYUNLK, 0)
}

80
vendor/github.com/jesseduffield/pty/pty_dragonfly.go generated vendored Normal file
View File

@@ -0,0 +1,80 @@
package pty
import (
"errors"
"os"
"strings"
"syscall"
"unsafe"
)
// same code as pty_darwin.go
func open() (pty, tty *os.File, err error) {
p, err := os.OpenFile("/dev/ptmx", os.O_RDWR, 0)
if err != nil {
return nil, nil, err
}
// In case of error after this point, make sure we close the ptmx fd.
defer func() {
if err != nil {
_ = p.Close() // Best effort.
}
}()
sname, err := ptsname(p)
if err != nil {
return nil, nil, err
}
if err := grantpt(p); err != nil {
return nil, nil, err
}
if err := unlockpt(p); err != nil {
return nil, nil, err
}
t, err := os.OpenFile(sname, os.O_RDWR, 0)
if err != nil {
return nil, nil, err
}
return p, t, nil
}
func grantpt(f *os.File) error {
_, err := isptmaster(f.Fd())
return err
}
func unlockpt(f *os.File) error {
_, err := isptmaster(f.Fd())
return err
}
func isptmaster(fd uintptr) (bool, error) {
err := ioctl(fd, syscall.TIOCISPTMASTER, 0)
return err == nil, err
}
var (
emptyFiodgnameArg fiodgnameArg
ioctl_FIODNAME = _IOW('f', 120, unsafe.Sizeof(emptyFiodgnameArg))
)
func ptsname(f *os.File) (string, error) {
name := make([]byte, _C_SPECNAMELEN)
fa := fiodgnameArg{Name: (*byte)(unsafe.Pointer(&name[0])), Len: _C_SPECNAMELEN, Pad_cgo_0: [4]byte{0, 0, 0, 0}}
err := ioctl(f.Fd(), ioctl_FIODNAME, uintptr(unsafe.Pointer(&fa)))
if err != nil {
return "", err
}
for i, c := range name {
if c == 0 {
s := "/dev/" + string(name[:i])
return strings.Replace(s, "ptm", "pts", -1), nil
}
}
return "", errors.New("TIOCPTYGNAME string not NUL-terminated")
}

78
vendor/github.com/jesseduffield/pty/pty_freebsd.go generated vendored Normal file
View File

@@ -0,0 +1,78 @@
package pty
import (
"errors"
"os"
"syscall"
"unsafe"
)
func posixOpenpt(oflag int) (fd int, err error) {
r0, _, e1 := syscall.Syscall(syscall.SYS_POSIX_OPENPT, uintptr(oflag), 0, 0)
fd = int(r0)
if e1 != 0 {
err = e1
}
return fd, err
}
func open() (pty, tty *os.File, err error) {
fd, err := posixOpenpt(syscall.O_RDWR | syscall.O_CLOEXEC)
if err != nil {
return nil, nil, err
}
p := os.NewFile(uintptr(fd), "/dev/pts")
// In case of error after this point, make sure we close the pts fd.
defer func() {
if err != nil {
_ = p.Close() // Best effort.
}
}()
sname, err := ptsname(p)
if err != nil {
return nil, nil, err
}
t, err := os.OpenFile("/dev/"+sname, os.O_RDWR, 0)
if err != nil {
return nil, nil, err
}
return p, t, nil
}
func isptmaster(fd uintptr) (bool, error) {
err := ioctl(fd, syscall.TIOCPTMASTER, 0)
return err == nil, err
}
var (
emptyFiodgnameArg fiodgnameArg
ioctlFIODGNAME = _IOW('f', 120, unsafe.Sizeof(emptyFiodgnameArg))
)
func ptsname(f *os.File) (string, error) {
master, err := isptmaster(f.Fd())
if err != nil {
return "", err
}
if !master {
return "", syscall.EINVAL
}
const n = _C_SPECNAMELEN + 1
var (
buf = make([]byte, n)
arg = fiodgnameArg{Len: n, Buf: (*byte)(unsafe.Pointer(&buf[0]))}
)
if err := ioctl(f.Fd(), ioctlFIODGNAME, uintptr(unsafe.Pointer(&arg))); err != nil {
return "", err
}
for i, c := range buf {
if c == 0 {
return string(buf[:i]), nil
}
}
return "", errors.New("FIODGNAME string not NUL-terminated")
}

51
vendor/github.com/jesseduffield/pty/pty_linux.go generated vendored Normal file
View File

@@ -0,0 +1,51 @@
package pty
import (
"os"
"strconv"
"syscall"
"unsafe"
)
func open() (pty, tty *os.File, err error) {
p, err := os.OpenFile("/dev/ptmx", os.O_RDWR, 0)
if err != nil {
return nil, nil, err
}
// In case of error after this point, make sure we close the ptmx fd.
defer func() {
if err != nil {
_ = p.Close() // Best effort.
}
}()
sname, err := ptsname(p)
if err != nil {
return nil, nil, err
}
if err := unlockpt(p); err != nil {
return nil, nil, err
}
t, err := os.OpenFile(sname, os.O_RDWR|syscall.O_NOCTTY, 0)
if err != nil {
return nil, nil, err
}
return p, t, nil
}
func ptsname(f *os.File) (string, error) {
var n _C_uint
err := ioctl(f.Fd(), syscall.TIOCGPTN, uintptr(unsafe.Pointer(&n)))
if err != nil {
return "", err
}
return "/dev/pts/" + strconv.Itoa(int(n)), nil
}
func unlockpt(f *os.File) error {
var u _C_int
// use TIOCSPTLCK with a zero valued arg to clear the slave pty lock
return ioctl(f.Fd(), syscall.TIOCSPTLCK, uintptr(unsafe.Pointer(&u)))
}

33
vendor/github.com/jesseduffield/pty/pty_openbsd.go generated vendored Normal file
View File

@@ -0,0 +1,33 @@
package pty
import (
"os"
"syscall"
"unsafe"
)
func open() (pty, tty *os.File, err error) {
/*
* from ptm(4):
* The PTMGET command allocates a free pseudo terminal, changes its
* ownership to the caller, revokes the access privileges for all previous
* users, opens the file descriptors for the master and slave devices and
* returns them to the caller in struct ptmget.
*/
p, err := os.OpenFile("/dev/ptm", os.O_RDWR|syscall.O_CLOEXEC, 0)
if err != nil {
return nil, nil, err
}
defer p.Close()
var ptm ptmget
if err := ioctl(p.Fd(), uintptr(ioctl_PTMGET), uintptr(unsafe.Pointer(&ptm))); err != nil {
return nil, nil, err
}
pty = os.NewFile(uintptr(ptm.Cfd), "/dev/ptm")
tty = os.NewFile(uintptr(ptm.Sfd), "/dev/ptm")
return pty, tty, nil
}

11
vendor/github.com/jesseduffield/pty/pty_unsupported.go generated vendored Normal file
View File

@@ -0,0 +1,11 @@
// +build !linux,!darwin,!freebsd,!dragonfly,!openbsd
package pty
import (
"os"
)
func open() (pty, tty *os.File, err error) {
return nil, nil, ErrUnsupported
}

54
vendor/github.com/jesseduffield/pty/run.go generated vendored Normal file
View File

@@ -0,0 +1,54 @@
// +build !windows
package pty
import (
"os"
"os/exec"
"syscall"
)
// Start assigns a pseudo-terminal tty os.File to c.Stdin, c.Stdout,
// and c.Stderr, calls c.Start, and returns the File of the tty's
// corresponding pty.
func Start(c *exec.Cmd) (pty *os.File, err error) {
return StartWithSize(c, nil)
}
// StartWithSize assigns a pseudo-terminal tty os.File to c.Stdin, c.Stdout,
// and c.Stderr, calls c.Start, and returns the File of the tty's
// corresponding pty.
//
// This will resize the pty to the specified size before starting the command
func StartWithSize(c *exec.Cmd, sz *Winsize) (pty *os.File, err error) {
pty, tty, err := Open()
if err != nil {
return nil, err
}
defer tty.Close()
if sz != nil {
err = Setsize(pty, sz)
if err != nil {
pty.Close()
return nil, err
}
}
if c.Stdout == nil {
c.Stdout = tty
}
if c.Stderr == nil {
c.Stderr = tty
}
c.Stdin = tty
if c.SysProcAttr == nil {
c.SysProcAttr = &syscall.SysProcAttr{}
}
c.SysProcAttr.Setctty = true
c.SysProcAttr.Setsid = true
err = c.Start()
if err != nil {
pty.Close()
return nil, err
}
return pty, err
}

10
vendor/github.com/jesseduffield/pty/types.go generated vendored Normal file
View File

@@ -0,0 +1,10 @@
// +build ignore
package pty
import "C"
type (
_C_int C.int
_C_uint C.uint
)

17
vendor/github.com/jesseduffield/pty/types_dragonfly.go generated vendored Normal file
View File

@@ -0,0 +1,17 @@
// +build ignore
package pty
/*
#define _KERNEL
#include <sys/conf.h>
#include <sys/param.h>
#include <sys/filio.h>
*/
import "C"
const (
_C_SPECNAMELEN = C.SPECNAMELEN /* max length of devicename */
)
type fiodgnameArg C.struct_fiodname_args

15
vendor/github.com/jesseduffield/pty/types_freebsd.go generated vendored Normal file
View File

@@ -0,0 +1,15 @@
// +build ignore
package pty
/*
#include <sys/param.h>
#include <sys/filio.h>
*/
import "C"
const (
_C_SPECNAMELEN = C.SPECNAMELEN /* max length of devicename */
)
type fiodgnameArg C.struct_fiodgname_arg

14
vendor/github.com/jesseduffield/pty/types_openbsd.go generated vendored Normal file
View File

@@ -0,0 +1,14 @@
// +build ignore
package pty
/*
#include <sys/time.h>
#include <stdlib.h>
#include <sys/tty.h>
*/
import "C"
type ptmget C.struct_ptmget
var ioctl_PTMGET = C.PTMGET

64
vendor/github.com/jesseduffield/pty/util.go generated vendored Normal file
View File

@@ -0,0 +1,64 @@
// +build !windows
package pty
import (
"os"
"syscall"
"unsafe"
)
// InheritSize applies the terminal size of master to slave. This should be run
// in a signal handler for syscall.SIGWINCH to automatically resize the slave when
// the master receives a window size change notification.
func InheritSize(master, slave *os.File) error {
size, err := GetsizeFull(master)
if err != nil {
return err
}
err = Setsize(slave, size)
if err != nil {
return err
}
return nil
}
// Setsize resizes t to s.
func Setsize(t *os.File, ws *Winsize) error {
return windowRectCall(ws, t.Fd(), syscall.TIOCSWINSZ)
}
// GetsizeFull returns the full terminal size description.
func GetsizeFull(t *os.File) (size *Winsize, err error) {
var ws Winsize
err = windowRectCall(&ws, t.Fd(), syscall.TIOCGWINSZ)
return &ws, err
}
// Getsize returns the number of rows (lines) and cols (positions
// in each line) in terminal t.
func Getsize(t *os.File) (rows, cols int, err error) {
ws, err := GetsizeFull(t)
return int(ws.Rows), int(ws.Cols), err
}
// Winsize describes the terminal size.
type Winsize struct {
Rows uint16 // ws_row: Number of rows (in cells)
Cols uint16 // ws_col: Number of columns (in cells)
X uint16 // ws_xpixel: Width in pixels
Y uint16 // ws_ypixel: Height in pixels
}
func windowRectCall(ws *Winsize, fd, a2 uintptr) error {
_, _, errno := syscall.Syscall(
syscall.SYS_IOCTL,
fd,
a2,
uintptr(unsafe.Pointer(ws)),
)
if errno != 0 {
return syscall.Errno(errno)
}
return nil
}

9
vendor/github.com/jesseduffield/pty/ztypes_386.go generated vendored Normal file
View File

@@ -0,0 +1,9 @@
// Created by cgo -godefs - DO NOT EDIT
// cgo -godefs types.go
package pty
type (
_C_int int32
_C_uint uint32
)

9
vendor/github.com/jesseduffield/pty/ztypes_amd64.go generated vendored Normal file
View File

@@ -0,0 +1,9 @@
// Created by cgo -godefs - DO NOT EDIT
// cgo -godefs types.go
package pty
type (
_C_int int32
_C_uint uint32
)

9
vendor/github.com/jesseduffield/pty/ztypes_arm.go generated vendored Normal file
View File

@@ -0,0 +1,9 @@
// Created by cgo -godefs - DO NOT EDIT
// cgo -godefs types.go
package pty
type (
_C_int int32
_C_uint uint32
)

11
vendor/github.com/jesseduffield/pty/ztypes_arm64.go generated vendored Normal file
View File

@@ -0,0 +1,11 @@
// Created by cgo -godefs - DO NOT EDIT
// cgo -godefs types.go
// +build arm64
package pty
type (
_C_int int32
_C_uint uint32
)

View File

@@ -0,0 +1,14 @@
// Created by cgo -godefs - DO NOT EDIT
// cgo -godefs types_dragonfly.go
package pty
const (
_C_SPECNAMELEN = 0x3f
)
type fiodgnameArg struct {
Name *byte
Len uint32
Pad_cgo_0 [4]byte
}

View File

@@ -0,0 +1,13 @@
// Created by cgo -godefs - DO NOT EDIT
// cgo -godefs types_freebsd.go
package pty
const (
_C_SPECNAMELEN = 0x3f
)
type fiodgnameArg struct {
Len int32
Buf *byte
}

View File

@@ -0,0 +1,14 @@
// Created by cgo -godefs - DO NOT EDIT
// cgo -godefs types_freebsd.go
package pty
const (
_C_SPECNAMELEN = 0x3f
)
type fiodgnameArg struct {
Len int32
Pad_cgo_0 [4]byte
Buf *byte
}

View File

@@ -0,0 +1,13 @@
// Created by cgo -godefs - DO NOT EDIT
// cgo -godefs types_freebsd.go
package pty
const (
_C_SPECNAMELEN = 0x3f
)
type fiodgnameArg struct {
Len int32
Buf *byte
}

12
vendor/github.com/jesseduffield/pty/ztypes_mipsx.go generated vendored Normal file
View File

@@ -0,0 +1,12 @@
// Created by cgo -godefs - DO NOT EDIT
// cgo -godefs types.go
// +build linux
// +build mips mipsle mips64 mips64le
package pty
type (
_C_int int32
_C_uint uint32
)

View File

@@ -0,0 +1,13 @@
// Created by cgo -godefs - DO NOT EDIT
// cgo -godefs types_openbsd.go
package pty
type ptmget struct {
Cfd int32
Sfd int32
Cn [16]int8
Sn [16]int8
}
var ioctl_PTMGET = 0x40287401

View File

@@ -0,0 +1,13 @@
// Created by cgo -godefs - DO NOT EDIT
// cgo -godefs types_openbsd.go
package pty
type ptmget struct {
Cfd int32
Sfd int32
Cn [16]int8
Sn [16]int8
}
var ioctl_PTMGET = 0x40287401

11
vendor/github.com/jesseduffield/pty/ztypes_ppc64.go generated vendored Normal file
View File

@@ -0,0 +1,11 @@
// +build ppc64
// Created by cgo -godefs - DO NOT EDIT
// cgo -godefs types.go
package pty
type (
_C_int int32
_C_uint uint32
)

11
vendor/github.com/jesseduffield/pty/ztypes_ppc64le.go generated vendored Normal file
View File

@@ -0,0 +1,11 @@
// +build ppc64le
// Created by cgo -godefs - DO NOT EDIT
// cgo -godefs types.go
package pty
type (
_C_int int32
_C_uint uint32
)

11
vendor/github.com/jesseduffield/pty/ztypes_s390x.go generated vendored Normal file
View File

@@ -0,0 +1,11 @@
// +build s390x
// Created by cgo -godefs - DO NOT EDIT
// cgo -godefs types.go
package pty
type (
_C_int int32
_C_uint uint32
)