Compare commits

...

128 Commits

Author SHA1 Message Date
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
Jesse Duffield
339e1b5dcf lenient sorting of tags on startup 2019-11-26 21:39:40 +11:00
Jesse Duffield
7113ed73d4 support older versions of git when getting remote branches 2019-11-26 21:36:07 +11:00
265 changed files with 47767 additions and 47237 deletions

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

View File

@@ -46,11 +46,7 @@ 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 +54,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
@@ -91,6 +77,15 @@ and the git version which builds from the most recent commit.
Instruction of how to install AUR content can be found here:
https://wiki.archlinux.org/index.php/Arch_User_Repository
### 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

View File

@@ -1,5 +1,10 @@
# User Config:
Default path for the config file:
* Linux: `~/.config/jesseduffield/lazygit/config.yml`
* MacOS: `~/Library/Application Support/jesseduffield/lazygit/config.yml`
## Default:
```yaml
@@ -19,6 +24,7 @@
commitLength:
show: true
mouseEvents: true
skipUnstageLineWarning: false
git:
merging:
# only applicable to unix users
@@ -30,6 +36,98 @@
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
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: 'X'
createRebaseOptionsMenu: 'm'
pushFiles: 'P'
pullFiles: 'p'
refresh: 'R'
createPatchOptionsMenu: '<c-p>'
nextTab: ']'
prevTab: '['
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:
@@ -100,3 +198,33 @@ If you have issues with a light terminal theme where you can't read / see the te
## 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'
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'
```

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 |

View File

@@ -4,6 +4,7 @@
<pre>
<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

29
go.mod
View File

@@ -10,30 +10,35 @@ 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.20200201013258-57fdcf23edc5
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/rollrus v0.0.0-20190701125922-dd028cb0bfd7 // 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
)

72
go.sum
View File

@@ -63,6 +63,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,12 +73,20 @@ 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.20200112025325-6c933915c351 h1:+sSqd2YotacWt+1MNRN8ZmXnYoiJeblZeptzKiHIyv0=
github.com/jesseduffield/gocui v0.3.1-0.20200112025325-6c933915c351/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20200131125953-f679540a7039 h1:CVhilJ8ZdN7GmAI+fbH9829Cp/8hbK7Lijbd4VaNgo0=
github.com/jesseduffield/gocui v0.3.1-0.20200131125953-f679540a7039/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20200131131454-a319843434ac h1:vp7I0RpFq4L46nFA9QQokzhFgr68LRGtwDO9xfq4F+A=
github.com/jesseduffield/gocui v0.3.1-0.20200131131454-a319843434ac/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20200201013258-57fdcf23edc5 h1:tE0w3tuL/bj1o5VMhjjE0ep6i7Fva+RYjKcMFcniJEY=
github.com/jesseduffield/gocui v0.3.1-0.20200201013258-57fdcf23edc5/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=
@@ -85,8 +95,12 @@ github.com/jesseduffield/rollrus v0.0.0-20190701125922-dd028cb0bfd7 h1:CRD7bVjlG
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=
@@ -96,6 +110,8 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW
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 +124,12 @@ 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.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
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 +138,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=
@@ -131,6 +149,8 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J
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=
@@ -154,18 +174,28 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
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/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 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
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=
@@ -197,8 +227,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
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=
@@ -212,8 +242,8 @@ golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR
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=
@@ -232,10 +262,9 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w
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 +273,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=
@@ -258,6 +288,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR
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 +305,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=

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(),

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 {

View File

@@ -44,6 +44,8 @@ func (c *Commit) GetDisplayStrings(isFocused bool) []string {
shaColor = green
case "rebasing":
shaColor = blue
case "reflog":
shaColor = blue
case "selected":
shaColor = magenta
default:
@@ -62,5 +64,5 @@ func (c *Commit) GetDisplayStrings(isFocused bool) []string {
tagString = utils.ColoredString(strings.Join(c.Tags, " "), color.FgMagenta) + " "
}
return []string{shaColor.Sprint(c.Sha), actionString + tagString + defaultColor.Sprint(c.Name)}
return []string{shaColor.Sprint(c.Sha[:8]), actionString + tagString + defaultColor.Sprint(c.Name)}
}

View File

@@ -46,7 +46,7 @@ func NewCommitListBuilder(log *logrus.Entry, gitCommand *GitCommand, osCommand *
}
// 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,7 +65,7 @@ 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) {
@@ -190,7 +190,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 +207,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,
@@ -281,12 +281,14 @@ 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 %s --abbrev=%d", limitFlag, 20))
if err != nil {
// assume if there is an error there are no commits yet for this branch
return ""

View File

@@ -163,7 +163,7 @@ func TestCommitListBuilderGetLog(t *testing.T) {
"Retrieves logs",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
assert.EqualValues(t, []string{"log", "--oneline", "-30", "--abbrev=20"}, args)
return exec.Command("echo", "6f0b32f commands/git : add GetCommits tests refactor\n9d9d775 circle : remove new line")
},
@@ -175,7 +175,7 @@ func TestCommitListBuilderGetLog(t *testing.T) {
"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)
assert.EqualValues(t, []string{"log", "--oneline", "-30", "--abbrev=20"}, args)
return exec.Command("test")
},
func(output string) {
@@ -188,7 +188,7 @@ func TestCommitListBuilderGetLog(t *testing.T) {
t.Run(s.testName, func(t *testing.T) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.getLog())
s.test(c.getLog(true))
})
}
}
@@ -212,7 +212,7 @@ func TestCommitListBuilderGetCommits(t *testing.T) {
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)
assert.EqualValues(t, []string{"log", "--oneline", "-30", "--abbrev=20"}, args)
return exec.Command("echo")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
@@ -239,7 +239,7 @@ func TestCommitListBuilderGetCommits(t *testing.T) {
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)
assert.EqualValues(t, []string{"log", "--oneline", "-30", "--abbrev=20"}, args)
return exec.Command("echo", "8a2bb0e commit 1\n78976bc commit 2")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
@@ -280,7 +280,7 @@ func TestCommitListBuilderGetCommits(t *testing.T) {
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)
assert.EqualValues(t, []string{"log", "--oneline", "-30", "--abbrev=20"}, args)
return exec.Command("echo", "8a2bb0e commit 1\n78976bc commit 2")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
@@ -312,7 +312,7 @@ func TestCommitListBuilderGetCommits(t *testing.T) {
t.Run(s.testName, func(t *testing.T) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.GetCommits())
s.test(c.GetCommits(true))
})
}
}

View File

