Compare commits

...
173 Commits
Author SHA1 Message Date
Jesse Duffield 31e201ca52 allow configuring side panel width 2020-03-04 00:12:23 +11:00
Jesse Duffield 0abd7ad6be update config 2020-03-04 00:12:23 +11:00
Jesse Duffield b3522c48d9 refactor 2020-03-04 00:12:23 +11:00
Jesse Duffield 0fc58a7986 fix test 2020-03-04 00:12:23 +11:00
Jesse Duffield 54241d8ab9 more generic way of supporting custom pagers 2020-03-04 00:12:23 +11:00
Jesse Duffield 355f1615ab supporing custom pagers step 1 2020-03-04 00:12:23 +11:00
Jesse Duffield 113252b0ae bump gocui 2020-03-04 00:12:23 +11:00
Jesse Duffield 1cd7d14029 Update README.md 2020-03-04 00:11:32 +11:00
Jesse Duffield 87c2fb6a4a Update Custom_Pagers.md 2020-03-04 00:06:49 +11:00
Jesse Duffield 9912998bb7 Create Custom_Pagers.md 2020-03-03 23:07:12 +11:00
Patrick DeVivo e223d3d8de Add TODOs badge to README
Closes #685
2020-03-02 22:39:19 +11:00
William Wagner Moraes Artero ec31fc4cc7 docs: moved services conf docs to config.md 2020-03-01 10:57:12 +11:00
William Wagner Moraes Artero 3ce2b9b79a chore: keeping coverage up :D 2020-03-01 10:57:12 +11:00
William Wagner Moraes Artero a79182e50d fix: accidentally escaped %s 2020-03-01 10:57:12 +11:00
William Wagner Moraes Artero 6f4c595dde docs: configuration and pull request services' settings 2020-03-01 10:57:12 +11:00
William Wagner Moraes Artero 0eb3090ad6 fix: owner groups (GitLab) 2020-03-01 10:57:12 +11:00
William Wagner Moraes Artero 6ea25bd259 feat: flexible service configuration 2020-03-01 10:57:12 +11:00
William Wagner Moraes Artero fe5f087f9c feat: configurable services 2020-03-01 10:57:12 +11:00
Jesse Duffield 79299be3b2 better keybindings for patch building mode 2020-02-29 18:48:10 +11:00
Jesse Duffield 4c9b620bd0 better keybindings for staging by line 2020-02-29 18:48:10 +11:00
Jesse Duffield a7508a5dfd fix cheatsheet script to support different contexts 2020-02-29 17:46:00 +11:00
hitsuji no shippo 1a3d765c4c fix keybinds document 2020-02-28 23:08:14 +11:00
Dawid Dziurla 4058c71ca0 Merge pull request #684 from ueberBrot/add-scoop-install-option
Add scoop install option to README.
2020-02-27 18:31:21 +01:00
Maurice de Bruyn 3fc22a6010 Add scoop install option to README.
Adds the install option scoop on Windows to the README.
2020-02-27 18:12:23 +01:00
David Chen a9fe0b8000 set --abbrev-commit to return 8-digit hash strings 2020-02-27 18:05:41 +11:00
David Chen 5af7b0235e fix #680: unpushed commits still appear to be green instead of red 2020-02-27 18:05:41 +11:00
Corentin Rossignon bf946200e9 Fix OutOfBound array access when looking for ReflogCommits
refs #679
2020-02-27 09:34:40 +11:00
Jesse Duffield 890cc87724 fix bug where commits appeared as green despite not being pushed 2020-02-27 09:33:09 +11:00
Jesse Duffield 8eb0b0f4ca do not close over variables in a function 2020-02-25 22:09:43 +11:00
Jesse Duffield e6a8dc0bcf better logic for checking if we're rebasing 2020-02-25 22:09:43 +11:00
Jesse Duffield 02c497fad6 show file list when diffing commits 2020-02-25 21:38:38 +11:00
Jesse Duffield d0ab747479 color active frames green by default 2020-02-25 21:27:50 +11:00
Jesse Duffield f94d0be2c9 refactor the way we render lists 2020-02-25 21:21:07 +11:00
Jesse Duffield 9fd9fd6816 better commit lines in fullscreen mode 2020-02-25 21:21:07 +11:00
David Chen b8717d750a keybinding doc for nextMatch/prevMatch in Config.md (#659) 2020-02-25 09:37:28 +11:00
Jesse Duffield 8ad01fe32f refresh commits when adding a tag 2020-02-25 09:10:23 +11:00
Jesse Duffield fdb543fa7d add half and fullscreen modes 2020-02-25 08:45:30 +11:00
Jesse Duffield 52b5a6410c show item counts in frames 2020-02-25 07:19:46 +11:00
Jesse Duffield 0034cfef5c show tags in commits panel 2020-02-24 23:13:54 +11:00
Jesse Duffield 78b62be96f better handling of clearing the search 2020-02-24 22:18:04 +11:00
Jesse Duffield 1f5ccab1ce eagerload commits when searching 2020-02-24 22:18:04 +11:00
Jesse Duffield 46be280c92 support searching in side panels
For now we're just doing side panels, because it will take more work
to support this in the various main panel contexts
2020-02-24 22:18:04 +11:00
Jesse Duffield 2a5763a771 switch custom command keybinding to ':' 2020-02-24 22:04:39 +11:00
Jesse Duffield 370cec098b show diff stat 2020-02-24 09:20:50 +11:00
Dawid Dziurla 49a2f0191f tasks: don't use a function that requires Go 1.12 2020-02-24 09:09:27 +11:00
Jesse Duffield fabdda0492 allow customizing background color in staging mode 2020-02-23 18:37:19 +11:00
Glenn Vriesman 6fc3290a05 Reflog: Use 20 sha digits instead of 7
Signed-off-by: Glenn Vriesman <glenn.vriesman@gmail.com>
2020-02-20 08:34:01 +11:00
Jesse Duffield 66e6369c28 allow fastforwarding the current branch 2020-02-18 23:07:38 +11:00
Jesse Duffield 0f0da9c32a fix wording 2020-02-16 09:57:49 +11:00
Jesse Duffield 0a69c1a02d add reset to reflog commit menu 2020-02-16 09:57:49 +11:00
Jesse Duffield feaf98bd01 add reset to upstream option on files panel 2020-02-16 09:57:49 +11:00
Jesse Duffield 0fe9c15ce8 add mixed option to HEAD resetting, remove @{upstream} 2020-02-16 09:57:49 +11:00
Jesse Duffield f528e12c83 allow resetting to tag 2020-02-16 09:57:49 +11:00
Jesse Duffield 8ca9f93ccf allow resetting to remote branch 2020-02-16 09:57:49 +11:00
Jesse Duffield 73d8064837 allow resetting to branch 2020-02-16 09:57:49 +11:00
Jesse Duffield 5b1f60b7eb refactor create reset menu logic 2020-02-16 09:57:49 +11:00
Jesse Duffield 2e1344f611 fix specs 2020-02-15 08:47:36 +11:00
Jesse Duffield 5b9996b16f remove old createMenu function 2020-02-15 08:47:36 +11:00
Jesse Duffield 6fdc1791e4 refactor stash options menu 2020-02-15 08:47:36 +11:00
Jesse Duffield fd4f37b5c3 refactor git flow menu 2020-02-15 08:47:36 +11:00
Jesse Duffield d76e8887e5 refactor patch options menu panel 2020-02-15 08:47:36 +11:00
Jesse Duffield eb9134685a refactor rebase menu panel 2020-02-15 08:47:36 +11:00
Jesse Duffield d929b84786 refactor recent repos menu panel 2020-02-15 08:47:36 +11:00
Jesse Duffield 8ef3297b11 refactor reflog reset options panel 2020-02-15 08:47:36 +11:00
Jesse Duffield 27c7aeb117 refactor workspace reset options panel 2020-02-15 08:47:36 +11:00
Jesse Duffield c9714600e8 refactor commit reset menu 2020-02-15 08:47:36 +11:00
Jesse Duffield 665fdded14 continue refactor of menu panel 2020-02-15 08:47:36 +11:00
Jesse Duffield 814a0ea36f begin refactor of menu panel 2020-02-15 08:47:36 +11:00
Jesse Duffield 71e018a3dd get whole commit SHA from rebase commits 2020-02-13 18:10:14 +11:00
Jesse Duffield efb26f8b60 refresh current branch graph when side panels refresh 2020-02-10 19:05:55 +11:00
Glenn Vriesman d9eb6e2682 Fixed tests
Signed-off-by: Glenn Vriesman <glenn.vriesman@gmail.com>
2020-02-09 23:47:22 +11:00
Glenn Vriesman b74107f2ba Use 8 instead of 7 digit long sha
Signed-off-by: Glenn Vriesman <glenn.vriesman@gmail.com>
2020-02-09 23:47:22 +11:00
Glenn Vriesman 0cd91a10c6 Increase internal sha size
This does not change the sha size that is displayed to the user

Signed-off-by: Glenn Vriesman <glenn.vriesman@gmail.com>
2020-02-09 23:47:22 +11:00
Jesse Duffield f062e1dcda ignore carriage returns 2020-02-09 16:43:02 +11:00
Glenn Vriesman 9f5397a2d4 Moved function to git.go
Signed-off-by: Glenn Vriesman <glenn.vriesman@gmail.com>
2020-02-06 23:19:29 +11:00
Glenn Vriesman 0164abbd4a Added feature to ignore tracked files
Signed-off-by: Glenn Vriesman <glenn.vriesman@gmail.com>
2020-02-06 23:19:29 +11:00
Jesse Duffield e92af63636 fix goreleaser 2020-02-06 09:45:50 +11:00
Marco Molteni 94501c683b doc: mention config file location for MacOS 2020-02-06 09:36:29 +11:00
Glenn Vriesman 047c3cf880 Added more keybinds
* Commit with editor
 * Commit without hook

Signed-off-by: Glenn Vriesman <glenn.vriesman@gmail.com>
2020-02-04 23:21:51 +11:00
Glenn Vriesman 47d7d87c82 Added commit keybinding to staging views 2020-02-04 23:21:51 +11:00
Glenn Vriesman 5f53d50492 Check cached when showing new file diffs
Signed-off-by: Glenn Vriesman <glenn.vriesman@gmail.com>
2020-02-04 08:41:41 +11:00
Jesse Duffield 5f71f87496 correctly compare new main height to previous 2020-02-03 21:50:31 +11:00
Chris Taylor c6cb90e8ca verify that VISUAL,EDITOR,LGCC envvars are set for non-interactive commands 2020-02-02 11:29:22 +11:00
Chris Taylor fb156bcaac add a helper to search a list for a pattern 2020-02-02 11:29:22 +11:00
Chris Taylor 75ba2196ba perpetuate this style of dependency injection 2020-02-02 11:29:22 +11:00
Chris Taylor 4cb50b15e4 make amend more non-interactive 2020-02-02 11:29:22 +11:00
Jesse Duffield ca5cbe4d44 bump gocui 2020-02-02 11:26:24 +11:00
Jesse Duffield df050472a1 more ticker improvements 2020-02-02 11:26:24 +11:00
Jesse Duffield c173ebf5b9 bump vendor directory 2020-02-01 00:23:22 +11:00
Jesse Duffield 434582b5f5 explicitly tell gocui when to start animating the loader 2020-02-01 00:23:22 +11:00
Jesse Duffield cf6be928a3 only rerender app status when we need to 2020-02-01 00:23:22 +11:00
Jesse Duffield c907c55144 close more things when switching repos or to a subprocess 2020-01-31 20:53:08 +11:00
David Chen ee433ab909 Update example config for Colemak Keyboard Layout users
I realized that the current example config in `Config.md` for a Colemak keyboard layout user will cause key conflicts in certain panels. This change addresses that issue.
2020-01-31 19:22:30 +11:00
Jesse Duffield bf69923b6d fix keybinding issues with freebsd/openbsd 2020-01-31 08:51:24 +11:00
Jesse Duffield 64782a433e fix segfault on line by line panel
The state object is sometimes undefined in the onclick method of the
line by line panel. Because we set it to nil in a bunch of places,
I've decided to just change the main context to 'normal' before setting
it to nil anywhere. That way the keybindings for the line by line panel
won't get executed and we won't get a segfault.
2020-01-31 08:27:49 +11:00
Jesse Duffield 44edb49a6e handle files that were deleted downstream but modified upstream 2020-01-29 19:07:47 +11:00
Jesse Duffield 1a6d269063 split main view vertically
When staging lines (or doing anything that requires the main view to split into two)
we want to split vertically if there's not much width available in the window.
If there is enough width we will split horizontally. The aim here is to allow for
sufficient room in the side panel. We might need to tweak this or make it configurable
but I think it's set to a pretty reasonable default i.e. switching to split vertically
when the window width falls under 220
2020-01-29 18:44:50 +11:00
Jesse Duffield b64953ebdb safely unstage lines 2020-01-29 18:19:11 +11:00
Jesse Duffield deaa2bcb15 remove rollbar 2020-01-29 17:29:36 +11:00
Jesse Duffield c166c57c5d make use of branch config when pushing/pulling 2020-01-29 15:19:19 +11:00
Jesse Duffield 6b77e4ee4a fix comment 2020-01-28 22:18:55 +11:00
Jesse Duffield e5534f060d use reflog timestamps rather than commit timestamps to show commit recency 2020-01-28 22:12:48 +11:00
Dawid Dziurla 466e0be560 Merge pull request #597 from jamiebrynes7/bugfix/fix-crash-on-exit
Fix crash on exit
2020-01-16 07:38:25 +01:00
Jamie Brynes 810adab957 handle case where file watcher is disabled 2020-01-16 00:30:53 +00:00
Jesse Duffield 83a3c9fc8d handle when fsnotify doesn't work 2020-01-12 14:46:23 +11:00
Jesse Duffield 5e95019b3f Missed a spot with this new string task thing
The issue here was that we were using a string task
but expecting to be able to set the origin straight after
to point at the conflict, but because it's async it was
actually resetting the origin to 0 after a little bit.

The proper solution here is maybe to add a flag to that thing
asking whether you want to reset main's origin. But I'm
too lazy to do that right now so instead I'm just using
setViewContent. That will probably cause issues in the future.
2020-01-12 14:43:17 +11:00
Jesse Duffield 8e7f278094 use mutexes on escape code 2020-01-12 14:01:45 +11:00
Jesse Duffield 83a895a463 reset origin when clicking on list item 2020-01-12 13:55:14 +11:00
Jesse Duffield 59ae1e1599 bump gocui 2020-01-12 13:55:14 +11:00
Jesse Duffield 77a82e9d51 use view line height to see if you should stop scrolling 2020-01-12 13:55:14 +11:00
Jesse Duffield bd79c2e8dc keep track of current view when pushing 2020-01-12 11:17:20 +11:00
Jesse Duffield 23bcc19180 allow fast flicking through any list panel
Up till now our approach to rendering things like file diffs, branch logs, and
commit patches, has been to run a command on the command line, wait for it to
complete, take its output as a string, and then write that string to the main
view (or secondary view e.g. when showing both staged and unstaged changes of a
file).

This has caused various issues. For once, if you are flicking through a list of
files and an untracked file is particularly large, not only will this require
lazygit to load that whole file into memory (or more accurately it's equally
large diff), it also will slow down the UI thread while loading that file, and
if the user continued down the list, the original command might eventually
resolve and replace whatever the diff is for the newly selected file.

Following what we've done in lazydocker, I've added a tasks package for when you
need something done but you want it to cancel as soon as something newer comes
up. Given this typically involves running a command to display to a view, I've
added a viewBufferManagerMap struct to the Gui struct which allows you to define
these tasks on a per-view basis.

viewBufferManagers can run files and directly write the output to their view,
meaning we no longer need to use so much memory.

In the tasks package there is a helper method called NewCmdTask which takes a
command, an initial amount of lines to read, and then runs that command, reads
that number of lines, and allows for a readLines channel to tell it to read more
lines. We read more lines when we scroll or resize the window.

There is an adapter for the tasks package in a file called tasks_adapter which
wraps the functions from the tasks package in gui-specific stuff like clearing
the main view before starting the next task that wants to write to the main
view.

I've removed some small features as part of this work, namely the little headers
that were at the top of the main view for some situations. For example, we no
longer show the upstream of a selected branch. I want to re-introduce this in
the future, but I didn't want to make this tasks system too complicated, and in
order to facilitate a header section in the main view we'd need to have a task
that gets the upstream for the current branch, writes it to the header, then
tells another task to write the branch log to the main view, but without
clearing inbetween. So it would get messy. I'm thinking instead of having a
separate 'header' view atop the main view to render that kind of thing (which
can happen in another PR)

I've also simplified the 'git show' to just call 'git show' and not do anything
fancy when it comes to merge commits.

I considered using this tasks approach whenever we write to a view. The only
thing is that the renderString method currently resets the origin of a view and
I don't want to lose that. So I've left some in there that I consider harmless,
but we should probably be just using tasks now for all rendering, even if it's
just strings we can instantly make.
2020-01-12 11:17:20 +11:00
Jesse Duffield 282f08df36 lazyload commits 2020-01-12 10:10:56 +11:00
Jesse Duffield d647a96ed5 add reflog reset options 2020-01-09 22:36:07 +11:00
Jesse Duffield 1b64ea3210 add checkout reflog commit keybinding 2020-01-09 22:36:07 +11:00
Jesse Duffield 9b32e99eb8 add reflog tab in commits panel 2020-01-09 22:36:07 +11:00
Jesse Duffield 79e696d8a7 switch to 'i' for toggling diff commits 2020-01-08 22:59:12 +11:00
Jesse Duffield a3ea19be8e update to new goreleaser schema 2020-01-08 22:48:35 +11:00
Jesse Duffield e0015a52e5 refresh side panels when resetting to upstream 2020-01-08 22:30:54 +11:00
Jamie Brynes aea4661be5 escape editor path 2020-01-08 22:24:36 +11:00
Jesse Duffield 80377e4716 add git flow support 2020-01-08 22:03:15 +11:00
Jesse Duffield c3d54f3c2e don't watch deleted files 2020-01-08 21:57:39 +11:00
Jesse Duffield c7d367a791 minor fixup 2020-01-08 21:55:52 +11:00
Jesse Duffield ba4253668d reduce to 50 2020-01-08 21:41:39 +11:00
Jesse Duffield 1ce5c69cd2 improve file watching
By default, macs have 256 open files allowed by a given process.
This sucks when you end up with over 256 files modified in a repo
because after you've watched all of them, lots of other calls to
the command line will fail due to violating the limit.

Given there's no easy platform agnostic way to see what you've got
configured for how many files a process can have open, I'm going to
arbitrarily set the max to 200 and when we hit the limit we start
unwatching older files to make way for new ones.

WIP
2020-01-08 21:34:02 +11:00
David Chen 205d731d7b added a seperate keybinding option for checking out commits 2020-01-07 19:14:54 -08:00
David Chen 3e875cc593 fix display of menu option keybindings 2020-01-07 13:26:29 -08:00
David Chen de2cfc7e17 cleanup 2020-01-07 12:48:11 -08:00
David Chen e72cab81c1 customizable keybinding for toggleDiffCommit 2020-01-07 10:03:13 -08:00
David Chen 844a2db83a Merge branch 'master' into custom-keybindings 2020-01-07 09:57:06 -08:00
David Chen 529ba45cc7 fixed keybinding display in merge_panel.go 2020-01-07 09:50:25 -08:00
Jesse Duffield 09aabce3cd allow commits to be checked out 2020-01-07 20:43:01 +11:00
Jesse Duffield eb2bfd3848 allow hard resetting to upstream branch 2020-01-07 20:26:01 +11:00
Dawid Dziurla adb5c8fe06 Merge pull request #576 from mattn/set-ascii
Use ASCII on Windows with east asian locale
2020-01-07 05:09:35 +01:00
Yasuhiro Matsumoto d914d40b2e Use ASCII on Windows with east asian locale 2020-01-07 11:32:11 +09:00
David Chen 66c7672a0c updated keybinding config docs 2020-01-07 08:38:07 +08:00
David Chen 983379d334 Merge branch 'master' into custom-keybindings 2020-01-07 00:03:49 +08:00
David Chen fd72a09d1e if statements to map 2020-01-06 23:37:33 +08:00
David Chen 0ddf7c05c8 PickBothHunks -> pickBothHunks 2020-01-06 23:37:21 +08:00
Dawid Dziurla 818134247d Merge pull request #575 from jesseduffield/ubuntu-daily-deprecate
README: drop ubuntu daily
2020-01-06 15:29:49 +01:00
Dawid Dziurla ed020e18a0 README: drop ubuntu daily 2020-01-06 15:26:29 +01:00
Dawid Dziurla 9e5acb84c0 Merge pull request #574 from jamiebrynes7/bugfix/apply-patch-windows
Fix applying patch on Windows machines
2020-01-05 21:46:15 +01:00
Jamie Brynes 5e45ae1584 fix applying patch on Windows machine
This bug was caused how the timestamp was formatted for the patch file.

On Windows machines, ":" is an invalid character for a filename, but the
`stampNano` format for time contains ":".

This fix adjusts the time format to be the `stampNano` format with "."
subsituted for ":".
2020-01-05 20:01:20 +00:00
David Chen 86b101c410 Merge branch 'master' into custom-keybindings 2020-01-04 08:12:36 +00:00
Dawid Dziurla 96ca7262e4 Merge pull request #572 from matejcik/master
make Ctrl+P visible
2020-01-03 15:13:48 +01:00
matejcik 0a31edecb6 make Ctrl+P visible 2020-01-03 15:09:59 +01:00
Dawid Dziurla 6ac6d142c0 Merge pull request #573 from jesseduffield/ubuntu-readme-update
README: update ubuntu info
2020-01-03 14:58:29 +01:00
Dawid Dziurla 5bb9900220 README: update ubuntu info 2020-01-03 14:49:51 +01:00
David Chen f99a200db0 Merge branch 'master' into custom-keybindings 2019-12-14 10:57:46 -08:00
Dawid Dziurla 24e73cdd8b Merge pull request #564 from jesseduffield/update-deps
update dependencies
2019-12-13 11:36:05 +01:00
Dawid Dziurla be8f589c32 update dependencies 2019-12-13 11:31:04 +01:00
Dawid Dziurla 3074ae99ea Merge pull request #560 from tim77/tim77-patch-1
Fedora installation instructions
2019-12-13 11:24:22 +01:00
Artem Polishchuk 873fe41ab3 Fedora installation instructions 2019-12-13 11:19:42 +01:00
David Chen 029de4ac86 re-position key names so that the menu will show 'enter' instead of 'ctrl-m', or 'esc' instead of 'ctrl-[' 2019-12-08 14:57:29 -08:00
David Chen 5f21f190b9 Merge branch 'master' into custom-keybindings 2019-12-08 14:40:11 -08:00
Jesse Duffield dab78c8a63 stop the files panel from stealing focus whenever files are refreshed 2019-12-08 21:27:28 +11:00
David Chen 0d1230a959 added keybinding for fetchRemote 2019-12-07 09:26:17 -08:00
David Chen c507e5f562 Merge branch 'master' into custom-keybindings 2019-12-07 09:19:43 -08:00
David Chen 63e353ad6a updated docs/examples 2019-12-06 22:48:19 -08:00
David Chen 7194dfa43c better error messages 2019-12-06 22:39:41 -08:00
David Chen e425f1df87 suggested keybinding improvements 2019-12-06 22:36:52 -08:00
Jesse Duffield 3f4613feb0 allow fetching remotes with 'f' 2019-12-07 16:23:04 +11:00
Jesse Duffield 033c21754b fix commit message char count 2019-12-07 16:21:26 +11:00
David Chen c89c35c6b3 bug fix: ctrl+combinations was not showing up in help menu 2019-12-06 10:26:39 -08:00
David Chen 1dbfea54bc better error handling 2019-12-04 19:16:47 -08:00
David Chen 0af8784707 added all possible custom keybindings to the documentation 2019-12-04 18:46:00 -08:00
David Chen 2ca5766f56 included default config file path 2019-12-04 18:35:29 -08:00
David Chen a0b842204c exmaple keybindings for colemak users 2019-12-04 18:35:07 -08:00
David Chen 0a26050b47 fix 2019-12-04 18:33:00 -08:00
David Chen c50ab9872d update documentation for custom keybindings 2019-12-04 18:18:28 -08:00
David Chen fa6893fda9 feature: custom keybindings 2019-12-04 18:01:06 -08:00
Dawid Dziurla 710abded64 Merge pull request #554 from jesseduffield/alias-V-v-keybinding
keybindings: alias V -> v
2019-11-27 20:40:17 +01:00
Dawid Dziurla 1c38db1fc7 keybindings: alias V -> v 2019-11-27 20:37:07 +01:00
288 changed files with 49879 additions and 48262 deletions
+31 -30
View File
@@ -17,16 +17,16 @@ builds:
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: 32-bit
amd64: x86_64
format_overrides:
- goos: windows
format: zip
archives:
- replacements:
darwin: Darwin
linux: Linux
windows: Windows
386: 32-bit
amd64: x86_64
format_overrides:
- goos: windows
format: zip
checksum:
name_template: 'checksums.txt'
snapshot:
@@ -38,27 +38,28 @@ changelog:
- '^docs:'
- '^test:'
- '^bump'
brew:
# Reporitory to push the tap to.
github:
owner: jesseduffield
name: homebrew-lazygit
brews:
-
# Reporitory to push the tap to.
github:
owner: jesseduffield
name: homebrew-lazygit
# Your app's homepage.
# Default is empty.
homepage: 'https://github.com/jesseduffield/lazygit/'
# Your app's homepage.
# Default is empty.
homepage: 'https://github.com/jesseduffield/lazygit/'
# Your app's description.
# Default is empty.
description: 'A simple terminal UI for git commands, written in Go'
# Your app's description.
# Default is empty.
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
# # 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
# test comment to see if goreleaser only releases on new commits
+42 -22
View File
@@ -1,8 +1,8 @@
# 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)]()
# 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)]() [![TODOs](https://badgen.net/https/api.tickgit.com/badgen/github.com/jesseduffield/lazygit)](https://www.tickgit.com/browse?repo=github.com/jesseduffield/lazygit)
A simple terminal UI for git commands, written in Go with the [gocui](https://github.com/jroimartin/gocui 'gocui') library.
Rant time: You've heard it before, git is _powerful_, but what good is that power when everything is so damn hard to do? Interactive rebasing requires you to edit a goddamn TODO file in your editor? *Are you kidding me?* To stage part of a file you need to use a command line program stepping through each hunk and if a hunk can't be split down any further but contains code you don't want to stage, bad luck? *Are you KIDDING me?!* Sometimes you get asked to stash your changes when switching branches only to realise that after you switch and unstash that there weren't even any conflicts and it would have been fine to just checkout the branch directly? *YOU HAVE GOT TO BE KIDDING ME!*
Rant time: You've heard it before, git is _powerful_, but what good is that power when everything is so damn hard to do? Interactive rebasing requires you to edit a goddamn TODO file in your editor? *Are you kidding me?* To stage part of a file you need to use a command line program stepping through each hunk and if a hunk can't be split down any further but contains code you don't want to stage, bad luck? *Are you KIDDING me?!* Sometimes you get asked to stash your changes when switching branches only to realise that after you switch and unstash that there weren't even any conflicts and it would have been fine to just checkout the branch directly? *YOU HAVE GOT TO BE KIDDING ME!*
If you're a mere mortal like me and you're tired of hearing how powerful git is when in your daily life it's a powerful pain in your ass, lazygit might be for you.
@@ -24,9 +24,11 @@ Github Sponsors is matching all donations dollar-for-dollar for 12 months so if
## Installation
### Homebrew
Normally the lazygit formula can be found in the Homebrew core but we suggest you tap our formula to get the frequently updated one. It works with Linux, too.
Tap:
```
brew install jesseduffield/lazygit/lazygit
```
@@ -38,19 +40,17 @@ brew install lazygit
```
### MacPorts
Latest version built from github releases.
Tap:
```
sudo port install lazygit
```
### Ubuntu
Packages for Ubuntu 16.04, 18.04 and 18.10 are available via [Launchpad PPA](https://launchpad.net/~lazygit-team).
**Release builds**
Built from git tags. Supposed to be more stable.
Packages for Ubuntu are available via [Launchpad PPA](https://launchpad.net/~lazygit-team).
```sh
sudo add-apt-repository ppa:lazygit-team/release
@@ -58,16 +58,6 @@ 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
sudo apt-get update
sudo apt-get install lazygit
```
### Void Linux
Packages for Void Linux are available in the distro repo
@@ -77,6 +67,17 @@ They follow upstream latest releases
```sh
sudo xbps-install -S lazygit
```
### Scoop (Windows)
You can install `lazygit` using [scoop](https://scoop.sh/). It's in the `extras` bucket:
```sh
# Add the extras bucket
scoop bucket add extras
# Install lazygit
scoop install lazygit
```
### Arch Linux
@@ -85,15 +86,24 @@ 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/
- 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
<https://wiki.archlinux.org/index.php/Arch_User_Repository>
### Fedora and CentOS 7
Packages for Fedora and CentOS 7 are available via [Copr](https://copr.fedorainfracloud.org/coprs/atim/lazygit/) (Cool Other Package Repo).
```sh
sudo dnf copr enable atim/lazygit -y
sudo dnf install lazygit
```
### Conda
Released versions are available for different platforms, see https://anaconda.org/conda-forge/lazygit
Released versions are available for different platforms, see <https://anaconda.org/conda-forge/lazygit>
```sh
conda install -c conda-forge lazygit
@@ -125,10 +135,11 @@ whichever rc file you're using).
- Rebase Magic tutorial [here](https://youtu.be/4XaToVut_hs)
- List of keybindings
[here](/docs/keybindings).
## Changing Directory On Exit
If you change repos in lazygit and want your shell to change directory into that repo on exiting lazygit, add this to your `~/.zshrc` (or other rc file):
```
lg()
{
@@ -142,8 +153,17 @@ lg()
fi
}
```
Then `source ~/.zshrc` and from now on when you call `lg` and exit you'll switch directories to whatever you were in inside lazyigt. To override this behaviour you can exit using `shift+Q` rather than just `q`.
## Configuration
Check the [configuration docs](docs/Config.md).
## Custom Pagers
See the [docs](docs/Custom_Pagers.md)
## Cool features
- Adding files easily
+170 -11
View File
@@ -1,25 +1,37 @@
# User Config:
# User Config
## Default:
Default path for the config file:
* Linux: `~/.config/jesseduffield/lazygit/config.yml`
* MacOS: `~/Library/Application Support/jesseduffield/lazygit/config.yml`
## Default
```yaml
gui:
# stuff relating to the UI
scrollHeight: 2 # how many lines you scroll by
scrollPastBottom: true # enable scrolling past the bottom
sidePanelWidth: 0.3333 # number from 0 to 1
theme:
lightTheme: false # For terminals with a light background
activeBorderColor:
- white
- bold
inactiveBorderColor:
- white
- green
optionsTextColor:
- blue
selectedLineBgColor:
- blue
commitLength:
show: true
mouseEvents: true
skipUnstageLineWarning: false
git:
paging:
colorArg: always
useConfig: false
merging:
# only applicable to unix users
manualCommit: false
@@ -30,32 +42,128 @@
days: 14 # how often an update is checked for
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
confirmOnQuit: false
keybinding:
universal:
quit: 'q'
quit-alt1: '<c-c>' # alternative/alias of quit
return: '<esc>' # return to previous menu, will quit if there's nowhere to return
quitWithoutChangingDirectory: 'Q'
togglePanel: '<tab>' # goto the next panel
prevItem: '<up>' # go one line up
nextItem: '<down>' # go one line down
prevItem-alt: 'k' # go one line up
nextItem-alt: 'j' # go one line down
prevBlock: '<left>' # goto the previous block / panel
nextBlock: '<right>' # goto the next block / panel
prevBlock-alt: 'h' # goto the previous block / panel
nextBlock-alt: 'l' # goto the next block / panel
nextMatch: 'n'
prevMatch: 'N'
optionMenu: 'x' # show help menu
optionMenu-alt1: '?' # show help menu
select: '<space>'
goInto: '<enter>'
remove: 'd'
new: 'n'
edit: 'e'
openFile: 'o'
scrollUpMain: '<pgup>' # main panel scrool up
scrollDownMain: '<pgdown>' # main panel scrool down
scrollUpMain-alt1: 'K' # main panel scrool up
scrollDownMain-alt1: 'J' # main panel scrool down
scrollUpMain-alt2: '<c-u>' # main panel scrool up
scrollDownMain-alt2: '<c-d>' # main panel scrool down
executeCustomCommand: ':'
createRebaseOptionsMenu: 'm'
pushFiles: 'P'
pullFiles: 'p'
refresh: 'R'
createPatchOptionsMenu: '<c-p>'
nextTab: ']'
prevTab: '['
nextScreenMode: '+'
prevScreenMode: '_'
status:
checkForUpdate: 'u'
recentRepos: '<enter>'
files:
commitChanges: 'c'
commitChangesWithoutHook: 'w' # commit changes without pre-commit hook
amendLastCommit: 'A'
commitChangesWithEditor: 'C'
ignoreFile: 'i'
refreshFiles: 'r'
stashAllChanges: 's'
viewStashOptions: 'S'
toggleStagedAll: 'a' # stage/unstage all
viewResetOptions: 'D'
fetch: 'f'
branches:
createPullRequest: 'o'
checkoutBranchByName: 'c'
forceCheckoutBranch: 'F'
rebaseBranch: 'r'
mergeIntoCurrentBranch: 'M'
viewGitFlowOptions: 'i'
fastForward: 'f' # fast-forward this branch from its upstream
pushTag: 'P'
setUpstream: 'u' # set as upstream of checked-out branch
fetchRemote: 'f'
commits:
squashDown: 's'
renameCommit: 'r'
renameCommitWithEditor: 'R'
viewResetOptions: 'g'
markCommitAsFixup: 'f'
createFixupCommit: 'F' # create fixup commit for this commit
squashAboveCommits: 'S'
moveDownCommit: '<c-j>' # move commit down one
moveUpCommit: '<c-k>' # move commit up one
amendToCommit: 'A'
pickCommit: 'p' # pick commit (when mid-rebase)
revertCommit: 't'
cherryPickCopy: 'c'
cherryPickCopyRange: 'C'
pasteCommits: 'v'
tagCommit: 'T'
toggleDiffCommit: 'i'
checkoutCommit: '<space>'
stash:
popStash: 'g'
commitFiles:
checkoutCommitFile: 'c'
main:
toggleDragSelect: 'v'
toggleDragSelect-alt: 'V'
toggleSelectHunk: 'a'
pickBothHunks: 'b'
undo: 'z'
```
## Platform Defaults:
## Platform Defaults
### Windows:
### Windows
```yaml
os:
openCommand: 'cmd /c "start "" {{filename}}"'
```
### Linux:
### Linux
```yaml
os:
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'
```
### OSX:
### OSX
```yaml
os:
openCommand: 'open {{filename}}'
```
### Recommended Config Values:
### Recommended Config Values
for users of VSCode
@@ -64,7 +172,7 @@ for users of VSCode
openCommand: 'code -r {{filename}}'
```
## Color Attributes:
## Color Attributes
For color attributes you can choose an array of attributes (with max one color attribute)
The available attributes are:
@@ -82,7 +190,7 @@ The available attributes are:
- reverse # useful for high-contrast
- underline
## Light terminal theme:
## Light terminal theme
If you have issues with a light terminal theme where you can't read / see the text add these settings
@@ -95,8 +203,59 @@ If you have issues with a light terminal theme where you can't read / see the te
- bold
inactiveBorderColor:
- black
selectedLineBgColor:
- blue
```
## Example Coloring:
## Example Coloring
![border example](/docs/resources/colored-border-example.png)
## Keybindings
For all possible keybinding options, check [Custom_Keybinding.md](https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybinding.md)
### Example Keybindings For Colemak Users
```yaml
keybinding:
universal:
prevItem-alt: 'u'
nextItem-alt: 'e'
prevBlock-alt: 'n'
nextBlock-alt: 'i'
nextMatch: '='
prevMatch: '-'
new: 'k'
edit: 'o'
openFile: 'O'
scrollUpMain-alt1: 'U'
scrollDownMain-alt1: 'E'
scrollUpMain-alt2: '<c-u>'
scrollDownMain-alt2: '<c-e>'
files:
ignoreFile: 'I'
commits:
moveDownCommit: '<c-e>'
moveUpCommit: '<c-u>'
toggleDiffCommit: 'l'
branches:
viewGitFlowOptions: 'I'
```
## Custom pull request URLs
Some git provider setups (e.g. on-premises GitLab) can have distinct URLs for git-related calls and
the web interface/API itself. To work with those, Lazygit needs to know where it needs to create
the pull request. You can do so on your `config.yml` file using the following syntax:
```yaml
services:
"<gitDomain>": "<provider>:<webDomain>"
```
Where:
- `gitDomain` stands for the domain used by git itself (i.e. the one present on clone URLs), e.g. `git.work.com`
- `provider` is one of `github`, `bitbucket` or `gitlab`
- `webDomain` is the URL where your git service exposes a web interface and APIs, e.g. `gitservice.work.com`
+64
View File
@@ -0,0 +1,64 @@
# Custom Pagers
Lazygit supports custom pagers, [configured](/docs/Config.md) in the config.yml file (which can be opened by pressing 'o' in the Status panel).
Support does not extend to windows users, because we're making use of a package which doesn't have windows support.
## Default:
```yaml
git:
paging:
colorArg: always
useConfig: false
```
the `colorArg` key is for whether you want the `--color=always` arg in your `git diff` command. Some pagers want it set to always, others want it set to 'never'.
## Delta:
```yaml
git:
paging:
colorArg: always
pager: delta --dark --paging=never --24-bit-color=never
```
![](https://i.imgur.com/QJpQkF3.png)
## Diff-so-fancy
```yaml
git:
paging:
colorArg: always
pager: diff-so-fancy
```
![](https://i.imgur.com/rjH1TpT.png)
## ydiff
```yaml
gui:
sidePanelWidth: 0.2 # gives you more space to show things side-by-side
git:
paging:
colorArg: never
pager: ydiff -p cat -s --wrap --width={{columnWidth}}
```
![](https://i.imgur.com/vaa8z0H.png)
Be careful with this one, I think the homebrew and pip versions are behind master. I needed to directly download the ydiff script to get the no-pager functionality working.
## Using git config
```yaml
git:
paging:
colorArg: always
useConfig: true
```
If you set `useConfig: true`, lazygit will use whatever pager is specified in $GIT_PAGER, $PAGER, or your git config. If the pager ends with something like ' | less' we will strip that part out, because less doesn't play nice with our rendering approach. If the custom pager uses less under the hood, that will also break rendering (hence the `--paging=never` flag for the `delta` pager).
+59
View File
@@ -0,0 +1,59 @@
## Possible keybindings
| Put in | You will get |
|---------------|----------------|
| `<f1>` | F1 |
| `<f2>` | F2 |
| `<f3>` | F3 |
| `<f4>` | F4 |
| `<f5>` | F5 |
| `<f6>` | F6 |
| `<f7>` | F7 |
| `<f8>` | F8 |
| `<f9>` | F9 |
| `<f10>` | F10 |
| `<f11>` | F11 |
| `<f12>` | F12 |
| `<insert>` | Insert |
| `<delete>` | Delete |
| `<home>` | Home |
| `<end>` | End |
| `<pgup>` | Pgup |
| `<pgdown>` | Pgdn |
| `<up>` | ArrowUp |
| `<down>` | ArrowDown |
| `<left>` | ArrowLeft |
| `<right>` | ArrowRight |
| `<tab>` | Tab |
| `<enter>` | Enter |
| `<esc>` | Esc |
| `<backspace>` | Backspace |
| `<c-space>` | CtrlSpace |
| `<c-/>` | CtrlSlash |
| `<space>` | Space |
| `<c-a>` | CtrlA |
| `<c-b>` | CtrlB |
| `<c-c>` | CtrlC |
| `<c-d>` | CtrlD |
| `<c-e>` | CtrlE |
| `<c-f>` | CtrlF |
| `<c-g>` | CtrlG |
| `<c-j>` | CtrlJ |
| `<c-k>` | CtrlK |
| `<c-l>` | CtrlL |
| `<c-n>` | CtrlN |
| `<c-o>` | CtrlO |
| `<c-p>` | CtrlP |
| `<c-q>` | CtrlQ |
| `<c-r>` | CtrlR |
| `<c-s>` | CtrlS |
| `<c-t>` | CtrlT |
| `<c-u>` | CtrlU |
| `<c-v>` | CtrlV |
| `<c-w>` | CtrlW |
| `<c-x>` | CtrlX |
| `<c-y>` | CtrlY |
| `<c-z>` | CtrlZ |
| `<c-4>` | Ctrl4 |
| `<c-5>` | Ctrl5 |
| `<c-6>` | Ctrl6 |
| `<c-8>` | Ctrl8 |
+139 -65
View File
@@ -1,46 +1,21 @@
# Lazygit menu
# Lazygit Keybindings
## Global
## Global Keybindings
<pre>
<kbd>pgup</kbd>: scroll up main panel (fn+up)
<kbd>pgdown</kbd>: scroll down main panel (fn+down)
<kbd>m</kbd>: view merge/rebase options
<kbd>ctrl+p</kbd>: view custom patch options
<kbd>P</kbd>: push
<kbd>p</kbd>: pull
<kbd>R</kbd>: refresh
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode
<kbd>:</kbd>: execute custom command
</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>w</kbd>: commit changes without pre-commit hook
<kbd>A</kbd>: amend last commit
<kbd>C</kbd>: commit changes using git editor
<kbd>space</kbd>: toggle staged
<kbd>d</kbd>: view 'discard changes' options
<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>a</kbd>: stage/unstage all
<kbd>t</kbd>: add patch
<kbd>D</kbd>: view reset options
<kbd>enter</kbd>: stage individual hunks/lines
<kbd>f</kbd>: fetch
<kbd>X</kbd>: execute custom command
</pre>
## Branches
## Branches Panel (Branches Tab)
<pre>
<kbd>space</kbd>: checkout
@@ -49,12 +24,56 @@
<kbd>F</kbd>: force checkout
<kbd>n</kbd>: new branch
<kbd>d</kbd>: delete branch
<kbd>r</kbd>: rebase branch
<kbd>r</kbd>: rebase checked-out branch onto this branch
<kbd>M</kbd>: merge into currently checked out branch
<kbd>i</kbd>: show git-flow options
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>g</kbd>: view reset options
</pre>
## Commits
## Branches Panel (Remote Branches (in Remotes tab))
<pre>
<kbd>esc</kbd>: return to remotes list
<kbd>g</kbd>: view reset options
<kbd>space</kbd>: checkout
<kbd>M</kbd>: merge into currently checked out branch
<kbd>d</kbd>: delete branch
<kbd>r</kbd>: rebase checked-out branch onto this branch
<kbd>u</kbd>: set as upstream of checked-out branch
</pre>
## Branches Panel (Remotes Tab)
<pre>
<kbd>f</kbd>: fetch remote
<kbd>n</kbd>: add new remote
<kbd>d</kbd>: remove remote
<kbd>e</kbd>: edit remote
</pre>
## Branches Panel (Tags Tab)
<pre>
<kbd>space</kbd>: checkout
<kbd>d</kbd>: delete tag
<kbd>P</kbd>: push tag
<kbd>n</kbd>: create tag
<kbd>g</kbd>: view reset options
</pre>
## Commit Files Panel
<pre>
<kbd>esc</kbd>: go back
<kbd>c</kbd>: checkout file
<kbd>d</kbd>: discard this commit's changes to this file
<kbd>o</kbd>: open file
<kbd>space</kbd>: toggle file included in patch
<kbd>enter</kbd>: enter file to add selected lines to the patch
</pre>
## Commits Panel (Commits Tab)
<pre>
<kbd>s</kbd>: squash down
@@ -75,46 +94,41 @@
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>v</kbd>: paste commits (cherry-pick)
<kbd>enter</kbd>: view commit's files
<kbd>space</kbd>: select commit to diff with another commit
<kbd>space</kbd>: checkout commit
<kbd>i</kbd>: select commit to diff with another commit
<kbd>T</kbd>: tag commit
</pre>
## Stash
## Commits Panel (Reflog Tab)
<pre>
<kbd>space</kbd>: apply
<kbd>g</kbd>: pop
<kbd>d</kbd>: drop
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: view reset options
</pre>
## Commit files
## Files Panel
<pre>
<kbd>esc</kbd>: go back
<kbd>c</kbd>: checkout file
<kbd>d</kbd>: discard this commit's changes to this file
<kbd>c</kbd>: commit changes
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>A</kbd>: amend last commit
<kbd>C</kbd>: commit changes using git editor
<kbd>space</kbd>: toggle staged
<kbd>d</kbd>: view 'discard changes' options
<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 changes
<kbd>S</kbd>: view stash options
<kbd>a</kbd>: stage/unstage all
<kbd>D</kbd>: view reset options
<kbd>enter</kbd>: stage individual hunks/lines
<kbd>f</kbd>: fetch
<kbd>g</kbd>: view upstream reset options
</pre>
## Main (Normal)
<pre>
<kbd>PgDn</kbd>: scroll down (fn+up)
<kbd>PgUp</kbd>: scroll up (fn+down)
</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)
## Main Panel (Merging)
<pre>
<kbd>esc</kbd>: return to files panel
@@ -126,3 +140,63 @@
<kbd>▼</kbd>: select bottom hunk
<kbd>z</kbd>: undo
</pre>
## Main Panel (Normal)
<pre>
<kbd> ̄</kbd>: scroll down (fn+up)
<kbd>¦</kbd>: scroll up (fn+down)
</pre>
## Main Panel (Patch Building)
<pre>
<kbd>esc</kbd>: exit line-by-line mode
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>space</kbd>: stage selection
<kbd>d</kbd>: reset selection
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
</pre>
## Main Panel (Staging)
<pre>
<kbd>esc</kbd>: return to files panel
<kbd>space</kbd>: stage selection
<kbd>d</kbd>: reset selection
<kbd>tab</kbd>: switch to other panel
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>e</kbd>: edit file
<kbd>o</kbd>: open file
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
<kbd>c</kbd>: commit changes
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>C</kbd>: commit changes using git editor
</pre>
## Stash Panel
<pre>
<kbd>space</kbd>: apply
<kbd>g</kbd>: pop
<kbd>d</kbd>: drop
</pre>
## Status Panel
<pre>
<kbd>e</kbd>: edit config file
<kbd>o</kbd>: open config file
<kbd>u</kbd>: check for update
<kbd>enter</kbd>: switch to a recent repo
</pre>
+133 -59
View File
@@ -1,46 +1,21 @@
# Lazygit menu
# Lazygit Keybindings
## Global
<pre>
<kbd>pgup</kbd>: scroll up main panel (fn+up)
<kbd>pgdown</kbd>: scroll down main panel (fn+down)
<kbd>m</kbd>: bekijk merge/rebase opties
<kbd>ctrl+p</kbd>: view custom patch options
<kbd>P</kbd>: push
<kbd>p</kbd>: pull
<kbd>R</kbd>: verversen
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode
<kbd>:</kbd>: voor aangepast commando uit
</pre>
## Status
<pre>
<kbd>e</kbd>: verander config file
<kbd>o</kbd>: open config file
<kbd>u</kbd>: check voor updates
<kbd>s</kbd>: wissel naar een recente repo
</pre>
## Bestanden
<pre>
<kbd>c</kbd>: Commit veranderingen
<kbd>w</kbd>: commit veranderingen zonder pre-commit hook
<kbd>A</kbd>: wijzig laatste commit
<kbd>C</kbd>: commit veranderingen met de git editor
<kbd>space</kbd>: toggle staged
<kbd>d</kbd>: bekijk 'veranderingen ongedaan maken' opties
<kbd>e</kbd>: verander bestand
<kbd>o</kbd>: open bestand
<kbd>i</kbd>: voeg toe aan .gitignore
<kbd>r</kbd>: refresh bestanden
<kbd>S</kbd>: stash-bestanden
<kbd>a</kbd>: toggle staged alle
<kbd>t</kbd>: bewerkingen toevoegen
<kbd>D</kbd>: bekijk reset opties
<kbd>enter</kbd>: stage individuele hunks/lijnen
<kbd>f</kbd>: fetch
<kbd>X</kbd>: voor aangepast commando uit
</pre>
## Branches
## Branches Panel (Branches Tab)
<pre>
<kbd>space</kbd>: uitchecken
@@ -51,10 +26,54 @@
<kbd>d</kbd>: verwijder branch
<kbd>r</kbd>: rebase branch
<kbd>M</kbd>: merge in met huidige checked out branch
<kbd>i</kbd>: show git-flow options
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>g</kbd>: bekijk reset opties
</pre>
## Commits
## Branches Panel (Remote Branches (in Remotes tab))
<pre>
<kbd>esc</kbd>: return to remotes list
<kbd>g</kbd>: bekijk reset opties
<kbd>space</kbd>: uitchecken
<kbd>M</kbd>: merge in met huidige checked out branch
<kbd>d</kbd>: verwijder branch
<kbd>r</kbd>: rebase branch
<kbd>u</kbd>: set as upstream of checked-out branch
</pre>
## Branches Panel (Remotes Tab)
<pre>
<kbd>f</kbd>: fetch remote
<kbd>n</kbd>: add new remote
<kbd>d</kbd>: remove remote
<kbd>e</kbd>: edit remote
</pre>
## Branches Panel (Tags Tab)
<pre>
<kbd>space</kbd>: uitchecken
<kbd>d</kbd>: delete tag
<kbd>P</kbd>: push tag
<kbd>n</kbd>: create tag
<kbd>g</kbd>: bekijk reset opties
</pre>
## Commit bestanden Panel
<pre>
<kbd>esc</kbd>: ga terug
<kbd>c</kbd>: bestand uitchecken
<kbd>d</kbd>: uitsluit deze commit zijn veranderingen aan dit bestand
<kbd>o</kbd>: open bestand
<kbd>space</kbd>: toggle file included in patch
<kbd>enter</kbd>: enter file to add selected lines to the patch
</pre>
## Commits Panel (Commits Tab)
<pre>
<kbd>s</kbd>: squash beneden
@@ -75,39 +94,41 @@
<kbd>C</kbd>: kopiëer commit reeks (cherry-pick)
<kbd>v</kbd>: plak commits (cherry-pick)
<kbd>enter</kbd>: bekijk gecommite bestanden
<kbd>space</kbd>: select commit to diff with another commit
<kbd>space</kbd>: checkout commit
<kbd>i</kbd>: select commit to diff with another commit
<kbd>T</kbd>: tag commit
</pre>
## Stash
## Commits Panel (Reflog Tab)
<pre>
<kbd>space</kbd>: toepassen
<kbd>g</kbd>: pop
<kbd>d</kbd>: drop
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: bekijk reset opties
</pre>
## Commit bestanden
## Bestanden Panel
<pre>
<kbd>esc</kbd>: ga terug
<kbd>c</kbd>: bestand uitchecken
<kbd>d</kbd>: uitsluit deze commit zijn veranderingen aan dit bestand
<kbd>c</kbd>: Commit veranderingen
<kbd>w</kbd>: commit veranderingen zonder pre-commit hook
<kbd>A</kbd>: wijzig laatste commit
<kbd>C</kbd>: commit veranderingen met de git editor
<kbd>space</kbd>: toggle staged
<kbd>d</kbd>: bekijk 'veranderingen ongedaan maken' opties
<kbd>e</kbd>: verander bestand
<kbd>o</kbd>: open bestand
<kbd>i</kbd>: voeg toe aan .gitignore
<kbd>r</kbd>: refresh bestanden
<kbd>s</kbd>: stash-bestanden
<kbd>S</kbd>: view stash options
<kbd>a</kbd>: toggle staged alle
<kbd>D</kbd>: bekijk reset opties
<kbd>enter</kbd>: stage individuele hunks/lijnen
<kbd>f</kbd>: fetch
<kbd>g</kbd>: view upstream reset options
</pre>
## Hoofd (Stage Lines/Hunks)
<pre>
<kbd>esc</kbd>: ga terug naar het bestanden paneel
<kbd>▲</kbd>: selecteer de vorige lijn
<kbd>▼</kbd>: selecteer de volgende lijn
<kbd>◄</kbd>: selecteer de vorige hunk
<kbd>►</kbd>: selecteer de volgende hunk
<kbd>space</kbd>: stage lijn
<kbd>a</kbd>: stage hunk
</pre>
## Hoofd (Merging)
## Hoofd Panel (Merging)
<pre>
<kbd>esc</kbd>: ga terug naar het bestanden paneel
@@ -120,9 +141,62 @@
<kbd>z</kbd>: ongedaan maken
</pre>
## Hoofd (Normaal)
## Hoofd Panel (Normaal)
<pre>
<kbd>PgDn</kbd>: scroll omlaag (fn+up)
<kbd>PgUp</kbd>: scroll omhoog (fn+down)
<kbd> ̄</kbd>: scroll omlaag (fn+up)
<kbd>¦</kbd>: scroll omhoog (fn+down)
</pre>
## Hoofd Panel (Patch Building)
<pre>
<kbd>esc</kbd>: exit line-by-line mode
<kbd>▲</kbd>: selecteer de vorige lijn
<kbd>▼</kbd>: selecteer de volgende lijn
<kbd>◄</kbd>: selecteer de vorige hunk
<kbd>►</kbd>: selecteer de volgende hunk
<kbd>space</kbd>: stage selection
<kbd>d</kbd>: reset selection
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
</pre>
## Hoofd Panel (Stage Lines/Hunks)
<pre>
<kbd>esc</kbd>: ga terug naar het bestanden paneel
<kbd>space</kbd>: stage selection
<kbd>d</kbd>: reset selection
<kbd>tab</kbd>: switch to other panel
<kbd>▲</kbd>: selecteer de vorige lijn
<kbd>▼</kbd>: selecteer de volgende lijn
<kbd>◄</kbd>: selecteer de vorige hunk
<kbd>►</kbd>: selecteer de volgende hunk
<kbd>e</kbd>: verander bestand
<kbd>o</kbd>: open bestand
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
<kbd>c</kbd>: Commit veranderingen
<kbd>w</kbd>: commit veranderingen zonder pre-commit hook
<kbd>C</kbd>: commit veranderingen met de git editor
</pre>
## Stash Panel
<pre>
<kbd>space</kbd>: toepassen
<kbd>g</kbd>: pop
<kbd>d</kbd>: drop
</pre>
## Status Panel
<pre>
<kbd>e</kbd>: verander config file
<kbd>o</kbd>: open config file
<kbd>u</kbd>: check voor updates
<kbd>enter</kbd>: wissel naar een recente repo
</pre>
+137 -63
View File
@@ -1,46 +1,21 @@
# Lazygit menu
# Lazygit Keybindings
## Globalne
<pre>
<kbd>pgup</kbd>: scroll up main panel (fn+up)
<kbd>pgdown</kbd>: scroll down main panel (fn+down)
<kbd>m</kbd>: view merge/rebase options
<kbd>ctrl+p</kbd>: view custom patch options
<kbd>P</kbd>: push
<kbd>p</kbd>: pull
<kbd>R</kbd>: odśwież
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode
<kbd>:</kbd>: execute custom command
</pre>
## Status
<pre>
<kbd>e</kbd>: edytuj plik konfiguracyjny
<kbd>o</kbd>: otwórz plik konfiguracyjny
<kbd>u</kbd>: sprawdź aktualizacje
<kbd>s</kbd>: switch to a recent repo
</pre>
## Pliki
<pre>
<kbd>c</kbd>: commituj zmiany
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>A</kbd>: zmień ostatnie zatwierdzenie
<kbd>C</kbd>: commituj zmiany używając edytora z gita
<kbd>space</kbd>: przełącz zatwierdzenie
<kbd>d</kbd>: view 'discard changes' options
<kbd>e</kbd>: edytuj plik
<kbd>o</kbd>: otwórz plik
<kbd>i</kbd>: dodaj do .gitignore
<kbd>r</kbd>: odśwież pliki
<kbd>S</kbd>: przechowaj pliki
<kbd>a</kbd>: przełącz wszystkie zatwierdzenia
<kbd>t</kbd>: dodaj łatkę
<kbd>D</kbd>: view reset options
<kbd>enter</kbd>: zatwierdź pojedyncze linie
<kbd>f</kbd>: fetch
<kbd>X</kbd>: execute custom command
</pre>
## Gałęzie
## Gałęzie Panel (Branches Tab)
<pre>
<kbd>space</kbd>: przełącz
@@ -51,10 +26,54 @@
<kbd>d</kbd>: usuń gałąź
<kbd>r</kbd>: rebase branch
<kbd>M</kbd>: scal do obecnej gałęzi
<kbd>i</kbd>: show git-flow options
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>g</kbd>: view reset options
</pre>
## Commity
## Gałęzie Panel (Remote Branches (in Remotes tab))
<pre>
<kbd>esc</kbd>: return to remotes list
<kbd>g</kbd>: view reset options
<kbd>space</kbd>: przełącz
<kbd>M</kbd>: scal do obecnej gałęzi
<kbd>d</kbd>: usuń gałąź
<kbd>r</kbd>: rebase branch
<kbd>u</kbd>: set as upstream of checked-out branch
</pre>
## Gałęzie Panel (Remotes Tab)
<pre>
<kbd>f</kbd>: fetch remote
<kbd>n</kbd>: add new remote
<kbd>d</kbd>: remove remote
<kbd>e</kbd>: edit remote
</pre>
## Gałęzie Panel (Tags Tab)
<pre>
<kbd>space</kbd>: przełącz
<kbd>d</kbd>: delete tag
<kbd>P</kbd>: push tag
<kbd>n</kbd>: create tag
<kbd>g</kbd>: view reset options
</pre>
## Commit files Panel
<pre>
<kbd>esc</kbd>: go back
<kbd>c</kbd>: checkout file
<kbd>d</kbd>: discard this commit's changes to this file
<kbd>o</kbd>: otwórz plik
<kbd>space</kbd>: toggle file included in patch
<kbd>enter</kbd>: enter file to add selected lines to the patch
</pre>
## Commity Panel (Commits Tab)
<pre>
<kbd>s</kbd>: ściśnij w dół
@@ -75,46 +94,41 @@
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>v</kbd>: paste commits (cherry-pick)
<kbd>enter</kbd>: view commit's files
<kbd>space</kbd>: select commit to diff with another commit
<kbd>space</kbd>: checkout commit
<kbd>i</kbd>: select commit to diff with another commit
<kbd>T</kbd>: tag commit
</pre>
## Schowek
## Commity Panel (Reflog Tab)
<pre>
<kbd>space</kbd>: zastosuj
<kbd>g</kbd>: wyciągnij
<kbd>d</kbd>: porzuć
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: view reset options
</pre>
## Commit files
## Pliki Panel
<pre>
<kbd>esc</kbd>: go back
<kbd>c</kbd>: checkout file
<kbd>d</kbd>: discard this commit's changes to this file
<kbd>c</kbd>: commituj zmiany
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>A</kbd>: zmień ostatnie zatwierdzenie
<kbd>C</kbd>: commituj zmiany używając edytora z gita
<kbd>space</kbd>: przełącz zatwierdzenie
<kbd>d</kbd>: view 'discard changes' options
<kbd>e</kbd>: edytuj plik
<kbd>o</kbd>: otwórz plik
<kbd>i</kbd>: dodaj do .gitignore
<kbd>r</kbd>: odśwież pliki
<kbd>s</kbd>: przechowaj pliki
<kbd>S</kbd>: view stash options
<kbd>a</kbd>: przełącz wszystkie zatwierdzenia
<kbd>D</kbd>: view reset options
<kbd>enter</kbd>: zatwierdź pojedyncze linie
<kbd>f</kbd>: fetch
<kbd>g</kbd>: view upstream reset options
</pre>
## Main (Normal)
<pre>
<kbd>PgDn</kbd>: scroll down (fn+up)
<kbd>PgUp</kbd>: scroll up (fn+down)
</pre>
## Main (Zatwierdzanie)
<pre>
<kbd>esc</kbd>: wróć do panelu plików
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>space</kbd>: zatwierdź linię
<kbd>a</kbd>: zatwierdź kawałek
</pre>
## Main (Merging)
## Main Panel (Merging)
<pre>
<kbd>esc</kbd>: wróć do panelu plików
@@ -126,3 +140,63 @@
<kbd>▼</kbd>: select bottom hunk
<kbd>z</kbd>: undo
</pre>
## Main Panel (Normal)
<pre>
<kbd> ̄</kbd>: scroll down (fn+up)
<kbd>¦</kbd>: scroll up (fn+down)
</pre>
## Main Panel (Patch Building)
<pre>
<kbd>esc</kbd>: exit line-by-line mode
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>space</kbd>: stage selection
<kbd>d</kbd>: reset selection
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
</pre>
## Main Panel (Zatwierdzanie)
<pre>
<kbd>esc</kbd>: wróć do panelu plików
<kbd>space</kbd>: stage selection
<kbd>d</kbd>: reset selection
<kbd>tab</kbd>: switch to other panel
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>e</kbd>: edytuj plik
<kbd>o</kbd>: otwórz plik
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
<kbd>c</kbd>: commituj zmiany
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>C</kbd>: commituj zmiany używając edytora z gita
</pre>
## Schowek Panel
<pre>
<kbd>space</kbd>: zastosuj
<kbd>g</kbd>: wyciągnij
<kbd>d</kbd>: porzuć
</pre>
## Status Panel
<pre>
<kbd>e</kbd>: edytuj plik konfiguracyjny
<kbd>o</kbd>: otwórz plik konfiguracyjny
<kbd>u</kbd>: sprawdź aktualizacje
<kbd>enter</kbd>: switch to a recent repo
</pre>
+16 -12
View File
@@ -10,30 +10,34 @@ require (
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
github.com/golang/protobuf v1.3.2 // indirect
github.com/google/go-cmp v0.3.1 // indirect
github.com/integrii/flaggy v1.3.0
github.com/jesseduffield/gocui v0.3.1-0.20191116013947-b13bda319532
github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/gocui v0.3.1-0.20200301081700-d6e485450113
github.com/jesseduffield/pty v1.2.1
github.com/jesseduffield/rollrus v0.0.0-20190701125922-dd028cb0bfd7
github.com/jesseduffield/termbox-go v0.0.0-20190630083001-9dd53af7214e // indirect
github.com/jesseduffield/termbox-go v0.0.0-20200130214842-1d31d1faa3c9 // indirect
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/mattn/go-colorable v0.1.4 // indirect
github.com/mattn/go-isatty v0.0.10 // indirect
github.com/mattn/go-runewidth v0.0.6 // indirect
github.com/mattn/go-isatty v0.0.11 // indirect
github.com/mattn/go-runewidth v0.0.8
github.com/mgutz/str v1.2.0
github.com/nicksnyder/go-i18n/v2 v2.0.2
github.com/nicksnyder/go-i18n/v2 v2.0.3
github.com/onsi/ginkgo v1.10.3 // indirect
github.com/onsi/gomega v1.7.1 // indirect
github.com/pelletier/go-toml v1.6.0 // indirect
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0
github.com/sirupsen/logrus v1.4.2
github.com/spf13/viper v1.5.0
github.com/spf13/afero v1.2.2 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.6.1
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
github.com/stretchr/testify v1.4.0
github.com/tcnksm/go-gitconfig v0.1.2
golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708 // indirect
golang.org/x/net v0.0.0-20191112182307-2180aed22343 // indirect
golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea // indirect
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 // indirect
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 // indirect
golang.org/x/text v0.3.2
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/src-d/go-git.v4 v4.13.1
gopkg.in/yaml.v2 v2.2.5
gopkg.in/yaml.v2 v2.2.7
)
+46 -39
View File
@@ -54,7 +54,6 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -63,6 +62,8 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
@@ -71,22 +72,22 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/integrii/flaggy v1.3.0 h1:8I5Qqz22C6+EwUqJuaN5ITh77obI8VSg6RwYLe2VB7o=
github.com/integrii/flaggy v1.3.0/go.mod h1:tnTxHeTJbah0gQ6/K0RW0J7fMUBk9MCF5blhm43LNpI=
github.com/integrii/flaggy v1.4.0 h1:A1x7SYx4jqu5NSrY14z8Z+0UyX2S5ygfJJrfolWR3zM=
github.com/integrii/flaggy v1.4.0/go.mod h1:tnTxHeTJbah0gQ6/K0RW0J7fMUBk9MCF5blhm43LNpI=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jesseduffield/gocui v0.3.1-0.20191116013947-b13bda319532 h1:V1Lk2rm5/p27NjnlF2ezzkxDaisHNcveMNueSD7RYgs=
github.com/jesseduffield/gocui v0.3.1-0.20191116013947-b13bda319532/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20200224201655-5024a02682ed h1:glGs+mzPZOl1iHiUsBW3918WeFwqsbQQ/jtLkkQXDi4=
github.com/jesseduffield/gocui v0.3.1-0.20200224201655-5024a02682ed/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20200301081700-d6e485450113 h1:jHZRVJUWsU8HaQ0crocz0i0BkpOqFLDJEO/AtBp+Ecs=
github.com/jesseduffield/gocui v0.3.1-0.20200301081700-d6e485450113/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/pty v1.2.1 h1:7xYBiwNH0PpWqC8JmvrPq1a/ksNqyCavzWu9pbBGYWI=
github.com/jesseduffield/pty v1.2.1/go.mod h1:7jlS40+UhOqkZJDIG1B/H21xnuET/+fvbbnHCa8wSIo=
github.com/jesseduffield/roll v0.0.0-20190629104057-695be2e62b00 h1:+JaOkfBNYQYlGD7dgru8mCwYNEc5tRRI8mThlVANhSM=
github.com/jesseduffield/roll v0.0.0-20190629104057-695be2e62b00/go.mod h1:cWNQljQAWYBp4wchyGfql4q2jRNZXxiE1KhVQgz+JaM=
github.com/jesseduffield/rollrus v0.0.0-20190701125922-dd028cb0bfd7 h1:CRD7bVjlGIiV+M0jlsa+XWpneW0KY0e7Y4z3GWb5S4o=
github.com/jesseduffield/rollrus v0.0.0-20190701125922-dd028cb0bfd7/go.mod h1:VspA3aTkEo0Q7TPCLmX1uHNP+Wb4iSDX09hmTRo1QYc=
github.com/jesseduffield/termbox-go v0.0.0-20190630083001-9dd53af7214e h1:tth7wr6+sfSbdpRWWrwvLYyS56HyIRVfq0Qcl2h28wM=
github.com/jesseduffield/termbox-go v0.0.0-20190630083001-9dd53af7214e/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4=
github.com/jesseduffield/termbox-go v0.0.0-20200130214842-1d31d1faa3c9 h1:iBBk1lhFwjwJw//J2m1yyz9S368GeXQTpMVACTyQMh0=
github.com/jesseduffield/termbox-go v0.0.0-20200130214842-1d31d1faa3c9/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
@@ -94,8 +95,9 @@ github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -108,10 +110,10 @@ github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-runewidth v0.0.6 h1:V2iyH+aX9C5fsYCpK60U8BYIvmhqxuOL3JZcqc1NB7k=
github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0=
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mgutz/str v1.2.0 h1:4IzWSdIz9qPQWLfKZ0rJcV0jcUDpxvP4JVZ4GXQyvSw=
github.com/mgutz/str v1.2.0/go.mod h1:w1v0ofgLaJdoD0HpQ3fycxKD1WtxpjSo151pK/31q6w=
@@ -120,8 +122,8 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nicksnyder/go-i18n/v2 v2.0.2 h1:KsHGcTByIM0mHZKQGy0nlJLOjPNjQ6MVib/3PvsBDNY=
github.com/nicksnyder/go-i18n/v2 v2.0.2/go.mod h1:JXS4+OKhbcwDoVTEj0sLFWL1vOwec2g/YBAxZ9owJqY=
github.com/nicksnyder/go-i18n/v2 v2.0.3 h1:ks/JkQiOEhhuF6jpNvx+Wih1NIiXzUnZeZVnJuI8R8M=
github.com/nicksnyder/go-i18n/v2 v2.0.3/go.mod h1:oDab7q8XCYMRlcrBnaY/7B1eOectbvj6B1UPBT+p5jo=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY=
@@ -129,8 +131,9 @@ github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+
github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -151,21 +154,27 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 h1:Xuk8ma/ibJ1fOy4Ee11vHhUFHQNpHhrBneOCNHVXS5w=
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0/go.mod h1:7AwjWCpdPhkSmNAgUv5C7EJ4AbmjEB3r047r3DXWu3Y=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.5.0 h1:GpsTwfsQ27oS/Aha/6d1oD7tpKIqWnOA6tgOX9HHkt4=
github.com/spf13/viper v1.5.0/go.mod h1:AkYRkVJF8TkSG/xet6PzXX+l39KhhXa2pdqVSxnTcn4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk=
github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4=
@@ -195,10 +204,9 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708 h1:pXVtWnwHkrWD9ru3sDxY/qFK/bfc0egRovX91EjWjf4=
golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -210,10 +218,9 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191112182307-2180aed22343 h1:00ohfJ4K98s3m6BGUoBd8nyfp4Yl0GoIKvw5abItTjI=
golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -230,12 +237,10 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e h1:D5TXcfTk7xF7hvieo4QErS3qqCB4teTffacDWr7CI+0=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be h1:QAcqgptGM8IQBC9K/RC4o+O9YmqEm0diQn9QmZw/0mU=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea h1:Mz1TMnfJDRJLk8S8OPCoJYgrsp/Se/2TBre2+vwX128=
golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 h1:gSbV7h1NRL2G1xTg/owz62CST1oJBmxy4QpMMregXVQ=
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@@ -244,6 +249,7 @@ golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
@@ -252,12 +258,13 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
@@ -273,6 +280,6 @@ gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bl
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-8
View File
@@ -14,7 +14,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/updates"
"github.com/jesseduffield/rollrus"
"github.com/shibukawa/configdir"
"github.com/sirupsen/logrus"
)
@@ -73,9 +72,7 @@ func newDevelopmentLogger(config config.AppConfigurer) *logrus.Logger {
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)
@@ -85,11 +82,6 @@ func newLogger(config config.AppConfigurer) *logrus.Entry {
// 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(),
-37
View File
@@ -1,15 +1,5 @@
package commands
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// Branch : A git branch
// duplicating this for now
type Branch struct {
@@ -17,31 +7,4 @@ type Branch struct {
Recency string
Pushables string
Pullables string
Selected bool
}
// GetDisplayStrings returns the display string of branch
func (b *Branch) GetDisplayStrings(isFocused bool) []string {
displayName := utils.ColoredString(b.Name, GetBranchColor(b.Name))
if isFocused && b.Selected && b.Pushables != "" && b.Pullables != "" {
displayName = fmt.Sprintf("%s ↑%s↓%s", displayName, b.Pushables, b.Pullables)
}
return []string{b.Recency, displayName}
}
// GetBranchColor branch color
func GetBranchColor(name string) color.Attribute {
branchType := strings.Split(name, "/")[0]
switch branchType {
case "feature":
return color.FgGreen
case "bugfix":
return color.FgYellow
case "hotfix":
return color.FgRed
default:
return theme.DefaultTextColor
}
}
+17 -9
View File
@@ -48,7 +48,7 @@ func (b *BranchListBuilder) obtainCurrentBranch() *Branch {
func (b *BranchListBuilder) obtainReflogBranches() []*Branch {
branches := make([]*Branch, 0)
// if we directly put this string in RunCommandWithOutput the compiler complains because it thinks it's a format string
unescaped := "git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD"
unescaped := "git reflog --date=relative --pretty='%gd|%gs' --grep-reflog='checkout: moving' HEAD"
rawString, err := b.GitCommand.OSCommand.RunCommandWithOutput(unescaped)
if err != nil {
return branches
@@ -56,9 +56,11 @@ func (b *BranchListBuilder) obtainReflogBranches() []*Branch {
branchLines := utils.SplitLines(rawString)
for _, line := range branchLines {
timeNumber, timeUnit, branchName := branchInfoFromLine(line)
timeUnit = abbreviatedTimeUnit(timeUnit)
branch := &Branch{Name: branchName, Recency: timeNumber + timeUnit}
recency, branchName := branchInfoFromLine(line)
if branchName == "" {
continue
}
branch := &Branch{Name: branchName, Recency: recency}
branches = append(branches, branch)
}
return uniqueByName(branches)
@@ -143,11 +145,17 @@ func uniqueByName(branches []*Branch) []*Branch {
// 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 branchInfoFromLine(line string) (string, string) {
// example line: HEAD@{12 minutes ago}|checkout: moving from pulling-from-forks to tim77-patch-1
r := regexp.MustCompile(`HEAD\@\{([^\s]+) ([^\s]+) ago\}\|.*?([^\s]*)$`)
matches := r.FindStringSubmatch(strings.TrimSpace(line))
if len(matches) == 0 {
return "", ""
}
since := matches[1]
unit := matches[2]
branchName := matches[3]
return since + abbreviatedTimeUnit(unit), branchName
}
func abbreviatedTimeUnit(timeUnit string) string {
+3 -54
View File
@@ -1,13 +1,5 @@
package commands
import (
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// Commit : A git commit
type Commit struct {
Sha string
@@ -17,50 +9,7 @@ type Commit struct {
Action string // one of "", "pick", "edit", "squash", "reword", "drop", "fixup"
Copied bool // to know if this commit is ready to be cherry-picked somewhere
Tags []string
}
// 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)
defaultColor := color.New(theme.DefaultTextColor)
magenta := color.New(color.FgMagenta)
// 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
case "selected":
shaColor = magenta
default:
shaColor = defaultColor
}
if c.Copied {
shaColor = copied
}
actionString := ""
tagString := ""
if c.Action != "" {
actionString = cyan.Sprint(utils.WithPadding(c.Action, 7)) + " "
} else if len(c.Tags) > 0 {
tagString = utils.ColoredString(strings.Join(c.Tags, " "), color.FgMagenta) + " "
}
return []string{shaColor.Sprint(c.Sha), actionString + tagString + defaultColor.Sprint(c.Name)}
ExtraInfo string // something like 'HEAD -> master, tag: v0.15.2'
Author string
Date string
}
-23
View File
@@ -1,10 +1,5 @@
package commands
import (
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/theme"
)
// CommitFile : A git commit file
type CommitFile struct {
Sha string
@@ -22,21 +17,3 @@ const (
// PART is for when you're only talking about specific lines that have been modified
PART
)
// GetDisplayStrings is a function.
func (f *CommitFile) GetDisplayStrings(isFocused bool) []string {
yellow := color.New(color.FgYellow)
green := color.New(color.FgGreen)
defaultColor := color.New(theme.DefaultTextColor)
var colour *color.Color
switch f.Status {
case UNSELECTED:
colour = defaultColor
case WHOLE:
colour = green
case PART:
colour = yellow
}
return []string{colour.Sprint(f.DisplayString)}
}
+53 -22
View File
@@ -23,6 +23,8 @@ import (
// 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
const SEPARATION_CHAR = "|"
// CommitListBuilder returns a list of Branch objects for the current repo
type CommitListBuilder struct {
Log *logrus.Entry
@@ -45,8 +47,41 @@ func NewCommitListBuilder(log *logrus.Entry, gitCommand *GitCommand, osCommand *
}, nil
}
// extractCommitFromLine takes a line from a git log and extracts the sha, message, date, and tag if present
// then puts them into a commit object
// example input:
// 8ad01fe32fcc20f07bc6693f87aa4977c327f1e1|10 hours ago|Jesse Duffield| (HEAD -> master, tag: v0.15.2)|refresh commits when adding a tag
func (c *CommitListBuilder) extractCommitFromLine(line string) *Commit {
split := strings.Split(line, SEPARATION_CHAR)
sha := split[0]
date := split[1]
author := split[2]
extraInfo := strings.TrimSpace(split[3])
message := strings.Join(split[4:], SEPARATION_CHAR)
tags := []string{}
if extraInfo != "" {
re := regexp.MustCompile(`tag: ([^,\)]+)`)
tagMatch := re.FindStringSubmatch(extraInfo)
if len(tagMatch) > 1 {
tags = append(tags, tagMatch[1])
}
}
return &Commit{
Sha: sha,
Name: message,
DisplayString: line,
Tags: tags,
ExtraInfo: extraInfo,
Date: date,
Author: author,
}
}
// GetCommits obtains the commits of the current branch
func (c *CommitListBuilder) GetCommits() ([]*Commit, error) {
func (c *CommitListBuilder) GetCommits(limit bool) ([]*Commit, error) {
commits := []*Commit{}
var rebasingCommits []*Commit
rebaseMode, err := c.GitCommand.RebaseMode()
@@ -65,21 +100,14 @@ func (c *CommitListBuilder) GetCommits() ([]*Commit, error) {
}
unpushedCommits := c.getUnpushedCommits()
log := c.getLog()
log := c.getLog(limit)
// 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, &Commit{
Sha: sha,
Name: strings.Join(splitLine[1:], " "),
Status: status,
DisplayString: strings.Join(splitLine, " "),
// TODO: add tags here
})
commit := c.extractCommitFromLine(line)
_, unpushed := unpushedCommits[commit.Sha[:8]]
commit.Status = map[bool]string{true: "unpushed", false: "pushed"}[unpushed]
commits = append(commits, commit)
}
if rebaseMode != "" {
currentCommit := commits[len(rebasingCommits)]
@@ -190,7 +218,7 @@ func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*Commit, error) {
}
splitLine := strings.Split(line, " ")
commits = append([]*Commit{{
Sha: splitLine[1][0:7],
Sha: splitLine[1],
Name: strings.Join(splitLine[2:], " "),
Status: "rebasing",
Action: splitLine[0],
@@ -207,7 +235,7 @@ func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*Commit, error) {
// Subject: second commit on master
func (c *CommitListBuilder) commitFromPatch(content string) (*Commit, error) {
lines := strings.Split(content, "\n")
sha := strings.Split(lines[0], " ")[1][0:7]
sha := strings.Split(lines[0], " ")[1]
name := strings.TrimPrefix(lines[3], "Subject: ")
return &Commit{
Sha: sha,
@@ -270,7 +298,7 @@ func (c *CommitListBuilder) getMergeBase() (string, error) {
// 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")
o, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..HEAD --abbrev-commit --abbrev=8")
if err != nil {
return pushables
}
@@ -281,12 +309,15 @@ func (c *CommitListBuilder) getUnpushedCommits() map[string]bool {
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")
// getLog gets the git log.
func (c *CommitListBuilder) getLog(limit bool) string {
limitFlag := ""
if limit {
limitFlag = "-30"
}
result, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git log --oneline --pretty=format:\"%%H%s%%ar%s%%aN%s%%d%s%%s\" %s --abbrev=%d", SEPARATION_CHAR, SEPARATION_CHAR, SEPARATION_CHAR, SEPARATION_CHAR, limitFlag, 20))
if err != nil {
// assume if there is an error there are no commits yet for this branch
return ""
-167
View File
@@ -149,170 +149,3 @@ func TestCommitListBuilderGetMergeBase(t *testing.T) {
})
}
}
// 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([]*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 []*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 []*Commit, err error) {
assert.NoError(t, err)
assert.Len(t, commits, 2)
assert.EqualValues(t, []*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 "branch":
assert.EqualValues(t, []string{"branch", "--contains"}, args)
// here too
return exec.Command("test")
case "rev-parse":
assert.EqualValues(t, []string{"rev-parse", "--short", "HEAD"}, args)
// here too
return exec.Command("test")
}
return nil
},
func(commits []*Commit, err error) {
assert.Error(t, err)
assert.Len(t, commits, 0)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.GetCommits())
})
}
}
-22
View File
@@ -1,7 +1,5 @@
package commands
import "github.com/fatih/color"
// File : A file from git status
// duplicating this for now
type File struct {
@@ -16,23 +14,3 @@ type File struct {
Type string // one of 'file', 'directory', and 'other'
ShortStatus string // e.g. 'AD', ' A', 'M ', '??'
}
// 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}
}
+123 -69
View File
@@ -7,6 +7,7 @@ import (
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
@@ -176,8 +177,8 @@ func stashEntryFromLine(line string, index int) *StashEntry {
}
// GetStashEntryDiff stash diff
func (c *GitCommand) GetStashEntryDiff(index int) (string, error) {
return c.OSCommand.RunCommandWithOutput("git stash show -p --color stash@{%d}", index)
func (c *GitCommand) ShowStashEntryCmdStr(index int) string {
return fmt.Sprintf("git stash show -p --color=%s stash@{%d}", c.colorArg(), index)
}
// GetStatusFiles git status files
@@ -191,10 +192,10 @@ func (c *GitCommand) GetStatusFiles() []*File {
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]
hasMergeConflicts := change == "UU" || change == "AA" || change == "DU"
hasInlineMergeConflicts := change == "UU" || change == "AA"
untracked := utils.IncludesString([]string{"??", "A ", "AM"}, change)
hasNoStagedChanges := utils.IncludesString([]string{" ", "U", "?"}, stagedChange)
hasMergeConflicts := utils.IncludesString([]string{"DD", "AA", "UU", "AU", "UA", "UD", "DU"}, change)
hasInlineMergeConflicts := utils.IncludesString([]string{"UU", "AA"}, change)
file := &File{
Name: filename,
@@ -265,7 +266,7 @@ func includesInt(list []int, a int) bool {
// ResetAndClean removes all unstaged changes and removes all untracked files
func (c *GitCommand) ResetAndClean() error {
if err := c.ResetHardHead(); err != nil {
if err := c.ResetHard("HEAD"); err != nil {
return err
}
@@ -404,12 +405,17 @@ func (c *GitCommand) AmendHead() (*exec.Cmd, error) {
}
// Pull pulls from repo
func (c *GitCommand) Pull(ask func(string) string) error {
return c.OSCommand.DetectUnamePass("git pull --no-edit", ask)
func (c *GitCommand) Pull(args string, ask func(string) string) error {
return c.OSCommand.DetectUnamePass("git pull --no-edit "+args, ask)
}
// PullWithoutPasswordCheck assumes that the pull will not prompt the user for a password
func (c *GitCommand) PullWithoutPasswordCheck(args string) error {
return c.OSCommand.RunCommand("git pull --no-edit " + args)
}
// Push pushes to a branch
func (c *GitCommand) Push(branchName string, force bool, upstream string, ask func(string) string) error {
func (c *GitCommand) Push(branchName string, force bool, upstream string, args string, ask func(string) string) error {
forceFlag := ""
if force {
forceFlag = "--force-with-lease"
@@ -420,7 +426,7 @@ func (c *GitCommand) Push(branchName string, force bool, upstream string, ask fu
setUpstreamArg = "--set-upstream " + upstream
}
cmd := fmt.Sprintf("git push --follow-tags %s %s", forceFlag, setUpstreamArg)
cmd := fmt.Sprintf("git push --follow-tags %s %s %s", forceFlag, setUpstreamArg, args)
return c.OSCommand.DetectUnamePass(cmd, ask)
}
@@ -515,7 +521,7 @@ func (c *GitCommand) DiscardUnstagedFileChanges(file *File) error {
return c.OSCommand.RunCommand("git checkout -- %s", quotedFileName)
}
// Checkout checks out a branch, with --force if you set the force arg to true
// Checkout checks out a branch (or commit), with --force if you set the force arg to true
func (c *GitCommand) Checkout(branch string, force bool) error {
forceArg := ""
if force {
@@ -538,7 +544,8 @@ func (c *GitCommand) PrepareCommitAmendSubProcess() *exec.Cmd {
// 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("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium -100 %s", branchName)
cmdStr := c.GetBranchGraphCmdStr(branchName)
return c.OSCommand.RunCommandWithOutput(cmdStr)
}
func (c *GitCommand) GetUpstreamForBranch(branchName string) (string, error) {
@@ -551,41 +558,12 @@ 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("git show --color --no-renames %s", sha)
if err != nil {
return "", err
}
func (c *GitCommand) ShowCmdStr(sha string) string {
return fmt.Sprintf("git show --color=%s --no-renames --stat -p %s", c.colorArg(), sha)
}
// 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("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("git diff --color %s...%s", secondLineWords[1], secondLineWords[2])
if err != nil {
return "", err
}
return show + mergeDiff, nil
func (c *GitCommand) GetBranchGraphCmdStr(branchName string) string {
return fmt.Sprintf("git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium %s", branchName)
}
// GetRemoteURL returns current repo remote url
@@ -606,29 +584,33 @@ func (c *GitCommand) CheckRemoteBranchExists(branch *Branch) bool {
// Diff returns the diff of a file
func (c *GitCommand) Diff(file *File, plain bool, cached bool) string {
// for now we assume an error means the file was deleted
s, _ := c.OSCommand.RunCommandWithOutput(c.DiffCmdStr(file, plain, cached))
return s
}
func (c *GitCommand) DiffCmdStr(file *File, plain bool, cached bool) string {
cachedArg := ""
trackedArg := "--"
colorArg := "--color"
colorArg := c.colorArg()
split := strings.Split(file.Name, " -> ") // in case of a renamed file we get the new filename
fileName := c.OSCommand.Quote(split[len(split)-1])
if cached {
cachedArg = "--cached"
}
if !file.Tracked && !file.HasStagedChanges {
if !file.Tracked && !file.HasStagedChanges && !cached {
trackedArg = "--no-index /dev/null"
}
if plain {
colorArg = ""
colorArg = "never"
}
// for now we assume an error means the file was deleted
s, _ := c.OSCommand.RunCommandWithOutput("git diff %s %s %s %s", colorArg, cachedArg, trackedArg, fileName)
return s
return fmt.Sprintf("git diff --color=%s %s %s %s", colorArg, cachedArg, trackedArg, fileName)
}
func (c *GitCommand) ApplyPatch(patch string, flags ...string) error {
c.Log.Warn(patch)
filepath := filepath.Join(c.Config.GetUserConfigDir(), utils.GetCurrentRepoName(), time.Now().Format(time.StampNano)+".patch")
filepath := filepath.Join(c.Config.GetUserConfigDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch")
if err := c.OSCommand.CreateFileWithContent(filepath, patch); err != nil {
return err
}
@@ -647,10 +629,12 @@ func (c *GitCommand) FastForward(branchName string, remoteName string, remoteBra
func (c *GitCommand) RunSkipEditorCommand(command string) error {
cmd := c.OSCommand.ExecutableFromString(command)
lazyGitPath := c.OSCommand.GetLazygitPath()
cmd.Env = append(
cmd.Env,
"LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY",
"EDITOR="+c.OSCommand.GetLazygitPath(),
"EDITOR="+lazyGitPath,
"VISUAL="+lazyGitPath,
)
return c.OSCommand.RunExecutable(cmd)
}
@@ -908,11 +892,17 @@ func (c *GitCommand) GetCommitFiles(commitSha string, patchManager *PatchManager
// ShowCommitFile get the diff of specified commit file
func (c *GitCommand) ShowCommitFile(commitSha, fileName string, plain bool) (string, error) {
colorArg := "--color"
cmdStr := c.ShowCommitFileCmdStr(commitSha, fileName, plain)
return c.OSCommand.RunCommandWithOutput(cmdStr)
}
func (c *GitCommand) ShowCommitFileCmdStr(commitSha, fileName string, plain bool) string {
colorArg := c.colorArg()
if plain {
colorArg = ""
colorArg = "never"
}
return c.OSCommand.RunCommandWithOutput("git show --no-renames %s %s -- %s", colorArg, commitSha, fileName)
return fmt.Sprintf("git show --no-renames --color=%s %s -- %s", colorArg, commitSha, fileName)
}
// CheckoutFile checks out the file for the given commit
@@ -956,24 +946,29 @@ func (c *GitCommand) DiscardAnyUnstagedFileChanges() error {
return c.OSCommand.RunCommand("git checkout -- .")
}
// RemoveTrackedFiles will delete the given file(s) even if they are currently tracked
func (c *GitCommand) RemoveTrackedFiles(name string) error {
return c.OSCommand.RunCommand("git rm -r --cached %s", name)
}
// RemoveUntrackedFiles runs `git clean -fd`
func (c *GitCommand) RemoveUntrackedFiles() error {
return c.OSCommand.RunCommand("git clean -fd")
}
// ResetHardHead runs `git reset --hard HEAD`
func (c *GitCommand) ResetHardHead() error {
return c.OSCommand.RunCommand("git reset --hard HEAD")
// ResetHardHead runs `git reset --hard`
func (c *GitCommand) ResetHard(ref string) error {
return c.OSCommand.RunCommand("git reset --hard " + ref)
}
// ResetSoftHead runs `git reset --soft HEAD`
func (c *GitCommand) ResetSoftHead() error {
return c.OSCommand.RunCommand("git reset --soft HEAD")
// ResetSoft runs `git reset --soft HEAD`
func (c *GitCommand) ResetSoft(ref string) error {
return c.OSCommand.RunCommand("git reset --soft " + ref)
}
// DiffCommits show diff between commits
func (c *GitCommand) DiffCommits(sha1, sha2 string) (string, error) {
return c.OSCommand.RunCommandWithOutput("git diff --color %s %s", sha1, sha2)
return c.OSCommand.RunCommandWithOutput("git diff --color=%s --stat -p %s %s", c.colorArg(), sha1, sha2)
}
// CreateFixupCommit creates a commit that fixes up a previous commit
@@ -1098,10 +1093,6 @@ func (c *GitCommand) CreateLightweightTag(tagName string, commitSha string) erro
return c.OSCommand.RunCommand("git tag %s %s", tagName, commitSha)
}
func (c *GitCommand) ShowTag(tagName string) (string, error) {
return c.OSCommand.RunCommandWithOutput("git tag -n99 %s", tagName)
}
func (c *GitCommand) DeleteTag(tagName string) error {
return c.OSCommand.RunCommand("git tag -d %s", tagName)
}
@@ -1109,3 +1100,66 @@ func (c *GitCommand) DeleteTag(tagName string) error {
func (c *GitCommand) PushTag(remoteName string, tagName string) error {
return c.OSCommand.RunCommand("git push %s %s", remoteName, tagName)
}
func (c *GitCommand) FetchRemote(remoteName string) error {
return c.OSCommand.RunCommand("git fetch %s", remoteName)
}
func (c *GitCommand) GetReflogCommits() ([]*Commit, error) {
output, err := c.OSCommand.RunCommandWithOutput("git reflog --abbrev=20")
if err != nil {
return nil, err
}
lines := strings.Split(strings.TrimSpace(output), "\n")
commits := make([]*Commit, len(lines))
re := regexp.MustCompile(`(\w+).*HEAD@\{\d+\}: (.*)`)
for i, line := range lines {
match := re.FindStringSubmatch(line)
if len(match) <= 1 {
continue
}
commits[i] = &Commit{
Sha: match[1],
Name: match[2],
Status: "reflog",
}
}
return commits, nil
}
func (c *GitCommand) ConfiguredPager() string {
if os.Getenv("GIT_PAGER") != "" {
return os.Getenv("GIT_PAGER")
}
if os.Getenv("PAGER") != "" {
return os.Getenv("PAGER")
}
output, err := c.OSCommand.RunCommandWithOutput("git config --get-all core.pager")
if err != nil {
return ""
}
trimmedOutput := strings.TrimSpace(output)
return strings.Split(trimmedOutput, "\n")[0]
}
func (c *GitCommand) GetPager(width int) string {
useConfig := c.Config.GetUserConfig().GetBool("git.paging.useConfig")
if useConfig {
pager := c.ConfiguredPager()
return strings.Split(pager, "| less")[0]
}
templateValues := map[string]string{
"columnWidth": strconv.Itoa(width/2 - 6),
}
pagerTemplate := c.Config.GetUserConfig().GetString("git.paging.pager")
return utils.ResolvePlaceholderString(pagerTemplate, templateValues)
}
func (c *GitCommand) colorArg() string {
return c.Config.GetUserConfig().GetString("git.paging.colorArg")
}
+44 -85
View File
@@ -5,6 +5,7 @@ import (
"io/ioutil"
"os"
"os/exec"
"regexp"
"testing"
"time"
@@ -318,21 +319,6 @@ func TestGitCommandGetStashEntries(t *testing.T) {
}
}
// TestGitCommandGetStashEntryDiff is a function.
func TestGitCommandGetStashEntryDiff(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"stash", "show", "-p", "--color", "stash@{1}"}, args)
return exec.Command("echo")
}
_, err := gitCmd.GetStashEntryDiff(1)
assert.NoError(t, err)
}
// TestGitCommandGetStatusFiles is a function.
func TestGitCommandGetStatusFiles(t *testing.T) {
type scenario struct {
@@ -1030,7 +1016,7 @@ func TestGitCommandPush(t *testing.T) {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = s.command
err := gitCmd.Push("test", s.forcePush, "", func(passOrUname string) string {
err := gitCmd.Push("test", s.forcePush, "", "", func(passOrUname string) string {
return "\n"
})
s.test(err)
@@ -1411,66 +1397,6 @@ func TestGitCommandDiscardAllFileChanges(t *testing.T) {
}
}
// TestGitCommandShow is a function.
func TestGitCommandShow(t *testing.T) {
type scenario struct {
testName string
arg string
command func(string, ...string) *exec.Cmd
test func(string, error)
}
scenarios := []scenario{
{
"regular commit",
"456abcde",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: "git show --color --no-renames 456abcde",
Replace: "echo \"commit ccc771d8b13d5b0d4635db4463556366470fd4f6\nblah\"",
},
{
Expect: "git rev-list -1 --merges 456abcde^...456abcde",
Replace: "echo",
},
}),
func(result string, err error) {
assert.NoError(t, err)
assert.Equal(t, "commit ccc771d8b13d5b0d4635db4463556366470fd4f6\nblah\n", result)
},
},
{
"merge commit",
"456abcde",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: "git show --color --no-renames 456abcde",
Replace: "echo \"commit ccc771d8b13d5b0d4635db4463556366470fd4f6\nMerge: 1a6a69a 3b51d7c\"",
},
{
Expect: "git rev-list -1 --merges 456abcde^...456abcde",
Replace: "echo aa30e006433628ba9281652952b34d8aacda9c01",
},
{
Expect: "git diff --color 1a6a69a...3b51d7c",
Replace: "echo blah",
},
}),
func(result string, err error) {
assert.NoError(t, err)
assert.Equal(t, "commit ccc771d8b13d5b0d4635db4463556366470fd4f6\nMerge: 1a6a69a 3b51d7c\nblah\n", result)
},
},
}
gitCmd := NewDummyGitCommand()
for _, s := range scenarios {
gitCmd.OSCommand.command = s.command
s.test(gitCmd.Show(s.arg))
}
}
// TestGitCommandCheckout is a function.
func TestGitCommandCheckout(t *testing.T) {
type scenario struct {
@@ -1523,7 +1449,7 @@ func TestGitCommandGetBranchGraph(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"log", "--graph", "--color", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium", "-100", "test"}, args)
assert.EqualValues(t, []string{"log", "--graph", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium", "test"}, args)
return exec.Command("echo")
}
@@ -1547,7 +1473,7 @@ func TestGitCommandDiff(t *testing.T) {
"Default case",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"diff", "--color", "--", "test.txt"}, args)
assert.EqualValues(t, []string{"diff", "--color=", "--", "test.txt"}, args)
return exec.Command("echo")
},
@@ -1563,7 +1489,7 @@ func TestGitCommandDiff(t *testing.T) {
"cached",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"diff", "--color", "--cached", "--", "test.txt"}, args)
assert.EqualValues(t, []string{"diff", "--color=", "--cached", "--", "test.txt"}, args)
return exec.Command("echo")
},
@@ -1579,7 +1505,7 @@ func TestGitCommandDiff(t *testing.T) {
"plain",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"diff", "--", "test.txt"}, args)
assert.EqualValues(t, []string{"diff", "--color=never", "--", "test.txt"}, args)
return exec.Command("echo")
},
@@ -1595,7 +1521,7 @@ func TestGitCommandDiff(t *testing.T) {
"File not tracked and file has no staged changes",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"diff", "--color", "--no-index", "/dev/null", "test.txt"}, args)
assert.EqualValues(t, []string{"diff", "--color=", "--no-index", "/dev/null", "test.txt"}, args)
return exec.Command("echo")
},
@@ -1945,7 +1871,7 @@ func TestGitCommandShowCommitFile(t *testing.T) {
"hello.txt",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: "git show --no-renames 123456 -- hello.txt",
Expect: "git show --no-renames --color=never 123456 -- hello.txt",
Replace: "echo -n hello",
},
}),
@@ -2106,10 +2032,11 @@ func TestGitCommandRemoveUntrackedFiles(t *testing.T) {
}
}
// TestGitCommandResetHardHead is a function.
func TestGitCommandResetHardHead(t *testing.T) {
// TestGitCommandResetHard is a function.
func TestGitCommandResetHard(t *testing.T) {
type scenario struct {
testName string
ref string
command func(string, ...string) *exec.Cmd
test func(error)
}
@@ -2117,6 +2044,7 @@ func TestGitCommandResetHardHead(t *testing.T) {
scenarios := []scenario{
{
"valid case",
"HEAD",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: `git reset --hard HEAD`,
@@ -2134,7 +2062,7 @@ func TestGitCommandResetHardHead(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd.OSCommand.command = s.command
s.test(gitCmd.ResetHardHead())
s.test(gitCmd.ResetHard(s.ref))
})
}
}
@@ -2174,6 +2102,37 @@ func TestGitCommandCreateFixupCommit(t *testing.T) {
}
}
// TestGitCommandSkipEditorCommand confirms that SkipEditorCommand injects
// environment variables that suppress an interactive editor
func TestGitCommandSkipEditorCommand(t *testing.T) {
cmd := NewDummyGitCommand()
cmd.OSCommand.SetBeforeExecuteCmd(func(cmd *exec.Cmd) {
test.AssertContainsMatch(
t,
cmd.Env,
regexp.MustCompile("^VISUAL="),
"expected VISUAL to be set for a non-interactive external command",
)
test.AssertContainsMatch(
t,
cmd.Env,
regexp.MustCompile("^EDITOR="),
"expected EDITOR to be set for a non-interactive external command",
)
test.AssertContainsMatch(
t,
cmd.Env,
regexp.MustCompile("^LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY$"),
"expected LAZYGIT_CLIENT_COMMAND to be set for a non-interactive external command",
)
})
cmd.RunSkipEditorCommand("true")
}
func TestFindDotGitDir(t *testing.T) {
type scenario struct {
testName string
+17 -1
View File
@@ -36,6 +36,7 @@ type OSCommand struct {
Platform *Platform
Config config.AppConfigurer
command func(string, ...string) *exec.Cmd
beforeExecuteCmd func(*exec.Cmd)
getGlobalGitConfig func(string) (string, error)
getenv func(string) string
}
@@ -47,6 +48,7 @@ func NewOSCommand(log *logrus.Entry, config config.AppConfigurer) *OSCommand {
Platform: getPlatform(),
Config: config,
command: exec.Command,
beforeExecuteCmd: func(*exec.Cmd) {},
getGlobalGitConfig: gitconfig.Global,
getenv: os.Getenv,
}
@@ -58,6 +60,10 @@ func (c *OSCommand) SetCommand(cmd func(string, ...string) *exec.Cmd) {
c.command = cmd
}
func (c *OSCommand) SetBeforeExecuteCmd(cmd func(*exec.Cmd)) {
c.beforeExecuteCmd = cmd
}
// RunCommandWithOutput wrapper around commands returning their output and error
// NOTE: If you don't pass any formatArgs we'll just use the command directly,
// however there's a bizarre compiler error/warning when you pass in a formatString
@@ -76,6 +82,7 @@ func (c *OSCommand) RunCommandWithOutput(formatString string, formatArgs ...inte
// RunExecutableWithOutput runs an executable file and returns its output
func (c *OSCommand) RunExecutableWithOutput(cmd *exec.Cmd) (string, error) {
c.beforeExecuteCmd(cmd)
return sanitisedCommandOutput(cmd.CombinedOutput())
}
@@ -308,6 +315,7 @@ func (c *OSCommand) FileExists(path string) (bool, error) {
// this is useful if you need to give your command some environment variables
// before running it
func (c *OSCommand) RunPreparedCommand(cmd *exec.Cmd) error {
c.beforeExecuteCmd(cmd)
out, err := cmd.CombinedOutput()
outString := string(out)
c.Log.Info(outString)
@@ -326,7 +334,7 @@ func (c *OSCommand) GetLazygitPath() string {
if err != nil {
ex = os.Args[0] // fallback to the first call argument if needed
}
return filepath.ToSlash(ex)
return `"` + filepath.ToSlash(ex) + `"`
}
// RunCustomCommand returns the pointer to a custom command
@@ -393,3 +401,11 @@ func (c *OSCommand) PipeCommands(commandStrings ...string) error {
}
return nil
}
func Kill(cmd *exec.Cmd) error {
if cmd.Process == nil {
// somebody got to it before we were able to, poor bastard
return nil
}
return cmd.Process.Kill()
}
+1 -1
View File
@@ -120,7 +120,7 @@ func coloredString(colorAttr color.Attribute, str string, selected bool, include
var cl *color.Color
attributes := []color.Attribute{colorAttr}
if selected {
attributes = append(attributes, color.BgBlue)
attributes = append(attributes, theme.SelectedLineBgColor)
}
cl = color.New(attributes...)
var clIncluded *color.Color
+55 -18
View File
@@ -5,6 +5,7 @@ import (
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/config"
)
// Service is a service that repository is on (Github, Bitbucket, ...)
@@ -26,27 +27,63 @@ type RepoInformation struct {
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?source=%s&t=1",
},
{
Name: "gitlab.com",
PullRequestURL: "https://gitlab.com/%s/%s/merge_requests/new?merge_request[source_branch]=%s",
},
// NewService builds a Service based on the host type
func NewService(typeName string, repositoryDomain string, siteDomain string) *Service {
var service *Service
switch typeName {
case "github":
service = &Service{
Name: repositoryDomain,
PullRequestURL: fmt.Sprintf("https://%s%s", siteDomain, "/%s/%s/compare/%s?expand=1"),
}
case "bitbucket":
service = &Service{
Name: repositoryDomain,
PullRequestURL: fmt.Sprintf("https://%s%s", siteDomain, "/%s/%s/pull-requests/new?source=%s&t=1"),
}
case "gitlab":
service = &Service{
Name: repositoryDomain,
PullRequestURL: fmt.Sprintf("https://%s%s", siteDomain, "/%s/%s/merge_requests/new?merge_request[source_branch]=%s"),
}
}
return service
}
func getServices(config config.AppConfigurer) []*Service {
services := []*Service{
NewService("github", "github.com", "github.com"),
NewService("bitbucket", "bitbucket.org", "bitbucket.org"),
NewService("gitlab", "gitlab.com", "gitlab.com"),
}
configServices := config.GetUserConfig().GetStringMapString("services")
for repoDomain, typeAndDomain := range configServices {
splitData := strings.Split(typeAndDomain, ":")
if len(splitData) != 2 {
// TODO log this misconfiguration
continue
}
service := NewService(splitData[0], repoDomain, splitData[1])
if service == nil {
// TODO log this unsupported service
continue
}
services = append(services, service)
}
return services
}
// NewPullRequest creates new instance of PullRequest
func NewPullRequest(gitCommand *GitCommand) *PullRequest {
return &PullRequest{
GitServices: getServices(),
GitServices: getServices(gitCommand.Config),
GitCommand: gitCommand,
}
}
@@ -85,7 +122,7 @@ func getRepoInfoFromURL(url string) *RepoInformation {
if isHTTP {
splits := strings.Split(url, "/")
owner := splits[len(splits)-2]
owner := strings.Join(splits[3:len(splits)-1], "/")
repo := strings.TrimSuffix(splits[len(splits)-1], ".git")
return &RepoInformation{
@@ -96,8 +133,8 @@ func getRepoInfoFromURL(url string) *RepoInformation {
tmpSplit := strings.Split(url, ":")
splits := strings.Split(tmpSplit[1], "/")
owner := splits[0]
repo := strings.TrimSuffix(splits[1], ".git")
owner := strings.Join(splits[0:len(splits)-1], "/")
repo := strings.TrimSuffix(splits[len(splits)-1], ".git")
return &RepoInformation{
Owner: owner,
+7
View File
@@ -147,6 +147,13 @@ func TestCreatePullRequest(t *testing.T) {
gitCommand := NewDummyGitCommand()
gitCommand.OSCommand.command = s.command
gitCommand.OSCommand.Config.GetUserConfig().Set("os.openLinkCommand", "open {{link}}")
gitCommand.Config.GetUserConfig().Set("services", map[string]string{
// valid configuration for a custom service URL
"git.work.com": "gitlab:code.work.com",
// invalid configurations for a custom service URL
"invalid.work.com": "noservice:invalid.work.com",
"noservice.work.com": "noservice.work.com",
})
dummyPullRequest := NewPullRequest(gitCommand)
s.test(dummyPullRequest.Create(s.branch))
})
-12
View File
@@ -1,19 +1,7 @@
package commands
import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
// Remote Branch : A git remote branch
type RemoteBranch struct {
Name string
Selected bool
RemoteName string
}
// GetDisplayStrings returns the display string of branch
func (b *RemoteBranch) GetDisplayStrings(isFocused bool) []string {
displayName := utils.ColoredString(b.Name, GetBranchColor(b.Name))
return []string{displayName}
}
-5
View File
@@ -4,8 +4,3 @@ package commands
type Tag struct {
Name string
}
// GetDisplayStrings returns the display string of a remote
func (r *Tag) GetDisplayStrings(isFocused bool) []string {
return []string{r.Name}
}
+105 -1
View File
@@ -243,18 +243,25 @@ func GetDefaultConfig() []byte {
scrollHeight: 2
scrollPastBottom: true
mouseEvents: true
skipUnstageLineWarning: false
sidePanelWidth: 0.3333
theme:
lightTheme: false
activeBorderColor:
- white
- green
- bold
inactiveBorderColor:
- white
optionsTextColor:
- blue
selectedLineBgColor:
- blue
commitLength:
show: true
git:
paging:
colorArg: always
useConfig: false
merging:
manualCommit: false
skipHookPrefix: 'WIP'
@@ -265,6 +272,103 @@ update:
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
splashUpdatesIndex: 0
confirmOnQuit: false
keybinding:
universal:
quit: 'q'
quit-alt1: '<c-c>'
return: '<esc>'
quitWithoutChangingDirectory: 'Q'
togglePanel: '<tab>'
prevItem: '<up>'
nextItem: '<down>'
prevItem-alt: 'k'
nextItem-alt: 'j'
prevBlock: '<left>'
nextBlock: '<right>'
prevBlock-alt: 'h'
nextBlock-alt: 'l'
nextMatch: 'n'
prevMatch: 'N'
startSearch: '/'
optionMenu: 'x'
optionMenu-alt1: '?'
select: '<space>'
goInto: '<enter>'
remove: 'd'
new: 'n'
edit: 'e'
openFile: 'o'
scrollUpMain: '<pgup>'
scrollDownMain: '<pgdown>'
scrollUpMain-alt1: 'K'
scrollDownMain-alt1: 'J'
scrollUpMain-alt2: '<c-u>'
scrollDownMain-alt2: '<c-d>'
executeCustomCommand: ':'
createRebaseOptionsMenu: 'm'
pushFiles: 'P'
pullFiles: 'p'
refresh: 'R'
createPatchOptionsMenu: '<c-p>'
nextTab: ']'
prevTab: '['
nextScreenMode: '+'
prevScreenMode: '_'
status:
checkForUpdate: 'u'
recentRepos: '<enter>'
files:
commitChanges: 'c'
commitChangesWithoutHook: 'w'
amendLastCommit: 'A'
commitChangesWithEditor: 'C'
ignoreFile: 'i'
refreshFiles: 'r'
stashAllChanges: 's'
viewStashOptions: 'S'
toggleStagedAll: 'a'
viewResetOptions: 'D'
fetch: 'f'
branches:
createPullRequest: 'o'
checkoutBranchByName: 'c'
forceCheckoutBranch: 'F'
rebaseBranch: 'r'
mergeIntoCurrentBranch: 'M'
viewGitFlowOptions: 'i'
fastForward: 'f'
pushTag: 'P'
setUpstream: 'u'
fetchRemote: 'f'
commits:
squashDown: 's'
renameCommit: 'r'
renameCommitWithEditor: 'R'
viewResetOptions: 'g'
markCommitAsFixup: 'f'
createFixupCommit: 'F'
squashAboveCommits: 'S'
moveDownCommit: '<c-j>'
moveUpCommit: '<c-k>'
amendToCommit: 'A'
pickCommit: 'p'
revertCommit: 't'
cherryPickCopy: 'c'
cherryPickCopyRange: 'C'
pasteCommits: 'v'
tagCommit: 'T'
toggleDiffCommit: 'i'
checkoutCommit: '<space>'
stash:
popStash: 'g'
commitFiles:
checkoutCommitFile: 'c'
main:
toggleDragSelect: 'v'
toggleDragSelect-alt: 'V'
toggleSelectHunk: 'a'
pickBothHunks: 'b'
undo: 'z'
`)
}
+20 -7
View File
@@ -1,6 +1,8 @@
package gui
import (
"time"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -49,15 +51,26 @@ func (m *statusManager) getStatusString() string {
// 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
})
gui.statusManager.addWaitingStatus(name)
defer gui.g.Update(func(g *gocui.Gui) error {
defer func() {
gui.statusManager.removeStatus(name)
return nil
})
}()
go func() {
ticker := time.NewTicker(time.Millisecond * 50)
defer ticker.Stop()
for range ticker.C {
appStatus := gui.statusManager.getStatusString()
gui.Log.Warn(appStatus)
if appStatus == "" {
return
}
if err := gui.renderString(gui.g, "appStatus", appStatus); err != nil {
gui.Log.Warn(err)
}
}
}()
if err := f(); err != nil {
gui.g.Update(func(g *gocui.Gui) error {
+75 -39
View File
@@ -4,9 +4,9 @@ import (
"fmt"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -37,40 +37,41 @@ func (gui *Gui) handleBranchSelect(g *gocui.Gui, v *gocui.View) error {
// This really shouldn't happen: there should always be a master branch
if len(gui.State.Branches) == 0 {
return gui.renderString(g, "main", gui.Tr.SLocalize("NoBranchesThisRepo"))
return gui.newStringTask("main", gui.Tr.SLocalize("NoBranchesThisRepo"))
}
branch := gui.getSelectedBranch()
if err := gui.focusPoint(0, gui.State.Panels.Branches.SelectedLine, len(gui.State.Branches), v); err != nil {
return err
}
go func() {
_ = gui.RenderSelectedBranchUpstreamDifferences()
}()
go func() {
upstream, _ := gui.GitCommand.GetUpstreamForBranch(branch.Name)
if strings.Contains(upstream, "no upstream configured for branch") || strings.Contains(upstream, "unknown revision or path not in the working tree") {
upstream = gui.Tr.SLocalize("notTrackingRemote")
}
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", fmt.Sprintf("%s → %s\n\n%s", utils.ColoredString(branch.Name, color.FgGreen), utils.ColoredString(upstream, color.FgRed), graph))
}()
if err := gui.RenderSelectedBranchUpstreamDifferences(); err != nil {
return err
}
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.GetBranchGraphCmdStr(branch.Name),
)
if err := gui.newCmdTask("main", cmd); err != nil {
gui.Log.Error(err)
}
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
}
return gui.newTask("branches", func(stop chan struct{}) error {
branch := gui.getSelectedBranch()
branch.Pushables, branch.Pullables = gui.GitCommand.GetBranchUpstreamDifferenceCount(branch.Name)
branch := gui.getSelectedBranch()
branch.Pushables, branch.Pullables = gui.GitCommand.GetBranchUpstreamDifferenceCount(branch.Name)
return gui.renderListPanel(gui.getBranchesView(), gui.State.Branches)
select {
case <-stop:
return nil
default:
}
branchesView := gui.getBranchesView()
displayStrings := presentation.GetBranchListDisplayStrings(gui.State.Branches, gui.currentViewName() == "branches", gui.State.Panels.Branches.SelectedLine)
gui.renderDisplayStrings(branchesView, displayStrings)
return nil
})
}
// gui.refreshStatus is called at the end of this because that's when we can
@@ -93,8 +94,7 @@ func (gui *Gui) refreshBranches(g *gocui.Gui) error {
// TODO: if we're in the remotes view and we've just deleted a remote we need to refresh accordingly
if gui.getBranchesView().Context == "local-branches" {
gui.refreshSelectedLine(&gui.State.Panels.Branches.SelectedLine, len(gui.State.Branches))
if err := gui.RenderSelectedBranchUpstreamDifferences(); err != nil {
if err := gui.renderLocalBranchesWithSelection(); err != nil {
return err
}
}
@@ -108,11 +108,13 @@ func (gui *Gui) renderLocalBranchesWithSelection() error {
branchesView := gui.getBranchesView()
gui.refreshSelectedLine(&gui.State.Panels.Branches.SelectedLine, len(gui.State.Branches))
if err := gui.renderListPanel(branchesView, gui.State.Branches); err != nil {
if err := gui.RenderSelectedBranchUpstreamDifferences(); err != nil {
return err
}
if err := gui.handleBranchSelect(gui.g, branchesView); err != nil {
return err
if gui.g.CurrentView() == branchesView {
if err := gui.handleBranchSelect(gui.g, branchesView); err != nil {
return err
}
}
return nil
@@ -128,7 +130,7 @@ func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error {
return gui.createErrorPanel(g, gui.Tr.SLocalize("AlreadyCheckedOutBranch"))
}
branch := gui.getSelectedBranch()
return gui.handleCheckoutBranch(branch.Name)
return gui.handleCheckoutRef(branch.Name)
}
func (gui *Gui) handleCreatePullRequestPress(g *gocui.Gui, v *gocui.View) error {
@@ -165,17 +167,17 @@ func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
}, nil)
}
func (gui *Gui) handleCheckoutBranch(branchName string) error {
if err := gui.GitCommand.Checkout(branchName, false); err != nil {
func (gui *Gui) handleCheckoutRef(ref string) error {
if err := gui.GitCommand.Checkout(ref, false); err != nil {
// note, this will only work for english-language git commands. If we force git to use english, and the error isn't this one, then the user will receive an english command they may not understand. I'm not sure what the best solution to this is. Running the command once in english and a second time in the native language is one option
if strings.Contains(err.Error(), "Please commit your changes or stash them before you switch branch") {
// offer to autostash changes
return gui.createConfirmationPanel(gui.g, gui.getBranchesView(), true, gui.Tr.SLocalize("AutoStashTitle"), gui.Tr.SLocalize("AutoStashPrompt"), func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.StashSave(gui.Tr.SLocalize("StashPrefix") + branchName); err != nil {
if err := gui.GitCommand.StashSave(gui.Tr.SLocalize("StashPrefix") + ref); err != nil {
return gui.createErrorPanel(g, err.Error())
}
if err := gui.GitCommand.Checkout(branchName, false); err != nil {
if err := gui.GitCommand.Checkout(ref, false); err != nil {
return gui.createErrorPanel(g, err.Error())
}
@@ -198,12 +200,13 @@ func (gui *Gui) handleCheckoutBranch(branchName string) error {
}
gui.State.Panels.Branches.SelectedLine = 0
gui.State.Panels.Commits.SelectedLine = 0
return gui.refreshSidePanels(gui.g)
}
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 {
return gui.handleCheckoutBranch(gui.trimmedContent(v))
return gui.handleCheckoutRef(gui.trimmedContent(v))
})
return nil
}
@@ -366,12 +369,19 @@ func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
go func() {
_ = gui.createLoaderPanel(gui.g, v, message)
if err := gui.GitCommand.FastForward(branch.Name, remoteName, remoteBranchName); err != nil {
_ = gui.createErrorPanel(gui.g, err.Error())
if gui.State.Panels.Branches.SelectedLine == 0 {
if err := gui.GitCommand.PullWithoutPasswordCheck("--ff-only"); err != nil {
_ = gui.createErrorPanel(gui.g, err.Error())
}
_ = gui.refreshSidePanels(gui.g)
} else {
_ = gui.closeConfirmationPrompt(gui.g, true)
if err := gui.GitCommand.FastForward(branch.Name, remoteName, remoteBranchName); err != nil {
_ = gui.createErrorPanel(gui.g, err.Error())
}
_ = gui.RenderSelectedBranchUpstreamDifferences()
}
_ = gui.closeConfirmationPrompt(gui.g, true)
}()
return nil
}
@@ -387,6 +397,7 @@ func (gui *Gui) onBranchesTabClick(tabIndex int) error {
func (gui *Gui) switchBranchesPanelContext(context string) error {
branchesView := gui.getBranchesView()
branchesView.Context = context
gui.onSearchEscape()
contextTabIndexMap := map[string]int{
"local-branches": 0,
@@ -422,3 +433,28 @@ func (gui *Gui) handlePrevBranchesTab(g *gocui.Gui, v *gocui.View) error {
utils.ModuloWithWrap(v.TabIndex-1, len(v.Tabs)),
)
}
func (gui *Gui) handleCreateResetToBranchMenu(g *gocui.Gui, v *gocui.View) error {
branch := gui.getSelectedBranch()
if branch == nil {
return nil
}
return gui.createResetMenu(branch.Name)
}
func (gui *Gui) onBranchesPanelSearchSelect(selectedLine int) error {
branchesView := gui.getBranchesView()
switch branchesView.Context {
case "local-branches":
gui.State.Panels.Branches.SelectedLine = selectedLine
return gui.handleBranchSelect(gui.g, branchesView)
case "remotes":
gui.State.Panels.Remotes.SelectedLine = selectedLine
return gui.handleRemoteSelect(gui.g, branchesView)
case "remote-branches":
gui.State.Panels.RemoteBranches.SelectedLine = selectedLine
return gui.handleRemoteBranchSelect(gui.g, branchesView)
}
return nil
}
+22 -12
View File
@@ -4,6 +4,7 @@ import (
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
)
func (gui *Gui) getSelectedCommitFile(g *gocui.Gui) *commands.CommitFile {
@@ -29,7 +30,9 @@ func (gui *Gui) handleCommitFileSelect(g *gocui.Gui, v *gocui.View) error {
}
gui.getMainView().Title = "Patch"
gui.State.Panels.LineByLine = nil
if gui.currentViewName() == "commitFiles" {
gui.handleEscapeLineByLinePanel()
}
commitFile := gui.getSelectedCommitFile(g)
if commitFile == nil {
@@ -43,11 +46,15 @@ func (gui *Gui) handleCommitFileSelect(g *gocui.Gui, v *gocui.View) error {
if err := gui.focusPoint(0, gui.State.Panels.CommitFiles.SelectedLine, len(gui.State.CommitFiles), v); err != nil {
return err
}
commitText, err := gui.GitCommand.ShowCommitFile(commitFile.Sha, commitFile.Name, false)
if err != nil {
return err
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowCommitFileCmdStr(commitFile.Sha, commitFile.Name, false),
)
if err := gui.newPtyTask("main", cmd); err != nil {
gui.Log.Error(err)
}
return gui.renderString(g, "main", commitText)
return nil
}
func (gui *Gui) handleSwitchToCommitsPanel(g *gocui.Gui, v *gocui.View) error {
@@ -106,11 +113,11 @@ func (gui *Gui) refreshCommitFilesView() error {
gui.refreshSelectedLine(&gui.State.Panels.CommitFiles.SelectedLine, len(gui.State.CommitFiles))
if err := gui.renderListPanel(gui.getCommitFilesView(), gui.State.CommitFiles); err != nil {
return err
}
commitsFileView := gui.getCommitFilesView()
displayStrings := presentation.GetCommitFileListDisplayStrings(gui.State.CommitFiles)
gui.renderDisplayStrings(commitsFileView, displayStrings)
return gui.handleCommitFileSelect(gui.g, gui.getCommitFilesView())
return gui.handleCommitFileSelect(gui.g, commitsFileView)
}
func (gui *Gui) handleOpenOldCommitFile(g *gocui.Gui, v *gocui.View) error {
@@ -190,9 +197,7 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
}
}
if err := gui.changeMainViewsContext("patch-building"); err != nil {
return err
}
gui.changeMainViewsContext("patch-building")
if err := gui.switchFocus(gui.g, gui.getCommitFilesView(), gui.getMainView()); err != nil {
return err
}
@@ -208,3 +213,8 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
return enterTheFile(selectedLineIdx)
}
func (gui *Gui) onCommitFilesPanelSearchSelect(selectedLine int) error {
gui.State.Panels.CommitFiles.SelectedLine = selectedLine
return gui.handleCommitFileSelect(gui.g, gui.getCommitFilesView())
}
+35
View File
@@ -83,3 +83,38 @@ func (gui *Gui) RenderCommitLength() {
v := gui.getCommitMessageView()
v.Subtitle = gui.getBufferLength(v)
}
// we've just copy+pasted the editor from gocui to here so that we can also re-
// render the commit message length on each keypress
func (gui *Gui) commitMessageEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
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
case key == gocui.KeyCtrlU:
v.EditDeleteToStartOfLine()
case key == gocui.KeyCtrlA:
v.EditGotoToStartOfLine()
case key == gocui.KeyCtrlE:
v.EditGotoToEndOfLine()
default:
v.EditWrite(ch)
}
gui.RenderCommitLength()
}
+163 -81
View File
@@ -1,14 +1,13 @@
package gui
import (
"fmt"
"strconv"
"github.com/fatih/color"
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -37,13 +36,23 @@ func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
return err
}
state := gui.State.Panels.Commits
if state.SelectedLine > 20 && state.LimitCommits {
state.LimitCommits = false
go func() {
if err := gui.refreshCommitsWithLimit(); err != nil {
_ = gui.createErrorPanel(gui.g, err.Error())
}
}()
}
gui.getMainView().Title = "Patch"
gui.getSecondaryView().Title = "Custom Patch"
gui.State.Panels.LineByLine = nil
gui.handleEscapeLineByLinePanel()
commit := gui.getSelectedCommit(g)
if commit == nil {
return gui.renderString(g, "main", gui.Tr.SLocalize("NoCommitsThisBranch"))
return gui.newStringTask("main", gui.Tr.SLocalize("NoCommitsThisBranch"))
}
if err := gui.focusPoint(0, gui.State.Panels.Commits.SelectedLine, len(gui.State.Commits), v); err != nil {
@@ -55,41 +64,32 @@ func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
return nil
}
commitText, err := gui.GitCommand.Show(commit.Sha)
if err != nil {
return err
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowCmdStr(commit.Sha),
)
if err := gui.newPtyTask("main", cmd); err != nil {
gui.Log.Error(err)
}
return gui.renderString(g, "main", commitText)
return nil
}
func (gui *Gui) refreshCommits(g *gocui.Gui) error {
g.Update(func(*gocui.Gui) error {
builder, err := commands.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr, gui.State.CherryPickedCommits, gui.State.DiffEntries)
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)
// I think this is here for the sake of some kind of rebasing thing
gui.refreshStatus(g)
if g.CurrentView() == v {
gui.handleCommitSelect(g, v)
if err := gui.refreshCommitsWithLimit(); err != nil {
return err
}
// doing this async because it shouldn't hold anything up
go func() {
if err := gui.refreshReflogCommits(); err != nil {
_ = gui.createErrorPanel(gui.g, err.Error())
}
}()
if g.CurrentView() == gui.getCommitFilesView() || (g.CurrentView() == gui.getMainView() || gui.State.MainContext == "patch-building") {
return gui.refreshCommitFilesView()
}
@@ -98,6 +98,27 @@ func (gui *Gui) refreshCommits(g *gocui.Gui) error {
return nil
}
func (gui *Gui) refreshCommitsWithLimit() error {
builder, err := commands.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr, gui.State.CherryPickedCommits, gui.State.DiffEntries)
if err != nil {
return err
}
commits, err := builder.GetCommits(gui.State.Panels.Commits.LimitCommits)
if err != nil {
return err
}
gui.State.Commits = commits
if gui.getCommitsView().Context == "branch-commits" {
if err := gui.renderBranchCommitsWithSelection(); err != nil {
return err
}
}
return nil
}
// specific functions
func (gui *Gui) handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error {
@@ -444,7 +465,7 @@ func (gui *Gui) handleToggleDiffCommit(g *gocui.Gui, v *gocui.View) error {
// get selected commit
commit := gui.getSelectedCommit(g)
if commit == nil {
return gui.renderString(g, "main", gui.Tr.SLocalize("NoCommitsThisBranch"))
return gui.newStringTask("main", gui.Tr.SLocalize("NoCommitsThisBranch"))
}
// if already selected commit delete
@@ -467,7 +488,7 @@ func (gui *Gui) handleToggleDiffCommit(g *gocui.Gui, v *gocui.View) error {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.renderString(g, "main", commitText)
return gui.newStringTask("main", commitText)
}
return nil
@@ -538,53 +559,6 @@ func (gui *Gui) handleSquashAllAboveFixupCommits(g *gocui.Gui, v *gocui.View) er
}, nil)
}
type resetOption struct {
description string
command string
}
// GetDisplayStrings is a function.
func (r *resetOption) GetDisplayStrings(isFocused bool) []string {
return []string{r.description, color.New(color.FgRed).Sprint(r.command)}
}
func (gui *Gui) handleCreateCommitResetMenu(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedCommit(g)
if commit == nil {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NoCommitsThisBranch"))
}
strengths := []string{"soft", "mixed", "hard"}
options := make([]*resetOption, len(strengths))
for i, strength := range strengths {
options[i] = &resetOption{
description: fmt.Sprintf("%s reset", strength),
command: fmt.Sprintf("reset --%s %s", strength, commit.Sha),
}
}
handleMenuPress := func(index int) error {
if err := gui.GitCommand.ResetToCommit(commit.Sha, strengths[index]); err != nil {
return err
}
if err := gui.refreshCommits(g); err != nil {
return err
}
if err := gui.refreshFiles(); err != nil {
return err
}
if err := gui.resetOrigin(gui.getCommitsView()); err != nil {
return err
}
gui.State.Panels.Commits.SelectedLine = 0
return gui.handleCommitSelect(g, gui.getCommitsView())
}
return gui.createMenu(fmt.Sprintf("%s %s", gui.Tr.SLocalize("resetTo"), commit.Sha), options, len(options), handleMenuPress)
}
func (gui *Gui) handleTagCommit(g *gocui.Gui, v *gocui.View) error {
// TODO: bring up menu asking if you want to make a lightweight or annotated tag
// if annotated, switch to a subprocess to create the message
@@ -611,3 +585,111 @@ func (gui *Gui) handleCreateLightweightTag(commitSha string) error {
return gui.handleCommitSelect(g, v)
})
}
func (gui *Gui) handleCheckoutCommit(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedCommit(g)
if commit == nil {
return nil
}
return gui.createConfirmationPanel(g, gui.getCommitsView(), true, gui.Tr.SLocalize("checkoutCommit"), gui.Tr.SLocalize("SureCheckoutThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
return gui.handleCheckoutRef(commit.Sha)
}, nil)
}
func (gui *Gui) renderBranchCommitsWithSelection() error {
commitsView := gui.getCommitsView()
gui.refreshSelectedLine(&gui.State.Panels.Commits.SelectedLine, len(gui.State.Commits))
displayStrings := presentation.GetCommitListDisplayStrings(gui.State.Commits, gui.State.ScreenMode != SCREEN_NORMAL)
gui.renderDisplayStrings(commitsView, displayStrings)
if gui.g.CurrentView() == commitsView && commitsView.Context == "branch-commits" {
if err := gui.handleCommitSelect(gui.g, commitsView); err != nil {
return err
}
}
return nil
}
func (gui *Gui) onCommitsTabClick(tabIndex int) error {
contexts := []string{"branch-commits", "reflog-commits"}
commitsView := gui.getCommitsView()
commitsView.TabIndex = tabIndex
return gui.switchCommitsPanelContext(contexts[tabIndex])
}
func (gui *Gui) switchCommitsPanelContext(context string) error {
commitsView := gui.getCommitsView()
commitsView.Context = context
gui.onSearchEscape()
contextTabIndexMap := map[string]int{
"branch-commits": 0,
"reflog-commits": 1,
}
commitsView.TabIndex = contextTabIndexMap[context]
return gui.refreshCommitsViewWithSelection()
}
func (gui *Gui) refreshCommitsViewWithSelection() error {
commitsView := gui.getCommitsView()
switch commitsView.Context {
case "branch-commits":
return gui.renderBranchCommitsWithSelection()
case "reflog-commits":
return gui.renderReflogCommitsWithSelection()
}
return nil
}
func (gui *Gui) handleNextCommitsTab(g *gocui.Gui, v *gocui.View) error {
return gui.onCommitsTabClick(
utils.ModuloWithWrap(v.TabIndex+1, len(v.Tabs)),
)
}
func (gui *Gui) handlePrevCommitsTab(g *gocui.Gui, v *gocui.View) error {
return gui.onCommitsTabClick(
utils.ModuloWithWrap(v.TabIndex-1, len(v.Tabs)),
)
}
func (gui *Gui) handleCreateCommitResetMenu(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedCommit(g)
if commit == nil {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NoCommitsThisBranch"))
}
return gui.createResetMenu(commit.Sha)
}
func (gui *Gui) onCommitsPanelSearchSelect(selectedLine int) error {
commitsView := gui.getCommitsView()
switch commitsView.Context {
case "branch-commits":
gui.State.Panels.Commits.SelectedLine = selectedLine
return gui.handleCommitSelect(gui.g, commitsView)
case "reflog-commits":
gui.State.Panels.ReflogCommits.SelectedLine = selectedLine
return gui.handleReflogCommitSelect(gui.g, commitsView)
}
return nil
}
func (gui *Gui) handleOpenSearchForCommitsPanel(g *gocui.Gui, v *gocui.View) error {
// we usually lazyload these commits but now that we're searching we need to load them now
if gui.State.Panels.Commits.LimitCommits {
gui.State.Panels.Commits.LimitCommits = false
if err := gui.refreshCommits(gui.g); err != nil {
return err
}
}
return gui.handleOpenSearch(gui.g, v)
}
+3
View File
@@ -75,6 +75,9 @@ func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt
return nil, err
}
confirmationView.HasLoader = hasLoader
if hasLoader {
gui.g.StartTicking()
}
confirmationView.Title = title
confirmationView.Wrap = true
confirmationView.FgColor = theme.GocuiDefaultTextColor
+3 -3
View File
@@ -4,9 +4,9 @@ package gui
// which currently just means a context that affects both the main and secondary views
// other views can have their context changed directly but this function helps
// keep the main and secondary views in sync
func (gui *Gui) changeMainViewsContext(context string) error {
func (gui *Gui) changeMainViewsContext(context string) {
if gui.State.MainContext == context {
return nil
return
}
switch context {
@@ -16,5 +16,5 @@ func (gui *Gui) changeMainViewsContext(context string) error {
}
gui.State.MainContext = context
return nil
return
}
+42
View File
@@ -0,0 +1,42 @@
package gui
import (
"github.com/jesseduffield/gocui"
)
func (gui *Gui) handleCreateDiscardMenu(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
}
return nil
}
menuItems := []*menuItem{
{
displayString: gui.Tr.SLocalize("discardAllChanges"),
onPress: func() error {
if err := gui.GitCommand.DiscardAllFileChanges(file); err != nil {
return err
}
return gui.refreshFiles()
},
},
}
if file.HasStagedChanges && file.HasUnstagedChanges {
menuItems = append(menuItems, &menuItem{
displayString: gui.Tr.SLocalize("discardUnstagedChanges"),
onPress: func() error {
if err := gui.GitCommand.DiscardUnstagedFileChanges(file); err != nil {
return err
}
return gui.refreshFiles()
},
})
}
return gui.createMenu(file.Name, menuItems, createMenuOptions{showCancel: true})
}
+108 -23
View File
@@ -6,22 +6,124 @@ import (
"github.com/fsnotify/fsnotify"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/sirupsen/logrus"
)
// macs for some bizarre reason cap the number of watchable files to 256.
// there's no obvious platform agonstic way to check the situation of the user's
// computer so we're just arbitrarily capping at 200. This isn't so bad because
// file watching is only really an added bonus for faster refreshing.
const MAX_WATCHED_FILES = 50
type fileWatcher struct {
Watcher *fsnotify.Watcher
WatchedFilenames []string
Log *logrus.Entry
Disabled bool
}
func NewFileWatcher(log *logrus.Entry) *fileWatcher {
watcher, err := fsnotify.NewWatcher()
log.Error(err)
return &fileWatcher{
Disabled: true,
}
if err != nil {
log.Error(err)
return &fileWatcher{
Disabled: true,
}
}
return &fileWatcher{
Watcher: watcher,
Log: log,
WatchedFilenames: make([]string, 0, MAX_WATCHED_FILES),
}
}
func (w *fileWatcher) watchingFilename(filename string) bool {
for _, watchedFilename := range w.WatchedFilenames {
if watchedFilename == filename {
return true
}
}
return false
}
func (w *fileWatcher) popOldestFilename() {
// shift the last off the array to make way for this one
oldestFilename := w.WatchedFilenames[0]
w.WatchedFilenames = w.WatchedFilenames[1:]
if err := w.Watcher.Remove(oldestFilename); err != nil {
// swallowing errors here because it doesn't really matter if we can't unwatch a file
w.Log.Warn(err)
}
}
func (w *fileWatcher) watchFilename(filename string) {
w.Log.Warn(filename)
if err := w.Watcher.Add(filename); err != nil {
// swallowing errors here because it doesn't really matter if we can't watch a file
w.Log.Warn(err)
}
// assume we're watching it now to be safe
w.WatchedFilenames = append(w.WatchedFilenames, filename)
}
func (w *fileWatcher) addFilesToFileWatcher(files []*commands.File) error {
if w.Disabled {
return nil
}
if len(files) == 0 {
return nil
}
// watch the files for changes
dirName, err := os.Getwd()
if err != nil {
return err
}
for _, file := range files[0:min(MAX_WATCHED_FILES, len(files))] {
if file.Deleted {
continue
}
filename := filepath.Join(dirName, file.Name)
if w.watchingFilename(filename) {
continue
}
if len(w.WatchedFilenames) > MAX_WATCHED_FILES {
w.popOldestFilename()
}
w.watchFilename(filename)
}
return nil
}
func min(a int, b int) int {
if a < b {
return a
}
return b
}
// NOTE: given that we often edit files ourselves, this may make us end up refreshing files too often
// TODO: consider watching the whole directory recursively (could be more expensive)
func (gui *Gui) watchFilesForChanges() {
var err error
gui.fileWatcher, err = fsnotify.NewWatcher()
if err != nil {
gui.Log.Error(err)
gui.fileWatcher = NewFileWatcher(gui.Log)
if gui.fileWatcher.Disabled {
return
}
go func() {
for {
select {
// watch for events
case event := <-gui.fileWatcher.Events:
case event := <-gui.fileWatcher.Watcher.Events:
if event.Op == fsnotify.Chmod {
// for some reason we pick up chmod events when they don't actually happen
continue
@@ -37,7 +139,7 @@ func (gui *Gui) watchFilesForChanges() {
}
// watch for errors
case err := <-gui.fileWatcher.Errors:
case err := <-gui.fileWatcher.Watcher.Errors:
if err != nil {
gui.Log.Warn(err)
}
@@ -45,20 +147,3 @@ func (gui *Gui) watchFilesForChanges() {
}
}()
}
func (gui *Gui) addFilesToFileWatcher(files []*commands.File) error {
// watch the files for changes
dirName, err := os.Getwd()
if err != nil {
return err
}
for _, file := range files {
if err := gui.fileWatcher.Add(filepath.Join(dirName, file.Name)); err != nil {
// swallowing errors here because it doesn't really matter if we can't watch a file
gui.Log.Warn(err)
}
}
return nil
}
+104 -214
View File
@@ -10,10 +10,9 @@ import (
"fmt"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
)
// list panel functions
@@ -27,21 +26,15 @@ func (gui *Gui) getSelectedFile(g *gocui.Gui) (*commands.File, error) {
return gui.State.Files[selectedLine], nil
}
func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View) error {
return gui.selectFile(false)
}
func (gui *Gui) selectFile(alreadySelected bool) error {
if _, err := gui.g.SetCurrentView("files"); err != nil {
return err
}
file, err := gui.getSelectedFile(gui.g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
}
return gui.renderString(gui.g, "main", gui.Tr.SLocalize("NoChangedFiles"))
gui.State.SplitMainPanel = false
gui.getMainView().Title = ""
return gui.newStringTask("main", gui.Tr.SLocalize("NoChangedFiles"))
}
if err := gui.focusPoint(0, gui.State.Panels.Files.SelectedLine, len(gui.State.Files), gui.getFilesView()); err != nil {
@@ -54,37 +47,40 @@ func (gui *Gui) selectFile(alreadySelected bool) error {
return gui.refreshMergePanel()
}
content := gui.GitCommand.Diff(file, false, false)
contentCached := gui.GitCommand.Diff(file, false, true)
leftContent := content
if !alreadySelected {
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
if err := gui.resetOrigin(gui.getSecondaryView()); err != nil {
return err
}
}
if file.HasStagedChanges && file.HasUnstagedChanges {
gui.State.SplitMainPanel = true
gui.getMainView().Title = gui.Tr.SLocalize("UnstagedChanges")
gui.getSecondaryView().Title = gui.Tr.SLocalize("StagedChanges")
cmdStr := gui.GitCommand.DiffCmdStr(file, false, true)
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
if err := gui.newPtyTask("secondary", cmd); err != nil {
return err
}
} else {
gui.State.SplitMainPanel = false
if file.HasUnstagedChanges {
leftContent = content
gui.getMainView().Title = gui.Tr.SLocalize("UnstagedChanges")
} else {
leftContent = contentCached
gui.getMainView().Title = gui.Tr.SLocalize("StagedChanges")
}
}
if alreadySelected {
gui.g.Update(func(*gocui.Gui) error {
if err := gui.setViewContent(gui.g, gui.getSecondaryView(), contentCached); err != nil {
return err
}
return gui.setViewContent(gui.g, gui.getMainView(), leftContent)
})
return nil
}
if err := gui.renderString(gui.g, "secondary", contentCached); err != nil {
cmdStr := gui.GitCommand.DiffCmdStr(file, false, !file.HasUnstagedChanges && file.HasStagedChanges)
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
if err := gui.newPtyTask("main", cmd); err != nil {
return err
}
return gui.renderString(gui.g, "main", leftContent)
return nil
}
func (gui *Gui) refreshFiles() error {
@@ -107,14 +103,8 @@ func (gui *Gui) refreshFiles() error {
}
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)
displayStrings := presentation.GetFileListDisplayStrings(gui.State.Files)
gui.renderDisplayStrings(filesView, displayStrings)
if g.CurrentView() == filesView || (g.CurrentView() == gui.getMainView() && g.CurrentView().Context == "merging") {
newSelectedFile, _ := gui.getSelectedFile(gui.g)
@@ -177,9 +167,7 @@ func (gui *Gui) enterFile(forceSecondaryFocused bool, selectedLineIdx int) error
if file.HasMergeConflicts {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("FileStagingRequirements"))
}
if err := gui.changeMainViewsContext("staging"); err != nil {
return err
}
gui.changeMainViewsContext("staging")
if err := gui.switchFocus(gui.g, gui.getFilesView(), gui.getMainView()); err != nil {
return err
}
@@ -221,6 +209,14 @@ func (gui *Gui) allFilesStaged() bool {
return true
}
func (gui *Gui) focusAndSelectFile(g *gocui.Gui, v *gocui.View) error {
if _, err := gui.g.SetCurrentView("files"); err != nil {
return err
}
return gui.selectFile(false)
}
func (gui *Gui) handleStageAll(g *gocui.Gui, v *gocui.View) error {
var err error
if gui.allFilesStaged() {
@@ -236,20 +232,33 @@ func (gui *Gui) handleStageAll(g *gocui.Gui, v *gocui.View) error {
return err
}
return gui.handleFileSelect(gui.g, v)
return gui.selectFile(false)
}
func (gui *Gui) handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
file, err := gui.getSelectedFile(gui.g)
if err != nil {
return gui.createErrorPanel(g, err.Error())
return gui.createErrorPanel(gui.g, err.Error())
}
if file.Tracked {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantIgnoreTrackFiles"))
return gui.createConfirmationPanel(gui.g, gui.g.CurrentView(), true, gui.Tr.SLocalize("IgnoreTracked"), gui.Tr.SLocalize("IgnoreTrackedPrompt"),
// On confirmation
func(_ *gocui.Gui, _ *gocui.View) error {
if err := gui.GitCommand.Ignore(file.Name); err != nil {
return err
}
if err := gui.GitCommand.RemoveTrackedFiles(file.Name); err != nil {
return err
}
return gui.refreshFiles()
}, nil)
}
if err := gui.GitCommand.Ignore(file.Name); err != nil {
return gui.createErrorPanel(g, err.Error())
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshFiles()
}
@@ -356,7 +365,7 @@ func (gui *Gui) refreshStateFiles() error {
files := gui.GitCommand.GetStatusFiles()
gui.State.Files = gui.GitCommand.MergeStatusFiles(gui.State.Files, files)
if err := gui.addFilesToFileWatcher(files); err != nil {
if err := gui.fileWatcher.addFilesToFileWatcher(files); err != nil {
return err
}
@@ -370,15 +379,15 @@ func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) {
if err != gui.Errors.ErrNoFiles {
return "", err
}
return "", gui.renderString(g, "main", gui.Tr.SLocalize("NoFilesDisplay"))
return "", gui.newStringTask("main", gui.Tr.SLocalize("NoFilesDisplay"))
}
if item.Type != "file" {
return "", gui.renderString(g, "main", gui.Tr.SLocalize("NotAFile"))
return "", gui.newStringTask("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 "", gui.newStringTask("main", err.Error())
}
return cat, nil
}
@@ -391,6 +400,17 @@ func (gui *Gui) handlePullFiles(g *gocui.Gui, v *gocui.View) error {
return err
}
if pullables == "?" {
// see if we have this branch in our config with an upstream
conf, err := gui.GitCommand.Repo.Config()
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
for branchName, branch := range conf.Branches {
if branchName == currentBranchName {
return gui.pullFiles(v, fmt.Sprintf("%s %s", branch.Remote, branchName))
}
}
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("EnterUpstream"), "origin/"+currentBranchName, func(g *gocui.Gui, v *gocui.View) error {
upstream := gui.trimmedContent(v)
if err := gui.GitCommand.SetUpstreamBranch(upstream); err != nil {
@@ -400,21 +420,21 @@ func (gui *Gui) handlePullFiles(g *gocui.Gui, v *gocui.View) error {
}
return gui.createErrorPanel(gui.g, errorMessage)
}
return gui.pullFiles(v)
return gui.pullFiles(v, "")
})
}
return gui.pullFiles(v)
return gui.pullFiles(v, "")
}
func (gui *Gui) pullFiles(v *gocui.View) error {
func (gui *Gui) pullFiles(v *gocui.View, args string) 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 {
err := gui.GitCommand.Pull(args, func(passOrUname string) string {
unamePassOpend = true
return gui.waitForPassUname(gui.g, v, passOrUname)
})
@@ -424,14 +444,14 @@ func (gui *Gui) pullFiles(v *gocui.View) error {
return nil
}
func (gui *Gui) pushWithForceFlag(g *gocui.Gui, v *gocui.View, force bool, upstream string) error {
func (gui *Gui) pushWithForceFlag(g *gocui.Gui, v *gocui.View, force bool, upstream string, args string) error {
if err := gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("PushWait")); err != nil {
return err
}
go func() {
unamePassOpend := false
branchName := gui.getCheckedOutBranch().Name
err := gui.GitCommand.Push(branchName, force, upstream, func(passOrUname string) string {
err := gui.GitCommand.Push(branchName, force, upstream, args, func(passOrUname string) string {
unamePassOpend = true
return gui.waitForPassUname(g, v, passOrUname)
})
@@ -449,14 +469,25 @@ func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error {
}
if pullables == "?" {
// see if we have this branch in our config with an upstream
conf, err := gui.GitCommand.Repo.Config()
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
for branchName, branch := range conf.Branches {
if branchName == currentBranchName {
return gui.pushWithForceFlag(g, v, false, "", fmt.Sprintf("%s %s", branch.Remote, branchName))
}
}
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("EnterUpstream"), "origin "+currentBranchName, func(g *gocui.Gui, v *gocui.View) error {
return gui.pushWithForceFlag(g, v, false, gui.trimmedContent(v))
return gui.pushWithForceFlag(g, v, false, gui.trimmedContent(v), "")
})
} else if pullables == "0" {
return gui.pushWithForceFlag(g, v, false, "")
return gui.pushWithForceFlag(g, v, false, "", "")
}
return gui.createConfirmationPanel(g, nil, true, gui.Tr.SLocalize("ForcePush"), gui.Tr.SLocalize("ForcePushPrompt"), func(g *gocui.Gui, v *gocui.View) error {
return gui.pushWithForceFlag(g, v, true, "")
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("ForcePush"), gui.Tr.SLocalize("ForcePushPrompt"), func(g *gocui.Gui, v *gocui.View) error {
return gui.pushWithForceFlag(g, v, true, "", "")
}, nil)
}
@@ -471,9 +502,7 @@ func (gui *Gui) handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error {
if !file.HasInlineMergeConflicts {
return gui.createErrorPanel(g, gui.Tr.SLocalize("FileNoMergeCons"))
}
if err := gui.changeMainViewsContext("merging"); err != nil {
return err
}
gui.changeMainViewsContext("merging")
if err := gui.switchFocus(g, v, gui.getMainView()); err != nil {
return err
}
@@ -505,134 +534,6 @@ func (gui *Gui) anyFilesWithMergeConflicts() bool {
return false
}
type discardOption struct {
handler func(fileName *commands.File) error
description string
}
type discardAllOption struct {
handler func() error
description string
command string
}
// GetDisplayStrings is a function.
func (r *discardOption) GetDisplayStrings(isFocused bool) []string {
return []string{r.description}
}
// GetDisplayStrings is a function.
func (r *discardAllOption) GetDisplayStrings(isFocused bool) []string {
return []string{r.description, color.New(color.FgRed).Sprint(r.command)}
}
func (gui *Gui) handleCreateDiscardMenu(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
}
return nil
}
options := []*discardOption{
{
description: gui.Tr.SLocalize("discardAllChanges"),
handler: func(file *commands.File) error {
return gui.GitCommand.DiscardAllFileChanges(file)
},
},
{
description: gui.Tr.SLocalize("cancel"),
handler: func(file *commands.File) error {
return nil
},
},
}
if file.HasStagedChanges && file.HasUnstagedChanges {
discardUnstagedChanges := &discardOption{
description: gui.Tr.SLocalize("discardUnstagedChanges"),
handler: func(file *commands.File) error {
return gui.GitCommand.DiscardUnstagedFileChanges(file)
},
}
options = append(options[:1], append([]*discardOption{discardUnstagedChanges}, options[1:]...)...)
}
handleMenuPress := func(index int) error {
file, err := gui.getSelectedFile(g)
if err != nil {
return err
}
if err := options[index].handler(file); err != nil {
return err
}
return gui.refreshFiles()
}
return gui.createMenu(file.Name, options, len(options), handleMenuPress)
}
func (gui *Gui) handleCreateResetMenu(g *gocui.Gui, v *gocui.View) error {
options := []*discardAllOption{
{
description: gui.Tr.SLocalize("discardAllChangesToAllFiles"),
command: "reset --hard HEAD && git clean -fd",
handler: func() error {
return gui.GitCommand.ResetAndClean()
},
},
{
description: gui.Tr.SLocalize("discardAnyUnstagedChanges"),
command: "git checkout -- .",
handler: func() error {
return gui.GitCommand.DiscardAnyUnstagedFileChanges()
},
},
{
description: gui.Tr.SLocalize("discardUntrackedFiles"),
command: "git clean -fd",
handler: func() error {
return gui.GitCommand.RemoveUntrackedFiles()
},
},
{
description: gui.Tr.SLocalize("softReset"),
command: "git reset --soft HEAD",
handler: func() error {
return gui.GitCommand.ResetSoftHead()
},
},
{
description: gui.Tr.SLocalize("hardReset"),
command: "git reset --hard HEAD",
handler: func() error {
return gui.GitCommand.ResetHardHead()
},
},
{
description: gui.Tr.SLocalize("cancel"),
handler: func() error {
return nil
},
},
}
handleMenuPress := func(index int) error {
if err := options[index].handler(); err != nil {
return err
}
return gui.refreshFiles()
}
return gui.createMenu("", options, len(options), handleMenuPress)
}
func (gui *Gui) handleCustomCommand(g *gocui.Gui, v *gocui.View) error {
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("CustomCommand"), "", func(g *gocui.Gui, v *gocui.View) error {
command := gui.trimmedContent(v)
@@ -641,45 +542,34 @@ func (gui *Gui) handleCustomCommand(g *gocui.Gui, v *gocui.View) error {
})
}
type stashOption struct {
description string
handler func() error
}
// GetDisplayStrings is a function.
func (o *stashOption) GetDisplayStrings(isFocused bool) []string {
return []string{o.description}
}
func (gui *Gui) handleCreateStashMenu(g *gocui.Gui, v *gocui.View) error {
options := []*stashOption{
menuItems := []*menuItem{
{
description: gui.Tr.SLocalize("stashAllChanges"),
handler: func() error {
displayString: gui.Tr.SLocalize("stashAllChanges"),
onPress: func() error {
return gui.handleStashSave(gui.GitCommand.StashSave)
},
},
{
description: gui.Tr.SLocalize("stashStagedChanges"),
handler: func() error {
displayString: gui.Tr.SLocalize("stashStagedChanges"),
onPress: func() error {
return gui.handleStashSave(gui.GitCommand.StashSaveStagedChanges)
},
},
{
description: gui.Tr.SLocalize("cancel"),
handler: func() error {
return nil
},
},
}
handleMenuPress := func(index int) error {
return options[index].handler()
}
return gui.createMenu(gui.Tr.SLocalize("stashOptions"), options, len(options), handleMenuPress)
return gui.createMenu(gui.Tr.SLocalize("stashOptions"), menuItems, createMenuOptions{showCancel: true})
}
func (gui *Gui) handleStashChanges(g *gocui.Gui, v *gocui.View) error {
return gui.handleStashSave(gui.GitCommand.StashSave)
}
func (gui *Gui) handleCreateResetToUpstreamMenu(g *gocui.Gui, v *gocui.View) error {
return gui.createResetMenu("@{upstream}")
}
func (gui *Gui) onFilesPanelSearchSelect(selectedLine int) error {
gui.State.Panels.Files.SelectedLine = selectedLine
return gui.focusAndSelectFile(gui.g, gui.getFilesView())
}
+86
View File
@@ -0,0 +1,86 @@
package gui
import (
"fmt"
"regexp"
"strings"
"github.com/jesseduffield/gocui"
)
func (gui *Gui) gitFlowFinishBranch(gitFlowConfig string, branchName string) error {
// need to find out what kind of branch this is
prefix := strings.SplitAfterN(branchName, "/", 2)[0]
suffix := strings.Replace(branchName, prefix, "", 1)
branchType := ""
for _, line := range strings.Split(strings.TrimSpace(gitFlowConfig), "\n") {
if strings.HasPrefix(line, "gitflow.prefix.") && strings.HasSuffix(line, prefix) {
// now I just need to how do you say
regex := regexp.MustCompile("gitflow.prefix.([^ ]*) .*")
matches := regex.FindAllStringSubmatch(line, 1)
if len(matches) > 0 && len(matches[0]) > 1 {
branchType = matches[0][1]
break
}
}
}
if branchType == "" {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NotAGitFlowBranch"))
}
subProcess := gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "finish", suffix)
gui.SubProcess = subProcess
return gui.Errors.ErrSubProcess
}
func (gui *Gui) handleCreateGitFlowMenu(g *gocui.Gui, v *gocui.View) error {
branch := gui.getSelectedBranch()
if branch == nil {
return nil
}
// get config
gitFlowConfig, err := gui.OSCommand.RunCommandWithOutput("git config --local --get-regexp gitflow")
if err != nil {
return gui.createErrorPanel(gui.g, "You need to install git-flow and enable it in this repo to use git-flow features")
}
startHandler := func(branchType string) func() error {
return func() error {
title := gui.Tr.TemplateLocalize("NewBranchNamePrompt", map[string]interface{}{"branchType": branchType})
return gui.createPromptPanel(gui.g, gui.getMenuView(), title, "", func(g *gocui.Gui, v *gocui.View) error {
name := gui.trimmedContent(v)
subProcess := gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "start", name)
gui.SubProcess = subProcess
return gui.Errors.ErrSubProcess
})
}
}
menuItems := []*menuItem{
{
// not localising here because it's one to one with the actual git flow commands
displayString: fmt.Sprintf("finish branch '%s'", branch.Name),
onPress: func() error {
return gui.gitFlowFinishBranch(gitFlowConfig, branch.Name)
},
},
{
displayString: "start feature",
onPress: startHandler("feature"),
},
{
displayString: "start hotfix",
onPress: startHandler("hotfix"),
},
{
displayString: "start release",
onPress: startHandler("release"),
},
}
return gui.createMenu("git flow", menuItems, createMenuOptions{})
}
+320 -121
View File
@@ -5,6 +5,7 @@ import (
"io/ioutil"
"math"
"os"
"runtime"
"sync"
// "io"
@@ -14,7 +15,6 @@ import (
"strings"
"time"
"github.com/fsnotify/fsnotify"
"github.com/go-errors/errors"
// "strings"
@@ -24,13 +24,22 @@ import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/tasks"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/updates"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/mattn/go-runewidth"
"github.com/sirupsen/logrus"
)
const (
SCREEN_NORMAL int = iota
SCREEN_HALF
SCREEN_FULL
)
const StartupPopupVersion = 1
// OverlappingEdges determines if panel edges overlap
@@ -67,20 +76,22 @@ 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
fileWatcher *fsnotify.Watcher
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
fileWatcher *fileWatcher
viewBufferManagerMap map[string]*tasks.ViewBufferManager
stopChan chan struct{}
}
// for now the staging panel state, unlike the other panel states, is going to be
@@ -127,6 +138,11 @@ type tagsPanelState struct {
type commitPanelState struct {
SelectedLine int
SpecificDiffMode bool
LimitCommits bool
}
type reflogCommitPanelState struct {
SelectedLine int
}
type stashPanelState struct {
@@ -154,6 +170,7 @@ type panelStates struct {
RemoteBranches *remoteBranchesState
Tags *tagsPanelState
Commits *commitPanelState
ReflogCommits *reflogCommitPanelState
Stash *stashPanelState
Menu *menuPanelState
LineByLine *lineByLinePanelState
@@ -162,12 +179,19 @@ type panelStates struct {
Status *statusPanelState
}
type searchingState struct {
view *gocui.View
isSearching bool
searchString string
}
type guiState struct {
Files []*commands.File
Branches []*commands.Branch
Commits []*commands.Commit
StashEntries []*commands.StashEntry
CommitFiles []*commands.CommitFile
ReflogCommits []*commands.Commit
DiffEntries []*commands.Commit
Remotes []*commands.Remote
RemoteBranches []*commands.RemoteBranch
@@ -184,6 +208,12 @@ type guiState struct {
RetainOriginalDir bool
IsRefreshingFiles bool
RefreshingFilesMutex sync.Mutex
Searching searchingState
ScreenMode int
SideView *gocui.View
Ptmx *os.File
PrevMainWidth int
PrevMainHeight int
}
// for now the split view will always be on
@@ -205,7 +235,8 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *comma
Remotes: &remotePanelState{SelectedLine: 0},
RemoteBranches: &remoteBranchesState{SelectedLine: -1},
Tags: &tagsPanelState{SelectedLine: -1},
Commits: &commitPanelState{SelectedLine: -1},
Commits: &commitPanelState{SelectedLine: -1, LimitCommits: true},
ReflogCommits: &reflogCommitPanelState{SelectedLine: 0}, // TODO: might need to make -1
CommitFiles: &commitFilesPanelState{SelectedLine: -1},
Stash: &stashPanelState{SelectedLine: -1},
Menu: &menuPanelState{SelectedLine: 0},
@@ -217,17 +248,21 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *comma
},
Status: &statusPanelState{},
},
ScreenMode: SCREEN_NORMAL,
SideView: nil,
Ptmx: nil,
}
gui := &Gui{
Log: log,
GitCommand: gitCommand,
OSCommand: oSCommand,
State: initialState,
Config: config,
Tr: tr,
Updater: updater,
statusManager: &statusManager{},
Log: log,
GitCommand: gitCommand,
OSCommand: oSCommand,
State: initialState,
Config: config,
Tr: tr,
Updater: updater,
statusManager: &statusManager{},
viewBufferManagerMap: map[string]*tasks.ViewBufferManager{},
}
gui.watchFilesForChanges()
@@ -237,6 +272,26 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *comma
return gui, nil
}
func (gui *Gui) nextScreenMode(g *gocui.Gui, v *gocui.View) error {
gui.State.ScreenMode = utils.NextIntInCycle([]int{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode)
// commits render differently depending on whether we're in fullscreen more or not
if err := gui.refreshCommitsViewWithSelection(); err != nil {
return err
}
return nil
}
func (gui *Gui) prevScreenMode(g *gocui.Gui, v *gocui.View) error {
gui.State.ScreenMode = utils.PrevIntInCycle([]int{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode)
// commits render differently depending on whether we're in fullscreen more or not
if err := gui.refreshCommitsViewWithSelection(); err != nil {
return err
}
return nil
}
func (gui *Gui) scrollUpView(viewName string) error {
mainView, _ := gui.g.View(viewName)
ox, oy := mainView.Origin()
@@ -252,8 +307,14 @@ func (gui *Gui) scrollDownView(viewName string) error {
_, sy := mainView.Size()
y += sy
}
if y < len(mainView.BufferLines()) {
return mainView.SetOrigin(ox, oy+gui.Config.GetUserConfig().GetInt("gui.scrollHeight"))
scrollHeight := gui.Config.GetUserConfig().GetInt("gui.scrollHeight")
if y < mainView.LinesHeight() {
if err := mainView.SetOrigin(ox, oy+scrollHeight); err != nil {
return err
}
}
if manager, ok := gui.viewBufferManagerMap[viewName]; ok {
manager.ReadLines(scrollHeight)
}
return nil
}
@@ -319,20 +380,19 @@ func (gui *Gui) onFocusLost(v *gocui.View, newView *gocui.View) error {
if v == nil {
return nil
}
if v.IsSearching() && newView.Name() != "search" {
gui.onSearchEscape()
}
switch v.Name() {
case "branches":
if v.Context == "local-branches" {
// This stops the branches panel from showing the upstream/downstream changes to the selected branch, when it loses focus
// inside renderListPanel it checks to see if the panel has focus
if err := gui.renderListPanel(gui.getBranchesView(), gui.State.Branches); err != nil {
return err
}
displayStrings := presentation.GetBranchListDisplayStrings(gui.State.Branches, false, -1)
gui.renderDisplayStrings(gui.getBranchesView(), displayStrings)
}
case "main":
// if we have lost focus to a first-class panel, we need to do some cleanup
if err := gui.changeMainViewsContext("normal"); err != nil {
return err
}
gui.changeMainViewsContext("normal")
case "commitFiles":
if gui.State.MainContext != "patch-building" {
if _, err := gui.g.SetViewOnBottom(v.Name()); err != nil {
@@ -352,6 +412,75 @@ func (gui *Gui) onFocus(v *gocui.View) error {
return nil
}
func (gui *Gui) getViewHeights() map[string]int {
currView := gui.g.CurrentView()
currentCyclebleView := gui.State.PreviousView
if currView != nil {
viewName := currView.Name()
usePreviousView := true
for _, view := range cyclableViews {
if view == viewName {
currentCyclebleView = viewName
usePreviousView = false
break
}
}
if usePreviousView {
currentCyclebleView = gui.State.PreviousView
}
}
// unfortunate result of the fact that these are separate views, have to map explicitly
if currentCyclebleView == "commitFiles" {
currentCyclebleView = "commits"
}
_, height := gui.g.Size()
if gui.State.ScreenMode == SCREEN_FULL || gui.State.ScreenMode == SCREEN_HALF {
vHeights := map[string]int{
"status": 0,
"files": 0,
"branches": 0,
"commits": 0,
"stash": 0,
"options": 0,
}
vHeights[currentCyclebleView] = height - 1
return vHeights
}
usableSpace := height - 7
extraSpace := usableSpace - (usableSpace/3)*3
if height >= 28 {
return map[string]int{
"status": 3,
"files": (usableSpace / 3) + extraSpace,
"branches": usableSpace / 3,
"commits": usableSpace / 3,
"stash": 3,
"options": 1,
}
}
defaultHeight := 3
if height < 21 {
defaultHeight = 1
}
vHeights := map[string]int{
"status": defaultHeight,
"files": defaultHeight,
"branches": defaultHeight,
"commits": defaultHeight,
"stash": defaultHeight,
"options": defaultHeight,
}
vHeights[currentCyclebleView] = height - defaultHeight*4 - 1
return vHeights
}
// layout is called for every screen re-render e.g. when the screen is resized
func (gui *Gui) layout(g *gocui.Gui) error {
g.Highlight = true
@@ -378,50 +507,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
return nil
}
currView := gui.g.CurrentView()
currentCyclebleView := gui.State.PreviousView
if currView != nil {
viewName := currView.Name()
usePreviouseView := true
for _, view := range cyclableViews {
if view == viewName {
currentCyclebleView = viewName
usePreviouseView = false
break
}
}
if usePreviouseView {
currentCyclebleView = gui.State.PreviousView
}
}
usableSpace := height - 7
extraSpace := usableSpace - (usableSpace/3)*3
vHeights := map[string]int{
"status": 3,
"files": (usableSpace / 3) + extraSpace,
"branches": usableSpace / 3,
"commits": usableSpace / 3,
"stash": 3,
"options": 1,
}
if height < 28 {
defaultHeight := 3
if height < 21 {
defaultHeight = 1
}
vHeights = map[string]int{
"status": defaultHeight,
"files": defaultHeight,
"branches": defaultHeight,
"commits": defaultHeight,
"stash": defaultHeight,
"options": defaultHeight,
}
vHeights[currentCyclebleView] = height - defaultHeight*4 - 1
}
vHeights := gui.getViewHeights()
optionsVersionBoundary := width - max(len(utils.Decolorise(information)), 1)
@@ -431,21 +517,51 @@ func (gui *Gui) layout(g *gocui.Gui) error {
appStatusOptionsBoundary = len(appStatus) + 2
}
panelSpacing := 1
if OverlappingEdges {
panelSpacing = 0
}
_, _ = g.SetViewOnBottom("limit")
g.DeleteView("limit")
sidePanelWidthRatio := gui.Config.GetUserConfig().GetFloat64("gui.sidePanelWidth")
textColor := theme.GocuiDefaultTextColor
leftSideWidth := width / 3
var leftSideWidth int
switch gui.State.ScreenMode {
case SCREEN_NORMAL:
leftSideWidth = int(float64(width) * sidePanelWidthRatio)
case SCREEN_HALF:
leftSideWidth = width / 2
case SCREEN_FULL:
currentView := gui.g.CurrentView()
if currentView != nil && currentView.Name() == "main" {
leftSideWidth = 0
} else {
leftSideWidth = width - 1
}
}
panelSplitX := width - 1
mainPanelLeft := leftSideWidth + 1
mainPanelRight := width - 1
secondaryPanelLeft := width - 1
secondaryPanelTop := 0
mainPanelBottom := height - 2
if gui.State.SplitMainPanel {
units := 7
leftSideWidth = width / units
panelSplitX = (1 + ((units - 1) / 2)) * width / units
if gui.State.ScreenMode == SCREEN_FULL {
mainPanelLeft = 0
panelSplitX = width/2 - 4
mainPanelRight = panelSplitX
secondaryPanelLeft = panelSplitX + 1
} else if width < 220 {
mainPanelBottom = height/2 - 1
secondaryPanelTop = mainPanelBottom + 1
secondaryPanelLeft = leftSideWidth + 1
} else {
units := 5
leftSideWidth = width / units
mainPanelLeft = leftSideWidth + 1
panelSplitX = (1 + ((units - 1) / 2)) * width / units
mainPanelRight = panelSplitX
secondaryPanelLeft = panelSplitX + 1
}
}
main := "main"
@@ -456,7 +572,22 @@ func (gui *Gui) layout(g *gocui.Gui) error {
secondary = "main"
}
v, err := g.SetView(main, leftSideWidth+panelSpacing, 0, panelSplitX, height-2, gocui.LEFT)
// reading more lines into main view buffers upon resize
prevMainView, err := gui.g.View("main")
if err == nil {
_, prevMainHeight := prevMainView.Size()
heightDiff := mainPanelBottom - prevMainHeight - 1
if heightDiff > 0 {
if manager, ok := gui.viewBufferManagerMap["main"]; ok {
manager.ReadLines(heightDiff)
}
if manager, ok := gui.viewBufferManagerMap["secondary"]; ok {
manager.ReadLines(heightDiff)
}
}
}
v, err := g.SetView(main, mainPanelLeft, 0, mainPanelRight, mainPanelBottom, gocui.LEFT)
if err != nil {
if err.Error() != "unknown view" {
return err
@@ -464,13 +595,16 @@ func (gui *Gui) layout(g *gocui.Gui) error {
v.Title = gui.Tr.SLocalize("DiffTitle")
v.Wrap = true
v.FgColor = textColor
v.IgnoreCarriageReturns = true
}
hiddenViewOffset := 0
hiddenViewOffset := 9999
hiddenSecondaryPanelOffset := 0
if !gui.State.SplitMainPanel {
hiddenViewOffset = 9999
hiddenSecondaryPanelOffset = hiddenViewOffset
}
secondaryView, err := g.SetView(secondary, panelSplitX+1+hiddenViewOffset, hiddenViewOffset, width-1+hiddenViewOffset, height-2+hiddenViewOffset, gocui.LEFT)
secondaryView, err := g.SetView(secondary, secondaryPanelLeft+hiddenSecondaryPanelOffset, hiddenSecondaryPanelOffset+secondaryPanelTop, width-1+hiddenSecondaryPanelOffset, height-2+hiddenSecondaryPanelOffset, gocui.LEFT)
if err != nil {
if err.Error() != "unknown view" {
return err
@@ -478,6 +612,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
secondaryView.Title = gui.Tr.SLocalize("DiffTitle")
secondaryView.Wrap = true
secondaryView.FgColor = gocui.ColorWhite
secondaryView.IgnoreCarriageReturns = true
}
if v, err := g.SetView("status", 0, 0, leftSideWidth, vHeights["status"]-1, gocui.BOTTOM|gocui.RIGHT); err != nil {
@@ -495,7 +630,8 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
filesView.Highlight = true
filesView.Title = gui.Tr.SLocalize("FilesTitle")
v.FgColor = textColor
filesView.SetOnSelectItem(gui.onSelectItemWrapper(gui.onFilesPanelSearchSelect))
filesView.ContainsList = true
}
branchesView, err := g.SetViewBeneath("branches", "files", vHeights["branches"])
@@ -506,6 +642,8 @@ func (gui *Gui) layout(g *gocui.Gui) error {
branchesView.Title = gui.Tr.SLocalize("BranchesTitle")
branchesView.Tabs = []string{"Local Branches", "Remotes", "Tags"}
branchesView.FgColor = textColor
branchesView.SetOnSelectItem(gui.onSelectItemWrapper(gui.onBranchesPanelSearchSelect))
branchesView.ContainsList = true
}
if v, err := g.SetViewBeneath("commitFiles", "branches", vHeights["commits"]); err != nil {
@@ -514,6 +652,8 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
v.Title = gui.Tr.SLocalize("CommitFiles")
v.FgColor = textColor
v.SetOnSelectItem(gui.onSelectItemWrapper(gui.onCommitFilesPanelSearchSelect))
v.ContainsList = true
}
commitsView, err := g.SetViewBeneath("commits", "branches", vHeights["commits"])
@@ -522,7 +662,10 @@ func (gui *Gui) layout(g *gocui.Gui) error {
return err
}
commitsView.Title = gui.Tr.SLocalize("CommitsTitle")
commitsView.Tabs = []string{"Commits", "Reflog"}
commitsView.FgColor = textColor
commitsView.SetOnSelectItem(gui.onSelectItemWrapper(gui.onCommitsPanelSearchSelect))
commitsView.ContainsList = true
}
stashView, err := g.SetViewBeneath("stash", "commits", vHeights["stash"])
@@ -532,6 +675,8 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
stashView.Title = gui.Tr.SLocalize("StashTitle")
stashView.FgColor = textColor
stashView.SetOnSelectItem(gui.onSelectItemWrapper(gui.onStashPanelSearchSelect))
stashView.ContainsList = true
}
if v, err := g.SetView("options", appStatusOptionsBoundary-1, height-2, optionsVersionBoundary-1, height, 0); err != nil {
@@ -539,13 +684,12 @@ func (gui *Gui) layout(g *gocui.Gui) error {
return err
}
v.Frame = false
userConfig := gui.Config.GetUserConfig()
v.FgColor = theme.GetColor(userConfig.GetStringSlice("gui.theme.optionsTextColor"))
v.FgColor = theme.OptionsColor
}
if gui.getCommitMessageView() == nil {
// doesn't matter where this view starts because it will be hidden
if commitMessageView, err := g.SetView("commitMessage", width, height, width*2, height*2, 0); err != nil {
if commitMessageView, err := g.SetView("commitMessage", hiddenViewOffset, hiddenViewOffset, hiddenViewOffset+10, hiddenViewOffset+10, 0); err != nil {
if err.Error() != "unknown view" {
return err
}
@@ -553,12 +697,13 @@ func (gui *Gui) layout(g *gocui.Gui) error {
commitMessageView.Title = gui.Tr.SLocalize("CommitMessage")
commitMessageView.FgColor = textColor
commitMessageView.Editable = true
commitMessageView.Editor = gocui.EditorFunc(gui.commitMessageEditor)
}
}
if check, _ := g.View("credentials"); check == nil {
// doesn't matter where this view starts because it will be hidden
if credentialsView, err := g.SetView("credentials", width, height, width*2, height*2, 0); err != nil {
if credentialsView, err := g.SetView("credentials", hiddenViewOffset, hiddenViewOffset, hiddenViewOffset+10, hiddenViewOffset+10, 0); err != nil {
if err.Error() != "unknown view" {
return err
}
@@ -572,6 +717,35 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
}
searchViewOffset := hiddenViewOffset
if gui.State.Searching.isSearching {
searchViewOffset = 0
}
// this view takes up one character. Its only purpose is to show the slash when searching
searchPrefix := "search: "
if searchPrefixView, err := g.SetView("searchPrefix", appStatusOptionsBoundary-1+searchViewOffset, height-2+searchViewOffset, len(searchPrefix)+searchViewOffset, height+searchViewOffset, 0); err != nil {
if err.Error() != "unknown view" {
return err
}
searchPrefixView.BgColor = gocui.ColorDefault
searchPrefixView.FgColor = gocui.ColorGreen
searchPrefixView.Frame = false
gui.setViewContent(gui.g, searchPrefixView, searchPrefix)
}
if searchView, err := g.SetView("search", appStatusOptionsBoundary-1+searchViewOffset+len(searchPrefix), height-2+searchViewOffset, optionsVersionBoundary+searchViewOffset, height+searchViewOffset, 0); err != nil {
if err.Error() != "unknown view" {
return err
}
searchView.BgColor = gocui.ColorDefault
searchView.FgColor = gocui.ColorGreen
searchView.Frame = false
searchView.Editable = true
}
if appStatusView, err := g.SetView("appStatus", -1, height-2, width, height, 0); err != nil {
if err.Error() != "unknown view" {
return err
@@ -623,7 +797,8 @@ func (gui *Gui) layout(g *gocui.Gui) error {
{view: branchesView, context: "local-branches", selectedLine: gui.State.Panels.Branches.SelectedLine, lineCount: len(gui.State.Branches)},
{view: branchesView, context: "remotes", selectedLine: gui.State.Panels.Remotes.SelectedLine, lineCount: len(gui.State.Remotes)},
{view: branchesView, context: "remote-branches", selectedLine: gui.State.Panels.RemoteBranches.SelectedLine, lineCount: len(gui.State.Remotes)},
{view: commitsView, context: "", selectedLine: gui.State.Panels.Commits.SelectedLine, lineCount: len(gui.State.Commits)},
{view: commitsView, context: "branch-commits", selectedLine: gui.State.Panels.Commits.SelectedLine, lineCount: len(gui.State.Commits)},
{view: commitsView, context: "reflog-commits", selectedLine: gui.State.Panels.ReflogCommits.SelectedLine, lineCount: len(gui.State.ReflogCommits)},
{view: stashView, context: "", selectedLine: gui.State.Panels.Stash.SelectedLine, lineCount: len(gui.State.StashEntries)},
}
@@ -642,6 +817,15 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
}
mainViewWidth, mainViewHeight := gui.getMainView().Size()
if mainViewWidth != gui.State.PrevMainWidth || mainViewHeight != gui.State.PrevMainHeight {
gui.State.PrevMainWidth = mainViewWidth
gui.State.PrevMainHeight = mainViewHeight
if err := gui.onResize(); err != nil {
return err
}
}
// here is a good place log some stuff
// if you download humanlog and do tail -f development.log | humanlog
// this will let you see these branches as prettified json
@@ -650,11 +834,10 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
func (gui *Gui) onInitialViewsCreation() error {
if err := gui.changeMainViewsContext("normal"); err != nil {
return err
}
gui.changeMainViewsContext("normal")
gui.getBranchesView().Context = "local-branches"
gui.getCommitsView().Context = "branch-commits"
return gui.loadNewRepo()
}
@@ -730,28 +913,27 @@ func (gui *Gui) fetch(g *gocui.Gui, v *gocui.View, canAskForCredentials bool) (u
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"),
"1-5": gui.Tr.SLocalize("jump"),
fmt.Sprintf("%s/%s", gui.getKeyDisplay("universal.scrollUpMain"), gui.getKeyDisplay("universal.scrollDownMain")): gui.Tr.SLocalize("scroll"),
fmt.Sprintf("%s %s %s %s", gui.getKeyDisplay("universal.prevBlock"), gui.getKeyDisplay("universal.nextBlock"), gui.getKeyDisplay("universal.prevItem"), gui.getKeyDisplay("universal.nextItem")): gui.Tr.SLocalize("navigate"),
fmt.Sprintf("%s/%s", gui.getKeyDisplay("universal.return"), gui.getKeyDisplay("universal.quit")): gui.Tr.SLocalize("close"),
fmt.Sprintf("%s", gui.getKeyDisplay("universal.optionMenu")): gui.Tr.SLocalize("menu"),
"1-5": gui.Tr.SLocalize("jump"),
})
}
func (gui *Gui) goEvery(interval time.Duration, function func() error) {
func (gui *Gui) goEvery(interval time.Duration, stop chan struct{}, function func() error) {
go func() {
for range time.Tick(interval) {
_ = function()
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
_ = function()
case <-stop:
return
}
}
}()
}
@@ -766,7 +948,7 @@ func (gui *Gui) startBackgroundFetch() {
if err != nil && strings.Contains(err.Error(), "exit status 128") && isNew {
_ = gui.createConfirmationPanel(gui.g, gui.g.CurrentView(), true, gui.Tr.SLocalize("NoAutomaticGitFetchTitle"), gui.Tr.SLocalize("NoAutomaticGitFetchBody"), nil, nil)
} else {
gui.goEvery(time.Second*60, func() error {
gui.goEvery(time.Second*60, gui.stopChan, func() error {
_, err := gui.fetch(gui.g, gui.g.CurrentView(), false)
return err
})
@@ -775,12 +957,21 @@ func (gui *Gui) startBackgroundFetch() {
// Run setup the gui with keybindings and start the mainloop
func (gui *Gui) Run() error {
g, err := gocui.NewGui(gocui.OutputNormal, OverlappingEdges)
g, err := gocui.NewGui(gocui.Output256, OverlappingEdges)
if err != nil {
return err
}
defer g.Close()
g.OnSearchEscape = gui.onSearchEscape
g.SearchEscapeKey = gui.getKey("universal.return")
g.NextSearchMatchKey = gui.getKey("universal.nextMatch")
g.PrevSearchMatchKey = gui.getKey("universal.prevMatch")
gui.stopChan = make(chan struct{})
g.ASCII = runtime.GOOS == "windows" && runewidth.IsEastAsian()
if gui.Config.GetUserConfig().GetBool("gui.mouseEvents") {
g.Mouse = true
}
@@ -807,8 +998,7 @@ func (gui *Gui) Run() error {
go gui.startBackgroundFetch()
}
gui.goEvery(time.Second*10, gui.refreshFiles)
gui.goEvery(time.Millisecond*50, gui.renderAppStatus)
gui.goEvery(time.Second*10, gui.stopChan, gui.refreshFiles)
g.SetManager(gocui.ManagerFunc(gui.layout), gocui.ManagerFunc(gui.getFocusLayout()))
@@ -828,6 +1018,17 @@ func (gui *Gui) Run() error {
func (gui *Gui) RunWithSubprocesses() error {
for {
if err := gui.Run(); err != nil {
for _, manager := range gui.viewBufferManagerMap {
manager.Close()
}
gui.viewBufferManagerMap = map[string]*tasks.ViewBufferManager{}
if !gui.fileWatcher.Disabled {
gui.fileWatcher.Watcher.Close()
}
close(gui.stopChan)
if err == gocui.ErrQuit {
if !gui.State.RetainOriginalDir {
if err := gui.recordCurrentDirectory(); err != nil {
@@ -835,8 +1036,6 @@ func (gui *Gui) RunWithSubprocesses() error {
}
}
gui.fileWatcher.Close()
break
} else if err == gui.Errors.ErrSwitchRepo {
continue
+489 -188
View File
File diff suppressed because it is too large Load Diff
+10 -1
View File
@@ -220,6 +220,10 @@ func (gui *Gui) handleMouseScrollDown(g *gocui.Gui, v *gocui.View) error {
return gui.handleCycleLine(1)
}
func (gui *Gui) getSelectedCommitFileName() string {
return gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLine].Name
}
func (gui *Gui) refreshMainView() error {
state := gui.State.Panels.LineByLine
@@ -227,7 +231,7 @@ func (gui *Gui) refreshMainView() error {
// I'd prefer not to have knowledge of contexts using this file but I'm not sure
// how to get around this
if gui.State.MainContext == "patch-building" {
filename := gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLine].Name
filename := gui.getSelectedCommitFileName()
includedLineIndices = gui.GitCommand.PatchManager.GetFileIncLineIndices(filename)
}
colorDiff := state.PatchParser.Render(state.FirstLineIdx, state.LastLineIdx, includedLineIndices)
@@ -314,3 +318,8 @@ func (gui *Gui) handleToggleSelectHunk(g *gocui.Gui, v *gocui.View) error {
return gui.focusSelection(state.SelectMode == HUNK)
}
func (gui *Gui) handleEscapeLineByLinePanel() {
gui.changeMainViewsContext("normal")
gui.State.Panels.LineByLine = nil
}
+23 -3
View File
@@ -34,6 +34,7 @@ func (lv *listView) handleLineChange(change int) error {
return err
}
}
view, err := lv.gui.g.View(lv.viewName)
if err != nil {
return err
@@ -56,7 +57,14 @@ func (lv *listView) handleClick(g *gocui.Gui, v *gocui.View) error {
*selectedLineIdxPtr = newSelectedLineIdx
if prevSelectedLineIdx == newSelectedLineIdx && lv.gui.currentViewName() == lv.viewName && lv.handleClickSelectedItem != nil {
if lv.rendersToMainView {
if err := lv.gui.resetOrigin(lv.gui.getMainView()); err != nil {
return err
}
}
prevViewName := lv.gui.currentViewName()
if prevSelectedLineIdx == newSelectedLineIdx && prevViewName == lv.viewName && lv.handleClickSelectedItem != nil {
return lv.handleClickSelectedItem(lv.gui.g, v)
}
return lv.handleItemSelect(lv.gui.g, v)
@@ -79,8 +87,8 @@ func (gui *Gui) getListViews() []*listView {
viewName: "files",
getItemsLength: func() int { return len(gui.State.Files) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Files.SelectedLine },
handleFocus: gui.wrappedHandler(func() error { return gui.selectFile(true) }),
handleItemSelect: gui.wrappedHandler(func() error { return gui.selectFile(true) }),
handleFocus: gui.focusAndSelectFile,
handleItemSelect: gui.focusAndSelectFile,
handleClickSelectedItem: gui.handleFilePress,
gui: gui,
rendersToMainView: true,
@@ -126,8 +134,10 @@ func (gui *Gui) getListViews() []*listView {
gui: gui,
rendersToMainView: true,
},
{
viewName: "commits",
context: "branch-commits",
getItemsLength: func() int { return len(gui.State.Commits) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Commits.SelectedLine },
handleFocus: gui.handleCommitSelect,
@@ -136,6 +146,16 @@ func (gui *Gui) getListViews() []*listView {
gui: gui,
rendersToMainView: true,
},
{
viewName: "commits",
context: "reflog-commits",
getItemsLength: func() int { return len(gui.State.ReflogCommits) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.ReflogCommits.SelectedLine },
handleFocus: gui.handleReflogCommitSelect,
handleItemSelect: gui.handleReflogCommitSelect,
gui: gui,
rendersToMainView: true,
},
{
viewName: "stash",
getItemsLength: func() int { return len(gui.State.StashEntries) },
+38 -10
View File
@@ -8,6 +8,12 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
type menuItem struct {
displayString string
displayStrings []string
onPress func() error
}
// list panel functions
func (gui *Gui) handleMenuSelect(g *gocui.Gui, v *gocui.View) error {
@@ -18,9 +24,9 @@ func (gui *Gui) handleMenuSelect(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) renderMenuOptions() error {
optionsMap := map[string]string{
"esc/q": gui.Tr.SLocalize("close"),
"↑ ↓": gui.Tr.SLocalize("navigate"),
"space": gui.Tr.SLocalize("execute"),
fmt.Sprintf("%s/%s", gui.getKeyDisplay("universal.return"), gui.getKeyDisplay("universal.quit")): gui.Tr.SLocalize("close"),
fmt.Sprintf("%s %s", gui.getKeyDisplay("universal.prevItem"), gui.getKeyDisplay("universal.nextItem")): gui.Tr.SLocalize("navigate"),
gui.getKeyDisplay("universal.select"): gui.Tr.SLocalize("execute"),
}
return gui.renderOptionsMap(optionsMap)
}
@@ -38,27 +44,49 @@ func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error {
return gui.returnFocus(g, v)
}
func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handlePress func(int) error) error {
isFocused := gui.g.CurrentView().Name() == "menu"
gui.State.MenuItemCount = itemCount
list, err := utils.RenderList(items, isFocused)
if err != nil {
return err
type createMenuOptions struct {
showCancel bool
}
func (gui *Gui) createMenu(title string, items []*menuItem, createMenuOptions createMenuOptions) error {
if createMenuOptions.showCancel {
// this is mutative but I'm okay with that for now
items = append(items, &menuItem{
displayStrings: []string{gui.Tr.SLocalize("cancel")},
onPress: func() error {
return nil
},
})
}
gui.State.MenuItemCount = len(items)
stringArrays := make([][]string, len(items))
for i, item := range items {
if item.displayStrings == nil {
stringArrays[i] = []string{item.displayString}
} else {
stringArrays[i] = item.displayStrings
}
}
list := utils.RenderDisplayStrings(stringArrays)
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 = theme.GocuiDefaultTextColor
menuView.ContainsList = true
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 {
if err := items[selectedLine].onPress(); err != nil {
return err
}
if _, err := gui.g.View("menu"); err == nil {
if _, err := gui.g.SetViewOnBottom("menu"); err != nil {
return err
+13 -11
View File
@@ -5,6 +5,7 @@ package gui
import (
"bufio"
"bytes"
"fmt"
"io/ioutil"
"math"
"os"
@@ -211,15 +212,16 @@ func (gui *Gui) refreshMergePanel() error {
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
if err := gui.setViewContent(gui.g, mainView, content); err != nil {
return err
}
gui.Log.Warn("scrolling to conflict")
if err := gui.scrollToConflict(gui.g); err != nil {
return err
}
return nil
}
@@ -243,11 +245,11 @@ func (gui *Gui) scrollToConflict(g *gocui.Gui) error {
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"),
fmt.Sprintf("%s %s", gui.getKeyDisplay("universal.prevItem"), gui.getKeyDisplay("universal.nextItem")): gui.Tr.SLocalize("selectHunk"),
fmt.Sprintf("%s %s", gui.getKeyDisplay("universal.prevBlock"), gui.getKeyDisplay("universal.nextBlock")): gui.Tr.SLocalize("navigateConflicts"),
gui.getKeyDisplay("universal.select"): gui.Tr.SLocalize("pickHunk"),
gui.getKeyDisplay("main.pickBothHunks"): gui.Tr.SLocalize("pickBothHunks"),
gui.getKeyDisplay("main.undo"): gui.Tr.SLocalize("undo"),
})
}
+17 -15
View File
@@ -3,8 +3,6 @@ package gui
import (
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -17,7 +15,7 @@ func (gui *Gui) getBindings(v *gocui.View) []*Binding {
bindings := gui.GetInitialKeybindings()
for _, binding := range bindings {
if binding.GetKey() != "" && binding.Description != "" {
if GetKeyDisplay(binding.Key) != "" && binding.Description != "" {
switch binding.ViewName {
case "":
bindingsGlobal = append(bindingsGlobal, binding)
@@ -38,19 +36,23 @@ func (gui *Gui) getBindings(v *gocui.View) []*Binding {
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
menuItems := make([]*menuItem, len(bindings))
for i, binding := range bindings {
innerBinding := binding // note to self, never close over loop variables
menuItems[i] = &menuItem{
displayStrings: []string{GetKeyDisplay(innerBinding.Key), innerBinding.Description},
onPress: func() error {
if innerBinding.Key == nil {
return nil
}
if err := gui.handleMenuClose(g, v); err != nil {
return err
}
return innerBinding.Handler(g, v)
},
}
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, len(bindings), handleMenuPress)
return gui.createMenu(strings.Title(gui.Tr.SLocalize("menu")), menuItems, createMenuOptions{})
}
+12 -26
View File
@@ -2,6 +2,7 @@ package gui
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int) error {
@@ -42,38 +43,24 @@ func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int) error {
return nil
}
func (gui *Gui) handleAddSelectionToPatch(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleToggleSelectionForPatch(g *gocui.Gui, v *gocui.View) error {
state := gui.State.Panels.LineByLine
toggleFunc := gui.GitCommand.PatchManager.AddFileLineRange
filename := gui.getSelectedCommitFileName()
includedLineIndices := gui.GitCommand.PatchManager.GetFileIncLineIndices(filename)
currentLineIsStaged := utils.IncludesInt(includedLineIndices, state.SelectedLineIdx)
if currentLineIsStaged {
toggleFunc = gui.GitCommand.PatchManager.RemoveFileLineRange
}
// add range of lines to those set for the file
commitFile := gui.getSelectedCommitFile(gui.g)
if commitFile == nil {
return gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
}
gui.GitCommand.PatchManager.AddFileLineRange(commitFile.Name, state.FirstLineIdx, state.LastLineIdx)
if err := gui.refreshCommitFilesView(); err != nil {
return err
}
if err := gui.refreshPatchBuildingPanel(-1); err != nil {
return err
}
return nil
}
func (gui *Gui) handleRemoveSelectionFromPatch(g *gocui.Gui, v *gocui.View) error {
state := gui.State.Panels.LineByLine
// add range of lines to those set for the file
commitFile := gui.getSelectedCommitFile(gui.g)
if commitFile == nil {
return gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
}
gui.GitCommand.PatchManager.RemoveFileLineRange(commitFile.Name, state.FirstLineIdx, state.LastLineIdx)
toggleFunc(commitFile.Name, state.FirstLineIdx, state.LastLineIdx)
if err := gui.refreshCommitFilesView(); err != nil {
return err
@@ -87,8 +74,7 @@ func (gui *Gui) handleRemoveSelectionFromPatch(g *gocui.Gui, v *gocui.View) erro
}
func (gui *Gui) handleEscapePatchBuildingPanel(g *gocui.Gui, v *gocui.View) error {
gui.State.Panels.LineByLine = nil
gui.changeMainViewsContext("normal")
gui.handleEscapeLineByLinePanel()
if gui.GitCommand.PatchManager.IsEmpty() {
gui.GitCommand.PatchManager.Reset()
+20 -25
View File
@@ -6,48 +6,43 @@ import (
"github.com/jesseduffield/gocui"
)
type patchMenuOption struct {
displayName string
function func() error
}
// GetDisplayStrings is a function.
func (o *patchMenuOption) GetDisplayStrings(isFocused bool) []string {
return []string{o.displayName}
}
func (gui *Gui) handleCreatePatchOptionsMenu(g *gocui.Gui, v *gocui.View) error {
if !gui.GitCommand.PatchManager.CommitSelected() {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NoPatchError"))
}
options := []*patchMenuOption{
{displayName: fmt.Sprintf("remove patch from original commit (%s)", gui.GitCommand.PatchManager.CommitSha), function: gui.handleDeletePatchFromCommit},
{displayName: "pull patch out into index", function: gui.handlePullPatchIntoWorkingTree},
{displayName: "reset patch", function: gui.handleResetPatch},
menuItems := []*menuItem{
{
displayString: fmt.Sprintf("remove patch from original commit (%s)", gui.GitCommand.PatchManager.CommitSha),
onPress: gui.handleDeletePatchFromCommit,
},
{
displayString: "pull patch out into index",
onPress: gui.handlePullPatchIntoWorkingTree,
},
{
displayString: "reset patch",
onPress: gui.handleResetPatch,
},
}
selectedCommit := gui.getSelectedCommit(gui.g)
if selectedCommit != nil && gui.GitCommand.PatchManager.CommitSha != selectedCommit.Sha {
// adding this option to index 1
options = append(
options[:1],
menuItems = append(
menuItems[:1],
append(
[]*patchMenuOption{
[]*menuItem{
{
displayName: fmt.Sprintf("move patch to selected commit (%s)", selectedCommit.Sha),
function: gui.handleMovePatchToSelectedCommit,
displayString: fmt.Sprintf("move patch to selected commit (%s)", selectedCommit.Sha),
onPress: gui.handleMovePatchToSelectedCommit,
},
}, options[1:]...,
}, menuItems[1:]...,
)...,
)
}
handleMenuPress := func(index int) error {
return options[index].function()
}
return gui.createMenu(gui.Tr.SLocalize("PatchOptionsTitle"), options, len(options), handleMenuPress)
return gui.createMenu(gui.Tr.SLocalize("PatchOptionsTitle"), menuItems, createMenuOptions{showCancel: true})
}
func (gui *Gui) getPatchCommitIndex() int {
+48
View File
@@ -0,0 +1,48 @@
package presentation
import (
"fmt"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func GetBranchListDisplayStrings(branches []*commands.Branch, isFocused bool, selectedLine int) [][]string {
lines := make([][]string, len(branches))
for i := range branches {
showUpstreamDifferences := isFocused && i == selectedLine
lines[i] = getBranchDisplayStrings(branches[i], showUpstreamDifferences)
}
return lines
}
// getBranchDisplayStrings returns the display string of branch
func getBranchDisplayStrings(b *commands.Branch, showUpstreamDifferences bool) []string {
displayName := utils.ColoredString(b.Name, GetBranchColor(b.Name))
if showUpstreamDifferences && b.Pushables != "" && b.Pullables != "" {
displayName = fmt.Sprintf("%s ↑%s↓%s", displayName, b.Pushables, b.Pullables)
}
return []string{b.Recency, displayName}
}
// GetBranchColor branch color
func GetBranchColor(name string) color.Attribute {
branchType := strings.Split(name, "/")[0]
switch branchType {
case "feature":
return color.FgGreen
case "bugfix":
return color.FgYellow
case "hotfix":
return color.FgRed
default:
return theme.DefaultTextColor
}
}
+35
View File
@@ -0,0 +1,35 @@
package presentation
import (
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/theme"
)
func GetCommitFileListDisplayStrings(branches []*commands.CommitFile) [][]string {
lines := make([][]string, len(branches))
for i := range branches {
lines[i] = getCommitFileDisplayStrings(branches[i])
}
return lines
}
// getCommitFileDisplayStrings returns the display string of branch
func getCommitFileDisplayStrings(f *commands.CommitFile) []string {
yellow := color.New(color.FgYellow)
green := color.New(color.FgGreen)
defaultColor := color.New(theme.DefaultTextColor)
var colour *color.Color
switch f.Status {
case commands.UNSELECTED:
colour = defaultColor
case commands.WHOLE:
colour = green
case commands.PART:
colour = yellow
}
return []string{colour.Sprint(f.DisplayString)}
}
+126
View File
@@ -0,0 +1,126 @@
package presentation
import (
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func GetCommitListDisplayStrings(commits []*commands.Commit, fullDescription bool) [][]string {
lines := make([][]string, len(commits))
var displayFunc func(*commands.Commit) []string
if fullDescription {
displayFunc = getFullDescriptionDisplayStringsForCommit
} else {
displayFunc = getDisplayStringsForCommit
}
for i := range commits {
lines[i] = displayFunc(commits[i])
}
return lines
}
func getFullDescriptionDisplayStringsForCommit(c *commands.Commit) []string {
red := color.New(color.FgRed)
yellow := color.New(color.FgYellow)
green := color.New(color.FgGreen)
blue := color.New(color.FgBlue)
cyan := color.New(color.FgCyan)
defaultColor := color.New(theme.DefaultTextColor)
magenta := color.New(color.FgMagenta)
// 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
case "reflog":
shaColor = blue
case "selected":
shaColor = magenta
default:
shaColor = defaultColor
}
if c.Copied {
shaColor = copied
}
tagString := ""
truncatedDate := utils.TruncateWithEllipsis(c.Date, 15)
secondColumnString := blue.Sprint(truncatedDate)
if c.Action != "" {
secondColumnString = cyan.Sprint(c.Action)
} else if c.ExtraInfo != "" {
tagColor := color.New(color.FgMagenta, color.Bold)
tagString = utils.ColoredStringDirect(c.ExtraInfo, tagColor) + " "
}
truncatedAuthor := utils.TruncateWithEllipsis(c.Author, 17)
return []string{shaColor.Sprint(c.Sha[:8]), secondColumnString, yellow.Sprint(truncatedAuthor), tagString + defaultColor.Sprint(c.Name)}
}
func getDisplayStringsForCommit(c *commands.Commit) []string {
red := color.New(color.FgRed)
yellow := color.New(color.FgYellow)
green := color.New(color.FgGreen)
blue := color.New(color.FgBlue)
cyan := color.New(color.FgCyan)
defaultColor := color.New(theme.DefaultTextColor)
magenta := color.New(color.FgMagenta)
// 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
case "reflog":
shaColor = blue
case "selected":
shaColor = magenta
default:
shaColor = defaultColor
}
if c.Copied {
shaColor = copied
}
actionString := ""
tagString := ""
if c.Action != "" {
actionString = cyan.Sprint(utils.WithPadding(c.Action, 7)) + " "
} else if len(c.Tags) > 0 {
tagColor := color.New(color.FgMagenta, color.Bold)
tagString = utils.ColoredStringDirect(strings.Join(c.Tags, " "), tagColor) + " "
}
return []string{shaColor.Sprint(c.Sha[:8]), actionString + tagString + defaultColor.Sprint(c.Name)}
}
+36
View File
@@ -0,0 +1,36 @@
package presentation
import (
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/commands"
)
func GetFileListDisplayStrings(files []*commands.File) [][]string {
lines := make([][]string, len(files))
for i := range files {
lines[i] = getFileDisplayStrings(files[i])
}
return lines
}
// getFileDisplayStrings returns the display string of branch
func getFileDisplayStrings(f *commands.File) []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}
}
+23
View File
@@ -0,0 +1,23 @@
package presentation
import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func GetRemoteBranchListDisplayStrings(branches []*commands.RemoteBranch) [][]string {
lines := make([][]string, len(branches))
for i := range branches {
lines[i] = getRemoteBranchDisplayStrings(branches[i])
}
return lines
}
// getRemoteBranchDisplayStrings returns the display string of branch
func getRemoteBranchDisplayStrings(b *commands.RemoteBranch) []string {
displayName := utils.ColoredString(b.Name, GetBranchColor(b.Name))
return []string{displayName}
}
+20
View File
@@ -0,0 +1,20 @@
package presentation
import (
"github.com/jesseduffield/lazygit/pkg/commands"
)
func GetRemoteListDisplayStrings(remotes []*commands.Remote) [][]string {
lines := make([][]string, len(remotes))
for i := range remotes {
lines[i] = getRemoteDisplayStrings(remotes[i])
}
return lines
}
// getRemoteDisplayStrings returns the display string of branch
func getRemoteDisplayStrings(r *commands.Remote) []string {
return []string{r.Name}
}
+20
View File
@@ -0,0 +1,20 @@
package presentation
import (
"github.com/jesseduffield/lazygit/pkg/commands"
)
func GetStashEntryListDisplayStrings(stashEntries []*commands.StashEntry) [][]string {
lines := make([][]string, len(stashEntries))
for i := range stashEntries {
lines[i] = getStashEntryDisplayStrings(stashEntries[i])
}
return lines
}
// getStashEntryDisplayStrings returns the display string of branch
func getStashEntryDisplayStrings(s *commands.StashEntry) []string {
return []string{s.DisplayString}
}
+20
View File
@@ -0,0 +1,20 @@
package presentation
import (
"github.com/jesseduffield/lazygit/pkg/commands"
)
func GetTagListDisplayStrings(tags []*commands.Tag) [][]string {
lines := make([][]string, len(tags))
for i := range tags {
lines[i] = getTagDisplayStrings(tags[i])
}
return lines
}
// getTagDisplayStrings returns the display string of branch
func getTagDisplayStrings(t *commands.Tag) []string {
return []string{t.Name}
}
+74
View File
@@ -0,0 +1,74 @@
// +build !windows
package gui
import (
"os/exec"
"github.com/jesseduffield/pty"
)
func (gui *Gui) onResize() error {
if gui.State.Ptmx == nil {
return nil
}
mainView := gui.getMainView()
width, height := mainView.Size()
if err := pty.Setsize(gui.State.Ptmx, &pty.Winsize{Cols: uint16(width), Rows: uint16(height)}); err != nil {
return err
}
// TODO: handle resizing properly
return nil
}
// Some commands need to output for a terminal to active certain behaviour.
// For example, git won't invoke the GIT_PAGER env var unless it thinks it's
// talking to a terminal. We typically write cmd outputs straight to a view,
// which is just an io.Reader. the pty package lets us wrap a command in a
// pseudo-terminal meaning we'll get the behaviour we want from the underlying
// command.
func (gui *Gui) newPtyTask(viewName string, cmd *exec.Cmd) error {
width, _ := gui.getMainView().Size()
pager := gui.GitCommand.GetPager(width)
if pager == "" {
// if we're not using a custom pager we don't need to use a pty
return gui.newCmdTask(viewName, cmd)
}
cmd.Env = append(cmd.Env, "GIT_PAGER="+pager)
view, err := gui.g.View(viewName)
if err != nil {
return nil // swallowing for now
}
_, height := view.Size()
_, oy := view.Origin()
manager := gui.getManager(view)
ptmx, err := pty.Start(cmd)
if err != nil {
return err
}
gui.State.Ptmx = ptmx
onClose := func() {
ptmx.Close()
gui.State.Ptmx = nil
}
if err := gui.onResize(); err != nil {
return err
}
if err := manager.NewTask(manager.NewCmdTask(ptmx, cmd, height+oy+10, onClose)); err != nil {
return err
}
return nil
}
+13
View File
@@ -0,0 +1,13 @@
// +build windows
package gui
import "os/exec"
func (gui *Gui) onResize() error {
return nil
}
func (gui *Gui) newPtyTask(viewName string, cmd *exec.Cmd) error {
return gui.newCmdTask(viewName, cmd)
}
+12 -22
View File
@@ -7,33 +7,23 @@ import (
"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"},
}
options := []string{"continue", "abort"}
if gui.State.WorkingTreeState == "rebasing" {
options = append(options, &option{value: "skip"})
options = append(options, "skip")
}
options = append(options, &option{value: "cancel"})
handleMenuPress := func(index int) error {
command := options[index].value
if command == "cancel" {
return nil
menuItems := make([]*menuItem, len(options))
for i, option := range options {
// note to self. Never, EVER, close over loop variables in a function
innerOption := option
menuItems[i] = &menuItem{
displayString: innerOption,
onPress: func() error {
return gui.genericMergeCommand(innerOption)
},
}
return gui.genericMergeCommand(command)
}
var title string
@@ -43,7 +33,7 @@ func (gui *Gui) handleCreateRebaseOptionsMenu(g *gocui.Gui, v *gocui.View) error
title = gui.Tr.SLocalize("RebaseOptionsTitle")
}
return gui.createMenu(title, options, len(options), handleMenuPress)
return gui.createMenu(title, menuItems, createMenuOptions{showCancel: true})
}
func (gui *Gui) genericMergeCommand(command string) error {
+21 -28
View File
@@ -10,41 +10,34 @@ import (
"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)
yellow := color.New(color.FgMagenta)
// we won't show the current repo hence the -1
recentRepos := make([]*recentRepo, reposCount-1)
menuItems := make([]*menuItem, reposCount-1)
for i, path := range recentRepoPaths[1:reposCount] {
recentRepos[i] = &recentRepo{path: path}
innerPath := path
menuItems[i] = &menuItem{
displayStrings: []string{
filepath.Base(innerPath),
yellow.Sprint(innerPath),
},
onPress: func() error {
if err := os.Chdir(innerPath); 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
},
}
}
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, len(recentRepos), handleMenuPress)
return gui.createMenu(gui.Tr.SLocalize("RecentRepos"), menuItems, createMenuOptions{showCancel: true})
}
// updateRecentRepoList registers the fact that we opened lazygit in this repo,
+103
View File
@@ -0,0 +1,103 @@
package gui
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
)
// list panel functions
func (gui *Gui) getSelectedReflogCommit() *commands.Commit {
selectedLine := gui.State.Panels.ReflogCommits.SelectedLine
if selectedLine == -1 || len(gui.State.ReflogCommits) == 0 {
return nil
}
return gui.State.ReflogCommits[selectedLine]
}
func (gui *Gui) handleReflogCommitSelect(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
gui.State.SplitMainPanel = false
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
gui.getMainView().Title = "Reflog Entry"
commit := gui.getSelectedReflogCommit()
if commit == nil {
return gui.newStringTask("main", "No reflog history")
}
if err := gui.focusPoint(0, gui.State.Panels.ReflogCommits.SelectedLine, len(gui.State.ReflogCommits), v); err != nil {
return err
}
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowCmdStr(commit.Sha),
)
if err := gui.newPtyTask("main", cmd); err != nil {
gui.Log.Error(err)
}
return nil
}
func (gui *Gui) refreshReflogCommits() error {
commits, err := gui.GitCommand.GetReflogCommits()
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
gui.State.ReflogCommits = commits
if gui.getCommitsView().Context == "reflog-commits" {
return gui.renderReflogCommitsWithSelection()
}
return nil
}
func (gui *Gui) renderReflogCommitsWithSelection() error {
commitsView := gui.getCommitsView()
gui.refreshSelectedLine(&gui.State.Panels.ReflogCommits.SelectedLine, len(gui.State.ReflogCommits))
displayStrings := presentation.GetCommitListDisplayStrings(gui.State.ReflogCommits, gui.State.ScreenMode != SCREEN_NORMAL)
gui.renderDisplayStrings(commitsView, displayStrings)
if gui.g.CurrentView() == commitsView && commitsView.Context == "reflog-commits" {
if err := gui.handleReflogCommitSelect(gui.g, commitsView); err != nil {
return err
}
}
return nil
}
func (gui *Gui) handleCheckoutReflogCommit(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedReflogCommit()
if commit == nil {
return nil
}
err := gui.createConfirmationPanel(g, gui.getCommitsView(), true, gui.Tr.SLocalize("checkoutCommit"), gui.Tr.SLocalize("SureCheckoutThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
return gui.handleCheckoutRef(commit.Sha)
}, nil)
if err != nil {
return err
}
gui.State.Panels.ReflogCommits.SelectedLine = 0
return nil
}
func (gui *Gui) handleCreateReflogResetMenu(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedReflogCommit()
return gui.createResetMenu(commit.Sha)
}
+26 -17
View File
@@ -2,12 +2,10 @@ package gui
import (
"fmt"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
)
// list panel functions
@@ -37,7 +35,7 @@ func (gui *Gui) handleRemoteBranchSelect(g *gocui.Gui, v *gocui.View) error {
remote := gui.getSelectedRemote()
remoteBranch := gui.getSelectedRemoteBranch()
if remoteBranch == nil {
return gui.renderString(g, "main", "No branches for this remote")
return gui.newStringTask("main", "No branches for this remote")
}
gui.focusPoint(0, gui.State.Panels.Menu.SelectedLine, gui.State.MenuItemCount, v)
@@ -45,13 +43,14 @@ func (gui *Gui) handleRemoteBranchSelect(g *gocui.Gui, v *gocui.View) error {
return err
}
go func() {
graph, err := gui.GitCommand.GetBranchGraph(fmt.Sprintf("%s/%s", remote.Name, remoteBranch.Name))
if err != nil && strings.HasPrefix(graph, "fatal: ambiguous argument") {
graph = gui.Tr.SLocalize("NoTrackingThisBranch")
}
_ = gui.renderString(g, "main", fmt.Sprintf("%s/%s\n\n%s", utils.ColoredString(remote.Name, color.FgRed), utils.ColoredString(remoteBranch.Name, color.FgGreen), graph))
}()
branchName := fmt.Sprintf("%s/%s", remote.Name, remoteBranch.Name)
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.GetBranchGraphCmdStr(branchName),
)
if err := gui.newCmdTask("main", cmd); err != nil {
gui.Log.Error(err)
}
return nil
}
@@ -64,11 +63,12 @@ func (gui *Gui) renderRemoteBranchesWithSelection() error {
branchesView := gui.getBranchesView()
gui.refreshSelectedLine(&gui.State.Panels.RemoteBranches.SelectedLine, len(gui.State.RemoteBranches))
if err := gui.renderListPanel(branchesView, gui.State.RemoteBranches); err != nil {
return err
}
if err := gui.handleRemoteBranchSelect(gui.g, branchesView); err != nil {
return err
displayStrings := presentation.GetRemoteBranchListDisplayStrings(gui.State.RemoteBranches)
gui.renderDisplayStrings(branchesView, displayStrings)
if gui.g.CurrentView() == branchesView && branchesView.Context == "remote-branches" {
if err := gui.handleRemoteBranchSelect(gui.g, branchesView); err != nil {
return err
}
}
return nil
@@ -79,7 +79,7 @@ func (gui *Gui) handleCheckoutRemoteBranch(g *gocui.Gui, v *gocui.View) error {
if remoteBranch == nil {
return nil
}
if err := gui.handleCheckoutBranch(remoteBranch.RemoteName + "/" + remoteBranch.Name); err != nil {
if err := gui.handleCheckoutRef(remoteBranch.RemoteName + "/" + remoteBranch.Name); err != nil {
return err
}
return gui.switchBranchesPanelContext("local-branches")
@@ -132,3 +132,12 @@ func (gui *Gui) handleSetBranchUpstream(g *gocui.Gui, v *gocui.View) error {
return gui.refreshSidePanels(gui.g)
}, nil)
}
func (gui *Gui) handleCreateResetToRemoteBranchMenu(g *gocui.Gui, v *gocui.View) error {
selectedBranch := gui.getSelectedRemoteBranch()
if selectedBranch == nil {
return nil
}
return gui.createResetMenu(fmt.Sprintf("%s/%s", selectedBranch.RemoteName, selectedBranch.Name))
}
+26 -7
View File
@@ -7,6 +7,7 @@ import (
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -36,13 +37,13 @@ func (gui *Gui) handleRemoteSelect(g *gocui.Gui, v *gocui.View) error {
remote := gui.getSelectedRemote()
if remote == nil {
return gui.renderString(g, "main", "No remotes")
return gui.newStringTask("main", "No remotes")
}
if err := gui.focusPoint(0, gui.State.Panels.Remotes.SelectedLine, len(gui.State.Remotes), v); err != nil {
return err
}
return gui.renderString(g, "main", fmt.Sprintf("%s\nUrls:\n%s", utils.ColoredString(remote.Name, color.FgGreen), strings.Join(remote.Urls, "\n")))
return gui.newStringTask("main", fmt.Sprintf("%s\nUrls:\n%s", utils.ColoredString(remote.Name, color.FgGreen), strings.Join(remote.Urls, "\n")))
}
func (gui *Gui) refreshRemotes() error {
@@ -80,11 +81,14 @@ func (gui *Gui) renderRemotesWithSelection() error {
branchesView := gui.getBranchesView()
gui.refreshSelectedLine(&gui.State.Panels.Remotes.SelectedLine, len(gui.State.Remotes))
if err := gui.renderListPanel(branchesView, gui.State.Remotes); err != nil {
return err
}
if err := gui.handleRemoteSelect(gui.g, branchesView); err != nil {
return err
displayStrings := presentation.GetRemoteListDisplayStrings(gui.State.Remotes)
gui.renderDisplayStrings(branchesView, displayStrings)
if gui.g.CurrentView() == branchesView && branchesView.Context == "remotes" {
if err := gui.handleRemoteSelect(gui.g, branchesView); err != nil {
return err
}
}
return nil
@@ -176,3 +180,18 @@ func (gui *Gui) handleEditRemote(g *gocui.Gui, v *gocui.View) error {
})
})
}
func (gui *Gui) handleFetchRemote(g *gocui.Gui, v *gocui.View) error {
remote := gui.getSelectedRemote()
if remote == nil {
return nil
}
return gui.WithWaitingStatus(gui.Tr.SLocalize("FetchingRemoteStatus"), func() error {
if err := gui.GitCommand.FetchRemote(remote.Name); err != nil {
return err
}
return gui.refreshRemotes()
})
}
+46
View File
@@ -0,0 +1,46 @@
package gui
import (
"fmt"
"github.com/fatih/color"
)
func (gui *Gui) createResetMenu(ref string) error {
strengths := []string{"soft", "mixed", "hard"}
menuItems := make([]*menuItem, len(strengths))
for i, strength := range strengths {
innerStrength := strength
menuItems[i] = &menuItem{
displayStrings: []string{
fmt.Sprintf("%s reset", strength),
color.New(color.FgRed).Sprint(
fmt.Sprintf("reset --%s %s", strength, ref),
),
},
onPress: func() error {
if err := gui.GitCommand.ResetToCommit(ref, innerStrength); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
gui.switchCommitsPanelContext("branch-commits")
gui.State.Panels.Commits.SelectedLine = 0
gui.State.Panels.ReflogCommits.SelectedLine = 0
if err := gui.refreshCommits(gui.g); err != nil {
return err
}
if err := gui.refreshFiles(); err != nil {
return err
}
if err := gui.resetOrigin(gui.getCommitsView()); err != nil {
return err
}
return gui.handleCommitSelect(gui.g, gui.getCommitsView())
},
}
}
return gui.createMenu(fmt.Sprintf("%s %s", gui.Tr.SLocalize("resetTo"), ref), menuItems, createMenuOptions{showCancel: true})
}
+91
View File
@@ -0,0 +1,91 @@
package gui
import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) handleOpenSearch(g *gocui.Gui, v *gocui.View) error {
gui.State.Searching.isSearching = true
gui.State.Searching.view = v
gui.renderString(gui.g, "search", "")
gui.switchFocus(gui.g, v, gui.getSearchView())
return nil
}
func (gui *Gui) handleSearch(g *gocui.Gui, v *gocui.View) error {
gui.State.Searching.searchString = gui.getSearchView().Buffer()
gui.switchFocus(gui.g, nil, gui.State.Searching.view)
if err := gui.State.Searching.view.Search(gui.State.Searching.searchString); err != nil {
return err
}
return nil
}
func (gui *Gui) onSelectItemWrapper(innerFunc func(int) error) func(int, int, int) error {
return func(y int, index int, total int) error {
if total == 0 {
gui.renderString(
gui.g,
"search",
fmt.Sprintf(
"no matches for '%s' %s",
gui.State.Searching.searchString,
utils.ColoredString(
fmt.Sprintf("%s: exit search mode", gui.getKeyDisplay("universal.return")),
theme.OptionsFgColor,
),
),
)
return nil
}
gui.renderString(
gui.g,
"search",
fmt.Sprintf(
"matches for '%s' (%d of %d) %s",
gui.State.Searching.searchString,
index+1,
total,
utils.ColoredString(
fmt.Sprintf(
"%s: next match, %s: previous match, %s: exit search mode",
gui.getKeyDisplay("universal.nextMatch"),
gui.getKeyDisplay("universal.prevMatch"),
gui.getKeyDisplay("universal.return"),
),
theme.OptionsFgColor,
),
),
)
if err := innerFunc(y); err != nil {
return err
}
return nil
}
}
func (gui *Gui) onSearchEscape() error {
gui.State.Searching.isSearching = false
if gui.State.Searching.view != nil {
gui.State.Searching.view.ClearSearch()
gui.State.Searching.view = nil
}
return nil
}
func (gui *Gui) handleSearchEscape(g *gocui.Gui, v *gocui.View) error {
if err := gui.switchFocus(gui.g, nil, gui.State.Searching.view); err != nil {
return err
}
gui.onSearchEscape()
return nil
}
+26 -8
View File
@@ -12,6 +12,13 @@ func (gui *Gui) refreshStagingPanel(forceSecondaryFocused bool, selectedLineIdx
state := gui.State.Panels.LineByLine
// We need to force focus here because the confirmation panel for safely staging lines does not return focus automatically.
// This is because if we tell it to return focus it will unconditionally return it to the main panel which may not be what we want
// e.g. in the event that there's nothing left to stage.
if err := gui.switchFocus(gui.g, nil, gui.getMainView()); err != nil {
return err
}
file, err := gui.getSelectedFile(gui.g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
@@ -87,26 +94,37 @@ func (gui *Gui) handleTogglePanel(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleStagingEscape(g *gocui.Gui, v *gocui.View) error {
gui.State.Panels.LineByLine = nil
gui.handleEscapeLineByLinePanel()
return gui.switchFocus(gui.g, nil, gui.getFilesView())
}
func (gui *Gui) handleStageSelection(g *gocui.Gui, v *gocui.View) error {
return gui.applySelection(false)
func (gui *Gui) handleToggleStagedSelection(g *gocui.Gui, v *gocui.View) error {
state := gui.State.Panels.LineByLine
return gui.applySelection(state.SecondaryFocused)
}
func (gui *Gui) handleResetSelection(g *gocui.Gui, v *gocui.View) error {
return gui.applySelection(true)
state := gui.State.Panels.LineByLine
if state.SecondaryFocused {
// for backwards compatibility
return gui.applySelection(true)
}
if gui.Config.GetUserConfig().GetBool("gui.skipUnstageLineWarning") {
return gui.createConfirmationPanel(gui.g, gui.getMainView(), false, "unstage lines", "Are you sure you want to delete the selected lines (git reset)? It is irreversible.\nTo disable this dialogue set the config key of 'gui.skipUnstageLineWarning' to true", func(*gocui.Gui, *gocui.View) error {
return gui.applySelection(true)
}, nil)
} else {
return gui.applySelection(true)
}
}
func (gui *Gui) applySelection(reverse bool) error {
state := gui.State.Panels.LineByLine
if !reverse && state.SecondaryFocused {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("CantStageStaged"))
}
file, err := gui.getSelectedFile(gui.g)
if err != nil {
return err
+19 -18
View File
@@ -1,11 +1,9 @@
package gui
import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
)
// list panel functions
@@ -34,16 +32,19 @@ func (gui *Gui) handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error {
stashEntry := gui.getSelectedStashEntry(v)
if stashEntry == nil {
return gui.renderString(g, "main", gui.Tr.SLocalize("NoStashEntries"))
return gui.newStringTask("main", gui.Tr.SLocalize("NoStashEntries"))
}
if err := gui.focusPoint(0, gui.State.Panels.Stash.SelectedLine, len(gui.State.StashEntries), 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)
}()
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowStashEntryCmdStr(stashEntry.Index),
)
if err := gui.newPtyTask("main", cmd); err != nil {
gui.Log.Error(err)
}
return nil
}
@@ -53,17 +54,12 @@ func (gui *Gui) refreshStashEntries(g *gocui.Gui) error {
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
}
stashView := gui.getStashView()
v := gui.getStashView()
v.Clear()
fmt.Fprint(v, list)
displayStrings := presentation.GetStashEntryListDisplayStrings(gui.State.StashEntries)
gui.renderDisplayStrings(stashView, displayStrings)
if err := gui.resetOrigin(v); err != nil {
if err := gui.resetOrigin(stashView); err != nil {
return err
}
return nil
@@ -119,3 +115,8 @@ func (gui *Gui) handleStashSave(stashFunc func(message string) error) error {
return gui.refreshFiles()
})
}
func (gui *Gui) onStashPanelSearchSelect(selectedLine int) error {
gui.State.Panels.Stash.SelectedLine = selectedLine
return gui.handleStashEntrySelect(gui.g, gui.getStashView())
}
+11 -11
View File
@@ -6,7 +6,7 @@ import (
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -35,7 +35,7 @@ func (gui *Gui) refreshStatus(g *gocui.Gui) error {
if len(branches) > 0 {
branch := branches[0]
name := utils.ColoredString(branch.Name, commands.GetBranchColor(branch.Name))
name := utils.ColoredString(branch.Name, presentation.GetBranchColor(branch.Name))
repoName := utils.GetCurrentRepoName()
status += fmt.Sprintf(" %s → %s", repoName, name)
}
@@ -111,7 +111,7 @@ func (gui *Gui) handleStatusSelect(g *gocui.Gui, v *gocui.View) error {
magenta.Sprint("Become a sponsor (github is matching all donations for 12 months): https://github.com/sponsors/jesseduffield"), // caffeine ain't free
}, "\n\n")
return gui.renderString(g, "main", dashboardString)
return gui.newStringTask("main", dashboardString)
}
func (gui *Gui) handleOpenConfig(g *gocui.Gui, v *gocui.View) error {
@@ -136,14 +136,6 @@ func lazygitTitle() string {
}
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
@@ -152,6 +144,14 @@ func (gui *Gui) updateWorkTreeState() error {
gui.State.WorkingTreeState = "rebasing"
return nil
}
merging, err := gui.GitCommand.IsInMergeState()
if err != nil {
return err
}
if merging {
gui.State.WorkingTreeState = "merging"
return nil
}
gui.State.WorkingTreeState = "normal"
return nil
}
+38 -24
View File
@@ -1,10 +1,9 @@
package gui
import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
)
// list panel functions
@@ -33,25 +32,18 @@ func (gui *Gui) handleTagSelect(g *gocui.Gui, v *gocui.View) error {
tag := gui.getSelectedTag()
if tag == nil {
return gui.renderString(g, "main", "No tags")
return gui.newStringTask("main", "No tags")
}
if err := gui.focusPoint(0, gui.State.Panels.Tags.SelectedLine, len(gui.State.Tags), v); err != nil {
return err
}
go func() {
show, err := gui.GitCommand.ShowTag(tag.Name)
if err != nil {
show = ""
}
graph, err := gui.GitCommand.GetBranchGraph(tag.Name)
if err != nil {
graph = "No graph for tag " + tag.Name
}
_ = gui.renderString(g, "main", fmt.Sprintf("%s\n%s", show, graph))
}()
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.GetBranchGraphCmdStr(tag.Name),
)
if err := gui.newCmdTask("main", cmd); err != nil {
gui.Log.Error(err)
}
return nil
}
@@ -75,11 +67,12 @@ func (gui *Gui) renderTagsWithSelection() error {
branchesView := gui.getBranchesView()
gui.refreshSelectedLine(&gui.State.Panels.Tags.SelectedLine, len(gui.State.Tags))
if err := gui.renderListPanel(branchesView, gui.State.Tags); err != nil {
return err
}
if err := gui.handleTagSelect(gui.g, branchesView); err != nil {
return err
displayStrings := presentation.GetTagListDisplayStrings(gui.State.Tags)
gui.renderDisplayStrings(branchesView, displayStrings)
if gui.g.CurrentView() == branchesView && branchesView.Context == "tags" {
if err := gui.handleTagSelect(gui.g, branchesView); err != nil {
return err
}
}
return nil
@@ -90,7 +83,7 @@ func (gui *Gui) handleCheckoutTag(g *gocui.Gui, v *gocui.View) error {
if tag == nil {
return nil
}
if err := gui.handleCheckoutBranch(tag.Name); err != nil {
if err := gui.handleCheckoutRef(tag.Name); err != nil {
return err
}
return gui.switchBranchesPanelContext("local-branches")
@@ -113,7 +106,13 @@ func (gui *Gui) handleDeleteTag(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.DeleteTag(tag.Name); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshTags()
if err := gui.refreshCommits(g); err != nil {
return gui.createErrorPanel(g, err.Error())
}
if err := gui.refreshTags(); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return nil
}, nil)
}
@@ -144,6 +143,21 @@ func (gui *Gui) handleCreateTag(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.CreateLightweightTag(v.Buffer(), ""); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshTags()
if err := gui.refreshCommits(g); err != nil {
return gui.createErrorPanel(g, err.Error())
}
if err := gui.refreshTags(); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return nil
})
}
func (gui *Gui) handleCreateResetToTagMenu(g *gocui.Gui, v *gocui.View) error {
tag := gui.getSelectedTag()
if tag == nil {
return nil
}
return gui.createResetMenu(tag.Name)
}
+91
View File
@@ -0,0 +1,91 @@
package gui
import (
"os/exec"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/tasks"
)
func (gui *Gui) newCmdTask(viewName string, cmd *exec.Cmd) error {
view, err := gui.g.View(viewName)
if err != nil {
return nil // swallowing for now
}
_, height := view.Size()
_, oy := view.Origin()
manager := gui.getManager(view)
r, err := cmd.StdoutPipe()
if err != nil {
return err
}
cmd.Stderr = cmd.Stdout
if err := cmd.Start(); err != nil {
return err
}
if err := manager.NewTask(manager.NewCmdTask(r, cmd, height+oy+10, nil)); err != nil {
return err
}
return nil
}
func (gui *Gui) newTask(viewName string, f func(chan struct{}) error) error {
view, err := gui.g.View(viewName)
if err != nil {
return nil // swallowing for now
}
manager := gui.getManager(view)
if err := manager.NewTask(f); err != nil {
return err
}
return nil
}
func (gui *Gui) newStringTask(viewName string, str string) error {
view, err := gui.g.View(viewName)
if err != nil {
return nil // swallowing for now
}
manager := gui.getManager(view)
f := func(stop chan struct{}) error {
return gui.renderString(gui.g, viewName, str)
}
if err := manager.NewTask(f); err != nil {
return err
}
return nil
}
func (gui *Gui) getManager(view *gocui.View) *tasks.ViewBufferManager {
manager, ok := gui.viewBufferManagerMap[view.Name()]
if !ok {
manager = tasks.NewViewBufferManager(
gui.Log,
view,
func() {
view.Clear()
},
func() {
gui.g.Update(func(*gocui.Gui) error {
gui.Log.Warn("updating view")
return nil
})
})
gui.viewBufferManagerMap[view.Name()] = manager
}
return manager
}
+14 -34
View File
@@ -102,7 +102,7 @@ func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error {
case "status":
return gui.handleStatusSelect(g, v)
case "files":
return gui.handleFileSelect(g, v)
return gui.focusAndSelectFile(g, v)
case "branches":
branchesView := gui.getBranchesView()
switch branchesView.Context {
@@ -135,6 +135,8 @@ func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error {
}
v.Highlight = false
return nil
case "search":
return nil
default:
panic(gui.Tr.SLocalize("NoViewMachingNewLineFocusedSwitchStatement"))
}
@@ -218,32 +220,7 @@ func (gui *Gui) resetOrigin(v *gocui.View) error {
// if the cursor down past the last item, move it to the last line
func (gui *Gui) focusPoint(cx int, cy int, lineCount int, v *gocui.View) error {
if cy < 0 || cy > lineCount {
return nil
}
ox, oy := v.Origin()
_, height := v.Size()
ly := height - 1
if ly == -1 {
ly = 0
}
// 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 > lineCount {
_ = v.SetCursor(cx, cy)
_ = v.SetOrigin(ox, 0)
} else if cy < oy {
_ = v.SetCursor(cx, 0)
_ = v.SetOrigin(ox, cy)
} else if cy > oy+ly {
_ = v.SetCursor(cx, ly)
_ = v.SetOrigin(ox, cy-ly)
} else {
_ = v.SetCursor(cx, cy-oy)
}
v.FocusPoint(cx, cy)
return nil
}
@@ -268,6 +245,9 @@ func (gui *Gui) renderString(g *gocui.Gui, viewName, s string) error {
if err := v.SetOrigin(0, 0); err != nil {
return err
}
if err := v.SetCursor(0, 0); err != nil {
return err
}
return gui.setViewContent(gui.g, v, s)
})
return nil
@@ -333,6 +313,11 @@ func (gui *Gui) getMenuView() *gocui.View {
return v
}
func (gui *Gui) getSearchView() *gocui.View {
v, _ := gui.g.View("search")
return v
}
func (gui *Gui) trimmedContent(v *gocui.View) string {
return strings.TrimSpace(v.Buffer())
}
@@ -402,18 +387,13 @@ func (gui *Gui) refreshSelectedLine(line *int, total int) {
}
}
func (gui *Gui) renderListPanel(v *gocui.View, items interface{}) error {
func (gui *Gui) renderDisplayStrings(v *gocui.View, displayStrings [][]string) {
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())
}
list := utils.RenderDisplayStrings(displayStrings)
v.Clear()
fmt.Fprint(v, list)
return nil
})
return nil
}
func (gui *Gui) renderPanelOptions() error {
+93
View File
@@ -0,0 +1,93 @@
package gui
import (
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
)
func (gui *Gui) handleCreateResetMenu(g *gocui.Gui, v *gocui.View) error {
red := color.New(color.FgRed)
menuItems := []*menuItem{
{
displayStrings: []string{
gui.Tr.SLocalize("discardAllChangesToAllFiles"),
red.Sprint("reset --hard HEAD && git clean -fd"),
},
onPress: func() error {
if err := gui.GitCommand.ResetAndClean(); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshFiles()
},
},
{
displayStrings: []string{
gui.Tr.SLocalize("discardAnyUnstagedChanges"),
red.Sprint("git checkout -- ."),
},
onPress: func() error {
if err := gui.GitCommand.DiscardAnyUnstagedFileChanges(); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshFiles()
},
},
{
displayStrings: []string{
gui.Tr.SLocalize("discardUntrackedFiles"),
red.Sprint("git clean -fd"),
},
onPress: func() error {
if err := gui.GitCommand.RemoveUntrackedFiles(); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshFiles()
},
},
{
displayStrings: []string{
gui.Tr.SLocalize("softReset"),
red.Sprint("git reset --soft HEAD"),
},
onPress: func() error {
if err := gui.GitCommand.ResetSoft("HEAD"); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshFiles()
},
},
{
displayStrings: []string{
"mixed reset",
red.Sprint("git reset --mixed HEAD"),
},
onPress: func() error {
if err := gui.GitCommand.ResetSoft("HEAD"); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshFiles()
},
},
{
displayStrings: []string{
gui.Tr.SLocalize("hardReset"),
red.Sprint("git reset --hard HEAD"),
},
onPress: func() error {
if err := gui.GitCommand.ResetHard("HEAD"); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshFiles()
},
},
}
return gui.createMenu("", menuItems, createMenuOptions{showCancel: true})
}
+6 -3
View File
@@ -136,9 +136,6 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &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",
@@ -760,6 +757,12 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "ReturnToRemotesList",
Other: `return to remotes list`,
}, &i18n.Message{
ID: "IgnoreTracked",
Other: "Ignore tracked file",
}, &i18n.Message{
ID: "IgnoreTrackedPrompt",
Other: "Are you sure you want to ignore a tracked file?",
},
)
}
+108 -6
View File
@@ -162,9 +162,6 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &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",
@@ -504,16 +501,19 @@ func addEnglish(i18nObject *i18n.Bundle) error {
Other: `select hunk`,
}, &i18n.Message{
ID: "StageSelection",
Other: `stage selection`,
Other: `toggle line staged / unstaged`,
}, &i18n.Message{
ID: "ResetSelection",
Other: `reset selection`,
Other: `delete change (git reset)`,
}, &i18n.Message{
ID: "ToggleDragSelect",
Other: `toggle drag select`,
}, &i18n.Message{
ID: "ToggleSelectHunk",
Other: `toggle select hunk`,
}, &i18n.Message{
ID: "ToggleSelectionForPatch",
Other: `add/remove line(s) to patch`,
},
&i18n.Message{
ID: "TogglePanel",
@@ -567,6 +567,54 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "RebaseOptionsTitle",
Other: "Rebase Options",
}, &i18n.Message{
ID: "CommitMessageTitle",
Other: "Commit Message",
}, &i18n.Message{
ID: "Local-BranchesTitle",
Other: "Branches Tab",
}, &i18n.Message{
ID: "SearchTitle",
Other: "Search",
}, &i18n.Message{
ID: "TagsTitle",
Other: "Tags Tab",
}, &i18n.Message{
ID: "Branch-CommitsTitle",
Other: "Commits Tab",
}, &i18n.Message{
ID: "MenuTitle",
Other: "Menu",
}, &i18n.Message{
ID: "RemotesTitle",
Other: "Remotes Tab",
}, &i18n.Message{
ID: "CredentialsTitle",
Other: "Credentials",
}, &i18n.Message{
ID: "Remote-BranchesTitle",
Other: "Remote Branches (in Remotes tab)",
}, &i18n.Message{
ID: "Patch-BuildingTitle",
Other: "Patch Building",
}, &i18n.Message{
ID: "InformationTitle",
Other: "Information",
}, &i18n.Message{
ID: "SecondaryTitle",
Other: "Secondary",
}, &i18n.Message{
ID: "Reflog-CommitsTitle",
Other: "Reflog Tab",
}, &i18n.Message{
ID: "Title",
Other: "Title",
}, &i18n.Message{
ID: "GlobalTitle",
Other: "Global Keybindings",
}, &i18n.Message{
ID: "MerginTitle",
Other: "Mergin",
}, &i18n.Message{
ID: "ConflictsResolved",
Other: "all merge conflicts resolved. Continue?",
@@ -654,6 +702,12 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "ScrollUp",
Other: "scroll up",
}, &i18n.Message{
ID: "scrollUpMainPanel",
Other: "scroll up main panel",
}, &i18n.Message{
ID: "scrollDownMainPanel",
Other: "scroll down main panel",
}, &i18n.Message{
ID: "AmendCommitTitle",
Other: "Amend Commit",
@@ -695,7 +749,7 @@ func addEnglish(i18nObject *i18n.Bundle) error {
Other: "view commit's files",
}, &i18n.Message{
ID: "CommitFilesTitle",
Other: "Commit files",
Other: "Commit Files",
}, &i18n.Message{
ID: "goBack",
Other: "go back",
@@ -753,6 +807,9 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "hardReset",
Other: "hard reset",
}, &i18n.Message{
ID: "hardResetUpstream",
Other: "hard reset to upstream branch",
}, &i18n.Message{
ID: "viewResetOptions",
Other: `view reset options`,
@@ -822,6 +879,9 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "toggleAddToPatch",
Other: "toggle file included in patch",
}, &i18n.Message{
ID: "ViewPatchOptions",
Other: "view custom patch options",
}, &i18n.Message{
ID: "PatchOptionsTitle",
Other: "Patch Options",
@@ -912,6 +972,48 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "CreateTagTitle",
Other: "Tag name:",
}, &i18n.Message{
ID: "fetchRemote",
Other: "fetch remote",
}, &i18n.Message{
ID: "FetchingRemoteStatus",
Other: "fetching remote",
}, &i18n.Message{
ID: "checkoutCommit",
Other: "checkout commit",
}, &i18n.Message{
ID: "SureCheckoutThisCommit",
Other: "Are you sure you want to checkout this commit?",
}, &i18n.Message{
ID: "gitFlowOptions",
Other: "show git-flow options",
}, &i18n.Message{
ID: "NotAGitFlowBranch",
Other: "This does not seem to be a git flow branch",
}, &i18n.Message{
ID: "NewBranchNamePrompt",
Other: "new {{.branchType}} name:",
}, &i18n.Message{
ID: "IgnoreTracked",
Other: "Ignore tracked file",
}, &i18n.Message{
ID: "IgnoreTrackedPrompt",
Other: "Are you sure you want to ignore a tracked file?",
}, &i18n.Message{
ID: "viewResetToUpstreamOptions",
Other: "view upstream reset options",
}, &i18n.Message{
ID: "nextScreenMode",
Other: "next screen mode (normal/half/fullscreen)",
}, &i18n.Message{
ID: "prevScreenMode",
Other: "prev screen mode",
}, &i18n.Message{
ID: "Panel",
Other: "Panel",
}, &i18n.Message{
ID: "Keybindings",
Other: "Keybindings",
},
)
}
+6 -3
View File
@@ -128,9 +128,6 @@ func addPolish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "CannotGitAdd",
Other: "Nie można git add --patch nieśledzonych plików",
}, &i18n.Message{
ID: "CantIgnoreTrackFiles",
Other: "Nie można zignorować nieśledzonych plików",
}, &i18n.Message{
ID: "NoStagedFilesToCommit",
Other: "Brak zatwierdzonych plików do commita",
@@ -743,6 +740,12 @@ func addPolish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "ReturnToRemotesList",
Other: `return to remotes list`,
}, &i18n.Message{
ID: "IgnoreTracked",
Other: "Ignore tracked file",
}, &i18n.Message{
ID: "IgnoreTrackedPrompt",
Other: "Are you sure you want to ignore a tracked file?",
},
)
}
+227
View File
@@ -0,0 +1,227 @@
package tasks
import (
"bufio"
"fmt"
"io"
"os/exec"
"sync"
"time"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/sirupsen/logrus"
)
type Task struct {
stop chan struct{}
stopped bool
stopMutex sync.Mutex
notifyStopped chan struct{}
Log *logrus.Entry
f func(chan struct{}) error
}
type ViewBufferManager struct {
writer io.Writer
waitingTask *Task
currentTask *Task
waitingMutex sync.Mutex
taskIDMutex sync.Mutex
Log *logrus.Entry
newTaskId int
readLines chan int
// beforeStart is the function that is called before starting a new task
beforeStart func()
refreshView func()
}
func NewViewBufferManager(log *logrus.Entry, writer io.Writer, beforeStart func(), refreshView func()) *ViewBufferManager {
return &ViewBufferManager{Log: log, writer: writer, beforeStart: beforeStart, refreshView: refreshView, readLines: make(chan int, 1024)}
}
func (m *ViewBufferManager) ReadLines(n int) {
go func() {
m.readLines <- n
}()
}
func (m *ViewBufferManager) NewCmdTask(r io.Reader, cmd *exec.Cmd, linesToRead int, onDone func()) func(chan struct{}) error {
return func(stop chan struct{}) error {
go func() {
<-stop
if err := commands.Kill(cmd); err != nil {
m.Log.Warn(err)
}
if onDone != nil {
onDone()
}
}()
loadingMutex := sync.Mutex{}
// not sure if it's the right move to redefine this or not
m.readLines = make(chan int, 1024)
done := make(chan struct{})
go func() {
scanner := bufio.NewScanner(r)
scanner.Split(bufio.ScanLines)
loaded := false
go func() {
ticker := time.NewTicker(time.Millisecond * 100)
defer ticker.Stop()
select {
case <-ticker.C:
loadingMutex.Lock()
if !loaded {
m.beforeStart()
m.writer.Write([]byte("loading..."))
m.refreshView()
}
loadingMutex.Unlock()
case <-stop:
return
}
}()
outer:
for {
select {
case linesToRead := <-m.readLines:
for i := 0; i < linesToRead; i++ {
ok := scanner.Scan()
loadingMutex.Lock()
if !loaded {
m.beforeStart()
loaded = true
}
loadingMutex.Unlock()
select {
case <-stop:
m.refreshView()
break outer
default:
}
if !ok {
m.refreshView()
break outer
}
m.writer.Write(append(scanner.Bytes(), []byte("\n")...))
}
m.refreshView()
case <-stop:
m.refreshView()
break outer
}
}
if err := cmd.Wait(); err != nil {
m.Log.Warn(err)
}
m.refreshView()
if onDone != nil {
onDone()
}
close(done)
}()
m.readLines <- linesToRead
<-done
return nil
}
}
// Close closes the task manager, killing whatever task may currently be running
func (t *ViewBufferManager) Close() {
if t.currentTask == nil {
return
}
c := make(chan struct{})
go func() {
t.currentTask.Stop()
c <- struct{}{}
}()
select {
case <-c:
return
case <-time.After(3 * time.Second):
fmt.Println("cannot kill child process")
}
}
// different kinds of tasks:
// 1) command based, where the manager can be asked to read more lines, but the command can be killed
// 2) string based, where the manager can also be asked to read more lines
func (m *ViewBufferManager) NewTask(f func(stop chan struct{}) error) error {
go func() {
m.taskIDMutex.Lock()
m.newTaskId++
taskID := m.newTaskId
m.Log.Infof("starting task %d", taskID)
m.taskIDMutex.Unlock()
m.waitingMutex.Lock()
defer m.waitingMutex.Unlock()
m.Log.Infof("done waiting")
if taskID < m.newTaskId {
m.Log.Infof("returning cos the task is obsolete")
return
}
stop := make(chan struct{})
notifyStopped := make(chan struct{})
if m.currentTask != nil {
m.Log.Info("asking task to stop")
m.currentTask.Stop()
m.Log.Info("task stopped")
}
m.currentTask = &Task{
stop: stop,
notifyStopped: notifyStopped,
Log: m.Log,
f: f,
}
go func() {
if err := f(stop); err != nil {
m.Log.Error(err) // might need an onError callback
}
m.Log.Infof("returning from task %d", taskID)
close(notifyStopped)
}()
}()
return nil
}
func (t *Task) Stop() {
t.stopMutex.Lock()
defer t.stopMutex.Unlock()
if t.stopped {
return
}
close(t.stop)
t.Log.Info("closed stop channel, waiting for notifyStopped message")
<-t.notifyStopped
t.Log.Info("received notifystopped message")
t.stopped = true
return
}
+13
View File
@@ -3,6 +3,7 @@ package test
import (
"fmt"
"os/exec"
"regexp"
"strings"
"testing"
@@ -43,3 +44,15 @@ func CreateMockCommand(t *testing.T, swappers []*CommandSwapper) func(cmd string
return command
}
}
func AssertContainsMatch(t *testing.T, strs []string, pattern *regexp.Regexp, message string) {
t.Helper()
for _, str := range strs {
if pattern.Match([]byte(str)) {
return
}
}
assert.Fail(t, message)
}
+75 -33
View File
@@ -20,12 +20,22 @@ var (
// InactiveBorderColor is the border color of the inactive active frames
InactiveBorderColor gocui.Attribute
// SelectedLineBgColor is the background color for the selected line
SelectedLineBgColor color.Attribute
OptionsFgColor color.Attribute
OptionsColor gocui.Attribute
)
// UpdateTheme updates all theme variables
func UpdateTheme(userConfig *viper.Viper) {
ActiveBorderColor = getColor(userConfig.GetStringSlice("gui.theme.activeBorderColor"))
InactiveBorderColor = getColor(userConfig.GetStringSlice("gui.theme.inactiveBorderColor"))
ActiveBorderColor = GetGocuiColor(userConfig.GetStringSlice("gui.theme.activeBorderColor"))
InactiveBorderColor = GetGocuiColor(userConfig.GetStringSlice("gui.theme.inactiveBorderColor"))
SelectedLineBgColor = GetBgColor(userConfig.GetStringSlice("gui.theme.selectedLineBgColor"))
OptionsColor = GetGocuiColor(userConfig.GetStringSlice("gui.theme.optionsTextColor"))
OptionsFgColor = GetFgColor(userConfig.GetStringSlice("gui.theme.optionsTextColor"))
isLightTheme := userConfig.GetBool("gui.theme.lightTheme")
if isLightTheme {
@@ -39,8 +49,8 @@ func UpdateTheme(userConfig *viper.Viper) {
}
}
// getAttribute gets the gocui color attribute from the string
func getAttribute(key string) gocui.Attribute {
// GetAttribute gets the gocui color attribute from the string
func GetGocuiAttribute(key string) gocui.Attribute {
colorMap := map[string]gocui.Attribute{
"default": gocui.ColorDefault,
"black": gocui.ColorBlack,
@@ -62,43 +72,75 @@ func getAttribute(key string) gocui.Attribute {
return gocui.ColorWhite
}
// getColor bitwise OR's a list of attributes obtained via the given keys
func getColor(keys []string) gocui.Attribute {
// GetFgAttribute gets the color foreground attribute from the string
func GetFgAttribute(key string) color.Attribute {
colorMap := map[string]color.Attribute{
"default": color.FgWhite,
"black": color.FgBlack,
"red": color.FgRed,
"green": color.FgGreen,
"yellow": color.FgYellow,
"blue": color.FgBlue,
"magenta": color.FgMagenta,
"cyan": color.FgCyan,
"white": color.FgWhite,
"bold": color.Bold,
"reverse": color.ReverseVideo,
"underline": color.Underline,
}
value, present := colorMap[key]
if present {
return value
}
return color.FgWhite
}
// GetBgAttribute gets the color background attribute from the string
func GetBgAttribute(key string) color.Attribute {
colorMap := map[string]color.Attribute{
"default": color.BgWhite,
"black": color.BgBlack,
"red": color.BgRed,
"green": color.BgGreen,
"yellow": color.BgYellow,
"blue": color.BgBlue,
"magenta": color.BgMagenta,
"cyan": color.BgCyan,
"white": color.BgWhite,
"bold": color.Bold,
"reverse": color.ReverseVideo,
"underline": color.Underline,
}
value, present := colorMap[key]
if present {
return value
}
return color.FgWhite
}
// GetGocuiColor bitwise OR's a list of attributes obtained via the given keys
func GetGocuiColor(keys []string) gocui.Attribute {
var attribute gocui.Attribute
for _, key := range keys {
attribute |= getAttribute(key)
attribute |= GetGocuiAttribute(key)
}
return attribute
}
// GetAttribute gets the gocui color attribute from the string
func 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 GetColor(keys []string) gocui.Attribute {
var attribute gocui.Attribute
func GetBgColor(keys []string) color.Attribute {
var attribute color.Attribute
for _, key := range keys {
attribute |= GetAttribute(key)
attribute |= GetBgAttribute(key)
}
return attribute
}
// GetColor bitwise OR's a list of attributes obtained via the given keys
func GetFgColor(keys []string) color.Attribute {
var attribute color.Attribute
for _, key := range keys {
attribute |= GetFgAttribute(key)
}
return attribute
}
+63 -57
View File
@@ -6,13 +6,10 @@ import (
"log"
"os"
"path/filepath"
"reflect"
"regexp"
"strings"
"time"
"github.com/go-errors/errors"
"github.com/fatih/color"
)
@@ -114,51 +111,11 @@ func Min(x, y int) int {
return y
}
type Displayable interface {
GetDisplayStrings(bool) []string
}
func RenderDisplayStrings(displayStringsArr [][]string) string {
padWidths := getPadWidths(displayStringsArr)
paddedDisplayStrings := getPaddedDisplayStrings(displayStringsArr, padWidths)
// 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
return strings.Join(paddedDisplayStrings, "\n")
}
// Decolorise strips a string of color
@@ -168,10 +125,16 @@ func Decolorise(str string) string {
}
func getPadWidths(stringArrays [][]string) []int {
if len(stringArrays[0]) <= 1 {
maxWidth := 0
for _, stringArray := range stringArrays {
if len(stringArray) > maxWidth {
maxWidth = len(stringArray)
}
}
if maxWidth-1 < 0 {
return []int{}
}
padWidths := make([]int, len(stringArrays[0])-1)
padWidths := make([]int, maxWidth-1)
for i := range padWidths {
for _, strings := range stringArrays {
uncoloredString := Decolorise(strings[i])
@@ -190,8 +153,14 @@ func getPaddedDisplayStrings(stringArrays [][]string, padWidths []int) []string
continue
}
for j, padWidth := range padWidths {
if len(stringArray)-1 < j {
continue
}
paddedDisplayStrings[i] += WithPadding(stringArray[j], padWidth) + " "
}
if len(stringArray)-1 < len(padWidths) {
continue
}
paddedDisplayStrings[i] += stringArray[len(padWidths)]
}
return paddedDisplayStrings
@@ -208,14 +177,6 @@ func displayArraysAligned(stringArrays [][]string) bool {
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 {
@@ -310,3 +271,48 @@ func ModuloWithWrap(n, max int) int {
return n
}
}
// NextIntInCycle returns the next int in a slice, returning to the first index if we've reached the end
func NextIntInCycle(sl []int, current int) int {
for i, val := range sl {
if val == current {
if i == len(sl)-1 {
return sl[0]
}
return sl[i+1]
}
}
return sl[0]
}
// PrevIntInCycle returns the prev int in a slice, returning to the first index if we've reached the end
func PrevIntInCycle(sl []int, current int) int {
for i, val := range sl {
if val == current {
if i > 0 {
return sl[i-1]
}
return sl[len(sl)-1]
}
}
return sl[len(sl)-1]
}
// TruncateWithEllipsis returns a string, truncated to a certain length, with an ellipsis
func TruncateWithEllipsis(str string, limit int) string {
if limit == 1 && len(str) > 1 {
return "."
}
if limit == 2 && len(str) > 2 {
return ".."
}
ellipsis := "..."
if len(str) <= limit {
return str
}
remainingLength := limit - len(ellipsis)
return str[0:remainingLength] + "..."
}
-158
View File
@@ -196,166 +196,8 @@ func TestDisplayArraysAligned(t *testing.T) {
}
}
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 {
+72 -17
View File
@@ -12,6 +12,7 @@ import (
"fmt"
"log"
"os"
"sort"
"strings"
"github.com/jesseduffield/lazygit/pkg/app"
@@ -60,33 +61,87 @@ func formatTitle(title string) string {
func formatBinding(binding *gui.Binding) string {
if binding.Alternative != "" {
return fmt.Sprintf(" <kbd>%s</kbd>: %s (%s)\n", binding.GetKey(), binding.Description, binding.Alternative)
return fmt.Sprintf(" <kbd>%s</kbd>: %s (%s)\n", gui.GetKeyDisplay(binding.Key), binding.Description, binding.Alternative)
}
return fmt.Sprintf(" <kbd>%s</kbd>: %s\n", binding.GetKey(), binding.Description)
return fmt.Sprintf(" <kbd>%s</kbd>: %s\n", gui.GetKeyDisplay(binding.Key), 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
bindings := mApp.Gui.GetInitialKeybindings()
type contextAndViewType struct {
context string
viewName string
}
contextAndViewBindingMap := map[contextAndViewType][]*gui.Binding{}
for _, binding := range bindings {
contexts := []string{}
if len(binding.Contexts) == 0 {
contexts = append(contexts, "")
} else {
for _, context := range binding.Contexts {
contexts = append(contexts, context)
}
}
viewName := binding.ViewName
for _, context := range contexts {
key := contextAndViewType{context: context, viewName: binding.ViewName}
existing := contextAndViewBindingMap[key]
if existing == nil {
contextAndViewBindingMap[key] = []*gui.Binding{binding}
} else {
contextAndViewBindingMap[key] = append(contextAndViewBindingMap[key], binding)
}
}
}
type groupedBindingsType struct {
contextAndView contextAndViewType
bindings []*gui.Binding
}
groupedBindings := make([]groupedBindingsType, len(contextAndViewBindingMap))
for contextAndView, contextBindings := range contextAndViewBindingMap {
groupedBindings = append(groupedBindings, groupedBindingsType{contextAndView: contextAndView, bindings: contextBindings})
}
sort.Slice(groupedBindings, func(i, j int) bool {
first := groupedBindings[i].contextAndView
second := groupedBindings[j].contextAndView
if first.viewName == "" {
return true
}
if second.viewName == "" {
return false
}
return first.viewName < second.viewName || (first.viewName == second.viewName && first.context < second.context)
})
for _, group := range groupedBindings {
contextAndView := group.contextAndView
contextBindings := group.bindings
mApp.Log.Warn("viewname: " + contextAndView.viewName + ", context: " + contextAndView.context)
viewName := contextAndView.viewName
if viewName == "" {
viewName = "global"
}
title := localisedTitle(mApp, viewName)
bindingSections = addBinding(title, bindingSections, binding)
}
for contextName, contextBindings := range mApp.Gui.GetContextMap() {
translatedView := localisedTitle(mApp, contextBindings[0].ViewName)
translatedContextName := localisedTitle(mApp, contextName)
title := fmt.Sprintf("%s (%s)", translatedView, translatedContextName)
translatedView := localisedTitle(mApp, viewName)
var title string
if contextAndView.context == "" {
addendum := " " + mApp.Tr.SLocalize("Panel")
if viewName == "global" {
addendum = ""
}
title = fmt.Sprintf("%s%s", translatedView, addendum)
} else {
translatedContextName := localisedTitle(mApp, contextAndView.context)
title = fmt.Sprintf("%s %s (%s)", translatedView, mApp.Tr.SLocalize("Panel"), translatedContextName)
}
for _, binding := range contextBindings {
bindingSections = addBinding(title, bindingSections, binding)
@@ -117,7 +172,7 @@ func addBinding(title string, bindingSections []*bindingSection, binding *gui.Bi
}
func formatSections(mApp *app.App, bindingSections []*bindingSection) string {
content := fmt.Sprintf("# Lazygit %s\n", mApp.Tr.SLocalize("menu"))
content := fmt.Sprintf("# Lazygit %s\n", mApp.Tr.SLocalize("Keybindings"))
for _, section := range bindingSections {
content += formatTitle(section.title)
+2
View File
@@ -12,3 +12,5 @@
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/
.idea/
+25 -9
View File
@@ -172,41 +172,57 @@ Other more specific types can also be used as flag types. They will be automati
- time.Duration
- []time.Duration
# Recommended Program Structure
# An Example Program
Best practice when using flaggy includes setting your program's name, description, and version (at build time).
Best practice when using flaggy includes setting your program's name, description, and version (at build time) as shown in this example program.
```go
package main
import "github.com/integrii/flaggy"
// make a variable for the version which will be set at build time
// Make a variable for the version which will be set at build time.
var version = "unknown"
// keep subcommands as globals so you can easily check if they were used later on
// Keep subcommands as globals so you can easily check if they were used later on.
var mySubcommand *flaggy.Subcommand
// Setup the variables you want your incoming flags to set.
var testVar string
// If you would like an environment variable as the default for a value, just populate the flag
// with the value of the environment by default. If the flag corresponding to this value is not
// used, then it will not be changed.
var myVar = os.Getenv("MY_VAR")
func init() {
// Set your program's name and description. These appear in help output.
flaggy.SetName("Test Program")
flaggy.SetDescription("A little example program")
// you can disable various things by changing bools on the default parser
// (or your own parser if you have created one)
// You can disable various things by changing bools on the default parser
// (or your own parser if you have created one).
flaggy.DefaultParser.ShowHelpOnUnexpected = false
// you can set a help prepend or append on the default parser
// You can set a help prepend or append on the default parser.
flaggy.DefaultParser.AdditionalHelpPrepend = "http://github.com/integrii/flaggy"
// Add a flag to the main program (this will be available in all subcommands as well).
flaggy.String(&testVar, "tv", "testVariable", "A variable just for testing things!")
// create any subcommands and set their parameters
// Create any subcommands and set their parameters.
mySubcommand = flaggy.NewSubcommand("mySubcommand")
mySubcommand.Description = "My great subcommand!"
// Add a flag to the subcommand.
mySubcommand.String(&myVar, "mv", "myVariable", "A variable just for me!")
// set the version and parse all inputs into variables
// Set the version and parse all inputs into variables.
flaggy.SetVersion(version)
flaggy.Parse()
}
func main(){
if mySubcommand.Used {
...
+3 -5
View File
@@ -5,22 +5,20 @@ package flaggy
// The return values represent the key being set, and any errors
// returned when setting the key, such as failures to convert the string
// into the appropriate flag value. We stop assigning values as soon
// as we find a parser that accepts it.
// as we find a any parser that accepts it.
func setValueForParsers(key string, value string, parsers ...ArgumentParser) (bool, error) {
var valueWasSet bool
for _, p := range parsers {
valueWasSet, err := p.SetValueForKey(key, value)
if err != nil {
return valueWasSet, err
}
if valueWasSet {
break
return true, nil
}
}
return valueWasSet, nil
return false, nil
}
// ArgumentParser represents a parser or subcommand
+12
View File
@@ -327,6 +327,18 @@ func exitOrPanic(code int) {
os.Exit(code)
}
// ShowHelpOnUnexpectedEnable enables the ShowHelpOnUnexpected behavior on the
// default parser. This causes unknown inputs to error out.
func ShowHelpOnUnexpectedEnable() {
DefaultParser.ShowHelpOnUnexpected = true
}
// ShowHelpOnUnexpectedDisable disables the ShowHelpOnUnexpected behavior on the
// default parser. This causes unknown inputs to error out.
func ShowHelpOnUnexpectedDisable() {
DefaultParser.ShowHelpOnUnexpected = false
}
// AddPositionalValue adds a positional value to the main parser at the global
// context
func AddPositionalValue(assignmentVar *string, name string, relativePosition int, required bool, description string) {
+23
View File
@@ -0,0 +1,23 @@
package flaggy
// parsedValue represents a flag or subcommand that was parsed. Primairily used
// to account for all parsed values in order to determine if unknown values were
// passed to the root parser after all subcommands have been parsed.
type parsedValue struct {
Key string
Value string
IsPositional bool // indicates that this value was positional and not a key/value
}
// newParsedValue creates and returns a new parsedValue struct with the
// supplied values set
func newParsedValue(key string, value string, isPositional bool) parsedValue {
if len(key) == 0 && len(value) == 0 {
panic("cant add parsed value with no key or value")
}
return parsedValue{
Key: key,
Value: value,
IsPositional: isPositional,
}
}
+90 -6
View File
@@ -4,18 +4,20 @@ import (
"errors"
"fmt"
"os"
"strconv"
"text/template"
)
// Parser represents the set of vars and subcommands we are expecting
// from our input args, and the parser than handles them all.
// Parser represents the set of flags and subcommands we are expecting
// from our input arguments. Parser is the top level struct responsible for
// parsing an entire set of subcommands and flags.
type Parser struct {
Subcommand
Version string // the optional version of the parser.
ShowHelpWithHFlag bool // display help when -h or --help passed
ShowVersionWithVersionFlag bool // display the version when --version passed
ShowHelpOnUnexpected bool // display help when an unexpected flag is passed
ShowHelpOnUnexpected bool // display help when an unexpected flag or subcommand is passed
TrailingArguments []string // everything after a -- is placed here
HelpTemplate *template.Template // template for Help output
trailingArgumentsExtracted bool // indicates that trailing args have been parsed and should not be appended again
@@ -46,8 +48,89 @@ func (p *Parser) ParseArgs(args []string) error {
return errors.New("Parser.Parse() called twice on parser with name: " + " " + p.Name + " " + p.ShortName)
}
p.parsed = true
// debugPrint("Kicking off parsing with args:", args)
return p.parse(p, args, 0)
debugPrint("Kicking off parsing with args:", args)
err := p.parse(p, args, 0)
if err != nil {
return err
}
// if we are set to crash on unexpected args, look for those here TODO
if p.ShowHelpOnUnexpected {
parsedValues := p.findAllParsedValues()
debugPrint("parsedValues:", parsedValues)
argsNotParsed := findArgsNotInParsedValues(args, parsedValues)
if len(argsNotParsed) > 0 {
// flatten out unused args for our error message
var argsNotParsedFlat string
for _, a := range argsNotParsed {
argsNotParsedFlat = argsNotParsedFlat + " " + a
}
p.ShowHelpAndExit("Unknown arguments supplied: " + argsNotParsedFlat)
}
}
return nil
}
// findArgsNotInParsedValues finds arguments not used in parsed values. The
// incoming args should be in the order supplied by the user and should not
// include the invoked binary, which is normally the first thing in os.Args.
func findArgsNotInParsedValues(args []string, parsedValues []parsedValue) []string {
var argsNotUsed []string
var skipNext bool
for _, a := range args {
// if the final argument (--) is seen, then we stop checking because all
// further values are trailing arguments.
if determineArgType(a) == argIsFinal {
return argsNotUsed
}
// allow for skipping the next arg when needed
if skipNext {
skipNext = false
continue
}
// strip flag slashes from incoming arguments so they match up with the
// keys from parsedValues.
arg := parseFlagToName(a)
// indicates that we found this arg used in one of the parsed values. Used
// to indicate which values should be added to argsNotUsed.
var foundArgUsed bool
// search all args for a corresponding parsed value
for _, pv := range parsedValues {
// this argumenet was a key
// debugPrint(pv.Key, "==", arg)
debugPrint(pv.Key + "==" + arg + " || (" + strconv.FormatBool(pv.IsPositional) + " && " + pv.Value + " == " + arg + ")")
if pv.Key == arg || (pv.IsPositional && pv.Value == arg) {
debugPrint("Found matching parsed arg for " + pv.Key)
foundArgUsed = true // the arg was used in this parsedValues set
// if the value is not a positional value and the parsed value had a
// value that was not blank, we skip the next value in the argument list
if !pv.IsPositional && len(pv.Value) > 0 {
skipNext = true
break
}
}
// this prevents excessive parsed values from being checked after we find
// the arg used for the first time
if foundArgUsed {
break
}
}
// if the arg was not used in any parsed values, then we add it to the slice
// of arguments not used
if !foundArgUsed {
argsNotUsed = append(argsNotUsed, arg)
}
}
return argsNotUsed
}
// ShowVersionAndExit shows the version of this parser
@@ -105,7 +188,8 @@ func (p *Parser) ShowHelpWithMessage(message string) {
}
}
// Disable show version with --version. It is enabled by default.
// DisableShowVersionWithVersion disables the showing of version information
// with --version. It is enabled by default.
func (p *Parser) DisableShowVersionWithVersion() {
p.ShowVersionWithVersionFlag = false
}
+86 -23
View File
@@ -23,15 +23,20 @@ type Subcommand struct {
Subcommands []*Subcommand
Flags []*Flag
PositionalFlags []*PositionalValue
AdditionalHelpPrepend string // additional prepended message when Help is displayed
AdditionalHelpAppend string // additional appended message when Help is displayed
Used bool // indicates this subcommand was found and parsed
Hidden bool // indicates this subcommand should be hidden from help
ParsedValues []parsedValue // a list of values and positionals parsed
AdditionalHelpPrepend string // additional prepended message when Help is displayed
AdditionalHelpAppend string // additional appended message when Help is displayed
Used bool // indicates this subcommand was found and parsed
Hidden bool // indicates this subcommand should be hidden from help
}
// NewSubcommand creates a new subcommand that can have flags or PositionalFlags
// added to it. The position starts with 1, not 0
func NewSubcommand(name string) *Subcommand {
if len(name) == 0 {
fmt.Fprintln(os.Stderr, "Error creating subcommand (NewSubcommand()). No subcommand name was specified.")
exitOrPanic(2)
}
newSC := &Subcommand{
Name: name,
}
@@ -39,10 +44,11 @@ func NewSubcommand(name string) *Subcommand {
}
// parseAllFlagsFromArgs parses the non-positional flags such as -f or -v=value
// out of the supplied args and returns the positional items in order.
// out of the supplied args and returns the resulting positional items in order,
// all the flag names found (without values), a bool to indicate if help was
// requested, and any errors found during parsing
func (sc *Subcommand) parseAllFlagsFromArgs(p *Parser, args []string) ([]string, bool, error) {
var err error
var positionalOnlyArguments []string
var helpRequested bool // indicates the user has supplied -h and we
// should render help if we are the last subcommand
@@ -58,7 +64,7 @@ func (sc *Subcommand) parseAllFlagsFromArgs(p *Parser, args []string) ([]string,
// find all the normal flags (not positional) and parse them out
for i, a := range args {
debugPrint("parsing arg", 1, a)
debugPrint("parsing arg:", a)
// evaluate if there is a following arg to avoid panics
var nextArgExists bool
@@ -121,62 +127,107 @@ func (sc *Subcommand) parseAllFlagsFromArgs(p *Parser, args []string) ([]string,
// this positional argument into a slice of their own, so that
// we can determine if its a subcommand or positional value later
positionalOnlyArguments = append(positionalOnlyArguments, a)
case argIsFlagWithSpace:
// track this as a parsed value with the subcommand
sc.addParsedPositionalValue(a)
case argIsFlagWithSpace: // a flag with a space. ex) -k v or --key value
a = parseFlagToName(a)
// debugPrint("Arg", i, "is flag with space:", a)
// parse next arg as value to this flag and apply to subcommand flags
// if the flag is a bool flag, then we check for a following positional
// and skip it if necessary
if flagIsBool(sc, p, a) {
debugPrint(sc.Name, "bool flag", a, "next var is:", nextArg)
_, err = setValueForParsers(a, "true", p, sc)
// set the value in this subcommand and its root parser
valueSet, err := setValueForParsers(a, "true", p, sc)
// if an error occurs, just return it and quit parsing
if err != nil {
return []string{}, false, err
}
// by default, we just assign the next argument to the value and continue
// log all values parsed by this subcommand. We leave the value blank
// because the bool value had no explicit true or false supplied
if valueSet {
sc.addParsedFlag(a, "")
}
// we've found and set a standalone bool flag, so we move on to the next
// argument in the list of arguments
continue
}
skipNext = true
debugPrint(sc.Name, "NOT bool flag", a)
// debugPrint(sc.Name, "NOT bool flag", a)
// if the next arg was not found, then show a Help message
if !nextArgExists {
p.ShowHelpWithMessage("Expected a following arg for flag " + a + ", but it did not exist.")
exitOrPanic(2)
}
_, err = setValueForParsers(a, nextArg, p, sc)
valueSet, err := setValueForParsers(a, nextArg, p, sc)
if err != nil {
return []string{}, false, err
}
case argIsFlagWithValue:
// log all parsed values in the subcommand
if valueSet {
sc.addParsedFlag(a, nextArg)
}
case argIsFlagWithValue: // a flag with an equals sign. ex) -k=v or --key=value
// debugPrint("Arg", i, "is flag with value:", a)
a = parseFlagToName(a)
// parse flag into key and value and apply to subcommand flags
key, val := parseArgWithValue(a)
_, err = setValueForParsers(key, val, p, sc)
// set the value in this subcommand and its root parser
valueSet, err := setValueForParsers(key, val, p, sc)
if err != nil {
return []string{}, false, err
}
// if this flag type was found and not set, and the parser is set to show
// Help when an unknown flag is found, then show Help and exit.
}
// log all values parsed by the subcommand
if valueSet {
sc.addParsedFlag(a, val)
}
}
}
return positionalOnlyArguments, helpRequested, nil
}
// Parse causes the argument parser to parse based on the supplied []string.
// depth specifies the non-flag subcommand positional depth
// findAllParsedValues finds all values parsed by all subcommands and this
// subcommand and its child subcommands
func (sc *Subcommand) findAllParsedValues() []parsedValue {
parsedValues := sc.ParsedValues
for _, sc := range sc.Subcommands {
// skip unused subcommands
if !sc.Used {
continue
}
parsedValues = append(parsedValues, sc.findAllParsedValues()...)
}
return parsedValues
}
// parse causes the argument parser to parse based on the supplied []string.
// depth specifies the non-flag subcommand positional depth. A slice of flags
// and subcommands parsed is returned so that the parser can ultimately decide
// if there were any unexpected values supplied by the user
func (sc *Subcommand) parse(p *Parser, args []string, depth int) error {
debugPrint("- Parsing subcommand", sc.Name, "with depth of", depth, "and args", args)
// if a command is parsed, its used
sc.Used = true
debugPrint("used subcommand", sc.Name, sc.ShortName)
if len(sc.Name) > 0 {
sc.addParsedPositionalValue(sc.Name)
}
if len(sc.ShortName) > 0 {
sc.addParsedPositionalValue(sc.ShortName)
}
// as subcommands are used, they become the context of the parser. This helps
// us understand how to display help based on which subcommand is being used
@@ -191,9 +242,10 @@ func (sc *Subcommand) parse(p *Parser, args []string, depth int) error {
sc.ensureNoConflictWithBuiltinVersion()
}
// Parse the normal flags out of the argument list and retain the positionals.
// Apply the flags to the parent parser and the current subcommand context.
// ./command -f -z subcommand someVar -b becomes ./command subcommand somevar
// Parse the normal flags out of the argument list and return the positionals
// (subcommands and positional values), along with the flags used.
// Then the flag values are applied to the parent parser and the current
// subcommand being parsed.
positionalOnlyArguments, helpRequested, err := sc.parseAllFlagsFromArgs(p, args)
if err != nil {
return err
@@ -292,7 +344,7 @@ func (sc *Subcommand) parse(p *Parser, args []string, depth int) error {
}
// find any positionals that were not used on subcommands that were
// found and throw help (unknown argument)
// found and throw help (unknown argument) in the global parse or subcommand
for _, pv := range p.PositionalFlags {
if pv.Required && !pv.Found {
p.ShowHelpWithMessage("Required global positional variable " + pv.Name + " not found at position " + strconv.Itoa(pv.Position))
@@ -309,6 +361,17 @@ func (sc *Subcommand) parse(p *Parser, args []string, depth int) error {
return nil
}
// addParsedFlag makes it easy to append flag values parsed by the subcommand
func (sc *Subcommand) addParsedFlag(key string, value string) {
sc.ParsedValues = append(sc.ParsedValues, newParsedValue(key, value, false))
}
// addParsedPositionalValue makes it easy to append positionals parsed by the
// subcommand
func (sc *Subcommand) addParsedPositionalValue(value string) {
sc.ParsedValues = append(sc.ParsedValues, newParsedValue("", value, true))
}
// FlagExists lets you know if the flag name exists as either a short or long
// name in the (sub)command
func (sc *Subcommand) FlagExists(name string) bool {
+6 -5
View File
@@ -5,8 +5,9 @@
package gocui
import (
"github.com/go-errors/errors"
"strconv"
"github.com/go-errors/errors"
)
type escapeInterpreter struct {
@@ -151,13 +152,13 @@ func (ei *escapeInterpreter) outputNormal() error {
switch {
case p >= 30 && p <= 37:
ei.curFgColor = Attribute(p - 30 + 1)
ei.curFgColor |= Attribute(p - 30 + 1)
case p == 39:
ei.curFgColor = ColorDefault
ei.curFgColor |= ColorDefault
case p >= 40 && p <= 47:
ei.curBgColor = Attribute(p - 40 + 1)
ei.curBgColor |= Attribute(p - 40 + 1)
case p == 49:
ei.curBgColor = ColorDefault
ei.curBgColor |= ColorDefault
case p == 1:
ei.curFgColor |= AttrBold
case p == 4:
+85 -10
View File
@@ -6,7 +6,9 @@ package gocui
import (
standardErrors "errors"
"fmt"
"strings"
"sync"
"time"
"github.com/go-errors/errors"
@@ -89,6 +91,16 @@ type Gui struct {
// SupportOverlaps is true when we allow for view edges to overlap with other
// view edges
SupportOverlaps bool
// tickingMutex ensures we don't have two loops ticking. The point of 'ticking'
// is to refresh the gui rapidly so that loader characters can be animated.
tickingMutex sync.Mutex
OnSearchEscape func() error
// these keys must either be of type Key of rune
SearchEscapeKey interface{}
NextSearchMatchKey interface{}
PrevSearchMatchKey interface{}
}
// NewGui returns a new Gui object with a given output mode.
@@ -119,15 +131,18 @@ func NewGui(mode OutputMode, supportOverlaps bool) (*Gui, error) {
// view edges
g.SupportOverlaps = supportOverlaps
// default keys for when searching strings in a view
g.SearchEscapeKey = KeyEsc
g.NextSearchMatchKey = 'n'
g.PrevSearchMatchKey = 'N'
return g, nil
}
// Close finalizes the library. It should be called after a successful
// initialization and when gocui is not needed anymore.
func (g *Gui) Close() {
go func() {
g.stop <- struct{}{}
}()
close(g.stop)
termbox.Close()
}
@@ -409,7 +424,6 @@ func (g *Gui) SetManagerFunc(manager func(*Gui) error) {
// MainLoop runs the main loop until an error is returned. A successful
// finish should return ErrQuit.
func (g *Gui) MainLoop() error {
g.loaderTick()
if err := g.flush(); err != nil {
return err
}
@@ -529,6 +543,11 @@ func (g *Gui) flush() error {
return err
}
}
if v.ContainsList {
if err := g.drawListFooter(v, fgColor, bgColor); err != nil {
return err
}
}
}
if err := g.draw(v); err != nil {
return err
@@ -716,6 +735,34 @@ func (g *Gui) drawSubtitle(v *View, fgColor, bgColor Attribute) error {
return nil
}
// drawListFooter draws the footer of a list view, showing something like '1 of 10'
func (g *Gui) drawListFooter(v *View, fgColor, bgColor Attribute) error {
if len(v.lines) == 0 {
return nil
}
message := fmt.Sprintf("%d of %d", v.cy+v.oy+1, len(v.lines))
if v.y1 < 0 || v.y1 >= g.maxY {
return nil
}
start := v.x1 - 1 - len(message)
if start < v.x0 {
return nil
}
for i, ch := range message {
x := start + i
if x >= v.x1 {
break
}
if err := g.SetRune(x, v.y1, ch, fgColor, bgColor); err != nil {
return err
}
}
return nil
}
// draw manages the cursor and calls the draw function of a view.
func (g *Gui) draw(v *View) error {
if g.Cursor {
@@ -801,6 +848,23 @@ func (g *Gui) execKeybindings(v *View, ev *termbox.Event) (matched bool, err err
var globalKb *keybinding
var matchingParentViewKb *keybinding
// if we're searching, and we've hit n/N/Esc, we ignore the default keybinding
if v.IsSearching() && Modifier(ev.Mod) == ModNone {
if eventMatchesKey(ev, g.NextSearchMatchKey) {
return true, v.gotoNextMatch()
} else if eventMatchesKey(ev, g.PrevSearchMatchKey) {
return true, v.gotoPreviousMatch()
} else if eventMatchesKey(ev, g.SearchEscapeKey) {
v.searcher.clearSearch()
if g.OnSearchEscape != nil {
if err := g.OnSearchEscape(); err != nil {
return true, err
}
}
return true, nil
}
}
for _, kb := range g.keybindings {
if kb.handler == nil {
continue
@@ -835,14 +899,25 @@ func (g *Gui) execKeybinding(v *View, kb *keybinding) (bool, error) {
return true, nil
}
func (g *Gui) loaderTick() {
func (g *Gui) StartTicking() {
go func() {
for range time.Tick(time.Millisecond * 50) {
for _, view := range g.Views() {
if view.HasLoader {
g.userEvents <- userEvent{func(g *Gui) error { return nil }}
break
g.tickingMutex.Lock()
defer g.tickingMutex.Unlock()
ticker := time.NewTicker(time.Millisecond * 50)
defer ticker.Stop()
outer:
for {
select {
case <-ticker.C:
for _, view := range g.Views() {
if view.HasLoader {
g.userEvents <- userEvent{func(g *Gui) error { return nil }}
continue outer
}
}
return
case <-g.stop:
return
}
}
}()
+14
View File
@@ -29,6 +29,20 @@ func newKeybinding(viewname string, contexts []string, key Key, ch rune, mod Mod
return kb
}
func eventMatchesKey(ev *termbox.Event, key interface{}) bool {
// assuming ModNone for now
if Modifier(ev.Mod) != ModNone {
return false
}
k, ch, err := getKey(key)
if err != nil {
return false
}
return k == Key(ev.Key) && ch == ev.Ch
}
// matchKeypress returns if the keybinding matches the keypress.
func (kb *keybinding) matchKeypress(key Key, ch rune, mod Modifier) bool {
return kb.key == key && kb.ch == ch && kb.mod == mod
+194 -9
View File
@@ -105,6 +105,140 @@ type View struct {
ParentView *View
Context string // this is for assigning keybindings to a view only in certain contexts
searcher *searcher
// when ContainsList is true, we show the current index and total count in the view
ContainsList bool
}
type searcher struct {
searchString string
searchPositions []cellPos
currentSearchIndex int
onSelectItem func(int, int, int) error
}
func (v *View) SetOnSelectItem(onSelectItem func(int, int, int) error) {
v.searcher.onSelectItem = onSelectItem
}
func (v *View) gotoNextMatch() error {
if len(v.searcher.searchPositions) == 0 {
return nil
}
if v.searcher.currentSearchIndex == len(v.searcher.searchPositions)-1 {
v.searcher.currentSearchIndex = 0
} else {
v.searcher.currentSearchIndex++
}
return v.SelectSearchResult(v.searcher.currentSearchIndex)
}
func (v *View) gotoPreviousMatch() error {
if len(v.searcher.searchPositions) == 0 {
return nil
}
if v.searcher.currentSearchIndex == 0 {
if len(v.searcher.searchPositions) > 0 {
v.searcher.currentSearchIndex = len(v.searcher.searchPositions) - 1
}
} else {
v.searcher.currentSearchIndex--
}
return v.SelectSearchResult(v.searcher.currentSearchIndex)
}
func (v *View) SelectSearchResult(index int) error {
y := v.searcher.searchPositions[index].y
v.FocusPoint(0, y)
if v.searcher.onSelectItem != nil {
return v.searcher.onSelectItem(y, index, len(v.searcher.searchPositions))
}
return nil
}
func (v *View) Search(str string) error {
v.writeMutex.Lock()
defer v.writeMutex.Unlock()
v.searcher.search(str)
v.updateSearchPositions()
if len(v.searcher.searchPositions) > 0 {
// get the first result past the current cursor
currentIndex := 0
adjustedY := v.oy + v.cy
adjustedX := v.ox + v.cx
for i, pos := range v.searcher.searchPositions {
if pos.y > adjustedY || (pos.y == adjustedY && pos.x > adjustedX) {
currentIndex = i
break
}
}
v.searcher.currentSearchIndex = currentIndex
return v.SelectSearchResult(currentIndex)
} else {
return v.searcher.onSelectItem(-1, -1, 0)
}
return nil
}
func (v *View) ClearSearch() {
v.searcher.clearSearch()
}
func (v *View) IsSearching() bool {
return v.searcher.searchString != ""
}
func (v *View) FocusPoint(cx int, cy int) {
lineCount := len(v.lines)
if cy < 0 || cy > lineCount {
return
}
_, height := v.Size()
ly := height - 1
if ly == -1 {
ly = 0
}
// 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 > lineCount {
v.cx = cx
v.cy = cy
v.oy = 0
} else if cy < v.oy {
v.cx = cx
v.cy = 0
v.oy = cy
} else if cy > v.oy+ly {
v.cx = cx
v.cy = ly
v.oy = cy - ly
} else {
v.cx = cx
v.cy = cy - v.oy
}
}
func (s *searcher) search(str string) {
s.searchString = str
s.searchPositions = []cellPos{}
s.currentSearchIndex = 0
}
func (s *searcher) clearSearch() {
s.searchString = ""
s.searchPositions = []cellPos{}
s.currentSearchIndex = 0
}
type cellPos struct {
x int
y int
}
type viewLine struct {
@@ -131,15 +265,16 @@ func (l lineType) String() string {
// newView returns a new View object.
func newView(name string, x0, y0, x1, y1 int, mode OutputMode) *View {
v := &View{
name: name,
x0: x0,
y0: y0,
x1: x1,
y1: y1,
Frame: true,
Editor: DefaultEditor,
tainted: true,
ei: newEscapeInterpreter(mode),
name: name,
x0: x0,
y0: y0,
x1: x1,
y1: y1,
Frame: true,
Editor: DefaultEditor,
tainted: true,
ei: newEscapeInterpreter(mode),
searcher: &searcher{},
}
return v
}
@@ -331,8 +466,35 @@ func (v *View) Rewind() {
v.readOffset = 0
}
func (v *View) updateSearchPositions() {
if v.searcher.searchString != "" {
v.searcher.searchPositions = []cellPos{}
for y, line := range v.lines {
lineLoop:
for x, _ := range line {
if line[x].chr == rune(v.searcher.searchString[0]) {
for offset := 1; offset < len(v.searcher.searchString); offset++ {
if len(line)-1 < x+offset {
continue lineLoop
}
if line[x+offset].chr != rune(v.searcher.searchString[offset]) {
continue lineLoop
}
}
v.searcher.searchPositions = append(v.searcher.searchPositions, cellPos{x: x, y: y})
}
}
}
}
}
// draw re-draws the view's contents.
func (v *View) draw() error {
v.writeMutex.Lock()
defer v.writeMutex.Unlock()
v.updateSearchPositions()
maxX, maxY := v.Size()
if v.Wrap {
@@ -392,6 +554,13 @@ func (v *View) draw() error {
if bgColor == ColorDefault {
bgColor = v.BgColor
}
if matched, selected := v.isPatternMatchedRune(x, y); matched {
if selected {
bgColor = ColorCyan
} else {
bgColor = ColorYellow
}
}
if err := v.setRune(x, y, c.chr, fgColor, bgColor); err != nil {
return err
@@ -403,6 +572,18 @@ func (v *View) draw() error {
return nil
}
func (v *View) isPatternMatchedRune(x, y int) (bool, bool) {
searchStringLength := len(v.searcher.searchString)
for i, pos := range v.searcher.searchPositions {
adjustedY := y + v.oy
adjustedX := x + v.ox
if adjustedY == pos.y && adjustedX >= pos.x && adjustedX < pos.x+searchStringLength {
return true, i == v.searcher.currentSearchIndex
}
}
return false, false
}
// realPosition returns the position in the internal buffer corresponding to the
// point (x, y) of the view.
func (v *View) realPosition(vx, vy int) (x, y int, err error) {
@@ -458,6 +639,8 @@ func (v *View) clearRunes() {
// BufferLines returns the lines in the view's internal
// buffer.
func (v *View) BufferLines() []string {
v.writeMutex.Lock()
defer v.writeMutex.Unlock()
lines := make([]string, len(v.lines))
for i, l := range v.lines {
str := lineType(l).String()
@@ -476,6 +659,8 @@ func (v *View) Buffer() string {
// ViewBufferLines returns the lines in the view's internal
// buffer that is shown to the user.
func (v *View) ViewBufferLines() []string {
v.writeMutex.Lock()
defer v.writeMutex.Unlock()
lines := make([]string, len(v.viewLines))
for i, l := range v.viewLines {
str := lineType(l.line).String()
-10
View File
@@ -1,10 +0,0 @@
// +build ignore
package pty
import "C"
type (
_C_int C.int
_C_uint C.uint
)
-17
View File
@@ -1,17 +0,0 @@
// +build ignore
package pty
/*
#define _KERNEL
#include <sys/conf.h>
#include <sys/param.h>
#include <sys/filio.h>
*/
import "C"
const (
_C_SPECNAMELEN = C.SPECNAMELEN /* max length of devicename */
)
type fiodgnameArg C.struct_fiodname_args
-15
View File
@@ -1,15 +0,0 @@
// +build ignore
package pty
/*
#include <sys/param.h>
#include <sys/filio.h>
*/
import "C"
const (
_C_SPECNAMELEN = C.SPECNAMELEN /* max length of devicename */
)
type fiodgnameArg C.struct_fiodgname_arg
-14
View File
@@ -1,14 +0,0 @@
// +build ignore
package pty
/*
#include <sys/time.h>
#include <stdlib.h>
#include <sys/tty.h>
*/
import "C"
type ptmget C.struct_ptmget
var ioctl_PTMGET = C.PTMGET
-1
View File
@@ -1 +0,0 @@
rollbar.test
-24
View File
@@ -1,24 +0,0 @@
0.2.0 - May 22nd, 2016
====================
* Do not use title to determine fingerprint.
0.1.1 - August 24th, 2016
=========================
* Fix Go 1.6 support by removing call to runtime.CallersFrames, which was added
in Go 1.7.
0.1.0 - August 23rd, 2016
=========================
* Allow passing in arbitrary function pointer stacks. (thanks @apg!)
* Remove unneeded exported constants.
* Make HTTP(S) endpoint configurable.
* Remove unneeded debug print statement.
0.0.1 - January 19th, 2015
==========================
* Initial release based on https://github.com/stvp/rollbar
-22
View File
@@ -1,22 +0,0 @@
Copyright (c) 2016 Stovepipe Studios
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-53
View File
@@ -1,53 +0,0 @@
roll
----
`roll` is a basic Rollbar client for Go that reports errors and logs
messages. It automatically builds stack traces and also supports
arbitrary traces. All errors and messages are sent to Rollbar
synchronously.
`roll` is intentionally simple. For more advanced functionality, check
out [heroku/rollbar](https://github.com/heroku/rollbar).
[API docs on godoc.org](http://godoc.org/github.com/stvp/roll)
Notes
=====
* Critical-, Error-, and Warning-level messages include a stack trace.
However, Go's `error` type doesn't include stack information from the
location the error was set or allocated. Instead, `roll` uses the
stack information from where the error was reported.
* Info- and Debug-level Rollbar messages do not include stack traces.
* When calling `roll` away from where the error actually occurred,
`roll`'s stack walking won't represent the actual stack trace at the
time the error occurred. The `*Stack` variants of Critical, Error, and
Warning take a `[]uintptr`, allowing the stack to be provided, rather
than walked.
Running Tests
=============
`go test` will run tests against a fake server by default.
If the environment variable `TOKEN` is a Rollbar access token, running
`go test` will produce errors using an environment named `test`.
TOKEN=f0df01587b8f76b2c217af34c479f9ea go test
Verify the reported errors manually in the Rollbar dashboard.
Contributors
============
* @challiwill
* @tysonmote
* @apg
This library was forked from [stvp/rollbar](https://github.com/stvp/rollbar),
which had contributions from:
* @kjk
* @Soulou
* @paulmach
-242
View File
@@ -1,242 +0,0 @@
package roll
import (
"bytes"
"encoding/json"
"fmt"
"hash/adler32"
"io"
"io/ioutil"
"net/http"
"os"
"reflect"
"runtime"
"strings"
"time"
)
const (
// By default, all Rollbar API requests are sent to this endpoint.
endpoint = "https://api.rollbar.com/api/1/item/"
// Identify this Rollbar client library to the Rollbar API.
clientName = "go-roll"
clientVersion = "0.2.0"
clientLanguage = "go"
)
var (
// Endpoint is the default HTTP(S) endpoint that all Rollbar API requests
// will be sent to. By default, this is Rollbar's "Items" API endpoint. If
// this is blank, no items will be sent to Rollbar.
Endpoint = endpoint
// Rollbar access token for the global client. If this is blank, no items
// will be sent to Rollbar.
Token = ""
// Environment for all items reported with the global client.
Environment = "development"
)
type rollbarSuccess struct {
Result map[string]string `json:"result"`
}
// Client reports items to a single Rollbar project.
type Client interface {
Critical(err error, custom map[string]string) (uuid string, e error)
CriticalStack(err error, ptrs []uintptr, custom map[string]string) (uuid string, e error)
Error(err error, custom map[string]string) (uuid string, e error)
ErrorStack(err error, ptrs []uintptr, custom map[string]string) (uuid string, e error)
Warning(err error, custom map[string]string) (uuid string, e error)
WarningStack(err error, ptrs []uintptr, custom map[string]string) (uuid string, e error)
Info(msg string, custom map[string]string) (uuid string, e error)
Debug(msg string, custom map[string]string) (uuid string, e error)
}
type rollbarClient struct {
token string
env string
}
// New creates a new Rollbar client that reports items to the given project
// token and with the given environment (eg. "production", "development", etc).
func New(token, env string) Client {
return &rollbarClient{token, env}
}
func Critical(err error, custom map[string]string) (uuid string, e error) {
return CriticalStack(err, getCallers(2), custom)
}
func CriticalStack(err error, ptrs []uintptr, custom map[string]string) (uuid string, e error) {
return New(Token, Environment).CriticalStack(err, ptrs, custom)
}
func Error(err error, custom map[string]string) (uuid string, e error) {
return ErrorStack(err, getCallers(2), custom)
}
func ErrorStack(err error, ptrs []uintptr, custom map[string]string) (uuid string, e error) {
return New(Token, Environment).ErrorStack(err, ptrs, custom)
}
func Warning(err error, custom map[string]string) (uuid string, e error) {
return WarningStack(err, getCallers(2), custom)
}
func WarningStack(err error, ptrs []uintptr, custom map[string]string) (uuid string, e error) {
return New(Token, Environment).WarningStack(err, ptrs, custom)
}
func Info(msg string, custom map[string]string) (uuid string, e error) {
return New(Token, Environment).Info(msg, custom)
}
func Debug(msg string, custom map[string]string) (uuid string, e error) {
return New(Token, Environment).Debug(msg, custom)
}
func (c *rollbarClient) Critical(err error, custom map[string]string) (uuid string, e error) {
return c.CriticalStack(err, getCallers(2), custom)
}
func (c *rollbarClient) CriticalStack(err error, callers []uintptr, custom map[string]string) (uuid string, e error) {
item := c.buildTraceItem("critical", err, callers, custom)
return c.send(item)
}
func (c *rollbarClient) Error(err error, custom map[string]string) (uuid string, e error) {
return c.ErrorStack(err, getCallers(2), custom)
}
func (c *rollbarClient) ErrorStack(err error, callers []uintptr, custom map[string]string) (uuid string, e error) {
item := c.buildTraceItem("error", err, callers, custom)
return c.send(item)
}
func (c *rollbarClient) Warning(err error, custom map[string]string) (uuid string, e error) {
return c.WarningStack(err, getCallers(2), custom)
}
func (c *rollbarClient) WarningStack(err error, callers []uintptr, custom map[string]string) (uuid string, e error) {
item := c.buildTraceItem("warning", err, callers, custom)
return c.send(item)
}
func (c *rollbarClient) Info(msg string, custom map[string]string) (uuid string, e error) {
item := c.buildMessageItem("info", msg, custom)
return c.send(item)
}
func (c *rollbarClient) Debug(msg string, custom map[string]string) (uuid string, e error) {
item := c.buildMessageItem("debug", msg, custom)
return c.send(item)
}
func (c *rollbarClient) buildTraceItem(level string, err error, callers []uintptr, custom map[string]string) (item map[string]interface{}) {
stack := buildRollbarFrames(callers)
item = c.buildItem(level, err.Error(), custom)
itemData := item["data"].(map[string]interface{})
itemData["fingerprint"] = stack.fingerprint()
itemData["body"] = map[string]interface{}{
"trace": map[string]interface{}{
"frames": stack,
"exception": map[string]interface{}{
"class": errorClass(err),
"message": err.Error(),
},
},
}
return item
}
func (c *rollbarClient) buildMessageItem(level string, msg string, custom map[string]string) (item map[string]interface{}) {
item = c.buildItem(level, msg, custom)
itemData := item["data"].(map[string]interface{})
itemData["body"] = map[string]interface{}{
"message": map[string]interface{}{
"body": msg,
},
}
return item
}
func (c *rollbarClient) buildItem(level, title string, custom map[string]string) map[string]interface{} {
hostname, _ := os.Hostname()
return map[string]interface{}{
"access_token": c.token,
"data": map[string]interface{}{
"environment": c.env,
"title": title,
"level": level,
"timestamp": time.Now().Unix(),
"platform": runtime.GOOS,
"language": clientLanguage,
"server": map[string]interface{}{
"host": hostname,
},
"notifier": map[string]interface{}{
"name": clientName,
"version": clientVersion,
},
"custom": custom,
},
}
}
// send reports the given item to Rollbar and returns either a UUID for the
// reported item or an error.
func (c *rollbarClient) send(item map[string]interface{}) (uuid string, err error) {
if len(c.token) == 0 || len(Endpoint) == 0 {
return "", nil
}
jsonBody, err := json.Marshal(item)
if err != nil {
return "", err
}
resp, err := http.Post(Endpoint, "application/json", bytes.NewReader(jsonBody))
if err != nil {
// If something goes wrong it really does not matter
return "", nil
}
defer func() {
io.Copy(ioutil.Discard, resp.Body)
resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK {
// If something goes wrong it really does not matter
return "", nil
}
// Extract UUID from JSON response
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", nil
}
success := rollbarSuccess{}
json.Unmarshal(body, &success)
return success.Result["uuid"], nil
}
// errorClass returns a class name for an error (eg. "ErrUnexpectedEOF"). For
// string errors, it returns an Adler-32 checksum of the error string.
func errorClass(err error) string {
class := reflect.TypeOf(err).String()
if class == "" {
return "panic"
} else if class == "*errors.errorString" {
checksum := adler32.Checksum([]byte(err.Error()))
return fmt.Sprintf("{%x}", checksum)
} else {
return strings.TrimPrefix(class, "*")
}
}

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