Compare commits

...

904 Commits

Author SHA1 Message Date
Jesse Duffield
ac5088eee6 allow both enter and space to execute menu item 2019-03-03 23:18:28 +11:00
Jesse Duffield
e36899d5c5 prevent crashes when scrolling up 2019-03-03 23:08:07 +11:00
Jesse Duffield
403526bc50 bump go mod 2019-03-03 16:15:52 +11:00
Jesse Duffield
a5d27764cd support user configuring mouse events to be enabled 2019-03-03 16:15:20 +11:00
Jesse Duffield
43758cbb5f i18n for rebase loading states 2019-03-03 16:11:20 +11:00
Jesse Duffield
0079015102 distinguish between inline and non-inline merge conflicts 2019-03-03 15:58:01 +11:00
Jesse Duffield
7a2176f479 acknowledge 'DU' statuses as being merge conflicts 2019-03-03 15:48:16 +11:00
Jesse Duffield
e0bdfad63a don't crash if we have no lines to stage 2019-03-03 15:48:01 +11:00
Jesse Duffield
f07fc31f8b fixup layout issue that was causing crashes when the window was too small 2019-03-03 15:34:53 +11:00
Jesse Duffield
4bb577ab7d show loading status for rebasing events 2019-03-03 15:21:33 +11:00
Jesse Duffield
8305d8e72f hide donate button if mouse events are disabled 2019-03-03 15:21:20 +11:00
Jesse Duffield
f68166e858 bump gocui to stop polling events after closing the gui when switching to a subprocess 2019-03-03 14:20:25 +11:00
Jesse Duffield
8925b161a7 windows support for skipping the editor 2019-03-03 12:44:10 +11:00
Jesse Duffield
0a1298765c use sh intead of bash for the sake of testing on the docker image 2019-03-02 21:31:48 +11:00
Jesse Duffield
273678f081 fix issue where you couldn't rearrange commits while rebasing onto a branch 2019-03-02 21:31:48 +11:00
Jesse Duffield
790235f64b add another match on the error message to tell us we've encountered merge conflicts 2019-03-02 21:31:48 +11:00
Jesse Duffield
abc0f7f0aa copy lazygit directory into docker container 2019-03-02 21:31:48 +11:00
Jesse Duffield
ab81f27fc7 don't show stack trace if lazygit is started outside of a git repo 2019-03-02 21:31:48 +11:00
Jesse Duffield
dbb01b028d populate dutch and polish i18n files with new messages 2019-03-02 21:31:48 +11:00
Jesse Duffield
0c886eddfb Revert "remove old rebase code now that we're only ever interactively rebasing"
This reverts commit 1a19b1412d.
2019-03-02 20:00:26 +11:00
Jesse Duffield
399346c2ee disable mouse feature until its ready 2019-03-02 20:00:17 +11:00
Jesse Duffield
7a170bbccf extend cheatsheet generator to contain context based keybindings 2019-03-02 19:05:21 +11:00
Jesse Duffield Duffield
8c0ea8f45f mouse support 2019-03-02 17:49:30 +11:00
Jesse Duffield
afbc028ad6 revert to the old keybinding for stash: I don't want anybody accidentally deleting changes they are trying to stash 2019-03-02 17:46:56 +11:00
Jesse Duffield
e331dfcaf8 update i18n 2019-03-02 17:46:56 +11:00
Jesse Duffield
1337f6e76a appease golangci 2019-03-02 17:45:53 +11:00
Jesse Duffield
4de31da4be fix up tests
This fixes up some git and oscommand tests, and pulls some tests into commit_list_builder_test.go

I've also made the NewDummyBlah functions public so that I didn't need to duplicate them across packages
I've also given OSCommand a SetCommand() method for setting the command on the struct
I've also created a file utils.go in the test package for creating convient 'CommandSwapper's, which
basically enable you to assert a sequence of commands on the command line, and swap each one out for
a different one to actually be executed
2019-03-02 13:39:09 +11:00
Jesse Duffield Duffield
23c51ba708 cleanup 2019-02-24 18:34:18 +11:00
Jesse Duffield Duffield
19a3ac603d improve script for making a test repo 2019-02-24 17:54:56 +11:00
Jesse Duffield Duffield
f4938deaae change type of cherryPickedCommits from []string to []*Commit 2019-02-24 17:34:19 +11:00
Jesse Duffield Duffield
639df512f3 decolorise strings before calculating padwidths 2019-02-24 17:05:17 +11:00
Jesse Duffield Duffield
a8858cbd12 support cherry picking commits 2019-02-24 13:51:52 +11:00
Jesse Duffield Duffield
1a19b1412d remove old rebase code now that we're only ever interactively rebasing 2019-02-24 11:03:14 +11:00
Jesse Duffield Duffield
95d451e59a Make it easier to run sync/async commands, switch to interactive rebase when rebasing on branches 2019-02-24 10:58:15 +11:00
Jesse Duffield
6c1d2d45ef some i18n and restricting rewording during interactive rebase 2019-02-24 09:42:35 +11:00
Jesse Duffield
f6b3a9b184 rearranging todo items while interactively rebasing 2019-02-24 09:42:34 +11:00
Jesse Duffield
cdc50e8557 more support for files with spaces 2019-02-24 09:42:34 +11:00
Jesse Duffield
0173fdb9df support file renames 2019-02-24 09:42:32 +11:00
Jesse Duffield
9661ea04f3 wrap amend command in a confirmation 2019-02-20 19:46:27 +11:00
Jesse Duffield
0228e25084 work towards more interactive rebase options 2019-02-19 23:36:36 +11:00
Jesse Duffield
935f774834 don't autostash when editing 2019-02-19 09:34:24 +11:00
Jesse Duffield
dcc7855fd0 pull commit list builder functions into their own builder struct 2019-02-19 09:18:30 +11:00
Jesse Duffield
a8e22ed82f show interactive rebase commits that are yet to go 2019-02-19 09:03:29 +11:00
Jesse Duffield
d44638130c add various interactive rebase commands 2019-02-18 23:27:54 +11:00
Jesse Duffield
76a27f417f rename any commit 2019-02-18 21:29:43 +11:00
Jesse Duffield
adc2529019 dealing better with errors at the top level 2019-02-18 19:42:23 +11:00
Jesse Duffield
43ab7318d3 remove HasMergeConflicts struct instance variables 2019-02-18 19:28:02 +11:00
Jesse Duffield
cb372d469f fix golangci errors 2019-02-16 21:30:29 +11:00
Jesse Duffield
88ba6efdd5 remove outdated TODO 2019-02-16 21:20:10 +11:00
Jesse Duffield
e011e9bc42 more work on rebasing feature 2019-02-16 21:01:17 +11:00
Jesse Duffield
ad93b4c863 consider whether the view has focus when rendering the contents of a view 2019-02-16 15:17:44 +11:00
Jesse Duffield
198cbee498 introduce panel contexts and more work on rebasing 2019-02-16 12:07:27 +11:00
Jesse Duffield
daca07eaca add loading panel 2019-02-16 12:03:22 +11:00
Jesse Duffield
34acaf7ac4 support users with gotest for coloured test output 2019-02-16 11:35:35 +11:00
Jesse Duffield
d967f65329 fix git tests 2019-02-16 11:24:47 +11:00
Jesse Duffield
306ac41fd8 bump gocui to support loader animations on views 2019-02-15 20:54:03 +11:00
Jesse Duffield
c101993405 post-merge cleanup 2019-02-11 22:47:14 +11:00
Jesse Duffield
6430ab6ac9 Merge branch 'master' into feature/rebasing 2019-02-11 22:46:27 +11:00
Jesse Duffield
e09f3905e9 update go.mod 2019-02-11 22:39:17 +11:00
Jesse Duffield
53e73313a2 bump gocui to version that uses go-errors as well 2019-02-11 22:39:17 +11:00
Jesse Duffield
0891797bf8 bump dep to include go-errors package 2019-02-11 22:39:17 +11:00
Jesse Duffield
cfe3605e6b use go-errors package to display stacktrace of errors that cause panics 2019-02-11 22:39:17 +11:00
Jesse Duffield
75ab8ec4d9 catch rebase errors and show in error panels 2019-02-11 21:29:47 +11:00
Jesse Duffield
77faf85cfc post-merge cleanup 2019-02-11 21:07:12 +11:00
Jesse Duffield
3d343e9b57 Merge branch 'master' into feature/rebasing 2019-02-11 21:02:53 +11:00
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
Jesse Duffield
a365615490 only use subprocess for merging, not rebasing 2018-12-11 22:16:48 +11:00
Jesse Duffield
9489a94473 Make merge panel its own panel 2018-12-11 22:02:12 +11:00
Jesse Duffield
e0ff46fe53 more work on rebasing including visual indicators 2018-12-11 09:39:54 +11:00
Glenn Vriesman
cce6f405a5 Making ci happier 2018-12-11 09:39:54 +11:00
Glenn Vriesman
e39d2ed44b Added check to invoke continue/refresh 2018-12-11 09:39:54 +11:00
Glenn Vriesman
7a7e885773 Added rebase support commands 2018-12-11 09:39:54 +11:00
Glenn Vriesman
34fd18a395 Error handling 2018-12-11 09:39:54 +11:00
Glenn Vriesman
a1ee11e54e Added error check to satisfy ci 2018-12-11 09:39:54 +11:00
Glenn Vriesman
7b850c56c4 Added some translations 2018-12-11 09:39:54 +11:00
Glenn Vriesman
88c01c1ded Added rebase keybinding 2018-12-11 09:39:54 +11:00
Glenn Vriesman
27994f7de8 Added rebase handler 2018-12-11 09:39:54 +11:00
Glenn Vriesman
670f0e37c7 Added rebase functions 2018-12-11 09:39:54 +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
Jesse Duffield
3101c50582 Merge pull request #297 from jesseduffield/hotfix/remove-files
add removeAll to git
2018-09-21 09:25:55 +10:00
Jesse Duffield
70ee4faf15 add removeAll to git 2018-09-21 09:23:00 +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
7b90d2496b Merge pull request #294 from jesseduffield/feature/recent-repos
Recent Repos Menu
2018-09-19 20:22:36 +10:00
Jesse Duffield
0367399cf3 bump deps to use forked termbox which doesn't crash as easily 2018-09-19 20:16:22 +10:00
Jesse Duffield
3072c93e13 Merge pull request #280 from jesseduffield/hotfix/cursor-positioning
Fix cursor positioning bugs
2018-09-19 19:55:14 +10:00
Jesse Duffield
4ea446205c Merge branch 'hotfix/cursor-positioning' into feature/recent-repos 2018-09-19 19:32:11 +10:00
Jesse Duffield
5a76b57952 one more spec to increase coverage 2018-09-19 19:31:29 +10:00
Jesse Duffield
6de291ff44 Merge branch 'hotfix/cursor-positioning' into feature/recent-repos 2018-09-19 19:23:42 +10:00
Jesse Duffield
64f0eeb42e fix specs 2018-09-19 19:23:31 +10:00
Jesse Duffield
baa9eff318 Merge branch 'hotfix/cursor-positioning' into feature/recent-repos 2018-09-19 19:17:05 +10:00
Jesse Duffield
fcaf4e339c fix specs 2018-09-19 19:16:55 +10:00
Jesse Duffield
e91fb21233 add recent repos menu option 2018-09-19 19:15:29 +10:00
Jesse Duffield
99a6439641 Merge branch 'master' into hotfix/cursor-positioning 2018-09-19 18:42:25 +10:00
Jesse Duffield
768b9453f8 Merge branch 'hotfix/cursor-positioning' into feature/recent-repos 2018-09-19 18:40:41 +10:00
Jesse Duffield
e95b2e5f0b update specs 2018-09-19 18:31:54 +10:00
Jesse Duffield
950cfeff6f add specs for menu utils 2018-09-19 18:19:26 +10:00
Jesse Duffield
fce895ed0d Merge pull request #287 from antham/add-tests-part-6
Add tests part 6
2018-09-18 21:47:46 +10:00
Jesse Duffield
c789bba673 color merged and unmerged commits differently 2018-09-18 21:45:35 +10:00
Jesse Duffield
b384fcf6af generalise popup menu panel 2018-09-18 21:07:25 +10:00
Anthony HAMON
60cf549a32 commands/git : reverse the logic 2018-09-18 09:23:41 +02:00
Anthony HAMON
6f0b32f95e commands/git : add GetCommits tests refactor
* switch GetCommitsToPush scope to private
* return a map instead of slice for look up
* remove useless includesString function
2018-09-17 21:19:17 +02:00
Jesse Duffield
f89bc10af1 appease golangci 2018-09-17 21:32:19 +10:00
Jesse Duffield
a66ac8092e minor refactor 2018-09-17 21:27:53 +10:00
Jesse Duffield
bd04ecff69 Merge branch 'master' into hotfix/cursor-positioning 2018-09-17 21:03:29 +10:00
Jesse Duffield
c00c834b35 standardise rendering of lists in panels 2018-09-17 21:02:30 +10:00
Anthony HAMON
9d9d775f50 circle : remove new line 2018-09-16 22:49:17 +02:00
Anthony HAMON
38036e0d20 circle : kill old cache 2018-09-16 22:41:41 +02:00
Anthony HAMON
9713a15167 commands/git : add test to GetBranchGraph, refactor 2018-09-16 22:12:03 +02:00
Anthony HAMON
b641d6bd96 commands/git : add test to Checkout, refactor 2018-09-16 22:08:23 +02:00
Anthony HAMON
67a42f49b4 commands/git : add test to RemoveFile, refactor 2018-09-16 22:03:56 +02:00
Anthony HAMON
bbc88071e9 gui : remove unreachable code 2018-09-16 20:46:25 +02:00
Jesse Duffield
ca2eec60fe Merge pull request #285 from antham/add-tests-part-5
Add tests to pkg/commands/git - Part 5
2018-09-16 22:03:10 +10:00
Anthony HAMON
c1b7a21631 commands/git : move tests 2018-09-16 11:11:09 +02:00
Anthony HAMON
91832f2c5e commands/git : add tests, refactor a bit 2018-09-16 11:11:09 +02:00
Mark Kopenga
c64fb87b2b Merge pull request #12 from jesseduffield/master
Update to latest master
2018-09-14 11:50:49 +02:00
Jesse Duffield
fa08c6c2a2 Merge pull request #284 from mingrammer/main-error
main: display an error message instead of panic when setup fails
2018-09-14 09:38:21 +10:00
mingrammer
3cf84a5af1 main: display an error message instead of panic when setup fails 2018-09-14 00:23:11 +09:00
Kristijan Husak
61f0801bd3 Add ammend commit action. 2018-09-13 12:44:32 +02:00
Jesse Duffield
eb4b5cd43b Merge pull request #282 from antham/add-tests-part-4
Add tests to pkg/commands/git - Part 4
2018-09-13 09:34:28 +10:00
Anthony HAMON
c92510ceba commands/git : add tests on SquashFixupCommit and refactor 2018-09-12 22:45:52 +02:00
Anthony HAMON
65a24d70c3 commands/git : add tests on SquashPreviousTwoCommits 2018-09-12 20:43:03 +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
Jesse Duffield
3b765e5417 add test for min method 2018-09-12 19:39:36 +10:00
Jesse Duffield
57f6a552d2 Merge branch 'hotfix/cursor-positioning' of https://github.com/jesseduffield/lazygit into hotfix/cursor-positioning 2018-09-12 18:49:14 +10:00
Jesse Duffield
35cae80de9 more efficient building of branch displaystrings 2018-09-12 18:49:09 +10:00
Jesse Duffield
b4b4cd83dd Merge branch 'master' into hotfix/cursor-positioning 2018-09-12 18:47:57 +10:00
Jesse Duffield
31c33dfdcb remove redundant comments 2018-09-12 18:47:37 +10:00
Jesse Duffield
79940b7ba9 Merge pull request #279 from jesseduffield/hotfix/file-ordering
Restore old file merging algorithm
2018-09-12 18:26:29 +10:00
Jesse Duffield
2ce8ac5850 restore old file sorting algorithm 2018-09-12 18:24:03 +10:00
Jesse Duffield
f8b484f638 don't use newlines at the end of panel buffers 2018-09-12 18:23:25 +10:00
Jesse Duffield
73e2c1005a Merge pull request #277 from antham/add-tests-part-3
Add tests to pkg/commands/git - Part 3
2018-09-12 17:35:09 +10:00
Anthony HAMON
97e0a6dc45 commands/git : remove extra space 2018-09-12 07:51:14 +02:00
Anthony HAMON
9bad0337fe commands/git : swap global/local get config 2018-09-12 07:50:49 +02:00
Anthony HAMON
f03544f392 commands/git : fix test 2018-09-11 22:20:59 +02:00
Anthony HAMON
0aba49af2b commands/git : fix typo 2018-09-11 21:56:17 +02:00
Anthony HAMON
ccbc5e569c commands/git : add test to Push func, refactor 2018-09-11 21:56:17 +02:00
Anthony HAMON
415aad600c commands/git : add test to Commit func, refactor 2018-09-11 21:56:17 +02:00
Anthony HAMON
d23577168f commands/git : remove dependency on gocui 2018-09-11 21:56:17 +02:00
Anthony HAMON
5c204b2813 commands/git: rewrite UsingGpg, add tests 2018-09-11 21:56:17 +02:00
Jesse Duffield
7d86278507 Merge pull request #274 from sascha-andres/master
fix: escape quote character on Linux
2018-09-11 09:07:36 +10:00
Sascha Andres
985196f5aa docs: add comments for new test code 2018-09-10 17:36:59 +02:00
Jesse Duffield
52b132fe01 better handling of cursor and origin positionings 2018-09-10 20:17:39 +10:00
Sascha Andres
9ec5a04cf5 merge: remote-tracking branch 'upstream/master' 2018-09-10 10:33:20 +02:00
Sascha Andres
bb9698810d refactor: move fallback to platform struct
Co-authored-by: Jesse Duffield <jessedduffield@gmail.com>
2018-09-10 10:31:15 +02:00
Jesse Duffield
7f4371ad71 Merge pull request #275 from mjarkk/master
Translated untranslated dutch phrases
2018-09-10 18:15:15 +10:00
Sascha Andres
74144e3892 fix: remove call to fmt.Println 2018-09-10 10:10:10 +02:00
mjarkk
07f87eb7cf Added missing dutch translations 2018-09-10 08:08:53 +02:00
Mark Kopenga
5600da9ee7 Merge pull request #11 from jesseduffield/master
updated to latest master
2018-09-10 07:28:11 +02:00
Sascha Andres
ba0cc20e22 feat: add test cases 2018-09-10 06:51:19 +02:00
Sascha Andres
717913e64c fix: escape quote character on Linux
Co-authored-by: Dawid Dziurla <dawidd0811@gmail.com>

Closes #269
2018-09-10 06:36:15 +02:00
Jesse Duffield
5a0431bb62 Merge pull request #272 from antham/master
Add missing go.sum file
2018-09-10 10:58:22 +10:00
Jesse Duffield
1e357e2362 Merge branch 'master' into master 2018-09-10 10:02:06 +10:00
Jesse Duffield
c3b62a555c Merge pull request #265 from antham/add-tests-part-2
Add tests to pkg/commands/git - Part 2
2018-09-10 10:01:55 +10:00
Jesse Duffield
557461c016 Merge branch 'master' into add-tests-part-2 2018-09-10 09:47:14 +10:00
Jesse Duffield
c2e670104e Merge branch 'master' into master 2018-09-10 09:46:01 +10:00
Jesse Duffield
bea9971f9c Merge pull request #271 from glvr182/feature/ignore-hidden-files
Change the gitignore a bit to cover more
2018-09-10 09:45:29 +10:00
Jesse Duffield
a8fdf1a646 Merge branch 'master' into feature/ignore-hidden-files 2018-09-10 09:45:18 +10:00
Jesse Duffield
f8ab4f4073 Merge pull request #234 from dawidd6/feature/help
WIP: Add menu panel (cheatsheet)
2018-09-10 09:43:28 +10:00
Anthony HAMON
24f15742d0 commands/git : rename variable 2018-09-09 20:08:46 +02:00
Glenn Vriesman
700f8c7e79 REPO: Added TODO entries to the gitignore for jesse 2018-09-09 13:37:38 +02:00
Anthony HAMON
28e8d6e472 add go.sum 2018-09-09 11:13:18 +02:00
Anthony HAMON
6076a75643 commands/git : fix function call 2018-09-09 10:52:34 +02:00
Anthony HAMON
b46e4b4976 commands/git : add several tests, do some cleanup 2018-09-09 10:52:34 +02:00
Anthony HAMON
99eca7b000 commands/git : replace make function 2018-09-09 10:52:34 +02:00
Anthony HAMON
a0faaf6893 commands/git : remove includes function 2018-09-09 10:52:34 +02:00
Anthony HAMON
56ad07ebab commands/git : rename functions 2018-09-09 10:52:34 +02:00
Anthony HAMON
1ecd74c357 commands/git : add tests for GetCommitsToPush 2018-09-09 10:52:34 +02:00
Anthony HAMON
ceab9706cb commands/git : add tests for UpstreamDifferentCount 2018-09-09 10:52:34 +02:00
Anthony HAMON
1cc7e9c02a rewrite to use subtests 2018-09-09 10:52:34 +02:00
Dawid Dziurla
63e400647a shorter english string 2018-09-09 10:47:13 +02:00
Dawid Dziurla
6f7de83bce Merge branch 'master' into feature/help
conflicts resolved
2018-09-09 10:41:01 +02:00
Jesse Duffield
5af03b6820 Merge pull request #264 from antham/master
Add go.mod to support modules landed in go1.11
2018-09-09 11:35:48 +10:00
Glenn Vriesman
a8866b158b REPO: Added and modified some ignore statements 2018-09-08 14:25:33 +02:00
antham
0d7a697e86 add go.mod 2018-09-07 21:35:04 +02:00
Dawid Dziurla
e80371fc6f satisfy golangci 2018-09-07 14:41:01 +02:00
Dawid Dziurla
9cef98f779 ladies and gentlemen...
this is fmt number x+1
2018-09-07 14:23:08 +02:00
Dawid Dziurla
ba6dedfb22 rewrite some of menu panel logic
panel keybindings are now on top and
global keybindings are below separated with empty newline
2018-09-07 14:19:16 +02:00
Jesse Duffield
ca715c5b23 support switching to recent repo 2018-09-07 09:41:15 +10:00
Jesse Duffield
ba7e6add86 Merge pull request #267 from glvr182/feature/add-effgo-link
Added entry to the contributing guide
2018-09-07 09:30:29 +10:00
Glenn Vriesman
a820189450 Added entry to the contributing guide 2018-09-06 17:31:19 +02:00
Jesse Duffield
ce95e6771a Merge pull request #263 from dawidd6/feature/confirm_quit
Add confirmOnQuit config option
2018-09-06 09:33:06 +10:00
Dawid Dziurla
e9268d1828 add confirmOnQuit config option 2018-09-05 19:56:11 +02:00
Dawid Dziurla
db2e2160a9 change menu keybinding from ? to x 2018-09-05 15:55:24 +02:00
Dawid Dziurla
08395ae76c workaround to include menu keybinding in cheatsheet 2018-09-05 15:45:20 +02:00
Dawid Dziurla
4188786749 update pl translation 2018-09-05 15:20:34 +02:00
Jesse Duffield
cf41338a9f Merge pull request #262 from jesseduffield/feature/50/jesse
Add commit counter using subtitle
2018-09-05 23:09:20 +10:00
Jesse Duffield
a2d40cfbf1 allow users to configure whether the commit length is shown 2018-09-05 23:02:13 +10:00
Dawid Dziurla
34d1648bd3 fmt strikes again 2018-09-05 13:23:06 +02:00
Dawid Dziurla
906f8e252e include global keybindings in menu 2018-09-05 13:16:40 +02:00
Jesse Duffield
986774e5c7 add commit count via gocui subtitle 2018-09-05 20:43:45 +10:00
Dawid Dziurla
98763e98cb initial commit message counter 2018-09-05 11:26:54 +02:00
Jesse Duffield
f777c60ea4 Merge pull request #260 from jesseduffield/hotfix/258-commit-message-newlines
Fix popup panel resizing
2018-09-05 19:15:39 +10:00
Dawid Dziurla
557009e660 help -> menu 2018-09-05 11:12:11 +02:00
Jesse Duffield
af876d2be2 Merge branch 'master' into hotfix/258-commit-message-newlines 2018-09-05 19:11:30 +10:00
Jesse Duffield
422b263df4 fix popup panel resizing 2018-09-05 19:10:46 +10:00
Jesse Duffield
4fc290b101 Update README.md 2018-09-05 18:44:24 +10:00
Jesse Duffield
c1bf1e52b0 Merge pull request #257 from antham/master
Increase circleci compile speed
2018-09-05 17:12:29 +10:00
Anthony HAMON
4d745fa525 update cache path in circleci 2018-09-05 08:55:15 +02:00
Jesse Duffield
f0e19690f5 Merge pull request #254 from jesseduffield/feature/dockerfile-for-alpine
Dockerfile for testing on alpine linux
2018-09-05 14:20:51 +10:00
Jesse Duffield
580c1cbc89 Merge branch 'master' into feature/dockerfile-for-alpine 2018-09-05 11:03:17 +10:00
Dawid Dziurla
e21f739f4f add renderGlobalOptions
render only global options for all panels
2018-09-04 16:07:31 +02:00
Dawid Dziurla
97ad4a1643 delete options 2018-09-04 15:40:29 +02:00
Dawid Dziurla
cbafadd48e move keys slice to guiState struct 2018-09-04 15:29:43 +02:00
Dawid Dziurla
7b84c162f4 set help panel fgcolor to white 2018-09-04 15:25:54 +02:00
Dawid Dziurla
f29c81fb5c add getMaxKeyLength 2018-09-04 15:25:02 +02:00
Jesse Duffield
59003c8bbf Merge pull request #253 from jesseduffield/hotfix/238-xdg-open-2
Update xdg-open command
2018-09-04 22:21:03 +10:00
Jesse Duffield
6c1d133315 dockerfile for testing on alpine linux 2018-09-04 22:19:08 +10:00
Jesse Duffield
33ea093d88 Merge branch 'master' into hotfix/238-xdg-open-2 2018-09-04 20:37:35 +10:00
Jesse Duffield
a81f8b84e3 Merge pull request #245 from antham/master
Rewrite SetupGit in GitCommand
2018-09-04 19:29:21 +10:00
Jesse Duffield
3f68fe42cb update xdg-open command 2018-09-04 19:18:18 +10:00
Anthony HAMON
172cd7c687 fix tests locally 2018-09-04 08:32:43 +02:00
Anthony HAMON
df3e7abd68 use RunCommand 2018-09-04 08:32:40 +02:00
Anthony HAMON
8c67578063 replace fmt with errors 2018-09-04 06:21:58 +02:00
Anthony HAMON
06846ef3ae rename NewApp to Setup 2018-09-04 06:21:58 +02:00
Anthony HAMON
43ad9a81c2 merge setup in function that create a new git command 2018-09-04 06:21:58 +02:00
Anthony HAMON
9f7775df26 pkg/git : remove unused Map function 2018-09-04 06:21:58 +02:00
Anthony HAMON
c1984528c8 pkg/git : add tests for SetupGit 2018-09-04 06:21:58 +02:00
Anthony HAMON
624d63d2fa pkg/git : remove panic in SetupGit method 2018-09-04 06:21:58 +02:00
Dawid Dziurla
67d99a24ea get selected branch from correct panel 2018-09-03 18:45:52 +02:00
Dawid Dziurla
bf8514f5e2 helperize spaces 2018-09-03 18:44:56 +02:00
Dawid Dziurla
93e88ea8fe add missing translated polish strings 2018-09-03 18:34:04 +02:00
Dawid Dziurla
a2073528f4 add missing untranslated dutch strings
@mjarkk
2018-09-03 18:33:38 +02:00
Dawid Dziurla
230a5afa4c remove capitalization of keybindings descriptions 2018-09-03 18:16:54 +02:00
Dawid Dziurla
c49e4dc287 get item position from correct panel 2018-09-03 18:07:38 +02:00
Dawid Dziurla
59f50010b6 apply fmt again 2018-09-03 18:01:07 +02:00
Dawid Dziurla
b5827b7d80 merge conflict effect fix 2018-09-03 17:57:03 +02:00
Dawid Dziurla
36874be45b apply very important fmt 2018-09-03 17:54:06 +02:00
Dawid Dziurla
83b7c60246 add untranslated dutch strings
@mjarkk
2018-09-03 17:54:06 +02:00
Dawid Dziurla
323016aa01 polish translation 2018-09-03 17:54:06 +02:00
Dawid Dziurla
b403b6e46d better english string 2018-09-03 17:54:06 +02:00
Dawid Dziurla
359636c1aa add generate_cheatsheet script
script is generating markdown document with small cheatsheet
in selected language
2018-09-03 17:54:06 +02:00
Dawid Dziurla
1fa55875e2 remove testing content 2018-09-03 17:54:06 +02:00
Dawid Dziurla
5177e458ef use Fprint instead of renderString
renderString is wrapping content
because of that lines are being select wrong
2018-09-03 17:54:06 +02:00
Dawid Dziurla
314c8c279a apply fmt on keybindings 2018-09-03 17:54:06 +02:00
Dawid Dziurla
20073d0293 don't panic
"panic: runtime error: index out of range"

when executing stash pop 'g' from help menu
2018-09-03 17:54:06 +02:00
Dawid Dziurla
90a4cada82 add missing descriptions 2018-09-03 17:54:06 +02:00
Dawid Dziurla
e376de6d1a explicitly delete 'help' view 2018-09-03 17:54:06 +02:00
Dawid Dziurla
265d7e121a use Key if it's a rune, otherwise KeyReadable 2018-09-03 17:54:06 +02:00
Dawid Dziurla
7ec5b6cc30 indent keybindings 2018-09-03 17:54:06 +02:00
Dawid Dziurla
9ceaf5b9a9 move descriptions to i18n 2018-09-03 17:52:05 +02:00
Dawid Dziurla
cc3fa4b79d make '?' key visible on every panel 2018-09-03 17:52:05 +02:00
Dawid Dziurla
653d590157 help panel size from getConfirmationPanelDimensions 2018-09-03 17:52:05 +02:00
Dawid Dziurla
8a01d11202 more error checks 2018-09-03 17:52:05 +02:00
Dawid Dziurla
28a9594ef7 update help panel
- delete scrolling ability
- lines are now selectable
- implemented handler execution when space is pressed
- add example descriptions for status panel keybindings
2018-09-03 17:52:05 +02:00
Dawid Dziurla
77623db1d0 apply fmt 2018-09-03 17:52:05 +02:00
Dawid Dziurla
6a99d36ae1 change key from 'H' to '?' 2018-09-03 17:52:05 +02:00
Dawid Dziurla
2416f585ce initial help panel 2018-09-03 17:52:05 +02:00
Dawid Dziurla
741e28d01a move bindings to getKeybindings() 2018-09-03 17:52:05 +02:00
Jesse Duffield
19a8029795 Merge pull request #250 from dawidd6/fix/tests
Fix testing with localized rmdir + don't test scripts directory
2018-09-03 22:04:16 +10:00
Jesse Duffield
796f17eef4 Merge branch 'master' into fix/tests 2018-09-03 21:20:17 +10:00
Jesse Duffield
8fbb3aea4f Merge pull request #242 from d-dorazio/rename-commits-in-user-editor
add keybinding to open user editor when renaming last commit
2018-09-03 20:11:13 +10:00
Jesse Duffield
6fc4cb1b96 Merge branch 'master' into rename-commits-in-user-editor 2018-09-03 19:53:16 +10:00
Jesse Duffield
3c1935fee4 Merge pull request #249 from jesseduffield/hotfix/238-xdg-open
238: opening with xdg-open
2018-09-03 19:48:42 +10:00
Jesse Duffield
dfb87d34dc Merge branch 'master' into hotfix/238-xdg-open 2018-09-03 19:42:06 +10:00
Jesse Duffield
4ab1a1f72b Merge branch 'master' into hotfix/238-xdg-open 2018-09-03 19:33:37 +10:00
Jesse Duffield
a9cd277070 add test for ResolvePlaceholderString 2018-09-03 19:31:27 +10:00
Dawid Dziurla
5c1463313d respect localized output of rmdir 2018-09-01 17:22:49 +02:00
Dawid Dziurla
0355fc3008 don't run tests on scripts/ directory 2018-09-01 17:22:23 +02:00
Daniele D'Orazio
39f065207e add simple test for PrepareCommitAmendSubProcess 2018-09-01 12:29:43 +02:00
Daniele D'Orazio
9e6a4a529a add keybinding to open user editor when renaming last commit 2018-09-01 12:14:42 +02:00
Jesse Duffield
87de803a6c update default config header 2018-09-01 14:38:30 +10:00
Jesse Duffield
3cafa2bb12 update config to reflect platform specific defaults 2018-09-01 14:35:46 +10:00
Jesse Duffield
d31520261f introduce platform specific defaults 2018-09-01 14:33:01 +10:00
Jesse Duffield
ad880e2d56 wrap windows start command in shell 2018-09-01 13:28:10 +10:00
Jesse Duffield
865809e625 better error handling for commands 2018-09-01 13:27:58 +10:00
Jesse Duffield
04d5a473d7 use start instead of cygstart to open files on windows 2018-09-01 12:53:51 +10:00
Jesse Duffield
f127ae62bb update config 2018-09-01 12:18:16 +10:00
Jesse Duffield
3f14b764d5 update tests 2018-09-01 12:13:41 +10:00
Jesse Duffield
ae0d88f855 WIP using runDirectCommand with xdg-open 2018-09-01 11:38:32 +10:00
Jesse Duffield
b65fa852f1 Merge pull request #248 from jesseduffield/hotfix/better-backwards-compatibility-error
Better error for nonbackwards compatible changes
2018-09-01 11:26:58 +10:00
Jesse Duffield
42500817e0 better error for nonbackwards compatible changes 2018-09-01 10:36:55 +10:00
Jesse Duffield
d8aba3aeee Merge pull request #241 from antham/master
Fix linting issues
2018-08-29 23:05:14 +10:00
antham
1b8836e92d fix fmt issue 2018-08-29 13:46:51 +02:00
Anthony HAMON
54326907c3 fix linting issues 2018-08-29 13:46:51 +02:00
Anthony HAMON
cda7b374e2 fix linting issues 2018-08-29 13:46:51 +02:00
Anthony HAMON
e889a40caf fix golint issue 2018-08-29 13:46:51 +02:00
Jesse Duffield
3f26ddc06f Merge pull request #225 from antham/master
Add tests to pkg/commands/git - Part 1
2018-08-29 20:25:39 +10:00
Anthony HAMON
dac7c90483 add cache based on Gopkg.lock checksum 2018-08-29 12:03:32 +02:00
Anthony HAMON
66e5dacf5e fix git tests 2018-08-29 12:03:32 +02:00
Anthony HAMON
e3ed899b20 refactor MergeStatusFiles 2018-08-29 12:03:32 +02:00
Anthony HAMON
d6b4d4b063 add tests for MergesStatusFiles 2018-08-29 12:03:32 +02:00
Anthony HAMON
45fa257128 add test for StashSave and refactor StashSave method 2018-08-29 12:03:32 +02:00
Anthony HAMON
99840d8fc4 add test for StashDo and refactor StashDo method 2018-08-29 12:03:32 +02:00
Anthony HAMON
85012dbc8f add tests for GetStatusFiles 2018-08-29 12:03:32 +02:00
Anthony HAMON
13f9073552 add test for GetStashEntryDiff 2018-08-29 12:03:32 +02:00
Anthony HAMON
49b507d2ff replace make 2018-08-29 12:03:32 +02:00
Anthony HAMON
8247fd69c9 add test for GetStashEntries 2018-08-29 12:03:32 +02:00
Anthony HAMON
983d0bd586 replace make 2018-08-29 12:03:32 +02:00
Anthony HAMON
ca9ce22693 use assert in tests, rename testing method 2018-08-29 12:03:32 +02:00
Jesse Duffield
cff1dee6dc more lenient version comparison 2018-08-29 09:37:47 +10:00
Jesse Duffield
2181a91fea Merge pull request #231 from jesseduffield/feature/24-support-unicode-characters
Support unicode characters
2018-08-28 20:09:27 +10:00
Jesse Duffield
8c2b8cfb51 support unicode characters 2018-08-28 20:08:35 +10:00
Jesse Duffield
145cba34a0 Merge pull request #228 from jesseduffield/hotfix/226-dont-panic-when-catting
226: dont panic when catting directories
2018-08-28 19:19:03 +10:00
Jesse Duffield
7e1e97d050 dont panic when catting directories 2018-08-28 19:12:35 +10:00
Jesse Duffield
320ccdb22a when panicking due to malformed gitconfig, show a more useful error 2018-08-28 18:01:53 +10:00
Jesse Duffield
db1d5328f2 Merge pull request #213 from jesseduffield/feature/add-all
Add 'a' keybinding to toggle staged/unstaged for all files
2018-08-27 21:01:52 +10:00
Jesse Duffield
da2d3f253f better translation 2018-08-27 20:58:17 +10:00
Jesse Duffield
b4323c029f Merge branch 'master' into feature/add-all 2018-08-27 20:57:50 +10:00
Jesse Duffield
203ad29349 use logrus entry rather than logger 2018-08-27 20:51:21 +10:00
Jesse Duffield
04735d0601 update call to update user config function 2018-08-27 20:49:47 +10:00
Jesse Duffield
bbe603ff5b separate english translation structs 2018-08-27 20:46:06 +10:00
Jesse Duffield
23a9f41d9d Merge branch 'feature/anonymous-reporting' 2018-08-27 20:35:55 +10:00
Jesse Duffield
1901901d24 Merge pull request #189 from jesseduffield/feature/auto-updates
Auto updates
2018-08-27 20:33:02 +10:00
Jesse Duffield
f861175f83 move update quit confirmation function into updates file 2018-08-27 20:26:49 +10:00
Jesse Duffield
25c60b1854 localize update errors 2018-08-27 20:23:47 +10:00
Jesse Duffield
38557f131d record last update check as soon as you begin checking 2018-08-27 20:16:26 +10:00
Jesse Duffield
96eef7838e better auto update logic 2018-08-27 20:08:10 +10:00
Jesse Duffield
2bf536265a disable updating on windows for now 2018-08-27 19:37:01 +10:00
Jesse Duffield
43f612feb1 Merge branch 'master' into feature/auto-updates 2018-08-27 19:20:07 +10:00
Jesse Duffield
a1c6adab59 Merge branch 'master' into feature/anonymous-reporting 2018-08-27 18:50:24 +10:00
Jesse Duffield
e72d090c5c Merge pull request #221 from antham/fix-master
Fix format issue
2018-08-27 18:05:18 +10:00
Anthony HAMON
a2a9e0c478 fix format issue 2018-08-27 10:01:05 +02:00
Jesse Duffield
d3953a1440 Merge pull request #206 from antham/add-pkg-commands-test
Add test to pkg/commands, refactor some methods
2018-08-27 17:58:02 +10:00
Jesse Duffield
a251cffb9a Merge pull request #220 from dawidd6/flag/config
Add config flag
2018-08-27 17:56:19 +10:00
Dawid Dziurla
b7f6bcb3ca add config flag 2018-08-26 17:31:28 +02:00
Anthony HAMON
55c6af258d increase parallel build 2018-08-26 11:30:03 +02:00
Jesse Duffield
540edc0c35 anonymous reporting data 2018-08-26 16:53:31 +10:00
Jesse Duffield
12261ceb05 update config docs for auto updates 2018-08-26 13:03:37 +10:00
Anthony HAMON
f6ab11e4ee run gofmt 2018-08-26 02:20:01 +02:00
Anthony HAMON
2d9f7009fa add gofmt to circle 2018-08-26 02:17:46 +02:00
Anthony HAMON
4b19b4108e add circleci cache 2018-08-26 01:58:20 +02:00
Anthony HAMON
a23753dc18 exclude darwin arm from building 2018-08-26 01:58:20 +02:00
antham
caf99d2d64 exclude openbsd cause of go-git bugs on this platform 2018-08-26 01:58:20 +02:00
antham
2273f4c0a5 add compile step in circleci 2018-08-26 01:58:20 +02:00
Anthony HAMON
23fe0290ad add openFile to gui struct 2018-08-26 01:58:20 +02:00
Anthony HAMON
ed2dcd9e46 add tests 2018-08-26 01:58:20 +02:00
Anthony HAMON
75e08993ea extract dependencies 2018-08-26 01:58:20 +02:00
Anthony HAMON
0b07cd19f7 switch GetOpenCommand scope to private 2018-08-26 01:58:20 +02:00
Anthony HAMON
38f11f1f4a move dummy functions, rename functions 2018-08-26 01:58:19 +02:00
Anthony HAMON
f91e2b12db add tests to pkg/commands 2018-08-26 01:58:19 +02:00
Anthony HAMON
364c1ac5e7 remove useless returned variable 2018-08-26 01:58:19 +02:00
Anthony HAMON
883fcf1083 remove useless returned variable 2018-08-26 01:58:19 +02:00
Anthony HAMON
a891bc90b7 simplify method 2018-08-26 01:58:19 +02:00
Anthony HAMON
7a74bc504b avoid useless allocation 2018-08-26 01:58:19 +02:00
Anthony HAMON
32f4d09e89 move platform specific code to dedicated platform files 2018-08-26 01:58:19 +02:00
Anthony HAMON
a5adfaee8a remove useless returned variable 2018-08-26 01:58:19 +02:00
Jesse Duffield
57decdd11d check error from setViewOnBottom 2018-08-25 17:38:03 +10:00
Jesse Duffield
87dc2bcad9 bump dependencies 2018-08-25 17:36:16 +10:00
Jesse Duffield
21f6e9ba87 auto-updates 2018-08-25 17:32:34 +10:00
Jesse Duffield
f24c95aede Merge branch 'master' into feature/auto-updates 2018-08-25 11:02:46 +10:00
Jesse Duffield
93ab892bdd Merge pull request #214 from jesseduffield/hotfix/global-rune-keybindings
Ignore global rune keybindings when in editable view
2018-08-25 09:14:05 +10:00
Jesse Duffield
ee7f88e123 ignore global rune keybindings when in editable view 2018-08-25 09:13:29 +10:00
Jesse Duffield
60422912c8 add 'a' keybinding to toggle staged/unstaged for all files 2018-08-25 08:59:51 +10:00
Jesse Duffield
6c389df57d Merge pull request #200 from remyabel/feature/esc-quits
Esc will quit when not in popup, fixes #197
2018-08-24 09:16:56 +10:00
Jesse Duffield
22de5e7b23 Merge pull request #210 from dawidd6/sirupsen/imports
Sirupsen -> sirupsen
2018-08-24 07:58:47 +10:00
Dawid Dziurla
bcbeec1a56 Sirupsen -> sirupsen 2018-08-23 14:22:03 +02:00
Jesse Duffield
5628eae502 WIP 2018-08-23 18:43:16 +10:00
Jesse Duffield
2cdd439286 Merge pull request #204 from remyabel/hotfix/fix-dutch-errors
Fix typos causing tests to fail
2018-08-23 10:19:31 +10:00
Tommy Nguyen
110ff38c0d Remove accidentally checked in code 2018-08-22 11:34:16 -04:00
Tommy Nguyen
2680b2f4fe Merge branch 'hotfix/fix-dutch-errors' of github.com:remyabel/lazygit into hotfix/fix-dutch-errors 2018-08-22 11:28:09 -04:00
Tommy Nguyen
3e8ef0d12d Remove space before punctuation 2018-08-22 11:27:45 -04:00
remyabel
bee4d98f52 Remove space before punctuation 2018-08-21 21:27:20 -04:00
Jesse Duffield
584d6b241c Merge pull request #198 from antham/fix-various-errors
Fix various errors reported by goreportcard
2018-08-22 09:28:19 +10:00
Anthony HAMON
37681627ab remove useless check 2018-08-21 20:54:48 +02:00
Anthony HAMON
810155ef2f fix documentation and reference issues 2018-08-21 20:54:46 +02:00
Tommy Nguyen
924a9bb2c9 Fix typos causing tests to fail 2018-08-21 10:55:33 -04:00
Jesse Duffield
4d635cd1cd Merge pull request #151 from alcohol/force-delete-branch
add option to force delete given branch
2018-08-21 23:45:27 +10:00
Jesse Duffield
3d49ab6666 Merge pull request #202 from dawidd6/test
Fix testPath for debian packaging
2018-08-21 23:33:25 +10:00
Tommy Nguyen
eff931a138 Update gocui fork 2018-08-21 09:00:16 -04:00
Tommy Nguyen
cd4063c763 s/escape/quit 2018-08-21 08:54:51 -04:00
Tommy Nguyen
646c205227 s/quit/escape, don't use special handling for views 2018-08-21 07:50:37 -04:00
Tommy Nguyen
dc911906b3 Esc will quit when not in popup, fixes #197 2018-08-21 06:36:20 -04:00
Rob Bast
182e475116 correct variable assignment 2018-08-21 12:09:13 +02:00
Rob Bast
5f6b61d28c adjust translation(s) for forced branch deletion 2018-08-21 12:06:42 +02:00
Rob Bast
810540edfa add return 2018-08-21 11:32:17 +02:00
Jesse Duffield
042c83387e make bom.sh executable for easier testing 2018-08-21 17:41:18 +10:00
Jesse Duffield
c6a8899060 Merge pull request #160 from remyabel/157_remove_bom
#157: clean BOM, allowing CSV files to display correctly
2018-08-21 17:39:55 +10:00
Jesse Duffield
da4c12bf9e Merge pull request #193 from antham/add-tests
Add tests to i18n package
2018-08-21 17:34:13 +10:00
Jesse Duffield
d6ee413587 Update Config.md 2018-08-21 13:42:17 +10:00
Jesse Duffield
9b63887867 add colored borders example image 2018-08-21 13:27:27 +10:00
Jesse Duffield
d35eaa062b remove org-global context in circle ci config 2018-08-21 12:57:19 +10:00
Jesse Duffield
0c27776a83 Merge pull request #179 from antham/master
Update circleci config, add badges
2018-08-21 12:49:22 +10:00
Anthony HAMON
4972a4c218 rewrite addBundles 2018-08-20 21:18:50 +02:00
Anthony HAMON
e4070ccb4f rewrite language detection, rewrite tests 2018-08-20 21:12:42 +02:00
Dawid Dziurla
483b4d939d fix testPath for debian packaging 2018-08-20 15:21:05 +02:00
Tommy Nguyen
45fea83771 Convert \r\n to \n; don't depend on unix2dos 2018-08-20 09:16:35 -04:00
Jesse Duffield
2aa89ade0d Merge pull request #195 from jesseduffield/hotfix/194-support-empty-version
194: Support empty version string
2018-08-20 20:54:23 +10:00
Jesse Duffield
37029f7db3 support empty version string 2018-08-20 20:52:32 +10:00
Jesse Duffield
954dfb12e4 Merge branch 'master' into feature/auto-updates 2018-08-20 19:53:53 +10:00
Jesse Duffield
8364509d1f Merge pull request #188 from jesseduffield/feature/force-push
Force push confirmation panel
2018-08-20 19:52:41 +10:00
Jesse Duffield
d938a437a2 WIP auto updates 2018-08-20 19:52:20 +10:00
Anthony HAMON
3c0fb9b324 add tests to i18n package 2018-08-20 09:02:57 +02:00
Rob Bast
8e3df6b981 add option to force delete given branch 2018-08-20 08:37:16 +02:00
Tommy Nguyen
5dd049eb82 Convert test to use new library 2018-08-19 23:39:57 -04:00
Tommy Nguyen
f1a4a7e1ff synchronize deps 2018-08-19 23:34:58 -04:00
Tommy Nguyen
108815c790 Add missing brace 2018-08-19 23:31:26 -04:00
remyabel
5c65066cbb Merge branch 'master' into 157_remove_bom 2018-08-19 23:29:47 -04:00
Jesse Duffield
64cf8f5b10 update dependencies 2018-08-20 13:25:33 +10:00
Jesse Duffield
5fcbe4ff8a Merge pull request #191 from jesseduffield/hotfix/174-language-not-found
174: Use fallback language if language not detected on startup
2018-08-20 13:23:26 +10:00
Jesse Duffield
fa8248bd70 use fallback language if language not detected on startup 2018-08-20 13:20:59 +10:00
Jesse Duffield
65c5a6014b Merge pull request #185 from antham/add-tests
Add test to utils package
2018-08-20 09:24:54 +10:00
Anthony HAMON
fc126a1718 remove test 2018-08-19 21:39:23 +02:00
Anthony HAMON
bb86a3ff7c update github template 2018-08-19 20:21:19 +02:00
Anthony HAMON
bbf7a9d790 add various badges
* golangci (https://golangci.com)
* circleci (https://circleci.com/)
* codecov.io (https://codecov.io)
* godoc
* tag release
2018-08-19 20:21:19 +02:00
Anthony HAMON
fbfa48f0fc update circleci
* define release worflow when a tag is created
* add dep install
* run tests with coverage
* add goreleaser
2018-08-19 20:21:14 +02:00
Tommy Nguyen
e8b12a086c Fix ineffectual assignment 2018-08-19 08:52:08 -04:00
Tommy Nguyen
766197de9d NormalizeLinefeeds removes rather than converts Window/Mac style lf's 2018-08-19 08:48:03 -04:00
Jesse Duffield
1e44001910 Merge branch 'master' into feature/auto-updates 2018-08-19 21:36:40 +10:00
Jesse Duffield
317926c808 fix golangci lint 2018-08-19 21:34:24 +10:00
Jesse Duffield
4d2346f80a popup force push confirmation panel if the local branch has diverged from the upstream branch 2018-08-19 21:28:13 +10:00
Tommy Nguyen
d2bdac29aa Merge branch 'master' into 157_remove_bom 2018-08-19 07:22:48 -04:00
Tommy Nguyen
cea736e6e9 Factor out into NormalizeLinefeeds; add tests 2018-08-19 07:20:50 -04:00
Jesse Duffield
e6712832b5 Merge pull request #187 from jesseduffield/hotfix/177-fix-gitignore
177: Fix ignore feature
2018-08-19 20:42:51 +10:00
Jesse Duffield
aa4d739577 fix ignore feature 2018-08-19 20:41:04 +10:00
Jesse Duffield
51558f51ab Merge pull request #186 from jesseduffield/hotfix/169-more-filenames-with-spaces
Handle filenames with spaces better
2018-08-19 20:19:53 +10:00
Jesse Duffield
35884f81e9 handle filenames with spaces better 2018-08-19 20:13:29 +10:00
Jesse Duffield
2008607108 remove VERSION file 2018-08-19 20:04:57 +10:00
Jesse Duffield
21be9fb3b1 bump version to v0.1.72 2018-08-19 19:48:31 +10:00
Jesse Duffield
80fac559c2 revert changes to ldflags 2018-08-19 19:48:21 +10:00
Jesse Duffield
e6a6301144 bump version to v0.1.71 2018-08-19 19:47:04 +10:00
Jesse Duffield
c5d4024d58 compare error message rather than error itself on no-commits error 2018-08-19 18:51:42 +10:00
Anthony HAMON
f91b4067f4 add test to utils package 2018-08-19 10:37:03 +02:00
Tommy Nguyen
b46d174f70 view_helpers.go: don't ignore return value 2018-08-19 02:21:33 -04:00
Tommy Nguyen
cdc6d45fa4 view_helpers.go: replace \r with \r\n 2018-08-19 02:19:19 -04:00
Jesse Duffield
81b07daa01 Merge branch 'hotfix/167-ambiguous-name-in-diff' 2018-08-19 15:06:06 +10:00
Jesse Duffield
93266cc2a4 support forked branches when getting project root 2018-08-19 15:05:36 +10:00
Jesse Duffield
60fc24eada Merge pull request #172 from jesseduffield/hotfix/167-ambiguous-name-in-diff
167: Support File names that match Branch names
2018-08-19 14:53:14 +10:00
Jesse Duffield
6978785ccf add user email and config to test repo generators 2018-08-19 14:52:08 +10:00
Jesse Duffield
cd9eada0c6 add test for variety of potential git diff situations 2018-08-19 14:48:39 +10:00
Jesse Duffield
7b3679d546 Merge pull request #181 from glvr182/feature/ignore-unused
Cleaned the gitignore and added the linux binary
2018-08-19 13:26:35 +10:00
Jesse Duffield
c8e63894b4 Merge pull request #182 from glvr182/feature/docs-images
Feature/docs images
2018-08-19 13:25:23 +10:00
Jesse Duffield
af93d04479 Merge pull request #183 from mjarkk/master
Removed logrus logger import from i18n
2018-08-19 13:23:29 +10:00
Mark Kopenga
29f2bdbba3 removed logrus logger import 2018-08-18 14:03:20 +02:00
Mark Kopenga
c90e865e34 Merge pull request #10 from jesseduffield/master
Updated to latest master
2018-08-18 14:00:41 +02:00
Jesse Duffield
b104dccab2 Merge pull request #163 from dawidd6/translation/pl
Add Polish translation
2018-08-18 21:08:56 +10:00
Glenn Vriesman
2dbd5be24e Removed duplicates 2018-08-18 13:04:44 +02:00
Glenn Vriesman
0a4cf6a544 Docs: Fixed typo 2018-08-18 12:56:15 +02:00
Glenn Vriesman
5f5e275a0e Docs: Made commit-diffs example a local file 2018-08-18 12:55:13 +02:00
Glenn Vriesman
7b85d146af Docs: Made resolving merge conflicts a local file 2018-08-18 12:53:26 +02:00
Glenn Vriesman
fd01cdb137 Docs: Made the example gif a local file 2018-08-18 12:51:15 +02:00
Glenn Vriesman
9e8dc37308 Docs: Moved slack image to docs/resources 2018-08-18 12:46:50 +02:00
Jesse Duffield
08666889f4 improve remove file logic 2018-08-18 20:14:44 +10:00
Jesse Duffield
13ac1d151a WIP updater package 2018-08-18 19:54:44 +10:00
Glenn Vriesman
c091401571 Cleaned the gitignore and added the linux binary 2018-08-18 11:49:37 +02:00
Jesse Duffield
0174375562 Merge pull request #180 from jesseduffield/hotfix/174-nil-pointer-reference-on-startup
Hotfix for issue 174: avoid nil pointer reference on startup
2018-08-18 19:45:28 +10:00
Jesse Duffield
1f756d3d0a avoid nil pointer reference on startup 2018-08-18 19:43:58 +10:00
Jesse Duffield
6473e5ca3c use runtime package to get GOOS and GOARCH 2018-08-18 17:28:03 +10:00
Jesse Duffield
896dda3adf bump version to v0.1.70 2018-08-18 17:17:22 +10:00
Jesse Duffield
b9ef1a4d67 leave bump commits out of release notes 2018-08-18 17:17:05 +10:00
Jesse Duffield
671c693459 bump version to v0.1.67 2018-08-18 17:12:44 +10:00
Jesse Duffield
41171304b2 add platform/os details to goreleaser ldflags 2018-08-18 17:10:27 +10:00
Jesse Duffield
f025b289f0 enforce white text color in prompt panels 2018-08-18 16:23:19 +10:00
Tommy Nguyen
018b43163c synchronize deps 2018-08-18 02:20:10 -04:00
remyabel
9a923eb300 Merge branch 'master' into 157_remove_bom 2018-08-18 02:11:13 -04:00
Jesse Duffield
3f5c1a4243 bump version to v0.1.66 2018-08-18 16:00:30 +10:00
Jesse Duffield
db7e623c0c bump version to v0.1.65 2018-08-18 16:00:12 +10:00
Jesse Duffield
ad8f02b986 add 386 architecture to releases 2018-08-18 16:00:00 +10:00
Jesse Duffield
29431ddc8e create dashboard 2018-08-18 15:30:56 +10:00
Jesse Duffield
a1a828a781 support opening and editing config file 2018-08-18 14:54:05 +10:00
Jesse Duffield
6b150a4be0 bump dependencies 2018-08-18 14:20:19 +10:00
Jesse Duffield
284c534251 user configurable border colors 2018-08-18 13:53:58 +10:00
Jesse Duffield
10fdb5a609 support writing back to user config 2018-08-18 13:22:05 +10:00
Jesse Duffield
4dc6d40b5a merge master 2018-08-18 12:08:21 +10:00
Jesse Duffield
99d40c2f8e run codecov report after other commands on circle ci 2018-08-18 11:17:31 +10:00
Mark Kopenga
0f145093fe Here i a fix for the error go complain on your pull 2018-08-17 20:43:51 +02:00
Jesse Duffield
bd91b9e1e9 add test repo for all the kinds of files that show up when diffing 2018-08-17 22:46:10 +10:00
Dawid Dziurla
bf5f3bb972 pl translation 3 2018-08-17 14:27:13 +02:00
Jesse Duffield
03a7e32694 support filenames that match branchnames 2018-08-17 22:25:53 +10:00
Dawid Dziurla
b0e08491ac pl translation 2 2018-08-17 14:11:48 +02:00
Jesse Duffield
aaa8558de8 Merge pull request #159 from remyabel/158_escape_backticks
#158: escapes backticks, which is a problem in shells like Bash
2018-08-17 21:11:20 +10:00
Mark Kopenga
633553bcf8 Merge pull request #7 from jesseduffield/master
Update to latest master
2018-08-17 10:33:10 +02:00
Jesse Duffield
933aae7da1 bump version to v0.1.64 2018-08-17 11:15:41 +10:00
Jesse Duffield
3b1689727a bump version to v0.1.63 2018-08-17 11:15:19 +10:00
Jesse Duffield
c08e6d9999 bump version to v0.1.62 2018-08-17 11:14:54 +10:00
Jesse Duffield
dcd3bb6bbd use platform independent command to remove a file or directory 2018-08-17 11:13:21 +10:00
Jesse Duffield
bd15f9d27a Merge pull request #164 from dawidd6/ubuntu/release-ppa
Add info about release PPA
2018-08-17 09:05:31 +10:00
Tommy Nguyen
52033b32f7 Use strings.Replace instead of regexp 2018-08-16 17:04:39 -04:00
Mark Kopenga
5c738fa493 Removed duplicates 2018-08-16 20:59:32 +02:00
Mark Kopenga
9a0f29ff5b Merge pull request #5 from jesseduffield/master
Updated to latest master
2018-08-16 20:08:14 +02:00
Dawid Dziurla
36eed80228 add info about release PPA 2018-08-16 19:26:43 +02:00
Dawid Dziurla
2d80a83d27 pl translation 1 2018-08-16 18:56:16 +02:00
Jesse Duffield
f4e701611b Merge pull request #162 from ponsfrilus/patch-3
Typo path → patch
2018-08-16 23:40:37 +10:00
Nicolas Borboën
ff15d86ced Typo path → patch 2018-08-16 14:15:55 +02:00
Jesse Duffield
8b57b21d79 add circle ci config 2018-08-16 22:07:02 +10:00
remyabel
64d8a55dbd Merge branch 'master' into 157_remove_bom 2018-08-16 07:59:31 -04:00
Jesse Duffield
563efc41c7 bump dependencies for i18n 2018-08-16 21:48:09 +10:00
Jesse Duffield
090537a1f1 Merge pull request #137 from mjarkk/master
Added a view basic translation functions and translation file
2018-08-16 21:46:42 +10:00
Mark Kopenga
fcf616bd62 Fixed it 2018-08-16 13:35:04 +02:00
Tommy Nguyen
74d81ae080 [rebase] Fix errors; update dependencies
Argument must be []byte not string

Don't commit bomtest.txt
2018-08-16 07:17:47 -04:00
Tommy Nguyen
a7755ab184 reformat 2018-08-16 07:00:13 -04:00
Mark Kopenga
faf218f465 Fixed comments from jesseduffield on issue #137 2018-08-16 11:31:50 +02:00
Mark Kopenga
90746502df Fixed comments from jesseduffield on issue #137 2018-08-16 11:31:03 +02:00
Jesse Duffield
df7d1df4cb bump dependencies 2018-08-16 19:20:59 +10:00
Jesse Duffield
5819e04c53 use shibukawa/configdir package to follow xdg spec config directory structure 2018-08-16 19:17:38 +10:00
Jesse Duffield
fd3ce21576 use yaml for config file rather than json 2018-08-16 18:43:46 +10:00
Tommy Nguyen
3a31b84d1a add BOM test generator 2018-08-16 02:00:34 -04:00
Tommy Nguyen
f09515867d #157: clean BOM, allowing CSV files to display correctly 2018-08-16 01:53:53 -04:00
Mark Kopenga
88e1a815fe Fixed comment on issue #137 from @jesseduffield 2018-08-16 07:16:32 +02:00
Tommy Nguyen
db94dde114 fix formatting 2018-08-15 23:58:44 -04:00
Tommy Nguyen
ee4660af97 #158: escapes backticks, which is a problem in shells like Bash 2018-08-15 23:55:55 -04:00
Jesse Duffield
59ab38fff6 Update README.md 2018-08-16 10:37:23 +10:00
Jesse Duffield
adc906bb87 add slack image file 2018-08-16 10:32:52 +10:00
Mark Kopenga
9abbfe5a43 Fully translated pkg/gui/confirmation_panel.go 2018-08-15 15:12:55 +02:00
Mark Kopenga
9112278ab7 Merge pull request #4 from jesseduffield/master
Update to latest master
2018-08-15 15:03:27 +02:00
Mark Kopenga
d00c46a712 Added all english translations to a file and fixed some typos 2018-08-15 14:57:20 +02:00
Jesse Duffield
29ed971558 add user configuration in json file 2018-08-15 22:06:37 +10:00
Jesse Duffield
8d99b400fd factor out code for new popup panels 2018-08-15 21:49:38 +10:00
Jesse Duffield
7c33c02930 dont panic if unable to close confirmation prompt 2018-08-15 21:43:31 +10:00
Mark Kopenga
50b41bfccc Translated pkg/gui/view_helpers.go 2018-08-15 11:49:43 +02:00
Mark Kopenga
295093a432 Translated pkg/gui/stash_panel.go 2018-08-15 11:12:46 +02:00
Mark Kopenga
10c53162ca Merge pull request #3 from jesseduffield/master
Updated to latest master
2018-08-15 10:54:43 +02:00
Mark Kopenga
7e926cf41d Added translation for pkg/gui/ confirmation_panel.go gui.go merge_panel.go 2018-08-15 10:53:05 +02:00
Mark Kopenga
d12cc5a74e Fully translated pkg/gui/commits_panel.go 2018-08-15 10:30:29 +02:00
Mark Kopenga
8418fa17a5 Fully translated pkg/gui/commit_message_panel.go 2018-08-15 09:15:31 +02:00
Jesse Duffield
905e6c16ba add credential helper to config of test repo 2018-08-15 15:15:21 +10:00
Jesse Duffield
db140842f3 bump version to v0.1.61 2018-08-15 09:56:15 +10:00
Jesse Duffield
7002ee5d9b bump version to v0.1.60 2018-08-15 09:52:23 +10:00
Jesse Duffield
0687c07f50 more robust reading of reflog to get branch name 2018-08-15 09:45:24 +10:00
Jesse Duffield
922f4ed5a4 try rev-parse of git symbolic-ref returns nothing 2018-08-15 09:42:02 +10:00
Mark Kopenga
3dba246029 Added translations for files_panel.go and fixed some typos 2018-08-14 22:29:17 +02:00
Mark Kopenga
be3f5846e4 Added more translations 2018-08-14 21:06:50 +02:00
Mark Kopenga
38a1a00cf1 Fixed comment from myself on issue: 137 2018-08-14 16:38:25 +02:00
Mark Kopenga
9c97b75aad Merge remote-tracking branch 'origin/master' 2018-08-14 16:14:11 +02:00
Mark Kopenga
883f436b0f can't go any further because of an error 2018-08-14 16:12:21 +02:00
Jesse Duffield
d923796cff Merge branch 'master' of https://github.com/mjarkk/lazygit 2018-08-14 23:48:18 +10:00
Jesse Duffield
ba2b6fbf1f pull errors out of package scope and store sentinel errors on the gui struct 2018-08-14 23:47:14 +10:00
Mark Kopenga
73a1682540 fixed package naming and added tr object to file_panel.go 2018-08-14 15:26:25 +02:00
Mark Kopenga
8e22d569a0 Merge pull request #2 from jesseduffield/master
Updated to latest master
2018-08-14 15:10:59 +02:00
Jesse Duffield
4d0702fba5 Merge branch 'master' of https://github.com/mjarkk/lazygit 2018-08-14 22:12:11 +10:00
Jesse Duffield
5cbacb0c67 make local i18n package confirm to project structure 2018-08-14 22:12:07 +10:00
Mark Kopenga
0568b32f0b Added more translations 2018-08-14 13:31:23 +02:00
Mark Kopenga
6e518142b4 added some commands 2018-08-14 12:56:11 +02:00
Mark Kopenga
0c39347224 Added auto detection for the system language 2018-08-14 12:52:26 +02:00
Mark Kopenga
dd7e93ac8d Added all the missing translations from dutch.go 2018-08-14 11:35:39 +02:00
Mark Kopenga
f792f74f0f Merge remote-tracking branch 'origin/master' 2018-08-14 11:28:11 +02:00
Mark Kopenga
5ad97add08 Added the translation to some words again 2018-08-14 11:27:46 +02:00
Jesse Duffield
652237d48f bump version to v0.1.59 2018-08-14 19:23:05 +10:00
Jesse Duffield
6a3f8eefa5 send version number through app 2018-08-14 19:22:38 +10:00
Mark Kopenga
17dcfbcc6f Merge pull request #1 from jesseduffield/master
Update to latest master
2018-08-14 11:06:34 +02:00
Mark Kopenga
dfafb98871 tried to update to latest master 2018-08-14 11:05:26 +02:00
Jesse Duffield
8cc8c4c228 bump version to v0.1.58 2018-08-14 18:49:15 +10:00
Jesse Duffield
574b34930c support files with spaces in name 2018-08-14 18:48:08 +10:00
Jesse Duffield
77080f44a4 bump version to v0.1.57 2018-08-14 18:38:04 +10:00
Jesse Duffield
97daa5eeb2 bump version to v0.1.56 2018-08-14 18:36:51 +10:00
Jesse Duffield
c476dfc1cb fix open command 2018-08-14 18:35:47 +10:00
Jesse Duffield
bde6182c94 better git squash logic 2018-08-14 18:30:06 +10:00
Jesse Duffield
efb049cd24 better error handling for stashing files 2018-08-14 18:29:25 +10:00
Jesse Duffield
88af0fb1b6 add gpg repo generator 2018-08-14 18:12:09 +10:00
Jesse Duffield
5f30f07ea5 Merge branch 'master' into feature/quoted-messages 2018-08-14 18:10:44 +10:00
Jesse Duffield
c02f474bf3 update dependencies 2018-08-14 18:05:18 +10:00
Jesse Duffield
b32173b0c7 output to development.log in append mode 2018-08-14 18:02:27 +10:00
Jesse Duffield
1c750fdb40 fix reflog command 2018-08-14 18:02:14 +10:00
Jesse Duffield
9ecd7908aa refactor commands to depend less on the shell 2018-08-14 17:47:33 +10:00
Jesse Duffield
ad3d332e05 Merge pull request #143 from jesseduffield/feature/clean_up
Platform should only be present once
2018-08-14 12:33:47 +10:00
Andrei Miulescu
842ceec9b0 Platform should only be present once 2018-08-14 12:24:32 +10:00
Jesse Duffield
95c7df4c61 use platform-specific message quoting 2018-08-14 11:16:52 +10:00
Jesse Duffield
c0a1f90604 Merge pull request #132 from jesseduffield/feature/project_restructure
Project Restructure Episode 1
2018-08-14 09:02:38 +10:00
Jesse Duffield
15528dc341 Merge branch 'master' into feature/project_restructure 2018-08-14 08:44:39 +10:00
Jesse Duffield
7ecbd7fbb3 update pre-commit hook 2018-08-14 08:42:08 +10:00
Jesse Duffield
45f640941c update dependencies 2018-08-14 08:34:31 +10:00
Jesse Duffield
047892962a centralise subprocess code to gui.go 2018-08-14 08:33:40 +10:00
Jesse Duffield
d4f4b46a1f check both local and global config for gpgsign 2018-08-14 08:33:27 +10:00
Jesse Duffield
f549ad0f37 use git command with message in subprocess if using gpgsign 2018-08-14 07:27:59 +10:00
Mark Kopenga
f2dfcb6e12 Added more messages and text issue: #137 2018-08-13 22:00:44 +02:00
Mark Kopenga
65eb3780a0 fixed typo 2018-08-13 20:48:49 +02:00
Mark Kopenga
d9959eb998 fixed @jesseduffield comment #137 2018-08-13 20:46:53 +02:00
Jesse Duffield
ebfed34145 add PR #135 keybindings to this branch 2018-08-13 23:46:08 +10:00
Jesse Duffield
450f269f02 Merge pull request #135 from jon-grangien/feature/ctrl-keybindings
Add C-u and C-d keybindings for scrolling up and down diff
2018-08-13 23:43:54 +10:00
Jesse Duffield
95d2b02664 update gitignore 2018-08-13 23:36:06 +10:00
Jesse Duffield
59e5545f38 discard log output when not in debug mode 2018-08-13 23:35:01 +10:00
mjarkk
62fc831407 Added a view basic translation functions + tranlation file 2018-08-13 15:33:45 +02:00
Jesse Duffield
fb0004481b correct cursor before returning item position 2018-08-13 21:42:56 +10:00
Jesse Duffield
3bd0246e4d add test repo contents to gitignore 2018-08-13 21:42:31 +10:00
Jesse Duffield
12de0345e4 minor cleanup 2018-08-13 21:35:54 +10:00
Jesse Duffield
9e725ae24e got this bad boy compiling again 2018-08-13 21:16:21 +10:00
Jesse Duffield
97cff65612 progress on refactor 2018-08-13 20:26:02 +10:00
codingInSpace
a32fc34a49 Add C-u and C-d keybindings for scrolling up and down diff 2018-08-13 04:09:32 -04:00
Jesse Duffield
f9c39ad64b add newline to version output 2018-08-13 17:20:03 +10:00
Jesse Duffield
1571e6b962 add newline to version output 2018-08-13 12:27:26 +10:00
Jesse Duffield
84b27d402d Update issue templates 2018-08-12 23:46:38 +10:00
Jesse Duffield
e6beb5d50b no more go-git for committing (reflecting the change in master) 2018-08-12 23:29:58 +10:00
Jesse Duffield
1354885db0 bump version to v0.1.55 2018-08-12 23:20:10 +10:00
Jesse Duffield
360bfa4642 bump version to v0.1.54 2018-08-12 23:17:47 +10:00
Jesse Duffield
bb0e580a27 bump version to v0.1.53 2018-08-12 23:17:25 +10:00
Jesse Duffield
1eb0061357 bump dependencies 2018-08-12 23:16:58 +10:00
Jesse Duffield
4cd2d70659 revert to using cli git command rather than go-git for committing 2018-08-12 23:14:37 +10:00
Jesse Duffield
35d695fd0c update gitignore to reflect new test repo directory structure 2018-08-12 23:00:51 +10:00
Andrei Miulescu
e8eb78617c Mid refactor change some more stuff 2018-08-12 21:04:47 +10:00
Andrei Miulescu
e65ddd7b6f Move some commands around 2018-08-12 20:22:20 +10:00
Jesse Duffield
c01bc09442 WIP refactor 2018-08-12 19:50:55 +10:00
Andrei Miulescu
dcd461d29f Restrucure project in a way where it is more modular 2018-08-12 19:31:27 +10:00
Jesse Duffield
98c22a36fd rearrange test repo generators 2018-08-12 15:54:59 +10:00
Jesse Duffield
7bfd8155c2 bump version to v0.1.52 2018-08-12 12:01:50 +10:00
Jesse Duffield
ed1518fd66 fix CPU issues caused by resizing popup panels 2018-08-12 12:00:30 +10:00
Jesse Duffield
400301e8fa bump gocui fork to support getting view dimensions 2018-08-12 11:39:37 +10:00
Jesse Duffield
f97a098c6f Merge branch 'master' of https://github.com/jesseduffield/lazygit 2018-08-12 11:05:24 +10:00
Jesse Duffield
2b3bdc8b8f rename merge conflict test repo 2018-08-12 11:05:21 +10:00
Jesse Duffield
7323e08fdb Merge pull request #127 from ponsfrilus/master
Proposal for a slightly better README.md
2018-08-12 00:51:31 +10:00
Jesse Duffield
838ed4f170 Merge pull request #104 from maxice8/add-void-readme
README.md: Add Void Linux instructions
2018-08-12 00:43:43 +10:00
Jesse Duffield
feba6b5318 Merge pull request #129 from ponsfrilus/patch-2
Use <kbd> tags
2018-08-12 00:42:50 +10:00
Nicolas Borboën
27234c1cab Use <kbd> tags
and that close #128
2018-08-11 12:13:14 +02:00
Jesse Duffield
8f6329c402 if you are using gpgsign, must use git editor to commit 2018-08-11 19:28:32 +10:00
Nicolas Borboën
0ac56f5ade Proposal for a slighty bettre README.md
* Text wrapped to 80 chars;
* Kind of table of content;
* Launchpad PPA link added;
* AUR package on a bullet list;
* Missing ponctuation added;
* Youtube tutorial and keybindings on a bullet list.
2018-08-11 11:11:55 +02:00
Jesse Duffield
471a7de418 bump version to v0.1.51 2018-08-11 17:08:20 +10:00
Jesse Duffield
77191ea67c show output line by line in deploy script 2018-08-11 17:07:56 +10:00
Jesse Duffield
176aa62fe0 bump version to v0.1.50 2018-08-11 17:06:50 +10:00
Jesse Duffield
1419be1aab support commit message via git editor using shift+C keybinding 2018-08-11 16:55:39 +10:00
Jesse Duffield
0ac306fe2a fix whitespace 2018-08-11 16:55:15 +10:00
Jesse Duffield
f0840c0f46 fallback to vi when trying to edit file and no EDITOR/VISUAL is defined 2018-08-11 16:55:05 +10:00
Jesse Duffield
6d4d6d3ac0 bump version to v0.1.49 2018-08-11 16:22:49 +10:00
Jesse Duffield
2dba6f6733 support case insensitive branch names 2018-08-11 16:11:17 +10:00
Jesse Duffield
aec125df70 Merge branch 'master' into add-void-readme 2018-08-11 15:50:42 +10:00
Jesse Duffield
9626ebdf35 correct typo in keybinding for delete branch 2018-08-11 15:45:38 +10:00
Jesse Duffield
2c140445e5 Merge branch 'master' into feature/deleting-branches 2018-08-11 15:43:56 +10:00
Jesse Duffield
62a231abb7 handle commit select on any commits panel refresh 2018-08-11 15:34:57 +10:00
Jesse Duffield
753ca75e55 clear commit panel after confirming 2018-08-11 15:26:02 +10:00
Jesse Duffield
5c8f0d4c9d gitignore test repo 2018-08-11 15:23:57 +10:00
Jesse Duffield
000a709783 update test repo generators including test for unicode characters 2018-08-11 15:22:41 +10:00
Jesse Duffield
5c185572a7 delete test file that was accidentally committed 2018-08-11 15:12:30 +10:00
Jesse Duffield
bb86d527e1 Merge branch 'master' of https://github.com/jesseduffield/lazygit 2018-08-11 15:12:10 +10:00
Jesse Duffield
0e3dec1069 Merge branch 'master' into feature/add-fixup-option-into-commits-panel 2018-08-11 15:11:19 +10:00
Jesse Duffield
7e141283f6 merge branch master 2018-08-11 15:09:37 +10:00
Jesse Duffield
e46f3b3393 add spew dependency to vendor 2018-08-11 15:04:42 +10:00
Jesse Duffield
73e740d1ba clean up fixup code
reduce log clutter
add log dumping with spew
2018-08-11 15:04:02 +10:00
Jesse Duffield
f1eaeec9ee remove time logging 2018-08-11 14:47:11 +10:00
Jesse Duffield
a713fff362 Merge pull request #98 from frealgagu/master
Adding arch linux packages to README.md
2018-08-11 14:04:44 +10:00
Jesse Duffield
6d3d40c41f handle error on confirmation panel resize 2018-08-11 13:27:34 +10:00
Jesse Duffield
2137d3f6d2 Merge branch 'feature/multiline-commit-restoring' of https://github.com/jesseduffield/lazygit into feature/multiline-commit-restoring 2018-08-11 13:24:15 +10:00
Jesse Duffield
8ae346787a revert use of stored values in confirmation panels 2018-08-11 13:24:05 +10:00
Jesse Duffield
b8daf71db6 Delete ZHgalGrWSF 2018-08-11 13:19:54 +10:00
Jesse Duffield
fd13cf14b4 Merge branch 'feature/multiline-commit-restoring' 2018-08-11 13:19:17 +10:00
Jesse Duffield
47bf649a69 switch focus back to files view after confirming commit message 2018-08-11 13:18:33 +10:00
Jesse Duffield
3b018e040f make
commit
messages
multilined
and add ability save commit message between edits
2018-08-11 13:17:20 +10:00
Jesse Duffield
b2fbccd392 remove time logging 2018-08-11 12:35:17 +10:00
Fredy Alberto García Güiza
542fd8e88a Fixing grammar about arch linux packages in README.md 2018-08-10 19:47:16 -05:00
Jesse Duffield
9fbc332c9a Merge pull request #114 from glvr182/feature/add-repo-name
Feature: Added repository name to status
2018-08-11 09:51:11 +10:00
Jesse Duffield
1ef794e09f Update README.md 2018-08-11 09:43:22 +10:00
Hubert Baumgartner
bfa47d3b91 extenden delete branch with error message and confirmation 2018-08-10 20:19:58 +02:00
Hubert Baumgartner
48cea4e1c4 added branch delete functionallity 2018-08-10 16:46:03 +02:00
Jesse Duffield
7aa884ed8f step one on restoring multiline commits 2018-08-10 23:36:54 +10:00
Glenn Vriesman
7eb673e574 Feature: Added repository name to status 2018-08-10 15:10:15 +02:00
Jesse Duffield
f20121cb0b add FIXME note about trimming the trailing newline upon confirming in a prompt panel 2018-08-10 23:08:03 +10:00
Jesse Duffield
aa4160d57a merge feature/clearing-commit-panel into master 2018-08-10 22:49:54 +10:00
Jesse Duffield
8bc6832546 Merge branch 'master' of https://github.com/jesseduffield/lazygit 2018-08-10 22:46:24 +10:00
Jesse Duffield
4853d186ee add git plumbing to Gopkg.lock 2018-08-10 22:46:04 +10:00
Jesse Duffield
c3611df8d8 Merge pull request #112 from jesseduffield/feature/case-insensitive-branch-name-comparisons
101: Compare branches for name equality regardless of case
2018-08-10 22:26:44 +10:00
Jesse Duffield
59650cff26 compare branches for name equality regardless of case 2018-08-10 22:24:10 +10:00
Jesse Duffield
f9abcdb159 Merge branch 'feature/branch-refactor' 2018-08-10 22:13:48 +10:00
Jesse Duffield
28505dddaf move color functions into utils 2018-08-10 22:09:10 +10:00
Jesse Duffield
95b7c1d0a4 revert to using the direct git command for getting the current branch because it breaks if you've just done a git init 2018-08-10 22:08:12 +10:00
Jesse Duffield
eb9f01ecfa move withPadding into utils file 2018-08-10 21:51:21 +10:00
Jesse Duffield
a8c6f31bc6 Merge pull request #107 from antham/master
Apply gofmt
2018-08-10 21:42:50 +10:00
Jesse Duffield
1fded005c4 revert to using default border color 2018-08-10 21:40:21 +10:00
Jesse Duffield
c470c1f575 keep asterisk at the checked out branch 2018-08-10 21:38:51 +10:00
Jesse Duffield
d08241b2ea Obtain branches in a more robust way. Begin refactor work on gitcommands 2018-08-10 21:34:17 +10:00
Anthony HAMON
de7ab80539 apply gofmt -s -w 2018-08-10 09:54:21 +02:00
Ching-Hsuan Yen
d3abd9eab1 Add fixup option into commits panel 2018-08-10 13:44:42 +08:00
Jesse Duffield
3f89b5bf71 Merge pull request #105 from jesseduffield/feature/add-go-git
Explicitly add go-git dependency
2018-08-10 11:51:51 +10:00
Jesse Duffield
87872f5514 explicitly add go-git dependency 2018-08-10 11:33:49 +10:00
maxice8
bf9a53fada README.md: Add Void Linux instructions 2018-08-09 19:39:18 -03:00
frealgagu
3adf5368dd Adding arch linux packages to README.md 2018-08-09 09:13:24 -05:00
Jesse Duffield
ed6f21ee74 bump version to v0.1.48 2018-08-09 23:35:06 +10:00
Jesse Duffield
75a186614a add scroll keybinding to displayed files panel options 2018-08-09 23:34:50 +10:00
Jesse Duffield
18aa5b1bbe bump version to v0.1.47 2018-08-09 23:34:16 +10:00
Jesse Duffield
89209d1253 bump version to v0.1.46 2018-08-09 23:27:11 +10:00
Jesse Duffield
bcad80250a add basic vim keybindings
add keybinding for the tab key to begin a newline when writing a commit message
it would have been shift+enter but currently that is not supported by gocui
2018-08-09 23:26:31 +10:00
Jesse Duffield
44eaccfd14 switch to using the log package rather than directly writing to file 2018-08-09 20:29:58 +10:00
Jesse Duffield
9537645d0c dynamic width for confirmation panel and better handling of a squashed terminal 2018-08-09 19:49:36 +10:00
Jesse Duffield
d08e3a55a1 WIP hard reset 2018-08-09 19:33:35 +10:00
Jesse Duffield
2cd0bd8125 Merge branch 'master' of https://github.com/jesseduffield/lazygit 2018-08-09 19:02:19 +10:00
Jesse Duffield
cc31cb1abe remove double quote escapes in commit message for auto version bumper 2018-08-09 19:02:16 +10:00
Jesse Duffield
a56643fe64 refactor repeater functions and refresh file panel every ten seconds 2018-08-09 19:01:42 +10:00
Jesse Duffield
2386283e45 Update README.md 2018-08-09 16:07:25 +10:00
Jesse Duffield
58977ed7f3 remove the dist folder after goreleaser runs 2018-08-09 15:47:55 +10:00
Jesse Duffield
a068548bcb remove speed monitoring 2018-08-09 15:44:28 +10:00
Jesse Duffield
b2f8b8b345 "bump version to v0.1.45" 2018-08-09 15:24:39 +10:00
Jesse Duffield
5f70b2d9cd throw error when git username not configured 2018-08-09 15:24:12 +10:00
Jesse Duffield
f33b2b2277 "bump version to v0.1.44" 2018-08-09 14:46:10 +10:00
Jesse Duffield
dbf65a422a bump dependencies 2018-08-09 14:41:58 +10:00
Jesse Duffield
ace8544512 create log if debugging and the file doesn't already exist 2018-08-09 14:36:26 +10:00
Jesse Duffield
4832d365f1 use go-git for commits 2018-08-09 14:33:51 +10:00
Jesse Duffield
bebe94b4b3 don't call projectPath until we know we're in debug mode 2018-08-09 13:37:48 +10:00
Jesse Duffield
750445dc8b use platform agnostic filepaths 2018-08-09 13:29:25 +10:00
Jesse Duffield
1f5f80b1bf platform independent path reading 2018-08-09 13:21:30 +10:00
Jesse Duffield
5cc34e7801 factor out platform specific logic into a struct on state 2018-08-09 13:14:24 +10:00
Jesse Duffield
ce8884f509 "bump version to v0.1.43" 2018-08-09 12:56:11 +10:00
Jesse Duffield
481a05f116 check for VERSION file in project directory rather than current directory 2018-08-09 12:55:41 +10:00
Jesse Duffield
6e8abbcdda use fallback file VERSION 2018-08-09 12:25:32 +10:00
Jesse Duffield
273eb6244b "bump version to v0.1.42" 2018-08-09 12:10:33 +10:00
Jesse Duffield
467741fa54 add basic script to bump patch number and call goreleaser 2018-08-09 12:10:22 +10:00
Jesse Duffield
a00bbf709f "bump version to v0.1.41" 2018-08-09 12:09:30 +10:00
Jesse Duffield
b90e66dca1 "bump version to v0.1.40" 2018-08-09 12:02:37 +10:00
Jesse Duffield
77eea90b0b "bump version to v0.1.39" 2018-08-09 12:00:23 +10:00
Jesse Duffield
79b012990b Update README.md 2018-08-09 11:07:41 +10:00
Jesse Duffield
45c0b9404a use zip for windows binaries 2018-08-09 11:05:10 +10:00
Jesse Duffield
50924f31b0 Merge branch 'master' of https://github.com/jesseduffield/lazygit 2018-08-09 11:00:57 +10:00
Jesse Duffield
705d273ad3 test comment to see if goreleaser only releases on new commits 2018-08-09 11:00:50 +10:00
Jesse Duffield
30c6fc2300 Update README.md 2018-08-09 10:48:57 +10:00
Jesse Duffield
16b202f227 Update README.md 2018-08-09 10:46:57 +10:00
Jesse Duffield
7c1dbb7c66 Update README.md 2018-08-09 10:46:27 +10:00
1083 changed files with 239054 additions and 2940 deletions

73
.circleci/config.yml Normal file
View File

@@ -0,0 +1,73 @@
version: 2
jobs:
build:
docker:
- image: circleci/golang:1.11
working_directory: /go/src/github.com/jesseduffield/lazygit
steps:
- checkout
- run:
name: Ensure go.mod file is up to date
command: |
export GO111MODULE=on
rm go.sum
mv go.mod /tmp/
go mod init
export GO111MODULE=auto
if [ $(diff /tmp/go.mod go.mod|wc -l) -gt 0 ]; then
diff /tmp/go.mod go.mod
exit 1;
fi
- run:
name: Run gofmt -s
command: |
if [ $(find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;|wc -l) -gt 0 ]; then
find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;
exit 1;
fi
- restore_cache:
keys:
- pkg-cache-{{ checksum "Gopkg.lock" }}-v3
- run:
name: Run tests
command: |
./test.sh
- run:
name: Compile project on every platform
command: |
go get github.com/mitchellh/gox
gox -parallel 10 -os "linux freebsd netbsd windows" -osarch "darwin/i386 darwin/amd64"
- run:
name: Push on codecov result
command: |
bash <(curl -s https://codecov.io/bash)
- save_cache:
key: pkg-cache-{{ checksum "Gopkg.lock" }}-v3
paths:
- ~/.cache/go-build
release:
docker:
- image: circleci/golang:1.10
working_directory: /go/src/github.com/jesseduffield/lazygit
steps:
- checkout
- run:
name: Run gorelease
command: |
curl -sL https://git.io/goreleaser | bash
workflows:
version: 2
build:
jobs:
- build
release:
jobs:
- release:
filters:
tags:
only: /v[0-9]+(\.[0-9]+)*/
branches:
ignore: /.*/

29
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,29 @@
---
name: Bug report
about: Create a report to help us improve
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. Windows]
- Lazygit Version [e.g. v0.1.45]
- The last commit id if you built project from sources (run : ```git-rev parse HEAD```)
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

34
.gitignore vendored
View File

@@ -1,6 +1,28 @@
development.log
commands.log
extra/lgit.rb
notes/go.notes
TODO.notes
TODO.md
# Please do not add personal files
# Logs
*.log
# Hidden
.*
# TODO
TODO.*
# Notes
*.notes
# Tests
test/repos/repo
coverage.txt
# Binaries
lazygit
# Exceptions
!.gitignore
!.goreleaser.yml
!.circleci/
!.github/
test/git_server/data

View File

@@ -1,34 +1,43 @@
# This is an example goreleaser.yaml file with some sane defaults.
# Make sure to check the documentation at http://goreleaser.com
builds:
- env:
- CGO_ENABLED=0
goos:
- freebsd
- windows
- darwin
- linux
goarch:
- amd64
- arm
- arm64
- env:
- CGO_ENABLED=0
goos:
- freebsd
- windows
- darwin
- linux
goarch:
- amd64
- arm
- arm64
- 386
# Default is `-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}`.
ldflags:
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.buildSource=binaryRelease
archive:
replacements:
darwin: Darwin
linux: Linux
windows: Windows
386: i386
386: 32-bit
amd64: x86_64
format_overrides:
- goos: windows
format: zip
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ .Tag }}-next"
name_template: '{{ .Tag }}-next'
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
- '^docs:'
- '^test:'
- '^bump'
brew:
# Reporitory to push the tap to.
github:
@@ -37,18 +46,19 @@ brew:
# Your app's homepage.
# Default is empty.
homepage: "https://github.com/jesseduffield/lazygit/"
homepage: 'https://github.com/jesseduffield/lazygit/'
# Your app's description.
# Default is empty.
description: "A simple terminal UI for git commands, written in Go"
description: 'A simple terminal UI for git commands, written in Go'
# # Packages your package depends on.
# dependencies:
# - git
# - zsh
# # Packages that conflict with your package.
# conflicts:
# - svn
# - bash
# test comment to see if goreleaser only releases on new commits

View File

@@ -15,9 +15,10 @@ welcome your pull requests:
1. Fork the repo and create your branch from `master`.
2. If you've added code that should be tested, add tests.
3. If you've added code that need documentation, update the documentation.
4. Be sure to test your modifications.
5. Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
6. Issue that pull request!
4. Make sure your code follows the [effective go](https://golang.org/doc/effective_go.html) guidelines as much as possible.
5. Be sure to test your modifications.
6. Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
7. Issue that pull request!
## Code of conduct
Please note by participating in this project, you agree to abide by the [code of conduct].

15
Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
# run with:
# docker build -t lazygit .
# 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
WORKDIR /go/src/github.com/jesseduffield/lazygit/
COPY --from=0 /go/src/github.com/jesseduffield/lazygit /go/src/github.com/jesseduffield/lazygit
COPY --from=0 /go/src/github.com/jesseduffield/lazygit/lazygit /bin/
RUN echo "alias gg=lazygit" >> ~/.profile

588
Gopkg.lock generated
View File

@@ -1,6 +1,85 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
digest = "1:e24ea5dbc89fbab51635ee32e5be4f61a9267cae20788efcae4c07efb4abec99"
name = "github.com/aws/aws-sdk-go"
packages = [
"aws",
"aws/awserr",
"aws/awsutil",
"aws/client",
"aws/client/metadata",
"aws/corehandlers",
"aws/credentials",
"aws/credentials/ec2rolecreds",
"aws/credentials/endpointcreds",
"aws/credentials/stscreds",
"aws/csm",
"aws/defaults",
"aws/ec2metadata",
"aws/endpoints",
"aws/request",
"aws/session",
"aws/signer/v4",
"internal/sdkio",
"internal/sdkrand",
"internal/sdkuri",
"internal/shareddefaults",
"private/protocol",
"private/protocol/eventstream",
"private/protocol/eventstream/eventstreamapi",
"private/protocol/query",
"private/protocol/query/queryutil",
"private/protocol/rest",
"private/protocol/restxml",
"private/protocol/xml/xmlutil",
"service/s3",
"service/sts",
]
pruneopts = "NUT"
revision = "4324bc9d8865bdb3e6aa86ec7772ca1272d2750e"
version = "v1.15.21"
[[projects]]
branch = "master"
digest = "1:37011b20a70e205b93ebea5287e1afa5618db54bf3998c36ff5a8e4b146a170a"
name = "github.com/bgentry/go-netrc"
packages = ["netrc"]
pruneopts = "NUT"
revision = "9fd32a8b3d3d3f9d43c341bfe098430e07609480"
[[projects]]
branch = "master"
digest = "1:cd7ba2b29e93e2a8384e813dfc80ebb0f85d9214762e6ca89bb55a58092eab87"
name = "github.com/cloudfoundry/jibber_jabber"
packages = ["."]
pruneopts = "NUT"
revision = "bcc4c8345a21301bf47c032ff42dd1aae2fe3027"
[[projects]]
digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39"
name = "github.com/davecgh/go-spew"
packages = ["spew"]
pruneopts = "NUT"
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
digest = "1:de4a74b504df31145ffa8ca0c4edbffa2f3eb7f466753962184611b618fa5981"
name = "github.com/emirpasic/gods"
packages = [
"containers",
"lists",
"lists/arraylist",
"trees",
"trees/binaryheap",
"utils",
]
pruneopts = "NUT"
revision = "f6c17b524822278a87e3b3bd809fec33b51f5b46"
version = "v1.9.0"
[[projects]]
digest = "1:ade392a843b2035effb4b4a2efa2c3bab3eb29b992e98bacf9c898b0ecb54e45"
name = "github.com/fatih/color"
@@ -9,6 +88,30 @@
revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4"
version = "v1.7.0"
[[projects]]
digest = "1:1b91ae0dc69a41d4c2ed23ea5cffb721ea63f5037ca4b81e6d6771fbb8f45129"
name = "github.com/fsnotify/fsnotify"
packages = ["."]
pruneopts = "NUT"
revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9"
version = "v1.4.7"
[[projects]]
digest = "1:ea1d5bfdb4ec5c2ee48c97865e6de1a28fa8c4849a3f56b27d521aa619038e06"
name = "github.com/go-errors/errors"
packages = ["."]
pruneopts = "NUT"
revision = "a6af135bd4e28680facf08a3d206b454abc877a4"
version = "v1.0.1"
[[projects]]
digest = "1:74d9b0a7b4107b41e0ade759fac64502876f82d29fb23d77b3dd24b194ee3dd5"
name = "github.com/go-ini/ini"
packages = ["."]
pruneopts = "NUT"
revision = "5cf292cae48347c2490ac1a58fe36735fb78df7e"
version = "v1.38.2"
[[projects]]
branch = "master"
digest = "1:4a8ed9b8cf22bd03bee5d74179fa06a282e4a73b6de949f7a865ff56cd2537e0"
@@ -19,11 +122,133 @@
[[projects]]
branch = "master"
digest = "1:82b3bbc50ba7b6065b0229ebf0fc990a76e47410acd510294e435aeb3523293e"
digest = "1:a5d940c38bf56f121721bfa747c66356df387cb9d5318c570c6d4170aab62862"
name = "github.com/hashicorp/go-cleanhttp"
packages = ["."]
pruneopts = "NUT"
revision = "d5fe4b57a186c716b0e00b8c301cbd9b4182694d"
[[projects]]
branch = "master"
digest = "1:b634d733abf079dc191d359e5a8d31479f1795d00e656f8a018a459571046266"
name = "github.com/hashicorp/go-getter"
packages = ["helper/url"]
pruneopts = "NUT"
revision = "4bda8fa99001c61db3cad96b421d4c12a81f256d"
[[projects]]
branch = "master"
digest = "1:fbab03227343a0285fc74a68dd2ff46cda7edecbbe5a3e98d2cecd00cc67b217"
name = "github.com/hashicorp/go-safetemp"
packages = ["."]
pruneopts = "NUT"
revision = "b1a1dbde6fdc11e3ae79efd9039009e22d4ae240"
[[projects]]
digest = "1:0b06ffe0c0764e413a6738e3f045d6bb14117359aef80a09f8c60fbff2ecad6b"
name = "github.com/hashicorp/go-version"
packages = ["."]
pruneopts = "NUT"
revision = "b5a281d3160aa11950a6182bd9a9dc2cb1e02d50"
version = "v1.0.0"
[[projects]]
branch = "master"
digest = "1:11c6c696067d3127ecf332b10f89394d386d9083f82baf71f40f2da31841a009"
name = "github.com/hashicorp/hcl"
packages = [
".",
"hcl/ast",
"hcl/parser",
"hcl/printer",
"hcl/scanner",
"hcl/strconv",
"hcl/token",
"json/parser",
"json/scanner",
"json/token",
]
pruneopts = "NUT"
revision = "ef8a98b0bbce4a65b5aa4c368430a80ddc533168"
[[projects]]
branch = "master"
digest = "1:d457d39e88f678ed14ac29517c3d74927a48dbc6a9f073fa241cf364a68cbe5c"
name = "github.com/heroku/rollrus"
packages = ["."]
pruneopts = "NUT"
revision = "fc0cef2ff331aebb24cd4e9ded7e20650f3d7006"
[[projects]]
branch = "master"
digest = "1:62fe3a7ea2050ecbd753a71889026f83d73329337ada66325cbafd5dea5f713d"
name = "github.com/jbenet/go-context"
packages = ["io"]
pruneopts = "NUT"
revision = "d14ea06fba99483203c19d92cfcd13ebe73135f4"
[[projects]]
branch = "master"
digest = "1:490643e333b848f3d6ab772c21082d706663dcf4a3c0fbe9a4b4ef7b205ce6c7"
name = "github.com/jesseduffield/go-getter"
packages = ["."]
pruneopts = "NUT"
revision = "906e15686e6309ff310c1c10463ab53287c3a678"
[[projects]]
branch = "master"
digest = "1:31a87f65dc451471f411d04742d2cb5ab79a699b8c73666b8fc29f47a8f43f7e"
name = "github.com/jesseduffield/gocui"
packages = ["."]
pruneopts = "NUT"
revision = "3c923f53ac9952af649af04a067405843558d56f"
revision = "b502ee11d6743144c86226ca0366adaed727214d"
[[projects]]
branch = "master"
digest = "1:a46c2f4863e5284ddb255c28750298e04bc8c0fc896bed6056e947673168b7be"
name = "github.com/jesseduffield/pty"
packages = ["."]
pruneopts = "NUT"
revision = "02db52c7e406c7abec44c717a173c7715e4c1b62"
[[projects]]
branch = "master"
digest = "1:3ab130f65766f5b7cc944d557df31c6a007ec017151705ec1e1b8719f2689021"
name = "github.com/jesseduffield/termbox-go"
packages = ["."]
pruneopts = "NUT"
revision = "1e272ff78dcb4c448870f464fda1cdcf2bf0b3dd"
[[projects]]
digest = "1:ac6d01547ec4f7f673311b4663909269bfb8249952de3279799289467837c3cc"
name = "github.com/jmespath/go-jmespath"
packages = ["."]
pruneopts = "NUT"
revision = "0b12d6b5"
[[projects]]
branch = "master"
digest = "1:263f9b0a0bcbfff9d5e7d9f2aa11f53995d98214fe0fb97e429e7a5f4534a0f9"
name = "github.com/kardianos/osext"
packages = ["."]
pruneopts = "NUT"
revision = "ae77be60afb1dcacde03767a8c37337fad28ac14"
[[projects]]
digest = "1:8021af4dcbd531ae89433c8c3a6520e51064114aaf8eb1724c3cf911c497c9ba"
name = "github.com/kevinburke/ssh_config"
packages = ["."]
pruneopts = "NUT"
revision = "9fc7bb800b555d63157c65a904c86a2cc7b4e795"
version = "0.4"
[[projects]]
digest = "1:d244f8666a838fe6ad70ec8fe77f50ebc29fdc3331a2729ba5886bef8435d10d"
name = "github.com/magiconair/properties"
packages = ["."]
pruneopts = "NUT"
revision = "c2353362d570a7bfa228149c62842019201cfb71"
version = "v1.8.0"
[[projects]]
digest = "1:08c231ec84231a7e23d67e4b58f975e1423695a32467a362ee55a803f9de8061"
@@ -50,12 +275,183 @@
version = "v0.0.2"
[[projects]]
branch = "master"
digest = "1:34d9354c2c5d916c05864327553047df59fc10e86ff1f408e4136eba0a25a5ec"
name = "github.com/nsf/termbox-go"
digest = "1:a25c9a6b41e100f4ce164db80260f2b687095ba9d8b46a1d6072d3686cc020db"
name = "github.com/mgutz/str"
packages = ["."]
pruneopts = "NUT"
revision = "5c94acc5e6eb520f1bcd183974e01171cc4c23b3"
revision = "968bf66e3da857419e4f6e71b2d5c9ae95682dc4"
version = "v1.2.0"
[[projects]]
branch = "master"
digest = "1:a4df73029d2c42fabcb6b41e327d2f87e685284ec03edf76921c267d9cfc9c23"
name = "github.com/mitchellh/go-homedir"
packages = ["."]
pruneopts = "NUT"
revision = "58046073cbffe2f25d425fe1331102f55cf719de"
[[projects]]
branch = "master"
digest = "1:18b773b92ac82a451c1276bd2776c1e55ce057ee202691ab33c8d6690efcc048"
name = "github.com/mitchellh/go-testing-interface"
packages = ["."]
pruneopts = "NUT"
revision = "a61a99592b77c9ba629d254a693acffaeb4b7e28"
[[projects]]
branch = "master"
digest = "1:5fe20cfe4ef484c237cec9f947b2a6fa90bad4b8610fd014f0e4211e13d82d5d"
name = "github.com/mitchellh/mapstructure"
packages = ["."]
pruneopts = "NUT"
revision = "f15292f7a699fcc1a38a80977f80a046874ba8ac"
[[projects]]
digest = "1:2c34c77bf3ec848da26e48af58fc511ed52750961fa848399d122882b8890928"
name = "github.com/nicksnyder/go-i18n"
packages = [
"v2/i18n",
"v2/internal",
"v2/internal/plural",
]
pruneopts = "NUT"
revision = "a16b91a3ba80db3a2301c70d1d302d42251c9079"
version = "v2.0.0-beta.5"
[[projects]]
digest = "1:cf254277d898b713195cc6b4a3fac8bf738b9f1121625df27843b52b267eec6c"
name = "github.com/pelletier/go-buffruneio"
packages = ["."]
pruneopts = "NUT"
revision = "c37440a7cf42ac63b919c752ca73a85067e05992"
version = "v0.2.0"
[[projects]]
digest = "1:51ea800cff51752ff68e12e04106f5887b4daec6f9356721238c28019f0b42db"
name = "github.com/pelletier/go-toml"
packages = ["."]
pruneopts = "NUT"
revision = "c01d1270ff3e442a8a57cddc1c92dc1138598194"
version = "v1.2.0"
[[projects]]
digest = "1:5cf3f025cbee5951a4ee961de067c8a89fc95a5adabead774f82822efabab121"
name = "github.com/pkg/errors"
packages = ["."]
pruneopts = "NUT"
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0"
[[projects]]
digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe"
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
pruneopts = "NUT"
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
digest = "1:d917313f309bda80d27274d53985bc65651f81a5b66b820749ac7f8ef061fd04"
name = "github.com/sergi/go-diff"
packages = ["diffmatchpatch"]
pruneopts = "NUT"
revision = "1744e2970ca51c86172c8190fadad617561ed6e7"
version = "v1.0.0"
[[projects]]
digest = "1:41618aee8828e62dfe62d44f579c06892d0e98907d1c6d5bcd83bfe8536ec5a3"
name = "github.com/shibukawa/configdir"
packages = ["."]
pruneopts = "NUT"
revision = "e180dbdc8da04c4fa04272e875ce64949f38bd3e"
[[projects]]
digest = "1:b2339e83ce9b5c4f79405f949429a7f68a9a904fed903c672aac1e7ceb7f5f02"
name = "github.com/sirupsen/logrus"
packages = ["."]
pruneopts = "NUT"
revision = "3e01752db0189b9157070a0e1668a620f9a85da2"
version = "v1.0.6"
[[projects]]
digest = "1:330e9062b308ac597e28485699c02223bd052437a6eed32a173c9227dcb9d95a"
name = "github.com/spf13/afero"
packages = [
".",
"mem",
]
pruneopts = "NUT"
revision = "787d034dfe70e44075ccc060d346146ef53270ad"
version = "v1.1.1"
[[projects]]
digest = "1:3fa7947ca83b98ae553590d993886e845a4bff19b7b007e869c6e0dd3b9da9cd"
name = "github.com/spf13/cast"
packages = ["."]
pruneopts = "NUT"
revision = "8965335b8c7107321228e3e3702cab9832751bac"
version = "v1.2.0"
[[projects]]
branch = "master"
digest = "1:f29f83301ed096daed24a90f4af591b7560cb14b9cc3e1827abbf04db7269ab5"
name = "github.com/spf13/jwalterweatherman"
packages = ["."]
pruneopts = "NUT"
revision = "14d3d4c518341bea657dd8a226f5121c0ff8c9f2"
[[projects]]
digest = "1:e3707aeaccd2adc89eba6c062fec72116fe1fc1ba71097da85b4d8ae1668a675"
name = "github.com/spf13/pflag"
packages = ["."]
pruneopts = "NUT"
revision = "9a97c102cda95a86cec2345a6f09f55a939babf5"
version = "v1.0.2"
[[projects]]
digest = "1:454979540e2a1582f375a17c106cf4e11e3bcac4baffb4af23e515c87f87de13"
name = "github.com/spf13/viper"
packages = ["."]
pruneopts = "NUT"
revision = "907c19d40d9a6c9bb55f040ff4ae45271a4754b9"
version = "v1.1.0"
[[projects]]
branch = "master"
digest = "1:0e9a5ac14bcc11f205031a671b28c7e05cb88b2ebbe06f383c1ab0b2c12c7cb5"
name = "github.com/spkg/bom"
packages = ["."]
pruneopts = "NUT"
revision = "59b7046e48ad6bac800c5e1dd5142282cbfcf154"
[[projects]]
digest = "1:ccca1dcd18bc54e23b517a3c5babeff2e3924a7d8fc1932162225876cfe4bfb0"
name = "github.com/src-d/gcfg"
packages = [
".",
"scanner",
"token",
"types",
]
pruneopts = "NUT"
revision = "f187355171c936ac84a82793659ebb4936bc1c23"
version = "v1.3.0"
[[projects]]
digest = "1:bacb8b590716ab7c33f2277240972c9582d389593ee8d66fc10074e0508b8126"
name = "github.com/stretchr/testify"
packages = ["assert"]
pruneopts = "NUT"
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
version = "v1.2.2"
[[projects]]
branch = "master"
digest = "1:e42372d3f4921ec35df07f9b23239631e9d28580f7c1edcca212bc6daddc68fe"
name = "github.com/stvp/roll"
packages = ["."]
pruneopts = "NUT"
revision = "3627a5cbeaeaa68023abd02bb8687925265f2f63"
[[projects]]
digest = "1:cd5ffc5bda4e0296ab3e4de90dbb415259c78e45e7fab13694b14cde8ab74541"
@@ -66,21 +462,191 @@
version = "v0.1.2"
[[projects]]
branch = "master"
digest = "1:4d8a79fbc6fa348fc94afa4235947c5196b7900ed71b94aa5fcbc7e273d150e1"
name = "golang.org/x/sys"
packages = ["unix"]
digest = "1:07e8742c479bab0066149ad02a710024154e76874fd0a2dba002d87702725825"
name = "github.com/ulikunitz/xz"
packages = [
".",
"internal/hash",
"internal/xlog",
"lzma",
]
pruneopts = "NUT"
revision = "0ffbfd41fbef8ffcf9b62b0b0aa3a5873ed7a4fe"
revision = "0c6b41e72360850ca4f98dc341fd999726ea007f"
version = "v0.5.4"
[[projects]]
digest = "1:3148cb3478c26a92b4c1a18abb9428234b281e278af6267840721a24b6cbc6a3"
name = "github.com/xanzy/ssh-agent"
packages = ["."]
pruneopts = "NUT"
revision = "640f0ab560aeb89d523bb6ac322b1244d5c3796c"
version = "v0.2.0"
[[projects]]
branch = "master"
digest = "1:dfcb1b2db354cafa48fc3cdafe4905a08bec4a9757919ab07155db0ca23855b4"
name = "golang.org/x/crypto"
packages = [
"cast5",
"curve25519",
"ed25519",
"ed25519/internal/edwards25519",
"internal/chacha20",
"internal/subtle",
"openpgp",
"openpgp/armor",
"openpgp/elgamal",
"openpgp/errors",
"openpgp/packet",
"openpgp/s2k",
"poly1305",
"ssh",
"ssh/agent",
"ssh/knownhosts",
"ssh/terminal",
]
pruneopts = "NUT"
revision = "de0752318171da717af4ce24d0a2e8626afaeb11"
[[projects]]
branch = "master"
digest = "1:76ee51c3f468493aff39dbacc401e8831fbb765104cbf613b89bef01cf4bad70"
name = "golang.org/x/net"
packages = ["context"]
pruneopts = "NUT"
revision = "c39426892332e1bb5ec0a434a079bf82f5d30c54"
[[projects]]
branch = "master"
digest = "1:ec76a40fbfda0c329ee58f4e3b14b4279a939efce89eca020e934e2e5234eddd"
name = "golang.org/x/sys"
packages = [
"unix",
"windows",
]
pruneopts = "NUT"
revision = "98c5dad5d1a0e8a73845ecc8897d0bd56586511d"
[[projects]]
digest = "1:a95288ef1ef4dfad6cba7fe30843e1683f71bc28c912ca1ba3f6a539d44db739"
name = "golang.org/x/text"
packages = [
"internal/gen",
"internal/tag",
"internal/triegen",
"internal/ucd",
"language",
"transform",
"unicode/cldr",
"unicode/norm",
]
pruneopts = "NUT"
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
version = "v0.3.0"
[[projects]]
digest = "1:47a697b155f5214ff14e68e39ce9c2e8d93e1fb035ae5ba7e247d044e0ce64e3"
name = "gopkg.in/src-d/go-billy.v4"
packages = [
".",
"helper/chroot",
"helper/polyfill",
"osfs",
"util",
]
pruneopts = "NUT"
revision = "83cf655d40b15b427014d7875d10850f96edba14"
version = "v4.2.0"
[[projects]]
digest = "1:e66078da2bd6e53c72518d7f6ae0c3c8c7f34c0df12c39435ce34a6bce165525"
name = "gopkg.in/src-d/go-git.v4"
packages = [
".",
"config",
"internal/revision",
"plumbing",
"plumbing/cache",
"plumbing/filemode",
"plumbing/format/config",
"plumbing/format/diff",
"plumbing/format/gitignore",
"plumbing/format/idxfile",
"plumbing/format/index",
"plumbing/format/objfile",
"plumbing/format/packfile",
"plumbing/format/pktline",
"plumbing/object",
"plumbing/protocol/packp",
"plumbing/protocol/packp/capability",
"plumbing/protocol/packp/sideband",
"plumbing/revlist",
"plumbing/storer",
"plumbing/transport",
"plumbing/transport/client",
"plumbing/transport/file",
"plumbing/transport/git",
"plumbing/transport/http",
"plumbing/transport/internal/common",
"plumbing/transport/server",
"plumbing/transport/ssh",
"storage",
"storage/filesystem",
"storage/filesystem/dotgit",
"storage/memory",
"utils/binary",
"utils/diff",
"utils/ioutil",
"utils/merkletrie",
"utils/merkletrie/filesystem",
"utils/merkletrie/index",
"utils/merkletrie/internal/frame",
"utils/merkletrie/noder",
]
pruneopts = "NUT"
revision = "43d17e14b714665ab5bc2ecc220b6740779d733f"
[[projects]]
digest = "1:b233ad4ec87ac916e7bf5e678e98a2cb9e8b52f6de6ad3e11834fc7a71b8e3bf"
name = "gopkg.in/warnings.v0"
packages = ["."]
pruneopts = "NUT"
revision = "ec4a0fea49c7b46c2aeb0b51aac55779c607e52b"
version = "v0.1.2"
[[projects]]
digest = "1:7c95b35057a0ff2e19f707173cc1a947fa43a6eb5c4d300d196ece0334046082"
name = "gopkg.in/yaml.v2"
packages = ["."]
pruneopts = "NUT"
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
version = "v2.2.1"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = [
"github.com/cloudfoundry/jibber_jabber",
"github.com/fatih/color",
"github.com/go-errors/errors",
"github.com/golang-collections/collections/stack",
"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",
"github.com/shibukawa/configdir",
"github.com/sirupsen/logrus",
"github.com/spf13/viper",
"github.com/spkg/bom",
"github.com/stretchr/testify/assert",
"github.com/tcnksm/go-gitconfig",
"golang.org/x/text/language",
"gopkg.in/src-d/go-git.v4",
"gopkg.in/src-d/go-git.v4/plumbing",
"gopkg.in/yaml.v2",
]
solver-name = "gps-cdcl"
solver-version = 1

View File

@@ -36,3 +36,15 @@
[[constraint]]
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"
[[constraint]]
branch = "master"
name = "github.com/spkg/bom"

114
Keybindings_en.md Normal file
View File

@@ -0,0 +1,114 @@
# Lazygit menu
## Global
<pre>
<kbd>m</kbd>: view merge/rebase options
<kbd>P</kbd>: push
<kbd>p</kbd>: pull
<kbd>R</kbd>: refresh
</pre>
## Status
<pre>
<kbd>e</kbd>: edit config file
<kbd>o</kbd>: open config file
<kbd>u</kbd>: check for update
<kbd>s</kbd>: switch to a recent repo
</pre>
## Files
<pre>
<kbd>c</kbd>: commit changes
<kbd>A</kbd>: amend last commit
<kbd>C</kbd>: commit changes using git editor
<kbd>space</kbd>: toggle staged
<kbd>d</kbd>: delete if untracked / checkout if tracked
<kbd>e</kbd>: edit file
<kbd>o</kbd>: open file
<kbd>i</kbd>: add to .gitignore
<kbd>r</kbd>: refresh files
<kbd>S</kbd>: stash files
<kbd>s</kbd>: soft reset to last commit
<kbd>a</kbd>: stage/unstage all
<kbd>t</kbd>: add patch
<kbd>D</kbd>: reset hard and remove untracked files
<kbd>enter</kbd>: stage individual hunks/lines
<kbd>f</kbd>: fetch
</pre>
## Branches
<pre>
<kbd>space</kbd>: checkout
<kbd>o</kbd>: create pull request
<kbd>c</kbd>: checkout by name
<kbd>F</kbd>: force checkout
<kbd>n</kbd>: new branch
<kbd>d</kbd>: delete branch
<kbd>r</kbd>: rebase branch
<kbd>M</kbd>: merge into currently checked out branch
<kbd>f</kbd>: fast-forward this branch from its upstream
</pre>
## Commits
<pre>
<kbd>s</kbd>: squash down
<kbd>r</kbd>: reword commit
<kbd>R</kbd>: rename commit with editor
<kbd>g</kbd>: reset to this commit
<kbd>f</kbd>: fixup commit
<kbd>d</kbd>: delete commit
<kbd>J</kbd>: move commit down one
<kbd>K</kbd>: move commit up one
<kbd>e</kbd>: edit commit
<kbd>A</kbd>: amend commit with staged changes
<kbd>p</kbd>: pick commit (when mid-rebase)
<kbd>t</kbd>: revert commit
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>v</kbd>: paste commits (cherry-pick)
</pre>
## Stash
<pre>
<kbd>space</kbd>: apply
<kbd>g</kbd>: pop
<kbd>d</kbd>: drop
</pre>
## Main (Normal)
<pre>
<kbd>PgDn</kbd>: scroll down
<kbd>PgUp</kbd>: scroll up
</pre>
## Main (Staging)
<pre>
<kbd>esc</kbd>: return to files panel
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>space</kbd>: stage line
<kbd>a</kbd>: stage hunk
</pre>
## Main (Merging)
<pre>
<kbd>esc</kbd>: return to files panel
<kbd>space</kbd>: pick hunk
<kbd>b</kbd>: pick both hunks
<kbd>◄</kbd>: select previous conflict
<kbd>►</kbd>: select next conflict
<kbd>▲</kbd>: select top hunk
<kbd>▼</kbd>: select bottom hunk
<kbd>z</kbd>: undo
</pre>

138
README.md
View File

@@ -1,30 +1,46 @@
# lazygit [![Go Report Card](https://goreportcard.com/badge/github.com/jesseduffield/lazygit)](https://goreportcard.com/report/github.com/jesseduffield/lazygit)
# lazygit [![CircleCI](https://circleci.com/gh/jesseduffield/lazygit.svg?style=svg)](https://circleci.com/gh/jesseduffield/lazygit) [![codecov](https://codecov.io/gh/jesseduffield/lazygit/branch/master/graph/badge.svg)](https://codecov.io/gh/jesseduffield/lazygit) [![Go Report Card](https://goreportcard.com/badge/github.com/jesseduffield/lazygit)](https://goreportcard.com/report/github.com/jesseduffield/lazygit) [![GolangCI](https://golangci.com/badges/github.com/jesseduffield/lazygit.svg)](https://golangci.com) [![GoDoc](https://godoc.org/github.com/jesseduffield/lazygit?status.svg)](http://godoc.org/github.com/jesseduffield/lazygit) [![GitHub tag](https://img.shields.io/github/tag/jesseduffield/lazygit.svg)]()
A simple terminal UI for git commands, written in Go with the [gocui](https://github.com/jroimartin/gocui "gocui") library.
Are YOU tired of typing every git command directly into the terminal, but you're too stubborn to use Sourcetree because you'll never forgive Atlassian for making Jira? This is the app for you!
Are YOU tired of typing every git command directly into the terminal, but you're
too stubborn to use Sourcetree because you'll never forgive Atlassian for making
Jira? This is the app for you!
[Tutorial](https://www.youtube.com/watch?v=VDXvbHZYeKY)
![Gif](https://image.ibb.co/mmeXho/optimisedgif.gif)
![Gif](/docs/resources/lazygit-example.gif)
* [Installation](https://github.com/jesseduffield/lazygit#installation)
* [Usage](https://github.com/jesseduffield/lazygit#usage),
[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://youtu.be/VDXvbHZYeKY)
* [Twitch Stream](https://www.twitch.tv/jesseduffield)
## Installation
### Via binary release
You can download a binary release [here](https://github.com/jesseduffield/lazygit/releases)
### Via Go
In a terminal call this command:
`go get github.com/jesseduffield/lazygit`
(if you don't have Go installed, you can follow the installation guide [here](https://golang.org/doc/install).
Please note:
If you get an error claiming that lazygit cannot be found or is not defined, you may need to add `~/go/bin` to your $PATH (MacOS/Linux), or `%HOME%\go\bin` (Windows). Not to be mistaked for `C:\Go\bin` (which is for Go's own binaries, not apps like Lazygit)
### Homebrew
```sh
brew tap jesseduffield/lazygit
brew install lazygit
```
### Ubuntu
Packages for Ubuntu 16.04, 18.04 and 18.10 are available via Launchpad PPA.
Packages for Ubuntu 16.04, 18.04 and 18.10 are available via [Launchpad PPA](https://launchpad.net/~lazygit-team).
They are built daily, straight from master branch.
**Release builds**
Built from git tags. Supposed to be more stable.
```sh
sudo add-apt-repository ppa:lazygit-team/release
sudo apt-get update
sudo apt-get install lazygit
```
**Daily builds**
Built from master branch once in 24 hours (or more sometimes).
```sh
sudo add-apt-repository ppa:lazygit-team/daily
@@ -32,30 +48,72 @@ sudo apt-get update
sudo apt-get install lazygit
```
### Void Linux
Packages for Void Linux are available in the distro repo
They follow upstream latest releases
```sh
sudo xbps-install -S lazygit
```
### Arch Linux
Packages for Arch Linux are available via AUR (Arch User Repository).
There are two packages. The stable one which is built with the latest release
and the git version which builds from the most recent commit.
* Stable: https://aur.archlinux.org/packages/lazygit/
* Development: https://aur.archlinux.org/packages/lazygit-git/
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).
### Go
```sh
go get github.com/jesseduffield/lazygit
```
Please note:
If you get an error claiming that lazygit cannot be found or is not defined, you
may need to add `~/go/bin` to your $PATH (MacOS/Linux), or `%HOME%\go\bin`
(Windows). Not to be mistaked for `C:\Go\bin` (which is for Go's own binaries,
not apps like Lazygit).
## Usage
Call `lazygit` in your terminal inside a git repository.
If you want, you can also add an alias for this with `echo "alias lg='lazygit'" >> ~/.zshrc` (or whichever rc file you're using).
Basic tutorial [Here](https://www.youtube.com/watch?v=VDXvbHZYeKY)
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).
[Keybindings](https://github.com/jesseduffield/lazygit/blob/master/docs/Keybindings.md)
* Basic video tutorial [here](https://youtu.be/VDXvbHZYeKY).
* List of keybindings
[here](/docs/Keybindings.md).
## Cool features
- Adding files easily
- Resolving merge conflicts
- Easily check out recent branches
- Scroll through logs/diffs of branches/commits/stash
- Quick pushing/pulling
- Squash down and rename commits
* Adding files easily
* Resolving merge conflicts
* Easily check out recent branches
* Scroll through logs/diffs of branches/commits/stash
* Quick pushing/pulling
* Squash down and rename commits
### Resolving merge conflicts
![Gif](https://image.ibb.co/iyxUTT/shortermerging.gif)
![Gif](/docs/resources/resolving-merge-conflicts.gif)
### Viewing commit diffs
![Viewing Commit Diffs](https://image.ibb.co/gPD02o/capture.png)
![Viewing Commit Diffs](/docs/resources/viewing-commit-diffs.png)
## Milestones
- [ ] Easy Installation (homebrew, release binaries)
- [x] Easy Installation (homebrew, release binaries)
- [ ] Configurable Keybindings
- [ ] Configurable Color Themes
- [ ] Spawning Subprocesses (help needed - have a look at https://github.com/jesseduffield/lazygit/pull/18)
@@ -65,6 +123,26 @@ Basic tutorial [Here](https://www.youtube.com/watch?v=VDXvbHZYeKY)
## Contributing
We love your input! Please check out the [contributing guide](CONTRIBUTING.md).
For contributor discussion about things not better discussed here in the repo, join the slack channel
[![Slack](/docs/resources/slack_rgb.png)](https://join.slack.com/t/lazygit/shared_invite/enQtNDE3MjIwNTYyMDA0LTM3Yjk3NzdiYzhhNTA1YjM4Y2M4MWNmNDBkOTI0YTE4YjQ1ZmI2YWRhZTgwNjg2YzhhYjg3NDBlMmQyMTI5N2M)
## 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, but I'll be improving on it whenever I find the time. If you have any feedback feel free to [raise an issue](https://github.com/jesseduffield/lazygit/issues)/[submit a PR](https://github.com/jesseduffield/lazygit/pulls).
This is still a work in progress so there's still bugs to iron out and as this
is my first project in Go the code could no doubt use an increase in quality,
but I'll be improving on it whenever I find the time. If you have any feedback
feel free to [raise an issue](https://github.com/jesseduffield/lazygit/issues)/[submit a PR](https://github.com/jesseduffield/lazygit/pulls).
## Social
If you want to see what I (Jesse) am up to in terms of development, follow me on
[twitter](https://twitter.com/DuffieldJesse) or watch me program on
[twitch](https://www.twitch.tv/jesseduffield).
## Alternatives
If you find that lazygit doesn't quite satisfy your requirements, these may be a better fit:
- [tig](https://github.com/jonas/tig)

View File

@@ -1,120 +0,0 @@
package main
import (
"fmt"
"strings"
"github.com/jesseduffield/gocui"
)
func handleBranchPress(g *gocui.Gui, v *gocui.View) error {
index := getItemPosition(v)
if index == 0 {
return createErrorPanel(g, "You have already checked out this branch")
}
branch := getSelectedBranch(v)
if output, err := gitCheckout(branch.Name, false); err != nil {
createErrorPanel(g, output)
}
return refreshSidePanels(g)
}
func handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
branch := getSelectedBranch(v)
return createConfirmationPanel(g, v, "Force Checkout Branch", "Are you sure you want force checkout? You will lose all local changes", func(g *gocui.Gui, v *gocui.View) error {
if output, err := gitCheckout(branch.Name, true); err != nil {
createErrorPanel(g, output)
}
return refreshSidePanels(g)
}, nil)
}
func handleCheckoutByName(g *gocui.Gui, v *gocui.View) error {
createPromptPanel(g, v, "Branch Name:", func(g *gocui.Gui, v *gocui.View) error {
if output, err := gitCheckout(trimmedContent(v), false); err != nil {
return createErrorPanel(g, output)
}
return refreshSidePanels(g)
})
return nil
}
func handleNewBranch(g *gocui.Gui, v *gocui.View) error {
branch := state.Branches[0]
createPromptPanel(g, v, "New Branch Name (Branch is off of "+branch.Name+")", func(g *gocui.Gui, v *gocui.View) error {
if output, err := gitNewBranch(trimmedContent(v)); err != nil {
return createErrorPanel(g, output)
}
refreshSidePanels(g)
return handleBranchSelect(g, v)
})
return nil
}
func handleMerge(g *gocui.Gui, v *gocui.View) error {
checkedOutBranch := state.Branches[0]
selectedBranch := getSelectedBranch(v)
defer refreshSidePanels(g)
if checkedOutBranch.Name == selectedBranch.Name {
return createErrorPanel(g, "You cannot merge a branch into itself")
}
if output, err := gitMerge(selectedBranch.Name); err != nil {
return createErrorPanel(g, output)
}
return nil
}
func getSelectedBranch(v *gocui.View) Branch {
lineNumber := getItemPosition(v)
return state.Branches[lineNumber]
}
func renderBranchesOptions(g *gocui.Gui) error {
return renderOptionsMap(g, map[string]string{
"space": "checkout",
"f": "force checkout",
"m": "merge",
"c": "checkout by name",
"n": "new branch",
"← → ↑ ↓": "navigate",
})
}
// may want to standardise how these select methods work
func handleBranchSelect(g *gocui.Gui, v *gocui.View) error {
if err := renderBranchesOptions(g); err != nil {
return err
}
// This really shouldn't happen: there should always be a master branch
if len(state.Branches) == 0 {
return renderString(g, "main", "No branches for this repo")
}
go func() {
branch := getSelectedBranch(v)
diff, err := getBranchGraph(branch.Name, branch.BaseBranch)
if err != nil && strings.HasPrefix(diff, "fatal: ambiguous argument") {
diff = "There is no tracking for this branch"
}
renderString(g, "main", diff)
}()
return nil
}
// 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 refreshBranches(g *gocui.Gui) error {
g.Update(func(g *gocui.Gui) error {
v, err := g.View("branches")
if err != nil {
panic(err)
}
state.Branches = getGitBranches()
v.Clear()
for _, branch := range state.Branches {
fmt.Fprintln(v, branch.DisplayString)
}
resetOrigin(v)
return refreshStatus(g)
})
return nil
}

View File

@@ -1,138 +0,0 @@
package main
import (
"errors"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
)
var (
// ErrNoCommits : When no commits are found for the branch
ErrNoCommits = errors.New("No commits for this branch")
)
func refreshCommits(g *gocui.Gui) error {
g.Update(func(*gocui.Gui) error {
state.Commits = getCommits()
v, err := g.View("commits")
if err != nil {
panic(err)
}
v.Clear()
red := color.New(color.FgRed)
yellow := color.New(color.FgYellow)
white := color.New(color.FgWhite)
shaColor := white
for _, commit := range state.Commits {
if commit.Pushed {
shaColor = red
} else {
shaColor = yellow
}
shaColor.Fprint(v, commit.Sha+" ")
white.Fprintln(v, commit.Name)
}
refreshStatus(g)
return nil
})
return nil
}
func handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error {
return createConfirmationPanel(g, commitView, "Reset To Commit", "Are you sure you want to reset to this commit?", func(g *gocui.Gui, v *gocui.View) error {
commit, err := getSelectedCommit(g)
devLog(commit)
if err != nil {
panic(err)
}
if output, err := gitResetToCommit(commit.Sha); err != nil {
return createErrorPanel(g, output)
}
if err := refreshCommits(g); err != nil {
panic(err)
}
if err := refreshFiles(g); err != nil {
panic(err)
}
resetOrigin(commitView)
return handleCommitSelect(g, nil)
}, nil)
}
func renderCommitsOptions(g *gocui.Gui) error {
return renderOptionsMap(g, map[string]string{
"s": "squash down",
"r": "rename",
"g": "reset to this commit",
"← → ↑ ↓": "navigate",
})
}
func handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
if err := renderCommitsOptions(g); err != nil {
return err
}
commit, err := getSelectedCommit(g)
if err != nil {
if err != ErrNoCommits {
return err
}
return renderString(g, "main", "No commits for this branch")
}
commitText := gitShow(commit.Sha)
return renderString(g, "main", commitText)
}
func handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
if getItemPosition(v) != 0 {
return createErrorPanel(g, "Can only squash topmost commit")
}
if len(state.Commits) == 1 {
return createErrorPanel(g, "You have no commits to squash with")
}
commit, err := getSelectedCommit(g)
if err != nil {
return err
}
if output, err := gitSquashPreviousTwoCommits(commit.Name); err != nil {
return createErrorPanel(g, output)
}
if err := refreshCommits(g); err != nil {
panic(err)
}
refreshStatus(g)
return handleCommitSelect(g, v)
}
func handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
if getItemPosition(v) != 0 {
return createErrorPanel(g, "Can only rename topmost commit")
}
createPromptPanel(g, v, "Rename Commit", func(g *gocui.Gui, v *gocui.View) error {
if output, err := gitRenameCommit(v.Buffer()); err != nil {
return createErrorPanel(g, output)
}
if err := refreshCommits(g); err != nil {
panic(err)
}
return handleCommitSelect(g, v)
})
return nil
}
func getSelectedCommit(g *gocui.Gui) (Commit, error) {
v, err := g.View("commits")
if err != nil {
panic(err)
}
if len(state.Commits) == 0 {
return Commit{}, ErrNoCommits
}
lineNumber := getItemPosition(v)
if lineNumber > len(state.Commits)-1 {
colorLog(color.FgRed, "potential error in getSelected Commit (mismatched ui and state)", state.Commits, lineNumber)
return state.Commits[len(state.Commits)-1], nil
}
return state.Commits[lineNumber], nil
}

View File

@@ -1,119 +0,0 @@
// lots of this has been directly ported from one of the example files, will brush up later
// Copyright 2014 The gocui Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
)
func wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.View) error) func(*gocui.Gui, *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
if function != nil {
if err := function(g, v); err != nil {
panic(err)
}
}
return closeConfirmationPrompt(g)
}
}
func closeConfirmationPrompt(g *gocui.Gui) error {
view, err := g.View("confirmation")
if err != nil {
panic(err)
}
if err := returnFocus(g, view); err != nil {
panic(err)
}
g.DeleteKeybindings("confirmation")
return g.DeleteView("confirmation")
}
func getMessageHeight(message string, width int) int {
lines := strings.Split(message, "\n")
lineCount := 0
for _, line := range lines {
lineCount += len(line)/width + 1
}
return lineCount
}
func getConfirmationPanelDimensions(g *gocui.Gui, prompt string) (int, int, int, int) {
width, height := g.Size()
panelWidth := 60
panelHeight := getMessageHeight(prompt, panelWidth)
return width/2 - panelWidth/2,
height/2 - panelHeight/2 - panelHeight%2 - 1,
width/2 + panelWidth/2,
height/2 + panelHeight/2
}
func createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, handleYes func(*gocui.Gui, *gocui.View) error) error {
// only need to fit one line
x0, y0, x1, y1 := getConfirmationPanelDimensions(g, "")
if confirmationView, err := g.SetView("confirmation", x0, y0, x1, y1, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
g.Cursor = true
confirmationView.Editable = true
confirmationView.Title = title
confirmationView.FgColor = gocui.ColorWhite
switchFocus(g, currentView, confirmationView)
return setKeyBindings(g, handleYes, nil)
}
return nil
}
func createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, handleYes, handleNo func(*gocui.Gui, *gocui.View) error) error {
g.Update(func(g *gocui.Gui) error {
// delete the existing confirmation panel if it exists
if view, _ := g.View("confirmation"); view != nil {
if err := closeConfirmationPrompt(g); err != nil {
panic(err)
}
}
x0, y0, x1, y1 := getConfirmationPanelDimensions(g, prompt)
if confirmationView, err := g.SetView("confirmation", x0, y0, x1, y1, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
confirmationView.Title = title
confirmationView.FgColor = gocui.ColorWhite
renderString(g, "confirmation", prompt)
switchFocus(g, currentView, confirmationView)
return setKeyBindings(g, handleYes, handleNo)
}
return nil
})
return nil
}
func setKeyBindings(g *gocui.Gui, handleYes, handleNo func(*gocui.Gui, *gocui.View) error) error {
renderString(g, "options", "esc: close, enter: confirm")
if err := g.SetKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone, wrappedConfirmationFunction(handleYes)); err != nil {
return err
}
return g.SetKeybinding("confirmation", gocui.KeyEsc, gocui.ModNone, wrappedConfirmationFunction(handleNo))
}
func createMessagePanel(g *gocui.Gui, currentView *gocui.View, title, prompt string) error {
return createConfirmationPanel(g, currentView, title, prompt, nil, nil)
}
func createErrorPanel(g *gocui.Gui, message string) error {
currentView := g.CurrentView()
colorFunction := color.New(color.FgRed).SprintFunc()
coloredMessage := colorFunction(strings.TrimSpace(message))
return createConfirmationPanel(g, currentView, "Error", coloredMessage, nil, nil)
}

83
docs/Config.md Normal file
View File

@@ -0,0 +1,83 @@
# User Config:
## Default:
```
gui:
# stuff relating to the UI
scrollHeight: 2 # how many lines you scroll by
scrollPastBottom: true # enable scrolling past the bottom
theme:
activeBorderColor:
- white
- bold
inactiveBorderColor:
- white
optionsTextColor:
- blue
commitLength:
show: true
git:
merging:
# only applicable to unix users
manualCommit: false
update:
method: prompt # can be: prompt | background | never
days: 14 # how often an update is checked for
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
confirmOnQuit: false
```
## Platform Defaults:
### Windows:
```
os:
openCommand: 'cmd /c "start "" {{filename}}"'
```
### Linux:
```
os:
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'
```
### OSX:
```
os:
openCommand: 'open {{filename}}'
```
### Recommended Config Values:
for users of VSCode
```
os:
openCommand: 'code -r {{filename}}'
```
## Color Attributes:
For color attributes you can choose an array of attributes (with max one color attribute)
The available attributes are:
- default
- black
- red
- green
- yellow
- blue
- magenta
- cyan
- white
- bold
- reverse # useful for high-contrast
- underline
## Example Coloring:
![border example](/docs/resources/colored-border-example.png)

View File

@@ -2,55 +2,84 @@
## Global:
← → ↑ ↓: navigate
PgUp/PgDn: scroll diff panel (use fn+up/down on osx)
q: quit
p: pull
shift+P: push
<pre>
<kbd>←</kbd><kbd>→</kbd><kbd>↑</kbd><kbd>↓</kbd>/<kbd>h</kbd><kbd>j</kbd><kbd>k</kbd><kbd>l</kbd>: navigate
<kbd>PgUp</kbd>/<kbd>PgDn</kbd> or <kbd>ctrl</kbd>+<kbd>u</kbd>/<kbd>ctrl</kbd>+<kbd>d</kbd>: scroll diff panel
(for <kbd>PgUp</kbd> and <kbd>PgDn</kbd>, use <kbd>fn</kbd>+<kbd>up</kbd>/<kbd>fn</kbd>+<kbd>down</kbd> on osx)
<kbd>q</kbd>: quit
<kbd>p</kbd>: pull
<kbd>shift</kbd>+<kbd>P</kbd>: push
</pre>
## Status Panel:
<pre>
<kbd>e</kbd>: edit config file
<kbd>o</kbd>: open config file
</pre>
## Files Panel:
space: toggle staged
c: commit changes
shift+S: stash files
t: add patched (i.e. pick chunks of a file to add)
o: open
e: edit
s: open in sublime (requires 'subl' command)
v: open in vscode (requires 'code' command)
i: add to .gitignore
d: delete if untracked checkout if tracked (aka go away)
shift+R: refresh files
<pre>
<kbd>space</kbd>: toggle staged
<kbd>a</kbd>: stage/unstage all
<kbd>c</kbd>: commit changes
<kbd>shift</kbd>+<kbd>C</kbd>: commit using git editor
<kbd>shift</kbd>+<kbd>S</kbd>: stash files
<kbd>t</kbd>: add patched (i.e. pick chunks of a file to add)
<kbd>o</kbd>: open
<kbd>e</kbd>: edit
<kbd>s</kbd>: open in sublime (requires 'subl' command)
<kbd>v</kbd>: open in vscode (requires 'code' command)
<kbd>i</kbd>: add to .gitignore
<kbd>d</kbd>: delete if untracked checkout if tracked (aka go away)
<kbd>shift</kbd>+<kbd>R</kbd>: refresh files
<kbd>shift</kbd>+<kbd>A</kbd>: abort merge
</pre>
## Branches Panel:
space: checkout branch
f: force checkout branch
m: merge into currently checked out branch
c: checkout by name
n: new branch
<pre>
<kbd>space</kbd>: checkout branch
<kbd>f</kbd>: force checkout branch
<kbd>m</kbd>: merge into currently checked out branch
<kbd>c</kbd>: checkout by name
<kbd>n</kbd>: new branch
<kbd>d</kbd>: delete branch
<kbd>D</kbd>: force delete branch
</pre>
## Commits Panel:
s: squash down (only available for topmost commit)
r: rename commit
g: reset to this commit
<pre>
<kbd>s</kbd>: squash down (only available for topmost commit)
<kbd>r</kbd>: rename commit
<kbd>shift</kbd>+<kbd>R</kbd>: rename commit using git editor
<kbd>g</kbd>: reset to this commit
</pre>
## Stash Panel:
space: apply
k: pop
d: drop
<pre>
<kbd>space</kbd>: apply
<kbd>g</kbd>: pop
<kbd>d</kbd>: drop
</pre>
## Popup Panel:
esc: close/cancel
enter: confirm
<pre>
<kbd>esc</kbd>: close/cancel
<kbd>enter</kbd>: confirm
<kbd>tab</kbd>: enter newline (if editing)
</pre>
## Resolving Merge Conflicts (Diff Panel):
← →: navigate conflicts
↑ ↓: select hunk
space: pick hunk
b: pick both hunks
z: undo (only available while still inside diff panel)
<pre>
<kbd>←</kbd><kbd>→</kbd>/<kbd>h</kbd><kbd>l</kbd>: navigate conflicts
<kbd>↑</kbd><kbd>↓</kbd>/<kbd>k</kbd><kbd>j</kbd>: select hunk
<kbd>space</kbd>: pick hunk
<kbd>b</kbd>: pick both hunks
<kbd>z</kbd>: undo (only available while still inside diff panel)
</pre>

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View File

@@ -1,353 +0,0 @@
package main
import (
// "io"
// "io/ioutil"
// "strings"
"errors"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
)
var (
// ErrNoFiles : when there are no modified files in the repo
ErrNoFiles = errors.New("No changed files")
)
func stagedFiles(files []GitFile) []GitFile {
result := make([]GitFile, 0)
for _, file := range files {
if file.HasStagedChanges {
result = append(result, file)
}
}
return result
}
func stageSelectedFile(g *gocui.Gui) error {
file, err := getSelectedFile(g)
if err != nil {
return err
}
return stageFile(file.Name)
}
func handleFilePress(g *gocui.Gui, v *gocui.View) error {
file, err := getSelectedFile(g)
if err != nil {
if err == ErrNoFiles {
return nil
}
return err
}
if file.HasMergeConflicts {
return handleSwitchToMerge(g, v)
}
if file.HasUnstagedChanges {
stageFile(file.Name)
} else {
unStageFile(file.Name, file.Tracked)
}
if err := refreshFiles(g); err != nil {
return err
}
return handleFileSelect(g, v)
}
func handleAddPatch(g *gocui.Gui, v *gocui.View) error {
file, err := getSelectedFile(g)
if err != nil {
if err == ErrNoFiles {
return nil
}
return err
}
if !file.HasUnstagedChanges {
return createErrorPanel(g, "File has no unstaged changes to add")
}
if !file.Tracked {
return createErrorPanel(g, "Cannot git add --patch untracked files")
}
gitAddPatch(g, file.Name)
return err
}
func getSelectedFile(g *gocui.Gui) (GitFile, error) {
if len(state.GitFiles) == 0 {
return GitFile{}, ErrNoFiles
}
filesView, err := g.View("files")
if err != nil {
panic(err)
}
lineNumber := getItemPosition(filesView)
return state.GitFiles[lineNumber], nil
}
func handleFileRemove(g *gocui.Gui, v *gocui.View) error {
file, err := getSelectedFile(g)
if err != nil {
if err == ErrNoFiles {
return nil
}
return err
}
var deleteVerb string
if file.Tracked {
deleteVerb = "checkout"
} else {
deleteVerb = "delete"
}
return createConfirmationPanel(g, v, strings.Title(deleteVerb)+" file", "Are you sure you want to "+deleteVerb+" "+file.Name+" (you will lose your changes)?", func(g *gocui.Gui, v *gocui.View) error {
if err := removeFile(file); err != nil {
panic(err)
}
return refreshFiles(g)
}, nil)
}
func handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
file, err := getSelectedFile(g)
if err != nil {
return createErrorPanel(g, err.Error())
}
if file.Tracked {
return createErrorPanel(g, "Cannot ignore tracked files")
}
gitIgnore(file.Name)
return refreshFiles(g)
}
func renderfilesOptions(g *gocui.Gui, gitFile *GitFile) error {
optionsMap := map[string]string{
"← → ↑ ↓": "navigate",
"S": "stash files",
"c": "commit changes",
"o": "open",
"i": "ignore",
"d": "delete",
"space": "toggle staged",
"R": "refresh",
"t": "add patch",
"e": "edit",
}
if state.HasMergeConflicts {
optionsMap["a"] = "abort merge"
optionsMap["m"] = "resolve merge conflicts"
}
if gitFile == nil {
return renderOptionsMap(g, optionsMap)
}
if gitFile.Tracked {
optionsMap["d"] = "checkout"
}
return renderOptionsMap(g, optionsMap)
}
func handleFileSelect(g *gocui.Gui, v *gocui.View) error {
gitFile, err := getSelectedFile(g)
if err != nil {
if err != ErrNoFiles {
return err
}
renderString(g, "main", "No changed files")
return renderfilesOptions(g, nil)
}
renderfilesOptions(g, &gitFile)
var content string
if gitFile.HasMergeConflicts {
return refreshMergePanel(g)
}
content = getDiff(gitFile)
return renderString(g, "main", content)
}
func handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
if len(stagedFiles(state.GitFiles)) == 0 && !state.HasMergeConflicts {
return createErrorPanel(g, "There are no staged files to commit")
}
createPromptPanel(g, filesView, "Commit message", func(g *gocui.Gui, v *gocui.View) error {
message := trimmedContent(v)
if message == "" {
return createErrorPanel(g, "You cannot commit without a commit message")
}
if output, err := gitCommit(g, message); err != nil {
return createErrorPanel(g, output)
}
refreshFiles(g)
return refreshCommits(g)
})
return nil
}
func genericFileOpen(g *gocui.Gui, v *gocui.View, open func(*gocui.Gui, string) (string, error)) error {
file, err := getSelectedFile(g)
if err != nil {
if err != ErrNoFiles {
return err
}
return nil
}
if _, err := open(g, file.Name); err != nil {
return createErrorPanel(g, err.Error())
}
return nil
}
func handleFileEdit(g *gocui.Gui, v *gocui.View) error {
return genericFileOpen(g, v, editFile)
}
func handleFileOpen(g *gocui.Gui, v *gocui.View) error {
return genericFileOpen(g, v, openFile)
}
func handleSublimeFileOpen(g *gocui.Gui, v *gocui.View) error {
return genericFileOpen(g, v, sublimeOpenFile)
}
func handleVsCodeFileOpen(g *gocui.Gui, v *gocui.View) error {
return genericFileOpen(g, v, vsCodeOpenFile)
}
func handleRefreshFiles(g *gocui.Gui, v *gocui.View) error {
return refreshFiles(g)
}
func refreshStateGitFiles() {
// get files to stage
gitFiles := getGitStatusFiles()
state.GitFiles = mergeGitStatusFiles(state.GitFiles, gitFiles)
updateHasMergeConflictStatus()
}
func updateHasMergeConflictStatus() error {
merging, err := isInMergeState()
if err != nil {
return err
}
state.HasMergeConflicts = merging
return nil
}
func renderGitFile(gitFile GitFile, filesView *gocui.View) {
// potentially inefficient to be instantiating these color
// objects with each render
red := color.New(color.FgRed)
green := color.New(color.FgGreen)
if !gitFile.Tracked && !gitFile.HasStagedChanges {
red.Fprintln(filesView, gitFile.DisplayString)
return
}
green.Fprint(filesView, gitFile.DisplayString[0:1])
red.Fprint(filesView, gitFile.DisplayString[1:3])
if gitFile.HasUnstagedChanges {
red.Fprintln(filesView, gitFile.Name)
} else {
green.Fprintln(filesView, gitFile.Name)
}
}
func catSelectedFile(g *gocui.Gui) (string, error) {
item, err := getSelectedFile(g)
if err != nil {
if err != ErrNoFiles {
return "", err
}
return "", renderString(g, "main", "No file to display")
}
cat, err := catFile(item.Name)
if err != nil {
panic(err)
}
return cat, nil
}
func refreshFiles(g *gocui.Gui) error {
filesView, err := g.View("files")
if err != nil {
return err
}
refreshStateGitFiles()
filesView.Clear()
for _, gitFile := range state.GitFiles {
renderGitFile(gitFile, filesView)
}
correctCursor(filesView)
if filesView == g.CurrentView() {
handleFileSelect(g, filesView)
}
return nil
}
func pullFiles(g *gocui.Gui, v *gocui.View) error {
devLog("pulling...")
createMessagePanel(g, v, "", "Pulling...")
go func() {
if output, err := gitPull(); err != nil {
createErrorPanel(g, output)
} else {
closeConfirmationPrompt(g)
refreshCommits(g)
refreshStatus(g)
devLog("pulled.")
}
refreshFiles(g)
}()
return nil
}
func pushFiles(g *gocui.Gui, v *gocui.View) error {
devLog("pushing...")
createMessagePanel(g, v, "", "Pushing...")
go func() {
if output, err := gitPush(); err != nil {
createErrorPanel(g, output)
} else {
closeConfirmationPrompt(g)
refreshCommits(g)
refreshStatus(g)
devLog("pushed.")
}
}()
return nil
}
func handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error {
mergeView, err := g.View("main")
if err != nil {
return err
}
file, err := getSelectedFile(g)
if err != nil {
if err != ErrNoFiles {
return err
}
return nil
}
if !file.HasMergeConflicts {
return createErrorPanel(g, "This file has no merge conflicts")
}
switchFocus(g, v, mergeView)
return refreshMergePanel(g)
}
func handleAbortMerge(g *gocui.Gui, v *gocui.View) error {
output, err := gitAbortMerge()
if err != nil {
return createErrorPanel(g, output)
}
createMessagePanel(g, v, "", "Merge aborted")
refreshStatus(g)
return refreshFiles(g)
}

View File

@@ -1,646 +0,0 @@
package main
import (
// "log"
"errors"
"fmt"
"os"
"os/exec"
"regexp"
"runtime"
"strings"
"time"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
gitconfig "github.com/tcnksm/go-gitconfig"
)
var (
// ErrNoCheckedOutBranch : When we have no checked out branch
ErrNoCheckedOutBranch = errors.New("No currently checked out branch")
// ErrNoOpenCommand : When we don't know which command to use to open a file
ErrNoOpenCommand = errors.New("Unsure what command to use to open this file")
)
// GitFile : A staged/unstaged file
// TODO: decide whether to give all of these the Git prefix
type GitFile struct {
Name string
HasStagedChanges bool
HasUnstagedChanges bool
Tracked bool
Deleted bool
HasMergeConflicts bool
DisplayString string
}
// Branch : A git branch
type Branch struct {
Name string
Type string
BaseBranch string
DisplayString string
}
// Commit : A git commit
type Commit struct {
Sha string
Name string
Pushed bool
DisplayString string
}
// StashEntry : A git stash entry
type StashEntry struct {
Index int
Name string
DisplayString string
}
// Map (from https://gobyexample.com/collection-functions)
func Map(vs []string, f func(string) string) []string {
vsm := make([]string, len(vs))
for i, v := range vs {
vsm[i] = f(v)
}
return vsm
}
func includesString(list []string, a string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
// not sure how to genericise this because []interface{} doesn't accept e.g.
// []int arguments
func includesInt(list []int, a int) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
func mergeGitStatusFiles(oldGitFiles, newGitFiles []GitFile) []GitFile {
if len(oldGitFiles) == 0 {
return newGitFiles
}
appendedIndexes := make([]int, 0)
// retain position of files we already could see
result := make([]GitFile, 0)
for _, oldGitFile := range oldGitFiles {
for newIndex, newGitFile := range newGitFiles {
if oldGitFile.Name == newGitFile.Name {
result = append(result, newGitFile)
appendedIndexes = append(appendedIndexes, newIndex)
break
}
}
}
// append any new files to the end
for index, newGitFile := range newGitFiles {
if !includesInt(appendedIndexes, index) {
result = append(result, newGitFile)
}
}
return result
}
func platformShell() (string, string) {
if runtime.GOOS == "windows" {
return "cmd", "/c"
}
return "bash", "-c"
}
func runDirectCommand(command string) (string, error) {
timeStart := time.Now()
commandLog(command)
shell, shellArg := platformShell()
cmdOut, err := exec.
Command(shell, shellArg, command).
CombinedOutput()
devLog("run direct command time for command: ", command, time.Now().Sub(timeStart))
return sanitisedCommandOutput(cmdOut, err)
}
func branchStringParts(branchString string) (string, string) {
// expect string to be something like '4w master`
splitBranchName := strings.Split(branchString, "\t")
// if we have no \t then we have no recency, so just output that as blank
if len(splitBranchName) == 1 {
return "", branchString
}
return splitBranchName[0], splitBranchName[1]
}
// branchPropertiesFromName : returns branch type, base, and color
func branchPropertiesFromName(name string) (string, string, color.Attribute) {
if strings.Contains(name, "feature/") {
return "feature", "develop", color.FgGreen
} else if strings.Contains(name, "bugfix/") {
return "bugfix", "develop", color.FgYellow
} else if strings.Contains(name, "hotfix/") {
return "hotfix", "master", color.FgRed
}
return "other", name, color.FgWhite
}
func coloredString(str string, colour *color.Color) string {
return colour.SprintFunc()(fmt.Sprint(str))
}
func withPadding(str string, padding int) string {
if padding-len(str) < 0 {
return str
}
return str + strings.Repeat(" ", padding-len(str))
}
// TODO: DRY up this function and getGitBranches
func getGitStashEntries() []StashEntry {
stashEntries := make([]StashEntry, 0)
rawString, _ := runDirectCommand("git stash list --pretty='%gs'")
for i, line := range splitLines(rawString) {
stashEntries = append(stashEntries, stashEntryFromLine(line, i))
}
return stashEntries
}
func stashEntryFromLine(line string, index int) StashEntry {
return StashEntry{
Name: line,
Index: index,
DisplayString: line,
}
}
func getStashEntryDiff(index int) (string, error) {
return runCommand("git stash show -p --color stash@{" + fmt.Sprint(index) + "}")
}
func includes(array []string, str string) bool {
for _, arrayStr := range array {
if arrayStr == str {
return true
}
}
return false
}
func getGitStatusFiles() []GitFile {
statusOutput, _ := getGitStatus()
statusStrings := splitLines(statusOutput)
gitFiles := make([]GitFile, 0)
for _, statusString := range statusStrings {
change := statusString[0:2]
stagedChange := change[0:1]
unstagedChange := statusString[1:2]
filename := statusString[3:]
tracked := !includes([]string{"??", "A "}, change)
gitFile := GitFile{
Name: filename,
DisplayString: statusString,
HasStagedChanges: !includes([]string{" ", "U", "?"}, stagedChange),
HasUnstagedChanges: unstagedChange != " ",
Tracked: tracked,
Deleted: unstagedChange == "D" || stagedChange == "D",
HasMergeConflicts: change == "UU",
}
devLog("tracked", gitFile.Tracked)
devLog("hasUnstagedChanges", gitFile.HasUnstagedChanges)
devLog("HasStagedChanges", gitFile.HasStagedChanges)
devLog("DisplayString", gitFile.DisplayString)
gitFiles = append(gitFiles, gitFile)
}
devLog(gitFiles)
return gitFiles
}
func gitStashDo(index int, method string) (string, error) {
return runCommand("git stash " + method + " stash@{" + fmt.Sprint(index) + "}")
}
func gitStashSave(message string) (string, error) {
output, err := runCommand("git stash save \"" + message + "\"")
if err != nil {
return output, err
}
// if there are no local changes to save, the exit code is 0, but we want
// to raise an error
if output == "No local changes to save\n" {
return output, errors.New(output)
}
return output, nil
}
func gitCheckout(branch string, force bool) (string, error) {
forceArg := ""
if force {
forceArg = "--force "
}
return runCommand("git checkout " + forceArg + branch)
}
func sanitisedCommandOutput(output []byte, err error) (string, error) {
outputString := string(output)
if outputString == "" && err != nil {
return err.Error(), err
}
return outputString, err
}
func runCommand(command string) (string, error) {
commandStartTime := time.Now()
commandLog(command)
splitCmd := strings.Split(command, " ")
devLog(splitCmd)
cmdOut, err := exec.Command(splitCmd[0], splitCmd[1:]...).CombinedOutput()
devLog("run command time: ", time.Now().Sub(commandStartTime))
return sanitisedCommandOutput(cmdOut, err)
}
func vsCodeOpenFile(g *gocui.Gui, filename string) (string, error) {
return runCommand("code -r " + filename)
}
func sublimeOpenFile(g *gocui.Gui, filename string) (string, error) {
return runCommand("subl " + filename)
}
func openFile(g *gocui.Gui, filename string) (string, error) {
cmdName, cmdTrail, err := getOpenCommand()
if err != nil {
return "", err
}
return runCommand(cmdName + " " + filename + cmdTrail)
}
func getOpenCommand() (string, string, error) {
//NextStep open equivalents: xdg-open (linux), cygstart (cygwin), open (OSX)
trailMap := map[string]string{
"xdg-open": " &>/dev/null &",
"cygstart": "",
"open": "",
}
for name, trail := range trailMap {
if out, _ := runCommand("which " + name); out != "exit status 1" {
return name, trail, nil
}
}
return "", "", ErrNoOpenCommand
}
func gitAddPatch(g *gocui.Gui, filename string) {
runSubProcess(g, "git", "add", "--patch", filename)
}
func editFile(g *gocui.Gui, filename string) (string, error) {
editor, _ := gitconfig.Global("core.editor")
if editor == "" {
editor = os.Getenv("VISUAL")
}
if editor == "" {
editor = os.Getenv("EDITOR")
}
if editor == "" {
return "", createErrorPanel(g, "No editor defined in $VISUAL, $EDITOR, or git config.")
}
runSubProcess(g, editor, filename)
return "", nil
}
func runSubProcess(g *gocui.Gui, cmdName string, commandArgs ...string) {
subprocess = exec.Command(cmdName, commandArgs...)
subprocess.Stdin = os.Stdin
subprocess.Stdout = os.Stdout
subprocess.Stderr = os.Stderr
g.Update(func(g *gocui.Gui) error {
return ErrSubprocess
})
}
func getBranchGraph(branch string, baseBranch string) (string, error) {
return runCommand("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium -100 " + branch)
// Leaving this guy commented out in case there's backlash from the design
// change and I want to make this configurable
// return runCommand("git log -p -30 --color --no-merges " + branch)
}
func verifyInGitRepo() {
if output, err := runCommand("git status"); err != nil {
fmt.Println(output)
os.Exit(1)
}
}
func getCommits() []Commit {
pushables := gitCommitsToPush()
log := getLog()
commits := make([]Commit, 0)
// now we can split it up and turn it into commits
lines := splitLines(log)
for _, line := range lines {
splitLine := strings.Split(line, " ")
sha := splitLine[0]
pushed := includesString(pushables, sha)
commits = append(commits, Commit{
Sha: sha,
Name: strings.Join(splitLine[1:], " "),
Pushed: pushed,
DisplayString: strings.Join(splitLine, " "),
})
}
return commits
}
func getLog() string {
// currently limiting to 30 for performance reasons
// TODO: add lazyloading when you scroll down
result, err := runDirectCommand("git log --oneline -30")
if err != nil {
// assume if there is an error there are no commits yet for this branch
return ""
}
return result
}
func gitIgnore(filename string) {
if _, err := runDirectCommand("echo '" + filename + "' >> .gitignore"); err != nil {
panic(err)
}
}
func gitShow(sha string) string {
result, err := runDirectCommand("git show --color " + sha)
if err != nil {
panic(err)
}
return result
}
func getDiff(file GitFile) string {
cachedArg := ""
if file.HasStagedChanges && !file.HasUnstagedChanges {
cachedArg = "--cached "
}
deletedArg := ""
if file.Deleted {
deletedArg = "-- "
}
trackedArg := ""
if !file.Tracked && !file.HasStagedChanges {
trackedArg = "--no-index /dev/null "
}
command := "git diff --color " + cachedArg + deletedArg + trackedArg + file.Name
// for now we assume an error means the file was deleted
s, _ := runCommand(command)
return s
}
func catFile(file string) (string, error) {
return runDirectCommand("cat " + file)
}
func stageFile(file string) error {
_, err := runCommand("git add " + file)
return err
}
func unStageFile(file string, tracked bool) error {
var command string
if tracked {
command = "git reset HEAD "
} else {
command = "git rm --cached "
}
devLog(command)
_, err := runCommand(command + file)
return err
}
func getGitStatus() (string, error) {
return runCommand("git status --untracked-files=all --short")
}
func isInMergeState() (bool, error) {
output, err := runCommand("git status --untracked-files=all")
if err != nil {
return false, err
}
return strings.Contains(output, "conclude merge") || strings.Contains(output, "unmerged paths"), nil
}
func removeFile(file GitFile) error {
// if the file isn't tracked, we assume you want to delete it
if !file.Tracked {
_, err := runCommand("rm -rf ./" + file.Name)
return err
}
// if the file is tracked, we assume you want to just check it out
_, err := runCommand("git checkout " + file.Name)
return err
}
func gitCommit(g *gocui.Gui, message string) (string, error) {
gpgsign, _ := gitconfig.Global("commit.gpgsign")
if gpgsign != "" {
runSubProcess(g, "bash", "-c", "git commit -m \""+message+"\"")
return "", nil
}
return runDirectCommand("git commit -m \"" + message + "\"")
}
func gitPull() (string, error) {
return runDirectCommand("git pull --no-edit")
}
func gitPush() (string, error) {
branchName := gitCurrentBranchName()
if branchName == "" {
return "", ErrNoCheckedOutBranch
}
return runDirectCommand("git push -u origin " + branchName)
}
func gitSquashPreviousTwoCommits(message string) (string, error) {
return runDirectCommand("git reset --soft HEAD^ && git commit --amend -m \"" + message + "\"")
}
func gitRenameCommit(message string) (string, error) {
return runDirectCommand("git commit --allow-empty --amend -m \"" + message + "\"")
}
func gitFetch() (string, error) {
return runDirectCommand("git fetch")
}
func gitResetToCommit(sha string) (string, error) {
return runDirectCommand("git reset " + sha)
}
func gitNewBranch(name string) (string, error) {
return runDirectCommand("git checkout -b " + name)
}
func gitListStash() (string, error) {
return runDirectCommand("git stash list")
}
func gitMerge(branchName string) (string, error) {
return runDirectCommand("git merge --no-edit " + branchName)
}
func gitAbortMerge() (string, error) {
return runDirectCommand("git merge --abort")
}
func gitUpstreamDifferenceCount() (string, string) {
pushableCount, err := runDirectCommand("git rev-list @{u}..head --count")
if err != nil {
return "?", "?"
}
pullableCount, err := runDirectCommand("git rev-list head..@{u} --count")
if err != nil {
return "?", "?"
}
return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount)
}
func gitCommitsToPush() []string {
pushables, err := runDirectCommand("git rev-list @{u}..head --abbrev-commit")
if err != nil {
return make([]string, 0)
}
return splitLines(pushables)
}
func gitCurrentBranchName() string {
branchName, err := runDirectCommand("git symbolic-ref --short HEAD")
// if there is an error, assume there are no branches yet
if err != nil {
return ""
}
return strings.TrimSpace(branchName)
}
// A line will have the form '10 days ago master' so we need to strip out the
// useful information from that into timeNumber, timeUnit, and branchName
func branchInfoFromLine(line string) (string, string, string) {
r := regexp.MustCompile("\\|.*\\s")
line = r.ReplaceAllString(line, " ")
words := strings.Split(line, " ")
return words[0], words[1], words[3]
}
func abbreviatedTimeUnit(timeUnit string) string {
r := regexp.MustCompile("s$")
timeUnit = r.ReplaceAllString(timeUnit, "")
timeUnitMap := map[string]string{
"hour": "h",
"minute": "m",
"second": "s",
"week": "w",
"year": "y",
"day": "d",
"month": "m",
}
return timeUnitMap[timeUnit]
}
func getBranches() []Branch {
branches := make([]Branch, 0)
rawString, err := runDirectCommand("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD")
if err != nil {
return branches
}
branchLines := splitLines(rawString)
for i, line := range branchLines {
timeNumber, timeUnit, branchName := branchInfoFromLine(line)
timeUnit = abbreviatedTimeUnit(timeUnit)
if branchAlreadyStored(branchName, branches) {
continue
}
branch := constructBranch(timeNumber+timeUnit, branchName, i)
branches = append(branches, branch)
}
return branches
}
func constructBranch(prefix, name string, index int) Branch {
branchType, branchBase, colourAttr := branchPropertiesFromName(name)
if index == 0 {
prefix = " *"
}
colour := color.New(colourAttr)
displayString := withPadding(prefix, 4) + coloredString(name, colour)
return Branch{
Name: name,
Type: branchType,
BaseBranch: branchBase,
DisplayString: displayString,
}
}
func getGitBranches() []Branch {
// check if there are any branches
branchCheck, _ := runCommand("git branch")
if branchCheck == "" {
return []Branch{constructBranch("", gitCurrentBranchName(), 0)}
}
branches := getBranches()
if len(branches) == 0 {
branches = append(branches, constructBranch("", gitCurrentBranchName(), 0))
}
branches = getAndMergeFetchedBranches(branches)
return branches
}
func branchAlreadyStored(branchName string, branches []Branch) bool {
for _, existingBranch := range branches {
if existingBranch.Name == branchName {
return true
}
}
return false
}
// here branches contains all the branches that we've checked out, along with
// the recency. In this function we append the branches that are in our heads
// directory i.e. things we've fetched but haven't necessarily checked out.
// Worth mentioning this has nothing to do with the 'git merge' operation
func getAndMergeFetchedBranches(branches []Branch) []Branch {
rawString, err := runDirectCommand("git branch --sort=-committerdate --no-color")
if err != nil {
return branches
}
branchLines := splitLines(rawString)
for _, line := range branchLines {
line = strings.Replace(line, "* ", "", -1)
line = strings.TrimSpace(line)
if branchAlreadyStored(line, branches) {
continue
}
branches = append(branches, constructBranch("", line, len(branches)))
}
return branches
}

64
go.mod Normal file
View File

@@ -0,0 +1,64 @@
module github.com/jesseduffield/lazygit
require (
github.com/aws/aws-sdk-go v1.15.21
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
github.com/davecgh/go-spew v1.1.0
github.com/emirpasic/gods v1.9.0
github.com/fatih/color v1.7.0
github.com/fsnotify/fsnotify v1.4.7
github.com/go-errors/errors v1.0.1
github.com/go-ini/ini v1.38.2
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
github.com/hashicorp/go-cleanhttp v0.0.0-20171218145408-d5fe4b57a186
github.com/hashicorp/go-getter v0.0.0-20180809191950-4bda8fa99001
github.com/hashicorp/go-safetemp v0.0.0-20180326211150-b1a1dbde6fdc
github.com/hashicorp/go-version v1.0.0
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce
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-20190303031804-b502ee11d674
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
github.com/kevinburke/ssh_config v0.0.0-20180317175531-9fc7bb800b55
github.com/magiconair/properties v1.8.0
github.com/mattn/go-colorable v0.0.9
github.com/mattn/go-isatty v0.0.3
github.com/mattn/go-runewidth v0.0.2
github.com/mgutz/str v1.2.0
github.com/mitchellh/go-homedir v0.0.0-20180801233206-58046073cbff
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77
github.com/mitchellh/mapstructure v0.0.0-20180715050151-f15292f7a699
github.com/nicksnyder/go-i18n v0.0.0-20180803040939-a16b91a3ba80
github.com/pelletier/go-buffruneio v0.2.0
github.com/pelletier/go-toml v1.2.0
github.com/pkg/errors v0.8.0
github.com/pmezard/go-difflib v1.0.0
github.com/sergi/go-diff v1.0.0
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0
github.com/sirupsen/logrus v1.0.6
github.com/spf13/afero v1.1.1
github.com/spf13/cast v1.2.0
github.com/spf13/jwalterweatherman v0.0.0-20180814060501-14d3d4c51834
github.com/spf13/pflag v1.0.2
github.com/spf13/viper v1.1.0
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
github.com/src-d/gcfg v1.3.0
github.com/stretchr/testify v1.2.2
github.com/stvp/roll v0.0.0-20170522205222-3627a5cbeaea
github.com/tcnksm/go-gitconfig v0.1.2
github.com/ulikunitz/xz v0.5.4
github.com/xanzy/ssh-agent v0.2.0
golang.org/x/crypto v0.0.0-20180808211826-de0752318171
golang.org/x/net v0.0.0-20180811021610-c39426892332
golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0
golang.org/x/text v0.3.0
gopkg.in/src-d/go-billy.v4 v4.2.0
gopkg.in/src-d/go-git.v4 v4.0.0-20180807092216-43d17e14b714
gopkg.in/warnings.v0 v0.1.2
gopkg.in/yaml.v2 v2.2.1
)

121
go.sum Normal file
View File

@@ -0,0 +1,121 @@
github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/aws/aws-sdk-go v1.15.21 h1:STLvc6RrpycslC1NRtTvt/YSgDkIGCTrB9K9vE5R2oQ=
github.com/aws/aws-sdk-go v1.15.21/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emirpasic/gods v1.9.0 h1:rUF4PuzEjMChMiNsVjdI+SyLu7rEqpQ5reNFnhC7oFo=
github.com/emirpasic/gods v1.9.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-ini/ini v1.38.2 h1:6Hl/z3p3iFkA0dlDfzYxuFuUGD+kaweypF6btsR2/Q4=
github.com/go-ini/ini v1.38.2/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 h1:zN2lZNZRflqFyxVaTIU61KNKQ9C0055u9CAfpmqUvo4=
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3/go.mod h1:nPpo7qLxd6XL3hWJG/O60sR8ZKfMCiIoNap5GvD12KU=
github.com/hashicorp/go-cleanhttp v0.0.0-20171218145408-d5fe4b57a186 h1:URgjUo+bs1KwatoNbwG0uCO4dHN4r1jsp4a5AGgHRjo=
github.com/hashicorp/go-cleanhttp v0.0.0-20171218145408-d5fe4b57a186/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-getter v0.0.0-20180809191950-4bda8fa99001 h1:MFPzqpPED05pFyGjNPJEC2sXM6EHTzFyvX+0s0JoZ48=
github.com/hashicorp/go-getter v0.0.0-20180809191950-4bda8fa99001/go.mod h1:6rdJFnhkXnzGOJbvkrdv4t9nLwKcVA+tmbQeUlkIzrU=
github.com/hashicorp/go-safetemp v0.0.0-20180326211150-b1a1dbde6fdc h1:wAa9fGALVHfjYxZuXRnmuJG2CnwRpJYOTvY6YdErAh0=
github.com/hashicorp/go-safetemp v0.0.0-20180326211150-b1a1dbde6fdc/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I=
github.com/hashicorp/go-version v1.0.0 h1:21MVWPKDphxa7ineQQTrCU5brh7OuVVAzGOCnnCPtE8=
github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce h1:xdsDDbiBDQTKASoGEZ+pEmF1OnWuu8AQ9I8iNbHNeno=
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
github.com/heroku/rollrus v0.0.0-20180515183152-fc0cef2ff331 h1:qio0y/sQdhbHRA3cmgczo04MaSV2zw+n46G1owvgWIk=
github.com/heroku/rollrus v0.0.0-20180515183152-fc0cef2ff331/go.mod h1:BT+PgT529opmb6mcUY+Fg0IwVRRmwqFyavEMU17GnBg=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jesseduffield/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-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=
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 h1:PJPDf8OUfOK1bb/NeTKd4f1QXZItOX389VN3B6qC8ro=
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/kevinburke/ssh_config v0.0.0-20180317175531-9fc7bb800b55 h1:S38dC4mEwxdw/U41+97VWdbun8mTcTjwg5Ujfg8QPME=
github.com/kevinburke/ssh_config v0.0.0-20180317175531-9fc7bb800b55/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mgutz/str v1.2.0 h1:4IzWSdIz9qPQWLfKZ0rJcV0jcUDpxvP4JVZ4GXQyvSw=
github.com/mgutz/str v1.2.0/go.mod h1:w1v0ofgLaJdoD0HpQ3fycxKD1WtxpjSo151pK/31q6w=
github.com/mitchellh/go-homedir v0.0.0-20180801233206-58046073cbff h1:jM4Eo4qMmmcqePS3u6X2lcEELtVuXWkWJIS/pRI3oSk=
github.com/mitchellh/go-homedir v0.0.0-20180801233206-58046073cbff/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 h1:7GoSOOW2jpsfkntVKaS2rAr1TJqfcxotyaUcuxoZSzg=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/mapstructure v0.0.0-20180715050151-f15292f7a699 h1:KXZJFdun9knAVAR8tg/aHJEr5DgtcbqyvzacK+CDCaI=
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.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=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 h1:Xuk8ma/ibJ1fOy4Ee11vHhUFHQNpHhrBneOCNHVXS5w=
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0/go.mod h1:7AwjWCpdPhkSmNAgUv5C7EJ4AbmjEB3r047r3DXWu3Y=
github.com/sirupsen/logrus v1.0.6 h1:hcP1GmhGigz/O7h1WVUM5KklBp1JoNS9FggWKdj/j3s=
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/spf13/afero v1.1.1 h1:Lt3ihYMlE+lreX1GS4Qw4ZsNpYQLxIXKBTEOXm3nt6I=
github.com/spf13/afero v1.1.1/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.2.0 h1:HHl1DSRbEQN2i8tJmtS6ViPyHx35+p51amrdsiTCrkg=
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
github.com/spf13/jwalterweatherman v0.0.0-20180814060501-14d3d4c51834 h1:kJI9pPzfsULT/72wy7mxkRQZPtKWgFdCA2RTGZ4v8/E=
github.com/spf13/jwalterweatherman v0.0.0-20180814060501-14d3d4c51834/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.2 h1:Fy0orTDgHdbnzHcsOgfCN4LtHf0ec3wwtiwJqwvf3Gc=
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.1.0 h1:V7OZpY8i3C1x/pDmU0zNNlfVoDz112fSYvtWMjjS3f4=
github.com/spf13/viper v1.1.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/src-d/gcfg v1.3.0 h1:2BEDr8r0I0b8h/fOqwtxCEiq2HJu8n2JGZJQFGXWLjg=
github.com/src-d/gcfg v1.3.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stvp/roll v0.0.0-20170522205222-3627a5cbeaea h1:jysxIKov/4GJ33wI2aRvuIK7yBwB28E5almlgDLPRpM=
github.com/stvp/roll v0.0.0-20170522205222-3627a5cbeaea/go.mod h1:Ffmqrj3nXIMIjeA4uW3Qjj0Ud9eDoTG0fu4JxyAr/tE=
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE=
github.com/ulikunitz/xz v0.5.4 h1:zATC2OoZ8H1TZll3FpbX+ikwmadbO699PE06cIkm9oU=
github.com/ulikunitz/xz v0.5.4/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/xanzy/ssh-agent v0.2.0 h1:Adglfbi5p9Z0BmK2oKU9nTG+zKfniSfnaMYB+ULd+Ro=
github.com/xanzy/ssh-agent v0.2.0/go.mod h1:0NyE30eGUDliuLEHJgYte/zncp2zdTStcOnWhgSqHD8=
golang.org/x/crypto v0.0.0-20180808211826-de0752318171 h1:vYogbvSFj2YXcjQxFHu/rASSOt9sLytpCaSkiwQ135I=
golang.org/x/crypto v0.0.0-20180808211826-de0752318171/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/net v0.0.0-20180811021610-c39426892332 h1:efGso+ep0DjyCBJPjvoz0HI6UldX4Md2F1rZFe1ir0E=
golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0 h1:8H8QZJ30plJyIVj60H3lr8TZGIq2Fh3Cyrs/ZNg1foU=
golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.0.0-20171214130843-f21a4dfb5e38/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/src-d/go-billy.v4 v4.2.0 h1:VGbrP1EsYxtvVPEiHui+4//imr4E5MGEFLx66bQtusg=
gopkg.in/src-d/go-billy.v4 v4.2.0/go.mod h1:ZHSF0JP+7oD97194otDUCD7Ofbk63+xFcfWP5bT6h+Q=
gopkg.in/src-d/go-git.v4 v4.0.0-20180807092216-43d17e14b714 h1:+wM2BGgQ1znCKBexOB4OrGVSDw8mtKNUSq3wqxZhi/k=
gopkg.in/src-d/go-git.v4 v4.0.0-20180807092216-43d17e14b714/go.mod h1:CzbUWqMn4pvmvndg3gnh5iZFmSsbhyhUWdI0IQ60AQo=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

237
gui.go
View File

@@ -1,237 +0,0 @@
package main
import (
// "io"
// "io/ioutil"
"strings"
"time"
// "strings"
"github.com/golang-collections/collections/stack"
"github.com/jesseduffield/gocui"
)
// OverlappingEdges determines if panel edges overlap
var OverlappingEdges = false
type stateType struct {
GitFiles []GitFile
Branches []Branch
Commits []Commit
StashEntries []StashEntry
PreviousView string
HasMergeConflicts bool
ConflictIndex int
ConflictTop bool
Conflicts []conflict
EditHistory *stack.Stack
}
type conflict struct {
start int
middle int
end int
}
var state = stateType{
GitFiles: make([]GitFile, 0),
PreviousView: "files",
Commits: make([]Commit, 0),
StashEntries: make([]StashEntry, 0),
ConflictIndex: 0,
ConflictTop: true,
Conflicts: make([]conflict, 0),
EditHistory: stack.New(),
}
func scrollUpMain(g *gocui.Gui, v *gocui.View) error {
mainView, _ := g.View("main")
ox, oy := mainView.Origin()
if oy >= 1 {
return mainView.SetOrigin(ox, oy-1)
}
return nil
}
func scrollDownMain(g *gocui.Gui, v *gocui.View) error {
mainView, _ := g.View("main")
ox, oy := mainView.Origin()
if oy < len(mainView.BufferLines()) {
return mainView.SetOrigin(ox, oy+1)
}
return nil
}
func handleRefresh(g *gocui.Gui, v *gocui.View) error {
return refreshSidePanels(g)
}
func layout(g *gocui.Gui) error {
g.Highlight = true
g.SelFgColor = gocui.ColorWhite | gocui.AttrBold
width, height := g.Size()
leftSideWidth := width / 3
statusFilesBoundary := 2
filesBranchesBoundary := 2 * height / 5 // height - 20
commitsBranchesBoundary := 3 * height / 5 // height - 10
commitsStashBoundary := height - 5 // height - 5
minimumHeight := 16
panelSpacing := 1
if OverlappingEdges {
panelSpacing = 0
}
if height < minimumHeight {
v, err := g.SetView("limit", 0, 0, width-1, height-1, 0)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = "Not enough space to render panels"
v.Wrap = true
}
return nil
}
g.DeleteView("limit")
optionsTop := height - 2
// hiding options if there's not enough space
if height < 30 {
optionsTop = height - 1
}
v, err := g.SetView("main", leftSideWidth+panelSpacing, 0, width-1, optionsTop, gocui.LEFT)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = "Diff"
v.Wrap = true
v.FgColor = gocui.ColorWhite
}
if v, err := g.SetView("status", 0, 0, leftSideWidth, statusFilesBoundary, gocui.BOTTOM|gocui.RIGHT); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = "Status"
v.FgColor = gocui.ColorWhite
}
filesView, err := g.SetView("files", 0, statusFilesBoundary+panelSpacing, leftSideWidth, filesBranchesBoundary, gocui.TOP|gocui.BOTTOM)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
filesView.Highlight = true
filesView.Title = "Files"
v.FgColor = gocui.ColorWhite
}
if v, err := g.SetView("branches", 0, filesBranchesBoundary+panelSpacing, leftSideWidth, commitsBranchesBoundary, gocui.TOP|gocui.BOTTOM); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = "Branches"
v.FgColor = gocui.ColorWhite
}
if v, err := g.SetView("commits", 0, commitsBranchesBoundary+panelSpacing, leftSideWidth, commitsStashBoundary, gocui.TOP|gocui.BOTTOM); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = "Commits"
v.FgColor = gocui.ColorWhite
}
if v, err := g.SetView("stash", 0, commitsStashBoundary+panelSpacing, leftSideWidth, optionsTop, gocui.TOP|gocui.RIGHT); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = "Stash"
v.FgColor = gocui.ColorWhite
}
if v, err := g.SetView("options", -1, optionsTop, width-len(version)-2, optionsTop+2, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.BgColor = gocui.ColorDefault
v.FgColor = gocui.ColorBlue
v.Frame = false
}
if v, err := g.SetView("version", width-len(version)-1, optionsTop, width, optionsTop+2, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.BgColor = gocui.ColorDefault
v.FgColor = gocui.ColorGreen
v.Frame = false
renderString(g, "version", version)
// these are only called once
handleFileSelect(g, filesView)
refreshFiles(g)
refreshBranches(g)
refreshCommits(g)
refreshStashEntries(g)
nextView(g, nil)
}
return nil
}
func fetch(g *gocui.Gui) {
gitFetch()
refreshStatus(g)
}
func updateLoader(g *gocui.Gui) {
if confirmationView, _ := g.View("confirmation"); confirmationView != nil {
content := trimmedContent(confirmationView)
if strings.Contains(content, "...") {
staticContent := strings.Split(content, "...")[0] + "..."
renderString(g, "confirmation", staticContent+" "+loader())
}
}
}
func run() (err error) {
g, err := gocui.NewGui(gocui.OutputNormal, OverlappingEdges)
if err != nil {
return
}
defer g.Close()
// periodically fetching to check for upstream differences
go func() {
for range time.Tick(time.Second * 60) {
fetch(g)
}
}()
go func() {
for range time.Tick(time.Millisecond * 10) {
updateLoader(g)
}
}()
g.SetManagerFunc(layout)
if err = keybindings(g); err != nil {
return
}
err = g.MainLoop()
return
}
func quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}

View File

@@ -1,68 +0,0 @@
package main
import "github.com/jesseduffield/gocui"
// Binding - a keybinding mapping a key and modifier to a handler. The keypress
// is only handled if the given view has focus, or handled globally if the view
// is ""
type Binding struct {
ViewName string
Handler func(*gocui.Gui, *gocui.View) error
Key interface{} // FIXME: find out how to get `gocui.Key | rune`
Modifier gocui.Modifier
}
func keybindings(g *gocui.Gui) error {
bindings := []Binding{
Binding{ViewName: "", Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: previousView},
Binding{ViewName: "", Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: nextView},
Binding{ViewName: "", Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: nextView},
Binding{ViewName: "", Key: 'q', Modifier: gocui.ModNone, Handler: quit},
Binding{ViewName: "", Key: gocui.KeyCtrlC, Modifier: gocui.ModNone, Handler: quit},
Binding{ViewName: "", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: cursorDown},
Binding{ViewName: "", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: cursorUp},
Binding{ViewName: "", Key: gocui.KeyPgup, Modifier: gocui.ModNone, Handler: scrollUpMain},
Binding{ViewName: "", Key: gocui.KeyPgdn, Modifier: gocui.ModNone, Handler: scrollDownMain},
Binding{ViewName: "", Key: 'P', Modifier: gocui.ModNone, Handler: pushFiles},
Binding{ViewName: "", Key: 'p', Modifier: gocui.ModNone, Handler: pullFiles},
Binding{ViewName: "", Key: 'R', Modifier: gocui.ModNone, Handler: handleRefresh},
Binding{ViewName: "files", Key: 'c', Modifier: gocui.ModNone, Handler: handleCommitPress},
Binding{ViewName: "files", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleFilePress},
Binding{ViewName: "files", Key: 'd', Modifier: gocui.ModNone, Handler: handleFileRemove},
Binding{ViewName: "files", Key: 'm', Modifier: gocui.ModNone, Handler: handleSwitchToMerge},
Binding{ViewName: "files", Key: 'e', Modifier: gocui.ModNone, Handler: handleFileEdit},
Binding{ViewName: "files", Key: 'o', Modifier: gocui.ModNone, Handler: handleFileOpen},
Binding{ViewName: "files", Key: 's', Modifier: gocui.ModNone, Handler: handleSublimeFileOpen},
Binding{ViewName: "files", Key: 'v', Modifier: gocui.ModNone, Handler: handleVsCodeFileOpen},
Binding{ViewName: "files", Key: 'i', Modifier: gocui.ModNone, Handler: handleIgnoreFile},
Binding{ViewName: "files", Key: 'r', Modifier: gocui.ModNone, Handler: handleRefreshFiles},
Binding{ViewName: "files", Key: 'S', Modifier: gocui.ModNone, Handler: handleStashSave},
Binding{ViewName: "files", Key: 'a', Modifier: gocui.ModNone, Handler: handleAbortMerge},
Binding{ViewName: "files", Key: 't', Modifier: gocui.ModNone, Handler: handleAddPatch},
Binding{ViewName: "main", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: handleSelectTop},
Binding{ViewName: "main", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: handleSelectBottom},
Binding{ViewName: "main", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: handleEscapeMerge},
Binding{ViewName: "main", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handlePickHunk},
Binding{ViewName: "main", Key: 'b', Modifier: gocui.ModNone, Handler: handlePickBothHunks},
Binding{ViewName: "main", Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: handleSelectPrevConflict},
Binding{ViewName: "main", Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: handleSelectNextConflict},
Binding{ViewName: "main", Key: 'z', Modifier: gocui.ModNone, Handler: handlePopFileSnapshot},
Binding{ViewName: "branches", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleBranchPress},
Binding{ViewName: "branches", Key: 'c', Modifier: gocui.ModNone, Handler: handleCheckoutByName},
Binding{ViewName: "branches", Key: 'F', Modifier: gocui.ModNone, Handler: handleForceCheckout},
Binding{ViewName: "branches", Key: 'n', Modifier: gocui.ModNone, Handler: handleNewBranch},
Binding{ViewName: "branches", Key: 'm', Modifier: gocui.ModNone, Handler: handleMerge},
Binding{ViewName: "commits", Key: 's', Modifier: gocui.ModNone, Handler: handleCommitSquashDown},
Binding{ViewName: "commits", Key: 'r', Modifier: gocui.ModNone, Handler: handleRenameCommit},
Binding{ViewName: "commits", Key: 'g', Modifier: gocui.ModNone, Handler: handleResetToCommit},
Binding{ViewName: "stash", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleStashApply},
Binding{ViewName: "stash", Key: 'k', Modifier: gocui.ModNone, Handler: handleStashPop},
Binding{ViewName: "stash", Key: 'd', Modifier: gocui.ModNone, Handler: handleStashDrop},
}
for _, binding := range bindings {
if err := g.SetKeybinding(binding.ViewName, binding.Key, binding.Modifier, binding.Handler); err != nil {
return err
}
}
return nil
}

104
main.go
View File

@@ -1,93 +1,61 @@
package main
import (
"errors"
"flag"
"fmt"
"log"
"os"
"os/exec"
"os/user"
"time"
"path/filepath"
"runtime"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/app"
"github.com/jesseduffield/lazygit/pkg/config"
)
// ErrSubProcess is raised when we are running a subprocess
var (
ErrSubprocess = errors.New("running subprocess")
subprocess *exec.Cmd
startTime time.Time
commit string
version = "unversioned"
date string
buildSource = "unknown"
commit string
version = "unversioned"
date string
configFlag = flag.Bool("config", false, "Print the current default config")
debuggingFlag = flag.Bool("debug", false, "a boolean")
versionFlag = flag.Bool("v", false, "Print the current version")
)
func homeDirectory() string {
usr, err := user.Current()
if err != nil {
log.Fatal(err)
}
return usr.HomeDir
}
func devLog(objects ...interface{}) {
localLog(color.FgWhite, homeDirectory()+"/go/src/github.com/jesseduffield/lazygit/development.log", objects...)
}
func colorLog(colour color.Attribute, objects ...interface{}) {
localLog(colour, homeDirectory()+"/go/src/github.com/jesseduffield/lazygit/development.log", objects...)
}
func commandLog(objects ...interface{}) {
localLog(color.FgWhite, homeDirectory()+"/go/src/github.com/jesseduffield/lazygit/commands.log", objects...)
}
func localLog(colour color.Attribute, path string, objects ...interface{}) {
if !*debuggingFlag {
return
}
f, _ := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0644)
defer f.Close()
for _, object := range objects {
colorFunction := color.New(colour).SprintFunc()
f.WriteString(colorFunction(fmt.Sprint(object)) + "\n")
}
}
func navigateToRepoRootDirectory() {
_, err := os.Stat(".git")
for os.IsNotExist(err) {
devLog("going up a directory to find the root")
os.Chdir("..")
_, err = os.Stat(".git")
}
func projectPath(path string) string {
gopath := os.Getenv("GOPATH")
return filepath.FromSlash(gopath + "/src/github.com/jesseduffield/lazygit/" + path)
}
func main() {
startTime = time.Now()
devLog("\n\n\n\n\n\n\n\n\n\n")
flag.Parse()
if *versionFlag {
fmt.Printf("commit=%s, build date=%s, version=%s", commit, date, version)
fmt.Printf("commit=%s, build date=%s, build source=%s, version=%s, os=%s, arch=%s\n", commit, date, buildSource, version, runtime.GOOS, runtime.GOARCH)
os.Exit(0)
}
verifyInGitRepo()
navigateToRepoRootDirectory()
for {
if err := run(); err != nil {
if err == gocui.ErrQuit {
break
} else if err == ErrSubprocess {
subprocess.Run()
} else {
log.Panicln(err)
}
}
if *configFlag {
fmt.Printf("%s\n", config.GetDefaultConfig())
os.Exit(0)
}
appConfig, err := config.NewAppConfig("lazygit", version, commit, date, buildSource, *debuggingFlag)
if err != nil {
log.Fatal(err.Error())
}
app, err := app.NewApp(appConfig)
if err == nil {
err = app.Run()
}
if err != nil {
newErr := errors.Wrap(err, 0)
stackTrace := newErr.ErrorStack()
app.Log.Error(stackTrace)
log.Fatal(fmt.Sprintf("%s\n\n%s", app.Tr.SLocalize("ErrorOccurred"), stackTrace))
}
}

View File

@@ -1,264 +0,0 @@
// though this panel is called the merge panel, it's really going to use the main panel. This may change in the future
package main
import (
"bufio"
"bytes"
"io/ioutil"
"math"
"os"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
)
func findConflicts(content string) ([]conflict, error) {
conflicts := make([]conflict, 0)
var newConflict conflict
for i, line := range splitLines(content) {
if line == "<<<<<<< HEAD" || line == "<<<<<<< MERGE_HEAD" || line == "<<<<<<< Updated upstream" {
newConflict = conflict{start: i}
} else if line == "=======" {
newConflict.middle = i
} else if strings.HasPrefix(line, ">>>>>>> ") {
newConflict.end = i
conflicts = append(conflicts, newConflict)
}
}
return conflicts, nil
}
func shiftConflict(conflicts []conflict) (conflict, []conflict) {
return conflicts[0], conflicts[1:]
}
func shouldHighlightLine(index int, conflict conflict, top bool) bool {
return (index >= conflict.start && index <= conflict.middle && top) || (index >= conflict.middle && index <= conflict.end && !top)
}
func coloredConflictFile(content string, conflicts []conflict, conflictIndex int, conflictTop, hasFocus bool) (string, error) {
if len(conflicts) == 0 {
return content, nil
}
conflict, remainingConflicts := shiftConflict(conflicts)
var outputBuffer bytes.Buffer
for i, line := range splitLines(content) {
colourAttr := color.FgWhite
if i == conflict.start || i == conflict.middle || i == conflict.end {
colourAttr = color.FgRed
}
colour := color.New(colourAttr)
if hasFocus && conflictIndex < len(conflicts) && conflicts[conflictIndex] == conflict && shouldHighlightLine(i, conflict, conflictTop) {
colour.Add(color.Bold)
}
if i == conflict.end && len(remainingConflicts) > 0 {
conflict, remainingConflicts = shiftConflict(remainingConflicts)
}
outputBuffer.WriteString(coloredString(line, colour) + "\n")
}
return outputBuffer.String(), nil
}
func handleSelectTop(g *gocui.Gui, v *gocui.View) error {
state.ConflictTop = true
return refreshMergePanel(g)
}
func handleSelectBottom(g *gocui.Gui, v *gocui.View) error {
state.ConflictTop = false
return refreshMergePanel(g)
}
func handleSelectNextConflict(g *gocui.Gui, v *gocui.View) error {
if state.ConflictIndex >= len(state.Conflicts)-1 {
return nil
}
state.ConflictIndex++
return refreshMergePanel(g)
}
func handleSelectPrevConflict(g *gocui.Gui, v *gocui.View) error {
if state.ConflictIndex <= 0 {
return nil
}
state.ConflictIndex--
return refreshMergePanel(g)
}
func isIndexToDelete(i int, conflict conflict, pick string) bool {
return i == conflict.middle ||
i == conflict.start ||
i == conflict.end ||
pick != "both" &&
(pick == "bottom" && i > conflict.start && i < conflict.middle) ||
(pick == "top" && i > conflict.middle && i < conflict.end)
}
func resolveConflict(g *gocui.Gui, conflict conflict, pick string) error {
gitFile, err := getSelectedFile(g)
if err != nil {
return err
}
file, err := os.Open(gitFile.Name)
if err != nil {
return err
}
defer file.Close()
reader := bufio.NewReader(file)
output := ""
for i := 0; true; i++ {
line, err := reader.ReadString('\n')
if err != nil {
break
}
if !isIndexToDelete(i, conflict, pick) {
output += line
}
}
devLog(output)
return ioutil.WriteFile(gitFile.Name, []byte(output), 0644)
}
func pushFileSnapshot(g *gocui.Gui) error {
gitFile, err := getSelectedFile(g)
if err != nil {
return err
}
content, err := catFile(gitFile.Name)
if err != nil {
return err
}
state.EditHistory.Push(content)
return nil
}
func handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error {
colorLog(color.FgCyan, "IM HERE")
if state.EditHistory.Len() == 0 {
return nil
}
prevContent := state.EditHistory.Pop().(string)
gitFile, err := getSelectedFile(g)
if err != nil {
return err
}
ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644)
return refreshMergePanel(g)
}
func handlePickHunk(g *gocui.Gui, v *gocui.View) error {
conflict := state.Conflicts[state.ConflictIndex]
pushFileSnapshot(g)
pick := "bottom"
if state.ConflictTop {
pick = "top"
}
err := resolveConflict(g, conflict, pick)
if err != nil {
panic(err)
}
refreshMergePanel(g)
return nil
}
func handlePickBothHunks(g *gocui.Gui, v *gocui.View) error {
conflict := state.Conflicts[state.ConflictIndex]
pushFileSnapshot(g)
err := resolveConflict(g, conflict, "both")
if err != nil {
panic(err)
}
return refreshMergePanel(g)
}
func currentViewName(g *gocui.Gui) string {
currentView := g.CurrentView()
return currentView.Name()
}
func refreshMergePanel(g *gocui.Gui) error {
cat, err := catSelectedFile(g)
if err != nil {
return err
}
state.Conflicts, err = findConflicts(cat)
if err != nil {
return err
}
if len(state.Conflicts) == 0 {
return handleCompleteMerge(g)
} else if state.ConflictIndex > len(state.Conflicts)-1 {
state.ConflictIndex = len(state.Conflicts) - 1
}
hasFocus := currentViewName(g) == "main"
if hasFocus {
renderMergeOptions(g)
}
content, err := coloredConflictFile(cat, state.Conflicts, state.ConflictIndex, state.ConflictTop, hasFocus)
if err != nil {
return err
}
if err := scrollToConflict(g); err != nil {
return err
}
return renderString(g, "main", content)
}
func scrollToConflict(g *gocui.Gui) error {
mainView, err := g.View("main")
if err != nil {
return err
}
if len(state.Conflicts) == 0 {
return nil
}
conflict := state.Conflicts[state.ConflictIndex]
ox, _ := mainView.Origin()
_, height := mainView.Size()
conflictMiddle := (conflict.end + conflict.start) / 2
newOriginY := int(math.Max(0, float64(conflictMiddle-(height/2))))
return mainView.SetOrigin(ox, newOriginY)
}
func switchToMerging(g *gocui.Gui) error {
state.ConflictIndex = 0
state.ConflictTop = true
_, err := g.SetCurrentView("main")
if err != nil {
return err
}
return refreshMergePanel(g)
}
func renderMergeOptions(g *gocui.Gui) error {
return renderOptionsMap(g, map[string]string{
"↑ ↓": "select hunk",
"← →": "navigate conflicts",
"space": "pick hunk",
"b": "pick both hunks",
"z": "undo",
})
}
func handleEscapeMerge(g *gocui.Gui, v *gocui.View) error {
filesView, err := g.View("files")
if err != nil {
return err
}
refreshFiles(g)
return switchFocus(g, v, filesView)
}
func handleCompleteMerge(g *gocui.Gui) error {
filesView, err := g.View("files")
if err != nil {
return err
}
stageSelectedFile(g)
refreshFiles(g)
return switchFocus(g, nil, filesView)
}

163
pkg/app/app.go Normal file
View File

@@ -0,0 +1,163 @@
package app
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/heroku/rollrus"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/config"
"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"
)
// App struct
type App struct {
closers []io.Closer
Config config.AppConfigurer
Log *logrus.Entry
OSCommand *commands.OSCommand
GitCommand *commands.GitCommand
Gui *gui.Gui
Tr *i18n.Localizer
Updater *updates.Updater // may only need this on the Gui
ClientContext string
}
func newProductionLogger(config config.AppConfigurer) *logrus.Logger {
log := logrus.New()
log.Out = ioutil.Discard
return log
}
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(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)
}
log.SetOutput(file)
return log
}
func newLogger(config config.AppConfigurer) *logrus.Entry {
var log *logrus.Logger
environment := "production"
if config.GetDebug() || os.Getenv("DEBUG") == "TRUE" {
environment = "development"
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)
log.Hooks.Add(hook)
}
return log.WithFields(logrus.Fields{
"debug": config.GetDebug(),
"version": config.GetVersion(),
"commit": config.GetCommit(),
"buildDate": config.GetBuildDate(),
})
}
// NewApp bootstrap a new application
func NewApp(config config.AppConfigurer) (*App, error) {
app := &App{
closers: []io.Closer{},
Config: config,
}
var err error
app.Log = newLogger(config)
app.Tr = i18n.NewLocalizer(app.Log)
// if we are being called in 'demon' mode, we can just return here
app.ClientContext = os.Getenv("LAZYGIT_CLIENT_COMMAND")
if app.ClientContext != "" {
return app, nil
}
app.OSCommand = commands.NewOSCommand(app.Log, config)
app.Updater, err = updates.NewUpdater(app.Log, config, app.OSCommand, app.Tr)
if err != nil {
return app, err
}
app.GitCommand, err = commands.NewGitCommand(app.Log, app.OSCommand, app.Tr, app.Config)
if err != nil {
if strings.Contains(err.Error(), "Not a git repository") {
fmt.Println("Not in a git repository. Use `git init` to create a new one")
os.Exit(1)
}
return app, err
}
app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config, app.Updater)
if err != nil {
return app, err
}
return app, nil
}
func (app *App) Run() error {
if app.ClientContext == "INTERACTIVE_REBASE" {
return app.Rebase()
}
if app.ClientContext == "EXIT_IMMEDIATELY" {
os.Exit(0)
}
return app.Gui.RunWithSubprocesses()
}
// Rebase contains logic for when we've been run in demon mode, meaning we've
// given lazygit as a command for git to call e.g. to edit a file
func (app *App) Rebase() error {
app.Log.Info("Lazygit invoked as interactive rebase demon")
app.Log.Info("args: ", os.Args)
if strings.HasSuffix(os.Args[1], "git-rebase-todo") {
if err := ioutil.WriteFile(os.Args[1], []byte(os.Getenv("LAZYGIT_REBASE_TODO")), 0644); err != nil {
return err
}
} else if strings.HasSuffix(os.Args[1], ".git/COMMIT_EDITMSG") {
// if we are rebasing and squashing, we'll see a COMMIT_EDITMSG
// but in this case we don't need to edit it, so we'll just return
} else {
app.Log.Info("Lazygit demon did not match on any use cases")
}
return nil
}
// Close closes any resources
func (app *App) Close() error {
for _, closer := range app.closers {
err := closer.Close()
if err != nil {
return err
}
}
return nil
}

48
pkg/commands/branch.go Normal file
View File

@@ -0,0 +1,48 @@
package commands
import (
"fmt"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// Branch : A git branch
// duplicating this for now
type Branch struct {
Name string
Recency string
Pushables string
Pullables string
Selected bool
}
// GetDisplayStrings returns the dispaly string of branch
func (b *Branch) GetDisplayStrings(isFocused bool) []string {
displayName := utils.ColoredString(b.Name, b.GetColor())
if isFocused && 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
func (b *Branch) GetColor() color.Attribute {
switch b.getType() {
case "feature":
return color.FgGreen
case "bugfix":
return color.FgYellow
case "hotfix":
return color.FgRed
default:
return color.FgWhite
}
}
// expected to return feature/bugfix/hotfix or blank string
func (b *Branch) getType() string {
return strings.Split(b.Name, "/")[0]
}

56
pkg/commands/commit.go Normal file
View File

@@ -0,0 +1,56 @@
package commands
import (
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// Commit : A git commit
type Commit struct {
Sha string
Name string
Status string // one of "unpushed", "pushed", "merged", or "rebasing"
DisplayString string
Action string // one of "", "pick", "edit", "squash", "reword", "drop", "fixup"
Copied bool // to know if this commit is ready to be cherry-picked somewhere
}
// GetDisplayStrings is a function.
func (c *Commit) GetDisplayStrings(isFocused bool) []string {
red := color.New(color.FgRed)
yellow := color.New(color.FgYellow)
green := color.New(color.FgGreen)
blue := color.New(color.FgBlue)
cyan := color.New(color.FgCyan)
white := color.New(color.FgWhite)
// for some reason, setting the background to blue pads out the other commits
// horizontally. For the sake of accessibility I'm considering this a feature,
// not a bug
copied := color.New(color.FgCyan, color.BgBlue)
var shaColor *color.Color
switch c.Status {
case "unpushed":
shaColor = red
case "pushed":
shaColor = yellow
case "merged":
shaColor = green
case "rebasing":
shaColor = blue
default:
shaColor = white
}
if c.Copied {
shaColor = copied
}
actionString := ""
if c.Action != "" {
actionString = cyan.Sprint(utils.WithPadding(c.Action, 7)) + " "
}
return []string{shaColor.Sprint(c.Sha), actionString + white.Sprint(c.Name)}
}

58
pkg/commands/dummies.go Normal file
View File

@@ -0,0 +1,58 @@
package commands
import (
"io/ioutil"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
yaml "gopkg.in/yaml.v2"
)
// This file exports dummy constructors for use by tests in other packages
// NewDummyOSCommand creates a new dummy OSCommand for testing
func NewDummyOSCommand() *OSCommand {
return NewOSCommand(NewDummyLog(), NewDummyAppConfig())
}
// NewDummyAppConfig creates a new dummy AppConfig for testing
func NewDummyAppConfig() *config.AppConfig {
appConfig := &config.AppConfig{
Name: "lazygit",
Version: "unversioned",
Commit: "",
BuildDate: "",
Debug: false,
BuildSource: "",
UserConfig: viper.New(),
}
_ = yaml.Unmarshal([]byte{}, appConfig.AppState)
return appConfig
}
// NewDummyLog creates a new dummy Log for testing
func NewDummyLog() *logrus.Entry {
log := logrus.New()
log.Out = ioutil.Discard
return log.WithField("test", "test")
}
// NewDummyGitCommand creates a new dummy GitCommand for testing
func NewDummyGitCommand() *GitCommand {
return NewDummyGitCommandWithOSCommand(NewDummyOSCommand())
}
// NewDummyGitCommandWithOSCommand creates a new dummy GitCommand for testing
func NewDummyGitCommandWithOSCommand(osCommand *OSCommand) *GitCommand {
return &GitCommand{
Log: NewDummyLog(),
OSCommand: osCommand,
Tr: i18n.NewLocalizer(NewDummyLog()),
Config: NewDummyAppConfig(),
getGlobalGitConfig: func(string) (string, error) { return "", nil },
getLocalGitConfig: func(string) (string, error) { return "", nil },
removeFile: func(string) error { return nil },
}
}

View File

@@ -0,0 +1,100 @@
// +build !windows
package commands
import (
"bufio"
"bytes"
"os"
"strings"
"unicode/utf8"
"github.com/go-errors/errors"
"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 := c.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)
}

37
pkg/commands/file.go Normal file
View File

@@ -0,0 +1,37 @@
package commands
import "github.com/fatih/color"
// File : A file from git status
// duplicating this for now
type File struct {
Name string
HasStagedChanges bool
HasUnstagedChanges bool
Tracked bool
Deleted bool
HasMergeConflicts bool
HasInlineMergeConflicts bool
DisplayString string
Type string // one of 'file', 'directory', and 'other'
}
// GetDisplayStrings returns the display string of a file
func (f *File) GetDisplayStrings(isFocused bool) []string {
// potentially inefficient to be instantiating these color
// objects with each render
red := color.New(color.FgRed)
green := color.New(color.FgGreen)
if !f.Tracked && !f.HasStagedChanges {
return []string{red.Sprint(f.DisplayString)}
}
output := green.Sprint(f.DisplayString[0:1])
output += red.Sprint(f.DisplayString[1:3])
if f.HasUnstagedChanges {
output += red.Sprint(f.Name)
} else {
output += green.Sprint(f.Name)
}
return []string{output}
}

798
pkg/commands/git.go Normal file
View File

@@ -0,0 +1,798 @@
package commands
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"strings"
"github.com/mgutz/str"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
gitconfig "github.com/tcnksm/go-gitconfig"
gogit "gopkg.in/src-d/go-git.v4"
)
func verifyInGitRepo(runCmd func(string) error) error {
return runCmd("git status")
}
func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir func(string) error) error {
for {
f, err := stat(".git")
if err == nil && f.IsDir() {
return nil
}
if !os.IsNotExist(err) {
return errors.Wrap(err, 0)
}
if err = chdir(".."); err != nil {
return errors.Wrap(err, 0)
}
}
}
func setupRepositoryAndWorktree(openGitRepository func(string) (*gogit.Repository, error), sLocalize func(string) string) (repository *gogit.Repository, worktree *gogit.Worktree, err error) {
repository, err = openGitRepository(".")
if err != nil {
if strings.Contains(err.Error(), `unquoted '\' must be followed by new line`) {
return nil, nil, errors.New(sLocalize("GitconfigParseErr"))
}
return
}
worktree, err = repository.Worktree()
if err != nil {
return
}
return
}
// GitCommand is our main git interface
type GitCommand struct {
Log *logrus.Entry
OSCommand *OSCommand
Worktree *gogit.Worktree
Repo *gogit.Repository
Tr *i18n.Localizer
Config config.AppConfigurer
getGlobalGitConfig func(string) (string, error)
getLocalGitConfig func(string) (string, error)
removeFile func(string) error
}
// NewGitCommand it runs git commands
func NewGitCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localizer, config config.AppConfigurer) (*GitCommand, error) {
var worktree *gogit.Worktree
var repo *gogit.Repository
fs := []func() error{
func() error {
return verifyInGitRepo(osCommand.RunCommand)
},
func() error {
return navigateToRepoRootDirectory(os.Stat, os.Chdir)
},
func() error {
var err error
repo, worktree, err = setupRepositoryAndWorktree(gogit.PlainOpen, tr.SLocalize)
return err
},
}
for _, f := range fs {
if err := f(); err != nil {
return nil, err
}
}
return &GitCommand{
Log: log,
OSCommand: osCommand,
Tr: tr,
Worktree: worktree,
Repo: repo,
Config: config,
getGlobalGitConfig: gitconfig.Global,
getLocalGitConfig: gitconfig.Local,
removeFile: os.RemoveAll,
}, nil
}
// GetStashEntries stash entryies
func (c *GitCommand) GetStashEntries() []*StashEntry {
rawString, _ := c.OSCommand.RunCommandWithOutput("git stash list --pretty='%gs'")
stashEntries := []*StashEntry{}
for i, line := range utils.SplitLines(rawString) {
stashEntries = append(stashEntries, stashEntryFromLine(line, i))
}
return stashEntries
}
func stashEntryFromLine(line string, index int) *StashEntry {
return &StashEntry{
Name: line,
Index: index,
DisplayString: line,
}
}
// GetStashEntryDiff stash diff
func (c *GitCommand) GetStashEntryDiff(index int) (string, error) {
return c.OSCommand.RunCommandWithOutput("git stash show -p --color stash@{" + fmt.Sprint(index) + "}")
}
// GetStatusFiles git status files
func (c *GitCommand) GetStatusFiles() []*File {
statusOutput, _ := c.GitStatus()
statusStrings := utils.SplitLines(statusOutput)
files := []*File{}
for _, statusString := range statusStrings {
change := statusString[0:2]
stagedChange := change[0:1]
unstagedChange := statusString[1:2]
filename := c.OSCommand.Unquote(statusString[3:])
_, untracked := map[string]bool{"??": true, "A ": true, "AM": true}[change]
_, hasNoStagedChanges := map[string]bool{" ": true, "U": true, "?": true}[stagedChange]
file := &File{
Name: filename,
DisplayString: statusString,
HasStagedChanges: !hasNoStagedChanges,
HasUnstagedChanges: unstagedChange != " ",
Tracked: !untracked,
Deleted: unstagedChange == "D" || stagedChange == "D",
HasMergeConflicts: change == "UU" || change == "AA" || change == "DU",
HasInlineMergeConflicts: change == "UU" || change == "AA",
Type: c.OSCommand.FileType(filename),
}
files = append(files, file)
}
return files
}
// StashDo modify stash
func (c *GitCommand) StashDo(index int, method string) error {
return c.OSCommand.RunCommand(fmt.Sprintf("git stash %s stash@{%d}", method, index))
}
// StashSave save stash
// TODO: before calling this, check if there is anything to save
func (c *GitCommand) StashSave(message string) error {
return c.OSCommand.RunCommand(fmt.Sprintf("git stash save %s", c.OSCommand.Quote(message)))
}
// MergeStatusFiles merge status files
func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []*File) []*File {
if len(oldFiles) == 0 {
return newFiles
}
appendedIndexes := []int{}
// retain position of files we already could see
result := []*File{}
for _, oldFile := range oldFiles {
for newIndex, newFile := range newFiles {
if oldFile.Name == newFile.Name {
result = append(result, newFile)
appendedIndexes = append(appendedIndexes, newIndex)
break
}
}
}
// append any new files to the end
for index, newFile := range newFiles {
if !includesInt(appendedIndexes, index) {
result = append(result, newFile)
}
}
return result
}
func includesInt(list []int, a int) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
// 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")
}
func (c *GitCommand) GetCurrentBranchUpstreamDifferenceCount() (string, string) {
return c.GetCommitDifferences("HEAD", "@{u}")
}
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) 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(fmt.Sprintf(command, from, to))
if err != nil {
return "?", "?"
}
return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount)
}
// RenameCommit renames the topmost commit with the given name
func (c *GitCommand) RenameCommit(name string) error {
return c.OSCommand.RunCommand(fmt.Sprintf("git commit --allow-empty --amend -m %s", c.OSCommand.Quote(name)))
}
// RebaseBranch interactive rebases onto a branch
func (c *GitCommand) RebaseBranch(branchName string) error {
cmd, err := c.PrepareInteractiveRebaseCommand(branchName, "", false)
if err != nil {
return err
}
return c.OSCommand.RunPreparedCommand(cmd)
}
// Fetch fetch git repo
func (c *GitCommand) Fetch(unamePassQuestion func(string) string, canAskForCredentials bool) error {
return c.OSCommand.DetectUnamePass("git fetch", func(question string) string {
if canAskForCredentials {
return unamePassQuestion(question)
}
return "\n"
})
}
// ResetToCommit reset to commit
func (c *GitCommand) ResetToCommit(sha string) error {
return c.OSCommand.RunCommand(fmt.Sprintf("git reset %s", sha))
}
// NewBranch create new branch
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"
if force {
command = "git branch -D"
}
return c.OSCommand.RunCommand(fmt.Sprintf("%s %s", command, branch))
}
// ListStash list stash
func (c *GitCommand) ListStash() (string, error) {
return c.OSCommand.RunCommandWithOutput("git stash list")
}
// Merge merge
func (c *GitCommand) Merge(branchName string) error {
return c.OSCommand.RunCommand(fmt.Sprintf("git merge --no-edit %s", branchName))
}
// AbortMerge abort merge
func (c *GitCommand) AbortMerge() error {
return c.OSCommand.RunCommand("git merge --abort")
}
// usingGpg tells us whether the user has gpg enabled so that we can know
// whether we need to run a subprocess to allow them to enter their password
func (c *GitCommand) usingGpg() bool {
gpgsign, _ := c.getLocalGitConfig("commit.gpgsign")
if gpgsign == "" {
gpgsign, _ = c.getGlobalGitConfig("commit.gpgsign")
}
value := strings.ToLower(gpgsign)
return value == "true" || value == "1" || value == "yes" || value == "on"
}
// Commit commits to git
func (c *GitCommand) Commit(message string) (*exec.Cmd, error) {
command := fmt.Sprintf("git commit -m %s", c.OSCommand.Quote(message))
if c.usingGpg() {
return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command), nil
}
return nil, c.OSCommand.RunCommand(command)
}
// AmendHead amends HEAD with whatever is staged in your working tree
func (c *GitCommand) AmendHead() (*exec.Cmd, error) {
command := "git commit --amend --no-edit"
if c.usingGpg() {
return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command), nil
}
return nil, c.OSCommand.RunCommand(command)
}
// Pull pulls from repo
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, ask func(string) string) error {
forceFlag := ""
if force {
forceFlag = "--force-with-lease "
}
cmd := fmt.Sprintf("git push %s-u origin %s", forceFlag, branchName)
return c.OSCommand.DetectUnamePass(cmd, ask)
}
// CatFile obtains the content of a file
func (c *GitCommand) CatFile(fileName string) (string, error) {
return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("cat %s", c.OSCommand.Quote(fileName)))
}
// StageFile stages a file
func (c *GitCommand) StageFile(fileName string) error {
return c.OSCommand.RunCommand(fmt.Sprintf("git add %s", c.OSCommand.Quote(fileName)))
}
// StageAll stages all files
func (c *GitCommand) StageAll() error {
return c.OSCommand.RunCommand("git add -A")
}
// UnstageAll stages all files
func (c *GitCommand) UnstageAll() error {
return c.OSCommand.RunCommand("git reset")
}
// UnStageFile unstages a file
func (c *GitCommand) UnStageFile(fileName string, tracked bool) error {
command := "git rm --cached %s"
if tracked {
command = "git reset HEAD %s"
}
// renamed files look like "file1 -> file2"
fileNames := strings.Split(fileName, " -> ")
for _, name := range fileNames {
if err := c.OSCommand.RunCommand(fmt.Sprintf(command, c.OSCommand.Quote(name))); err != nil {
return err
}
}
return nil
}
// GitStatus returns the plaintext short status of the repo
func (c *GitCommand) GitStatus() (string, error) {
return c.OSCommand.RunCommandWithOutput("git status --untracked-files=all --short")
}
// IsInMergeState states whether we are still mid-merge
func (c *GitCommand) IsInMergeState() (bool, error) {
output, err := c.OSCommand.RunCommandWithOutput("git status --untracked-files=all")
if err != nil {
return false, err
}
return strings.Contains(output, "conclude merge") || strings.Contains(output, "unmerged paths"), nil
}
// RebaseMode returns "" for non-rebase mode, "normal" for normal rebase
// and "interactive" for interactive rebase
func (c *GitCommand) RebaseMode() (string, error) {
exists, err := c.OSCommand.FileExists(".git/rebase-apply")
if err != nil {
return "", err
}
if exists {
return "normal", nil
}
exists, err = c.OSCommand.FileExists(".git/rebase-merge")
if exists {
return "interactive", err
} else {
return "", err
}
}
// RemoveFile directly
func (c *GitCommand) RemoveFile(file *File) error {
// if the file isn't tracked, we assume you want to delete it
quotedFileName := c.OSCommand.Quote(file.Name)
if file.HasStagedChanges {
if err := c.OSCommand.RunCommand(fmt.Sprintf("git reset -- %s", quotedFileName)); err != nil {
return err
}
}
if !file.Tracked {
return c.removeFile(file.Name)
}
// if the file is tracked, we assume you want to just check it out
return c.OSCommand.RunCommand(fmt.Sprintf("git checkout -- %s", quotedFileName))
}
// Checkout checks out a branch, with --force if you set the force arg to true
func (c *GitCommand) Checkout(branch string, force bool) error {
forceArg := ""
if force {
forceArg = "--force "
}
return c.OSCommand.RunCommand(fmt.Sprintf("git checkout %s %s", forceArg, branch))
}
// AddPatch prepares a subprocess for adding a patch by patch
// this will eventually be swapped out for a better solution inside the Gui
func (c *GitCommand) AddPatch(filename string) *exec.Cmd {
return c.OSCommand.PrepareSubProcess("git", "add", "--patch", c.OSCommand.Quote(filename))
}
// PrepareCommitSubProcess prepares a subprocess for `git commit`
func (c *GitCommand) PrepareCommitSubProcess() *exec.Cmd {
return c.OSCommand.PrepareSubProcess("git", "commit")
}
// PrepareCommitAmendSubProcess prepares a subprocess for `git commit --amend --allow-empty`
func (c *GitCommand) PrepareCommitAmendSubProcess() *exec.Cmd {
return c.OSCommand.PrepareSubProcess("git", "commit", "--amend", "--allow-empty")
}
// GetBranchGraph gets the color-formatted graph of the log for the given branch
// Currently it limits the result to 100 commits, but when we get async stuff
// working we can do lazy loading
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))
}
// Ignore adds a file to the gitignore for the repo
func (c *GitCommand) Ignore(filename string) error {
return c.OSCommand.AppendLineToFile(".gitignore", filename)
}
// Show shows the diff of a commit
func (c *GitCommand) Show(sha string) (string, error) {
show, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git show --color %s", sha))
if err != nil {
return "", err
}
// if this is a merge commit, we need to go a step further and get the diff between the two branches we merged
revList, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git rev-list -1 --merges %s^...%s", sha, sha))
if err != nil {
// turns out we get an error here when it's the first commit. We'll just return the original show
return show, nil
}
if len(revList) == 0 {
return show, nil
}
// we want to pull out 1a6a69a and 3b51d7c from this:
// commit ccc771d8b13d5b0d4635db4463556366470fd4f6
// Merge: 1a6a69a 3b51d7c
lines := utils.SplitLines(show)
if len(lines) < 2 {
return show, nil
}
secondLineWords := strings.Split(lines[1], " ")
if len(secondLineWords) < 3 {
return show, nil
}
mergeDiff, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git diff --color %s...%s", secondLineWords[1], secondLineWords[2]))
if err != nil {
return "", err
}
return show + mergeDiff, nil
}
// 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, plain bool) string {
cachedArg := ""
trackedArg := "--"
colorArg := "--color"
split := strings.Split(file.Name, " -> ") // in case of a renamed file we get the new filename
fileName := c.OSCommand.Quote(split[len(split)-1])
if file.HasStagedChanges && !file.HasUnstagedChanges {
cachedArg = "--cached"
}
if !file.Tracked && !file.HasStagedChanges {
trackedArg = "--no-index /dev/null"
}
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", c.OSCommand.Quote(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))
}
func (c *GitCommand) RunSkipEditorCommand(command string) error {
cmd := c.OSCommand.ExecutableFromString(command)
cmd.Env = append(
os.Environ(),
"LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY",
"EDITOR="+c.OSCommand.GetLazygitPath(),
)
return c.OSCommand.RunExecutable(cmd)
}
// GenericMerge takes a commandType of "merge" or "rebase" and a command of "abort", "skip" or "continue"
// By default we skip the editor in the case where a commit will be made
func (c *GitCommand) GenericMerge(commandType string, command string) error {
return c.RunSkipEditorCommand(
fmt.Sprintf(
"git %s --%s",
commandType,
command,
),
)
}
func (c *GitCommand) RewordCommit(commits []*Commit, index int) (*exec.Cmd, error) {
todo, err := c.GenerateGenericRebaseTodo(commits, index, "reword")
if err != nil {
return nil, err
}
return c.PrepareInteractiveRebaseCommand(commits[index+1].Sha, todo, false)
}
func (c *GitCommand) MoveCommitDown(commits []*Commit, index int) error {
// we must ensure that we have at least two commits after the selected one
if len(commits) <= index+2 {
// assuming they aren't picking the bottom commit
return errors.New(c.Tr.SLocalize("NoRoom"))
}
todo := ""
orderedCommits := append(commits[0:index], commits[index+1], commits[index])
for _, commit := range orderedCommits {
todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo
}
cmd, err := c.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todo, true)
if err != nil {
return err
}
return c.OSCommand.RunPreparedCommand(cmd)
}
func (c *GitCommand) InteractiveRebase(commits []*Commit, index int, action string) error {
todo, err := c.GenerateGenericRebaseTodo(commits, index, action)
if err != nil {
return err
}
cmd, err := c.PrepareInteractiveRebaseCommand(commits[index+1].Sha, todo, true)
if err != nil {
return err
}
return c.OSCommand.RunPreparedCommand(cmd)
}
// PrepareInteractiveRebaseCommand returns the cmd for an interactive rebase
// we tell git to run lazygit to edit the todo list, and we pass the client
// lazygit a todo string to write to the todo file
func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string, overrideEditor bool) (*exec.Cmd, error) {
ex := c.OSCommand.GetLazygitPath()
debug := "FALSE"
if c.OSCommand.Config.GetDebug() == true {
debug = "TRUE"
}
splitCmd := str.ToArgv(fmt.Sprintf("git rebase --interactive --autostash %s", baseSha))
cmd := c.OSCommand.command(splitCmd[0], splitCmd[1:]...)
gitSequenceEditor := ex
if todo == "" {
gitSequenceEditor = "true"
}
cmd.Env = os.Environ()
cmd.Env = append(
cmd.Env,
"LAZYGIT_CLIENT_COMMAND=INTERACTIVE_REBASE",
"LAZYGIT_REBASE_TODO="+todo,
"DEBUG="+debug,
"LANG=en_US.UTF-8", // Force using EN as language
"LC_ALL=en_US.UTF-8", // Force using EN as language
"GIT_SEQUENCE_EDITOR="+gitSequenceEditor,
)
if overrideEditor {
cmd.Env = append(cmd.Env, "EDITOR="+ex)
}
return cmd, nil
}
func (c *GitCommand) HardReset(baseSha string) error {
return c.OSCommand.RunCommand("git reset --hard " + baseSha)
}
func (c *GitCommand) SoftReset(baseSha string) error {
return c.OSCommand.RunCommand("git reset --soft " + baseSha)
}
func (c *GitCommand) GenerateGenericRebaseTodo(commits []*Commit, index int, action string) (string, error) {
if len(commits) <= index+1 {
// assuming they aren't picking the bottom commit
return "", errors.New(c.Tr.SLocalize("CannotRebaseOntoFirstCommit"))
}
todo := ""
for i, commit := range commits[0 : index+1] {
a := "pick"
if i == index {
a = action
}
todo = a + " " + commit.Sha + " " + commit.Name + "\n" + todo
}
return todo, nil
}
// AmendTo amends the given commit with whatever files are staged
func (c *GitCommand) AmendTo(sha string) error {
if err := c.OSCommand.RunCommand(fmt.Sprintf("git commit --fixup=%s", sha)); err != nil {
return err
}
return c.RunSkipEditorCommand(
fmt.Sprintf(
"git rebase --interactive --autostash --autosquash %s^", sha,
),
)
}
// EditRebaseTodo sets the action at a given index in the git-rebase-todo file
func (c *GitCommand) EditRebaseTodo(index int, action string) error {
fileName := ".git/rebase-merge/git-rebase-todo"
bytes, err := ioutil.ReadFile(fileName)
if err != nil {
return err
}
content := strings.Split(string(bytes), "\n")
commitCount := c.getTodoCommitCount(content)
// we have the most recent commit at the bottom whereas the todo file has
// it at the bottom, so we need to subtract our index from the commit count
contentIndex := commitCount - 1 - index
splitLine := strings.Split(content[contentIndex], " ")
content[contentIndex] = action + " " + strings.Join(splitLine[1:], " ")
result := strings.Join(content, "\n")
return ioutil.WriteFile(fileName, []byte(result), 0644)
}
func (c *GitCommand) getTodoCommitCount(content []string) int {
// count lines that are not blank and are not comments
commitCount := 0
for _, line := range content {
if line != "" && !strings.HasPrefix(line, "#") {
commitCount++
}
}
return commitCount
}
// MoveTodoDown moves a rebase todo item down by one position
func (c *GitCommand) MoveTodoDown(index int) error {
fileName := ".git/rebase-merge/git-rebase-todo"
bytes, err := ioutil.ReadFile(fileName)
if err != nil {
return err
}
content := strings.Split(string(bytes), "\n")
commitCount := c.getTodoCommitCount(content)
contentIndex := commitCount - 1 - index
rearrangedContent := append(content[0:contentIndex-1], content[contentIndex], content[contentIndex-1])
rearrangedContent = append(rearrangedContent, content[contentIndex+1:]...)
result := strings.Join(rearrangedContent, "\n")
return ioutil.WriteFile(fileName, []byte(result), 0644)
}
// Revert reverts the selected commit by sha
func (c *GitCommand) Revert(sha string) error {
return c.OSCommand.RunCommand(fmt.Sprintf("git revert %s", sha))
}
// CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD
func (c *GitCommand) CherryPickCommits(commits []*Commit) error {
todo := ""
for _, commit := range commits {
todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo
}
cmd, err := c.PrepareInteractiveRebaseCommand("HEAD", todo, false)
if err != nil {
return err
}
return c.OSCommand.RunPreparedCommand(cmd)
}

View File

@@ -0,0 +1,9 @@
package commands
// Conflict : A git conflict with a start middle and end corresponding to line
// numbers in the file where the conflict bars appear
type Conflict struct {
Start int
Middle int
End int
}

1713
pkg/commands/git_test.go Normal file

File diff suppressed because it is too large Load Diff

298
pkg/commands/os.go Normal file
View File

@@ -0,0 +1,298 @@
package commands
import (
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"github.com/go-errors/errors"
"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"
)
// Platform stores the os state
type Platform struct {
os string
shell string
shellArg string
escapedQuote string
openCommand string
openLinkCommand string
fallbackEscapedQuote string
}
// OSCommand holds all the os commands
type OSCommand struct {
Log *logrus.Entry
Platform *Platform
Config config.AppConfigurer
command func(string, ...string) *exec.Cmd
getGlobalGitConfig func(string) (string, error)
getenv func(string) string
}
// NewOSCommand os command runner
func NewOSCommand(log *logrus.Entry, config config.AppConfigurer) *OSCommand {
return &OSCommand{
Log: log,
Platform: getPlatform(),
Config: config,
command: exec.Command,
getGlobalGitConfig: gitconfig.Global,
getenv: os.Getenv,
}
}
// SetCommand sets the command function used by the struct.
// To be used for testing only
func (c *OSCommand) SetCommand(cmd func(string, ...string) *exec.Cmd) {
c.command = cmd
}
// RunCommandWithOutput wrapper around commands returning their output and error
func (c *OSCommand) RunCommandWithOutput(command string) (string, error) {
c.Log.WithField("command", command).Info("RunCommand")
cmd := c.ExecutableFromString(command)
return sanitisedCommandOutput(cmd.CombinedOutput())
}
// RunExecutableWithOutput runs an executable file and returns its output
func (c *OSCommand) RunExecutableWithOutput(cmd *exec.Cmd) (string, error) {
return sanitisedCommandOutput(cmd.CombinedOutput())
}
// RunExecutable runs an executable file and returns an error if there was one
func (c *OSCommand) RunExecutable(cmd *exec.Cmd) error {
_, err := c.RunExecutableWithOutput(cmd)
return err
}
// ExecutableFromString takes a string like `git status` and returns an executable command for it
func (c *OSCommand) ExecutableFromString(commandStr string) *exec.Cmd {
splitCmd := str.ToArgv(commandStr)
c.Log.Info(splitCmd)
return c.command(splitCmd[0], splitCmd[1:]...)
}
// 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)
return err
}
// FileType tells us if the file is a file, directory or other
func (c *OSCommand) FileType(path string) string {
fileInfo, err := os.Stat(path)
if err != nil {
return "other"
}
if fileInfo.IsDir() {
return "directory"
}
return "file"
}
// RunDirectCommand wrapper around direct commands
func (c *OSCommand) RunDirectCommand(command string) (string, error) {
c.Log.WithField("command", command).Info("RunDirectCommand")
return sanitisedCommandOutput(
c.command(c.Platform.shell, c.Platform.shellArg, command).
CombinedOutput(),
)
}
func sanitisedCommandOutput(output []byte, err error) (string, error) {
outputString := string(output)
if err != nil {
// errors like 'exit status 1' are not very useful so we'll create an error
// from the combined output
if outputString == "" {
return "", errors.Wrap(err, 0)
}
return outputString, errors.New(outputString)
}
return outputString, nil
}
// OpenFile opens a file with the given
func (c *OSCommand) OpenFile(filename string) error {
commandTemplate := c.Config.GetUserConfig().GetString("os.openCommand")
templateValues := map[string]string{
"filename": c.Quote(filename),
}
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
err := c.RunCommand(command)
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) {
editor, _ := c.getGlobalGitConfig("core.editor")
if editor == "" {
editor = c.getenv("VISUAL")
}
if editor == "" {
editor = c.getenv("EDITOR")
}
if editor == "" {
if err := c.RunCommand("which vi"); err == nil {
editor = "vi"
}
}
if editor == "" {
return nil, errors.New("No editor defined in $VISUAL, $EDITOR, or git config")
}
return c.PrepareSubProcess(editor, filename), nil
}
// PrepareSubProcess iniPrepareSubProcessrocess then tells the Gui to switch to it
func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *exec.Cmd {
return c.command(cmdName, commandArgs...)
}
// Quote wraps a message in platform-specific quotation marks
func (c *OSCommand) Quote(message string) string {
message = strings.Replace(message, "`", "\\`", -1)
escapedQuote := c.Platform.escapedQuote
if strings.Contains(message, c.Platform.escapedQuote) {
escapedQuote = c.Platform.fallbackEscapedQuote
}
return escapedQuote + message + escapedQuote
}
// Unquote removes wrapping quotations marks if they are present
// this is needed for removing quotes from staged filenames with spaces
func (c *OSCommand) Unquote(message string) string {
return strings.Replace(message, `"`, "", -1)
}
// AppendLineToFile adds a new line in file
func (c *OSCommand) AppendLineToFile(filename, line string) error {
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
return errors.Wrap(err, 0)
}
defer f.Close()
_, err = f.WriteString("\n" + line)
if err != nil {
return errors.Wrap(err, 0)
}
return nil
}
// 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 "", errors.Wrap(err, 0)
}
if _, err := tmpfile.WriteString(content); err != nil {
c.Log.Error(err)
return "", errors.Wrap(err, 0)
}
if err := tmpfile.Close(); err != nil {
c.Log.Error(err)
return "", errors.Wrap(err, 0)
}
return tmpfile.Name(), nil
}
// RemoveFile removes a file at the specified path
func (c *OSCommand) RemoveFile(filename string) error {
err := os.Remove(filename)
return errors.Wrap(err, 0)
}
// FileExists checks whether a file exists at the specified path
func (c *OSCommand) FileExists(path string) (bool, error) {
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
return true, nil
}
// RunPreparedCommand takes a pointer to an exec.Cmd and runs it
// this is useful if you need to give your command some environment variables
// before running it
func (c *OSCommand) RunPreparedCommand(cmd *exec.Cmd) error {
out, err := cmd.CombinedOutput()
outString := string(out)
c.Log.Info(outString)
if err != nil {
if len(outString) == 0 {
return err
}
return errors.New(outString)
}
return nil
}
// GetLazygitPath returns the path of the currently executed file
func (c *OSCommand) GetLazygitPath() string {
ex, err := os.Executable() // get the executable path for git to use
if err != nil {
ex = os.Args[0] // fallback to the first call argument if needed
}
return filepath.ToSlash(ex)
}

View File

@@ -0,0 +1,19 @@
// +build !windows
package commands
import (
"runtime"
)
func getPlatform() *Platform {
return &Platform{
os: runtime.GOOS,
shell: "bash",
shellArg: "-c",
escapedQuote: "'",
openCommand: "open {{filename}}",
openLinkCommand: "open {{link}}",
fallbackEscapedQuote: "\"",
}
}

377
pkg/commands/os_test.go Normal file
View File

@@ -0,0 +1,377 @@
package commands
import (
"io/ioutil"
"os"
"os/exec"
"testing"
"github.com/stretchr/testify/assert"
)
// TestOSCommandRunCommandWithOutput is a function.
func TestOSCommandRunCommandWithOutput(t *testing.T) {
type scenario struct {
command string
test func(string, error)
}
scenarios := []scenario{
{
"echo -n '123'",
func(output string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "123", output)
},
},
{
"rmdir unexisting-folder",
func(output string, err error) {
assert.Regexp(t, "rmdir.*unexisting-folder.*", err.Error())
},
},
}
for _, s := range scenarios {
s.test(NewDummyOSCommand().RunCommandWithOutput(s.command))
}
}
// TestOSCommandRunCommand is a function.
func TestOSCommandRunCommand(t *testing.T) {
type scenario struct {
command string
test func(error)
}
scenarios := []scenario{
{
"rmdir unexisting-folder",
func(err error) {
assert.Regexp(t, "rmdir.*unexisting-folder.*", err.Error())
},
},
}
for _, s := range scenarios {
s.test(NewDummyOSCommand().RunCommand(s.command))
}
}
// TestOSCommandOpenFile is a function.
func TestOSCommandOpenFile(t *testing.T) {
type scenario struct {
filename string
command func(string, ...string) *exec.Cmd
test func(error)
}
scenarios := []scenario{
{
"test",
func(name string, arg ...string) *exec.Cmd {
return exec.Command("exit", "1")
},
func(err error) {
assert.Error(t, err)
},
},
{
"test",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "open", name)
assert.Equal(t, []string{"test"}, arg)
return exec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
},
{
"filename with spaces",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "open", name)
assert.Equal(t, []string{"filename with spaces"}, arg)
return exec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
OSCmd := NewDummyOSCommand()
OSCmd.command = s.command
OSCmd.Config.GetUserConfig().Set("os.openCommand", "open {{filename}}")
s.test(OSCmd.OpenFile(s.filename))
}
}
// TestOSCommandEditFile is a function.
func TestOSCommandEditFile(t *testing.T) {
type scenario struct {
filename string
command func(string, ...string) *exec.Cmd
getenv func(string) string
getGlobalGitConfig func(string) (string, error)
test func(*exec.Cmd, error)
}
scenarios := []scenario{
{
"test",
func(name string, arg ...string) *exec.Cmd {
return exec.Command("exit", "1")
},
func(env string) string {
return ""
},
func(cf string) (string, error) {
return "", nil
},
func(cmd *exec.Cmd, err error) {
assert.EqualError(t, err, "No editor defined in $VISUAL, $EDITOR, or git config")
},
},
{
"test",
func(name string, arg ...string) *exec.Cmd {
if name == "which" {
return exec.Command("exit", "1")
}
assert.EqualValues(t, "nano", name)
return nil
},
func(env string) string {
return ""
},
func(cf string) (string, error) {
return "nano", nil
},
func(cmd *exec.Cmd, err error) {
assert.NoError(t, err)
},
},
{
"test",
func(name string, arg ...string) *exec.Cmd {
if name == "which" {
return exec.Command("exit", "1")
}
assert.EqualValues(t, "nano", name)
return nil
},
func(env string) string {
if env == "VISUAL" {
return "nano"
}
return ""
},
func(cf string) (string, error) {
return "", nil
},
func(cmd *exec.Cmd, err error) {
assert.NoError(t, err)
},
},
{
"test",
func(name string, arg ...string) *exec.Cmd {
if name == "which" {
return exec.Command("exit", "1")
}
assert.EqualValues(t, "emacs", name)
return nil
},
func(env string) string {
if env == "EDITOR" {
return "emacs"
}
return ""
},
func(cf string) (string, error) {
return "", nil
},
func(cmd *exec.Cmd, err error) {
assert.NoError(t, err)
},
},
{
"test",
func(name string, arg ...string) *exec.Cmd {
if name == "which" {
return exec.Command("echo")
}
assert.EqualValues(t, "vi", name)
return nil
},
func(env string) string {
return ""
},
func(cf string) (string, error) {
return "", nil
},
func(cmd *exec.Cmd, err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
OSCmd := NewDummyOSCommand()
OSCmd.command = s.command
OSCmd.getGlobalGitConfig = s.getGlobalGitConfig
OSCmd.getenv = s.getenv
s.test(OSCmd.EditFile(s.filename))
}
}
// TestOSCommandQuote is a function.
func TestOSCommandQuote(t *testing.T) {
osCommand := NewDummyOSCommand()
actual := osCommand.Quote("hello `test`")
expected := osCommand.Platform.escapedQuote + "hello \\`test\\`" + osCommand.Platform.escapedQuote
assert.EqualValues(t, expected, actual)
}
// TestOSCommandQuoteSingleQuote tests the quote function with ' quotes explicitly for Linux
func TestOSCommandQuoteSingleQuote(t *testing.T) {
osCommand := NewDummyOSCommand()
osCommand.Platform.os = "linux"
actual := osCommand.Quote("hello 'test'")
expected := osCommand.Platform.fallbackEscapedQuote + "hello 'test'" + osCommand.Platform.fallbackEscapedQuote
assert.EqualValues(t, expected, actual)
}
// TestOSCommandQuoteDoubleQuote tests the quote function with " quotes explicitly for Linux
func TestOSCommandQuoteDoubleQuote(t *testing.T) {
osCommand := NewDummyOSCommand()
osCommand.Platform.os = "linux"
actual := osCommand.Quote(`hello "test"`)
expected := osCommand.Platform.escapedQuote + "hello \"test\"" + osCommand.Platform.escapedQuote
assert.EqualValues(t, expected, actual)
}
// TestOSCommandUnquote is a function.
func TestOSCommandUnquote(t *testing.T) {
osCommand := NewDummyOSCommand()
actual := osCommand.Unquote(`hello "test"`)
expected := "hello test"
assert.EqualValues(t, expected, actual)
}
// TestOSCommandFileType is a function.
func TestOSCommandFileType(t *testing.T) {
type scenario struct {
path string
setup func()
test func(string)
}
scenarios := []scenario{
{
"testFile",
func() {
if _, err := os.Create("testFile"); err != nil {
panic(err)
}
},
func(output string) {
assert.EqualValues(t, "file", output)
},
},
{
"file with spaces",
func() {
if _, err := os.Create("file with spaces"); err != nil {
panic(err)
}
},
func(output string) {
assert.EqualValues(t, "file", output)
},
},
{
"testDirectory",
func() {
if err := os.Mkdir("testDirectory", 0644); err != nil {
panic(err)
}
},
func(output string) {
assert.EqualValues(t, "directory", output)
},
},
{
"nonExistant",
func() {},
func(output string) {
assert.EqualValues(t, "other", output)
},
},
}
for _, s := range scenarios {
s.setup()
s.test(NewDummyOSCommand().FileType(s.path))
_ = 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,11 @@
package commands
func getPlatform() *Platform {
return &Platform{
os: "windows",
shell: "cmd",
shellArg: "/c",
escapedQuote: `\"`,
fallbackEscapedQuote: "\\'",
}
}

View File

@@ -0,0 +1,106 @@
package commands
import (
"fmt"
"strings"
"github.com/go-errors/errors"
)
// 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

@@ -0,0 +1,13 @@
package commands
// StashEntry : A git stash entry
type StashEntry struct {
Index int
Name string
DisplayString string
}
// GetDisplayStrings returns the dispaly string of branch
func (s *StashEntry) GetDisplayStrings(isFocused bool) []string {
return []string{s.DisplayString}
}

280
pkg/config/app_config.go Normal file
View File

@@ -0,0 +1,280 @@
package config
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"github.com/shibukawa/configdir"
"github.com/spf13/viper"
yaml "gopkg.in/yaml.v2"
)
// AppConfig contains the base configuration fields required for lazygit.
type AppConfig struct {
Debug bool `long:"debug" env:"DEBUG" default:"false"`
Version string `long:"version" env:"VERSION" default:"unversioned"`
Commit string `long:"commit" env:"COMMIT"`
BuildDate string `long:"build-date" env:"BUILD_DATE"`
Name string `long:"name" env:"NAME" default:"lazygit"`
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
// from AppConfig and still be used by lazygit.
type AppConfigurer interface {
GetDebug() bool
GetVersion() string
GetCommit() string
GetBuildDate() string
GetName() string
GetBuildSource() string
GetUserConfig() *viper.Viper
GetAppState() *AppState
WriteToUserConfig(string, string) error
SaveAppState() error
LoadAppState() error
SetIsNewRepo(bool)
GetIsNewRepo() bool
}
// NewAppConfig makes a new app config
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag bool) (*AppConfig, error) {
userConfig, err := LoadConfig("config", true)
if err != nil {
return nil, err
}
if os.Getenv("DEBUG") == "TRUE" {
debuggingFlag = true
}
appConfig := &AppConfig{
Name: "lazygit",
Version: version,
Commit: commit,
BuildDate: date,
Debug: debuggingFlag,
BuildSource: buildSource,
UserConfig: userConfig,
AppState: &AppState{},
IsNewRepo: false,
}
if err := appConfig.LoadAppState(); err != nil {
return nil, err
}
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
}
// GetVersion returns debug flag
func (c *AppConfig) GetVersion() string {
return c.Version
}
// GetCommit returns debug flag
func (c *AppConfig) GetCommit() string {
return c.Commit
}
// GetBuildDate returns debug flag
func (c *AppConfig) GetBuildDate() string {
return c.BuildDate
}
// GetName returns debug flag
func (c *AppConfig) GetName() string {
return c.Name
}
// GetBuildSource returns the source of the build. For builds from goreleaser
// this will be binaryBuild
func (c *AppConfig) GetBuildSource() string {
return c.BuildSource
}
// GetUserConfig returns the user config
func (c *AppConfig) GetUserConfig() *viper.Viper {
return c.UserConfig
}
// GetAppState returns the app state
func (c *AppConfig) GetAppState() *AppState {
return c.AppState
}
func newViper(filename string) (*viper.Viper, error) {
v := viper.New()
v.SetConfigType("yaml")
v.SetConfigName(filename)
return v, nil
}
// LoadConfig gets the user's config
func LoadConfig(filename string, withDefaults bool) (*viper.Viper, error) {
v, err := newViper(filename)
if err != nil {
return nil, err
}
if withDefaults {
if err = LoadDefaults(v, GetDefaultConfig()); err != nil {
return nil, err
}
if err = LoadDefaults(v, GetPlatformDefaultConfig()); err != nil {
return nil, err
}
}
if err = LoadAndMergeFile(v, filename+".yml"); err != nil {
return nil, err
}
return v, nil
}
// LoadDefaults loads in the defaults defined in this file
func LoadDefaults(v *viper.Viper, defaults []byte) error {
return v.MergeConfig(bytes.NewBuffer(defaults))
}
func prepareConfigFile(filename string) (string, error) {
// chucking my name there is not for vanity purposes, the xdg spec (and that
// function) requires a vendor name. May as well line up with github
configDirs := configdir.New("jesseduffield", "lazygit")
folder := configDirs.QueryFolderContainsFile(filename)
if folder == nil {
// create the file as empty
folders := configDirs.QueryFolders(configdir.Global)
if err := folders[0].WriteFile(filename, []byte{}); err != nil {
return "", err
}
folder = configDirs.QueryFolderContainsFile(filename)
}
return filepath.Join(folder.Path, filename), nil
}
// LoadAndMergeFile Loads the config/state file, creating
// 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 {
return err
}
v.AddConfigPath(filepath.Dir(configPath))
return v.MergeInConfig()
}
// WriteToUserConfig adds a key/value pair to the user's config and saves it
func (c *AppConfig) WriteToUserConfig(key, value string) error {
// reloading the user config directly (without defaults) so that we're not
// writing any defaults back to the user's config
v, err := LoadConfig("config", false)
if err != nil {
return err
}
v.Set(key, value)
return v.WriteConfig()
}
// SaveAppState marhsalls the AppState struct and writes it to the disk
func (c *AppConfig) SaveAppState() error {
marshalledAppState, err := yaml.Marshal(c.AppState)
if err != nil {
return err
}
filepath, err := prepareConfigFile("state.yml")
if err != nil {
return err
}
return ioutil.WriteFile(filepath, marshalledAppState, 0644)
}
// LoadAppState loads recorded AppState from file
func (c *AppConfig) LoadAppState() error {
filepath, err := prepareConfigFile("state.yml")
if err != nil {
return err
}
appStateBytes, err := ioutil.ReadFile(filepath)
if err != nil {
return err
}
if len(appStateBytes) == 0 {
return yaml.Unmarshal(getDefaultAppState(), c.AppState)
}
return yaml.Unmarshal(appStateBytes, c.AppState)
}
// GetDefaultConfig returns the application default configuration
func GetDefaultConfig() []byte {
return []byte(
`gui:
## stuff relating to the UI
scrollHeight: 2
scrollPastBottom: true
mouseEvents: false # will default to true when the feature is complete
theme:
activeBorderColor:
- white
- bold
inactiveBorderColor:
- white
optionsTextColor:
- blue
commitLength:
show: true
git:
merging:
manualCommit: false
update:
method: prompt # can be: prompt | background | never
days: 14 # how often a update is checked for
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
confirmOnQuit: false
`)
}
// AppState stores data between runs of the app like when the last update check
// was performed and which other repos have been checked out
type AppState struct {
LastUpdateCheck int64
RecentRepos []string
}
func getDefaultAppState() []byte {
return []byte(`
lastUpdateCheck: 0
recentRepos: []
`)
}
// // commenting this out until we use it again
// func homeDirectory() string {
// usr, err := user.Current()
// if err != nil {
// log.Fatal(err)
// }
// return usr.HomeDir
// }

View File

@@ -0,0 +1,11 @@
// +build !windows,!linux
package config
// GetPlatformDefaultConfig gets the defaults for the platform
func GetPlatformDefaultConfig() []byte {
return []byte(
`os:
openCommand: 'open {{filename}}'
openLinkCommand: 'open {{link}}'`)
}

View File

@@ -0,0 +1,9 @@
package config
// GetPlatformDefaultConfig gets the defaults for the platform
func GetPlatformDefaultConfig() []byte {
return []byte(
`os:
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'
openLinkCommand: 'sh -c "xdg-open {{link}} >/dev/null"'`)
}

View File

@@ -0,0 +1,9 @@
package config
// GetPlatformDefaultConfig gets the defaults for the platform
func GetPlatformDefaultConfig() []byte {
return []byte(
`os:
openCommand: 'cmd /c "start "" {{filename}}"'
openLinkCommand: 'cmd /c "start "" {{link}}"'`)
}

View File

@@ -0,0 +1,165 @@
package git
import (
"regexp"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
"gopkg.in/src-d/go-git.v4/plumbing"
)
// 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
// if we find out we need to use one of these functions in the git.go file, we
// can just pull them out of here and put them there and then call them from in here
// BranchListBuilder returns a list of Branch objects for the current repo
type BranchListBuilder struct {
Log *logrus.Entry
GitCommand *commands.GitCommand
}
// NewBranchListBuilder builds a new branch list builder
func NewBranchListBuilder(log *logrus.Entry, gitCommand *commands.GitCommand) (*BranchListBuilder, error) {
return &BranchListBuilder{
Log: log,
GitCommand: gitCommand,
}, nil
}
func (b *BranchListBuilder) obtainCurrentBranch() *commands.Branch {
branchName, err := b.GitCommand.CurrentBranchName()
if err != nil {
panic(err.Error())
}
return &commands.Branch{Name: strings.TrimSpace(branchName)}
}
func (b *BranchListBuilder) obtainReflogBranches() []*commands.Branch {
branches := make([]*commands.Branch, 0)
rawString, err := b.GitCommand.OSCommand.RunCommandWithOutput("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD")
if err != nil {
return branches
}
branchLines := utils.SplitLines(rawString)
for _, line := range branchLines {
timeNumber, timeUnit, branchName := branchInfoFromLine(line)
timeUnit = abbreviatedTimeUnit(timeUnit)
branch := &commands.Branch{Name: branchName, Recency: timeNumber + timeUnit}
branches = append(branches, branch)
}
return uniqueByName(branches)
}
func (b *BranchListBuilder) obtainSafeBranches() []*commands.Branch {
branches := make([]*commands.Branch, 0)
bIter, err := b.GitCommand.Repo.Branches()
if err != nil {
panic(err)
}
err = bIter.ForEach(func(b *plumbing.Reference) error {
name := b.Name().Short()
branches = append(branches, &commands.Branch{Name: name})
return nil
})
return branches
}
func (b *BranchListBuilder) appendNewBranches(finalBranches, newBranches, existingBranches []*commands.Branch, included bool) []*commands.Branch {
for _, newBranch := range newBranches {
if included == branchIncluded(newBranch.Name, existingBranches) {
finalBranches = append(finalBranches, newBranch)
}
}
return finalBranches
}
func sanitisedReflogName(reflogBranch *commands.Branch, safeBranches []*commands.Branch) string {
for _, safeBranch := range safeBranches {
if strings.ToLower(safeBranch.Name) == strings.ToLower(reflogBranch.Name) {
return safeBranch.Name
}
}
return reflogBranch.Name
}
// Build the list of branches for the current repo
func (b *BranchListBuilder) Build() []*commands.Branch {
branches := make([]*commands.Branch, 0)
head := b.obtainCurrentBranch()
safeBranches := b.obtainSafeBranches()
reflogBranches := b.obtainReflogBranches()
for i, reflogBranch := range reflogBranches {
reflogBranches[i].Name = sanitisedReflogName(reflogBranch, safeBranches)
}
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
}
func branchIncluded(branchName string, branches []*commands.Branch) bool {
for _, existingBranch := range branches {
if strings.ToLower(existingBranch.Name) == strings.ToLower(branchName) {
return true
}
}
return false
}
func uniqueByName(branches []*commands.Branch) []*commands.Branch {
finalBranches := make([]*commands.Branch, 0)
for _, branch := range branches {
if branchIncluded(branch.Name, finalBranches) {
continue
}
finalBranches = append(finalBranches, branch)
}
return finalBranches
}
// A line will have the form '10 days ago master' so we need to strip out the
// useful information from that into timeNumber, timeUnit, and branchName
func branchInfoFromLine(line string) (string, string, string) {
r := regexp.MustCompile("\\|.*\\s")
line = r.ReplaceAllString(line, " ")
words := strings.Split(line, " ")
return words[0], words[1], words[len(words)-1]
}
func abbreviatedTimeUnit(timeUnit string) string {
r := regexp.MustCompile("s$")
timeUnit = r.ReplaceAllString(timeUnit, "")
timeUnitMap := map[string]string{
"hour": "h",
"minute": "m",
"second": "s",
"week": "w",
"year": "y",
"day": "d",
"month": "m",
}
return timeUnitMap[timeUnit]
}

View File

@@ -0,0 +1,288 @@
package git
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
// context:
// here we get the commits from git log but format them to show whether they're
// unpushed/pushed/merged into the base branch or not, or if they're yet to
// be processed as part of a rebase (these won't appear in git log but we
// grab them from the rebase-related files in the .git directory to show them
// if we find out we need to use one of these functions in the git.go file, we
// can just pull them out of here and put them there and then call them from in here
// CommitListBuilder returns a list of Branch objects for the current repo
type CommitListBuilder struct {
Log *logrus.Entry
GitCommand *commands.GitCommand
OSCommand *commands.OSCommand
Tr *i18n.Localizer
CherryPickedCommits []*commands.Commit
}
// NewCommitListBuilder builds a new commit list builder
func NewCommitListBuilder(log *logrus.Entry, gitCommand *commands.GitCommand, osCommand *commands.OSCommand, tr *i18n.Localizer, cherryPickedCommits []*commands.Commit) (*CommitListBuilder, error) {
return &CommitListBuilder{
Log: log,
GitCommand: gitCommand,
OSCommand: osCommand,
Tr: tr,
CherryPickedCommits: cherryPickedCommits,
}, nil
}
// GetCommits obtains the commits of the current branch
func (c *CommitListBuilder) GetCommits() ([]*commands.Commit, error) {
commits := []*commands.Commit{}
var rebasingCommits []*commands.Commit
rebaseMode, err := c.GitCommand.RebaseMode()
if err != nil {
return nil, err
}
if rebaseMode != "" {
// here we want to also prepend the commits that we're in the process of rebasing
rebasingCommits, err = c.getRebasingCommits(rebaseMode)
if err != nil {
return nil, err
}
if len(rebasingCommits) > 0 {
commits = append(commits, rebasingCommits...)
}
}
unpushedCommits := c.getUnpushedCommits()
log := c.getLog()
// now we can split it up and turn it into commits
for _, line := range utils.SplitLines(log) {
splitLine := strings.Split(line, " ")
sha := splitLine[0]
_, unpushed := unpushedCommits[sha]
status := map[bool]string{true: "unpushed", false: "pushed"}[unpushed]
commits = append(commits, &commands.Commit{
Sha: sha,
Name: strings.Join(splitLine[1:], " "),
Status: status,
DisplayString: strings.Join(splitLine, " "),
})
}
if rebaseMode != "" {
currentCommit := commits[len(rebasingCommits)]
blue := color.New(color.FgYellow)
youAreHere := blue.Sprintf("<-- %s ---", c.Tr.SLocalize("YouAreHere"))
currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name)
}
commits, err = c.setCommitMergedStatuses(commits)
if err != nil {
return nil, err
}
commits, err = c.setCommitCherryPickStatuses(commits)
if err != nil {
return nil, err
}
return commits, nil
}
// getRebasingCommits obtains the commits that we're in the process of rebasing
func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*commands.Commit, error) {
switch rebaseMode {
case "normal":
return c.getNormalRebasingCommits()
case "interactive":
return c.getInteractiveRebasingCommits()
default:
return nil, nil
}
}
func (c *CommitListBuilder) getNormalRebasingCommits() ([]*commands.Commit, error) {
rewrittenCount := 0
bytesContent, err := ioutil.ReadFile(".git/rebase-apply/rewritten")
if err == nil {
content := string(bytesContent)
rewrittenCount = len(strings.Split(content, "\n"))
}
// we know we're rebasing, so lets get all the files whose names have numbers
commits := []*commands.Commit{}
err = filepath.Walk(".git/rebase-apply", func(path string, f os.FileInfo, err error) error {
if rewrittenCount > 0 {
rewrittenCount--
return nil
}
if err != nil {
return err
}
re := regexp.MustCompile(`^\d+$`)
if !re.MatchString(f.Name()) {
return nil
}
bytesContent, err := ioutil.ReadFile(path)
if err != nil {
return err
}
content := string(bytesContent)
commit, err := c.commitFromPatch(content)
if err != nil {
return err
}
commits = append([]*commands.Commit{commit}, commits...)
return nil
})
if err != nil {
return nil, err
}
return commits, nil
}
// git-rebase-todo example:
// pick ac446ae94ee560bdb8d1d057278657b251aaef17 ac446ae
// pick afb893148791a2fbd8091aeb81deba4930c73031 afb8931
// git-rebase-todo.backup example:
// pick 49cbba374296938ea86bbd4bf4fee2f6ba5cccf6 third commit on master
// pick ac446ae94ee560bdb8d1d057278657b251aaef17 blah commit on master
// pick afb893148791a2fbd8091aeb81deba4930c73031 fourth commit on master
// getInteractiveRebasingCommits takes our git-rebase-todo and our git-rebase-todo.backup files
// and extracts out the sha and names of commits that we still have to go
// in the rebase:
func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*commands.Commit, error) {
bytesContent, err := ioutil.ReadFile(".git/rebase-merge/git-rebase-todo")
if err != nil {
c.Log.Info(fmt.Sprintf("error occured reading git-rebase-todo: %s", err.Error()))
// we assume an error means the file doesn't exist so we just return
return nil, nil
}
commits := []*commands.Commit{}
lines := strings.Split(string(bytesContent), "\n")
for _, line := range lines {
if line == "" || line == "noop" {
return commits, nil
}
splitLine := strings.Split(line, " ")
commits = append([]*commands.Commit{{
Sha: splitLine[1][0:7],
Name: strings.Join(splitLine[2:], " "),
Status: "rebasing",
Action: splitLine[0],
}}, commits...)
}
return nil, nil
}
// assuming the file starts like this:
// From e93d4193e6dd45ca9cf3a5a273d7ba6cd8b8fb20 Mon Sep 17 00:00:00 2001
// From: Lazygit Tester <test@example.com>
// Date: Wed, 5 Dec 2018 21:03:23 +1100
// Subject: second commit on master
func (c *CommitListBuilder) commitFromPatch(content string) (*commands.Commit, error) {
lines := strings.Split(content, "\n")
sha := strings.Split(lines[0], " ")[1][0:7]
name := strings.TrimPrefix(lines[3], "Subject: ")
return &commands.Commit{
Sha: sha,
Name: name,
Status: "rebasing",
}, nil
}
func (c *CommitListBuilder) setCommitMergedStatuses(commits []*commands.Commit) ([]*commands.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
}
if commit.Status != "pushed" {
continue
}
if passedAncestor {
commits[i].Status = "merged"
}
}
return commits, nil
}
func (c *CommitListBuilder) setCommitCherryPickStatuses(commits []*commands.Commit) ([]*commands.Commit, error) {
for _, commit := range commits {
for _, cherryPickedCommit := range c.CherryPickedCommits {
if commit.Sha == cherryPickedCommit.Sha {
commit.Copied = true
}
}
}
return commits, nil
}
func (c *CommitListBuilder) getMergeBase() (string, error) {
currentBranch, err := c.GitCommand.CurrentBranchName()
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
}
// getUnpushedCommits Returns the sha's of the commits that have not yet been pushed
// to the remote branch of the current branch, a map is returned to ease look up
func (c *CommitListBuilder) getUnpushedCommits() map[string]bool {
pushables := map[string]bool{}
o, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..HEAD --abbrev-commit")
if err != nil {
return pushables
}
for _, p := range utils.SplitLines(o) {
pushables[p] = true
}
return pushables
}
// getLog gets the git log (currently limited to 30 commits for performance
// until we work out lazy loading
func (c *CommitListBuilder) getLog() string {
// currently limiting to 30 for performance reasons
// TODO: add lazyloading when you scroll down
result, err := c.OSCommand.RunCommandWithOutput("git log --oneline -30")
if err != nil {
// assume if there is an error there are no commits yet for this branch
return ""
}
return result
}

View File

@@ -0,0 +1,315 @@
package git
import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/stretchr/testify/assert"
)
// NewDummyCommitListBuilder creates a new dummy CommitListBuilder for testing
func NewDummyCommitListBuilder() *CommitListBuilder {
osCommand := commands.NewDummyOSCommand()
return &CommitListBuilder{
Log: commands.NewDummyLog(),
GitCommand: commands.NewDummyGitCommandWithOSCommand(osCommand),
OSCommand: osCommand,
Tr: i18n.NewLocalizer(commands.NewDummyLog()),
CherryPickedCommits: []*commands.Commit{},
}
}
// TestCommitListBuilderGetUnpushedCommits is a function.
func TestCommitListBuilderGetUnpushedCommits(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(map[string]bool)
}
scenarios := []scenario{
{
"Can't retrieve pushable commits",
func(string, ...string) *exec.Cmd {
return exec.Command("test")
},
func(pushables map[string]bool) {
assert.EqualValues(t, map[string]bool{}, pushables)
},
},
{
"Retrieve pushable commits",
func(cmd string, args ...string) *exec.Cmd {
return exec.Command("echo", "8a2bb0e\n78976bc")
},
func(pushables map[string]bool) {
assert.Len(t, pushables, 2)
assert.EqualValues(t, map[string]bool{"8a2bb0e": true, "78976bc": true}, pushables)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.getUnpushedCommits())
})
}
}
// TestCommitListBuilderGetMergeBase is a function.
func TestCommitListBuilderGetMergeBase(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) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.getMergeBase())
})
}
}
// TestCommitListBuilderGetLog is a function.
func TestCommitListBuilderGetLog(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(string)
}
scenarios := []scenario{
{
"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)
},
},
{
"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 _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.getLog())
})
}
}
// TestCommitListBuilderGetCommits is a function.
func TestCommitListBuilderGetCommits(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func([]*commands.Commit, error)
}
scenarios := []scenario{
{
"No data found",
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")
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 []*commands.Commit, err error) {
assert.NoError(t, err)
assert.Len(t, commits, 0)
},
},
{
"GetCommits returns 2 commits, 1 unpushed, the other merged",
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)
return exec.Command("echo", "master")
}
return nil
},
func(commits []*commands.Commit, err error) {
assert.NoError(t, err)
assert.Len(t, commits, 2)
assert.EqualValues(t, []*commands.Commit{
{
Sha: "8a2bb0e",
Name: "commit 1",
Status: "unpushed",
DisplayString: "8a2bb0e commit 1",
},
{
Sha: "78976bc",
Name: "commit 2",
Status: "merged",
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 []*commands.Commit, err error) {
assert.Error(t, err)
assert.Len(t, commits, 0)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.GetCommits())
})
}
}

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

@@ -0,0 +1,157 @@
package git
import (
"regexp"
"strconv"
"strings"
"github.com/go-errors/errors"
"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,85 @@
package git
import (
"io/ioutil"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/stretchr/testify/assert"
)
// NewDummyPatchModifier constructs a new dummy patch modifier for testing
func NewDummyPatchModifier() *PatchModifier {
return &PatchModifier{
Log: commands.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,68 @@
package git
import (
"io/ioutil"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/stretchr/testify/assert"
)
// NewDummyPatchParser constructs a new dummy patch parser for testing
func NewDummyPatchParser() *PatchParser {
return &PatchParser{
Log: commands.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

@@ -0,0 +1,70 @@
package gui
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type appStatus struct {
name string
statusType string
duration int
}
type statusManager struct {
statuses []appStatus
}
func (m *statusManager) removeStatus(name string) {
newStatuses := []appStatus{}
for _, status := range m.statuses {
if status.name != name {
newStatuses = append(newStatuses, status)
}
}
m.statuses = newStatuses
}
func (m *statusManager) addWaitingStatus(name string) {
m.removeStatus(name)
newStatus := appStatus{
name: name,
statusType: "waiting",
duration: 0,
}
m.statuses = append([]appStatus{newStatus}, m.statuses...)
}
func (m *statusManager) getStatusString() string {
if len(m.statuses) == 0 {
return ""
}
topStatus := m.statuses[0]
if topStatus.statusType == "waiting" {
return topStatus.name + " " + utils.Loader()
}
return topStatus.name
}
// WithWaitingStatus wraps a function and shows a waiting status while the function is still executing
func (gui *Gui) WithWaitingStatus(name string, f func() error) error {
go func() {
gui.g.Update(func(g *gocui.Gui) error {
gui.statusManager.addWaitingStatus(name)
return nil
})
defer gui.g.Update(func(g *gocui.Gui) error {
gui.statusManager.removeStatus(name)
return nil
})
if err := f(); err != nil {
gui.g.Update(func(g *gocui.Gui) error {
return gui.createErrorPanel(gui.g, err.Error())
})
}
}()
return nil
}

317
pkg/gui/branches_panel.go Normal file
View File

@@ -0,0 +1,317 @@
package gui
import (
"fmt"
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/git"
)
// 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 {
if gui.popupPanelFocused() {
return nil
}
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
// 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.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.RenderSelectedBranchUpstreamDifferences(); err != nil {
return err
}
return gui.refreshStatus(g)
})
return nil
}
func (gui *Gui) handleBranchesNextLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
panelState := gui.State.Panels.Branches
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Branches), false)
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
return gui.handleBranchSelect(gui.g, v)
}
func (gui *Gui) handleBranchesPrevLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
panelState := gui.State.Panels.Branches
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Branches), true)
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
return gui.handleBranchSelect(gui.g, v)
}
// specific functions
func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error {
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()
if err := gui.GitCommand.Checkout(branch.Name, false); err != nil {
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.createLoaderPanel(gui.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()
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 {
if err := gui.GitCommand.Checkout(branch.Name, true); err != nil {
gui.createErrorPanel(g, err.Error())
}
return gui.refreshSidePanels(g)
}, nil)
}
func (gui *Gui) handleCheckoutByName(g *gocui.Gui, v *gocui.View) error {
gui.createPromptPanel(g, v, gui.Tr.SLocalize("BranchName")+":", func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.Checkout(gui.trimmedContent(v), false); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return gui.refreshSidePanels(g)
})
return nil
}
func (gui *Gui) handleNewBranch(g *gocui.Gui, v *gocui.View) error {
branch := gui.State.Branches[0]
message := gui.Tr.TemplateLocalize(
"NewBranchNameBranchOff",
Teml{
"branchName": branch.Name,
},
)
gui.createPromptPanel(g, v, message, func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.NewBranch(gui.trimmedContent(v)); err != nil {
return gui.createErrorPanel(g, err.Error())
}
gui.refreshSidePanels(g)
return gui.handleBranchSelect(g, v)
})
return nil
}
func (gui *Gui) handleDeleteBranch(g *gocui.Gui, v *gocui.View) error {
return gui.deleteBranch(g, v, false)
}
func (gui *Gui) handleForceDeleteBranch(g *gocui.Gui, v *gocui.View) error {
return gui.deleteBranch(g, v, true)
}
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]
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
if force {
messageID = "ForceDeleteBranchMessage"
} else {
messageID = "DeleteBranchMessage"
}
message := gui.Tr.TemplateLocalize(
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 {
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)
}
func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error {
checkedOutBranch := gui.State.Branches[0].Name
selectedBranch := gui.getSelectedBranch().Name
if checkedOutBranch == selectedBranch {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantMergeBranchIntoItself"))
}
prompt := gui.Tr.TemplateLocalize(
"ConfirmMerge",
Teml{
"checkedOutBranch": checkedOutBranch,
"selectedBranch": selectedBranch,
},
)
return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("MergingTitle"), prompt,
func(g *gocui.Gui, v *gocui.View) error {
err := gui.GitCommand.Merge(selectedBranch)
return gui.handleGenericMergeCommandResult(err)
}, nil)
}
func (gui *Gui) handleRebase(g *gocui.Gui, v *gocui.View) error {
checkedOutBranch := gui.State.Branches[0].Name
selectedBranch := gui.getSelectedBranch().Name
if selectedBranch == checkedOutBranch {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantRebaseOntoSelf"))
}
prompt := gui.Tr.TemplateLocalize(
"ConfirmRebase",
Teml{
"checkedOutBranch": checkedOutBranch,
"selectedBranch": selectedBranch,
},
)
return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("RebasingTitle"), prompt,
func(g *gocui.Gui, v *gocui.View) error {
err := gui.GitCommand.RebaseBranch(selectedBranch)
return gui.handleGenericMergeCommandResult(err)
}, nil)
}
func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
branch := gui.getSelectedBranch()
if branch == nil {
return nil
}
if branch.Pushables == "" {
return nil
}
if branch.Pushables == "?" {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("FwdNoUpstream"))
}
if branch.Pushables != "0" {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("FwdCommitsToPush"))
}
upstream := "origin" // hardcoding for now
message := gui.Tr.TemplateLocalize(
"Fetching",
Teml{
"from": fmt.Sprintf("%s/%s", upstream, branch.Name),
"to": branch.Name,
},
)
go func() {
_ = gui.createLoaderPanel(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()
}
}()
return nil
}

View File

@@ -0,0 +1,102 @@
package gui
import (
"os/exec"
"strconv"
"strings"
"github.com/jesseduffield/gocui"
)
// runSyncOrAsyncCommand takes the output of a command that may have returned
// either no error, an error, or a subprocess to execute, and if a subprocess
// needs to be set on the gui object, it does so, and then returns the error
func (gui *Gui) runSyncOrAsyncCommand(sub *exec.Cmd, err error) error {
if err != nil {
if err != gui.Errors.ErrSubProcess {
return gui.createErrorPanel(gui.g, err.Error())
}
}
if sub != nil {
gui.SubProcess = sub
return gui.Errors.ErrSubProcess
}
return nil
}
func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
message := gui.trimmedContent(v)
if message == "" {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CommitWithoutMessageErr"))
}
if err := gui.runSyncOrAsyncCommand(gui.GitCommand.Commit(message)); err != nil {
return err
}
v.Clear()
_ = v.SetCursor(0, 0)
_ = v.SetOrigin(0, 0)
_, _ = g.SetViewOnBottom("commitMessage")
_ = gui.switchFocus(g, v, gui.getFilesView())
return gui.refreshSidePanels(g)
}
func (gui *Gui) handleCommitClose(g *gocui.Gui, v *gocui.View) error {
g.SetViewOnBottom("commitMessage")
return gui.switchFocus(g, v, gui.getFilesView())
}
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{
"keyBindClose": "esc",
"keyBindConfirm": "enter",
},
)
return gui.renderString(g, "options", message)
}
func (gui *Gui) simpleEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
switch {
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
v.EditDelete(true)
case key == gocui.KeyDelete:
v.EditDelete(false)
case key == gocui.KeyArrowDown:
v.MoveCursor(0, 1, false)
case key == gocui.KeyArrowUp:
v.MoveCursor(0, -1, false)
case key == gocui.KeyArrowLeft:
v.MoveCursor(-1, 0, false)
case key == gocui.KeyArrowRight:
v.MoveCursor(1, 0, false)
case key == gocui.KeyTab:
v.EditNewLine()
case key == gocui.KeySpace:
v.EditWrite(' ')
case key == gocui.KeyInsert:
v.Overwrite = !v.Overwrite
default:
v.EditWrite(ch)
}
gui.RenderCommitLength()
}
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
}
v := gui.getCommitMessageView()
v.Subtitle = gui.getBufferLength(v)
}

439
pkg/gui/commits_panel.go Normal file
View File

@@ -0,0 +1,439 @@
package gui
import (
"fmt"
"strconv"
"github.com/go-errors/errors"
"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) 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 {
if gui.popupPanelFocused() {
return nil
}
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
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 {
builder, err := git.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr, gui.State.CherryPickedCommits)
if err != nil {
return err
}
commits, err := builder.GetCommits()
if err != nil {
return err
}
gui.State.Commits = commits
gui.refreshSelectedLine(&gui.State.Panels.Commits.SelectedLine, len(gui.State.Commits))
isFocused := gui.g.CurrentView().Name() == "commits"
list, err := utils.RenderList(gui.State.Commits, isFocused)
if err != nil {
return err
}
v := gui.getCommitsView()
v.Clear()
fmt.Fprint(v, list)
gui.refreshStatus(g)
if v == g.CurrentView() {
gui.handleCommitSelect(g, v)
}
return nil
})
return nil
}
func (gui *Gui) handleCommitsNextLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
panelState := gui.State.Panels.Commits
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Commits), false)
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
return gui.handleCommitSelect(gui.g, v)
}
func (gui *Gui) handleCommitsPrevLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
panelState := gui.State.Panels.Commits
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Commits), true)
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
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 := 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())
}
if err := gui.refreshCommits(g); err != nil {
panic(err)
}
if err := gui.refreshFiles(); err != nil {
panic(err)
}
gui.resetOrigin(commitView)
gui.State.Panels.Commits.SelectedLine = 0
return gui.handleCommitSelect(g, commitView)
}, nil)
}
func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
if len(gui.State.Commits) <= 1 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash"))
}
applied, err := gui.handleMidRebaseCommand("squash")
if err != nil {
return err
}
if applied {
return nil
}
gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("Squash"), gui.Tr.SLocalize("SureSquashThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("SquashingStatus"), func() error {
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "squash")
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
return nil
}
// TODO: move to files panel
func (gui *Gui) anyUnStagedChanges(files []*commands.File) bool {
for _, file := range files {
if file.Tracked && file.HasUnstagedChanges {
return true
}
}
return false
}
func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
if len(gui.State.Commits) <= 1 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash"))
}
applied, err := gui.handleMidRebaseCommand("fixup")
if err != nil {
return err
}
if applied {
return nil
}
gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("Fixup"), gui.Tr.SLocalize("SureFixupThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("FixingStatus"), func() error {
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "fixup")
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
return nil
}
func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
applied, err := gui.handleMidRebaseCommand("reword")
if err != nil {
return err
}
if applied {
return nil
}
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 {
if err := gui.GitCommand.RenameCommit(v.Buffer()); err != nil {
return gui.createErrorPanel(g, err.Error())
}
if err := gui.refreshCommits(g); err != nil {
panic(err)
}
return gui.handleCommitSelect(g, v)
})
}
func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
applied, err := gui.handleMidRebaseCommand("reword")
if err != nil {
return err
}
if applied {
return nil
}
subProcess, err := gui.GitCommand.RewordCommit(gui.State.Commits, gui.State.Panels.Commits.SelectedLine)
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
if subProcess != nil {
gui.SubProcess = subProcess
return gui.Errors.ErrSubProcess
}
return nil
}
// handleMidRebaseCommand sees if the selected commit is in fact a rebasing
// commit meaning you are trying to edit the todo file rather than actually
// begin a rebase. It then updates the todo file with that action
func (gui *Gui) handleMidRebaseCommand(action string) (bool, error) {
selectedCommit := gui.State.Commits[gui.State.Panels.Commits.SelectedLine]
if selectedCommit.Status != "rebasing" {
return false, nil
}
// for now we do not support setting 'reword' because it requires an editor
// and that means we either unconditionally wait around for the subprocess to ask for
// our input or we set a lazygit client as the EDITOR env variable and have it
// request us to edit the commit message when prompted.
if action == "reword" {
return true, gui.createErrorPanel(gui.g, gui.Tr.SLocalize("rewordNotSupported"))
}
if err := gui.GitCommand.EditRebaseTodo(gui.State.Panels.Commits.SelectedLine, action); err != nil {
return false, gui.createErrorPanel(gui.g, err.Error())
}
return true, gui.refreshCommits(gui.g)
}
// handleMoveTodoDown like handleMidRebaseCommand but for moving an item up in the todo list
func (gui *Gui) handleMoveTodoDown(index int) (bool, error) {
selectedCommit := gui.State.Commits[index]
if selectedCommit.Status != "rebasing" {
return false, nil
}
if gui.State.Commits[index+1].Status != "rebasing" {
return true, nil
}
if err := gui.GitCommand.MoveTodoDown(index); err != nil {
return true, gui.createErrorPanel(gui.g, err.Error())
}
return true, gui.refreshCommits(gui.g)
}
func (gui *Gui) handleCommitDelete(g *gocui.Gui, v *gocui.View) error {
applied, err := gui.handleMidRebaseCommand("drop")
if err != nil {
return err
}
if applied {
return nil
}
return gui.createConfirmationPanel(gui.g, v, gui.Tr.SLocalize("DeleteCommitTitle"), gui.Tr.SLocalize("DeleteCommitPrompt"), func(*gocui.Gui, *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("DeletingStatus"), func() error {
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "drop")
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
}
func (gui *Gui) handleCommitMoveDown(g *gocui.Gui, v *gocui.View) error {
index := gui.State.Panels.Commits.SelectedLine
selectedCommit := gui.State.Commits[index]
if selectedCommit.Status == "rebasing" {
if gui.State.Commits[index+1].Status != "rebasing" {
return nil
}
if err := gui.GitCommand.MoveTodoDown(index); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
gui.State.Panels.Commits.SelectedLine++
return gui.refreshCommits(gui.g)
}
return gui.WithWaitingStatus(gui.Tr.SLocalize("MovingStatus"), func() error {
err := gui.GitCommand.MoveCommitDown(gui.State.Commits, index)
if err == nil {
gui.State.Panels.Commits.SelectedLine++
}
return gui.handleGenericMergeCommandResult(err)
})
}
func (gui *Gui) handleCommitMoveUp(g *gocui.Gui, v *gocui.View) error {
index := gui.State.Panels.Commits.SelectedLine
if index == 0 {
return nil
}
selectedCommit := gui.State.Commits[index]
if selectedCommit.Status == "rebasing" {
if err := gui.GitCommand.MoveTodoDown(index - 1); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
gui.State.Panels.Commits.SelectedLine--
return gui.refreshCommits(gui.g)
}
return gui.WithWaitingStatus(gui.Tr.SLocalize("MovingStatus"), func() error {
err := gui.GitCommand.MoveCommitDown(gui.State.Commits, index-1)
if err == nil {
gui.State.Panels.Commits.SelectedLine--
}
return gui.handleGenericMergeCommandResult(err)
})
}
func (gui *Gui) handleCommitEdit(g *gocui.Gui, v *gocui.View) error {
applied, err := gui.handleMidRebaseCommand("edit")
if err != nil {
return err
}
if applied {
return nil
}
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
err = gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "edit")
return gui.handleGenericMergeCommandResult(err)
})
}
func (gui *Gui) handleCommitAmendTo(g *gocui.Gui, v *gocui.View) error {
return gui.createConfirmationPanel(gui.g, v, gui.Tr.SLocalize("AmendCommitTitle"), gui.Tr.SLocalize("AmendCommitPrompt"), func(*gocui.Gui, *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("AmendingStatus"), func() error {
err := gui.GitCommand.AmendTo(gui.State.Commits[gui.State.Panels.Commits.SelectedLine].Sha)
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
}
func (gui *Gui) handleCommitPick(g *gocui.Gui, v *gocui.View) error {
applied, err := gui.handleMidRebaseCommand("pick")
if err != nil {
return err
}
if applied {
return nil
}
// at this point we aren't actually rebasing so we will interpret this as an
// attempt to pull. We might revoke this later after enabling configurable keybindings
return gui.pullFiles(g, v)
}
func (gui *Gui) handleCommitRevert(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.Revert(gui.State.Commits[gui.State.Panels.Commits.SelectedLine].Sha); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
gui.State.Panels.Commits.SelectedLine++
return gui.refreshCommits(gui.g)
}
func (gui *Gui) handleCopyCommit(g *gocui.Gui, v *gocui.View) error {
// get currently selected commit, add the sha to state.
commit := gui.State.Commits[gui.State.Panels.Commits.SelectedLine]
// we will un-copy it if it's already copied
for index, cherryPickedCommit := range gui.State.CherryPickedCommits {
if commit.Sha == cherryPickedCommit.Sha {
gui.State.CherryPickedCommits = append(gui.State.CherryPickedCommits[0:index], gui.State.CherryPickedCommits[index+1:]...)
return gui.refreshCommits(gui.g)
}
}
gui.addCommitToCherryPickedCommits(gui.State.Panels.Commits.SelectedLine)
return gui.refreshCommits(gui.g)
}
func (gui *Gui) addCommitToCherryPickedCommits(index int) {
// not super happy with modifying the state of the Commits array here
// but the alternative would be very tricky
gui.State.Commits[index].Copied = true
newCommits := []*commands.Commit{}
for _, commit := range gui.State.Commits {
if commit.Copied {
// duplicating just the things we need to put in the rebase TODO list
newCommits = append(newCommits, &commands.Commit{Name: commit.Name, Sha: commit.Sha})
}
}
gui.State.CherryPickedCommits = newCommits
}
func (gui *Gui) handleCopyCommitRange(g *gocui.Gui, v *gocui.View) error {
// whenever I add a commit, I need to make sure I retain its order
// find the last commit that is copied that's above our position
// if there are none, startIndex = 0
startIndex := 0
for index, commit := range gui.State.Commits[0:gui.State.Panels.Commits.SelectedLine] {
if commit.Copied {
startIndex = index
}
}
gui.Log.Info("commit copy start index: " + strconv.Itoa(startIndex))
for index := startIndex; index <= gui.State.Panels.Commits.SelectedLine; index++ {
gui.addCommitToCherryPickedCommits(index)
}
return gui.refreshCommits(gui.g)
}
// HandlePasteCommits begins a cherry-pick rebase with the commits the user has copied
func (gui *Gui) HandlePasteCommits(g *gocui.Gui, v *gocui.View) error {
return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("CherryPick"), gui.Tr.SLocalize("SureCherryPick"), func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("CherryPickingStatus"), func() error {
err := gui.GitCommand.CherryPickCommits(gui.State.CherryPickedCommits)
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
}

View File

@@ -0,0 +1,182 @@
// lots of this has been directly ported from one of the example files, will brush up later
// Copyright 2014 The gocui Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gui
import (
"strings"
"time"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
)
func (gui *Gui) wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.View) error) func(*gocui.Gui, *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
if function != nil {
if err := function(g, v); err != nil {
return err
}
}
return gui.closeConfirmationPrompt(g)
}
}
func (gui *Gui) closeConfirmationPrompt(g *gocui.Gui) error {
view, err := g.View("confirmation")
if err != nil {
return nil // if it's already been closed we can just return
}
if err := gui.returnFocus(g, view); err != nil {
panic(err)
}
g.DeleteKeybindings("confirmation")
return g.DeleteView("confirmation")
}
func (gui *Gui) getMessageHeight(wrap bool, message string, width int) int {
lines := strings.Split(message, "\n")
lineCount := 0
// 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, wrap bool, prompt string) (int, int, int, int) {
width, height := g.Size()
panelWidth := width / 2
panelHeight := gui.getMessageHeight(wrap, prompt, panelWidth)
return width/2 - panelWidth/2,
height/2 - panelHeight/2 - panelHeight%2 - 1,
width/2 + panelWidth/2,
height/2 + panelHeight/2
}
func (gui *Gui) createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, handleConfirm func(*gocui.Gui, *gocui.View) error) error {
gui.onNewPopupPanel()
confirmationView, err := gui.prepareConfirmationPanel(currentView, title, "", false)
if err != nil {
return err
}
confirmationView.Editable = true
return gui.setKeyBindings(g, handleConfirm, nil)
}
func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt string, hasLoader bool) (*gocui.View, error) {
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, true, prompt)
confirmationView, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0)
if err != nil {
if err.Error() != "unknown view" {
return nil, err
}
confirmationView.HasLoader = hasLoader
confirmationView.Title = title
confirmationView.Wrap = true
confirmationView.FgColor = gocui.ColorWhite
}
gui.g.Update(func(g *gocui.Gui) error {
return gui.switchFocus(gui.g, currentView, confirmationView)
})
return confirmationView, nil
}
func (gui *Gui) onNewPopupPanel() {
viewNames := []string{"commitMessage",
"credentials",
"menu"}
for _, viewName := range viewNames {
_, _ = gui.g.SetViewOnBottom(viewName)
}
}
func (gui *Gui) createLoaderPanel(g *gocui.Gui, currentView *gocui.View, prompt string) error {
return gui.createPopupPanel(g, currentView, "", prompt, true, nil, nil)
}
// it is very important that within this function we never include the original prompt in any error messages, because it may contain e.g. a user password
func (gui *Gui) createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
return gui.createPopupPanel(g, currentView, title, prompt, false, handleConfirm, handleClose)
}
func (gui *Gui) createPopupPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, hasLoader bool, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
gui.onNewPopupPanel()
g.Update(func(g *gocui.Gui) error {
// delete the existing confirmation panel if it exists
if view, _ := g.View("confirmation"); view != nil {
if err := gui.closeConfirmationPrompt(g); err != nil {
errMessage := gui.Tr.TemplateLocalize(
"CantCloseConfirmationPrompt",
Teml{
"error": err.Error(),
},
)
gui.Log.Error(errMessage)
}
}
confirmationView, err := gui.prepareConfirmationPanel(currentView, title, prompt, hasLoader)
if err != nil {
return err
}
confirmationView.Editable = false
if err := gui.renderString(g, "confirmation", prompt); err != nil {
return err
}
return gui.setKeyBindings(g, handleConfirm, handleClose)
})
return nil
}
func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
actions := gui.Tr.TemplateLocalize(
"CloseConfirm",
Teml{
"keyBindClose": "esc",
"keyBindConfirm": "enter",
},
)
if err := gui.renderString(g, "options", actions); err != nil {
return err
}
if err := g.SetKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone, gui.wrappedConfirmationFunction(handleConfirm)); err != nil {
return err
}
return g.SetKeybinding("confirmation", gocui.KeyEsc, gocui.ModNone, gui.wrappedConfirmationFunction(handleClose))
}
func (gui *Gui) createMessagePanel(g *gocui.Gui, currentView *gocui.View, title, prompt string) error {
return gui.createPopupPanel(g, currentView, title, prompt, false, nil, nil)
}
// 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(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)
}

75
pkg/gui/context.go Normal file
View File

@@ -0,0 +1,75 @@
package gui
func (gui *Gui) titleMap() map[string]string {
return map[string]string{
"commits": gui.Tr.SLocalize("DiffTitle"),
"branches": gui.Tr.SLocalize("LogTitle"),
"files": gui.Tr.SLocalize("DiffTitle"),
"status": "",
"stash": gui.Tr.SLocalize("DiffTitle"),
}
}
func (gui *Gui) contextTitleMap() map[string]map[string]string {
return map[string]map[string]string{
"main": {
"staging": gui.Tr.SLocalize("StagingMainTitle"),
"merging": gui.Tr.SLocalize("MergingMainTitle"),
"normal": "",
},
}
}
func (gui *Gui) setMainTitle() error {
currentViewName := gui.g.CurrentView().Name()
var newTitle string
if context, ok := gui.State.Contexts[currentViewName]; ok {
newTitle = gui.contextTitleMap()[currentViewName][context]
} else if title, ok := gui.titleMap()[currentViewName]; ok {
newTitle = title
} else {
return nil
}
gui.getMainView().Title = newTitle
return nil
}
func (gui *Gui) changeContext(viewName, context string) error {
if gui.State.Contexts[viewName] == context {
return nil
}
contextMap := gui.GetContextMap()
gui.g.DeleteKeybindings(viewName)
bindings := contextMap[viewName][context]
for _, binding := range bindings {
if err := gui.g.SetKeybinding(viewName, binding.Key, binding.Modifier, binding.Handler); err != nil {
return err
}
}
gui.State.Contexts[viewName] = context
return gui.setMainTitle()
}
func (gui *Gui) setInitialContexts() error {
contextMap := gui.GetContextMap()
initialContexts := map[string]string{
"main": "normal",
}
for viewName, context := range initialContexts {
bindings := contextMap[viewName][context]
for _, binding := range bindings {
if err := gui.g.SetKeybinding(binding.ViewName, binding.Key, binding.Modifier, binding.Handler); err != nil {
return err
}
}
}
gui.State.Contexts = initialContexts
return nil
}

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()
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()
}
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())
}
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(), false)
} else {
_ = gui.closeConfirmationPrompt(g)
_ = gui.refreshSidePanels(g)
}
}

518
pkg/gui/files_panel.go Normal file
View File

@@ -0,0 +1,518 @@
package gui
import (
// "io"
// "io/ioutil"
// "strings"
"fmt"
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"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) handleFilesFocus(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
cx, cy := v.Cursor()
_, oy := v.Origin()
prevSelectedLine := gui.State.Panels.Files.SelectedLine
newSelectedLine := cy - oy
if newSelectedLine > len(gui.State.Files)-1 || len(utils.Decolorise(gui.State.Files[newSelectedLine].DisplayString)) < cx {
return gui.handleFileSelect(gui.g, v, false)
}
gui.State.Panels.Files.SelectedLine = newSelectedLine
if prevSelectedLine == newSelectedLine && gui.currentViewName() == v.Name() {
return gui.handleFilePress(gui.g, v)
} else {
return gui.handleFileSelect(gui.g, v, true)
}
}
func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View, alreadySelected bool) error {
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
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 err := gui.focusPoint(0, gui.State.Panels.Files.SelectedLine, v); err != nil {
return err
}
if file.HasInlineMergeConflicts {
return gui.refreshMergePanel()
}
content := gui.GitCommand.Diff(file, false)
if alreadySelected {
g.Update(func(*gocui.Gui) error {
return gui.setViewContent(gui.g, gui.getMainView(), content)
})
return nil
}
return gui.renderString(g, "main", content)
}
func (gui *Gui) refreshFiles() error {
selectedFile, _ := gui.getSelectedFile(gui.g)
filesView := gui.getFilesView()
if err := gui.refreshStateFiles(); err != nil {
return err
}
gui.g.Update(func(g *gocui.Gui) error {
filesView.Clear()
isFocused := gui.g.CurrentView().Name() == "files"
list, err := utils.RenderList(gui.State.Files, isFocused)
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 {
if gui.popupPanelFocused() {
return nil
}
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 {
if gui.popupPanelFocused() {
return nil
}
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)
for _, file := range files {
if file.HasStagedChanges {
result = append(result, file)
}
}
return result
}
func (gui *Gui) trackedFiles() []*commands.File {
files := gui.State.Files
result := make([]*commands.File, 0)
for _, file := range files {
if file.Tracked {
result = append(result, file)
}
}
return result
}
func (gui *Gui) stageSelectedFile(g *gocui.Gui) error {
file, err := gui.getSelectedFile(g)
if err != nil {
return err
}
return gui.GitCommand.StageFile(file.Name)
}
func (gui *Gui) handleEnterFile(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
}
return nil
}
if file.HasInlineMergeConflicts {
return gui.handleSwitchToMerge(g, v)
}
if !file.HasUnstagedChanges || file.HasMergeConflicts {
return gui.createErrorPanel(g, gui.Tr.SLocalize("FileStagingRequirements"))
}
if err := gui.changeContext("main", "staging"); err != nil {
return err
}
if err := gui.switchFocus(g, v, gui.getMainView()); 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 {
if err == gui.Errors.ErrNoFiles {
return nil
}
return err
}
if file.HasInlineMergeConflicts {
return gui.handleSwitchToMerge(g, v)
}
if file.HasUnstagedChanges {
gui.GitCommand.StageFile(file.Name)
} else {
gui.GitCommand.UnStageFile(file.Name, file.Tracked)
}
if err := gui.refreshFiles(); err != nil {
return err
}
return gui.handleFileSelect(g, v, true)
}
func (gui *Gui) allFilesStaged() bool {
for _, file := range gui.State.Files {
if file.HasUnstagedChanges {
return false
}
}
return true
}
func (gui *Gui) handleStageAll(g *gocui.Gui, v *gocui.View) error {
var err error
if gui.allFilesStaged() {
err = gui.GitCommand.UnstageAll()
} else {
err = gui.GitCommand.StageAll()
}
if err != nil {
_ = gui.createErrorPanel(g, err.Error())
}
if err := gui.refreshFiles(); err != nil {
return err
}
return gui.handleFileSelect(g, v, false)
}
func (gui *Gui) handleAddPatch(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
if err == gui.Errors.ErrNoFiles {
return nil
}
return err
}
if !file.HasUnstagedChanges {
return gui.createErrorPanel(g, gui.Tr.SLocalize("FileHasNoUnstagedChanges"))
}
if !file.Tracked {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CannotGitAdd"))
}
gui.SubProcess = gui.GitCommand.AddPatch(file.Name)
return gui.Errors.ErrSubProcess
}
func (gui *Gui) handleFileRemove(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
if err == gui.Errors.ErrNoFiles {
return nil
}
return err
}
var deleteVerb string
if file.Tracked {
deleteVerb = gui.Tr.SLocalize("checkout")
} else {
deleteVerb = gui.Tr.SLocalize("delete")
}
message := gui.Tr.TemplateLocalize(
"SureTo",
Teml{
"deleteVerb": deleteVerb,
"fileName": file.Name,
},
)
return gui.createConfirmationPanel(g, v, strings.Title(deleteVerb)+" file", message, func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.RemoveFile(file); err != nil {
return err
}
return gui.refreshFiles()
}, nil)
}
func (gui *Gui) handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
return gui.createErrorPanel(g, err.Error())
}
if file.Tracked {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantIgnoreTrackFiles"))
}
if err := gui.GitCommand.Ignore(file.Name); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return gui.refreshFiles()
}
func (gui *Gui) handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
if len(gui.stagedFiles()) == 0 && gui.State.WorkingTreeState == "normal" {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit"))
}
commitMessageView := gui.getCommitMessageView()
g.Update(func(g *gocui.Gui) error {
g.SetViewOnTop("commitMessage")
gui.switchFocus(g, filesView, commitMessageView)
gui.RenderCommitLength()
return nil
})
return nil
}
func (gui *Gui) handleAmendCommitPress(g *gocui.Gui, filesView *gocui.View) error {
if len(gui.stagedFiles()) == 0 && gui.State.WorkingTreeState == "normal" {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit"))
}
if len(gui.State.Commits) == 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoCommitToAmend"))
}
title := strings.Title(gui.Tr.SLocalize("AmendLastCommit"))
question := gui.Tr.SLocalize("SureToAmend")
return gui.createConfirmationPanel(g, filesView, title, question, func(g *gocui.Gui, v *gocui.View) error {
if err := gui.runSyncOrAsyncCommand(gui.GitCommand.AmendHead()); err != nil {
return err
}
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 {
if len(gui.stagedFiles()) == 0 && gui.State.WorkingTreeState == "normal" {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit"))
}
gui.PrepareSubProcess(g, "git", "commit")
return nil
}
// PrepareSubProcess - prepare a subprocess for execution and tell the gui to switch to it
func (gui *Gui) PrepareSubProcess(g *gocui.Gui, commands ...string) {
gui.SubProcess = gui.GitCommand.PrepareCommitSubProcess()
g.Update(func(g *gocui.Gui) error {
return gui.Errors.ErrSubProcess
})
}
func (gui *Gui) editFile(filename string) error {
return gui.runSyncOrAsyncCommand(gui.OSCommand.EditFile(filename))
}
func (gui *Gui) handleFileEdit(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
return err
}
return gui.editFile(file.Name)
}
func (gui *Gui) handleFileOpen(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
return err
}
return gui.openFile(file.Name)
}
func (gui *Gui) handleRefreshFiles(g *gocui.Gui, v *gocui.View) error {
return gui.refreshFiles()
}
func (gui *Gui) refreshStateFiles() error {
// 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))
return gui.updateWorkTreeState()
}
func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) {
item, err := gui.getSelectedFile(g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
return "", err
}
return "", gui.renderString(g, "main", gui.Tr.SLocalize("NoFilesDisplay"))
}
if item.Type != "file" {
return "", gui.renderString(g, "main", gui.Tr.SLocalize("NotAFile"))
}
cat, err := gui.GitCommand.CatFile(item.Name)
if err != nil {
gui.Log.Error(err)
return "", gui.renderString(g, "main", err.Error())
}
return cat, nil
}
func (gui *Gui) pullFiles(g *gocui.Gui, v *gocui.View) error {
if err := gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("PullWait")); err != nil {
return err
}
go func() {
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(g *gocui.Gui, v *gocui.View, force bool) error {
if err := gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("PushWait")); err != nil {
return err
}
go func() {
unamePassOpend := false
branchName := gui.State.Branches[0].Name
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.GetCurrentBranchUpstreamDifferenceCount()
if pullables == "?" || pullables == "0" {
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(g, v, true)
}, nil)
return err
}
func (gui *Gui) handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
}
return nil
}
if !file.HasInlineMergeConflicts {
return gui.createErrorPanel(g, gui.Tr.SLocalize("FileNoMergeCons"))
}
if err := gui.changeContext("main", "merging"); err != nil {
return err
}
if err := gui.switchFocus(g, v, gui.getMainView()); err != nil {
return err
}
return gui.refreshMergePanel()
}
func (gui *Gui) handleAbortMerge(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.AbortMerge(); err != nil {
return gui.createErrorPanel(g, err.Error())
}
gui.createMessagePanel(g, v, "", gui.Tr.SLocalize("MergeAborted"))
gui.refreshStatus(g)
return gui.refreshFiles()
}
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.ResetAndClean(); err != nil {
gui.createErrorPanel(g, err.Error())
}
return gui.refreshFiles()
}, nil)
}
func (gui *Gui) openFile(filename string) error {
if err := gui.OSCommand.OpenFile(filename); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return nil
}
func (gui *Gui) anyFilesWithMergeConflicts() bool {
for _, file := range gui.State.Files {
if file.HasMergeConflicts {
return true
}
}
return false
}
func (gui *Gui) handleSoftReset(g *gocui.Gui, v *gocui.View) error {
return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("SoftReset"), gui.Tr.SLocalize("ConfirmSoftReset"), func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.SoftReset("HEAD^"); err != nil {
return gui.createErrorPanel(g, err.Error())
}
if err := gui.refreshCommits(gui.g); err != nil {
return err
}
return gui.refreshFiles()
}, nil)
}

646
pkg/gui/gui.go Normal file
View File

@@ -0,0 +1,646 @@
package gui
import (
"math"
"sync"
// "io"
// "io/ioutil"
"io/ioutil"
"os"
"os/exec"
"strings"
"time"
"github.com/go-errors/errors"
// "strings"
"github.com/fatih/color"
"github.com/golang-collections/collections/stack"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/updates"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
// OverlappingEdges determines if panel edges overlap
var OverlappingEdges = false
// SentinelErrors are the errors that have special meaning and need to be checked
// by calling functions. The less of these, the better
type SentinelErrors struct {
ErrSubProcess error
ErrNoFiles error
ErrSwitchRepo error
}
// GenerateSentinelErrors makes the sentinel errors for the gui. We're defining it here
// because we can't do package-scoped errors with localization, and also because
// it seems like package-scoped variables are bad in general
// https://dave.cheney.net/2017/06/11/go-without-package-scoped-variables
// In the future it would be good to implement some of the recommendations of
// that article. For now, if we don't need an error to be a sentinel, we will just
// define it inline. This has implications for error messages that pop up everywhere
// in that we'll be duplicating the default values. We may need to look at
// having a default localisation bundle defined, and just using keys-only when
// localising things in the code.
func (gui *Gui) GenerateSentinelErrors() {
gui.Errors = SentinelErrors{
ErrSubProcess: errors.New(gui.Tr.SLocalize("RunningSubprocess")),
ErrNoFiles: errors.New(gui.Tr.SLocalize("NoChangedFiles")),
ErrSwitchRepo: errors.New("switching repo"),
}
}
// Teml is short for template used to make the required map[string]interface{} shorter when using gui.Tr.SLocalize and gui.Tr.TemplateLocalize
type Teml i18n.Teml
// Gui wraps the gocui Gui object which handles rendering and events
type Gui struct {
g *gocui.Gui
Log *logrus.Entry
GitCommand *commands.GitCommand
OSCommand *commands.OSCommand
SubProcess *exec.Cmd
State guiState
Config config.AppConfigurer
Tr *i18n.Localizer
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 mergingPanelState struct {
ConflictIndex int
ConflictTop bool
Conflicts []commands.Conflict
EditHistory *stack.Stack
}
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
Branches *branchPanelState
Commits *commitPanelState
Stash *stashPanelState
Menu *menuPanelState
Staging *stagingPanelState
Merging *mergingPanelState
}
type guiState struct {
Files []*commands.File
Branches []*commands.Branch
Commits []*commands.Commit
StashEntries []*commands.StashEntry
PreviousView string
Platform commands.Platform
Updating bool
Panels *panelStates
WorkingTreeState string // one of "merging", "rebasing", "normal"
Contexts map[string]string
CherryPickedCommits []*commands.Commit
}
// NewGui builds a new gui handler
func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *commands.OSCommand, tr *i18n.Localizer, config config.AppConfigurer, updater *updates.Updater) (*Gui, error) {
initialState := guiState{
Files: make([]*commands.File, 0),
PreviousView: "files",
Commits: make([]*commands.Commit, 0),
CherryPickedCommits: make([]*commands.Commit, 0),
StashEntries: make([]*commands.StashEntry, 0),
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},
Merging: &mergingPanelState{
ConflictIndex: 0,
ConflictTop: true,
Conflicts: []commands.Conflict{},
EditHistory: stack.New(),
},
},
}
gui := &Gui{
Log: log,
GitCommand: gitCommand,
OSCommand: oSCommand,
State: initialState,
Config: config,
Tr: tr,
Updater: updater,
statusManager: &statusManager{},
}
gui.GenerateSentinelErrors()
return gui, nil
}
func (gui *Gui) scrollUpMain(g *gocui.Gui, v *gocui.View) error {
mainView, _ := g.View("main")
ox, oy := mainView.Origin()
newOy := int(math.Max(0, float64(oy-gui.Config.GetUserConfig().GetInt("gui.scrollHeight"))))
return mainView.SetOrigin(ox, newOy)
}
func (gui *Gui) scrollDownMain(g *gocui.Gui, v *gocui.View) error {
mainView, _ := g.View("main")
ox, oy := mainView.Origin()
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
}
func (gui *Gui) handleRefresh(g *gocui.Gui, v *gocui.View) error {
return gui.refreshSidePanels(g)
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
// getFocusLayout returns a manager function for when view gain and lose focus
func (gui *Gui) getFocusLayout() func(g *gocui.Gui) error {
var focusedView *gocui.View
return func(g *gocui.Gui) error {
v := gui.g.CurrentView()
if v != focusedView {
if err := gui.onFocusChange(); err != nil {
return err
}
if err := gui.onFocusLost(focusedView); err != nil {
return err
}
if err := gui.onFocus(v); err != nil {
return err
}
focusedView = v
}
return nil
}
}
func (gui *Gui) onFocusChange() error {
currentView := gui.g.CurrentView()
for _, view := range gui.g.Views() {
view.Highlight = view == currentView
}
return gui.setMainTitle()
}
func (gui *Gui) onFocusLost(v *gocui.View) error {
if v == nil {
return nil
}
if v.Name() == "branches" {
if err := gui.renderListPanel(gui.getBranchesView(), gui.State.Branches); err != nil {
return err
}
} else if v.Name() == "main" {
// if we have lost focus to a popup panel, that's okay
if gui.popupPanelFocused() {
return nil
}
if err := gui.changeContext("main", "normal"); err != nil {
return err
}
}
gui.Log.Info(v.Name() + " focus lost")
return nil
}
func (gui *Gui) onFocus(v *gocui.View) error {
if v == nil {
return nil
}
gui.Log.Info(v.Name() + " focus gained")
return nil
}
// layout is called for every screen re-render e.g. when the screen is resized
func (gui *Gui) layout(g *gocui.Gui) error {
g.Highlight = true
width, height := g.Size()
information := gui.Config.GetVersion()
if gui.g.Mouse {
donate := color.New(color.FgMagenta, color.Underline).Sprint(gui.Tr.SLocalize("Donate"))
information = donate + " " + information
}
leftSideWidth := width / 3
statusFilesBoundary := 2
filesBranchesBoundary := 2 * height / 5
commitsBranchesBoundary := 3 * height / 5
optionsTop := height - 2
commitsStashBoundary := optionsTop - 3
optionsVersionBoundary := width - max(len(utils.Decolorise(information)), 1)
minimumHeight := 18
minimumWidth := 10
appStatus := gui.statusManager.getStatusString()
appStatusOptionsBoundary := 0
if appStatus != "" {
appStatusOptionsBoundary = len(appStatus) + 2
}
panelSpacing := 1
if OverlappingEdges {
panelSpacing = 0
}
if height < minimumHeight || width < minimumWidth {
v, err := g.SetView("limit", 0, 0, max(width-1, 2), max(height-1, 2), 0)
if err != nil {
if err.Error() != "unknown view" {
return err
}
v.Title = gui.Tr.SLocalize("NotEnoughSpace")
v.Wrap = true
g.SetViewOnTop("limit")
}
return nil
}
_, _ = g.SetViewOnBottom("limit")
g.DeleteView("limit")
v, err := g.SetView("main", leftSideWidth+panelSpacing, 0, width-1, optionsTop, gocui.LEFT)
if err != nil {
if err.Error() != "unknown view" {
return err
}
v.Title = gui.Tr.SLocalize("DiffTitle")
v.Wrap = true
v.FgColor = gocui.ColorWhite
}
if v, err := g.SetView("status", 0, 0, leftSideWidth, statusFilesBoundary, gocui.BOTTOM|gocui.RIGHT); err != nil {
if err.Error() != "unknown view" {
return err
}
v.Title = gui.Tr.SLocalize("StatusTitle")
v.FgColor = gocui.ColorWhite
}
filesView, err := g.SetView("files", 0, statusFilesBoundary+panelSpacing, leftSideWidth, filesBranchesBoundary, gocui.TOP|gocui.BOTTOM)
if err != nil {
if err.Error() != "unknown view" {
return err
}
filesView.Highlight = true
filesView.Title = gui.Tr.SLocalize("FilesTitle")
v.FgColor = gocui.ColorWhite
}
branchesView, err := g.SetView("branches", 0, filesBranchesBoundary+panelSpacing, leftSideWidth, commitsBranchesBoundary, gocui.TOP|gocui.BOTTOM)
if err != nil {
if err.Error() != "unknown view" {
return err
}
branchesView.Title = gui.Tr.SLocalize("BranchesTitle")
branchesView.FgColor = gocui.ColorWhite
}
commitsView, err := g.SetView("commits", 0, commitsBranchesBoundary+panelSpacing, leftSideWidth, commitsStashBoundary, gocui.TOP|gocui.BOTTOM)
if err != nil {
if err.Error() != "unknown view" {
return err
}
commitsView.Title = gui.Tr.SLocalize("CommitsTitle")
commitsView.FgColor = gocui.ColorWhite
}
stashView, err := g.SetView("stash", 0, commitsStashBoundary+panelSpacing, leftSideWidth, optionsTop, gocui.TOP|gocui.RIGHT)
if err != nil {
if err.Error() != "unknown view" {
return err
}
stashView.Title = gui.Tr.SLocalize("StashTitle")
stashView.FgColor = gocui.ColorWhite
}
if v, err := g.SetView("options", appStatusOptionsBoundary-1, optionsTop, optionsVersionBoundary-1, optionsTop+2, 0); err != nil {
if err.Error() != "unknown view" {
return err
}
v.Frame = false
if v.FgColor, err = gui.GetOptionsPanelTextColor(); err != nil {
return err
}
}
if gui.getCommitMessageView() == nil {
// doesn't matter where this view starts because it will be hidden
if commitMessageView, err := g.SetView("commitMessage", 0, 0, width/2, height/2, 0); err != nil {
if err.Error() != "unknown view" {
return err
}
g.SetViewOnBottom("commitMessage")
commitMessageView.Title = gui.Tr.SLocalize("CommitMessage")
commitMessageView.FgColor = gocui.ColorWhite
commitMessageView.Editable = true
commitMessageView.Editor = gocui.EditorFunc(gui.simpleEditor)
}
}
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.Error() != "unknown view" {
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.Error() != "unknown view" {
return err
}
appStatusView.BgColor = gocui.ColorDefault
appStatusView.FgColor = gocui.ColorCyan
appStatusView.Frame = false
if _, err := g.SetViewOnBottom("appStatus"); err != nil {
return err
}
}
if v, err := g.SetView("information", optionsVersionBoundary-1, optionsTop, width, optionsTop+2, 0); err != nil {
if err.Error() != "unknown view" {
return err
}
v.BgColor = gocui.ColorDefault
v.FgColor = gocui.ColorGreen
v.Frame = false
if err := gui.renderString(g, "information", information); err != nil {
return err
}
// these are only called once (it's a place to put all the things you want
// to happen on startup after the screen is first rendered)
gui.Updater.CheckForNewUpdate(gui.onBackgroundUpdateCheckFinish, false)
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
}
if err := gui.switchFocus(g, nil, filesView); err != nil {
return err
}
if gui.Config.GetUserConfig().GetString("reporting") == "undetermined" {
if err := gui.promptAnonymousReporting(); err != nil {
return err
}
}
}
listViews := map[*gocui.View]int{
filesView: gui.State.Panels.Files.SelectedLine,
branchesView: gui.State.Panels.Branches.SelectedLine,
commitsView: gui.State.Panels.Commits.SelectedLine,
stashView: gui.State.Panels.Stash.SelectedLine,
}
// menu view might not exist so we check to be safe
if menuView, err := gui.g.View("menu"); err == nil {
listViews[menuView] = gui.State.Panels.Menu.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, 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 unamePassOpend, err
}
func (gui *Gui) renderAppStatus() error {
appStatus := gui.statusManager.getStatusString()
if appStatus != "" {
return gui.renderString(gui.g, "appStatus", appStatus)
}
return nil
}
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"),
"x": gui.Tr.SLocalize("menu"),
})
}
func (gui *Gui) goEvery(interval time.Duration, function func() error) {
go func() {
for range time.Tick(interval) {
_ = function()
}
}()
}
// Run setup the gui with keybindings and start the mainloop
func (gui *Gui) Run() error {
g, err := gocui.NewGui(gocui.OutputNormal, OverlappingEdges)
if err != nil {
return err
}
defer g.Close()
if gui.Config.GetUserConfig().GetBool("gui.mouseEvents") {
g.Mouse = true
}
gui.g = g // TODO: always use gui.g rather than passing g around everywhere
if err := gui.SetColorScheme(); err != nil {
return err
}
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(time.Second*60, func() error {
_, err := gui.fetch(gui.g, gui.g.CurrentView(), false)
return err
})
}
}()
gui.goEvery(time.Second*10, gui.refreshFiles)
gui.goEvery(time.Millisecond*50, gui.renderAppStatus)
g.SetManager(gocui.ManagerFunc(gui.layout), gocui.ManagerFunc(gui.getFocusLayout()))
if err = gui.keybindings(g); err != nil {
return err
}
err = g.MainLoop()
return err
}
// RunWithSubprocesses loops, instantiating a new gocui.Gui with each iteration
// if the error returned from a run is a ErrSubProcess, it runs the subprocess
// otherwise it handles the error, possibly by quitting the application
func (gui *Gui) RunWithSubprocesses() error {
for {
if err := gui.Run(); err != nil {
if err == gocui.ErrQuit {
break
} else if err == gui.Errors.ErrSwitchRepo {
continue
} else if err == gui.Errors.ErrSubProcess {
gui.SubProcess.Stdin = os.Stdin
gui.SubProcess.Stdout = os.Stdout
gui.SubProcess.Stderr = os.Stderr
gui.SubProcess.Run()
gui.SubProcess.Stdout = ioutil.Discard
gui.SubProcess.Stderr = ioutil.Discard
gui.SubProcess.Stdin = nil
gui.SubProcess = nil
} else {
return err
}
}
}
return nil
}
func (gui *Gui) quit(g *gocui.Gui, v *gocui.View) error {
if gui.State.Updating {
return gui.createUpdateQuitConfirmation(g, v)
}
if gui.Config.GetUserConfig().GetBool("confirmOnQuit") {
return gui.createConfirmationPanel(g, v, "", gui.Tr.SLocalize("ConfirmQuit"), func(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}, nil)
}
return gocui.ErrQuit
}
func (gui *Gui) handleDonate(g *gocui.Gui, v *gocui.View) error {
if !gui.g.Mouse {
return nil
}
cx, _ := v.Cursor()
if cx > len(gui.Tr.SLocalize("Donate")) {
return nil
}
return gui.OSCommand.OpenLink("https://donorbox.org/lazygit")
}

687
pkg/gui/keybindings.go Normal file
View File

@@ -0,0 +1,687 @@
package gui
import (
"github.com/jesseduffield/gocui"
)
// Binding - a keybinding mapping a key and modifier to a handler. The keypress
// is only handled if the given view has focus, or handled globally if the view
// is ""
type Binding struct {
ViewName string
Handler func(*gocui.Gui, *gocui.View) error
Key interface{} // FIXME: find out how to get `gocui.Key | rune`
Modifier gocui.Modifier
Description string
}
// GetDisplayStrings returns the display string of a file
func (b *Binding) GetDisplayStrings(isFocused bool) []string {
return []string{b.GetKey(), b.Description}
}
// GetKey is a function.
func (b *Binding) GetKey() string {
key := 0
switch b.Key.(type) {
case rune:
key = int(b.Key.(rune))
case gocui.Key:
key = int(b.Key.(gocui.Key))
}
// special keys
switch key {
case 27:
return "esc"
case 13:
return "enter"
case 32:
return "space"
case 65514:
return "►"
case 65515:
return "◄"
case 65517:
return "▲"
case 65516:
return "▼"
case 65508:
return "PgUp"
case 65507:
return "PgDn"
}
return string(key)
}
// GetInitialKeybindings is a function.
func (gui *Gui) GetInitialKeybindings() []*Binding {
bindings := []*Binding{
{
ViewName: "",
Key: 'q',
Modifier: gocui.ModNone,
Handler: gui.quit,
}, {
ViewName: "",
Key: gocui.KeyCtrlC,
Modifier: gocui.ModNone,
Handler: gui.quit,
}, {
ViewName: "",
Key: gocui.KeyEsc,
Modifier: gocui.ModNone,
Handler: gui.quit,
}, {
ViewName: "",
Key: gocui.KeyPgup,
Modifier: gocui.ModNone,
Handler: gui.scrollUpMain,
}, {
ViewName: "",
Key: gocui.KeyPgdn,
Modifier: gocui.ModNone,
Handler: gui.scrollDownMain,
}, {
ViewName: "",
Key: gocui.KeyCtrlU,
Modifier: gocui.ModNone,
Handler: gui.scrollUpMain,
}, {
ViewName: "",
Key: gocui.KeyCtrlD,
Modifier: gocui.ModNone,
Handler: gui.scrollDownMain,
}, {
ViewName: "",
Key: 'm',
Modifier: gocui.ModNone,
Handler: gui.handleCreateRebaseOptionsMenu,
Description: gui.Tr.SLocalize("ViewMergeRebaseOptions"),
}, {
ViewName: "",
Key: 'P',
Modifier: gocui.ModNone,
Handler: gui.pushFiles,
Description: gui.Tr.SLocalize("push"),
}, {
ViewName: "",
Key: 'p',
Modifier: gocui.ModNone,
Handler: gui.pullFiles,
Description: gui.Tr.SLocalize("pull"),
}, {
ViewName: "",
Key: 'R',
Modifier: gocui.ModNone,
Handler: gui.handleRefresh,
Description: gui.Tr.SLocalize("refresh"),
}, {
ViewName: "",
Key: 'x',
Modifier: gocui.ModNone,
Handler: gui.handleCreateOptionsMenu,
}, {
ViewName: "status",
Key: 'e',
Modifier: gocui.ModNone,
Handler: gui.handleEditConfig,
Description: gui.Tr.SLocalize("EditConfig"),
}, {
ViewName: "status",
Key: 'o',
Modifier: gocui.ModNone,
Handler: gui.handleOpenConfig,
Description: gui.Tr.SLocalize("OpenConfig"),
}, {
ViewName: "status",
Key: 'u',
Modifier: gocui.ModNone,
Handler: gui.handleCheckForUpdate,
Description: gui.Tr.SLocalize("checkForUpdate"),
}, {
ViewName: "status",
Key: 's',
Modifier: gocui.ModNone,
Handler: gui.handleCreateRecentReposMenu,
Description: gui.Tr.SLocalize("SwitchRepo"),
},
{
ViewName: "files",
Key: 'c',
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',
Modifier: gocui.ModNone,
Handler: gui.handleCommitEditorPress,
Description: gui.Tr.SLocalize("CommitChangesWithEditor"),
}, {
ViewName: "files",
Key: gocui.KeySpace,
Modifier: gocui.ModNone,
Handler: gui.handleFilePress,
Description: gui.Tr.SLocalize("toggleStaged"),
}, {
ViewName: "files",
Key: 'd',
Modifier: gocui.ModNone,
Handler: gui.handleFileRemove,
Description: gui.Tr.SLocalize("removeFile"),
}, {
ViewName: "files",
Key: 'e',
Modifier: gocui.ModNone,
Handler: gui.handleFileEdit,
Description: gui.Tr.SLocalize("editFile"),
}, {
ViewName: "files",
Key: 'o',
Modifier: gocui.ModNone,
Handler: gui.handleFileOpen,
Description: gui.Tr.SLocalize("openFile"),
}, {
ViewName: "files",
Key: 'i',
Modifier: gocui.ModNone,
Handler: gui.handleIgnoreFile,
Description: gui.Tr.SLocalize("ignoreFile"),
}, {
ViewName: "files",
Key: 'r',
Modifier: gocui.ModNone,
Handler: gui.handleRefreshFiles,
Description: gui.Tr.SLocalize("refreshFiles"),
}, {
ViewName: "files",
Key: 'S',
Modifier: gocui.ModNone,
Handler: gui.handleStashSave,
Description: gui.Tr.SLocalize("stashFiles"),
}, {
ViewName: "files",
Key: 's',
Modifier: gocui.ModNone,
Handler: gui.handleSoftReset,
Description: gui.Tr.SLocalize("softReset"),
}, {
ViewName: "files",
Key: 'a',
Modifier: gocui.ModNone,
Handler: gui.handleStageAll,
Description: gui.Tr.SLocalize("toggleStagedAll"),
}, {
ViewName: "files",
Key: 't',
Modifier: gocui.ModNone,
Handler: gui.handleAddPatch,
Description: gui.Tr.SLocalize("addPatch"),
}, {
ViewName: "files",
Key: 'D',
Modifier: gocui.ModNone,
Handler: gui.handleResetAndClean,
Description: gui.Tr.SLocalize("resetHard"),
}, {
ViewName: "files",
Key: gocui.KeyEnter,
Modifier: gocui.ModNone,
Handler: gui.handleEnterFile,
Description: gui.Tr.SLocalize("StageLines"),
}, {
ViewName: "files",
Key: 'f',
Modifier: gocui.ModNone,
Handler: gui.handleGitFetch,
Description: gui.Tr.SLocalize("fetch"),
}, {
ViewName: "branches",
Key: gocui.KeySpace,
Modifier: gocui.ModNone,
Handler: gui.handleBranchPress,
Description: gui.Tr.SLocalize("checkout"),
}, {
ViewName: "branches",
Key: 'o',
Modifier: gocui.ModNone,
Handler: gui.handleCreatePullRequestPress,
Description: gui.Tr.SLocalize("createPullRequest"),
}, {
ViewName: "branches",
Key: 'c',
Modifier: gocui.ModNone,
Handler: gui.handleCheckoutByName,
Description: gui.Tr.SLocalize("checkoutByName"),
}, {
ViewName: "branches",
Key: 'F',
Modifier: gocui.ModNone,
Handler: gui.handleForceCheckout,
Description: gui.Tr.SLocalize("forceCheckout"),
}, {
ViewName: "branches",
Key: 'n',
Modifier: gocui.ModNone,
Handler: gui.handleNewBranch,
Description: gui.Tr.SLocalize("newBranch"),
}, {
ViewName: "branches",
Key: 'd',
Modifier: gocui.ModNone,
Handler: gui.handleDeleteBranch,
Description: gui.Tr.SLocalize("deleteBranch"),
}, {
ViewName: "branches",
Key: 'r',
Modifier: gocui.ModNone,
Handler: gui.handleRebase,
Description: gui.Tr.SLocalize("rebaseBranch"),
}, {
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',
Modifier: gocui.ModNone,
Handler: gui.handleCommitSquashDown,
Description: gui.Tr.SLocalize("squashDown"),
}, {
ViewName: "commits",
Key: 'r',
Modifier: gocui.ModNone,
Handler: gui.handleRenameCommit,
Description: gui.Tr.SLocalize("renameCommit"),
}, {
ViewName: "commits",
Key: 'R',
Modifier: gocui.ModNone,
Handler: gui.handleRenameCommitEditor,
Description: gui.Tr.SLocalize("renameCommitEditor"),
}, {
ViewName: "commits",
Key: 'g',
Modifier: gocui.ModNone,
Handler: gui.handleResetToCommit,
Description: gui.Tr.SLocalize("resetToThisCommit"),
}, {
ViewName: "commits",
Key: 'f',
Modifier: gocui.ModNone,
Handler: gui.handleCommitFixup,
Description: gui.Tr.SLocalize("fixupCommit"),
}, {
ViewName: "commits",
Key: 'd',
Modifier: gocui.ModNone,
Handler: gui.handleCommitDelete,
Description: gui.Tr.SLocalize("deleteCommit"),
}, {
ViewName: "commits",
Key: 'J',
Modifier: gocui.ModNone,
Handler: gui.handleCommitMoveDown,
Description: gui.Tr.SLocalize("moveDownCommit"),
}, {
ViewName: "commits",
Key: 'K',
Modifier: gocui.ModNone,
Handler: gui.handleCommitMoveUp,
Description: gui.Tr.SLocalize("moveUpCommit"),
}, {
ViewName: "commits",
Key: 'e',
Modifier: gocui.ModNone,
Handler: gui.handleCommitEdit,
Description: gui.Tr.SLocalize("editCommit"),
}, {
ViewName: "commits",
Key: 'A',
Modifier: gocui.ModNone,
Handler: gui.handleCommitAmendTo,
Description: gui.Tr.SLocalize("amendToCommit"),
}, {
ViewName: "commits",
Key: 'p',
Modifier: gocui.ModNone,
Handler: gui.handleCommitPick,
Description: gui.Tr.SLocalize("pickCommit"),
}, {
ViewName: "commits",
Key: 't',
Modifier: gocui.ModNone,
Handler: gui.handleCommitRevert,
Description: gui.Tr.SLocalize("revertCommit"),
}, {
ViewName: "commits",
Key: 'c',
Modifier: gocui.ModNone,
Handler: gui.handleCopyCommit,
Description: gui.Tr.SLocalize("cherryPickCopy"),
}, {
ViewName: "commits",
Key: 'C',
Modifier: gocui.ModNone,
Handler: gui.handleCopyCommitRange,
Description: gui.Tr.SLocalize("cherryPickCopyRange"),
}, {
ViewName: "commits",
Key: 'v',
Modifier: gocui.ModNone,
Handler: gui.HandlePasteCommits,
Description: gui.Tr.SLocalize("pasteCommits"),
}, {
ViewName: "stash",
Key: gocui.KeySpace,
Modifier: gocui.ModNone,
Handler: gui.handleStashApply,
Description: gui.Tr.SLocalize("apply"),
}, {
ViewName: "stash",
Key: 'g',
Modifier: gocui.ModNone,
Handler: gui.handleStashPop,
Description: gui.Tr.SLocalize("pop"),
}, {
ViewName: "stash",
Key: 'd',
Modifier: gocui.ModNone,
Handler: gui.handleStashDrop,
Description: gui.Tr.SLocalize("drop"),
}, {
ViewName: "commitMessage",
Key: gocui.KeyEnter,
Modifier: gocui.ModNone,
Handler: gui.handleCommitConfirm,
}, {
ViewName: "commitMessage",
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,
Modifier: gocui.ModNone,
Handler: gui.handleMenuClose,
}, {
ViewName: "menu",
Key: 'q',
Modifier: gocui.ModNone,
Handler: gui.handleMenuClose,
}, {
ViewName: "information",
Key: gocui.MouseLeft,
Modifier: gocui.ModNone,
Handler: gui.handleDonate,
},
}
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: 'h', Modifier: gocui.ModNone, Handler: gui.previousView},
{ViewName: viewName, Key: 'l', Modifier: gocui.ModNone, Handler: gui.nextView},
}...)
}
listPanelMap := map[string]struct {
prevLine func(*gocui.Gui, *gocui.View) error
nextLine func(*gocui.Gui, *gocui.View) error
focus func(*gocui.Gui, *gocui.View) error
}{
"menu": {prevLine: gui.handleMenuPrevLine, nextLine: gui.handleMenuNextLine, focus: gui.handleMenuSelect},
"files": {prevLine: gui.handleFilesPrevLine, nextLine: gui.handleFilesNextLine, focus: gui.handleFilesFocus},
"branches": {prevLine: gui.handleBranchesPrevLine, nextLine: gui.handleBranchesNextLine, focus: gui.handleBranchSelect},
"commits": {prevLine: gui.handleCommitsPrevLine, nextLine: gui.handleCommitsNextLine, focus: gui.handleCommitSelect},
"stash": {prevLine: gui.handleStashPrevLine, nextLine: gui.handleStashNextLine, focus: gui.handleStashEntrySelect},
"status": {focus: gui.handleStatusSelect},
}
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: gocui.MouseWheelUp, 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},
{ViewName: viewName, Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, Handler: functions.nextLine},
{ViewName: viewName, Key: gocui.MouseLeft, Modifier: gocui.ModNone, Handler: functions.focus},
}...)
}
return bindings
}
// GetCurrentKeybindings gets the list of keybindings given the current context
func (gui *Gui) GetCurrentKeybindings() []*Binding {
bindings := gui.GetInitialKeybindings()
viewName := gui.currentViewName()
currentContext := gui.State.Contexts[viewName]
contextBindings := gui.GetContextMap()[viewName][currentContext]
return append(bindings, contextBindings...)
}
func (gui *Gui) keybindings(g *gocui.Gui) error {
bindings := gui.GetInitialKeybindings()
for _, binding := range bindings {
if err := g.SetKeybinding(binding.ViewName, binding.Key, binding.Modifier, binding.Handler); err != nil {
return err
}
}
if err := gui.setInitialContexts(); err != nil {
return err
}
return nil
}
func (gui *Gui) GetContextMap() map[string]map[string][]*Binding {
return map[string]map[string][]*Binding{
"main": {
"normal": {
{
ViewName: "main",
Key: gocui.MouseWheelDown,
Modifier: gocui.ModNone,
Handler: gui.scrollDownMain,
Description: gui.Tr.SLocalize("ScrollDown"),
}, {
ViewName: "main",
Key: gocui.MouseWheelUp,
Modifier: gocui.ModNone,
Handler: gui.scrollUpMain,
Description: gui.Tr.SLocalize("ScrollUp"),
},
},
"staging": {
{
ViewName: "main",
Key: gocui.KeyEsc,
Modifier: gocui.ModNone,
Handler: gui.handleStagingEscape,
Description: gui.Tr.SLocalize("EscapeStaging"),
}, {
ViewName: "main",
Key: gocui.KeyArrowUp,
Modifier: gocui.ModNone,
Handler: gui.handleStagingPrevLine,
Description: gui.Tr.SLocalize("PrevLine"),
}, {
ViewName: "main",
Key: gocui.KeyArrowDown,
Modifier: gocui.ModNone,
Handler: gui.handleStagingNextLine,
Description: gui.Tr.SLocalize("NextLine"),
}, {
ViewName: "main",
Key: 'k',
Modifier: gocui.ModNone,
Handler: gui.handleStagingPrevLine,
}, {
ViewName: "main",
Key: 'j',
Modifier: gocui.ModNone,
Handler: gui.handleStagingNextLine,
}, {
ViewName: "main",
Key: gocui.MouseWheelUp,
Modifier: gocui.ModNone,
Handler: gui.handleStagingPrevLine,
}, {
ViewName: "main",
Key: gocui.MouseWheelDown,
Modifier: gocui.ModNone,
Handler: gui.handleStagingNextLine,
}, {
ViewName: "main",
Key: gocui.KeyArrowLeft,
Modifier: gocui.ModNone,
Handler: gui.handleStagingPrevHunk,
Description: gui.Tr.SLocalize("PrevHunk"),
}, {
ViewName: "main",
Key: gocui.KeyArrowRight,
Modifier: gocui.ModNone,
Handler: gui.handleStagingNextHunk,
Description: gui.Tr.SLocalize("NextHunk"),
}, {
ViewName: "main",
Key: 'h',
Modifier: gocui.ModNone,
Handler: gui.handleStagingPrevHunk,
}, {
ViewName: "main",
Key: 'l',
Modifier: gocui.ModNone,
Handler: gui.handleStagingNextHunk,
}, {
ViewName: "main",
Key: gocui.KeySpace,
Modifier: gocui.ModNone,
Handler: gui.handleStageLine,
Description: gui.Tr.SLocalize("StageLine"),
}, {
ViewName: "main",
Key: 'a',
Modifier: gocui.ModNone,
Handler: gui.handleStageHunk,
Description: gui.Tr.SLocalize("StageHunk"),
},
},
"merging": {
{
ViewName: "main",
Key: gocui.KeyEsc,
Modifier: gocui.ModNone,
Handler: gui.handleEscapeMerge,
Description: gui.Tr.SLocalize("EscapeStaging"),
}, {
ViewName: "main",
Key: gocui.KeySpace,
Modifier: gocui.ModNone,
Handler: gui.handlePickHunk,
Description: gui.Tr.SLocalize("PickHunk"),
}, {
ViewName: "main",
Key: 'b',
Modifier: gocui.ModNone,
Handler: gui.handlePickBothHunks,
Description: gui.Tr.SLocalize("PickBothHunks"),
}, {
ViewName: "main",
Key: gocui.KeyArrowLeft,
Modifier: gocui.ModNone,
Handler: gui.handleSelectPrevConflict,
Description: gui.Tr.SLocalize("PrevConflict"),
}, {
ViewName: "main",
Key: gocui.KeyArrowRight,
Modifier: gocui.ModNone,
Handler: gui.handleSelectNextConflict,
Description: gui.Tr.SLocalize("NextConflict"),
}, {
ViewName: "main",
Key: gocui.KeyArrowUp,
Modifier: gocui.ModNone,
Handler: gui.handleSelectTop,
Description: gui.Tr.SLocalize("SelectTop"),
}, {
ViewName: "main",
Key: gocui.KeyArrowDown,
Modifier: gocui.ModNone,
Handler: gui.handleSelectBottom,
Description: gui.Tr.SLocalize("SelectBottom"),
}, {
ViewName: "main",
Key: gocui.MouseWheelUp,
Modifier: gocui.ModNone,
Handler: gui.handleSelectTop,
}, {
ViewName: "main",
Key: gocui.MouseWheelDown,
Modifier: gocui.ModNone,
Handler: gui.handleSelectBottom,
}, {
ViewName: "main",
Key: 'h',
Modifier: gocui.ModNone,
Handler: gui.handleSelectPrevConflict,
}, {
ViewName: "main",
Key: 'l',
Modifier: gocui.ModNone,
Handler: gui.handleSelectNextConflict,
}, {
ViewName: "main",
Key: 'k',
Modifier: gocui.ModNone,
Handler: gui.handleSelectTop,
}, {
ViewName: "main",
Key: 'j',
Modifier: gocui.ModNone,
Handler: gui.handleSelectBottom,
}, {
ViewName: "main",
Key: 'z',
Modifier: gocui.ModNone,
Handler: gui.handlePopFileSnapshot,
Description: gui.Tr.SLocalize("Undo"),
},
},
},
}
}

99
pkg/gui/menu_panel.go Normal file
View File

@@ -0,0 +1,99 @@
package gui
import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// list panel functions
func (gui *Gui) handleMenuSelect(g *gocui.Gui, v *gocui.View) error {
return gui.focusPoint(0, gui.State.Panels.Menu.SelectedLine, v)
}
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(optionsMap)
}
func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error {
for _, key := range []gocui.Key{gocui.KeySpace, gocui.KeyEnter} {
if err := g.DeleteKeybinding("menu", key, gocui.ModNone); err != nil {
return err
}
}
err := g.DeleteView("menu")
if err != nil {
return err
}
return gui.returnFocus(g, v)
}
func (gui *Gui) createMenu(title string, items interface{}, handlePress func(int) error) error {
isFocused := gui.g.CurrentView().Name() == "menu"
list, err := utils.RenderList(items, isFocused)
if err != nil {
return err
}
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, false, list)
menuView, _ := gui.g.SetView("menu", x0, y0, x1, y1, 0)
menuView.Title = title
menuView.FgColor = gocui.ColorWhite
menuView.Clear()
fmt.Fprint(menuView, list)
gui.State.Panels.Menu.SelectedLine = 0
wrappedHandlePress := func(g *gocui.Gui, v *gocui.View) error {
selectedLine := gui.State.Panels.Menu.SelectedLine
if err := handlePress(selectedLine); err != nil {
return err
}
if _, err := gui.g.View("menu"); err == nil {
if _, err := gui.g.SetViewOnBottom("menu"); err != nil {
return err
}
}
return gui.returnFocus(gui.g, menuView)
}
for _, key := range []gocui.Key{gocui.KeySpace, gocui.KeyEnter} {
if err := gui.g.SetKeybinding("menu", key, gocui.ModNone, wrappedHandlePress); err != nil {
return err
}
}
gui.g.Update(func(g *gocui.Gui) error {
if _, err := gui.g.View("menu"); err == nil {
if _, err := g.SetViewOnTop("menu"); err != nil {
return err
}
}
currentView := gui.g.CurrentView()
return gui.switchFocus(gui.g, currentView, menuView)
})
return nil
}

290
pkg/gui/merge_panel.go Normal file
View File

@@ -0,0 +1,290 @@
// though this panel is called the merge panel, it's really going to use the main panel. This may change in the future
package gui
import (
"bufio"
"bytes"
"io/ioutil"
"math"
"os"
"strings"
"github.com/fatih/color"
"github.com/golang-collections/collections/stack"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) findConflicts(content string) ([]commands.Conflict, error) {
conflicts := make([]commands.Conflict, 0)
var newConflict commands.Conflict
for i, line := range utils.SplitLines(content) {
trimmedLine := strings.TrimPrefix(line, "++")
gui.Log.Info(trimmedLine)
if trimmedLine == "<<<<<<< HEAD" || trimmedLine == "<<<<<<< MERGE_HEAD" || trimmedLine == "<<<<<<< Updated upstream" {
newConflict = commands.Conflict{Start: i}
} else if trimmedLine == "=======" {
newConflict.Middle = i
} else if strings.HasPrefix(trimmedLine, ">>>>>>> ") {
newConflict.End = i
conflicts = append(conflicts, newConflict)
}
}
return conflicts, nil
}
func (gui *Gui) shiftConflict(conflicts []commands.Conflict) (commands.Conflict, []commands.Conflict) {
return conflicts[0], conflicts[1:]
}
func (gui *Gui) shouldHighlightLine(index int, conflict commands.Conflict, top bool) bool {
return (index >= conflict.Start && index <= conflict.Middle && top) || (index >= conflict.Middle && index <= conflict.End && !top)
}
func (gui *Gui) coloredConflictFile(content string, conflicts []commands.Conflict, conflictIndex int, conflictTop, hasFocus bool) (string, error) {
if len(conflicts) == 0 {
return content, nil
}
conflict, remainingConflicts := gui.shiftConflict(conflicts)
var outputBuffer bytes.Buffer
for i, line := range utils.SplitLines(content) {
colourAttr := color.FgWhite
if i == conflict.Start || i == conflict.Middle || i == conflict.End {
colourAttr = color.FgRed
}
colour := color.New(colourAttr)
if hasFocus && conflictIndex < len(conflicts) && conflicts[conflictIndex] == conflict && gui.shouldHighlightLine(i, conflict, conflictTop) {
colour.Add(color.Bold)
}
if i == conflict.End && len(remainingConflicts) > 0 {
conflict, remainingConflicts = gui.shiftConflict(remainingConflicts)
}
outputBuffer.WriteString(utils.ColoredStringDirect(line, colour) + "\n")
}
return outputBuffer.String(), nil
}
func (gui *Gui) handleSelectTop(g *gocui.Gui, v *gocui.View) error {
gui.State.Panels.Merging.ConflictTop = true
return gui.refreshMergePanel()
}
func (gui *Gui) handleSelectBottom(g *gocui.Gui, v *gocui.View) error {
gui.State.Panels.Merging.ConflictTop = false
return gui.refreshMergePanel()
}
func (gui *Gui) handleSelectNextConflict(g *gocui.Gui, v *gocui.View) error {
if gui.State.Panels.Merging.ConflictIndex >= len(gui.State.Panels.Merging.Conflicts)-1 {
return nil
}
gui.State.Panels.Merging.ConflictIndex++
return gui.refreshMergePanel()
}
func (gui *Gui) handleSelectPrevConflict(g *gocui.Gui, v *gocui.View) error {
if gui.State.Panels.Merging.ConflictIndex <= 0 {
return nil
}
gui.State.Panels.Merging.ConflictIndex--
return gui.refreshMergePanel()
}
func (gui *Gui) isIndexToDelete(i int, conflict commands.Conflict, pick string) bool {
return i == conflict.Middle ||
i == conflict.Start ||
i == conflict.End ||
pick != "both" &&
(pick == "bottom" && i > conflict.Start && i < conflict.Middle) ||
(pick == "top" && i > conflict.Middle && i < conflict.End)
}
func (gui *Gui) resolveConflict(g *gocui.Gui, conflict commands.Conflict, pick string) error {
gitFile, err := gui.getSelectedFile(g)
if err != nil {
return err
}
file, err := os.Open(gitFile.Name)
if err != nil {
return err
}
defer file.Close()
reader := bufio.NewReader(file)
output := ""
for i := 0; true; i++ {
line, err := reader.ReadString('\n')
if err != nil {
break
}
if !gui.isIndexToDelete(i, conflict, pick) {
output += line
}
}
gui.Log.Info(output)
return ioutil.WriteFile(gitFile.Name, []byte(output), 0644)
}
func (gui *Gui) pushFileSnapshot(g *gocui.Gui) error {
gitFile, err := gui.getSelectedFile(g)
if err != nil {
return err
}
content, err := gui.GitCommand.CatFile(gitFile.Name)
if err != nil {
return err
}
gui.State.Panels.Merging.EditHistory.Push(content)
return nil
}
func (gui *Gui) handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error {
if gui.State.Panels.Merging.EditHistory.Len() == 0 {
return nil
}
prevContent := gui.State.Panels.Merging.EditHistory.Pop().(string)
gitFile, err := gui.getSelectedFile(g)
if err != nil {
return err
}
ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644)
return gui.refreshMergePanel()
}
func (gui *Gui) handlePickHunk(g *gocui.Gui, v *gocui.View) error {
conflict := gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex]
gui.pushFileSnapshot(g)
pick := "bottom"
if gui.State.Panels.Merging.ConflictTop {
pick = "top"
}
err := gui.resolveConflict(g, conflict, pick)
if err != nil {
panic(err)
}
// if that was the last conflict, finish the merge for this file
if len(gui.State.Panels.Merging.Conflicts) == 1 {
if err := gui.handleCompleteMerge(); err != nil {
return err
}
}
return gui.refreshMergePanel()
}
func (gui *Gui) handlePickBothHunks(g *gocui.Gui, v *gocui.View) error {
conflict := gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex]
gui.pushFileSnapshot(g)
err := gui.resolveConflict(g, conflict, "both")
if err != nil {
panic(err)
}
return gui.refreshMergePanel()
}
func (gui *Gui) refreshMergePanel() error {
panelState := gui.State.Panels.Merging
cat, err := gui.catSelectedFile(gui.g)
if err != nil {
return err
}
if cat == "" {
return nil
}
panelState.Conflicts, err = gui.findConflicts(cat)
if err != nil {
return err
}
// handle potential fixes that the user made in their editor since we last refreshed
if len(panelState.Conflicts) == 0 {
return gui.handleCompleteMerge()
} else if panelState.ConflictIndex > len(panelState.Conflicts)-1 {
panelState.ConflictIndex = len(panelState.Conflicts) - 1
}
hasFocus := gui.currentViewName() == "main"
content, err := gui.coloredConflictFile(cat, panelState.Conflicts, panelState.ConflictIndex, panelState.ConflictTop, hasFocus)
if err != nil {
return err
}
if err := gui.renderString(gui.g, "main", content); err != nil {
return err
}
if err := gui.scrollToConflict(gui.g); err != nil {
return err
}
mainView := gui.getMainView()
mainView.Wrap = false
return nil
}
func (gui *Gui) scrollToConflict(g *gocui.Gui) error {
panelState := gui.State.Panels.Merging
if len(panelState.Conflicts) == 0 {
return nil
}
mergingView := gui.getMainView()
conflict := panelState.Conflicts[panelState.ConflictIndex]
ox, _ := mergingView.Origin()
_, height := mergingView.Size()
conflictMiddle := (conflict.End + conflict.Start) / 2
newOriginY := int(math.Max(0, float64(conflictMiddle-(height/2))))
gui.g.Update(func(g *gocui.Gui) error {
return mergingView.SetOrigin(ox, newOriginY)
})
return nil
}
func (gui *Gui) renderMergeOptions() error {
return gui.renderOptionsMap(map[string]string{
"↑ ↓": gui.Tr.SLocalize("selectHunk"),
"← →": gui.Tr.SLocalize("navigateConflicts"),
"space": gui.Tr.SLocalize("pickHunk"),
"b": gui.Tr.SLocalize("pickBothHunks"),
"z": gui.Tr.SLocalize("undo"),
})
}
func (gui *Gui) handleEscapeMerge(g *gocui.Gui, v *gocui.View) error {
gui.State.Panels.Merging.EditHistory = stack.New()
if err := gui.refreshFiles(); err != nil {
return err
}
// it's possible this method won't be called from the merging view so we need to
// ensure we only 'return' focus if we already have it
if gui.g.CurrentView() == gui.getMainView() {
return gui.switchFocus(g, v, gui.getFilesView())
}
return nil
}
func (gui *Gui) handleCompleteMerge() error {
if err := gui.stageSelectedFile(gui.g); err != nil {
return err
}
if err := gui.refreshFiles(); err != nil {
return err
}
// if we got conflicts after unstashing, we don't want to call any git
// commands to continue rebasing/merging here
if gui.State.WorkingTreeState == "normal" {
return gui.handleEscapeMerge(gui.g, gui.getMainView())
}
// if there are no more files with merge conflicts, we should ask whether the user wants to continue
if !gui.anyFilesWithMergeConflicts() {
return gui.promptToContinue()
}
return gui.handleEscapeMerge(gui.g, gui.getMainView())
}
// promptToContinue asks the user if they want to continue the rebase/merge that's in progress
func (gui *Gui) promptToContinue() error {
return gui.createConfirmationPanel(gui.g, gui.getFilesView(), "continue", gui.Tr.SLocalize("ConflictsResolved"), func(g *gocui.Gui, v *gocui.View) error {
return gui.genericMergeCommand("continue")
}, nil)
}

View File

@@ -0,0 +1,53 @@
package gui
import (
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
)
func (gui *Gui) getBindings(v *gocui.View) []*Binding {
var (
bindingsGlobal, bindingsPanel []*Binding
)
bindings := gui.GetCurrentKeybindings()
for _, binding := range bindings {
if binding.GetKey() != "" && binding.Description != "" {
switch binding.ViewName {
case "":
bindingsGlobal = append(bindingsGlobal, binding)
case v.Name():
bindingsPanel = append(bindingsPanel, binding)
}
}
}
// append dummy element to have a separator between
// panel and global keybindings
bindingsPanel = append(bindingsPanel, &Binding{})
return append(bindingsPanel, bindingsGlobal...)
}
func (gui *Gui) handleCreateOptionsMenu(g *gocui.Gui, v *gocui.View) error {
bindings := gui.getBindings(v)
handleMenuPress := func(index int) error {
if bindings[index].Key == nil {
return nil
}
if index >= len(bindings) {
return errors.New("Index is greater than size of bindings")
}
err := gui.handleMenuClose(g, v)
if err != nil {
return err
}
return bindings[index].Handler(g, v)
}
return gui.createMenu(strings.Title(gui.Tr.SLocalize("menu")), bindings, handleMenuPress)
}

View File

@@ -0,0 +1,91 @@
package gui
import (
"fmt"
"strings"
"github.com/jesseduffield/gocui"
)
type option struct {
value string
}
// GetDisplayStrings is a function.
func (r *option) GetDisplayStrings(isFocused bool) []string {
return []string{r.value}
}
func (gui *Gui) handleCreateRebaseOptionsMenu(g *gocui.Gui, v *gocui.View) error {
options := []*option{
{value: "continue"},
{value: "abort"},
}
if gui.State.WorkingTreeState == "rebasing" {
options = append(options, &option{value: "skip"})
}
handleMenuPress := func(index int) error {
command := options[index].value
return gui.genericMergeCommand(command)
}
var title string
if gui.State.WorkingTreeState == "merging" {
title = gui.Tr.SLocalize("MergeOptionsTitle")
} else {
title = gui.Tr.SLocalize("RebaseOptionsTitle")
}
return gui.createMenu(title, options, handleMenuPress)
}
func (gui *Gui) genericMergeCommand(command string) error {
status := gui.State.WorkingTreeState
if status != "merging" && status != "rebasing" {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NotMergingOrRebasing"))
}
commandType := strings.Replace(status, "ing", "e", 1)
// we should end up with a command like 'git merge --continue'
// it's impossible for a rebase to require a commit so we'll use a subprocess only if it's a merge
if status == "merging" && command != "abort" && gui.Config.GetUserConfig().GetBool("git.merging.manualCommit") {
sub := gui.OSCommand.PrepareSubProcess("git", commandType, fmt.Sprintf("--%s", command))
if sub != nil {
gui.SubProcess = sub
return gui.Errors.ErrSubProcess
}
return nil
}
result := gui.GitCommand.GenericMerge(commandType, command)
if err := gui.handleGenericMergeCommandResult(result); err != nil {
return err
}
return nil
}
func (gui *Gui) handleGenericMergeCommandResult(result error) error {
if err := gui.refreshSidePanels(gui.g); err != nil {
return err
}
if result == nil {
return nil
} else if result == gui.Errors.ErrSubProcess {
return result
} else if strings.Contains(result.Error(), "No changes - did you forget to use") {
return gui.genericMergeCommand("skip")
} else if strings.Contains(result.Error(), "When you have resolved this problem") || strings.Contains(result.Error(), "fix conflicts") || strings.Contains(result.Error(), "Resolve all conflicts manually") {
return gui.createConfirmationPanel(gui.g, gui.getFilesView(), gui.Tr.SLocalize("FoundConflictsTitle"), gui.Tr.SLocalize("FoundConflicts"),
func(g *gocui.Gui, v *gocui.View) error {
return nil
}, func(g *gocui.Gui, v *gocui.View) error {
return gui.genericMergeCommand("abort")
},
)
} else {
return gui.createErrorPanel(gui.g, result.Error())
}
}

View File

@@ -0,0 +1,76 @@
package gui
import (
"os"
"path/filepath"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type recentRepo struct {
path string
}
// GetDisplayStrings returns the path from a recent repo.
func (r *recentRepo) GetDisplayStrings(isFocused bool) []string {
yellow := color.New(color.FgMagenta)
base := filepath.Base(r.path)
path := yellow.Sprint(r.path)
return []string{base, path}
}
func (gui *Gui) handleCreateRecentReposMenu(g *gocui.Gui, v *gocui.View) error {
recentRepoPaths := gui.Config.GetAppState().RecentRepos
reposCount := utils.Min(len(recentRepoPaths), 20)
// we won't show the current repo hence the -1
recentRepos := make([]*recentRepo, reposCount-1)
for i, path := range recentRepoPaths[1:reposCount] {
recentRepos[i] = &recentRepo{path: path}
}
handleMenuPress := func(index int) error {
repo := recentRepos[index]
if err := os.Chdir(repo.path); err != nil {
return err
}
newGitCommand, err := commands.NewGitCommand(gui.Log, gui.OSCommand, gui.Tr, gui.Config)
if err != nil {
return err
}
gui.GitCommand = newGitCommand
return gui.Errors.ErrSwitchRepo
}
return gui.createMenu(gui.Tr.SLocalize("RecentRepos"), recentRepos, handleMenuPress)
}
// updateRecentRepoList registers the fact that we opened lazygit in this repo,
// so that we can open the same repo via the 'recent repos' menu
func (gui *Gui) updateRecentRepoList() error {
recentRepos := gui.Config.GetAppState().RecentRepos
currentRepo, err := os.Getwd()
if err != nil {
return err
}
known, recentRepos := newRecentReposList(recentRepos, currentRepo)
gui.Config.SetIsNewRepo(known)
gui.Config.GetAppState().RecentRepos = recentRepos
return gui.Config.SaveAppState()
}
// 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 isNew, newRepos
}

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

@@ -0,0 +1,222 @@
package gui
import (
"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 gui.createErrorPanel(gui.g, "No lines to stage")
}
if err := gui.focusLineAndHunk(); err != nil {
return err
}
mainView := gui.getMainView()
mainView.Highlight = true
mainView.Wrap = false
gui.g.Update(func(*gocui.Gui) error {
return gui.setViewContent(gui.g, gui.getMainView(), colorDiff)
})
return nil
}
func (gui *Gui) handleStagingEscape(g *gocui.Gui, v *gocui.View) error {
gui.State.Panels.Staging = nil
return gui.switchFocus(gui.g, nil, gui.getFilesView())
}
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.getMainView()
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(); err != nil {
return err
}
if err := gui.refreshStagingPanel(); err != nil {
return err
}
return nil
}

145
pkg/gui/stash_panel.go Normal file
View File

@@ -0,0 +1,145 @@
package gui
import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"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 {
if gui.popupPanelFocused() {
return nil
}
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
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 {
gui.State.StashEntries = gui.GitCommand.GetStashEntries()
gui.refreshSelectedLine(&gui.State.Panels.Stash.SelectedLine, len(gui.State.StashEntries))
isFocused := gui.g.CurrentView().Name() == "stash"
list, err := utils.RenderList(gui.State.StashEntries, isFocused)
if err != nil {
return err
}
v := gui.getStashView()
v.Clear()
fmt.Fprint(v, list)
if err := gui.resetOrigin(v); err != nil {
return err
}
return nil
})
return nil
}
func (gui *Gui) handleStashNextLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
panelState := gui.State.Panels.Stash
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.StashEntries), false)
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
return gui.handleStashEntrySelect(gui.g, v)
}
func (gui *Gui) handleStashPrevLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
panelState := gui.State.Panels.Stash
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.StashEntries), true)
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
return gui.handleStashEntrySelect(gui.g, v)
}
// specific functions
func (gui *Gui) handleStashApply(g *gocui.Gui, v *gocui.View) error {
return gui.stashDo(g, v, "apply")
}
func (gui *Gui) handleStashPop(g *gocui.Gui, v *gocui.View) error {
return gui.stashDo(g, v, "pop")
}
func (gui *Gui) handleStashDrop(g *gocui.Gui, v *gocui.View) error {
title := gui.Tr.SLocalize("StashDrop")
message := gui.Tr.SLocalize("SureDropStashEntry")
return gui.createConfirmationPanel(g, v, title, message, func(g *gocui.Gui, v *gocui.View) error {
return gui.stashDo(g, v, "drop")
}, nil)
}
func (gui *Gui) stashDo(g *gocui.Gui, v *gocui.View, method string) error {
stashEntry := gui.getSelectedStashEntry(v)
if stashEntry == nil {
errorMessage := gui.Tr.TemplateLocalize(
"NoStashTo",
Teml{
"method": method,
},
)
return gui.createErrorPanel(g, errorMessage)
}
if err := gui.GitCommand.StashDo(stashEntry.Index, method); err != nil {
gui.createErrorPanel(g, err.Error())
}
gui.refreshStashEntries(g)
return gui.refreshFiles()
}
func (gui *Gui) handleStashSave(g *gocui.Gui, filesView *gocui.View) error {
if len(gui.trackedFiles()) == 0 && len(gui.stagedFiles()) == 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoTrackedStagedFilesStash"))
}
gui.createPromptPanel(g, filesView, gui.Tr.SLocalize("StashChanges"), func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.StashSave(gui.trimmedContent(v)); err != nil {
gui.createErrorPanel(g, err.Error())
}
gui.refreshStashEntries(g)
return gui.refreshFiles()
})
return nil
}

114
pkg/gui/status_panel.go Normal file
View File

@@ -0,0 +1,114 @@
package gui
import (
"fmt"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) refreshStatus(g *gocui.Gui) error {
v, err := g.View("status")
if err != nil {
panic(err)
}
// for some reason if this isn't wrapped in an update the clear seems to
// be applied after the other things or something like that; the panel's
// contents end up cleared
g.Update(func(*gocui.Gui) error {
v.Clear()
pushables, pullables := gui.GitCommand.GetCurrentBranchUpstreamDifferenceCount()
fmt.Fprint(v, "↑"+pushables+"↓"+pullables)
branches := gui.State.Branches
if err := gui.updateWorkTreeState(); err != nil {
return err
}
if gui.State.WorkingTreeState != "normal" {
fmt.Fprint(v, utils.ColoredString(fmt.Sprintf(" (%s)", gui.State.WorkingTreeState), color.FgYellow))
}
if len(branches) == 0 {
return nil
}
branch := branches[0]
name := utils.ColoredString(branch.Name, branch.GetColor())
repo := utils.GetCurrentRepoName()
fmt.Fprint(v, " "+repo+" → "+name)
return nil
})
return nil
}
func (gui *Gui) handleCheckForUpdate(g *gocui.Gui, v *gocui.View) error {
gui.Updater.CheckForNewUpdate(gui.onUserUpdateCheckFinish, true)
return gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("CheckingForUpdates"))
}
func (gui *Gui) handleStatusSelect(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
magenta := color.New(color.FgMagenta)
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",
magenta.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 {
return gui.openFile(gui.Config.GetUserConfig().ConfigFileUsed())
}
func (gui *Gui) handleEditConfig(g *gocui.Gui, v *gocui.View) error {
filename := gui.Config.GetUserConfig().ConfigFileUsed()
return gui.editFile(filename)
}
func lazygitTitle() string {
return `
_ _ _
| | (_) |
| | __ _ _____ _ __ _ _| |_
| |/ _` + "`" + ` |_ / | | |/ _` + "`" + ` | | __|
| | (_| |/ /| |_| | (_| | | |_
|_|\__,_/___|\__, |\__, |_|\__|
__/ | __/ |
|___/ |___/ `
}
func (gui *Gui) updateWorkTreeState() error {
merging, err := gui.GitCommand.IsInMergeState()
if err != nil {
return err
}
if merging {
gui.State.WorkingTreeState = "merging"
return nil
}
rebaseMode, err := gui.GitCommand.RebaseMode()
if err != nil {
return err
}
if rebaseMode != "" {
gui.State.WorkingTreeState = "rebasing"
return nil
}
gui.State.WorkingTreeState = "normal"
return nil
}

54
pkg/gui/theme.go Normal file
View File

@@ -0,0 +1,54 @@
package gui
import (
"github.com/jesseduffield/gocui"
)
// GetAttribute gets the gocui color attribute from the string
func (gui *Gui) GetAttribute(key string) gocui.Attribute {
colorMap := map[string]gocui.Attribute{
"default": gocui.ColorDefault,
"black": gocui.ColorBlack,
"red": gocui.ColorRed,
"green": gocui.ColorGreen,
"yellow": gocui.ColorYellow,
"blue": gocui.ColorBlue,
"magenta": gocui.ColorMagenta,
"cyan": gocui.ColorCyan,
"white": gocui.ColorWhite,
"bold": gocui.AttrBold,
"reverse": gocui.AttrReverse,
"underline": gocui.AttrUnderline,
}
value, present := colorMap[key]
if present {
return value
}
return gocui.ColorWhite
}
// GetColor bitwise OR's a list of attributes obtained via the given keys
func (gui *Gui) GetColor(keys []string) gocui.Attribute {
var attribute gocui.Attribute
for _, key := range keys {
attribute = attribute | gui.GetAttribute(key)
}
return attribute
}
// GetOptionsPanelTextColor gets the color of the options panel text
func (gui *Gui) GetOptionsPanelTextColor() (gocui.Attribute, error) {
userConfig := gui.Config.GetUserConfig()
optionsColor := userConfig.GetStringSlice("gui.theme.optionsTextColor")
return gui.GetColor(optionsColor), nil
}
// SetColorScheme sets the color scheme for the app based on the user config
func (gui *Gui) SetColorScheme() error {
userConfig := gui.Config.GetUserConfig()
activeBorderColor := userConfig.GetStringSlice("gui.theme.activeBorderColor")
inactiveBorderColor := userConfig.GetStringSlice("gui.theme.inactiveBorderColor")
gui.g.FgColor = gui.GetColor(inactiveBorderColor)
gui.g.SelFgColor = gui.GetColor(activeBorderColor)
return nil
}

65
pkg/gui/updates.go Normal file
View File

@@ -0,0 +1,65 @@
package gui
import "github.com/jesseduffield/gocui"
func (gui *Gui) showUpdatePrompt(newVersion string) error {
title := "New version available!"
message := "Download latest version? (enter/esc)"
currentView := gui.g.CurrentView()
return gui.createConfirmationPanel(gui.g, currentView, title, message, func(g *gocui.Gui, v *gocui.View) error {
gui.startUpdating(newVersion)
return nil
}, nil)
}
func (gui *Gui) onUserUpdateCheckFinish(newVersion string, err error) error {
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
if newVersion == "" {
return gui.createErrorPanel(gui.g, "New version not found")
}
return gui.showUpdatePrompt(newVersion)
}
func (gui *Gui) onBackgroundUpdateCheckFinish(newVersion string, err error) error {
if err != nil {
// ignoring the error for now so that I'm not annoying users
gui.Log.Error(err.Error())
return nil
}
if newVersion == "" {
return nil
}
if gui.Config.GetUserConfig().Get("update.method") == "background" {
gui.startUpdating(newVersion)
return nil
}
return gui.showUpdatePrompt(newVersion)
}
func (gui *Gui) startUpdating(newVersion string) {
gui.State.Updating = true
gui.statusManager.addWaitingStatus("updating")
gui.Updater.Update(newVersion, gui.onUpdateFinish)
}
func (gui *Gui) onUpdateFinish(err error) error {
gui.State.Updating = false
gui.statusManager.removeStatus("updating")
if err := gui.renderString(gui.g, "appStatus", ""); err != nil {
return err
}
if err != nil {
return gui.createErrorPanel(gui.g, "Update failed: "+err.Error())
}
return nil
}
func (gui *Gui) createUpdateQuitConfirmation(g *gocui.Gui, v *gocui.View) error {
title := "Currently Updating"
message := "An update is in progress. Are you sure you want to quit?"
return gui.createConfirmationPanel(gui.g, v, title, message, func(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}, nil)
}

399
pkg/gui/view_helpers.go Normal file
View File

@@ -0,0 +1,399 @@
package gui
import (
"fmt"
"sort"
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/spkg/bom"
)
var cyclableViews = []string{"status", "files", "branches", "commits", "stash"}
func (gui *Gui) refreshSidePanels(g *gocui.Gui) error {
if err := gui.refreshBranches(g); err != nil {
return err
}
if err := gui.refreshFiles(); 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 {
var focusedViewName string
if v == nil || v.Name() == cyclableViews[len(cyclableViews)-1] {
focusedViewName = cyclableViews[0]
} else {
for i := range cyclableViews {
if v.Name() == cyclableViews[i] {
focusedViewName = cyclableViews[i+1]
break
}
if i == len(cyclableViews)-1 {
message := gui.Tr.TemplateLocalize(
"IssntListOfViews",
Teml{
"name": v.Name(),
},
)
gui.Log.Info(message)
return nil
}
}
}
focusedView, err := g.View(focusedViewName)
if err != nil {
panic(err)
}
return gui.switchFocus(g, v, focusedView)
}
func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error {
var focusedViewName string
if v == nil || v.Name() == cyclableViews[0] {
focusedViewName = cyclableViews[len(cyclableViews)-1]
} else {
for i := range cyclableViews {
if v.Name() == cyclableViews[i] {
focusedViewName = cyclableViews[i-1] // TODO: make this work properly
break
}
if i == len(cyclableViews)-1 {
message := gui.Tr.TemplateLocalize(
"IssntListOfViews",
Teml{
"name": v.Name(),
},
)
gui.Log.Info(message)
return nil
}
}
}
focusedView, err := g.View(focusedViewName)
if err != nil {
panic(err)
}
return gui.switchFocus(g, v, focusedView)
}
func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error {
switch v.Name() {
case "menu":
return gui.handleMenuSelect(g, v)
case "status":
return gui.handleStatusSelect(g, v)
case "files":
return gui.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":
if gui.State.Contexts["main"] == "merging" {
return gui.refreshMergePanel()
}
v.Highlight = false
return nil
default:
panic(gui.Tr.SLocalize("NoViewMachingNewLineFocusedSwitchStatement"))
}
}
func (gui *Gui) returnFocus(g *gocui.Gui, v *gocui.View) error {
previousView, err := g.View(gui.State.PreviousView)
if err != nil {
// always fall back to files view if there's no 'previous' view stored
previousView, err = g.View("files")
if err != nil {
gui.Log.Error(err)
}
}
return gui.switchFocus(g, v, previousView)
}
// pass in oldView = nil if you don't want to be able to return to your old view
// TODO: move some of this logic into our onFocusLost and onFocus hooks
func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error {
// we assume we'll never want to return focus to a confirmation panel i.e.
// we should never stack confirmation panels
if oldView != nil && oldView.Name() != "confirmation" {
// 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()
}
}
gui.Log.Info("setting highlight to true for view" + newView.Name())
message := gui.Tr.TemplateLocalize(
"newFocusedViewIs",
Teml{
"newFocusedView": newView.Name(),
},
)
gui.Log.Info(message)
if _, err := g.SetCurrentView(newView.Name()); err != nil {
return err
}
if _, err := g.SetViewOnTop(newView.Name()); err != nil {
return err
}
g.Cursor = newView.Editable
if err := gui.renderPanelOptions(); err != nil {
return err
}
return gui.newLineFocused(g, newView)
}
func (gui *Gui) resetOrigin(v *gocui.View) error {
if err := v.SetCursor(0, 0); err != nil {
return err
}
return v.SetOrigin(0, 0)
}
// if the cursor down past the last item, move it to the last line
func (gui *Gui) focusPoint(cx int, cy int, v *gocui.View) error {
if cy < 0 {
return nil
}
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)
if err != nil {
return nil // return gracefully if view has been deleted
}
if err := v.SetOrigin(0, 0); err != nil {
return err
}
return gui.setViewContent(gui.g, v, s)
})
return nil
}
func (gui *Gui) optionsMapToString(optionsMap map[string]string) string {
optionsArray := make([]string, 0)
for key, description := range optionsMap {
optionsArray = append(optionsArray, key+": "+description)
}
sort.Strings(optionsArray)
return strings.Join(optionsArray, ", ")
}
func (gui *Gui) renderOptionsMap(optionsMap map[string]string) error {
return gui.renderString(gui.g, "options", gui.optionsMapToString(optionsMap))
}
// TODO: refactor properly
// i'm so sorry but had to add this getBranchesView
func (gui *Gui) getFilesView() *gocui.View {
v, _ := gui.g.View("files")
return v
}
func (gui *Gui) getCommitsView() *gocui.View {
v, _ := gui.g.View("commits")
return v
}
func (gui *Gui) getCommitMessageView() *gocui.View {
v, _ := gui.g.View("commitMessage")
return v
}
func (gui *Gui) getBranchesView() *gocui.View {
v, _ := gui.g.View("branches")
return v
}
func (gui *Gui) getMainView() *gocui.View {
v, _ := gui.g.View("main")
return v
}
func (gui *Gui) getStashView() *gocui.View {
v, _ := gui.g.View("stash")
return v
}
func (gui *Gui) trimmedContent(v *gocui.View) string {
return strings.TrimSpace(v.Buffer())
}
func (gui *Gui) currentViewName() string {
currentView := gui.g.CurrentView()
return currentView.Name()
}
func (gui *Gui) resizeCurrentPopupPanel(g *gocui.Gui) error {
v := g.CurrentView()
if v.Name() == "commitMessage" || v.Name() == "credentials" || v.Name() == "confirmation" {
return gui.resizePopupPanel(g, v)
}
return nil
}
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, v.Wrap, content)
vx0, vy0, vx1, vy1 := v.Dimensions()
if vx0 == x0 && vy0 == y0 && vx1 == x1 && vy1 == y1 {
return nil
}
gui.Log.Info(gui.Tr.SLocalize("resizingPopupPanel"))
_, err := g.SetView(v.Name(), x0, y0, x1, y1, 0)
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 {
isFocused := gui.g.CurrentView().Name() == v.Name()
list, err := utils.RenderList(items, isFocused)
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":
if gui.State.Contexts["main"] == "merging" {
return gui.renderMergeOptions()
}
}
return gui.renderGlobalOptions()
}
func (gui *Gui) handleFocusView(g *gocui.Gui, v *gocui.View) error {
_, err := gui.g.SetCurrentView(v.Name())
return err
}
func (gui *Gui) popupPanelFocused() bool {
viewNames := []string{"commitMessage",
"credentials",
"menu"}
for _, viewName := range viewNames {
if gui.currentViewName() == viewName {
return true
}
}
return false
}

648
pkg/i18n/dutch.go Normal file
View File

@@ -0,0 +1,648 @@
package i18n
import (
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
)
// addDutch will add all dutch translations
func addDutch(i18nObject *i18n.Bundle) error {
// add the translations
return i18nObject.AddMessages(language.Dutch,
&i18n.Message{
ID: "NotEnoughSpace",
Other: "Niet genoeg ruimte om de panelen te renderen",
}, &i18n.Message{
ID: "DiffTitle",
Other: "Diff",
}, &i18n.Message{
ID: "LogTitle",
Other: "Log",
}, &i18n.Message{
ID: "FilesTitle",
Other: "Bestanden",
}, &i18n.Message{
ID: "BranchesTitle",
Other: "Branches",
}, &i18n.Message{
ID: "CommitsTitle",
Other: "Commits",
}, &i18n.Message{
ID: "StashTitle",
Other: "Stash",
}, &i18n.Message{
ID: "StagingMainTitle",
Other: `Stage Lines/Hunks`,
}, &i18n.Message{
ID: "MergingMainTitle",
Other: "Resolve merge conflicts",
}, &i18n.Message{
ID: "CommitMessage",
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",
}, &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",
}, &i18n.Message{
ID: "StatusTitle",
Other: "Status",
}, &i18n.Message{
ID: "GlobalTitle",
Other: "Global",
}, &i18n.Message{
ID: "navigate",
Other: "navigeer",
}, &i18n.Message{
ID: "menu",
Other: "menu",
}, &i18n.Message{
ID: "execute",
Other: "uitvoeren",
}, &i18n.Message{
ID: "stashFiles",
Other: "stash-bestanden",
}, &i18n.Message{
ID: "open",
Other: "open",
}, &i18n.Message{
ID: "ignore",
Other: "negeren",
}, &i18n.Message{
ID: "delete",
Other: "verwijderen",
}, &i18n.Message{
ID: "toggleStaged",
Other: "toggle staged",
}, &i18n.Message{
ID: "toggleStagedAll",
Other: "toggle staged alle",
}, &i18n.Message{
ID: "refresh",
Other: "verversen",
}, &i18n.Message{
ID: "push",
Other: "push",
}, &i18n.Message{
ID: "pull",
Other: "pull",
}, &i18n.Message{
ID: "addPatch",
Other: "bewerkingen toevoegen",
}, &i18n.Message{
ID: "edit",
Other: "bewerken",
}, &i18n.Message{
ID: "scroll",
Other: "scroll",
}, &i18n.Message{
ID: "abortMerge",
Other: "samenvoegen afbreken",
}, &i18n.Message{
ID: "resolveMergeConflicts",
Other: "los merge conflicten op",
}, &i18n.Message{
ID: "checkout",
Other: "uitchecken",
}, &i18n.Message{
ID: "NoChangedFiles",
Other: "Geen bestanden veranderd",
}, &i18n.Message{
ID: "FileHasNoUnstagedChanges",
Other: "Het bestand heeft geen unstaged veranderingen om toe te voegen",
}, &i18n.Message{
ID: "CannotGitAdd",
Other: "Kan commando niet uitvoeren git add --path untracked files",
}, &i18n.Message{
ID: "CantIgnoreTrackFiles",
Other: "Kan gevolgde bestanden niet negeren",
}, &i18n.Message{
ID: "NoStagedFilesToCommit",
Other: "Er zijn geen staged bestanden om te commiten",
}, &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: "Pullen...",
}, &i18n.Message{
ID: "PushWait",
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` 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 verwijderd)",
}, &i18n.Message{
ID: "AlreadyCheckedOutBranch",
Other: "Je hebt deze branch al uitgecheckt",
}, &i18n.Message{
ID: "SureForceCheckout",
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",
}, &i18n.Message{
ID: "BranchName",
Other: "Branch naam",
}, &i18n.Message{
ID: "NewBranchNameBranchOff",
Other: "Nieuw branch naam (Branch is afgeleid van {{.branchName}})",
}, &i18n.Message{
ID: "CantDeleteCheckOutBranch",
Other: "Je kan een uitgecheckte branch niet verwijderen!",
}, &i18n.Message{
ID: "DeleteBranch",
Other: "Verwijder branch",
}, &i18n.Message{
ID: "DeleteBranchMessage",
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?",
}, &i18n.Message{
ID: "rebaseBranch",
Other: "rebase branch",
}, &i18n.Message{
ID: "CantMergeBranchIntoItself",
Other: "Je kan niet een branch in zichzelf mergen",
}, &i18n.Message{
ID: "forceCheckout",
Other: "forceer checkout",
}, &i18n.Message{
ID: "merge",
Other: "samenvoegen",
}, &i18n.Message{
ID: "checkoutByName",
Other: "uitchecken bij naam",
}, &i18n.Message{
ID: "newBranch",
Other: "nieuwe branch",
}, &i18n.Message{
ID: "deleteBranch",
Other: "verwijder branch",
}, &i18n.Message{
ID: "forceDeleteBranch",
Other: "verwijder branch (forceer)",
}, &i18n.Message{
ID: "NoBranchesThisRepo",
Other: "Geen branches voor deze repo",
}, &i18n.Message{
ID: "NoTrackingThisBranch",
Other: "deze branch wordt niet gevolgd",
}, &i18n.Message{
ID: "CommitWithoutMessageErr",
Other: "Je kan geen commit maken zonder commit bericht",
}, &i18n.Message{
ID: "CloseConfirm",
Other: "{{.keyBindClose}}: Sluiten, {{.keyBindConfirm}}: Bevestigen",
}, &i18n.Message{
ID: "close",
Other: "sluiten",
}, &i18n.Message{
ID: "SureResetThisCommit",
Other: "Weet je het zeker dat je wil resetten naar deze commit?",
}, &i18n.Message{
ID: "ResetToCommit",
Other: "Reset Naar Commit",
}, &i18n.Message{
ID: "squashDown",
Other: "squash beneden",
}, &i18n.Message{
ID: "rename",
Other: "hernoemen",
}, &i18n.Message{
ID: "resetToThisCommit",
Other: "reset naar deze commit",
}, &i18n.Message{
ID: "fixupCommit",
Other: "Fixup commit",
}, &i18n.Message{
ID: "NoCommitsThisBranch",
Other: "Er zijn geen commits voor deze branch",
}, &i18n.Message{
ID: "OnlySquashTopmostCommit",
Other: "Kan alleen bovenste commit squashen",
}, &i18n.Message{
ID: "YouNoCommitsToSquash",
Other: "Je hebt geen commits om mee te squashen",
}, &i18n.Message{
ID: "CantFixupWhileUnstagedChanges",
Other: "Kan geen Fixup uitvoeren op unstaged veranderingen",
}, &i18n.Message{
ID: "Fixup",
Other: "Fixup",
}, &i18n.Message{
ID: "SureFixupThisCommit",
Other: "Weet je zeker dat je fixup wil uitvoeren op deze commit? De commit hieronder zol worden squashed in deze",
}, &i18n.Message{
ID: "OnlyRenameTopCommit",
Other: "Je kan alleen de bovenste commit hernoemen",
}, &i18n.Message{
ID: "renameCommit",
Other: "hernoem commit",
}, &i18n.Message{
ID: "renameCommitEditor",
Other: "rename commit with editor",
}, &i18n.Message{
ID: "PotentialErrInGetselectedCommit",
Other: "Er is mogelijk een error in getSelected Commit (geen match tussen ui en state)",
}, &i18n.Message{
ID: "Error",
Other: "Foutmelding",
}, &i18n.Message{
ID: "resizingPopupPanel",
Other: "resizen popup paneel",
}, &i18n.Message{
ID: "RunningSubprocess",
Other: "subprocess lopend",
}, &i18n.Message{
ID: "selectHunk",
Other: "selecteer stuk",
}, &i18n.Message{
ID: "navigateConflicts",
Other: "navigeer conflicts",
}, &i18n.Message{
ID: "pickHunk",
Other: "kies stuk",
}, &i18n.Message{
ID: "pickBothHunks",
Other: "kies beide stukken",
}, &i18n.Message{
ID: "undo",
Other: "ongedaan maken",
}, &i18n.Message{
ID: "pop",
Other: "pop",
}, &i18n.Message{
ID: "drop",
Other: "drop",
}, &i18n.Message{
ID: "apply",
Other: "toepassen",
}, &i18n.Message{
ID: "NoStashEntries",
Other: "Geen stash items",
}, &i18n.Message{
ID: "StashDrop",
Other: "Stash drop",
}, &i18n.Message{
ID: "SureDropStashEntry",
Other: "Weet je het zeker dat je deze stash entry wil laten vallen?",
}, &i18n.Message{
ID: "NoStashTo",
Other: "Geen stash voor {{.method}}",
}, &i18n.Message{
ID: "NoTrackedStagedFilesStash",
Other: "Je hebt geen tracked/staged bestanden om te laten stashen",
}, &i18n.Message{
ID: "StashChanges",
Other: "Stash veranderingen",
}, &i18n.Message{
ID: "IssntListOfViews",
Other: "{{.name}} is niet in de lijst van weergaves",
}, &i18n.Message{
ID: "NoViewMachingNewLineFocusedSwitchStatement",
Other: "Er machen geen weergave met de newLineFocused switch declaratie",
}, &i18n.Message{
ID: "newFocusedViewIs",
Other: "nieuw gefocussed weergave is {{.newFocusedView}}",
}, &i18n.Message{
ID: "CantCloseConfirmationPrompt",
Other: "Kon de bevestiging prompt niet sluiten: {{.error}}",
}, &i18n.Message{
ID: "ClearFilePanel",
Other: "maak bestandsvenster leeg",
}, &i18n.Message{
ID: "MergeAborted",
Other: "Merge afgebroken",
}, &i18n.Message{
ID: "OpenConfig",
Other: "open config file",
}, &i18n.Message{
ID: "EditConfig",
Other: "verander config file",
}, &i18n.Message{
ID: "ForcePush",
Other: "Forceer push",
}, &i18n.Message{
ID: "ForcePushPrompt",
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: "zoeken naar updates...",
}, &i18n.Message{
ID: "OnLatestVersionErr",
Other: "Je hebt al de laatste versie",
}, &i18n.Message{
ID: "MajorVersionErr",
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 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)`,
}, &i18n.Message{
ID: "editFile",
Other: `verander bestand`,
}, &i18n.Message{
ID: "openFile",
Other: `open bestand`,
}, &i18n.Message{
ID: "ignoreFile",
Other: `voeg toe aan .gitignore`,
}, &i18n.Message{
ID: "refreshFiles",
Other: `refresh bestanden`,
}, &i18n.Message{
ID: "resetHard",
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: `Stage Lines/Hunks`,
}, &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`,
}, &i18n.Message{
ID: "RebasingTitle",
Other: "Rebasing",
}, &i18n.Message{
ID: "MergingTitle",
Other: "Merging",
}, &i18n.Message{
ID: "ConfirmRebase",
Other: "Are you sure you want to rebase {{.checkedOutBranch}} onto {{.selectedBranch}}?",
}, &i18n.Message{
ID: "ConfirmMerge",
Other: "Are you sure you want to merge {{.selectedBranch}} into {{.checkedOutBranch}}?",
}, &i18n.Message{
ID: "FwdNoUpstream",
Other: "Cannot fast-forward a branch with no upstream",
}, &i18n.Message{
ID: "ErrorOccurred",
Other: "An error occurred! Please create an issue at https://github.com/jesseduffield/lazygit/issues",
}, &i18n.Message{
ID: "FwdCommitsToPush",
Other: "Cannot fast-forward a branch with commits to push",
}, &i18n.Message{
ID: "MainTitle",
Other: "Main",
}, &i18n.Message{
ID: "NormalTitle",
Other: "Normal",
}, &i18n.Message{
ID: "softReset",
Other: "soft reset to last commit",
}, &i18n.Message{
ID: "SoftReset",
Other: "Soft reset",
}, &i18n.Message{
ID: "ConfirmSoftReset",
Other: "Are you sure you want to `reset --soft HEAD^`? The changes in your topmost commit will be placed in your working tree",
}, &i18n.Message{
ID: "CantRebaseOntoSelf",
Other: "You cannot rebase a branch onto itself",
}, &i18n.Message{
ID: "SureSquashThisCommit",
Other: "Are you sure you want to squash this commit into the commit below?",
}, &i18n.Message{
ID: "Squash",
Other: "Squash",
}, &i18n.Message{
ID: "pickCommit",
Other: "pick commit (when mid-rebase)",
}, &i18n.Message{
ID: "revertCommit",
Other: "revert commit",
}, &i18n.Message{
ID: "deleteCommit",
Other: "delete commit",
}, &i18n.Message{
ID: "moveDownCommit",
Other: "move commit down one",
}, &i18n.Message{
ID: "moveUpCommit",
Other: "move commit up one",
}, &i18n.Message{
ID: "editCommit",
Other: "edit commit",
}, &i18n.Message{
ID: "amendToCommit",
Other: "amend commit with staged changes",
}, &i18n.Message{
ID: "FoundConflicts",
Other: "Damn, conflicts! To abort press 'esc', otherwise press 'enter'",
}, &i18n.Message{
ID: "FoundConflictsTitle",
Other: "Auto-merge failed",
}, &i18n.Message{
ID: "Undo",
Other: "undo",
}, &i18n.Message{
ID: "PickHunk",
Other: "pick hunk",
}, &i18n.Message{
ID: "PickBothHunks",
Other: "pick both hunks",
}, &i18n.Message{
ID: "ViewMergeRebaseOptions",
Other: "view merge/rebase options",
}, &i18n.Message{
ID: "NotMergingOrRebasing",
Other: "You are currently neither rebasing nor merging",
}, &i18n.Message{
ID: "RecentRepos",
Other: "recent repositories",
}, &i18n.Message{
ID: "MergeOptionsTitle",
Other: "Merge Options",
}, &i18n.Message{
ID: "RebaseOptionsTitle",
Other: "Rebase Options",
}, &i18n.Message{
ID: "ConflictsResolved",
Other: "all merge conflicts resolved. Continue?",
}, &i18n.Message{
ID: "NoRoom",
Other: "Not enough room",
}, &i18n.Message{
ID: "YouAreHere",
Other: "YOU ARE HERE",
}, &i18n.Message{
ID: "rewordNotSupported",
Other: "rewording commits while interactively rebasing is not currently supported",
}, &i18n.Message{
ID: "cherryPickCopy",
Other: "copy commit (cherry-pick)",
}, &i18n.Message{
ID: "cherryPickCopyRange",
Other: "copy commit range (cherry-pick)",
}, &i18n.Message{
ID: "pasteCommits",
Other: "paste commits (cherry-pick)",
}, &i18n.Message{
ID: "SureCherryPick",
Other: "Are you sure you want to cherry-pick the copied commits onto this branch?",
}, &i18n.Message{
ID: "CherryPick",
Other: "Cherry-Pick",
}, &i18n.Message{
ID: "CannotRebaseOntoFirstCommit",
Other: "You cannot interactive rebase onto the first commit",
}, &i18n.Message{
ID: "Donate",
Other: "Donate",
}, &i18n.Message{
ID: "PrevLine",
Other: "select previous line",
}, &i18n.Message{
ID: "NextLine",
Other: "select next line",
}, &i18n.Message{
ID: "PrevHunk",
Other: "select previous hunk",
}, &i18n.Message{
ID: "NextHunk",
Other: "select next hunk",
}, &i18n.Message{
ID: "PrevConflict",
Other: "select previous conflict",
}, &i18n.Message{
ID: "NextConflict",
Other: "select next conflict",
}, &i18n.Message{
ID: "SelectTop",
Other: "select top hunk",
}, &i18n.Message{
ID: "SelectBottom",
Other: "select bottom hunk",
}, &i18n.Message{
ID: "ScrollDown",
Other: "scroll down",
}, &i18n.Message{
ID: "ScrollUp",
Other: "scroll up",
}, &i18n.Message{
ID: "AmendCommitTitle",
Other: "Amend Commit",
}, &i18n.Message{
ID: "AmendCommitPrompt",
Other: "Are you sure you want to amend this commit with your staged files?",
}, &i18n.Message{
ID: "DeleteCommitTitle",
Other: "Delete Commit",
}, &i18n.Message{
ID: "DeleteCommitPrompt",
Other: "Are you sure you want to delete this commit?",
}, &i18n.Message{
ID: "SquashingStatus",
Other: "squashing",
}, &i18n.Message{
ID: "FixingStatus",
Other: "fixing up",
}, &i18n.Message{
ID: "DeletingStatus",
Other: "deleting",
}, &i18n.Message{
ID: "MovingStatus",
Other: "moving",
}, &i18n.Message{
ID: "RebasingStatus",
Other: "rebasing",
}, &i18n.Message{
ID: "AmendingStatus",
Other: "amending",
}, &i18n.Message{
ID: "CherryPickingStatus",
Other: "cherry-picking",
},
)
}

671
pkg/i18n/english.go Normal file
View File

@@ -0,0 +1,671 @@
/*
Todo list when making a new translation
- Copy this file and rename it to the language you want to translate to like someLanguage.go
- Change the addEnglish() name to the language you want to translate to like addSomeLanguage()
- change the first function argument of i18nObject.AddMessages( to the language you want to translate to like language.SomeLanguage
- Remove this todo and the about section
*/
package i18n
import (
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
)
func addEnglish(i18nObject *i18n.Bundle) error {
return i18nObject.AddMessages(language.English,
&i18n.Message{
ID: "NotEnoughSpace",
Other: "Not enough space to render panels",
}, &i18n.Message{
ID: "DiffTitle",
Other: "Diff",
}, &i18n.Message{
ID: "LogTitle",
Other: "Log",
}, &i18n.Message{
ID: "FilesTitle",
Other: "Files",
}, &i18n.Message{
ID: "BranchesTitle",
Other: "Branches",
}, &i18n.Message{
ID: "CommitsTitle",
Other: "Commits",
}, &i18n.Message{
ID: "StashTitle",
Other: "Stash",
}, &i18n.Message{
ID: "StagingMainTitle",
Other: `Stage Lines/Hunks`,
}, &i18n.Message{
ID: "MergingMainTitle",
Other: "Resolve merge conflicts",
}, &i18n.Message{
ID: "MainTitle",
Other: "Main",
}, &i18n.Message{
ID: "StagingTitle",
Other: "Staging",
}, &i18n.Message{
ID: "MergingTitle",
Other: "Merging",
}, &i18n.Message{
ID: "NormalTitle",
Other: "Normal",
}, &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? Afterwards, you can change commit message from the commits panel.",
}, &i18n.Message{
ID: "NoCommitToAmend",
Other: "There's no commit to amend.",
}, &i18n.Message{
ID: "CommitChangesWithEditor",
Other: "commit changes using git editor",
}, &i18n.Message{
ID: "StatusTitle",
Other: "Status",
}, &i18n.Message{
ID: "GlobalTitle",
Other: "Global",
}, &i18n.Message{
ID: "navigate",
Other: "navigate",
}, &i18n.Message{
ID: "menu",
Other: "menu",
}, &i18n.Message{
ID: "execute",
Other: "execute",
}, &i18n.Message{
ID: "stashFiles",
Other: "stash files",
}, &i18n.Message{
ID: "softReset",
Other: "soft reset to last commit",
}, &i18n.Message{
ID: "open",
Other: "open",
}, &i18n.Message{
ID: "ignore",
Other: "ignore",
}, &i18n.Message{
ID: "delete",
Other: "delete",
}, &i18n.Message{
ID: "toggleStaged",
Other: "toggle staged",
}, &i18n.Message{
ID: "toggleStagedAll",
Other: "stage/unstage all",
}, &i18n.Message{
ID: "refresh",
Other: "refresh",
}, &i18n.Message{
ID: "push",
Other: "push",
}, &i18n.Message{
ID: "pull",
Other: "pull",
}, &i18n.Message{
ID: "addPatch",
Other: "add patch",
}, &i18n.Message{
ID: "edit",
Other: "edit",
}, &i18n.Message{
ID: "scroll",
Other: "scroll",
}, &i18n.Message{
ID: "abortMerge",
Other: "abort merge",
}, &i18n.Message{
ID: "resolveMergeConflicts",
Other: "resolve merge conflicts",
}, &i18n.Message{
ID: "checkout",
Other: "checkout",
}, &i18n.Message{
ID: "NoChangedFiles",
Other: "No changed files",
}, &i18n.Message{
ID: "FileHasNoUnstagedChanges",
Other: "File has no unstaged changes to add",
}, &i18n.Message{
ID: "CannotGitAdd",
Other: "Cannot git add --patch untracked files",
}, &i18n.Message{
ID: "CantIgnoreTrackFiles",
Other: "Cannot ignore tracked files",
}, &i18n.Message{
ID: "NoStagedFilesToCommit",
Other: "There are no staged files to commit",
}, &i18n.Message{
ID: "NoFilesDisplay",
Other: "No file to display",
}, &i18n.Message{
ID: "NotAFile",
Other: "Not a file",
}, &i18n.Message{
ID: "PullWait",
Other: "Pulling...",
}, &i18n.Message{
ID: "PushWait",
Other: "Pushing...",
}, &i18n.Message{
ID: "FetchWait",
Other: "Fetching...",
}, &i18n.Message{
ID: "FileNoMergeCons",
Other: "This file has no inline merge conflicts",
}, &i18n.Message{
ID: "SureResetHardHead",
Other: "Are you sure you want to `reset --hard HEAD` and `clean -fd`? You may lose changes",
}, &i18n.Message{
ID: "SoftReset",
Other: "Soft reset",
}, &i18n.Message{
ID: "ConfirmSoftReset",
Other: "Are you sure you want to `reset --soft HEAD^`? The changes in your topmost commit will be placed in your working tree",
}, &i18n.Message{
ID: "SureTo",
Other: "Are you sure you want to {{.deleteVerb}} {{.fileName}} (you will lose your changes)?",
}, &i18n.Message{
ID: "AlreadyCheckedOutBranch",
Other: "You have already checked out this branch",
}, &i18n.Message{
ID: "SureForceCheckout",
Other: "Are you sure you want force checkout? You will lose all local changes",
}, &i18n.Message{
ID: "ForceCheckoutBranch",
Other: "Force Checkout Branch",
}, &i18n.Message{
ID: "BranchName",
Other: "Branch name",
}, &i18n.Message{
ID: "NewBranchNameBranchOff",
Other: "New Branch Name (Branch is off of {{.branchName}})",
}, &i18n.Message{
ID: "CantDeleteCheckOutBranch",
Other: "You cannot delete the checked out branch!",
}, &i18n.Message{
ID: "DeleteBranch",
Other: "Delete Branch",
}, &i18n.Message{
ID: "DeleteBranchMessage",
Other: "Are you sure you want to delete the branch {{.selectedBranchName}}?",
}, &i18n.Message{
ID: "ForceDeleteBranchMessage",
Other: "{{.selectedBranchName}} is not fully merged. Are you sure you want to delete it?",
}, &i18n.Message{
ID: "rebaseBranch",
Other: "rebase branch",
}, &i18n.Message{
ID: "CantRebaseOntoSelf",
Other: "You cannot rebase a branch onto itself",
}, &i18n.Message{
ID: "CantMergeBranchIntoItself",
Other: "You cannot merge a branch into itself",
}, &i18n.Message{
ID: "forceCheckout",
Other: "force checkout",
}, &i18n.Message{
ID: "merge",
Other: "merge",
}, &i18n.Message{
ID: "checkoutByName",
Other: "checkout by name",
}, &i18n.Message{
ID: "newBranch",
Other: "new branch",
}, &i18n.Message{
ID: "deleteBranch",
Other: "delete branch",
}, &i18n.Message{
ID: "forceDeleteBranch",
Other: "delete branch (force)",
}, &i18n.Message{
ID: "NoBranchesThisRepo",
Other: "No branches for this repo",
}, &i18n.Message{
ID: "NoTrackingThisBranch",
Other: "There is no tracking for this branch",
}, &i18n.Message{
ID: "CommitWithoutMessageErr",
Other: "You cannot commit without a commit message",
}, &i18n.Message{
ID: "CloseConfirm",
Other: "{{.keyBindClose}}: close, {{.keyBindConfirm}}: confirm",
}, &i18n.Message{
ID: "close",
Other: "close",
}, &i18n.Message{
ID: "SureResetThisCommit",
Other: "Are you sure you want to reset to this commit?",
}, &i18n.Message{
ID: "ResetToCommit",
Other: "Reset To Commit",
}, &i18n.Message{
ID: "squashDown",
Other: "squash down",
}, &i18n.Message{
ID: "rename",
Other: "rename",
}, &i18n.Message{
ID: "resetToThisCommit",
Other: "reset to this commit",
}, &i18n.Message{
ID: "fixupCommit",
Other: "fixup commit",
}, &i18n.Message{
ID: "NoCommitsThisBranch",
Other: "No commits for this branch",
}, &i18n.Message{
ID: "OnlySquashTopmostCommit",
Other: "Can only squash topmost commit",
}, &i18n.Message{
ID: "YouNoCommitsToSquash",
Other: "You have no commits to squash with",
}, &i18n.Message{
ID: "CantFixupWhileUnstagedChanges",
Other: "Can't fixup while there are unstaged changes",
}, &i18n.Message{
ID: "Fixup",
Other: "Fixup",
}, &i18n.Message{
ID: "SureFixupThisCommit",
Other: "Are you sure you want to 'fixup' this commit? It will be merged into the commit below",
}, &i18n.Message{
ID: "SureSquashThisCommit",
Other: "Are you sure you want to squash this commit into the commit below?",
}, &i18n.Message{
ID: "Squash",
Other: "Squash",
}, &i18n.Message{
ID: "pickCommit",
Other: "pick commit (when mid-rebase)",
}, &i18n.Message{
ID: "revertCommit",
Other: "revert commit",
}, &i18n.Message{
ID: "OnlyRenameTopCommit",
Other: "Can only reword topmost commit from within lazygit. Use shift+R instead",
}, &i18n.Message{
ID: "renameCommit",
Other: "reword commit",
}, &i18n.Message{
ID: "deleteCommit",
Other: "delete commit",
}, &i18n.Message{
ID: "moveDownCommit",
Other: "move commit down one",
}, &i18n.Message{
ID: "moveUpCommit",
Other: "move commit up one",
}, &i18n.Message{
ID: "editCommit",
Other: "edit commit",
}, &i18n.Message{
ID: "amendToCommit",
Other: "amend commit with staged changes",
}, &i18n.Message{
ID: "renameCommitEditor",
Other: "rename commit with editor",
}, &i18n.Message{
ID: "PotentialErrInGetselectedCommit",
Other: "potential error in getSelected Commit (mismatched ui and state)",
}, &i18n.Message{
ID: "NoCommitsThisBranch",
Other: "No commits for this branch",
}, &i18n.Message{
ID: "Error",
Other: "Error",
}, &i18n.Message{
ID: "resizingPopupPanel",
Other: "resizing popup panel",
}, &i18n.Message{
ID: "RunningSubprocess",
Other: "running subprocess",
}, &i18n.Message{
ID: "selectHunk",
Other: "select hunk",
}, &i18n.Message{
ID: "navigateConflicts",
Other: "navigate conflicts",
}, &i18n.Message{
ID: "pickHunk",
Other: "pick hunk",
}, &i18n.Message{
ID: "pickBothHunks",
Other: "pick both hunks",
}, &i18n.Message{
ID: "undo",
Other: "undo",
}, &i18n.Message{
ID: "pop",
Other: "pop",
}, &i18n.Message{
ID: "drop",
Other: "drop",
}, &i18n.Message{
ID: "apply",
Other: "apply",
}, &i18n.Message{
ID: "NoStashEntries",
Other: "No stash entries",
}, &i18n.Message{
ID: "StashDrop",
Other: "Stash drop",
}, &i18n.Message{
ID: "SureDropStashEntry",
Other: "Are you sure you want to drop this stash entry?",
}, &i18n.Message{
ID: "NoStashTo",
Other: "No stash to {{.method}}",
}, &i18n.Message{
ID: "NoTrackedStagedFilesStash",
Other: "You have no tracked/staged files to stash",
}, &i18n.Message{
ID: "StashChanges",
Other: "Stash changes",
}, &i18n.Message{
ID: "IssntListOfViews",
Other: "{{.name}} is not in the list of views",
}, &i18n.Message{
ID: "NoViewMachingNewLineFocusedSwitchStatement",
Other: "No view matching newLineFocused switch statement",
}, &i18n.Message{
ID: "newFocusedViewIs",
Other: "new focused view is {{.newFocusedView}}",
}, &i18n.Message{
ID: "CantCloseConfirmationPrompt",
Other: "Could not close confirmation prompt: {{.error}}",
}, &i18n.Message{
ID: "NoChangedFiles",
Other: "No changed files",
}, &i18n.Message{
ID: "ClearFilePanel",
Other: "Clear file panel",
}, &i18n.Message{
ID: "MergeAborted",
Other: "Merge aborted",
}, &i18n.Message{
ID: "OpenConfig",
Other: "open config file",
}, &i18n.Message{
ID: "EditConfig",
Other: "edit config file",
}, &i18n.Message{
ID: "ForcePush",
Other: "Force push",
}, &i18n.Message{
ID: "ForcePushPrompt",
Other: "Your branch has diverged from the remote branch. Press 'esc' to cancel, or 'enter' to force push.",
}, &i18n.Message{
ID: "checkForUpdate",
Other: "check for update",
}, &i18n.Message{
ID: "CheckingForUpdates",
Other: "Checking for updates...",
}, &i18n.Message{
ID: "OnLatestVersionErr",
Other: "You already have the latest version",
}, &i18n.Message{
ID: "MajorVersionErr",
Other: "New version ({{.newVersion}}) has non-backwards compatible changes compared to the current version ({{.currentVersion}})",
}, &i18n.Message{
ID: "CouldNotFindBinaryErr",
Other: "Could not find any binary at {{.url}}",
}, &i18n.Message{
ID: "AnonymousReportingTitle",
Other: "Help make lazygit better",
}, &i18n.Message{
ID: "AnonymousReportingPrompt",
Other: "Would you like to enable anonymous reporting data to help improve lazygit? (enter/esc)",
}, &i18n.Message{
ID: "GitconfigParseErr",
Other: `Gogit failed to parse your gitconfig file due to the presence of unquoted '\' characters. Removing these should fix the issue.`,
}, &i18n.Message{
ID: "removeFile",
Other: `delete if untracked / checkout if tracked`,
}, &i18n.Message{
ID: "editFile",
Other: `edit file`,
}, &i18n.Message{
ID: "openFile",
Other: `open file`,
}, &i18n.Message{
ID: "ignoreFile",
Other: `add to .gitignore`,
}, &i18n.Message{
ID: "refreshFiles",
Other: `refresh files`,
}, &i18n.Message{
ID: "resetHard",
Other: `reset hard and remove untracked files`,
}, &i18n.Message{
ID: "mergeIntoCurrentBranch",
Other: `merge into currently checked out branch`,
}, &i18n.Message{
ID: "ConfirmQuit",
Other: `Are you sure you want to quit?`,
}, &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: "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}} ...",
}, &i18n.Message{
ID: "FoundConflicts",
Other: "Damn, conflicts! To abort press 'esc', otherwise press 'enter'",
}, &i18n.Message{
ID: "FoundConflictsTitle",
Other: "Auto-merge failed",
}, &i18n.Message{
ID: "Undo",
Other: "undo",
}, &i18n.Message{
ID: "PickHunk",
Other: "pick hunk",
}, &i18n.Message{
ID: "PickBothHunks",
Other: "pick both hunks",
}, &i18n.Message{
ID: "ViewMergeRebaseOptions",
Other: "view merge/rebase options",
}, &i18n.Message{
ID: "NotMergingOrRebasing",
Other: "You are currently neither rebasing nor merging",
}, &i18n.Message{
ID: "RecentRepos",
Other: "recent repositories",
}, &i18n.Message{
ID: "MergeOptionsTitle",
Other: "Merge Options",
}, &i18n.Message{
ID: "RebaseOptionsTitle",
Other: "Rebase Options",
}, &i18n.Message{
ID: "ConflictsResolved",
Other: "all merge conflicts resolved. Continue?",
}, &i18n.Message{
ID: "RebasingTitle",
Other: "Rebasing",
}, &i18n.Message{
ID: "MergingTitle",
Other: "Merging",
}, &i18n.Message{
ID: "ConfirmRebase",
Other: "Are you sure you want to rebase {{.checkedOutBranch}} onto {{.selectedBranch}}?",
}, &i18n.Message{
ID: "ConfirmMerge",
Other: "Are you sure you want to merge {{.selectedBranch}} into {{.checkedOutBranch}}?",
}, &i18n.Message{}, &i18n.Message{
ID: "FwdNoUpstream",
Other: "Cannot fast-forward a branch with no upstream",
}, &i18n.Message{
ID: "FwdCommitsToPush",
Other: "Cannot fast-forward a branch with commits to push",
}, &i18n.Message{
ID: "ErrorOccurred",
Other: "An error occurred! Please create an issue at https://github.com/jesseduffield/lazygit/issues",
}, &i18n.Message{
ID: "NoRoom",
Other: "Not enough room",
}, &i18n.Message{
ID: "YouAreHere",
Other: "YOU ARE HERE",
}, &i18n.Message{
ID: "rewordNotSupported",
Other: "rewording commits while interactively rebasing is not currently supported",
}, &i18n.Message{
ID: "cherryPickCopy",
Other: "copy commit (cherry-pick)",
}, &i18n.Message{
ID: "cherryPickCopyRange",
Other: "copy commit range (cherry-pick)",
}, &i18n.Message{
ID: "pasteCommits",
Other: "paste commits (cherry-pick)",
}, &i18n.Message{
ID: "SureCherryPick",
Other: "Are you sure you want to cherry-pick the copied commits onto this branch?",
}, &i18n.Message{
ID: "CherryPick",
Other: "Cherry-Pick",
}, &i18n.Message{
ID: "CannotRebaseOntoFirstCommit",
Other: "You cannot interactive rebase onto the first commit",
}, &i18n.Message{
ID: "Donate",
Other: "Donate",
}, &i18n.Message{
ID: "PrevLine",
Other: "select previous line",
}, &i18n.Message{
ID: "NextLine",
Other: "select next line",
}, &i18n.Message{
ID: "PrevHunk",
Other: "select previous hunk",
}, &i18n.Message{
ID: "NextHunk",
Other: "select next hunk",
}, &i18n.Message{
ID: "PrevConflict",
Other: "select previous conflict",
}, &i18n.Message{
ID: "NextConflict",
Other: "select next conflict",
}, &i18n.Message{
ID: "SelectTop",
Other: "select top hunk",
}, &i18n.Message{
ID: "SelectBottom",
Other: "select bottom hunk",
}, &i18n.Message{
ID: "ScrollDown",
Other: "scroll down",
}, &i18n.Message{
ID: "ScrollUp",
Other: "scroll up",
}, &i18n.Message{
ID: "AmendCommitTitle",
Other: "Amend Commit",
}, &i18n.Message{
ID: "AmendCommitPrompt",
Other: "Are you sure you want to amend this commit with your staged files?",
}, &i18n.Message{
ID: "DeleteCommitTitle",
Other: "Delete Commit",
}, &i18n.Message{
ID: "DeleteCommitPrompt",
Other: "Are you sure you want to delete this commit?",
}, &i18n.Message{
ID: "SquashingStatus",
Other: "squashing",
}, &i18n.Message{
ID: "FixingStatus",
Other: "fixing up",
}, &i18n.Message{
ID: "DeletingStatus",
Other: "deleting",
}, &i18n.Message{
ID: "MovingStatus",
Other: "moving",
}, &i18n.Message{
ID: "RebasingStatus",
Other: "rebasing",
}, &i18n.Message{
ID: "AmendingStatus",
Other: "amending",
}, &i18n.Message{
ID: "CherryPickingStatus",
Other: "cherry-picking",
},
)
}

102
pkg/i18n/i18n.go Normal file
View File

@@ -0,0 +1,102 @@
package i18n
import (
"github.com/cloudfoundry/jibber_jabber"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/sirupsen/logrus"
"golang.org/x/text/language"
)
// Teml is short for template used to make the required map[string]interface{} shorter when using gui.Tr.SLocalize and gui.Tr.TemplateLocalize
type Teml map[string]interface{}
// Localizer will translate a message into the user's language
type Localizer struct {
i18nLocalizer *i18n.Localizer
language string
Log *logrus.Entry
}
// NewLocalizer creates a new Localizer
func NewLocalizer(log *logrus.Entry) *Localizer {
userLang := detectLanguage(jibber_jabber.DetectLanguage)
log.Info("language: " + userLang)
return setupLocalizer(log, userLang)
}
// Localize handels the translations
// expects i18n.LocalizeConfig as input: https://godoc.org/github.com/nicksnyder/go-i18n/v2/i18n#Localizer.MustLocalize
// output: translated string
func (l *Localizer) Localize(config *i18n.LocalizeConfig) string {
return l.i18nLocalizer.MustLocalize(config)
}
// SLocalize (short localize) is for 1 line localizations
// ID: The id that is used in the .toml translation files
// Other: the default message it needs to return if there is no translation found or the system is english
func (l *Localizer) SLocalize(ID string) string {
return l.Localize(&i18n.LocalizeConfig{
DefaultMessage: &i18n.Message{
ID: ID,
},
})
}
// TemplateLocalize allows the Other input to be dynamic
func (l *Localizer) TemplateLocalize(ID string, TemplateData map[string]interface{}) string {
return l.Localize(&i18n.LocalizeConfig{
DefaultMessage: &i18n.Message{
ID: ID,
},
TemplateData: TemplateData,
})
}
// GetLanguage returns the currently selected language, e.g 'en'
func (l *Localizer) GetLanguage() string {
return l.language
}
// add translation file(s)
func addBundles(log *logrus.Entry, i18nBundle *i18n.Bundle) {
fs := []func(*i18n.Bundle) error{
addPolish,
addDutch,
addEnglish,
}
for _, f := range fs {
if err := f(i18nBundle); err != nil {
log.Fatal(err)
}
}
}
// detectLanguage extracts user language from environment
func detectLanguage(langDetector func() (string, error)) string {
if userLang, err := langDetector(); err == nil {
return userLang
}
return "C"
}
// setupLocalizer creates a new localizer using given userLang
func setupLocalizer(log *logrus.Entry, userLang string) *Localizer {
// create a i18n bundle that can be used to add translations and other things
i18nBundle := &i18n.Bundle{DefaultLanguage: language.English}
addBundles(log, i18nBundle)
// return the new localizer that can be used to translate text
i18nLocalizer := i18n.NewLocalizer(i18nBundle, userLang)
return &Localizer{
i18nLocalizer: i18nLocalizer,
language: userLang,
Log: log,
}
}

91
pkg/i18n/i18n_test.go Normal file
View File

@@ -0,0 +1,91 @@
package i18n
import (
"fmt"
"io/ioutil"
"testing"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
func getDummyLog() *logrus.Entry {
log := logrus.New()
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)
expected string
}
scenarios := []scenario{
{
func() (string, error) {
return "", fmt.Errorf("An error occurred")
},
"C",
},
{
func() (string, error) {
return "en", nil
},
"en",
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, detectLanguage(s.langDetector))
}
}
// TestLocalizer is a function.
func TestLocalizer(t *testing.T) {
type scenario struct {
userLang string
test func(*Localizer)
}
scenarios := []scenario{
{
"C",
func(l *Localizer) {
assert.EqualValues(t, "C", l.GetLanguage())
assert.Equal(t, "Diff", l.Localize(&i18n.LocalizeConfig{
DefaultMessage: &i18n.Message{
ID: "DiffTitle",
},
}))
assert.Equal(t, "Diff", l.SLocalize("DiffTitle"))
assert.Equal(t, "Are you sure you want to delete the branch test?", l.TemplateLocalize("DeleteBranchMessage", Teml{"selectedBranchName": "test"}))
},
},
{
"nl",
func(l *Localizer) {
assert.EqualValues(t, "nl", l.GetLanguage())
assert.Equal(t, "Diff", l.Localize(&i18n.LocalizeConfig{
DefaultMessage: &i18n.Message{
ID: "DiffTitle",
},
}))
assert.Equal(t, "Diff", l.SLocalize("DiffTitle"))
assert.Equal(t, "Weet je zeker dat je branch test wilt verwijderen?", l.TemplateLocalize("DeleteBranchMessage", Teml{"selectedBranchName": "test"}))
},
},
}
for _, s := range scenarios {
s.test(setupLocalizer(getDummyLog(), s.userLang))
}
}

631
pkg/i18n/polish.go Normal file
View File

@@ -0,0 +1,631 @@
package i18n
import (
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
)
func addPolish(i18nObject *i18n.Bundle) error {
return i18nObject.AddMessages(language.Polish,
&i18n.Message{
ID: "NotEnoughSpace",
Other: "Za mało miejsca do wyświetlenia paneli",
}, &i18n.Message{
ID: "DiffTitle",
Other: "Różnice",
}, &i18n.Message{
ID: "LogTitle",
Other: "Log",
}, &i18n.Message{
ID: "FilesTitle",
Other: "Pliki",
}, &i18n.Message{
ID: "BranchesTitle",
Other: "Gałęzie",
}, &i18n.Message{
ID: "CommitsTitle",
Other: "Commity",
}, &i18n.Message{
ID: "StashTitle",
Other: "Schowek",
}, &i18n.Message{
ID: "StagingMainTitle",
Other: `Stage Lines/Hunks`,
}, &i18n.Message{
ID: "MergingMainTitle",
Other: "Resolve merge conflicts",
}, &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",
}, &i18n.Message{
ID: "StatusTitle",
Other: "Status",
}, &i18n.Message{
ID: "GlobalTitle",
Other: "Globalne",
}, &i18n.Message{
ID: "navigate",
Other: "nawiguj",
}, &i18n.Message{
ID: "menu",
Other: "menu",
}, &i18n.Message{
ID: "execute",
Other: "wykonaj",
}, &i18n.Message{
ID: "stashFiles",
Other: "przechowaj pliki",
}, &i18n.Message{
ID: "open",
Other: "otwórz",
}, &i18n.Message{
ID: "ignore",
Other: "ignoruj",
}, &i18n.Message{
ID: "delete",
Other: "usuń",
}, &i18n.Message{
ID: "toggleStaged",
Other: "przełącz zatwierdzenie",
}, &i18n.Message{
ID: "toggleStagedAll",
Other: "przełącz wszystkie zatwierdzenia",
}, &i18n.Message{
ID: "refresh",
Other: "odśwież",
}, &i18n.Message{
ID: "addPatch",
Other: "dodaj łatkę",
}, &i18n.Message{
ID: "edit",
Other: "edytuj",
}, &i18n.Message{
ID: "scroll",
Other: "przewiń",
}, &i18n.Message{
ID: "abortMerge",
Other: "o scalaniu",
}, &i18n.Message{
ID: "resolveMergeConflicts",
Other: "rozwiąż konflikty scalania",
}, &i18n.Message{
ID: "checkout",
Other: "przełącz",
}, &i18n.Message{
ID: "NoChangedFiles",
Other: "Brak zmienionych plików",
}, &i18n.Message{
ID: "FileHasNoUnstagedChanges",
Other: "Plik nie zawiera żadnych nieopublikowanych zmian do dodania",
}, &i18n.Message{
ID: "CannotGitAdd",
Other: "Nie można git add --patch nieśledzonych plików",
}, &i18n.Message{
ID: "CantIgnoreTrackFiles",
Other: "Nie można zignorować nieśledzonych plików",
}, &i18n.Message{
ID: "NoStagedFilesToCommit",
Other: "Brak zatwierdzonych plików do commita",
}, &i18n.Message{
ID: "NoFilesDisplay",
Other: "Brak pliku do wyświetlenia",
}, &i18n.Message{
ID: "PullWait",
Other: "Wciąganie zmian...",
}, &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` i `clean -fd`? Możesz stracić wprowadzone zmiany",
}, &i18n.Message{
ID: "SureTo",
Other: "Jesteś pewny, że chcesz {{.deleteVerb}} {{.fileName}} (stracisz swoje wprowadzone zmiany)?",
}, &i18n.Message{
ID: "AlreadyCheckedOutBranch",
Other: "Już przęłączono na tą gałąź",
}, &i18n.Message{
ID: "SureForceCheckout",
Other: "Jesteś pewny, że chcesz wymusić przełączenie? Stracisz wszystkie lokalne zmiany",
}, &i18n.Message{
ID: "ForceCheckoutBranch",
Other: "Wymuś przełączenie gałęzi",
}, &i18n.Message{
ID: "BranchName",
Other: "Nazwa gałęzi",
}, &i18n.Message{
ID: "NewBranchNameBranchOff",
Other: "Nazwa nowej gałęzi (gałąź na bazie {{.branchName}})",
}, &i18n.Message{
ID: "CantDeleteCheckOutBranch",
Other: "Nie możesz usunąć obecnej przełączonej gałęzi!",
}, &i18n.Message{
ID: "DeleteBranch",
Other: "Usuń gałąź",
}, &i18n.Message{
ID: "DeleteBranchMessage",
Other: "Jesteś pewien, że chcesz usunąć gałąź {{.selectedBranchName}} ?",
}, &i18n.Message{
ID: "ForceDeleteBranchMessage",
Other: "Na pewno wymusić usunięcie gałęzi {{.selectedBranchName}}?",
}, &i18n.Message{
ID: "rebaseBranch",
Other: "rebase branch",
}, &i18n.Message{
ID: "CantRebaseOntoSelf",
Other: "You cannot rebase a branch onto itself",
}, &i18n.Message{
ID: "CantMergeBranchIntoItself",
Other: "Nie możesz scalić gałęzi do samej siebie",
}, &i18n.Message{
ID: "forceCheckout",
Other: "wymuś przełączenie",
}, &i18n.Message{
ID: "merge",
Other: "scal",
}, &i18n.Message{
ID: "checkoutByName",
Other: "przełącz używając nazwy",
}, &i18n.Message{
ID: "newBranch",
Other: "nowa gałąź",
}, &i18n.Message{
ID: "deleteBranch",
Other: "usuń gałąź",
}, &i18n.Message{
ID: "forceDeleteBranch",
Other: "usuń gałąź (wymuś)",
}, &i18n.Message{
ID: "NoBranchesThisRepo",
Other: "Brak gałęzi dla tego repozytorium",
}, &i18n.Message{
ID: "NoTrackingThisBranch",
Other: "Brak śledzenia dla tej gałęzi",
}, &i18n.Message{
ID: "CommitWithoutMessageErr",
Other: "Nie możesz commitować bez podania wiadomości",
}, &i18n.Message{
ID: "CloseConfirm",
Other: "{{.keyBindClose}}: zamknij, {{.keyBindConfirm}}: potwierdź",
}, &i18n.Message{
ID: "close",
Other: "zamknij",
}, &i18n.Message{
ID: "SureResetThisCommit",
Other: "Jesteś pewny, że chcesz zresetować ten commit?",
}, &i18n.Message{
ID: "ResetToCommit",
Other: "Zresetuj, aby commitować",
}, &i18n.Message{
ID: "squashDown",
Other: "ściśnij w dół",
}, &i18n.Message{
ID: "rename",
Other: "przemianuj",
}, &i18n.Message{
ID: "resetToThisCommit",
Other: "zresetuj do tego commita",
}, &i18n.Message{
ID: "fixupCommit",
Other: "napraw commit",
}, &i18n.Message{
ID: "NoCommitsThisBranch",
Other: "Brak commitów dla tej gałęzi",
}, &i18n.Message{
ID: "OnlySquashTopmostCommit",
Other: "Można tylko ścisnąć najwyższy commit",
}, &i18n.Message{
ID: "YouNoCommitsToSquash",
Other: "Nie masz commitów do ściśnięcia",
}, &i18n.Message{
ID: "CantFixupWhileUnstagedChanges",
Other: "Nie można wykonać naprawy, kiedy istnieją niezatwierdzone zmiany",
}, &i18n.Message{
ID: "Fixup",
Other: "Napraw",
}, &i18n.Message{
ID: "SureFixupThisCommit",
Other: "Jesteś pewny, ze chcesz naprawić ten commit? Commit poniżej zostanie ściśnięty w górę wraz z tym",
}, &i18n.Message{
ID: "OnlyRenameTopCommit",
Other: "Można przmianować tylko najwyższy commit",
}, &i18n.Message{
ID: "renameCommit",
Other: "przemianuj commit",
}, &i18n.Message{
ID: "renameCommitEditor",
Other: "przemianuj commit w edytorze",
}, &i18n.Message{
ID: "PotentialErrInGetselectedCommit",
Other: "potencjalny błąd w getSelected Commit (niedopasowane ui i stan)",
}, &i18n.Message{
ID: "Error",
Other: "Błąd",
}, &i18n.Message{
ID: "resizingPopupPanel",
Other: "skalowanie wyskakującego panelu",
}, &i18n.Message{
ID: "RunningSubprocess",
Other: "uruchomiony podproces",
}, &i18n.Message{
ID: "selectHunk",
Other: "wybierz kawałek",
}, &i18n.Message{
ID: "navigateConflicts",
Other: "nawiguj konflikty",
}, &i18n.Message{
ID: "pickHunk",
Other: "wybierz kawałek",
}, &i18n.Message{
ID: "pickBothHunks",
Other: "wybierz oba kawałki",
}, &i18n.Message{
ID: "undo",
Other: "cofnij",
}, &i18n.Message{
ID: "pop",
Other: "wyciągnij",
}, &i18n.Message{
ID: "drop",
Other: "porzuć",
}, &i18n.Message{
ID: "apply",
Other: "zastosuj",
}, &i18n.Message{
ID: "NoStashEntries",
Other: "Brak pozycji w schowku",
}, &i18n.Message{
ID: "StashDrop",
Other: "Porzuć schowek",
}, &i18n.Message{
ID: "SureDropStashEntry",
Other: "Jesteś pewny, że chcesz porzucić tę pozycję w schowku?",
}, &i18n.Message{
ID: "NoStashTo",
Other: "Brak schowka dla {{.method}}",
}, &i18n.Message{
ID: "NoTrackedStagedFilesStash",
Other: "Nie masz śledzonych/zatwierdzonych plików do przechowania",
}, &i18n.Message{
ID: "StashChanges",
Other: "Przechowaj zmiany",
}, &i18n.Message{
ID: "IssntListOfViews",
Other: "{{.name}} nie jest na liście widoków",
}, &i18n.Message{
ID: "NoViewMachingNewLineFocusedSwitchStatement",
Other: "Brak widoku pasującego do instrukcji przełączania newLineFocused",
}, &i18n.Message{
ID: "newFocusedViewIs",
Other: "nowy skupiony widok to {{.newFocusedView}}",
}, &i18n.Message{
ID: "CantCloseConfirmationPrompt",
Other: "Nie można zamknąć monitu potwierdzenia: {{.error}}",
}, &i18n.Message{
ID: "ClearFilePanel",
Other: "Wyczyść panel plików",
}, &i18n.Message{
ID: "MergeAborted",
Other: "Scalanie anulowane",
}, &i18n.Message{
ID: "OpenConfig",
Other: "otwórz plik konfiguracyjny",
}, &i18n.Message{
ID: "EditConfig",
Other: "edytuj plik konfiguracyjny",
}, &i18n.Message{
ID: "ForcePush",
Other: "Wymuś wypchnięcie",
}, &i18n.Message{
ID: "ForcePushPrompt",
Other: "Twoja gałąź rozeszła się z gałęzią zdalną. Wciśnij 'esc' aby anulować lub 'enter' aby wymusić wypchnięcie.",
}, &i18n.Message{
ID: "checkForUpdate",
Other: "sprawdź aktualizacje",
}, &i18n.Message{
ID: "CheckingForUpdates",
Other: "Sprawdzanie aktualizacji...",
}, &i18n.Message{
ID: "OnLatestVersionErr",
Other: "Już posiadasz najnowszą wersję",
}, &i18n.Message{
ID: "MajorVersionErr",
Other: "Nowa wersja ({{.newVersion}}) posiada niekompatybilne zmiany w porównaniu do obecnej wersji ({{.currentVersion}})",
}, &i18n.Message{
ID: "CouldNotFindBinaryErr",
Other: "Nie można znaleźć pliku binarnego w {{.url}}",
}, &i18n.Message{
ID: "AnonymousReportingTitle",
Other: "Help make lazygit better",
}, &i18n.Message{
ID: "AnonymousReportingPrompt",
Other: "Włączyć anonimowe raportowanie błędów w celu pomocy w usprawnianiu lazygita (enter/esc)?",
}, &i18n.Message{
ID: "removeFile",
Other: `usuń jeśli nie śledzony / przełącz jeśli śledzony`,
}, &i18n.Message{
ID: "editFile",
Other: `edytuj plik`,
}, &i18n.Message{
ID: "openFile",
Other: `otwórz plik`,
}, &i18n.Message{
ID: "ignoreFile",
Other: `dodaj do .gitignore`,
}, &i18n.Message{
ID: "refreshFiles",
Other: `odśwież pliki`,
}, &i18n.Message{
ID: "resetHard",
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`,
}, &i18n.Message{
ID: "RebasingTitle",
Other: "Rebasing",
}, &i18n.Message{
ID: "MergingTitle",
Other: "Merging",
}, &i18n.Message{
ID: "ConfirmRebase",
Other: "Are you sure you want to rebase {{.checkedOutBranch}} onto {{.selectedBranch}}?",
}, &i18n.Message{
ID: "ConfirmMerge",
Other: "Are you sure you want to merge {{.selectedBranch}} into {{.checkedOutBranch}}?",
}, &i18n.Message{}, &i18n.Message{
ID: "FwdNoUpstream",
Other: "Cannot fast-forward a branch with no upstream",
}, &i18n.Message{
ID: "FwdCommitsToPush",
Other: "Cannot fast-forward a branch with commits to push",
}, &i18n.Message{
ID: "ErrorOccurred",
Other: "An error occurred! Please create an issue at https://github.com/jesseduffield/lazygit/issues",
}, &i18n.Message{
ID: "MainTitle",
Other: "Main",
}, &i18n.Message{
ID: "NormalTitle",
Other: "Normal",
}, &i18n.Message{
ID: "softReset",
Other: "soft reset to last commit",
}, &i18n.Message{
ID: "SoftReset",
Other: "Soft reset",
}, &i18n.Message{
ID: "ConfirmSoftReset",
Other: "Are you sure you want to `reset --soft HEAD^`? The changes in your topmost commit will be placed in your working tree",
}, &i18n.Message{
ID: "SureSquashThisCommit",
Other: "Are you sure you want to squash this commit into the commit below?",
}, &i18n.Message{
ID: "Squash",
Other: "Squash",
}, &i18n.Message{
ID: "pickCommit",
Other: "pick commit (when mid-rebase)",
}, &i18n.Message{
ID: "revertCommit",
Other: "revert commit",
}, &i18n.Message{
ID: "deleteCommit",
Other: "delete commit",
}, &i18n.Message{
ID: "moveDownCommit",
Other: "move commit down one",
}, &i18n.Message{
ID: "moveUpCommit",
Other: "move commit up one",
}, &i18n.Message{
ID: "editCommit",
Other: "edit commit",
}, &i18n.Message{
ID: "amendToCommit",
Other: "amend commit with staged changes",
}, &i18n.Message{
ID: "FoundConflicts",
Other: "Damn, conflicts! To abort press 'esc', otherwise press 'enter'",
}, &i18n.Message{
ID: "FoundConflictsTitle",
Other: "Auto-merge failed",
}, &i18n.Message{
ID: "Undo",
Other: "undo",
}, &i18n.Message{
ID: "PickHunk",
Other: "pick hunk",
}, &i18n.Message{
ID: "PickBothHunks",
Other: "pick both hunks",
}, &i18n.Message{
ID: "ViewMergeRebaseOptions",
Other: "view merge/rebase options",
}, &i18n.Message{
ID: "NotMergingOrRebasing",
Other: "You are currently neither rebasing nor merging",
}, &i18n.Message{
ID: "RecentRepos",
Other: "recent repositories",
}, &i18n.Message{
ID: "MergeOptionsTitle",
Other: "Merge Options",
}, &i18n.Message{
ID: "RebaseOptionsTitle",
Other: "Rebase Options",
}, &i18n.Message{
ID: "ConflictsResolved",
Other: "all merge conflicts resolved. Continue?",
}, &i18n.Message{
ID: "NoRoom",
Other: "Not enough room",
}, &i18n.Message{
ID: "YouAreHere",
Other: "YOU ARE HERE",
}, &i18n.Message{
ID: "rewordNotSupported",
Other: "rewording commits while interactively rebasing is not currently supported",
}, &i18n.Message{
ID: "cherryPickCopy",
Other: "copy commit (cherry-pick)",
}, &i18n.Message{
ID: "cherryPickCopyRange",
Other: "copy commit range (cherry-pick)",
}, &i18n.Message{
ID: "pasteCommits",
Other: "paste commits (cherry-pick)",
}, &i18n.Message{
ID: "SureCherryPick",
Other: "Are you sure you want to cherry-pick the copied commits onto this branch?",
}, &i18n.Message{
ID: "CherryPick",
Other: "Cherry-Pick",
}, &i18n.Message{
ID: "CannotRebaseOntoFirstCommit",
Other: "You cannot interactive rebase onto the first commit",
}, &i18n.Message{
ID: "Donate",
Other: "Donate",
}, &i18n.Message{
ID: "PrevLine",
Other: "select previous line",
}, &i18n.Message{
ID: "NextLine",
Other: "select next line",
}, &i18n.Message{
ID: "PrevHunk",
Other: "select previous hunk",
}, &i18n.Message{
ID: "NextHunk",
Other: "select next hunk",
}, &i18n.Message{
ID: "PrevConflict",
Other: "select previous conflict",
}, &i18n.Message{
ID: "NextConflict",
Other: "select next conflict",
}, &i18n.Message{
ID: "SelectTop",
Other: "select top hunk",
}, &i18n.Message{
ID: "SelectBottom",
Other: "select bottom hunk",
}, &i18n.Message{
ID: "ScrollDown",
Other: "scroll down",
}, &i18n.Message{
ID: "ScrollUp",
Other: "scroll up",
}, &i18n.Message{
ID: "AmendCommitTitle",
Other: "Amend Commit",
}, &i18n.Message{
ID: "AmendCommitPrompt",
Other: "Are you sure you want to amend this commit with your staged files?",
}, &i18n.Message{
ID: "DeleteCommitTitle",
Other: "Delete Commit",
}, &i18n.Message{
ID: "DeleteCommitPrompt",
Other: "Are you sure you want to delete this commit?",
}, &i18n.Message{
ID: "SquashingStatus",
Other: "squashing",
}, &i18n.Message{
ID: "FixingStatus",
Other: "fixing up",
}, &i18n.Message{
ID: "DeletingStatus",
Other: "deleting",
}, &i18n.Message{
ID: "MovingStatus",
Other: "moving",
}, &i18n.Message{
ID: "RebasingStatus",
Other: "rebasing",
}, &i18n.Message{
ID: "AmendingStatus",
Other: "amending",
}, &i18n.Message{
ID: "CherryPickingStatus",
Other: "cherry-picking",
},
)
}

31
pkg/test/test.go Normal file
View File

@@ -0,0 +1,31 @@
package test
import (
"github.com/go-errors/errors"
"os"
"os/exec"
"path/filepath"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// GenerateRepo generates a repo from test/repos and changes the directory to be
// inside the newly made repo
func GenerateRepo(filename string) error {
reposDir := "/test/repos/"
testPath := utils.GetProjectRoot() + reposDir
// workaround for debian packaging
if _, err := os.Stat(testPath); os.IsNotExist(err) {
cwd, _ := os.Getwd()
testPath = filepath.Dir(filepath.Dir(cwd)) + reposDir
}
if err := os.Chdir(testPath); err != nil {
return err
}
if output, err := exec.Command("bash", filename).CombinedOutput(); err != nil {
return errors.New(string(output))
}
return os.Chdir(testPath + "repo")
}

45
pkg/test/utils.go Normal file
View File

@@ -0,0 +1,45 @@
package test
import (
"fmt"
"os/exec"
"strings"
"testing"
"github.com/mgutz/str"
"github.com/stretchr/testify/assert"
)
// CommandSwapper takes a command, verifies that it is what it's expected to be
// and then returns a replacement command that will actually be called by the os
type CommandSwapper struct {
Expect string
Replace string
}
// SwapCommand verifies the command is what we expected, and swaps it out for a different command
func (i *CommandSwapper) SwapCommand(t *testing.T, cmd string, args []string) *exec.Cmd {
splitCmd := str.ToArgv(i.Expect)
assert.EqualValues(t, splitCmd[0], cmd, fmt.Sprintf("received command: %s %s", cmd, strings.Join(args, " ")))
if len(splitCmd) > 1 {
assert.EqualValues(t, splitCmd[1:], args, fmt.Sprintf("received command: %s %s", cmd, strings.Join(args, " ")))
}
splitCmd = str.ToArgv(i.Replace)
return exec.Command(splitCmd[0], splitCmd[1:]...)
}
// CreateMockCommand creates a command function that will verify its receiving the right sequence of commands from lazygit
func CreateMockCommand(t *testing.T, swappers []*CommandSwapper) func(cmd string, args ...string) *exec.Cmd {
commandIndex := 0
return func(cmd string, args ...string) *exec.Cmd {
var command *exec.Cmd
if commandIndex > len(swappers)-1 {
assert.Fail(t, fmt.Sprintf("too many commands run. This command was (%s %s)", cmd, strings.Join(args, " ")))
}
command = swappers[commandIndex].SwapCommand(t, cmd, args)
commandIndex++
return command
}
}

313
pkg/updates/updates.go Normal file
View File

@@ -0,0 +1,313 @@
package updates
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/go-errors/errors"
"github.com/kardianos/osext"
getter "github.com/jesseduffield/go-getter"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/sirupsen/logrus"
)
// Updater checks for updates and does updates
type Updater struct {
Log *logrus.Entry
Config config.AppConfigurer
OSCommand *commands.OSCommand
Tr *i18n.Localizer
}
// Updaterer implements the check and update methods
type Updaterer interface {
CheckForNewUpdate()
Update()
}
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")
return &Updater{
Log: contextLogger,
Config: config,
OSCommand: osCommand,
Tr: tr,
}, nil
}
func (u *Updater) getLatestVersionNumber() (string, error) {
req, err := http.NewRequest("GET", PROJECT_URL+"/releases/latest", nil)
if err != nil {
return "", err
}
req.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
dec := json.NewDecoder(resp.Body)
data := struct {
TagName string `json:"tag_name"`
}{}
if err := dec.Decode(&data); err != nil {
return "", err
}
return data.TagName, nil
}
// RecordLastUpdateCheck records last time an update check was performed
func (u *Updater) RecordLastUpdateCheck() error {
u.Config.GetAppState().LastUpdateCheck = time.Now().Unix()
return u.Config.SaveAppState()
}
// expecting version to be of the form `v12.34.56`
func (u *Updater) majorVersionDiffers(oldVersion, newVersion string) bool {
if oldVersion == "unversioned" {
return false
}
oldVersion = strings.TrimPrefix(oldVersion, "v")
newVersion = strings.TrimPrefix(newVersion, "v")
return strings.Split(oldVersion, ".")[0] != strings.Split(newVersion, ".")[0]
}
func (u *Updater) checkForNewUpdate() (string, error) {
u.Log.Info("Checking for an updated version")
currentVersion := u.Config.GetVersion()
if err := u.RecordLastUpdateCheck(); err != nil {
return "", err
}
newVersion, err := u.getLatestVersionNumber()
if err != nil {
return "", err
}
u.Log.Info("Current version is " + currentVersion)
u.Log.Info("New version is " + newVersion)
if newVersion == currentVersion {
return "", errors.New(u.Tr.SLocalize("OnLatestVersionErr"))
}
if u.majorVersionDiffers(currentVersion, newVersion) {
errMessage := u.Tr.TemplateLocalize(
"MajorVersionErr",
i18n.Teml{
"newVersion": newVersion,
"currentVersion": currentVersion,
},
)
return "", errors.New(errMessage)
}
rawUrl, err := u.getBinaryUrl(newVersion)
if err != nil {
return "", err
}
u.Log.Info("Checking for resource at url " + rawUrl)
if !u.verifyResourceFound(rawUrl) {
errMessage := u.Tr.TemplateLocalize(
"CouldNotFindBinaryErr",
i18n.Teml{
"url": rawUrl,
},
)
return "", errors.New(errMessage)
}
u.Log.Info("Verified resource is available, ready to update")
return newVersion, nil
}
// CheckForNewUpdate checks if there is an available update
func (u *Updater) CheckForNewUpdate(onFinish func(string, error) error, userRequested bool) {
if !userRequested && u.skipUpdateCheck() {
return
}
go func() {
newVersion, err := u.checkForNewUpdate()
if err = onFinish(newVersion, err); err != nil {
u.Log.Error(err)
}
}()
}
func (u *Updater) skipUpdateCheck() bool {
// will remove the check for windows after adding a manifest file asking for
// the required permissions
if runtime.GOOS == "windows" {
u.Log.Info("Updating is currently not supported for windows until we can fix permission issues")
return true
}
if u.Config.GetVersion() == "unversioned" {
u.Log.Info("Current version is not built from an official release so we won't check for an update")
return true
}
if u.Config.GetBuildSource() != "buildBinary" {
u.Log.Info("Binary is not built with the buildBinary flag so we won't check for an update")
return true
}
userConfig := u.Config.GetUserConfig()
if userConfig.Get("update.method") == "never" {
u.Log.Info("Update method is set to never so we won't check for an update")
return true
}
currentTimestamp := time.Now().Unix()
lastUpdateCheck := u.Config.GetAppState().LastUpdateCheck
days := userConfig.GetInt64("update.days")
if (currentTimestamp-lastUpdateCheck)/(60*60*24) < days {
u.Log.Info("Last update was too recent so we won't check for an update")
return true
}
return false
}
func (u *Updater) mappedOs(os string) string {
osMap := map[string]string{
"darwin": "Darwin",
"linux": "Linux",
"windows": "Windows",
}
result, found := osMap[os]
if found {
return result
}
return os
}
func (u *Updater) mappedArch(arch string) string {
archMap := map[string]string{
"386": "32-bit",
"amd64": "x86_64",
}
result, found := archMap[arch]
if found {
return result
}
return arch
}
// example: https://github.com/jesseduffield/lazygit/releases/download/v0.1.73/lazygit_0.1.73_Darwin_x86_64.tar.gz
func (u *Updater) getBinaryUrl(newVersion string) (string, error) {
extension := "tar.gz"
if runtime.GOOS == "windows" {
extension = "zip"
}
url := fmt.Sprintf(
"%s/releases/download/%s/lazygit_%s_%s_%s.%s",
PROJECT_URL,
newVersion,
newVersion[1:],
u.mappedOs(runtime.GOOS),
u.mappedArch(runtime.GOARCH),
extension,
)
u.Log.Info("Url for latest release is " + url)
return url, nil
}
// Update downloads the latest binary and replaces the current binary with it
func (u *Updater) Update(newVersion string, onFinish func(error) error) {
go func() {
err := u.update(newVersion)
if err = onFinish(err); err != nil {
u.Log.Error(err)
}
}()
}
func (u *Updater) update(newVersion string) error {
rawUrl, err := u.getBinaryUrl(newVersion)
if err != nil {
return err
}
u.Log.Info("Updating with url " + rawUrl)
return u.downloadAndInstall(rawUrl)
}
func (u *Updater) downloadAndInstall(rawUrl string) error {
url, err := url.Parse(rawUrl)
if err != nil {
return err
}
g := new(getter.HttpGetter)
tempDir, err := ioutil.TempDir("", "lazygit")
if err != nil {
return err
}
defer os.RemoveAll(tempDir)
u.Log.Info("Temp directory is " + tempDir)
// Get it!
if err := g.Get(tempDir, url); err != nil {
return err
}
// get the path of the current binary
binaryPath, err := osext.Executable()
if err != nil {
return err
}
u.Log.Info("Binary path is " + binaryPath)
binaryName := filepath.Base(binaryPath)
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)
if _, err := os.Stat(tempPath); err != nil {
return err
}
// swap out the old binary for the new one
err = os.Rename(tempPath, binaryPath)
if err != nil {
return err
}
u.Log.Info("Update complete!")
return nil
}
func (u *Updater) verifyResourceFound(rawUrl string) bool {
resp, err := http.Head(rawUrl)
if err != nil {
return false
}
defer resp.Body.Close()
u.Log.Info("Received status code ", resp.StatusCode)
// 403 means the resource is there (not going to bother adding extra request headers)
// 404 means its not
return resp.StatusCode == 403
}

252
pkg/utils/utils.go Normal file
View File

@@ -0,0 +1,252 @@
package utils
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"reflect"
"regexp"
"strings"
"time"
"github.com/go-errors/errors"
"github.com/fatih/color"
)
// SplitLines takes a multiline string and splits it on newlines
// currently we are also stripping \r's which may have adverse effects for
// windows users (but no issues have been raised yet)
func SplitLines(multilineString string) []string {
multilineString = strings.Replace(multilineString, "\r", "", -1)
if multilineString == "" || multilineString == "\n" {
return make([]string, 0)
}
lines := strings.Split(multilineString, "\n")
if lines[len(lines)-1] == "" {
return lines[:len(lines)-1]
}
return lines
}
// WithPadding pads a string as much as you want
func WithPadding(str string, padding int) string {
if padding-len(str) < 0 {
return str
}
return str + strings.Repeat(" ", padding-len(str))
}
// ColoredString takes a string and a colour attribute and returns a colored
// string with that attribute
func ColoredString(str string, colorAttribute color.Attribute) string {
colour := color.New(colorAttribute)
return ColoredStringDirect(str, colour)
}
// ColoredStringDirect used for aggregating a few color attributes rather than
// just sending a single one
func ColoredStringDirect(str string, colour *color.Color) string {
return colour.SprintFunc()(fmt.Sprint(str))
}
// GetCurrentRepoName gets the repo's base name
func GetCurrentRepoName() string {
pwd, err := os.Getwd()
if err != nil {
log.Fatalln(err.Error())
}
return filepath.Base(pwd)
}
// TrimTrailingNewline - Trims the trailing newline
// TODO: replace with `chomp` after refactor
func TrimTrailingNewline(str string) string {
if strings.HasSuffix(str, "\n") {
return str[:len(str)-1]
}
return str
}
// NormalizeLinefeeds - Removes all Windows and Mac style line feeds
func NormalizeLinefeeds(str string) string {
str = strings.Replace(str, "\r\n", "\n", -1)
str = strings.Replace(str, "\r", "", -1)
return str
}
// GetProjectRoot returns the path to the root of the project. Only to be used
// in testing contexts, as with binaries it's unlikely this path will exist on
// the machine
func GetProjectRoot() string {
dir, err := os.Getwd()
if err != nil {
panic(err)
}
return strings.Split(dir, "lazygit")[0] + "lazygit"
}
// Loader dumps a string to be displayed as a loader
func Loader() string {
characters := "|/-\\"
now := time.Now()
nanos := now.UnixNano()
index := nanos / 50000000 % int64(len(characters))
return characters[index : index+1]
}
// ResolvePlaceholderString populates a template with values
func ResolvePlaceholderString(str string, arguments map[string]string) string {
for key, value := range arguments {
str = strings.Replace(str, "{{"+key+"}}", value, -1)
}
return str
}
// Min returns the minimum of two integers
func Min(x, y int) int {
if x < y {
return x
}
return y
}
type Displayable interface {
GetDisplayStrings(bool) []string
}
// RenderList takes a slice of items, confirms they implement the Displayable
// interface, then generates a list of their displaystrings to write to a panel's
// buffer
func RenderList(slice interface{}, isFocused bool) (string, error) {
s := reflect.ValueOf(slice)
if s.Kind() != reflect.Slice {
return "", errors.New("RenderList given a non-slice type")
}
displayables := make([]Displayable, s.Len())
for i := 0; i < s.Len(); i++ {
value, ok := s.Index(i).Interface().(Displayable)
if !ok {
return "", errors.New("item does not implement the Displayable interface")
}
displayables[i] = value
}
return renderDisplayableList(displayables, isFocused)
}
// renderDisplayableList takes a list of displayable items, obtains their display
// strings via GetDisplayStrings() and then returns a single string containing
// each item's string representation on its own line, with appropriate horizontal
// padding between the item's own strings
func renderDisplayableList(items []Displayable, isFocused bool) (string, error) {
if len(items) == 0 {
return "", nil
}
stringArrays := getDisplayStringArrays(items, isFocused)
if !displayArraysAligned(stringArrays) {
return "", errors.New("Each item must return the same number of strings to display")
}
padWidths := getPadWidths(stringArrays)
paddedDisplayStrings := getPaddedDisplayStrings(stringArrays, padWidths)
return strings.Join(paddedDisplayStrings, "\n"), nil
}
// Decolorise strips a string of color
func Decolorise(str string) string {
re := regexp.MustCompile(`\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]`)
return re.ReplaceAllString(str, "")
}
func getPadWidths(stringArrays [][]string) []int {
if len(stringArrays[0]) <= 1 {
return []int{}
}
padWidths := make([]int, len(stringArrays[0])-1)
for i := range padWidths {
for _, strings := range stringArrays {
uncoloredString := Decolorise(strings[i])
if len(uncoloredString) > padWidths[i] {
padWidths[i] = len(uncoloredString)
}
}
}
return padWidths
}
func getPaddedDisplayStrings(stringArrays [][]string, padWidths []int) []string {
paddedDisplayStrings := make([]string, len(stringArrays))
for i, stringArray := range stringArrays {
if len(stringArray) == 0 {
continue
}
for j, padWidth := range padWidths {
paddedDisplayStrings[i] += WithPadding(stringArray[j], padWidth) + " "
}
paddedDisplayStrings[i] += stringArray[len(padWidths)]
}
return paddedDisplayStrings
}
// displayArraysAligned returns true if every string array returned from our
// list of displayables has the same length
func displayArraysAligned(stringArrays [][]string) bool {
for _, strings := range stringArrays {
if len(strings) != len(stringArrays[0]) {
return false
}
}
return true
}
func getDisplayStringArrays(displayables []Displayable, isFocused bool) [][]string {
stringArrays := make([][]string, len(displayables))
for i, item := range displayables {
stringArrays[i] = item.GetDisplayStrings(isFocused)
}
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)
}

575
pkg/utils/utils_test.go Normal file
View File

@@ -0,0 +1,575 @@
package utils
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestSplitLines is a function.
func TestSplitLines(t *testing.T) {
type scenario struct {
multilineString string
expected []string
}
scenarios := []scenario{
{
"",
[]string{},
},
{
"\n",
[]string{},
},
{
"hello world !\nhello universe !\n",
[]string{
"hello world !",
"hello universe !",
},
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, SplitLines(s.multilineString))
}
}
// TestWithPadding is a function.
func TestWithPadding(t *testing.T) {
type scenario struct {
str string
padding int
expected string
}
scenarios := []scenario{
{
"hello world !",
1,
"hello world !",
},
{
"hello world !",
14,
"hello world ! ",
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, WithPadding(s.str, s.padding))
}
}
// TestTrimTrailingNewline is a function.
func TestTrimTrailingNewline(t *testing.T) {
type scenario struct {
str string
expected string
}
scenarios := []scenario{
{
"hello world !\n",
"hello world !",
},
{
"hello world !",
"hello world !",
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, TrimTrailingNewline(s.str))
}
}
// TestNormalizeLinefeeds is a function.
func TestNormalizeLinefeeds(t *testing.T) {
type scenario struct {
byteArray []byte
expected []byte
}
var scenarios = []scenario{
{
// \r\n
[]byte{97, 115, 100, 102, 13, 10},
[]byte{97, 115, 100, 102, 10},
},
{
// bash\r\nblah
[]byte{97, 115, 100, 102, 13, 10, 97, 115, 100, 102},
[]byte{97, 115, 100, 102, 10, 97, 115, 100, 102},
},
{
// \r
[]byte{97, 115, 100, 102, 13},
[]byte{97, 115, 100, 102},
},
{
// \n
[]byte{97, 115, 100, 102, 10},
[]byte{97, 115, 100, 102, 10},
},
}
for _, s := range scenarios {
assert.EqualValues(t, string(s.expected), NormalizeLinefeeds(string(s.byteArray)))
}
}
// TestResolvePlaceholderString is a function.
func TestResolvePlaceholderString(t *testing.T) {
type scenario struct {
templateString string
arguments map[string]string
expected string
}
scenarios := []scenario{
{
"",
map[string]string{},
"",
},
{
"hello",
map[string]string{},
"hello",
},
{
"hello {{arg}}",
map[string]string{},
"hello {{arg}}",
},
{
"hello {{arg}}",
map[string]string{"arg": "there"},
"hello there",
},
{
"hello",
map[string]string{"arg": "there"},
"hello",
},
{
"{{nothing}}",
map[string]string{"nothing": ""},
"",
},
{
"{{}} {{ this }} { should not throw}} an {{{{}}}} error",
map[string]string{
"blah": "blah",
"this": "won't match",
},
"{{}} {{ this }} { should not throw}} an {{{{}}}} error",
},
}
for _, s := range scenarios {
assert.EqualValues(t, string(s.expected), ResolvePlaceholderString(s.templateString, s.arguments))
}
}
// TestDisplayArraysAligned is a function.
func TestDisplayArraysAligned(t *testing.T) {
type scenario struct {
input [][]string
expected bool
}
scenarios := []scenario{
{
[][]string{{"", ""}, {"", ""}},
true,
},
{
[][]string{{""}, {"", ""}},
false,
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, displayArraysAligned(s.input))
}
}
type myDisplayable struct {
strings []string
}
type myStruct struct{}
// GetDisplayStrings is a function.
func (d *myDisplayable) GetDisplayStrings(isFocused bool) []string {
if isFocused {
return append(d.strings, "blah")
}
return d.strings
}
// TestGetDisplayStringArrays is a function.
func TestGetDisplayStringArrays(t *testing.T) {
type scenario struct {
input []Displayable
isFocused bool
expected [][]string
}
scenarios := []scenario{
{
[]Displayable{
Displayable(&myDisplayable{[]string{"a", "b"}}),
Displayable(&myDisplayable{[]string{"c", "d"}}),
},
false,
[][]string{{"a", "b"}, {"c", "d"}},
},
{
[]Displayable{
Displayable(&myDisplayable{[]string{"a", "b"}}),
Displayable(&myDisplayable{[]string{"c", "d"}}),
},
true,
[][]string{{"a", "b", "blah"}, {"c", "d", "blah"}},
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, getDisplayStringArrays(s.input, s.isFocused))
}
}
// TestRenderDisplayableList is a function.
func TestRenderDisplayableList(t *testing.T) {
type scenario struct {
input []Displayable
isFocused bool
expectedString string
expectedErrorMessage string
}
scenarios := []scenario{
{
[]Displayable{
Displayable(&myDisplayable{[]string{}}),
Displayable(&myDisplayable{[]string{}}),
},
false,
"\n",
"",
},
{
[]Displayable{
Displayable(&myDisplayable{[]string{"aa", "b"}}),
Displayable(&myDisplayable{[]string{"c", "d"}}),
},
false,
"aa b\nc d",
"",
},
{
[]Displayable{
Displayable(&myDisplayable{[]string{"a"}}),
Displayable(&myDisplayable{[]string{"b", "c"}}),
},
false,
"",
"Each item must return the same number of strings to display",
},
{
[]Displayable{
Displayable(&myDisplayable{[]string{"a"}}),
Displayable(&myDisplayable{[]string{"b"}}),
},
true,
"a blah\nb blah",
"",
},
}
for _, s := range scenarios {
str, err := renderDisplayableList(s.input, s.isFocused)
assert.EqualValues(t, s.expectedString, str)
if s.expectedErrorMessage != "" {
assert.EqualError(t, err, s.expectedErrorMessage)
} else {
assert.NoError(t, err)
}
}
}
// TestRenderList is a function.
func TestRenderList(t *testing.T) {
type scenario struct {
input interface{}
isFocused bool
expectedString string
expectedErrorMessage string
}
scenarios := []scenario{
{
[]*myDisplayable{
{[]string{"aa", "b"}},
{[]string{"c", "d"}},
},
false,
"aa b\nc d",
"",
},
{
[]*myStruct{
{},
{},
},
false,
"",
"item does not implement the Displayable interface",
},
{
&myStruct{},
false,
"",
"RenderList given a non-slice type",
},
{
[]*myDisplayable{
{[]string{"a"}},
},
true,
"a blah",
"",
},
}
for _, s := range scenarios {
str, err := RenderList(s.input, s.isFocused)
assert.EqualValues(t, s.expectedString, str)
if s.expectedErrorMessage != "" {
assert.EqualError(t, err, s.expectedErrorMessage)
} else {
assert.NoError(t, err)
}
}
}
// TestGetPaddedDisplayStrings is a function.
func TestGetPaddedDisplayStrings(t *testing.T) {
type scenario struct {
stringArrays [][]string
padWidths []int
expected []string
}
scenarios := []scenario{
{
[][]string{{"a", "b"}, {"c", "d"}},
[]int{1},
[]string{"a b", "c d"},
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, getPaddedDisplayStrings(s.stringArrays, s.padWidths))
}
}
// TestGetPadWidths is a function.
func TestGetPadWidths(t *testing.T) {
type scenario struct {
stringArrays [][]string
expected []int
}
scenarios := []scenario{
{
[][]string{{""}, {""}},
[]int{},
},
{
[][]string{{"a"}, {""}},
[]int{},
},
{
[][]string{{"aa", "b", "ccc"}, {"c", "d", "e"}},
[]int{2, 1},
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, getPadWidths(s.stringArrays))
}
}
// TestMin is a function.
func TestMin(t *testing.T) {
type scenario struct {
a int
b int
expected int
}
scenarios := []scenario{
{
1,
1,
1,
},
{
1,
2,
1,
},
{
2,
1,
1,
},
}
for _, s := range scenarios {
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

@@ -0,0 +1,126 @@
// 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"
"log"
"os"
"strings"
"github.com/jesseduffield/lazygit/pkg/app"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui"
)
type bindingSection struct {
title string
bindings []*gui.Binding
}
func main() {
mConfig, _ := config.NewAppConfig("", "", "", "", "", true)
mApp, _ := app.NewApp(mConfig)
lang := mApp.Tr.GetLanguage()
file, _ := os.Create("Keybindings_" + lang + ".md")
bindingSections := getBindingSections(mApp)
content := formatSections(mApp, bindingSections)
writeString(file, content)
}
func writeString(file *os.File, str string) {
_, err := file.WriteString(str)
if err != nil {
log.Fatal(err)
}
}
func localisedTitle(mApp *app.App, str string) string {
viewTitle := strings.Title(str) + "Title"
return mApp.Tr.SLocalize(viewTitle)
}
func formatTitle(title string) string {
return fmt.Sprintf("\n## %s\n\n", title)
}
func formatBinding(binding *gui.Binding) string {
return fmt.Sprintf(" <kbd>%s</kbd>: %s\n", binding.GetKey(), binding.Description)
}
func getBindingSections(mApp *app.App) []*bindingSection {
bindingSections := []*bindingSection{}
// TODO: add context-based keybindings
for _, binding := range mApp.Gui.GetInitialKeybindings() {
if binding.Description == "" {
continue
}
viewName := binding.ViewName
if viewName == "" {
viewName = "global"
}
title := localisedTitle(mApp, viewName)
bindingSections = addBinding(title, bindingSections, binding)
}
for view, contexts := range mApp.Gui.GetContextMap() {
for contextName, contextBindings := range contexts {
translatedView := localisedTitle(mApp, view)
translatedContextName := localisedTitle(mApp, contextName)
title := fmt.Sprintf("%s (%s)", translatedView, translatedContextName)
for _, binding := range contextBindings {
bindingSections = addBinding(title, bindingSections, binding)
}
}
}
return bindingSections
}
func addBinding(title string, bindingSections []*bindingSection, binding *gui.Binding) []*bindingSection {
if binding.Description == "" {
return bindingSections
}
for _, section := range bindingSections {
if title == section.title {
section.bindings = append(section.bindings, binding)
return bindingSections
}
}
section := &bindingSection{
title: title,
bindings: []*gui.Binding{binding},
}
return append(bindingSections, section)
}
func formatSections(mApp *app.App, bindingSections []*bindingSection) string {
content := fmt.Sprintf("# Lazygit %s\n", mApp.Tr.SLocalize("menu"))
for _, section := range bindingSections {
content += formatTitle(section.title)
content += "<pre>\n"
for _, binding := range section.bindings {
content += formatBinding(binding)
}
content += "</pre>\n"
}
return content
}

63
scripts/push_new_patch/main.go Executable file
View File

@@ -0,0 +1,63 @@
// call from project root with
// 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
package main
import (
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"strconv"
"strings"
)
func main() {
version, err := ioutil.ReadFile("VERSION")
if err != nil {
log.Panicln(err.Error())
}
stringVersion := string(version)
fmt.Println("VERSION was " + stringVersion)
runCommand("git", "pull")
splitVersion := strings.Split(stringVersion, ".")
patch := splitVersion[len(splitVersion)-1]
newPatch, err := strconv.Atoi(patch)
splitVersion[len(splitVersion)-1] = strconv.FormatInt(int64(newPatch)+1, 10)
newVersion := strings.Join(splitVersion, ".")
err = ioutil.WriteFile("VERSION", []byte(newVersion), 0644)
if err != nil {
log.Panicln(err.Error())
}
runCommand("git", "add", "VERSION")
runCommand("git", "commit", "-m", "bump version to "+newVersion, "--", "VERSION")
runCommand("git", "push")
runCommand("git", "tag", newVersion)
runCommand("git", "push", "origin", newVersion)
runCommand("goreleaser", "--rm-dist")
runCommand("rm", "-rf", "dist")
}
func runCommand(args ...string) {
fmt.Println(strings.Join(args, " "))
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Start()
if err != nil {
panic(err.Error())
}
err = cmd.Wait()
if err != nil {
panic(err.Error())
}
}

View File

@@ -1,93 +0,0 @@
package main
import (
"fmt"
"github.com/jesseduffield/gocui"
)
func refreshStashEntries(g *gocui.Gui) error {
g.Update(func(g *gocui.Gui) error {
v, err := g.View("stash")
if err != nil {
panic(err)
}
state.StashEntries = getGitStashEntries()
v.Clear()
for _, stashEntry := range state.StashEntries {
fmt.Fprintln(v, stashEntry.DisplayString)
}
return resetOrigin(v)
})
return nil
}
func getSelectedStashEntry(v *gocui.View) *StashEntry {
if len(state.StashEntries) == 0 {
return nil
}
lineNumber := getItemPosition(v)
return &state.StashEntries[lineNumber]
}
func renderStashOptions(g *gocui.Gui) error {
return renderOptionsMap(g, map[string]string{
"space": "apply",
"k": "pop",
"d": "drop",
"← → ↑ ↓": "navigate",
})
}
func handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error {
if err := renderStashOptions(g); err != nil {
return err
}
go func() {
stashEntry := getSelectedStashEntry(v)
if stashEntry == nil {
renderString(g, "main", "No stash entries")
return
}
diff, _ := getStashEntryDiff(stashEntry.Index)
renderString(g, "main", diff)
}()
return nil
}
func handleStashApply(g *gocui.Gui, v *gocui.View) error {
return stashDo(g, v, "apply")
}
func handleStashPop(g *gocui.Gui, v *gocui.View) error {
return stashDo(g, v, "pop")
}
func handleStashDrop(g *gocui.Gui, v *gocui.View) error {
return createConfirmationPanel(g, v, "Stash drop", "Are you sure you want to drop this stash entry?", func(g *gocui.Gui, v *gocui.View) error {
return stashDo(g, v, "drop")
}, nil)
}
func stashDo(g *gocui.Gui, v *gocui.View, method string) error {
stashEntry := getSelectedStashEntry(v)
if stashEntry == nil {
return createErrorPanel(g, "No stash to "+method)
}
if output, err := gitStashDo(stashEntry.Index, method); err != nil {
createErrorPanel(g, output)
}
refreshStashEntries(g)
return refreshFiles(g)
}
func handleStashSave(g *gocui.Gui, filesView *gocui.View) error {
createPromptPanel(g, filesView, "Stash changes", func(g *gocui.Gui, v *gocui.View) error {
if output, err := gitStashSave(trimmedContent(v)); err != nil {
createErrorPanel(g, output)
}
refreshStashEntries(g)
return refreshFiles(g)
})
return nil
}

View File

@@ -1,43 +0,0 @@
package main
import (
"fmt"
"time"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
)
func refreshStatus(g *gocui.Gui) error {
v, err := g.View("status")
if err != nil {
panic(err)
}
// for some reason if this isn't wrapped in an update the clear seems to
// be applied after the other things or something like that; the panel's
// contents end up cleared
g.Update(func(*gocui.Gui) error {
v.Clear()
pushables, pullables := gitUpstreamDifferenceCount()
fmt.Fprint(v, "↑"+pushables+"↓"+pullables)
branches := state.Branches
if err := updateHasMergeConflictStatus(); err != nil {
return err
}
if state.HasMergeConflicts {
colour := color.New(color.FgYellow)
fmt.Fprint(v, coloredString(" (merging)", colour))
}
if len(branches) == 0 {
return nil
}
branch := branches[0]
// utilising the fact these all have padding to only grab the name
// from the display string with the existing coloring applied
fmt.Fprint(v, " "+branch.DisplayString[4:])
colorLog(color.FgCyan, time.Now().Sub(startTime))
return nil
})
return nil
}

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