@@ -176,8 +176,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 stash@{%d}", index)
}
// GetStatusFiles git status files
@@ -191,10 +191,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 +265,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 +404,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 +425,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 +520,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 +543,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 +557,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 --no-renames %s", 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 --abbrev-commit --decorate --date=relative --pretty=medium %s", branchName)
}
// GetRemoteURL returns current repo remote url
@@ -606,6 +583,12 @@ 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"
@@ -614,21 +597,19 @@ func (c *GitCommand) Diff(file *File, plain bool, cached bool) string {
if cached {
cachedArg = "--cached"
}
if !file.Tracked && !file.HasStagedChanges {
if !file.Tracked && !file.HasStagedChanges && !cached {
trackedArg = "--no-index /dev/null"
}
if plain {
colorArg = ""
}
// 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 %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 +628,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 +891,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) {
cmdStr := c.ShowCommitFileCmdStr(commitSha, fileName, plain)
return c.OSCommand.RunCommandWithOutput(cmdStr)
}
func (c *GitCommand) ShowCommitFileCmdStr(commitSha, fileName string, plain bool) string {
colorArg := "--color"
if plain {
colorArg = ""
}
return c.OSCommand.RunCommandWithOutput("git show --no-renames %s %s -- %s", colorArg, commitSha, fileName)
return fmt.Sprintf("git show --no-renames %s %s -- %s", colorArg, commitSha, fileName)
}
// CheckoutFile checks out the file for the given commit
@@ -956,19 +945,24 @@ 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
@@ -1098,10 +1092,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 +1099,32 @@ 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")
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
}

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", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium", "test"}, args)
return exec.Command("echo")
}
@@ -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

View File

@@ -9,7 +9,7 @@ import (
func (c *GitCommand) GetRemotes() ([]*Remote, error) {
// get remote branches
unescaped := "git for-each-ref --format='%(refname:strip=2)' refs/remotes"
unescaped := "git branch -r"
remoteBranchesStr, err := c.OSCommand.RunCommandWithOutput(unescaped)
if err != nil {
return nil, err
@@ -25,7 +25,7 @@ func (c *GitCommand) GetRemotes() ([]*Remote, error) {
for i, goGitRemote := range goGitRemotes {
remoteName := goGitRemote.Config().Name
re := regexp.MustCompile(fmt.Sprintf("%s\\/(.*)", remoteName))
re := regexp.MustCompile(fmt.Sprintf(`%s\/([\S]+)`, remoteName))
matches := re.FindAllStringSubmatch(remoteBranchesStr, -1)
branches := make([]*RemoteBranch, len(matches))
for j, match := range matches {

View File

@@ -3,6 +3,7 @@ package commands
import (
"regexp"
"sort"
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
@@ -10,6 +11,14 @@ import (
const semverRegex = `v?((\d+\.?)+)([^\d]?.*)`
func convertToInt(s string) int {
i, err := strconv.Atoi(s)
if err != nil {
return 0
}
return i
}
func (c *GitCommand) GetTags() ([]*Tag, error) {
// get remote branches
remoteBranchesStr, err := c.OSCommand.RunCommandWithOutput(`git tag --list`)
@@ -59,10 +68,10 @@ func (c *GitCommand) GetTags() ([]*Tag, error) {
if len(numbersB) == k {
return false
}
if mustConvertToInt(numbersA[k]) < mustConvertToInt(numbersB[k]) {
if convertToInt(numbersA[k]) < convertToInt(numbersB[k]) {
return true
}
if mustConvertToInt(numbersA[k]) > mustConvertToInt(numbersB[k]) {
if convertToInt(numbersA[k]) > convertToInt(numbersB[k]) {
return false
}
k++

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

View File

@@ -243,6 +243,7 @@ func GetDefaultConfig() []byte {
scrollHeight: 2
scrollPastBottom: true
mouseEvents: true
skipUnstageLineWarning: false
theme:
lightTheme: false
activeBorderColor:
@@ -265,6 +266,98 @@ 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'
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: 'X'
createRebaseOptionsMenu: 'm'
pushFiles: 'P'
pullFiles: 'p'
refresh: 'R'
createPatchOptionsMenu: '<c-p>'
nextTab: ']'
prevTab: '['
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'
`)
}

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 {

View File

@@ -4,7 +4,6 @@ import (
"fmt"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
@@ -37,40 +36,45 @@ 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 {
// here we tell the selected branch that it is selected.
// this is necessary for showing stats on a branch that is selected, because
// the displaystring function doesn't have access to gui state to tell if it's selected
for i, branch := range gui.State.Branches {
branch.Selected = i == gui.State.Panels.Branches.SelectedLine
}
branch := gui.getSelectedBranch()
branch.Pushables, branch.Pullables = gui.GitCommand.GetBranchUpstreamDifferenceCount(branch.Name)
return gui.renderListPanel(gui.getBranchesView(), gui.State.Branches)
branch := gui.getSelectedBranch()
branch.Pushables, branch.Pullables = gui.GitCommand.GetBranchUpstreamDifferenceCount(branch.Name)
select {
case <-stop:
return nil
default:
}
return gui.renderListPanel(gui.getBranchesView(), gui.State.Branches)
})
}
// gui.refreshStatus is called at the end of this because that's when we can
@@ -93,8 +97,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 +111,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 +133,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 +170,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 +203,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 +372,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
}
@@ -422,3 +435,12 @@ 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)
}

View File

@@ -29,7 +29,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 +45,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.newCmdTask("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 {
@@ -190,9 +196,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
}

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()
}

View File

@@ -1,10 +1,8 @@
package gui
import (
"fmt"
"strconv"
"github.com/fatih/color"
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
@@ -37,13 +35,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 +63,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.newCmdTask("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 +97,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 +464,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 +487,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 +558,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 +584,80 @@ 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))
if err := gui.renderListPanel(commitsView, gui.State.Commits); err != nil {
return err
}
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
contextTabIndexMap := map[string]int{
"branch-commits": 0,
"reflog-commits": 1,
}
commitsView.TabIndex = contextTabIndexMap[context]
switch 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)
}

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

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
}

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})
}

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
}

View File

@@ -10,7 +10,6 @@ import (
"fmt"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
@@ -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.newCmdTask("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.newCmdTask("main", cmd); err != nil {
return err
}
return gui.renderString(gui.g, "main", leftContent)
return nil
}
func (gui *Gui) refreshFiles() error {
@@ -177,9 +173,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 +215,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 +238,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 +371,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 +385,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 +406,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 +426,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 +450,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 +475,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 +508,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 +540,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 +548,29 @@ 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}")
}

86
pkg/gui/git_flow.go Normal file
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{})
}

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"
@@ -25,9 +25,11 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/config"
"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"
)
@@ -67,20 +69,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 +131,11 @@ type tagsPanelState struct {
type commitPanelState struct {
SelectedLine int
SpecificDiffMode bool
LimitCommits bool
}
type reflogCommitPanelState struct {
SelectedLine int
}
type stashPanelState struct {
@@ -154,6 +163,7 @@ type panelStates struct {
RemoteBranches *remoteBranchesState
Tags *tagsPanelState
Commits *commitPanelState
ReflogCommits *reflogCommitPanelState
Stash *stashPanelState
Menu *menuPanelState
LineByLine *lineByLinePanelState
@@ -168,6 +178,7 @@ type guiState struct {
Commits []*commands.Commit
StashEntries []*commands.StashEntry
CommitFiles []*commands.CommitFile
ReflogCommits []*commands.Commit
DiffEntries []*commands.Commit
Remotes []*commands.Remote
RemoteBranches []*commands.RemoteBranch
@@ -205,7 +216,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},
@@ -220,14 +232,15 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *comma
}
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()
@@ -252,8 +265,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
}
@@ -330,9 +349,7 @@ func (gui *Gui) onFocusLost(v *gocui.View, newView *gocui.View) error {
}
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 {
@@ -442,10 +459,22 @@ func (gui *Gui) layout(g *gocui.Gui) error {
textColor := theme.GocuiDefaultTextColor
leftSideWidth := width / 3
panelSplitX := width - 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 width < 220 {
mainPanelBottom = height/2 - 1
secondaryPanelTop = mainPanelBottom + 1
secondaryPanelLeft = leftSideWidth + 1
} else {
units := 5
leftSideWidth = width / units
panelSplitX = (1 + ((units - 1) / 2)) * width / units
mainPanelRight = panelSplitX
secondaryPanelLeft = panelSplitX + 1
}
}
main := "main"
@@ -456,7 +485,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, leftSideWidth+panelSpacing, 0, mainPanelRight, mainPanelBottom, gocui.LEFT)
if err != nil {
if err.Error() != "unknown view" {
return err
@@ -464,13 +508,14 @@ 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
if !gui.State.SplitMainPanel {
hiddenViewOffset = 9999
}
secondaryView, err := g.SetView(secondary, panelSplitX+1+hiddenViewOffset, hiddenViewOffset, width-1+hiddenViewOffset, height-2+hiddenViewOffset, gocui.LEFT)
secondaryView, err := g.SetView(secondary, secondaryPanelLeft+hiddenViewOffset, hiddenViewOffset+secondaryPanelTop, width-1+hiddenViewOffset, height-2+hiddenViewOffset, gocui.LEFT)
if err != nil {
if err.Error() != "unknown view" {
return err
@@ -478,6 +523,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 {
@@ -522,6 +568,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
return err
}
commitsView.Title = gui.Tr.SLocalize("CommitsTitle")
commitsView.Tabs = []string{"Commits", "Reflog"}
commitsView.FgColor = textColor
}
@@ -545,7 +592,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
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 +600,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
}
@@ -623,7 +671,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)},
}
@@ -650,11 +699,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 +778,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 +813,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
})
@@ -780,6 +827,9 @@ func (gui *Gui) Run() error {
return err
}
defer g.Close()
gui.stopChan = make(chan struct{})
g.ASCII = runtime.GOOS == "windows" && runewidth.IsEastAsian()
if gui.Config.GetUserConfig().GetBool("gui.mouseEvents") {
g.Mouse = true
@@ -807,8 +857,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 +877,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 +895,6 @@ func (gui *Gui) RunWithSubprocesses() error {
}
}
gui.fileWatcher.Close()
break
} else if err == gui.Errors.ErrSwitchRepo {
continue

File diff suppressed because it is too large Load Diff

View File

@@ -314,3 +314,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
}

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) },

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,14 +44,34 @@ 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
@@ -56,9 +82,10 @@ func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handl
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

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"),
})
}

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{})
}

View File

@@ -87,8 +87,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()

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 {

View File

@@ -7,33 +7,21 @@ 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 {
menuItems[i] = &menuItem{
displayString: option,
onPress: func() error {
return gui.genericMergeCommand(option)
},
}
return gui.genericMergeCommand(command)
}
var title string
@@ -43,7 +31,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 {

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
pkg/gui/reflog_panel.go Normal file
View File

@@ -0,0 +1,103 @@
package gui
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
)
// 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.newCmdTask("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))
if err := gui.renderListPanel(commitsView, gui.State.ReflogCommits); err != nil {
return err
}
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)
}

View File

@@ -2,12 +2,9 @@ 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"
)
// list panel functions
@@ -37,7 +34,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 +42,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
}
@@ -67,8 +65,10 @@ func (gui *Gui) renderRemoteBranchesWithSelection() error {
if err := gui.renderListPanel(branchesView, gui.State.RemoteBranches); err != nil {
return err
}
if err := gui.handleRemoteBranchSelect(gui.g, branchesView); err != nil {
return err
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))
}

View File

@@ -36,13 +36,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 {
@@ -83,8 +83,10 @@ func (gui *Gui) renderRemotesWithSelection() error {
if err := gui.renderListPanel(branchesView, gui.State.Remotes); err != nil {
return err
}
if err := gui.handleRemoteSelect(gui.g, branchesView); err != nil {
return err
if gui.g.CurrentView() == branchesView && branchesView.Context == "remotes" {
if err := gui.handleRemoteSelect(gui.g, branchesView); err != nil {
return err
}
}
return nil
@@ -176,3 +178,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()
})
}

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})
}

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,36 @@ 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)
return gui.applySelectionWithPrompt(false)
}
func (gui *Gui) handleResetSelection(g *gocui.Gui, v *gocui.View) error {
return gui.applySelection(true)
return gui.applySelectionWithPrompt(true)
}
func (gui *Gui) applySelection(reverse bool) error {
func (gui *Gui) applySelectionWithPrompt(reverse bool) error {
state := gui.State.Panels.LineByLine
if !reverse && state.SecondaryFocused {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("CantStageStaged"))
} else if reverse && !state.SecondaryFocused && !gui.Config.GetUserConfig().GetBool("gui.skipUnstageLineWarning") {
return gui.createConfirmationPanel(gui.g, gui.getMainView(), false, "unstage lines", "Are you sure you want to unstage these lines? 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(reverse)
}, nil)
}
return gui.applySelection(reverse)
}
func (gui *Gui) applySelection(reverse bool) error {
state := gui.State.Panels.LineByLine
file, err := gui.getSelectedFile(gui.g)
if err != nil {
return err

View File

@@ -34,16 +34,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.newCmdTask("main", cmd); err != nil {
gui.Log.Error(err)
}
return nil
}

View File

@@ -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 {

View File

@@ -1,8 +1,6 @@
package gui
import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
)
@@ -33,25 +31,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
}
@@ -78,8 +69,10 @@ func (gui *Gui) renderTagsWithSelection() error {
if err := gui.renderListPanel(branchesView, gui.State.Tags); err != nil {
return err
}
if err := gui.handleTagSelect(gui.g, branchesView); err != nil {
return err
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")
@@ -147,3 +140,12 @@ func (gui *Gui) handleCreateTag(g *gocui.Gui, v *gocui.View) error {
return gui.refreshTags()
})
}
func (gui *Gui) handleCreateResetToTagMenu(g *gocui.Gui, v *gocui.View) error {
tag := gui.getSelectedTag()
if tag == nil {
return nil
}
return gui.createResetMenu(tag.Name)
}

78
pkg/gui/tasks_adapter.go Normal file
View File

@@ -0,0 +1,78 @@
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)
if err := manager.NewTask(manager.NewCmdTask(cmd, height+oy+10)); 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 { return nil })
})
gui.viewBufferManagerMap[view.Name()] = manager
}
return manager
}

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 {

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})
}

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?",
},
)
}

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",
@@ -753,6 +750,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 +822,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 +915,36 @@ 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",
},
)
}

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?",
},
)
}

228
pkg/tasks/tasks.go Normal file
View File

@@ -0,0 +1,228 @@
package tasks
import (
"bufio"
"fmt"
"io"
"os/exec"
"sync"
"time"
"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(cmd *exec.Cmd, linesToRead int) func(chan struct{}) error {
return func(stop chan struct{}) error {
r, err := cmd.StdoutPipe()
if err != nil {
return err
}
cmd.Stderr = cmd.Stdout
if err := cmd.Start(); err != nil {
return err
}
go func() {
<-stop
if cmd.ProcessState.ExitCode() == -1 {
if err := kill(cmd); err != nil {
m.Log.Warn(err)
}
}
}()
// 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:
if !loaded {
m.beforeStart()
m.writer.Write([]byte("loading..."))
m.refreshView()
}
case <-stop:
return
}
}()
outer:
for {
select {
case linesToRead := <-m.readLines:
for i := 0; i < linesToRead; i++ {
ok := scanner.Scan()
if !loaded {
m.beforeStart()
loaded = true
}
select {
case <-stop:
break outer
default:
}
if !ok {
m.refreshView()
break outer
}
m.writer.Write(append(scanner.Bytes(), []byte("\n")...))
}
m.refreshView()
case <-stop:
break outer
}
}
if err := cmd.Wait(); err != nil {
m.Log.Warn(err)
}
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{}, 1)
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()
if taskID < m.newTaskId {
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
}
// kill kills a process
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()
}

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)
}

View File

@@ -151,14 +151,14 @@ func renderDisplayableList(items []Displayable, isFocused bool) (string, error)
stringArrays := getDisplayStringArrays(items, isFocused)
if !displayArraysAligned(stringArrays) {
return "", errors.New("Each item must return the same number of strings to display")
}
return RenderDisplayStrings(stringArrays), nil
}
padWidths := getPadWidths(stringArrays)
paddedDisplayStrings := getPaddedDisplayStrings(stringArrays, padWidths)
func RenderDisplayStrings(displayStringsArr [][]string) string {
padWidths := getPadWidths(displayStringsArr)
paddedDisplayStrings := getPaddedDisplayStrings(displayStringsArr, padWidths)
return strings.Join(paddedDisplayStrings, "\n"), nil
return strings.Join(paddedDisplayStrings, "\n")
}
// Decolorise strips a string of color
@@ -168,10 +168,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 +196,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

View File

@@ -276,8 +276,8 @@ func TestRenderDisplayableList(t *testing.T) {
Displayable(&myDisplayable{[]string{"b", "c"}}),
},
false,
"a \nb c",
"",
"Each item must return the same number of strings to display",
},
{
[]Displayable{

View File

@@ -12,3 +12,5 @@
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/
.idea/

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 {
...

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

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
vendor/github.com/integrii/flaggy/parsedValue.go generated vendored Normal file
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,
}
}

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
}

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 {

View File

@@ -5,8 +5,10 @@
package gocui
import (
"github.com/go-errors/errors"
"strconv"
"sync"
"github.com/go-errors/errors"
)
type escapeInterpreter struct {
@@ -15,6 +17,7 @@ type escapeInterpreter struct {
csiParam []string
curFgColor, curBgColor Attribute
mode OutputMode
mutex sync.Mutex
}
type escapeState int
@@ -34,6 +37,9 @@ var (
// runes in case of error will output the non-parsed runes as a string.
func (ei *escapeInterpreter) runes() []rune {
ei.mutex.Lock()
defer ei.mutex.Unlock()
switch ei.state {
case stateNone:
return []rune{0x1b}
@@ -66,6 +72,9 @@ func newEscapeInterpreter(mode OutputMode) *escapeInterpreter {
// reset sets the escapeInterpreter in initial state.
func (ei *escapeInterpreter) reset() {
ei.mutex.Lock()
defer ei.mutex.Unlock()
ei.state = stateNone
ei.curFgColor = ColorDefault
ei.curBgColor = ColorDefault
@@ -76,6 +85,9 @@ func (ei *escapeInterpreter) reset() {
// of an escape sequence, and as such should not be printed verbatim. Otherwise,
// it's not an escape sequence.
func (ei *escapeInterpreter) parseOne(ch rune) (isEscape bool, err error) {
ei.mutex.Lock()
defer ei.mutex.Unlock()
// Sanity checks
if len(ei.csiParam) > 20 {
return false, errCSITooLong
@@ -179,6 +191,9 @@ func (ei *escapeInterpreter) outputNormal() error {
// 0x11 - 0xe8: 216 different colors
// 0xe9 - 0x1ff: 24 different shades of grey
func (ei *escapeInterpreter) output256() error {
ei.mutex.Lock()
defer ei.mutex.Unlock()
if len(ei.csiParam) < 3 {
return ei.outputNormal()
}

View File

@@ -7,6 +7,7 @@ package gocui
import (
standardErrors "errors"
"strings"
"sync"
"time"
"github.com/go-errors/errors"
@@ -89,6 +90,10 @@ 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
}
// NewGui returns a new Gui object with a given output mode.
@@ -125,9 +130,7 @@ func NewGui(mode OutputMode, supportOverlaps bool) (*Gui, error) {
// 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 +412,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
}
@@ -835,14 +837,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
}
}
}()

View File

@@ -458,6 +458,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 +478,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()

View File

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

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

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

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

View File

@@ -1 +0,0 @@
rollbar.test

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

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.

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

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, "*")
}
}

View File

@@ -1,98 +0,0 @@
package roll
import (
"fmt"
"hash/crc32"
"os"
"runtime"
"strings"
)
var (
knownFilePathPatterns = []string{
"github.com/",
"code.google.com/",
"bitbucket.org/",
"launchpad.net/",
"gopkg.in/",
}
)
func getCallers(skip int) (pc []uintptr) {
pc = make([]uintptr, 1000)
i := runtime.Callers(skip+1, pc)
return pc[0:i]
}
// -- rollbarFrames
type rollbarFrame struct {
Filename string `json:"filename"`
Method string `json:"method"`
Line int `json:"lineno"`
}
type rollbarFrames []rollbarFrame
// buildRollbarFrames takes a slice of function pointers and returns a Rollbar
// API payload containing the filename, method name, and line number of each
// function.
func buildRollbarFrames(callers []uintptr) (frames rollbarFrames) {
frames = rollbarFrames{}
// 2016-08-24 - runtime.CallersFrames was added in Go 1.7, which should
// replace the following code when roll is able to require Go 1.7+.
for _, caller := range callers {
frame := rollbarFrame{
Filename: "???",
Method: "???",
}
if fn := runtime.FuncForPC(caller); fn != nil {
name, line := fn.FileLine(caller)
frame.Filename = scrubFile(name)
frame.Line = line
frame.Method = scrubFunction(fn.Name())
}
frames = append(frames, frame)
}
return frames
}
// fingerprint returns a checksum that uniquely identifies a stacktrace by the
// filename, method name, and line number of every frame in the stack.
func (f rollbarFrames) fingerprint() string {
hash := crc32.NewIEEE()
for _, frame := range f {
fmt.Fprintf(hash, "%s%s%d", frame.Filename, frame.Method, frame.Line)
}
return fmt.Sprintf("%x", hash.Sum32())
}
// -- Helpers
// scrubFile removes unneeded information from the path of a source file. This
// makes them shorter in Rollbar UI as well as making them the same, regardless
// of the machine the code was compiled on.
//
// Example:
// /home/foo/go/src/github.com/stvp/roll/rollbar.go -> github.com/stvp/roll/rollbar.go
func scrubFile(s string) string {
var i int
for _, pattern := range knownFilePathPatterns {
i = strings.Index(s, pattern)
if i != -1 {
return s[i:]
}
}
return s
}
// scrubFunction removes unneeded information from the full name of a function.
//
// Example:
// github.com/stvp/roll.getCallers -> roll.getCallers
func scrubFunction(name string) string {
end := strings.LastIndex(name, string(os.PathSeparator))
return name[end+1 : len(name)]
}

View File

@@ -1,22 +0,0 @@
The MIT License (MIT)
Copyright © Heroku 2014 - 2015
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.

View File

@@ -1,15 +0,0 @@
[![Build Status](https://travis-ci.org/heroku/rollrus.svg?branch=master)](https://travis-ci.org/heroku/rollrus)&nbsp;[![GoDoc](https://godoc.org/github.com/heroku/rollrus?status.svg)](https://godoc.org/github.com/heroku/rollrus)
# What
Rollrus is what happens when [Logrus](https://github.com/sirupsen/logrus) meets [Roll](https://github.com/stvp/roll).
When a .Error, .Fatal or .Panic logging function is called, report the details to rollbar via a Logrus hook.
Delivery is synchronous to help ensure that logs are delivered.
If the error includes a [`StackTrace`](https://godoc.org/github.com/pkg/errors#StackTrace), that `StackTrace` is reported to rollbar.
# Usage
Examples available in the [tests](https://github.com/heroku/rollrus/blob/master/rollrus_test.go) or on [GoDoc](https://godoc.org/github.com/heroku/rollrus).

View File

@@ -1,7 +0,0 @@
module github.com/jesseduffield/rollrus
require (
github.com/jesseduffield/roll v0.0.0-20190629104057-695be2e62b00
github.com/pkg/errors v0.8.1
github.com/sirupsen/logrus v1.3.0
)

View File

@@ -1,19 +0,0 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/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/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME=
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

View File

@@ -1,287 +0,0 @@
// Package rollrus combines github.com/jesseduffield/roll with github.com/sirupsen/logrus
// via logrus.Hook mechanism, so that whenever logrus' logger.Error/f(),
// logger.Fatal/f() or logger.Panic/f() are used the messages are
// intercepted and sent to rollbar.
//
// Using SetupLogging should suffice for basic use cases that use the logrus
// singleton logger.
//
// More custom uses are supported by creating a new Hook with NewHook and
// registering that hook with the logrus Logger of choice.
//
// The levels can be customized with the WithLevels OptionFunc.
//
// Specific errors can be ignored with the WithIgnoredErrors OptionFunc. This is
// useful for ignoring errors such as context.Canceled.
//
// See the Examples in the tests for more usage.
package rollrus
import (
"fmt"
"os"
"time"
"github.com/jesseduffield/roll"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
var defaultTriggerLevels = []logrus.Level{
logrus.ErrorLevel,
logrus.FatalLevel,
logrus.PanicLevel,
}
// Hook is a wrapper for the rollbar Client and is usable as a logrus.Hook.
type Hook struct {
roll.Client
triggers []logrus.Level
ignoredErrors []error
ignoreErrorFunc func(error) bool
ignoreFunc func(error, map[string]string) bool
// only used for tests to verify whether or not a report happened.
reported bool
}
// OptionFunc that can be passed to NewHook.
type OptionFunc func(*Hook)
// wellKnownErrorFields are the names of the fields to be checked for values of
// type `error`, in priority order.
var wellKnownErrorFields = []string{
logrus.ErrorKey, "err",
}
// WithLevels is an OptionFunc that customizes the log.Levels the hook will
// report on.
func WithLevels(levels ...logrus.Level) OptionFunc {
return func(h *Hook) {
h.triggers = levels
}
}
// WithMinLevel is an OptionFunc that customizes the log.Levels the hook will
// report on by selecting all levels more severe than the one provided.
func WithMinLevel(level logrus.Level) OptionFunc {
var levels []logrus.Level
for _, l := range logrus.AllLevels {
if l <= level {
levels = append(levels, l)
}
}
return func(h *Hook) {
h.triggers = levels
}
}
// WithIgnoredErrors is an OptionFunc that whitelists certain errors to prevent
// them from firing. See https://golang.org/ref/spec#Comparison_operators
func WithIgnoredErrors(errors ...error) OptionFunc {
return func(h *Hook) {
h.ignoredErrors = append(h.ignoredErrors, errors...)
}
}
// WithIgnoreErrorFunc is an OptionFunc that receives the error that is about
// to be logged and returns true/false if it wants to fire a rollbar alert for.
func WithIgnoreErrorFunc(fn func(error) bool) OptionFunc {
return func(h *Hook) {
h.ignoreErrorFunc = fn
}
}
// WithIgnoreFunc is an OptionFunc that receives the error and custom fields that are about
// to be logged and returns true/false if it wants to fire a rollbar alert for.
func WithIgnoreFunc(fn func(err error, fields map[string]string) bool) OptionFunc {
return func(h *Hook) {
h.ignoreFunc = fn
}
}
// NewHook creates a hook that is intended for use with your own logrus.Logger
// instance. Uses the defualt report levels defined in wellKnownErrorFields.
func NewHook(token string, env string, opts ...OptionFunc) *Hook {
h := NewHookForLevels(token, env, defaultTriggerLevels)
for _, o := range opts {
o(h)
}
return h
}
// NewHookForLevels provided by the caller. Otherwise works like NewHook.
func NewHookForLevels(token string, env string, levels []logrus.Level) *Hook {
return &Hook{
Client: roll.New(token, env),
triggers: levels,
ignoredErrors: make([]error, 0),
ignoreErrorFunc: func(error) bool { return false },
ignoreFunc: func(error, map[string]string) bool { return false },
}
}
// SetupLogging for use on Heroku. If token is not an empty string a rollbar
// hook is added with the environment set to env. The log formatter is set to a
// TextFormatter with timestamps disabled.
func SetupLogging(token, env string) {
setupLogging(token, env, defaultTriggerLevels)
}
// SetupLoggingForLevels works like SetupLogging, but allows you to
// set the levels on which to trigger this hook.
func SetupLoggingForLevels(token, env string, levels []logrus.Level) {
setupLogging(token, env, levels)
}
func setupLogging(token, env string, levels []logrus.Level) {
logrus.SetFormatter(&logrus.TextFormatter{DisableTimestamp: true})
if token != "" {
logrus.AddHook(NewHookForLevels(token, env, levels))
}
}
// ReportPanic attempts to report the panic to rollbar using the provided
// client and then re-panic. If it can't report the panic it will print an
// error to stderr.
func (r *Hook) ReportPanic() {
if p := recover(); p != nil {
if _, err := r.Client.Critical(fmt.Errorf("panic: %q", p), nil); err != nil {
fmt.Fprintf(os.Stderr, "reporting_panic=false err=%q\n", err)
}
panic(p)
}
}
// ReportPanic attempts to report the panic to rollbar if the token is set
func ReportPanic(token, env string) {
if token != "" {
h := &Hook{Client: roll.New(token, env)}
h.ReportPanic()
}
}
// Levels returns the logrus log.Levels that this hook handles
func (r *Hook) Levels() []logrus.Level {
if r.triggers == nil {
return defaultTriggerLevels
}
return r.triggers
}
// Fire the hook. This is called by Logrus for entries that match the levels
// returned by Levels().
func (r *Hook) Fire(entry *logrus.Entry) error {
trace, cause := extractError(entry)
for _, ie := range r.ignoredErrors {
if ie == cause {
return nil
}
}
if r.ignoreErrorFunc(cause) {
return nil
}
m := convertFields(entry.Data)
if _, exists := m["time"]; !exists {
m["time"] = entry.Time.Format(time.RFC3339)
}
if r.ignoreFunc(cause, m) {
return nil
}
return r.report(entry, cause, m, trace)
}
func (r *Hook) report(entry *logrus.Entry, cause error, m map[string]string, trace []uintptr) (err error) {
hasTrace := len(trace) > 0
level := entry.Level
r.reported = true
switch {
case hasTrace && level == logrus.FatalLevel:
_, err = r.Client.CriticalStack(cause, trace, m)
case hasTrace && level == logrus.PanicLevel:
_, err = r.Client.CriticalStack(cause, trace, m)
case hasTrace && level == logrus.ErrorLevel:
_, err = r.Client.ErrorStack(cause, trace, m)
case hasTrace && level == logrus.WarnLevel:
_, err = r.Client.WarningStack(cause, trace, m)
case level == logrus.FatalLevel || level == logrus.PanicLevel:
_, err = r.Client.Critical(cause, m)
case level == logrus.ErrorLevel:
_, err = r.Client.Error(cause, m)
case level == logrus.WarnLevel:
_, err = r.Client.Warning(cause, m)
case level == logrus.InfoLevel:
_, err = r.Client.Info(entry.Message, m)
case level == logrus.DebugLevel:
_, err = r.Client.Debug(entry.Message, m)
}
return err
}
// convertFields converts from log.Fields to map[string]string so that we can
// report extra fields to Rollbar
func convertFields(fields logrus.Fields) map[string]string {
m := make(map[string]string)
for k, v := range fields {
switch t := v.(type) {
case time.Time:
m[k] = t.Format(time.RFC3339)
default:
if s, ok := v.(fmt.Stringer); ok {
m[k] = s.String()
} else {
m[k] = fmt.Sprintf("%+v", t)
}
}
}
return m
}
// extractError attempts to extract an error from a well known field, err or error
func extractError(entry *logrus.Entry) ([]uintptr, error) {
var trace []uintptr
fields := entry.Data
type stackTracer interface {
StackTrace() errors.StackTrace
}
for _, f := range wellKnownErrorFields {
e, ok := fields[f]
if !ok {
continue
}
err, ok := e.(error)
if !ok {
continue
}
cause := errors.Cause(err)
tracer, ok := err.(stackTracer)
if ok {
return copyStackTrace(tracer.StackTrace()), cause
}
return trace, cause
}
// when no error found, default to the logged message.
return trace, fmt.Errorf(entry.Message)
}
func copyStackTrace(trace errors.StackTrace) (out []uintptr) {
for _, frame := range trace {
out = append(out, uintptr(frame))
}
return
}

View File

@@ -2,13 +2,16 @@
package termbox
import "github.com/mattn/go-runewidth"
import "fmt"
import "os"
import "os/signal"
import "syscall"
import "runtime"
import "time"
import (
"fmt"
"os"
"os/signal"
"runtime"
"syscall"
"time"
"github.com/mattn/go-runewidth"
)
// public API
@@ -26,13 +29,21 @@ func Init() error {
var err error
out, err = os.OpenFile("/dev/tty", syscall.O_WRONLY, 0)
if err != nil {
return err
}
in, err = syscall.Open("/dev/tty", syscall.O_RDONLY, 0)
if err != nil {
return err
if runtime.GOOS == "openbsd" || runtime.GOOS == "freebsd" {
out, err = os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
return err
}
in = int(out.Fd())
} else {
out, err = os.OpenFile("/dev/tty", os.O_WRONLY, 0)
if err != nil {
return err
}
in, err = syscall.Open("/dev/tty", syscall.O_RDONLY, 0)
if err != nil {
return err
}
}
err = setup_term()

View File

@@ -1,39 +0,0 @@
// +build ignore
package termbox
/*
#include <termios.h>
#include <sys/ioctl.h>
*/
import "C"
type syscall_Termios C.struct_termios
const (
syscall_IGNBRK = C.IGNBRK
syscall_BRKINT = C.BRKINT
syscall_PARMRK = C.PARMRK
syscall_ISTRIP = C.ISTRIP
syscall_INLCR = C.INLCR
syscall_IGNCR = C.IGNCR
syscall_ICRNL = C.ICRNL
syscall_IXON = C.IXON
syscall_OPOST = C.OPOST
syscall_ECHO = C.ECHO
syscall_ECHONL = C.ECHONL
syscall_ICANON = C.ICANON
syscall_ISIG = C.ISIG
syscall_IEXTEN = C.IEXTEN
syscall_CSIZE = C.CSIZE
syscall_PARENB = C.PARENB
syscall_CS8 = C.CS8
syscall_VMIN = C.VMIN
syscall_VTIME = C.VTIME
// on darwin change these to (on *bsd too?):
// C.TIOCGETA
// C.TIOCSETA
syscall_TCGETS = C.TCGETS
syscall_TCSETS = C.TCSETS
)

View File

@@ -26,6 +26,7 @@ The tool is sponsored by the [marvin + konsorten GmbH](http://www.konsorten.de).
We thank all the authors who provided code to this library:
* Felix Kollmann
* Nicolas Perraut
## License

View File

@@ -0,0 +1,11 @@
// +build linux darwin
package sequences
import (
"fmt"
)
func EnableVirtualTerminalProcessing(stream uintptr, enable bool) error {
return fmt.Errorf("windows only package")
}

View File

@@ -1,5 +1,5 @@
module github.com/mattn/go-isatty
require golang.org/x/sys v0.0.0-20191008105621-543471e840be
go 1.12
go 1.14
require golang.org/x/sys v0.0.0-20191026070338-33540a1f6037

View File

@@ -1,4 +1,2 @@
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/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-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@@ -8,7 +8,7 @@ import (
// IsTerminal returns true if the given file descriptor is a terminal.
func IsTerminal(fd uintptr) bool {
path, err := syscall.Fd2path(fd)
path, err := syscall.Fd2path(int(fd))
if err != nil {
return false
}

View File

@@ -2,7 +2,7 @@ go-runewidth
============
[![Build Status](https://travis-ci.org/mattn/go-runewidth.png?branch=master)](https://travis-ci.org/mattn/go-runewidth)
[![Coverage Status](https://coveralls.io/repos/mattn/go-runewidth/badge.png?branch=HEAD)](https://coveralls.io/r/mattn/go-runewidth?branch=HEAD)
[![Codecov](https://codecov.io/gh/mattn/go-runewidth/branch/master/graph/badge.svg)](https://codecov.io/gh/mattn/go-runewidth)
[![GoDoc](https://godoc.org/github.com/mattn/go-runewidth?status.svg)](http://godoc.org/github.com/mattn/go-runewidth)
[![Go Report Card](https://goreportcard.com/badge/github.com/mattn/go-runewidth)](https://goreportcard.com/report/github.com/mattn/go-runewidth)

12
vendor/github.com/mattn/go-runewidth/go.test.sh generated vendored Normal file
View File

@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -e
echo "" > coverage.txt
for d in $(go list ./... | grep -v vendor); do
go test -race -coverprofile=profile.out -covermode=atomic "$d"
if [ -f profile.out ]; then
cat profile.out >> coverage.txt
rm profile.out
fi
done

View File

@@ -50,7 +50,6 @@ func inTables(r rune, ts ...table) bool {
}
func inTable(r rune, t table) bool {
// func (t table) IncludesRune(r rune) bool {
if r < t[0].first {
return false
}
@@ -102,11 +101,9 @@ func NewCondition() *Condition {
// See http://www.unicode.org/reports/tr11/
func (c *Condition) RuneWidth(r rune) int {
switch {
case r < 0 || r > 0x10FFFF ||
inTables(r, nonprint, combining, notassigned):
case r < 0 || r > 0x10FFFF || inTables(r, nonprint, combining, notassigned):
return 0
case (c.EastAsianWidth && IsAmbiguousWidth(r)) ||
inTables(r, doublewidth, emoji):
case (c.EastAsianWidth && IsAmbiguousWidth(r)) || inTables(r, doublewidth):
return 2
default:
return 1
@@ -128,9 +125,12 @@ func (c *Condition) stringWidthZeroJoiner(s string) (width int) {
}
w := c.RuneWidth(r)
if r2 == 0x200D && inTables(r, emoji) && inTables(r1, emoji) {
w = 0
if width < w {
width = w
}
} else {
width += w
}
width += w
r1, r2 = r2, r
}
return width

View File

@@ -1,3 +1,5 @@
// Code generated by script/generate.go. DO NOT EDIT.
package runewidth
var combining = table{

View File

@@ -1,2 +1,5 @@
test_program/test_program_bin
fuzz/
cmd/tomll/tomll
cmd/tomljson/tomljson
cmd/tomltestgen/tomltestgen

132
vendor/github.com/pelletier/go-toml/CONTRIBUTING.md generated vendored Normal file
View File

@@ -0,0 +1,132 @@
## Contributing
Thank you for your interest in go-toml! We appreciate you considering
contributing to go-toml!
The main goal is the project is to provide an easy-to-use TOML
implementation for Go that gets the job done and gets out of your way
dealing with TOML is probably not the central piece of your project.
As the single maintainer of go-toml, time is scarce. All help, big or
small, is more than welcomed!
### Ask questions
Any question you may have, somebody else might have it too. Always feel
free to ask them on the [issues tracker][issues-tracker]. We will try to
answer them as clearly and quickly as possible, time permitting.
Asking questions also helps us identify areas where the documentation needs
improvement, or new features that weren't envisioned before. Sometimes, a
seemingly innocent question leads to the fix of a bug. Don't hesitate and
ask away!
### Improve the documentation
The best way to share your knowledge and experience with go-toml is to
improve the documentation. Fix a typo, clarify an interface, add an
example, anything goes!
The documentation is present in the [README][readme] and thorough the
source code. On release, it gets updated on [GoDoc][godoc]. To make a
change to the documentation, create a pull request with your proposed
changes. For simple changes like that, the easiest way to go is probably
the "Fork this project and edit the file" button on Github, displayed at
the top right of the file. Unless it's a trivial change (for example a
typo), provide a little bit of context in your pull request description or
commit message.
### Report a bug
Found a bug! Sorry to hear that :(. Help us and other track them down and
fix by reporting it. [File a new bug report][bug-report] on the [issues
tracker][issues-tracker]. The template should provide enough guidance on
what to include. When in doubt: add more details! By reducing ambiguity and
providing more information, it decreases back and forth and saves everyone
time.
### Code changes
Want to contribute a patch? Very happy to hear that!
First, some high-level rules:
* A short proposal with some POC code is better than a lengthy piece of
text with no code. Code speaks louder than words.
* No backward-incompatible patch will be accepted unless discussed.
Sometimes it's hard, and Go's lack of versioning by default does not
help, but we try not to break people's programs unless we absolutely have
to.
* If you are writing a new feature or extending an existing one, make sure
to write some documentation.
* Bug fixes need to be accompanied with regression tests.
* New code needs to be tested.
* Your commit messages need to explain why the change is needed, even if
already included in the PR description.
It does sound like a lot, but those best practices are here to save time
overall and continuously improve the quality of the project, which is
something everyone benefits from.
#### Get started
The fairly standard code contribution process looks like that:
1. [Fork the project][fork].
2. Make your changes, commit on any branch you like.
3. [Open up a pull request][pull-request]
4. Review, potential ask for changes.
5. Merge. You're in!
Feel free to ask for help! You can create draft pull requests to gather
some early feedback!
#### Run the tests
You can run tests for go-toml using Go's test tool: `go test ./...`.
When creating a pull requests, all tests will be ran on Linux on a few Go
versions (Travis CI), and on Windows using the latest Go version
(AppVeyor).
#### Style
Try to look around and follow the same format and structure as the rest of
the code. We enforce using `go fmt` on the whole code base.
---
### Maintainers-only
#### Merge pull request
Checklist:
* Passing CI.
* Does not introduce backward-incompatible changes (unless discussed).
* Has relevant doc changes.
* Has relevant unit tests.
1. Merge using "squash and merge".
2. Make sure to edit the commit message to keep all the useful information
nice and clean.
3. Make sure the commit title is clear and contains the PR number (#123).
#### New release
1. Go to [releases][releases]. Click on "X commits to master since this
release".
2. Make note of all the changes. Look for backward incompatible changes,
new features, and bug fixes.
3. Pick the new version using the above and semver.
4. Create a [new release][new-release].
5. Follow the same format as [1.1.0][release-110].
[issues-tracker]: https://github.com/pelletier/go-toml/issues
[bug-report]: https://github.com/pelletier/go-toml/issues/new?template=bug_report.md
[godoc]: https://godoc.org/github.com/pelletier/go-toml
[readme]: ./README.md
[fork]: https://help.github.com/articles/fork-a-repo
[pull-request]: https://help.github.com/en/articles/creating-a-pull-request
[releases]: https://github.com/pelletier/go-toml/releases
[new-release]: https://github.com/pelletier/go-toml/releases/new
[release-110]: https://github.com/pelletier/go-toml/releases/tag/v1.1.0

11
vendor/github.com/pelletier/go-toml/Dockerfile generated vendored Normal file
View File

@@ -0,0 +1,11 @@
FROM golang:1.12-alpine3.9 as builder
WORKDIR /go/src/github.com/pelletier/go-toml
COPY . .
ENV CGO_ENABLED=0
ENV GOOS=linux
RUN go install ./...
FROM scratch
COPY --from=builder /go/bin/tomll /usr/bin/tomll
COPY --from=builder /go/bin/tomljson /usr/bin/tomljson
COPY --from=builder /go/bin/jsontoml /usr/bin/jsontoml

View File

@@ -0,0 +1,5 @@
**Issue:** add link to pelletier/go-toml issue here
Explanation of what this pull request does.
More detailed description of the decisions being made and the reasons why (if the patch is non-trivial).

View File

@@ -3,13 +3,14 @@
Go library for the [TOML](https://github.com/mojombo/toml) format.
This library supports TOML version
[v0.4.0](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md)
[v0.5.0](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.5.0.md)
[![GoDoc](https://godoc.org/github.com/pelletier/go-toml?status.svg)](http://godoc.org/github.com/pelletier/go-toml)
[![license](https://img.shields.io/github/license/pelletier/go-toml.svg)](https://github.com/pelletier/go-toml/blob/master/LICENSE)
[![Build Status](https://travis-ci.org/pelletier/go-toml.svg?branch=master)](https://travis-ci.org/pelletier/go-toml)
[![Coverage Status](https://coveralls.io/repos/github/pelletier/go-toml/badge.svg?branch=master)](https://coveralls.io/github/pelletier/go-toml?branch=master)
[![Build Status](https://dev.azure.com/pelletierthomas/go-toml-ci/_apis/build/status/pelletier.go-toml?branchName=master)](https://dev.azure.com/pelletierthomas/go-toml-ci/_build/latest?definitionId=1&branchName=master)
[![codecov](https://codecov.io/gh/pelletier/go-toml/branch/master/graph/badge.svg)](https://codecov.io/gh/pelletier/go-toml)
[![Go Report Card](https://goreportcard.com/badge/github.com/pelletier/go-toml)](https://goreportcard.com/report/github.com/pelletier/go-toml)
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fpelletier%2Fgo-toml.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fpelletier%2Fgo-toml?ref=badge_shield)
## Features
@@ -98,6 +99,30 @@ Go-toml provides two handy command line tools:
go install github.com/pelletier/go-toml/cmd/tomljson
tomljson --help
```
* `jsontoml`: Reads a JSON file and outputs a TOML representation.
```
go install github.com/pelletier/go-toml/cmd/jsontoml
jsontoml --help
```
### Docker image
Those tools are also availble as a Docker image from
[dockerhub](https://hub.docker.com/r/pelletier/go-toml). For example, to
use `tomljson`:
```
docker run -v $PWD:/workdir pelletier/go-toml tomljson /workdir/example.toml
```
Only master (`latest`) and tagged versions are published to dockerhub. You
can build your own image as usual:
```
docker build -t go-toml .
```
## Contribute
@@ -107,12 +132,7 @@ much appreciated!
### Run tests
You have to make sure two kind of tests run:
1. The Go unit tests
2. The TOML examples base
You can run both of them using `./test.sh`.
`go test ./...`
### Fuzzing

167
vendor/github.com/pelletier/go-toml/azure-pipelines.yml generated vendored Normal file
View File

@@ -0,0 +1,167 @@
trigger:
- master
stages:
- stage: fuzzit
displayName: "Run Fuzzit"
dependsOn: []
condition: and(succeeded(), eq(variables['Build.SourceBranchName'], 'master'))
jobs:
- job: submit
displayName: "Submit"
pool:
vmImage: ubuntu-latest
steps:
- task: GoTool@0
displayName: "Install Go 1.13"
inputs:
version: "1.13"
- script: echo "##vso[task.setvariable variable=PATH]${PATH}:/home/vsts/go/bin/"
- script: mkdir -p ${HOME}/go/src/github.com/pelletier/go-toml
- script: cp -R . ${HOME}/go/src/github.com/pelletier/go-toml
- task: Bash@3
inputs:
filePath: './fuzzit.sh'
env:
TYPE: fuzzing
FUZZIT_API_KEY: $(FUZZIT_API_KEY)
- stage: run_checks
displayName: "Check"
dependsOn: []
jobs:
- job: fmt
displayName: "fmt"
pool:
vmImage: ubuntu-latest
steps:
- task: GoTool@0
displayName: "Install Go 1.13"
inputs:
version: "1.13"
- task: Go@0
displayName: "go fmt ./..."
inputs:
command: 'custom'
customCommand: 'fmt'
arguments: './...'
- job: coverage
displayName: "coverage"
pool:
vmImage: ubuntu-latest
steps:
- task: GoTool@0
displayName: "Install Go 1.13"
inputs:
version: "1.13"
- task: Go@0
displayName: "Generate coverage"
inputs:
command: 'test'
arguments: "-race -coverprofile=coverage.txt -covermode=atomic"
- task: Bash@3
inputs:
targetType: 'inline'
script: 'bash <(curl -s https://codecov.io/bash) -t $(CODECOV_TOKEN)'
- job: benchmark
displayName: "benchmark"
pool:
vmImage: ubuntu-latest
steps:
- task: GoTool@0
displayName: "Install Go 1.13"
inputs:
version: "1.13"
- script: echo "##vso[task.setvariable variable=PATH]${PATH}:/home/vsts/go/bin/"
- task: Bash@3
inputs:
filePath: './benchmark.sh'
arguments: "master $(Build.Repository.Uri)"
- job: fuzzing
displayName: "fuzzing"
pool:
vmImage: ubuntu-latest
steps:
- task: GoTool@0
displayName: "Install Go 1.13"
inputs:
version: "1.13"
- script: echo "##vso[task.setvariable variable=PATH]${PATH}:/home/vsts/go/bin/"
- script: mkdir -p ${HOME}/go/src/github.com/pelletier/go-toml
- script: cp -R . ${HOME}/go/src/github.com/pelletier/go-toml
- task: Bash@3
inputs:
filePath: './fuzzit.sh'
env:
TYPE: local-regression
- job: go_unit_tests
displayName: "unit tests"
strategy:
matrix:
linux 1.13:
goVersion: '1.13'
imageName: 'ubuntu-latest'
mac 1.13:
goVersion: '1.13'
imageName: 'macos-10.13'
windows 1.13:
goVersion: '1.13'
imageName: 'vs2017-win2016'
linux 1.12:
goVersion: '1.12'
imageName: 'ubuntu-latest'
mac 1.12:
goVersion: '1.12'
imageName: 'macos-10.13'
windows 1.12:
goVersion: '1.12'
imageName: 'vs2017-win2016'
pool:
vmImage: $(imageName)
steps:
- task: GoTool@0
displayName: "Install Go $(goVersion)"
inputs:
version: $(goVersion)
- task: Go@0
displayName: "go test ./..."
inputs:
command: 'test'
arguments: './...'
- stage: build_docker_image
displayName: "Build Docker image"
dependsOn: run_checks
jobs:
- job: build
displayName: "Build"
pool:
vmImage: ubuntu-latest
steps:
- task: Docker@2
inputs:
command: 'build'
Dockerfile: 'Dockerfile'
buildContext: '.'
addPipelineData: false
- stage: publish_docker_image
displayName: "Publish Docker image"
dependsOn: build_docker_image
condition: and(succeeded(), eq(variables['Build.SourceBranchName'], 'master'))
jobs:
- job: publish
displayName: "Publish"
pool:
vmImage: ubuntu-latest
steps:
- task: Docker@2
inputs:
containerRegistry: 'DockerHub'
repository: 'pelletier/go-toml'
command: 'buildAndPush'
Dockerfile: 'Dockerfile'
buildContext: '.'
tags: 'latest'

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