Compare commits

...
106 Commits
Author SHA1 Message Date
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
Jesse Duffield 3dd1daacdc unescape another string 2019-11-21 22:17:18 +11:00
Jesse Duffield bad06bb634 fix typo 2019-11-21 22:09:02 +11:00
Jesse Duffield e18e81f5eb don't pass single commands directly to RunCommand (or equivalent function)
when it contains percentages.

This is a really strange one. It's a linting warning in my editor
and it doesn't stop me from compiling, but it breaks `go test`.

A basic file to reproduce what I'm talking about:

package main

import "fmt"

func main() {
	notSprintf("test %s") // compiler complains here thinking %s needs a corresponding argument
}

func notSprintf(formatStr string, formatArgs ...interface{}) string {
	if formatArgs != nil {
		return formatStr
	}
	return fmt.Sprintf(formatStr, formatArgs...)
}
2019-11-21 22:07:14 +11:00
Jesse Duffield 67a446234c fix specs 2019-11-21 22:07:14 +11:00
Jesse Duffield f905b27b00 couple of things to clean up after rebasing onto master 2019-11-21 22:07:14 +11:00
Jesse Duffield e36ee0b4f1 give RunCommand the same input signature as fmt.Sprintf 2019-11-21 22:07:14 +11:00
Jesse Duffield 3c13229145 add tags panel 2019-11-21 22:07:14 +11:00
Jesse Duffield cea24c2cf9 allow editing remotes 2019-11-21 22:07:14 +11:00
Jesse Duffield 64017cf874 require double clicking menu items so you know what you're clicking 2019-11-21 22:07:14 +11:00
Jesse Duffield b3bce8a1ba refactor confirmation prompt code 2019-11-21 22:07:14 +11:00
Jesse Duffield 3b0cef2ec8 better handling of click events in list views 2019-11-21 22:07:14 +11:00
Jesse Duffield 07cbae4019 support setting upstream 2019-11-21 22:07:14 +11:00
Jesse Duffield b42202ea1c better fast forward 2019-11-21 22:07:14 +11:00
Jesse Duffield 8347dcd671 make upstream branch display more lenient on git errors 2019-11-21 22:07:14 +11:00
Jesse Duffield dcb5285797 support rebasing onto remote branch 2019-11-21 22:07:14 +11:00
Jesse Duffield a9cd647075 support deleting remote branches 2019-11-21 22:07:14 +11:00
Jesse Duffield f0cd730fbb ensure we switch tabs when switching context 2019-11-21 22:07:14 +11:00
Jesse Duffield 2afbd7ba7f support merging remote branches into checked out branch 2019-11-21 22:07:14 +11:00
Jesse Duffield 55ff0c0dee support detached heads when showing the selected branch 2019-11-21 22:07:14 +11:00
Jesse Duffield 6b7aaeca45 support adding/removing remotes 2019-11-21 22:07:14 +11:00
Jesse Duffield 1f3e1720a3 split RemoteBranch out from Branch 2019-11-21 22:07:14 +11:00
Jesse Duffield b7f2d0366b get branches with git for-each-ref 2019-11-21 22:07:14 +11:00
Jesse Duffield 6bd0979b4a only refresh branches panel on focus lost when in the local-branches context 2019-11-21 22:07:14 +11:00
Jesse Duffield 986abc1e45 support viewing a remote branch 2019-11-21 22:07:14 +11:00
Jesse Duffield 61dac10bb9 support navigating remotes view 2019-11-21 22:07:14 +11:00
Jesse Duffield b5385f2560 remove redundant logging 2019-11-21 22:07:14 +11:00
Jesse Duffield 92e43d9e77 allow changing tabs with [ and ] 2019-11-21 22:07:14 +11:00
Jesse Duffield 325408d0e3 get remote branches when getting remotes 2019-11-21 22:07:14 +11:00
Jesse Duffield eeb667954f trying to use gogit with branches from remotes 2019-11-21 22:07:14 +11:00
Jesse Duffield 8aa1062e06 extract out some logic for list views 2019-11-21 22:07:14 +11:00
Jesse Duffield 7e0a8f235e add contexts to views 2019-11-21 22:07:14 +11:00
Jesse Duffield 44bbc106a9 bump gocui to get contexts on keybindings 2019-11-21 22:07:14 +11:00
Jesse Duffield e6be849eb2 add remotes context to branches view 2019-11-21 22:07:14 +11:00
Jesse Duffield 092f27495a add remote model 2019-11-21 22:07:14 +11:00
Dawid Dziurla 7849f91d80 Merge pull request #543 from lhaeger/master
Readme: add MacPorts install source
2019-11-17 09:14:29 +01:00
Lothar Haeger 5f769da74d Readme: add MacPorts install source 2019-11-17 08:08:04 +01:00
Jesse Duffield 963c034b48 go mod tidy && go mod vendor 2019-11-14 22:22:47 +11:00
Jesse Duffield f15e47bb67 add file watching for modified files
log createErrorPanel error

swallow error when adding file to watcher
2019-11-14 22:22:47 +11:00
Jesse Duffield 7995d56a85 allow editing or opening a file while resolving merge conflicts 2019-11-14 22:22:47 +11:00
Jesse Duffield 30aed94aa8 update go git 2019-11-14 09:41:56 +11:00
Jesse Duffield 3b1d705473 show upstream branch for branch 2019-11-13 22:25:42 +11:00
Jesse Duffield f43ba728e3 prompt to set upstream when pulling on untracked branch
prompt to set upstream when pulling on untracked branch
2019-11-13 21:36:16 +11:00
Dawid Dziurla b662362570 Merge pull request #538 from jesseduffield/dawidd6-patch-1
README: fix typo
2019-11-12 23:46:27 +01:00
Dawid Dziurla 99ece6fc35 README: fix typo
https://github.com/jesseduffield/lazygit/commit/945fccd211aac07e6813d3f30173390b26954404
2019-11-12 23:42:57 +01:00
314 changed files with 53783 additions and 48997 deletions
+10 -10
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:
+18 -16
View File
@@ -37,13 +37,16 @@ Core:
brew install lazygit
```
### MacPorts
Latest version built from github releases.
Tap:
```
sudo port install lazygit
```
### Ubuntu
Packages for Ubuntu 16.04, 18.04 and 18.10 are available via [Launchpad PPA](https://launchpad.net/~lazygit-team).
**Release builds**
Built from git tags. Supposed to be more stable.
Packages for Ubuntu are available via [Launchpad PPA](https://launchpad.net/~lazygit-team).
```sh
sudo add-apt-repository ppa:lazygit-team/release
@@ -51,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
@@ -84,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
@@ -121,7 +123,7 @@ whichever rc file you're using).
## Changing Directory On Exit
If you change repos in lazygit and want your shell to change directory into that repo on exiting lazygit, add this to your `~./zshrc` (or other rc file):
If you change repos in lazygit and want your shell to change directory into that repo on exiting lazygit, add this to your `~/.zshrc` (or other rc file):
```
lg()
{
+121
View File
@@ -1,5 +1,7 @@
# User Config:
Default path for the config file: `~/.config/jesseduffield/lazygit/config.yml`
## Default:
```yaml
@@ -30,6 +32,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 +194,30 @@ 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>'
```
+59
View File
@@ -0,0 +1,59 @@
## Possible keybindings
| Put in | You will get |
|---------------|----------------|
| `<f1>` | F1 |
| `<f2>` | F2 |
| `<f3>` | F3 |
| `<f4>` | F4 |
| `<f5>` | F5 |
| `<f6>` | F6 |
| `<f7>` | F7 |
| `<f8>` | F8 |
| `<f9>` | F9 |
| `<f10>` | F10 |
| `<f11>` | F11 |
| `<f12>` | F12 |
| `<insert>` | Insert |
| `<delete>` | Delete |
| `<home>` | Home |
| `<end>` | End |
| `<pgup>` | Pgup |
| `<pgdown>` | Pgdn |
| `<up>` | ArrowUp |
| `<down>` | ArrowDown |
| `<left>` | ArrowLeft |
| `<right>` | ArrowRight |
| `<tab>` | Tab |
| `<enter>` | Enter |
| `<esc>` | Esc |
| `<backspace>` | Backspace |
| `<c-space>` | CtrlSpace |
| `<c-/>` | CtrlSlash |
| `<space>` | Space |
| `<c-a>` | CtrlA |
| `<c-b>` | CtrlB |
| `<c-c>` | CtrlC |
| `<c-d>` | CtrlD |
| `<c-e>` | CtrlE |
| `<c-f>` | CtrlF |
| `<c-g>` | CtrlG |
| `<c-j>` | CtrlJ |
| `<c-k>` | CtrlK |
| `<c-l>` | CtrlL |
| `<c-n>` | CtrlN |
| `<c-o>` | CtrlO |
| `<c-p>` | CtrlP |
| `<c-q>` | CtrlQ |
| `<c-r>` | CtrlR |
| `<c-s>` | CtrlS |
| `<c-t>` | CtrlT |
| `<c-u>` | CtrlU |
| `<c-v>` | CtrlV |
| `<c-w>` | CtrlW |
| `<c-x>` | CtrlX |
| `<c-y>` | CtrlY |
| `<c-z>` | CtrlZ |
| `<c-4>` | Ctrl4 |
| `<c-5>` | Ctrl5 |
| `<c-6>` | Ctrl6 |
| `<c-8>` | Ctrl8 |
+1
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
+17 -7
View File
@@ -5,30 +5,40 @@ go 1.13
require (
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
github.com/fatih/color v1.7.0
github.com/fsnotify/fsnotify v1.4.7
github.com/go-errors/errors v1.0.1
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.20191110053728-01cdcccd0508
github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/gocui v0.3.1-0.20191116013947-b13bda319532
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/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.7
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-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
)
+44 -18
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,14 +73,12 @@ 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.20191110053728-01cdcccd0508 h1:8CPQLUe+0QXxnbnUfaMxh1UGxg3rYCqCvbxecC9rrIY=
github.com/jesseduffield/gocui v0.3.1-0.20191110053728-01cdcccd0508/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/pty v1.1.3 h1:JTPQ/bbGH3IBwQhxKZiFxfD4wv7cYrY7LNTwbucsZ64=
github.com/jesseduffield/pty v1.1.3/go.mod h1:7jlS40+UhOqkZJDIG1B/H21xnuET/+fvbbnHCa8wSIo=
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/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=
@@ -89,6 +89,8 @@ github.com/jesseduffield/termbox-go v0.0.0-20190630083001-9dd53af7214e h1:tth7wr
github.com/jesseduffield/termbox-go v0.0.0-20190630083001-9dd53af7214e/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=
@@ -98,6 +100,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=
@@ -110,10 +114,10 @@ github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-runewidth v0.0.6 h1:V2iyH+aX9C5fsYCpK60U8BYIvmhqxuOL3JZcqc1NB7k=
github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
github.com/mattn/go-runewidth v0.0.7/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=
@@ -122,8 +126,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=
@@ -133,6 +137,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=
@@ -156,18 +162,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=
@@ -199,6 +215,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-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,6 +230,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-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -230,8 +250,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-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=
@@ -240,6 +261,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=
@@ -250,8 +272,12 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
@@ -267,6 +293,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=
+6 -9
View File
@@ -22,7 +22,7 @@ type Branch struct {
// GetDisplayStrings returns the display string of branch
func (b *Branch) GetDisplayStrings(isFocused bool) []string {
displayName := utils.ColoredString(b.Name, b.GetColor())
displayName := utils.ColoredString(b.Name, GetBranchColor(b.Name))
if isFocused && b.Selected && b.Pushables != "" && b.Pullables != "" {
displayName = fmt.Sprintf("%s ↑%s↓%s", displayName, b.Pushables, b.Pullables)
}
@@ -30,9 +30,11 @@ func (b *Branch) GetDisplayStrings(isFocused bool) []string {
return []string{b.Recency, displayName}
}
// GetColor branch color
func (b *Branch) GetColor() color.Attribute {
switch b.getType() {
// GetBranchColor branch color
func GetBranchColor(name string) color.Attribute {
branchType := strings.Split(name, "/")[0]
switch branchType {
case "feature":
return color.FgGreen
case "bugfix":
@@ -43,8 +45,3 @@ func (b *Branch) GetColor() color.Attribute {
return theme.DefaultTextColor
}
}
// expected to return feature/bugfix/hotfix or blank string
func (b *Branch) getType() string {
return strings.Split(b.Name, "/")[0]
}
+3 -1
View File
@@ -47,7 +47,9 @@ func (b *BranchListBuilder) obtainCurrentBranch() *Branch {
func (b *BranchListBuilder) obtainReflogBranches() []*Branch {
branches := make([]*Branch, 0)
rawString, err := b.GitCommand.OSCommand.RunCommandWithOutput("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD")
// 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"
rawString, err := b.GitCommand.OSCommand.RunCommandWithOutput(unescaped)
if err != nil {
return branches
}
+9 -1
View File
@@ -1,6 +1,8 @@
package commands
import (
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
@@ -14,6 +16,7 @@ type Commit struct {
DisplayString string
Action string // one of "", "pick", "edit", "squash", "reword", "drop", "fixup"
Copied bool // to know if this commit is ready to be cherry-picked somewhere
Tags []string
}
// GetDisplayStrings is a function.
@@ -41,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:
@@ -52,9 +57,12 @@ func (c *Commit) GetDisplayStrings(isFocused bool) []string {
}
actionString := ""
tagString := ""
if c.Action != "" {
actionString = cyan.Sprint(utils.WithPadding(c.Action, 7)) + " "
} else if len(c.Tags) > 0 {
tagString = utils.ColoredString(strings.Join(c.Tags, " "), color.FgMagenta) + " "
}
return []string{shaColor.Sprint(c.Sha), actionString + defaultColor.Sprint(c.Name)}
return []string{shaColor.Sprint(c.Sha), actionString + tagString + defaultColor.Sprint(c.Name)}
}
+2 -1
View File
@@ -78,6 +78,7 @@ func (c *CommitListBuilder) GetCommits() ([]*Commit, error) {
Name: strings.Join(splitLine[1:], " "),
Status: status,
DisplayString: strings.Join(splitLine, " "),
// TODO: add tags here
})
}
if rebaseMode != "" {
@@ -261,7 +262,7 @@ func (c *CommitListBuilder) getMergeBase() (string, error) {
}
// swallowing error because it's not a big deal; probably because there are no commits yet
output, _ := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git merge-base HEAD %s", baseBranch))
output, _ := c.OSCommand.RunCommandWithOutput("git merge-base HEAD %s", baseBranch)
return output, nil
}
+4
View File
@@ -289,6 +289,10 @@ func TestCommitListBuilderGetCommits(t *testing.T) {
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
// here's where we are returning the error
return exec.Command("test")
case "branch":
assert.EqualValues(t, []string{"branch", "--contains"}, args)
// here too
return exec.Command("test")
case "rev-parse":
assert.EqualValues(t, []string{"rev-parse", "--short", "HEAD"}, args)
// here too
+144 -58
View File
@@ -6,6 +6,7 @@ import (
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"time"
@@ -21,7 +22,13 @@ import (
gogit "gopkg.in/src-d/go-git.v4"
)
func verifyInGitRepo(runCmd func(string) error) error {
// this takes something like:
// * (HEAD detached at 264fc6f5)
// remotes
// and returns '264fc6f5' as the second match
const CurrentBranchNameRegex = `(?m)^\*.*?([^ ]*?)\)?$`
func verifyInGitRepo(runCmd func(string, ...interface{}) error) error {
return runCmd("git status")
}
@@ -150,7 +157,9 @@ func findDotGitDir(stat func(string) (os.FileInfo, error), readFile func(filenam
// GetStashEntries stash entries
func (c *GitCommand) GetStashEntries() []*StashEntry {
rawString, _ := c.OSCommand.RunCommandWithOutput("git stash list --pretty='%gs'")
// if we directly put this string in RunCommandWithOutput the compiler complains because it thinks it's a format string
unescaped := "git stash list --pretty='%gs'"
rawString, _ := c.OSCommand.RunCommandWithOutput(unescaped)
stashEntries := []*StashEntry{}
for i, line := range utils.SplitLines(rawString) {
stashEntries = append(stashEntries, stashEntryFromLine(line, i))
@@ -168,7 +177,7 @@ 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@{" + fmt.Sprint(index) + "}")
return c.OSCommand.RunCommandWithOutput("git stash show -p --color stash@{%d}", index)
}
// GetStatusFiles git status files
@@ -206,13 +215,13 @@ func (c *GitCommand) GetStatusFiles() []*File {
// StashDo modify stash
func (c *GitCommand) StashDo(index int, method string) error {
return c.OSCommand.RunCommand(fmt.Sprintf("git stash %s stash@{%d}", method, index))
return c.OSCommand.RunCommand("git stash %s stash@{%d}", method, index)
}
// StashSave save stash
// TODO: before calling this, check if there is anything to save
func (c *GitCommand) StashSave(message string) error {
return c.OSCommand.RunCommand(fmt.Sprintf("git stash save %s", c.OSCommand.Quote(message)))
return c.OSCommand.RunCommand("git stash save %s", c.OSCommand.Quote(message))
}
// MergeStatusFiles merge status files
@@ -256,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
}
@@ -264,23 +273,22 @@ func (c *GitCommand) ResetAndClean() error {
}
func (c *GitCommand) GetCurrentBranchUpstreamDifferenceCount() (string, string) {
return c.GetCommitDifferences("HEAD", "@{u}")
return c.GetCommitDifferences("HEAD", "HEAD@{u}")
}
func (c *GitCommand) GetBranchUpstreamDifferenceCount(branchName string) (string, string) {
upstream := "origin" // hardcoded for now
return c.GetCommitDifferences(branchName, fmt.Sprintf("%s/%s", upstream, branchName))
return c.GetCommitDifferences(branchName, branchName+"@{u}")
}
// GetCommitDifferences checks how many pushables/pullables there are for the
// current branch
func (c *GitCommand) GetCommitDifferences(from, to string) (string, string) {
command := "git rev-list %s..%s --count"
pushableCount, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf(command, to, from))
pushableCount, err := c.OSCommand.RunCommandWithOutput(command, to, from)
if err != nil {
return "?", "?"
}
pullableCount, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf(command, from, to))
pullableCount, err := c.OSCommand.RunCommandWithOutput(command, from, to)
if err != nil {
return "?", "?"
}
@@ -289,7 +297,7 @@ func (c *GitCommand) GetCommitDifferences(from, to string) (string, string) {
// RenameCommit renames the topmost commit with the given name
func (c *GitCommand) RenameCommit(name string) error {
return c.OSCommand.RunCommand(fmt.Sprintf("git commit --allow-empty --amend -m %s", c.OSCommand.Quote(name)))
return c.OSCommand.RunCommand("git commit --allow-empty --amend -m %s", c.OSCommand.Quote(name))
}
// RebaseBranch interactive rebases onto a branch
@@ -314,22 +322,25 @@ func (c *GitCommand) Fetch(unamePassQuestion func(string) string, canAskForCrede
// ResetToCommit reset to commit
func (c *GitCommand) ResetToCommit(sha string, strength string) error {
return c.OSCommand.RunCommand(fmt.Sprintf("git reset --%s %s", strength, sha))
return c.OSCommand.RunCommand("git reset --%s %s", strength, sha)
}
// NewBranch create new branch
func (c *GitCommand) NewBranch(name string) error {
return c.OSCommand.RunCommand(fmt.Sprintf("git checkout -b %s", name))
return c.OSCommand.RunCommand("git checkout -b %s", name)
}
// CurrentBranchName is a function.
func (c *GitCommand) CurrentBranchName() (string, error) {
branchName, err := c.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD")
if err != nil {
branchName, err = c.OSCommand.RunCommandWithOutput("git rev-parse --short HEAD")
if err != nil || branchName == "HEAD\n" {
output, err := c.OSCommand.RunCommandWithOutput("git branch --contains")
if err != nil {
return "", err
}
re := regexp.MustCompile(CurrentBranchNameRegex)
match := re.FindStringSubmatch(output)
branchName = match[1]
}
return utils.TrimTrailingNewline(branchName), nil
}
@@ -342,7 +353,7 @@ func (c *GitCommand) DeleteBranch(branch string, force bool) error {
command = "git branch -D"
}
return c.OSCommand.RunCommand(fmt.Sprintf("%s %s", command, branch))
return c.OSCommand.RunCommand("%s %s", command, branch)
}
// ListStash list stash
@@ -352,7 +363,7 @@ func (c *GitCommand) ListStash() (string, error) {
// Merge merge
func (c *GitCommand) Merge(branchName string) error {
return c.OSCommand.RunCommand(fmt.Sprintf("git merge --no-edit %s", branchName))
return c.OSCommand.RunCommand("git merge --no-edit %s", branchName)
}
// AbortMerge abort merge
@@ -409,18 +420,18 @@ func (c *GitCommand) Push(branchName string, force bool, upstream string, ask fu
setUpstreamArg = "--set-upstream " + upstream
}
cmd := fmt.Sprintf("git push %s %s", forceFlag, setUpstreamArg)
cmd := fmt.Sprintf("git push --follow-tags %s %s", forceFlag, setUpstreamArg)
return c.OSCommand.DetectUnamePass(cmd, ask)
}
// CatFile obtains the content of a file
func (c *GitCommand) CatFile(fileName string) (string, error) {
return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("cat %s", c.OSCommand.Quote(fileName)))
return c.OSCommand.RunCommandWithOutput("cat %s", c.OSCommand.Quote(fileName))
}
// StageFile stages a file
func (c *GitCommand) StageFile(fileName string) error {
return c.OSCommand.RunCommand(fmt.Sprintf("git add %s", c.OSCommand.Quote(fileName)))
return c.OSCommand.RunCommand("git add %s", c.OSCommand.Quote(fileName))
}
// StageAll stages all files
@@ -443,7 +454,7 @@ func (c *GitCommand) UnStageFile(fileName string, tracked bool) error {
// renamed files look like "file1 -> file2"
fileNames := strings.Split(fileName, " -> ")
for _, name := range fileNames {
if err := c.OSCommand.RunCommand(fmt.Sprintf(command, c.OSCommand.Quote(name))); err != nil {
if err := c.OSCommand.RunCommand(command, c.OSCommand.Quote(name)); err != nil {
return err
}
}
@@ -487,7 +498,7 @@ func (c *GitCommand) DiscardAllFileChanges(file *File) error {
// if the file isn't tracked, we assume you want to delete it
quotedFileName := c.OSCommand.Quote(file.Name)
if file.HasStagedChanges || file.HasMergeConflicts {
if err := c.OSCommand.RunCommand(fmt.Sprintf("git reset -- %s", quotedFileName)); err != nil {
if err := c.OSCommand.RunCommand("git reset -- %s", quotedFileName); err != nil {
return err
}
}
@@ -501,16 +512,16 @@ func (c *GitCommand) DiscardAllFileChanges(file *File) error {
// DiscardUnstagedFileChanges directly
func (c *GitCommand) DiscardUnstagedFileChanges(file *File) error {
quotedFileName := c.OSCommand.Quote(file.Name)
return c.OSCommand.RunCommand(fmt.Sprintf("git checkout -- %s", quotedFileName))
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 {
forceArg = "--force "
}
return c.OSCommand.RunCommand(fmt.Sprintf("git checkout %s %s", forceArg, branch))
return c.OSCommand.RunCommand("git checkout %s %s", forceArg, branch)
}
// PrepareCommitSubProcess prepares a subprocess for `git commit`
@@ -527,7 +538,12 @@ 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(fmt.Sprintf("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium -100 %s", branchName))
return c.OSCommand.RunCommandWithOutput("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium -100 %s", branchName)
}
func (c *GitCommand) GetUpstreamForBranch(branchName string) (string, error) {
output, err := c.OSCommand.RunCommandWithOutput("git rev-parse --abbrev-ref --symbolic-full-name %s@{u}", branchName)
return strings.TrimSpace(output), err
}
// Ignore adds a file to the gitignore for the repo
@@ -537,13 +553,13 @@ func (c *GitCommand) Ignore(filename string) error {
// Show shows the diff of a commit
func (c *GitCommand) Show(sha string) (string, error) {
show, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git show --color --no-renames %s", sha))
show, err := c.OSCommand.RunCommandWithOutput("git show --color --no-renames %s", sha)
if err != nil {
return "", err
}
// if this is a merge commit, we need to go a step further and get the diff between the two branches we merged
revList, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git rev-list -1 --merges %s^...%s", sha, sha))
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
@@ -565,7 +581,7 @@ func (c *GitCommand) Show(sha string) (string, error) {
return show, nil
}
mergeDiff, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git diff --color %s...%s", secondLineWords[1], secondLineWords[2]))
mergeDiff, err := c.OSCommand.RunCommandWithOutput("git diff --color %s...%s", secondLineWords[1], secondLineWords[2])
if err != nil {
return "", err
}
@@ -580,10 +596,10 @@ func (c *GitCommand) GetRemoteURL() string {
// CheckRemoteBranchExists Returns remote branch
func (c *GitCommand) CheckRemoteBranchExists(branch *Branch) bool {
_, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf(
_, err := c.OSCommand.RunCommandWithOutput(
"git show-ref --verify -- refs/remotes/origin/%s",
branch.Name,
))
)
return err == nil
}
@@ -605,16 +621,14 @@ func (c *GitCommand) Diff(file *File, plain bool, cached bool) string {
colorArg = ""
}
command := fmt.Sprintf("git diff %s %s %s %s", colorArg, cachedArg, trackedArg, fileName)
// for now we assume an error means the file was deleted
s, _ := c.OSCommand.RunCommandWithOutput(command)
s, _ := c.OSCommand.RunCommandWithOutput("git diff %s %s %s %s", colorArg, cachedArg, trackedArg, fileName)
return s
}
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
}
@@ -624,12 +638,11 @@ func (c *GitCommand) ApplyPatch(patch string, flags ...string) error {
flagStr += " --" + flag
}
return c.OSCommand.RunCommand(fmt.Sprintf("git apply %s %s", flagStr, c.OSCommand.Quote(filepath)))
return c.OSCommand.RunCommand("git apply %s %s", flagStr, c.OSCommand.Quote(filepath))
}
func (c *GitCommand) FastForward(branchName string) error {
upstream := "origin" // hardcoding for now
return c.OSCommand.RunCommand(fmt.Sprintf("git fetch %s %s:%s", upstream, branchName, branchName))
func (c *GitCommand) FastForward(branchName string, remoteName string, remoteBranchName string) error {
return c.OSCommand.RunCommand("git fetch %s %s:%s", remoteName, remoteBranchName, branchName)
}
func (c *GitCommand) RunSkipEditorCommand(command string) error {
@@ -849,7 +862,7 @@ func (c *GitCommand) MoveTodoDown(index int) error {
// Revert reverts the selected commit by sha
func (c *GitCommand) Revert(sha string) error {
return c.OSCommand.RunCommand(fmt.Sprintf("git revert %s", sha))
return c.OSCommand.RunCommand("git revert %s", sha)
}
// CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD
@@ -869,8 +882,7 @@ func (c *GitCommand) CherryPickCommits(commits []*Commit) error {
// GetCommitFiles get the specified commit files
func (c *GitCommand) GetCommitFiles(commitSha string, patchManager *PatchManager) ([]*CommitFile, error) {
cmd := fmt.Sprintf("git show --pretty= --name-only --no-renames %s", commitSha)
files, err := c.OSCommand.RunCommandWithOutput(cmd)
files, err := c.OSCommand.RunCommandWithOutput("git show --pretty= --name-only --no-renames %s", commitSha)
if err != nil {
return nil, err
}
@@ -900,14 +912,12 @@ func (c *GitCommand) ShowCommitFile(commitSha, fileName string, plain bool) (str
if plain {
colorArg = ""
}
cmd := fmt.Sprintf("git show --no-renames %s %s -- %s", colorArg, commitSha, fileName)
return c.OSCommand.RunCommandWithOutput(cmd)
return c.OSCommand.RunCommandWithOutput("git show --no-renames %s %s -- %s", colorArg, commitSha, fileName)
}
// CheckoutFile checks out the file for the given commit
func (c *GitCommand) CheckoutFile(commitSha, fileName string) error {
cmd := fmt.Sprintf("git checkout %s %s", commitSha, fileName)
return c.OSCommand.RunCommand(cmd)
return c.OSCommand.RunCommand("git checkout %s %s", commitSha, fileName)
}
// DiscardOldFileChanges discards changes to a file from an old commit
@@ -917,7 +927,7 @@ func (c *GitCommand) DiscardOldFileChanges(commits []*Commit, commitIndex int, f
}
// check if file exists in previous commit (this command returns an error if the file doesn't exist)
if err := c.OSCommand.RunCommand(fmt.Sprintf("git cat-file -e HEAD^:%s", fileName)); err != nil {
if err := c.OSCommand.RunCommand("git cat-file -e HEAD^:%s", fileName); err != nil {
if err := c.OSCommand.Remove(fileName); err != nil {
return err
}
@@ -951,26 +961,24 @@ func (c *GitCommand) RemoveUntrackedFiles() error {
return c.OSCommand.RunCommand("git clean -fd")
}
// ResetHardHead runs `git reset --hard HEAD`
func (c *GitCommand) ResetHardHead() error {
return c.OSCommand.RunCommand("git reset --hard HEAD")
// ResetHardHead runs `git reset --hard`
func (c *GitCommand) ResetHard(ref string) error {
return c.OSCommand.RunCommand("git reset --hard " + ref)
}
// ResetSoftHead runs `git reset --soft HEAD`
func (c *GitCommand) ResetSoftHead() error {
return c.OSCommand.RunCommand("git reset --soft HEAD")
// ResetSoft runs `git reset --soft HEAD`
func (c *GitCommand) ResetSoft(ref string) error {
return c.OSCommand.RunCommand("git reset --soft " + ref)
}
// DiffCommits show diff between commits
func (c *GitCommand) DiffCommits(sha1, sha2 string) (string, error) {
cmd := fmt.Sprintf("git diff --color %s %s", sha1, sha2)
return c.OSCommand.RunCommandWithOutput(cmd)
return c.OSCommand.RunCommandWithOutput("git diff --color %s %s", sha1, sha2)
}
// CreateFixupCommit creates a commit that fixes up a previous commit
func (c *GitCommand) CreateFixupCommit(sha string) error {
cmd := fmt.Sprintf("git commit --fixup=%s", sha)
return c.OSCommand.RunCommand(cmd)
return c.OSCommand.RunCommand("git commit --fixup=%s", sha)
}
// SquashAllAboveFixupCommits squashes all fixup! commits above the given one
@@ -1052,3 +1060,81 @@ func (c *GitCommand) BeginInteractiveRebaseForCommit(commits []*Commit, commitIn
return nil
}
func (c *GitCommand) SetUpstreamBranch(upstream string) error {
return c.OSCommand.RunCommand("git branch -u %s", upstream)
}
func (c *GitCommand) AddRemote(name string, url string) error {
return c.OSCommand.RunCommand("git remote add %s %s", name, url)
}
func (c *GitCommand) RemoveRemote(name string) error {
return c.OSCommand.RunCommand("git remote remove %s", name)
}
func (c *GitCommand) IsHeadDetached() bool {
err := c.OSCommand.RunCommand("git symbolic-ref -q HEAD")
return err != nil
}
func (c *GitCommand) DeleteRemoteBranch(remoteName string, branchName string) error {
return c.OSCommand.RunCommand("git push %s --delete %s", remoteName, branchName)
}
func (c *GitCommand) SetBranchUpstream(remoteName string, remoteBranchName string, branchName string) error {
return c.OSCommand.RunCommand("git branch --set-upstream-to=%s/%s %s", remoteName, remoteBranchName, branchName)
}
func (c *GitCommand) RenameRemote(oldRemoteName string, newRemoteName string) error {
return c.OSCommand.RunCommand("git remote rename %s %s", oldRemoteName, newRemoteName)
}
func (c *GitCommand) UpdateRemoteUrl(remoteName string, updatedUrl string) error {
return c.OSCommand.RunCommand("git remote set-url %s %s", remoteName, updatedUrl)
}
func (c *GitCommand) CreateLightweightTag(tagName string, commitSha string) error {
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)
}
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
}
+15 -13
View File
@@ -58,14 +58,14 @@ func (f fileInfoMock) Sys() interface{} {
func TestVerifyInGitRepo(t *testing.T) {
type scenario struct {
testName string
runCmd func(string) error
runCmd func(string, ...interface{}) error
test func(error)
}
scenarios := []scenario{
{
"Valid git repository",
func(string) error {
func(string, ...interface{}) error {
return nil
},
func(err error) {
@@ -74,7 +74,7 @@ func TestVerifyInGitRepo(t *testing.T) {
},
{
"Not a valid git repository",
func(string) error {
func(string, ...interface{}) error {
return fmt.Errorf("fatal: Not a git repository (or any of the parent directories): .git")
},
func(err error) {
@@ -990,7 +990,7 @@ func TestGitCommandPush(t *testing.T) {
"Push with force disabled",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push"}, args)
assert.EqualValues(t, []string{"push", "--follow-tags"}, args)
return exec.Command("echo")
},
@@ -1003,7 +1003,7 @@ func TestGitCommandPush(t *testing.T) {
"Push with force enabled",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push", "--force-with-lease"}, args)
assert.EqualValues(t, []string{"push", "--follow-tags", "--force-with-lease"}, args)
return exec.Command("echo")
},
@@ -1016,7 +1016,7 @@ func TestGitCommandPush(t *testing.T) {
"Push with an error occurring",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push"}, args)
assert.EqualValues(t, []string{"push", "--follow-tags"}, args)
return exec.Command("test")
},
false,
@@ -1639,7 +1639,7 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
},
},
{
"falls back to git rev-parse if symbolic-ref fails",
"falls back to git `git branch --contains` if symbolic-ref fails",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
@@ -1647,9 +1647,9 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("test")
case "rev-parse":
assert.EqualValues(t, []string{"rev-parse", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
case "branch":
assert.EqualValues(t, []string{"branch", "--contains"}, args)
return exec.Command("echo", "* master")
}
return nil
@@ -2106,10 +2106,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 +2118,7 @@ func TestGitCommandResetHardHead(t *testing.T) {
scenarios := []scenario{
{
"valid case",
"HEAD",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: `git reset --hard HEAD`,
@@ -2134,7 +2136,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))
})
}
}
+58
View File
@@ -0,0 +1,58 @@
package commands
import (
"fmt"
"regexp"
"sort"
"strings"
)
func (c *GitCommand) GetRemotes() ([]*Remote, error) {
// get remote branches
unescaped := "git branch -r"
remoteBranchesStr, err := c.OSCommand.RunCommandWithOutput(unescaped)
if err != nil {
return nil, err
}
goGitRemotes, err := c.Repo.Remotes()
if err != nil {
return nil, err
}
// first step is to get our remotes from go-git
remotes := make([]*Remote, len(goGitRemotes))
for i, goGitRemote := range goGitRemotes {
remoteName := goGitRemote.Config().Name
re := regexp.MustCompile(fmt.Sprintf(`%s\/([\S]+)`, remoteName))
matches := re.FindAllStringSubmatch(remoteBranchesStr, -1)
branches := make([]*RemoteBranch, len(matches))
for j, match := range matches {
branches[j] = &RemoteBranch{
Name: match[1],
RemoteName: remoteName,
}
}
remotes[i] = &Remote{
Name: goGitRemote.Config().Name,
Urls: goGitRemote.Config().URLs,
Branches: branches,
}
}
// now lets sort our remotes by name alphabetically
sort.Slice(remotes, func(i, j int) bool {
// we want origin at the top because we'll be most likely to want it
if remotes[i].Name == "origin" {
return true
}
if remotes[j].Name == "origin" {
return false
}
return strings.ToLower(remotes[i].Name) < strings.ToLower(remotes[j].Name)
})
return remotes, nil
}
+87
View File
@@ -0,0 +1,87 @@
package commands
import (
"regexp"
"sort"
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
)
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`)
if err != nil {
return nil, err
}
content := utils.TrimTrailingNewline(remoteBranchesStr)
if content == "" {
return nil, nil
}
split := strings.Split(content, "\n")
// first step is to get our remotes from go-git
tags := make([]*Tag, len(split))
for i, tagName := range split {
tags[i] = &Tag{
Name: tagName,
}
}
// now lets sort our tags by name numerically
re := regexp.MustCompile(semverRegex)
// the reason this is complicated is because we're both sorting alphabetically
// and when we're dealing with semver strings
sort.Slice(tags, func(i, j int) bool {
a := tags[i].Name
b := tags[j].Name
matchA := re.FindStringSubmatch(a)
matchB := re.FindStringSubmatch(b)
if len(matchA) > 0 && len(matchB) > 0 {
numbersA := strings.Split(matchA[1], ".")
numbersB := strings.Split(matchB[1], ".")
k := 0
for {
if len(numbersA) == k && len(numbersB) == k {
break
}
if len(numbersA) == k {
return true
}
if len(numbersB) == k {
return false
}
if convertToInt(numbersA[k]) < convertToInt(numbersB[k]) {
return true
}
if convertToInt(numbersA[k]) > convertToInt(numbersB[k]) {
return false
}
k++
}
return strings.ToLower(matchA[3]) < strings.ToLower(matchB[3])
}
return strings.ToLower(a) < strings.ToLower(b)
})
return tags, nil
}
+14 -4
View File
@@ -1,6 +1,7 @@
package commands
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
@@ -58,7 +59,16 @@ func (c *OSCommand) SetCommand(cmd func(string, ...string) *exec.Cmd) {
}
// RunCommandWithOutput wrapper around commands returning their output and error
func (c *OSCommand) RunCommandWithOutput(command string) (string, 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
// with a percent sign because it thinks it's supposed to be a formatString when
// in that case it's not. To get around that error you'll need to define the string
// in a variable and pass the variable into RunCommandWithOutput.
func (c *OSCommand) RunCommandWithOutput(formatString string, formatArgs ...interface{}) (string, error) {
command := formatString
if formatArgs != nil {
command = fmt.Sprintf(formatString, formatArgs...)
}
c.Log.WithField("command", command).Info("RunCommand")
cmd := c.ExecutableFromString(command)
return sanitisedCommandOutput(cmd.CombinedOutput())
@@ -114,8 +124,8 @@ func (c *OSCommand) DetectUnamePass(command string, ask func(string) string) err
}
// RunCommand runs a command and just returns the error
func (c *OSCommand) RunCommand(command string) error {
_, err := c.RunCommandWithOutput(command)
func (c *OSCommand) RunCommand(formatString string, formatArgs ...interface{}) error {
_, err := c.RunCommandWithOutput(formatString, formatArgs...)
return err
}
@@ -316,7 +326,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
+24
View File
@@ -0,0 +1,24 @@
package commands
import (
"fmt"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// Remote : A git remote
type Remote struct {
Name string
Urls []string
Selected bool
Branches []*RemoteBranch
}
// GetDisplayStrings returns the display string of a remote
func (r *Remote) GetDisplayStrings(isFocused bool) []string {
branchCount := len(r.Branches)
return []string{r.Name, utils.ColoredString(fmt.Sprintf("%d branches", branchCount), color.FgBlue)}
}
+19
View File
@@ -0,0 +1,19 @@
package commands
import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
// Remote Branch : A git remote branch
type RemoteBranch struct {
Name string
Selected bool
RemoteName string
}
// GetDisplayStrings returns the display string of branch
func (b *RemoteBranch) GetDisplayStrings(isFocused bool) []string {
displayName := utils.ColoredString(b.Name, GetBranchColor(b.Name))
return []string{displayName}
}
+11
View File
@@ -0,0 +1,11 @@
package commands
// Tag : A git tag
type Tag struct {
Name string
}
// GetDisplayStrings returns the display string of a remote
func (r *Tag) GetDisplayStrings(isFocused bool) []string {
return []string{r.Name}
}
+92
View File
@@ -265,6 +265,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'
`)
}
+135 -62
View File
@@ -4,8 +4,10 @@ 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
@@ -19,14 +21,6 @@ func (gui *Gui) getSelectedBranch() *commands.Branch {
return gui.State.Branches[selectedLine]
}
func (gui *Gui) handleBranchesClick(g *gocui.Gui, v *gocui.View) error {
itemCount := len(gui.State.Branches)
handleSelect := gui.handleBranchSelect
selectedLine := &gui.State.Panels.Branches.SelectedLine
return gui.handleClick(v, itemCount, selectedLine, handleSelect)
}
// may want to standardise how these select methods work
func (gui *Gui) handleBranchSelect(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
@@ -53,11 +47,15 @@ func (gui *Gui) handleBranchSelect(g *gocui.Gui, v *gocui.View) error {
_ = 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", graph)
_ = gui.renderString(g, "main", fmt.Sprintf("%s → %s\n\n%s", utils.ColoredString(branch.Name, color.FgGreen), utils.ColoredString(upstream, color.FgRed), graph))
}()
return nil
}
@@ -78,6 +76,14 @@ func (gui *Gui) RenderSelectedBranchUpstreamDifferences() error {
// gui.refreshStatus is called at the end of this because that's when we can
// be sure there is a state.Branches array to pick the current branch from
func (gui *Gui) refreshBranches(g *gocui.Gui) error {
if err := gui.refreshRemotes(); err != nil {
return err
}
if err := gui.refreshTags(); err != nil {
return err
}
g.Update(func(g *gocui.Gui) error {
builder, err := commands.NewBranchListBuilder(gui.Log, gui.GitCommand)
if err != nil {
@@ -85,9 +91,12 @@ func (gui *Gui) refreshBranches(g *gocui.Gui) error {
}
gui.State.Branches = builder.Build()
gui.refreshSelectedLine(&gui.State.Panels.Branches.SelectedLine, len(gui.State.Branches))
if err := gui.RenderSelectedBranchUpstreamDifferences(); err != nil {
return err
// 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 {
return err
}
}
return gui.refreshStatus(g)
@@ -95,32 +104,20 @@ func (gui *Gui) refreshBranches(g *gocui.Gui) error {
return nil
}
func (gui *Gui) handleBranchesNextLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
func (gui *Gui) renderLocalBranchesWithSelection() error {
branchesView := gui.getBranchesView()
panelState := gui.State.Panels.Branches
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Branches), false)
if err := gui.resetOrigin(gui.getMainView()); err != nil {
gui.refreshSelectedLine(&gui.State.Panels.Branches.SelectedLine, len(gui.State.Branches))
if err := gui.renderListPanel(branchesView, gui.State.Branches); err != nil {
return err
}
return gui.handleBranchSelect(gui.g, v)
}
func (gui *Gui) handleBranchesPrevLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
if gui.g.CurrentView() == branchesView && branchesView.Context == "local-branches" {
if err := gui.handleBranchSelect(gui.g, branchesView); err != nil {
return err
}
}
panelState := gui.State.Panels.Branches
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Branches), true)
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
return gui.handleBranchSelect(gui.g, v)
return nil
}
// specific functions
@@ -133,7 +130,7 @@ func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error {
return gui.createErrorPanel(g, gui.Tr.SLocalize("AlreadyCheckedOutBranch"))
}
branch := gui.getSelectedBranch()
return gui.handleCheckoutBranch(branch.Name)
return gui.handleCheckoutRef(branch.Name)
}
func (gui *Gui) handleCreatePullRequestPress(g *gocui.Gui, v *gocui.View) error {
@@ -170,17 +167,17 @@ func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
}, nil)
}
func (gui *Gui) handleCheckoutBranch(branchName string) error {
if err := gui.GitCommand.Checkout(branchName, false); err != nil {
func (gui *Gui) handleCheckoutRef(ref string) error {
if err := gui.GitCommand.Checkout(ref, false); err != nil {
// note, this will only work for english-language git commands. If we force git to use english, and the error isn't this one, then the user will receive an english command they may not understand. I'm not sure what the best solution to this is. Running the command once in english and a second time in the native language is one option
if strings.Contains(err.Error(), "Please commit your changes or stash them before you switch branch") {
// offer to autostash changes
return gui.createConfirmationPanel(gui.g, gui.getBranchesView(), true, gui.Tr.SLocalize("AutoStashTitle"), gui.Tr.SLocalize("AutoStashPrompt"), func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.StashSave(gui.Tr.SLocalize("StashPrefix") + branchName); err != nil {
if err := gui.GitCommand.StashSave(gui.Tr.SLocalize("StashPrefix") + ref); err != nil {
return gui.createErrorPanel(g, err.Error())
}
if err := gui.GitCommand.Checkout(branchName, false); err != nil {
if err := gui.GitCommand.Checkout(ref, false); err != nil {
return gui.createErrorPanel(g, err.Error())
}
@@ -203,18 +200,27 @@ 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
}
func (gui *Gui) getCheckedOutBranch() *commands.Branch {
if len(gui.State.Branches) == 0 {
return nil
}
return gui.State.Branches[0]
}
func (gui *Gui) handleNewBranch(g *gocui.Gui, v *gocui.View) error {
branch := gui.State.Branches[0]
branch := gui.getCheckedOutBranch()
message := gui.Tr.TemplateLocalize(
"NewBranchNameBranchOff",
Teml{
@@ -244,7 +250,7 @@ func (gui *Gui) deleteBranch(g *gocui.Gui, v *gocui.View, force bool) error {
if selectedBranch == nil {
return nil
}
checkedOutBranch := gui.State.Branches[0]
checkedOutBranch := gui.getCheckedOutBranch()
if checkedOutBranch.Name == selectedBranch.Name {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantDeleteCheckOutBranch"))
}
@@ -277,44 +283,54 @@ func (gui *Gui) deleteNamedBranch(g *gocui.Gui, v *gocui.View, selectedBranch *c
}, nil)
}
func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error {
checkedOutBranch := gui.State.Branches[0].Name
selectedBranch := gui.getSelectedBranch().Name
if checkedOutBranch == selectedBranch {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantMergeBranchIntoItself"))
func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error {
if gui.GitCommand.IsHeadDetached() {
return gui.createErrorPanel(gui.g, "Cannot merge branch in detached head state. You might have checked out a commit directly or a remote branch, in which case you should checkout the local branch you want to be on")
}
checkedOutBranchName := gui.getCheckedOutBranch().Name
if checkedOutBranchName == branchName {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("CantMergeBranchIntoItself"))
}
prompt := gui.Tr.TemplateLocalize(
"ConfirmMerge",
Teml{
"checkedOutBranch": checkedOutBranch,
"selectedBranch": selectedBranch,
"checkedOutBranch": checkedOutBranchName,
"selectedBranch": branchName,
},
)
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("MergingTitle"), prompt,
return gui.createConfirmationPanel(gui.g, gui.getBranchesView(), true, gui.Tr.SLocalize("MergingTitle"), prompt,
func(g *gocui.Gui, v *gocui.View) error {
err := gui.GitCommand.Merge(selectedBranch)
err := gui.GitCommand.Merge(branchName)
return gui.handleGenericMergeCommandResult(err)
}, nil)
}
func (gui *Gui) handleRebase(g *gocui.Gui, v *gocui.View) error {
checkedOutBranch := gui.State.Branches[0].Name
selectedBranch := gui.getSelectedBranch().Name
if selectedBranch == checkedOutBranch {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantRebaseOntoSelf"))
func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error {
selectedBranchName := gui.getSelectedBranch().Name
return gui.mergeBranchIntoCheckedOutBranch(selectedBranchName)
}
func (gui *Gui) handleRebaseOntoLocalBranch(g *gocui.Gui, v *gocui.View) error {
selectedBranchName := gui.getSelectedBranch().Name
return gui.handleRebaseOntoBranch(selectedBranchName)
}
func (gui *Gui) handleRebaseOntoBranch(selectedBranchName string) error {
checkedOutBranch := gui.getCheckedOutBranch().Name
if selectedBranchName == checkedOutBranch {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("CantRebaseOntoSelf"))
}
prompt := gui.Tr.TemplateLocalize(
"ConfirmRebase",
Teml{
"checkedOutBranch": checkedOutBranch,
"selectedBranch": selectedBranch,
"selectedBranch": selectedBranchName,
},
)
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("RebasingTitle"), prompt,
return gui.createConfirmationPanel(gui.g, gui.getBranchesView(), true, gui.Tr.SLocalize("RebasingTitle"), prompt,
func(g *gocui.Gui, v *gocui.View) error {
err := gui.GitCommand.RebaseBranch(selectedBranch)
err := gui.GitCommand.RebaseBranch(selectedBranchName)
return gui.handleGenericMergeCommandResult(err)
}, nil)
}
@@ -333,17 +349,27 @@ func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
if branch.Pushables != "0" {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("FwdCommitsToPush"))
}
upstream := "origin" // hardcoding for now
upstream, err := gui.GitCommand.GetUpstreamForBranch(branch.Name)
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
split := strings.Split(upstream, "/")
remoteName := split[0]
remoteBranchName := strings.Join(split[1:], "/")
message := gui.Tr.TemplateLocalize(
"Fetching",
Teml{
"from": fmt.Sprintf("%s/%s", upstream, branch.Name),
"from": fmt.Sprintf("%s/%s", remoteName, remoteBranchName),
"to": branch.Name,
},
)
go func() {
_ = gui.createLoaderPanel(gui.g, v, message)
if err := gui.GitCommand.FastForward(branch.Name); err != nil {
if err := gui.GitCommand.FastForward(branch.Name, remoteName, remoteBranchName); err != nil {
_ = gui.createErrorPanel(gui.g, err.Error())
} else {
_ = gui.closeConfirmationPrompt(gui.g, true)
@@ -352,3 +378,50 @@ func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
}()
return nil
}
func (gui *Gui) onBranchesTabClick(tabIndex int) error {
contexts := []string{"local-branches", "remotes", "tags"}
branchesView := gui.getBranchesView()
branchesView.TabIndex = tabIndex
return gui.switchBranchesPanelContext(contexts[tabIndex])
}
func (gui *Gui) switchBranchesPanelContext(context string) error {
branchesView := gui.getBranchesView()
branchesView.Context = context
contextTabIndexMap := map[string]int{
"local-branches": 0,
"remotes": 1,
"remote-branches": 1,
"tags": 2,
}
branchesView.TabIndex = contextTabIndexMap[context]
switch context {
case "local-branches":
return gui.renderLocalBranchesWithSelection()
case "remotes":
return gui.renderRemotesWithSelection()
case "remote-branches":
return gui.renderRemoteBranchesWithSelection()
case "tags":
return gui.renderTagsWithSelection()
}
return nil
}
func (gui *Gui) handleNextBranchesTab(g *gocui.Gui, v *gocui.View) error {
return gui.onBranchesTabClick(
utils.ModuloWithWrap(v.TabIndex+1, len(v.Tabs)),
)
}
func (gui *Gui) handlePrevBranchesTab(g *gocui.Gui, v *gocui.View) error {
return gui.onBranchesTabClick(
utils.ModuloWithWrap(v.TabIndex-1, len(v.Tabs)),
)
}
+2 -20
View File
@@ -50,26 +50,8 @@ func (gui *Gui) handleCommitFileSelect(g *gocui.Gui, v *gocui.View) error {
return gui.renderString(g, "main", commitText)
}
func (gui *Gui) handleCommitFilesNextLine(g *gocui.Gui, v *gocui.View) error {
panelState := gui.State.Panels.CommitFiles
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.CommitFiles), false)
return gui.handleCommitFileSelect(gui.g, v)
}
func (gui *Gui) handleCommitFilesPrevLine(g *gocui.Gui, v *gocui.View) error {
panelState := gui.State.Panels.CommitFiles
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.CommitFiles), true)
return gui.handleCommitFileSelect(gui.g, v)
}
func (gui *Gui) handleSwitchToCommitsPanel(g *gocui.Gui, v *gocui.View) error {
commitsView, err := g.View("commits")
if err != nil {
return err
}
return gui.switchFocus(g, v, commitsView)
return gui.switchFocus(g, v, gui.getCommitsView())
}
func (gui *Gui) handleCheckoutCommitFile(g *gocui.Gui, v *gocui.View) error {
@@ -208,7 +190,7 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
}
}
if err := gui.changeContext("patch-building"); err != nil {
if err := gui.changeMainViewsContext("patch-building"); err != nil {
return err
}
if err := gui.switchFocus(gui.g, gui.getCommitFilesView(), gui.getMainView()); err != nil {
+35
View File
@@ -83,3 +83,38 @@ func (gui *Gui) RenderCommitLength() {
v := gui.getCommitMessageView()
v.Subtitle = gui.getBufferLength(v)
}
// we've just copy+pasted the editor from gocui to here so that we can also re-
// render the commit message length on each keypress
func (gui *Gui) commitMessageEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
switch {
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
v.EditDelete(true)
case key == gocui.KeyDelete:
v.EditDelete(false)
case key == gocui.KeyArrowDown:
v.MoveCursor(0, 1, false)
case key == gocui.KeyArrowUp:
v.MoveCursor(0, -1, false)
case key == gocui.KeyArrowLeft:
v.MoveCursor(-1, 0, false)
case key == gocui.KeyArrowRight:
v.MoveCursor(1, 0, false)
case key == gocui.KeyTab:
v.EditNewLine()
case key == gocui.KeySpace:
v.EditWrite(' ')
case key == gocui.KeyInsert:
v.Overwrite = !v.Overwrite
case key == gocui.KeyCtrlU:
v.EditDeleteToStartOfLine()
case key == gocui.KeyCtrlA:
v.EditGotoToStartOfLine()
case key == gocui.KeyCtrlE:
v.EditGotoToEndOfLine()
default:
v.EditWrite(ch)
}
gui.RenderCommitLength()
}
+107 -64
View File
@@ -23,27 +23,6 @@ func (gui *Gui) getSelectedCommit(g *gocui.Gui) *commands.Commit {
return gui.State.Commits[selectedLine]
}
func (gui *Gui) handleCommitsClick(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
prevSelectedLineIdx := gui.State.Panels.Commits.SelectedLine
newSelectedLineIdx := v.SelectedLineIdx()
if newSelectedLineIdx > len(gui.State.Commits)-1 {
return gui.handleCommitSelect(gui.g, v)
}
gui.State.Panels.Commits.SelectedLine = newSelectedLineIdx
if prevSelectedLineIdx == newSelectedLineIdx && gui.currentViewName() == v.Name() {
return gui.handleSwitchToCommitFilesPanel(gui.g, v)
} else {
return gui.handleCommitSelect(gui.g, v)
}
}
func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
@@ -95,23 +74,20 @@ func (gui *Gui) refreshCommits(g *gocui.Gui) error {
}
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)
// doing this async because it shouldn't hold anything up
go func() {
if err := gui.refreshReflogCommits(); err != nil {
_ = gui.createErrorPanel(gui.g, err.Error())
}
}()
gui.refreshStatus(g)
if g.CurrentView() == v {
gui.handleCommitSelect(g, v)
if gui.getCommitsView().Context == "branch-commits" {
if err := gui.renderBranchCommitsWithSelection(); err != nil {
return err
}
}
if g.CurrentView() == gui.getCommitFilesView() || (g.CurrentView() == gui.getMainView() || gui.State.Context == "patch-building") {
if g.CurrentView() == gui.getCommitFilesView() || (g.CurrentView() == gui.getMainView() || gui.State.MainContext == "patch-building") {
return gui.refreshCommitFilesView()
}
return nil
@@ -119,34 +95,6 @@ func (gui *Gui) refreshCommits(g *gocui.Gui) error {
return nil
}
func (gui *Gui) handleCommitsNextLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
panelState := gui.State.Panels.Commits
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Commits), false)
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
return gui.handleCommitSelect(gui.g, v)
}
func (gui *Gui) handleCommitsPrevLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
panelState := gui.State.Panels.Commits
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Commits), true)
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
return gui.handleCommitSelect(gui.g, v)
}
// specific functions
func (gui *Gui) handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error {
@@ -405,7 +353,7 @@ func (gui *Gui) handleCommitPick(g *gocui.Gui, v *gocui.View) error {
// at this point we aren't actually rebasing so we will interpret this as an
// attempt to pull. We might revoke this later after enabling configurable keybindings
return gui.pullFiles(g, v)
return gui.handlePullFiles(g, v)
}
func (gui *Gui) handleCommitRevert(g *gocui.Gui, v *gocui.View) error {
@@ -633,3 +581,98 @@ func (gui *Gui) handleCreateCommitResetMenu(g *gocui.Gui, v *gocui.View) error {
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
commit := gui.getSelectedCommit(g)
if commit == nil {
return nil
}
return gui.handleCreateLightweightTag(commit.Sha)
}
func (gui *Gui) handleCreateLightweightTag(commitSha string) error {
return gui.createPromptPanel(gui.g, gui.getCommitsView(), gui.Tr.SLocalize("TagNameTitle"), "", func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.CreateLightweightTag(v.Buffer(), commitSha); err != nil {
return gui.createErrorPanel(g, err.Error())
}
if err := gui.refreshCommits(g); err != nil {
return gui.createErrorPanel(g, err.Error())
}
if err := gui.refreshTags(); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return 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)),
)
}
+33 -35
View File
@@ -17,11 +17,13 @@ import (
func (gui *Gui) wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.View) error, returnFocusOnClose bool) func(*gocui.Gui, *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
if function != nil {
if err := function(g, v); err != nil {
return err
}
}
return gui.closeConfirmationPrompt(g, returnFocusOnClose)
}
}
@@ -31,6 +33,7 @@ func (gui *Gui) closeConfirmationPrompt(g *gocui.Gui, returnFocusOnClose bool) e
if err != nil {
return nil // if it's already been closed we can just return
}
view.Editable = false
if returnFocusOnClose {
if err := gui.returnFocus(g, view); err != nil {
panic(err)
@@ -64,20 +67,6 @@ func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, wrap bool, prompt s
height/2 + panelHeight/2
}
func (gui *Gui) createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, initialContent string, handleConfirm func(*gocui.Gui, *gocui.View) error) error {
gui.onNewPopupPanel()
confirmationView, err := gui.prepareConfirmationPanel(currentView, title, initialContent, false)
if err != nil {
return err
}
confirmationView.Editable = true
if err := gui.renderString(g, "confirmation", initialContent); err != nil {
return err
}
// in the future we might want to give createPromptPanel the returnFocusOnClose arg too, but for now we're always setting it to true
return gui.setKeyBindings(g, handleConfirm, nil, true)
}
func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt string, hasLoader bool) (*gocui.View, error) {
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, true, prompt)
confirmationView, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0)
@@ -105,35 +94,31 @@ func (gui *Gui) onNewPopupPanel() {
}
}
func (gui *Gui) createLoaderPanel(g *gocui.Gui, currentView *gocui.View, prompt string) error {
return gui.createPopupPanel(g, currentView, "", prompt, true, true, nil, nil)
}
// it is very important that within this function we never include the original prompt in any error messages, because it may contain e.g. a user password
func (gui *Gui) createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, returnFocusOnClose bool, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
return gui.createPopupPanel(g, currentView, title, prompt, false, returnFocusOnClose, handleConfirm, handleClose)
}
func (gui *Gui) createPopupPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, hasLoader bool, returnFocusOnClose bool, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
func (gui *Gui) createPopupPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, hasLoader bool, returnFocusOnClose bool, editable bool, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
gui.onNewPopupPanel()
g.Update(func(g *gocui.Gui) error {
// delete the existing confirmation panel if it exists
if view, _ := g.View("confirmation"); view != nil {
if err := gui.closeConfirmationPrompt(g, true); err != nil {
errMessage := gui.Tr.TemplateLocalize(
"CantCloseConfirmationPrompt",
Teml{
"error": err.Error(),
},
)
gui.Log.Error(errMessage)
gui.Log.Error(err)
}
}
confirmationView, err := gui.prepareConfirmationPanel(currentView, title, prompt, hasLoader)
if err != nil {
return err
}
confirmationView.Editable = false
confirmationView.Editable = editable
if editable {
go func() {
// TODO: remove this wait (right now if you remove it the EditGotoToEndOfLine method doesn't seem to work)
time.Sleep(time.Millisecond)
gui.g.Update(func(g *gocui.Gui) error {
confirmationView.EditGotoToEndOfLine()
return nil
})
}()
}
if err := gui.renderString(g, "confirmation", prompt); err != nil {
return err
}
@@ -142,6 +127,19 @@ func (gui *Gui) createPopupPanel(g *gocui.Gui, currentView *gocui.View, title, p
return nil
}
func (gui *Gui) createLoaderPanel(g *gocui.Gui, currentView *gocui.View, prompt string) error {
return gui.createPopupPanel(g, currentView, "", prompt, true, true, false, nil, nil)
}
// it is very important that within this function we never include the original prompt in any error messages, because it may contain e.g. a user password
func (gui *Gui) createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, returnFocusOnClose bool, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
return gui.createPopupPanel(g, currentView, title, prompt, false, returnFocusOnClose, false, handleConfirm, handleClose)
}
func (gui *Gui) createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, initialContent string, handleConfirm func(*gocui.Gui, *gocui.View) error) error {
return gui.createPopupPanel(gui.g, currentView, title, initialContent, false, true, true, handleConfirm, nil)
}
func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error, returnFocusOnClose bool) error {
actions := gui.Tr.TemplateLocalize(
"CloseConfirm",
@@ -153,14 +151,14 @@ func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*go
if err := gui.renderString(g, "options", actions); err != nil {
return err
}
if err := g.SetKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone, gui.wrappedConfirmationFunction(handleConfirm, returnFocusOnClose)); err != nil {
if err := g.SetKeybinding("confirmation", nil, gocui.KeyEnter, gocui.ModNone, gui.wrappedConfirmationFunction(handleConfirm, returnFocusOnClose)); err != nil {
return err
}
return g.SetKeybinding("confirmation", gocui.KeyEsc, gocui.ModNone, gui.wrappedConfirmationFunction(handleClose, returnFocusOnClose))
return g.SetKeybinding("confirmation", nil, gocui.KeyEsc, gocui.ModNone, gui.wrappedConfirmationFunction(handleClose, returnFocusOnClose))
}
func (gui *Gui) createMessagePanel(g *gocui.Gui, currentView *gocui.View, title, prompt string) error {
return gui.createPopupPanel(g, currentView, title, prompt, false, true, nil, nil)
return gui.createPopupPanel(g, currentView, title, prompt, false, true, false, nil, nil)
}
// createSpecificErrorPanel allows you to create an error popup, specifying the
+11 -36
View File
@@ -1,45 +1,20 @@
package gui
func (gui *Gui) changeContext(context string) error {
oldContext := gui.State.Context
if gui.State.Context == context {
// changeContext is a helper function for when we want to change a 'main' context
// 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 {
if gui.State.MainContext == context {
return nil
}
contextMap := gui.GetContextMap()
oldBindings := contextMap[oldContext]
for _, binding := range oldBindings {
if err := gui.g.DeleteKeybinding(binding.ViewName, binding.Key, binding.Modifier); err != nil {
return err
}
switch context {
case "normal", "patch-building", "staging", "merging":
gui.getMainView().Context = context
gui.getSecondaryView().Context = context
}
bindings := contextMap[context]
for _, binding := range bindings {
if err := gui.g.SetKeybinding(binding.ViewName, binding.Key, binding.Modifier, binding.Handler); err != nil {
return err
}
}
gui.State.Context = context
return nil
}
func (gui *Gui) setInitialContext() error {
contextMap := gui.GetContextMap()
initialContext := "normal"
bindings := contextMap[initialContext]
for _, binding := range bindings {
if err := gui.g.SetKeybinding(binding.ViewName, binding.Key, binding.Modifier, binding.Handler); err != nil {
return err
}
}
gui.State.Context = initialContext
gui.State.MainContext = context
return nil
}
+138
View File
@@ -0,0 +1,138 @@
package gui
import (
"os"
"path/filepath"
"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
}
func NewFileWatcher(log *logrus.Entry) *fileWatcher {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Error(err)
return nil
}
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 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() {
gui.fileWatcher = NewFileWatcher(gui.Log)
if gui.fileWatcher == nil {
return
}
go func() {
for {
select {
// watch for 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
}
// only refresh if we're not already
if !gui.State.IsRefreshingFiles {
if err := gui.refreshFiles(); err != nil {
err = gui.createErrorPanel(gui.g, err.Error())
if err != nil {
gui.Log.Error(err)
}
}
}
// watch for errors
case err := <-gui.fileWatcher.Watcher.Errors:
if err != nil {
gui.Log.Warn(err)
}
}
}
}()
}
+64 -132
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,45 +26,22 @@ func (gui *Gui) getSelectedFile(g *gocui.Gui) (*commands.File, error) {
return gui.State.Files[selectedLine], nil
}
func (gui *Gui) handleFilesClick(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
prevSelectedLineIdx := gui.State.Panels.Files.SelectedLine
newSelectedLineIdx := v.SelectedLineIdx()
if newSelectedLineIdx > len(gui.State.Files)-1 {
return gui.handleFileSelect(gui.g, v, false)
}
gui.State.Panels.Files.SelectedLine = newSelectedLineIdx
if prevSelectedLineIdx == newSelectedLineIdx && gui.currentViewName() == v.Name() {
return gui.handleFilePress(gui.g, v)
} else {
return gui.handleFileSelect(gui.g, v, true)
}
}
func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View, alreadySelected bool) error {
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
file, err := gui.getSelectedFile(g)
func (gui *Gui) selectFile(alreadySelected bool) error {
file, err := gui.getSelectedFile(gui.g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
}
return gui.renderString(g, "main", gui.Tr.SLocalize("NoChangedFiles"))
return gui.renderString(gui.g, "main", gui.Tr.SLocalize("NoChangedFiles"))
}
if err := gui.focusPoint(0, gui.State.Panels.Files.SelectedLine, len(gui.State.Files), v); err != nil {
if err := gui.focusPoint(0, gui.State.Panels.Files.SelectedLine, len(gui.State.Files), gui.getFilesView()); err != nil {
return err
}
if file.HasInlineMergeConflicts {
gui.getMainView().Title = gui.Tr.SLocalize("MergeConflictsTitle")
gui.State.SplitMainPanel = false
return gui.refreshMergePanel()
}
@@ -88,7 +64,7 @@ func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View, alreadySelected bo
}
if alreadySelected {
g.Update(func(*gocui.Gui) error {
gui.g.Update(func(*gocui.Gui) error {
if err := gui.setViewContent(gui.g, gui.getSecondaryView(), contentCached); err != nil {
return err
}
@@ -96,13 +72,20 @@ func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View, alreadySelected bo
})
return nil
}
if err := gui.renderString(g, "secondary", contentCached); err != nil {
if err := gui.renderString(gui.g, "secondary", contentCached); err != nil {
return err
}
return gui.renderString(g, "main", leftContent)
return gui.renderString(gui.g, "main", leftContent)
}
func (gui *Gui) refreshFiles() error {
gui.State.RefreshingFilesMutex.Lock()
gui.State.IsRefreshingFiles = true
defer func() {
gui.State.IsRefreshingFiles = false
gui.State.RefreshingFilesMutex.Unlock()
}()
selectedFile, _ := gui.getSelectedFile(gui.g)
filesView := gui.getFilesView()
@@ -124,10 +107,10 @@ func (gui *Gui) refreshFiles() error {
}
fmt.Fprint(filesView, list)
if filesView == g.CurrentView() {
if g.CurrentView() == filesView || (g.CurrentView() == gui.getMainView() && g.CurrentView().Context == "merging") {
newSelectedFile, _ := gui.getSelectedFile(gui.g)
alreadySelected := newSelectedFile.Name == selectedFile.Name
return gui.handleFileSelect(g, filesView, alreadySelected)
return gui.selectFile(alreadySelected)
}
return nil
})
@@ -135,28 +118,6 @@ func (gui *Gui) refreshFiles() error {
return nil
}
func (gui *Gui) handleFilesNextLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
panelState := gui.State.Panels.Files
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Files), false)
return gui.handleFileSelect(gui.g, v, false)
}
func (gui *Gui) handleFilesPrevLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
panelState := gui.State.Panels.Files
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Files), true)
return gui.handleFileSelect(gui.g, v, false)
}
// specific functions
func (gui *Gui) stagedFiles() []*commands.File {
@@ -207,7 +168,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.changeContext("staging"); err != nil {
if err := gui.changeMainViewsContext("staging"); err != nil {
return err
}
if err := gui.switchFocus(gui.g, gui.getFilesView(), gui.getMainView()); err != nil {
@@ -239,7 +200,7 @@ func (gui *Gui) handleFilePress(g *gocui.Gui, v *gocui.View) error {
return err
}
return gui.handleFileSelect(g, v, true)
return gui.selectFile(true)
}
func (gui *Gui) allFilesStaged() bool {
@@ -251,6 +212,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() {
@@ -266,7 +235,7 @@ func (gui *Gui) handleStageAll(g *gocui.Gui, v *gocui.View) error {
return err
}
return gui.handleFileSelect(g, v, false)
return gui.selectFile(false)
}
func (gui *Gui) handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
@@ -385,6 +354,11 @@ func (gui *Gui) refreshStateFiles() error {
// get files to stage
files := gui.GitCommand.GetStatusFiles()
gui.State.Files = gui.GitCommand.MergeStatusFiles(gui.State.Files, files)
if err := gui.fileWatcher.addFilesToFileWatcher(files); err != nil {
return err
}
gui.refreshSelectedLine(&gui.State.Panels.Files.SelectedLine, len(gui.State.Files))
return gui.updateWorkTreeState()
}
@@ -408,7 +382,31 @@ func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) {
return cat, nil
}
func (gui *Gui) pullFiles(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handlePullFiles(g *gocui.Gui, v *gocui.View) error {
// if we have no upstream branch we need to set that first
_, pullables := gui.GitCommand.GetCurrentBranchUpstreamDifferenceCount()
currentBranchName, err := gui.GitCommand.CurrentBranchName()
if err != nil {
return err
}
if pullables == "?" {
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 {
errorMessage := err.Error()
if strings.Contains(errorMessage, "does not exist") {
errorMessage = fmt.Sprintf("upstream branch %s not found.\nIf you expect it to exist, you should fetch (with 'f').\nOtherwise, you should push (with 'shift+P')", upstream)
}
return gui.createErrorPanel(gui.g, errorMessage)
}
return gui.pullFiles(v)
})
}
return gui.pullFiles(v)
}
func (gui *Gui) pullFiles(v *gocui.View) error {
if err := gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("PullWait")); err != nil {
return err
}
@@ -417,10 +415,11 @@ func (gui *Gui) pullFiles(g *gocui.Gui, v *gocui.View) error {
unamePassOpend := false
err := gui.GitCommand.Pull(func(passOrUname string) string {
unamePassOpend = true
return gui.waitForPassUname(g, v, passOrUname)
return gui.waitForPassUname(gui.g, v, passOrUname)
})
gui.HandleCredentialsPopup(g, unamePassOpend, err)
gui.HandleCredentialsPopup(gui.g, unamePassOpend, err)
}()
return nil
}
@@ -430,7 +429,7 @@ func (gui *Gui) pushWithForceFlag(g *gocui.Gui, v *gocui.View, force bool, upstr
}
go func() {
unamePassOpend := false
branchName := gui.State.Branches[0].Name
branchName := gui.getCheckedOutBranch().Name
err := gui.GitCommand.Push(branchName, force, upstream, func(passOrUname string) string {
unamePassOpend = true
return gui.waitForPassUname(g, v, passOrUname)
@@ -471,7 +470,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.changeContext("merging"); err != nil {
if err := gui.changeMainViewsContext("merging"); err != nil {
return err
}
if err := gui.switchFocus(g, v, gui.getMainView()); err != nil {
@@ -510,22 +509,11 @@ type discardOption struct {
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 {
@@ -577,62 +565,6 @@ func (gui *Gui) handleCreateDiscardMenu(g *gocui.Gui, v *gocui.View) error {
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)
+106
View File
@@ -0,0 +1,106 @@
package gui
import (
"fmt"
"regexp"
"strings"
"github.com/jesseduffield/gocui"
)
type gitFlowOption struct {
handler func() error
description string
}
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
}
// GetDisplayStrings is a function.
func (r *gitFlowOption) GetDisplayStrings(isFocused bool) []string {
return []string{r.description}
}
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
})
}
}
options := []*gitFlowOption{
{
// not localising here because it's one to one with the actual git flow commands
description: fmt.Sprintf("finish branch '%s'", branch.Name),
handler: func() error {
return gui.gitFlowFinishBranch(gitFlowConfig, branch.Name)
},
},
{
description: "start feature",
handler: startHandler("feature"),
},
{
description: "start hotfix",
handler: startHandler("hotfix"),
},
{
description: "start release",
handler: startHandler("release"),
},
{
description: gui.Tr.SLocalize("cancel"),
handler: func() error {
return nil
},
},
}
handleMenuPress := func(index int) error {
return options[index].handler()
}
return gui.createMenu("git flow", options, len(options), handleMenuPress)
}
+116 -51
View File
@@ -5,6 +5,7 @@ import (
"io/ioutil"
"math"
"os"
"runtime"
"sync"
// "io"
@@ -27,6 +28,7 @@ import (
"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"
)
@@ -79,6 +81,7 @@ type Gui struct {
statusManager *statusManager
credentials credentials
waitForIntro sync.WaitGroup
fileWatcher *fileWatcher
}
// for now the staging panel state, unlike the other panel states, is going to be
@@ -105,15 +108,32 @@ type filePanelState struct {
SelectedLine int
}
// TODO: consider splitting this out into the window and the branches view
type branchPanelState struct {
SelectedLine int
}
type remotePanelState struct {
SelectedLine int
}
type remoteBranchesState struct {
SelectedLine int
}
type tagsPanelState struct {
SelectedLine int
}
type commitPanelState struct {
SelectedLine int
SpecificDiffMode bool
}
type reflogCommitPanelState struct {
SelectedLine int
}
type stashPanelState struct {
SelectedLine int
}
@@ -133,34 +153,44 @@ type statusPanelState struct {
}
type panelStates struct {
Files *filePanelState
Branches *branchPanelState
Commits *commitPanelState
Stash *stashPanelState
Menu *menuPanelState
LineByLine *lineByLinePanelState
Merging *mergingPanelState
CommitFiles *commitFilesPanelState
Status *statusPanelState
Files *filePanelState
Branches *branchPanelState
Remotes *remotePanelState
RemoteBranches *remoteBranchesState
Tags *tagsPanelState
Commits *commitPanelState
ReflogCommits *reflogCommitPanelState
Stash *stashPanelState
Menu *menuPanelState
LineByLine *lineByLinePanelState
Merging *mergingPanelState
CommitFiles *commitFilesPanelState
Status *statusPanelState
}
type guiState struct {
Files []*commands.File
Branches []*commands.Branch
Commits []*commands.Commit
StashEntries []*commands.StashEntry
CommitFiles []*commands.CommitFile
DiffEntries []*commands.Commit
MenuItemCount int // can't store the actual list because it's of interface{} type
PreviousView string
Platform commands.Platform
Updating bool
Panels *panelStates
WorkingTreeState string // one of "merging", "rebasing", "normal"
Context string // important not to set this value directly but to use gui.changeContext("new context")
CherryPickedCommits []*commands.Commit
SplitMainPanel bool
RetainOriginalDir bool
Files []*commands.File
Branches []*commands.Branch
Commits []*commands.Commit
StashEntries []*commands.StashEntry
CommitFiles []*commands.CommitFile
ReflogCommits []*commands.Commit
DiffEntries []*commands.Commit
Remotes []*commands.Remote
RemoteBranches []*commands.RemoteBranch
Tags []*commands.Tag
MenuItemCount int // can't store the actual list because it's of interface{} type
PreviousView string
Platform commands.Platform
Updating bool
Panels *panelStates
WorkingTreeState string // one of "merging", "rebasing", "normal"
MainContext string // used to keep the main and secondary views' contexts in sync
CherryPickedCommits []*commands.Commit
SplitMainPanel bool
RetainOriginalDir bool
IsRefreshingFiles bool
RefreshingFilesMutex sync.Mutex
}
// for now the split view will always be on
@@ -177,12 +207,16 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *comma
DiffEntries: make([]*commands.Commit, 0),
Platform: *oSCommand.Platform,
Panels: &panelStates{
Files: &filePanelState{SelectedLine: -1},
Branches: &branchPanelState{SelectedLine: 0},
Commits: &commitPanelState{SelectedLine: -1},
CommitFiles: &commitFilesPanelState{SelectedLine: -1},
Stash: &stashPanelState{SelectedLine: -1},
Menu: &menuPanelState{SelectedLine: 0},
Files: &filePanelState{SelectedLine: -1},
Branches: &branchPanelState{SelectedLine: 0},
Remotes: &remotePanelState{SelectedLine: 0},
RemoteBranches: &remoteBranchesState{SelectedLine: -1},
Tags: &tagsPanelState{SelectedLine: -1},
Commits: &commitPanelState{SelectedLine: -1},
ReflogCommits: &reflogCommitPanelState{SelectedLine: 0}, // TODO: might need to make -1
CommitFiles: &commitFilesPanelState{SelectedLine: -1},
Stash: &stashPanelState{SelectedLine: -1},
Menu: &menuPanelState{SelectedLine: 0},
Merging: &mergingPanelState{
ConflictIndex: 0,
ConflictTop: true,
@@ -204,6 +238,8 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *comma
statusManager: &statusManager{},
}
gui.watchFilesForChanges()
gui.GenerateSentinelErrors()
return gui, nil
@@ -293,18 +329,20 @@ func (gui *Gui) onFocusLost(v *gocui.View, newView *gocui.View) error {
}
switch v.Name() {
case "branches":
// This stops the branches panel from showing the upstream/downstream changes to the selected branch, when it loses focus
// inside renderListPanel it checks to see if the panel has focus
if err := gui.renderListPanel(gui.getBranchesView(), gui.State.Branches); err != nil {
return err
if v.Context == "local-branches" {
// This stops the branches panel from showing the upstream/downstream changes to the selected branch, when it loses focus
// inside renderListPanel it checks to see if the panel has focus
if err := gui.renderListPanel(gui.getBranchesView(), gui.State.Branches); err != nil {
return err
}
}
case "main":
// if we have lost focus to a first-class panel, we need to do some cleanup
if err := gui.changeContext("normal"); err != nil {
if err := gui.changeMainViewsContext("normal"); err != nil {
return err
}
case "commitFiles":
if gui.State.Context != "patch-building" {
if gui.State.MainContext != "patch-building" {
if _, err := gui.g.SetViewOnBottom(v.Name()); err != nil {
return err
}
@@ -474,6 +512,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
return err
}
branchesView.Title = gui.Tr.SLocalize("BranchesTitle")
branchesView.Tabs = []string{"Local Branches", "Remotes", "Tags"}
branchesView.FgColor = textColor
}
@@ -491,6 +530,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
}
@@ -522,6 +562,7 @@ 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)
}
}
@@ -565,7 +606,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
// doing this here because it'll only happen once
if err := gui.loadNewRepo(); err != nil {
if err := gui.onInitialViewsCreation(); err != nil {
return err
}
}
@@ -583,22 +624,31 @@ func (gui *Gui) layout(g *gocui.Gui) error {
type listViewState struct {
selectedLine int
lineCount int
view *gocui.View
context string
}
listViews := map[*gocui.View]listViewState{
filesView: {selectedLine: gui.State.Panels.Files.SelectedLine, lineCount: len(gui.State.Files)},
branchesView: {selectedLine: gui.State.Panels.Branches.SelectedLine, lineCount: len(gui.State.Branches)},
commitsView: {selectedLine: gui.State.Panels.Commits.SelectedLine, lineCount: len(gui.State.Commits)},
stashView: {selectedLine: gui.State.Panels.Stash.SelectedLine, lineCount: len(gui.State.StashEntries)},
listViews := []listViewState{
{view: filesView, context: "", selectedLine: gui.State.Panels.Files.SelectedLine, lineCount: len(gui.State.Files)},
{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: "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)},
}
// menu view might not exist so we check to be safe
if menuView, err := gui.g.View("menu"); err == nil {
listViews[menuView] = listViewState{selectedLine: gui.State.Panels.Menu.SelectedLine, lineCount: gui.State.MenuItemCount}
listViews = append(listViews, listViewState{view: menuView, context: "", selectedLine: gui.State.Panels.Menu.SelectedLine, lineCount: gui.State.MenuItemCount})
}
for view, state := range listViews {
for _, listView := range listViews {
// ignore views where the context doesn't match up with the selected line we're trying to focus
if listView.context != "" && (listView.view.Context != listView.context) {
continue
}
// check if the selected line is now out of view and if so refocus it
if err := gui.focusPoint(0, state.selectedLine, state.lineCount, view); err != nil {
if err := gui.focusPoint(0, listView.selectedLine, listView.lineCount, listView.view); err != nil {
return err
}
}
@@ -610,6 +660,17 @@ func (gui *Gui) layout(g *gocui.Gui) error {
return gui.resizeCurrentPopupPanel(g)
}
func (gui *Gui) onInitialViewsCreation() error {
if err := gui.changeMainViewsContext("normal"); err != nil {
return err
}
gui.getBranchesView().Context = "local-branches"
gui.getCommitsView().Context = "branch-commits"
return gui.loadNewRepo()
}
func (gui *Gui) loadNewRepo() error {
gui.Updater.CheckForNewUpdate(gui.onBackgroundUpdateCheckFinish, false)
if err := gui.updateRecentRepoList(); err != nil {
@@ -691,11 +752,11 @@ func (gui *Gui) renderAppStatus() error {
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"),
})
}
@@ -732,6 +793,8 @@ func (gui *Gui) Run() error {
}
defer g.Close()
g.ASCII = runtime.GOOS == "windows" && runewidth.IsEastAsian()
if gui.Config.GetUserConfig().GetBool("gui.mouseEvents") {
g.Mouse = true
}
@@ -786,6 +849,8 @@ func (gui *Gui) RunWithSubprocesses() error {
}
}
gui.fileWatcher.Watcher.Close()
break
} else if err == gui.Errors.ErrSwitchRepo {
continue
+998 -566
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -226,7 +226,7 @@ func (gui *Gui) refreshMainView() error {
var includedLineIndices []int
// I'd prefer not to have knowledge of contexts using this file but I'm not sure
// how to get around this
if gui.State.Context == "patch-building" {
if gui.State.MainContext == "patch-building" {
filename := gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLine].Name
includedLineIndices = gui.GitCommand.PatchManager.GetFileIncLineIndices(filename)
}
+170
View File
@@ -0,0 +1,170 @@
package gui
import "github.com/jesseduffield/gocui"
type listView struct {
viewName string
context string
getItemsLength func() int
getSelectedLineIdxPtr func() *int
handleFocus func(g *gocui.Gui, v *gocui.View) error
handleItemSelect func(g *gocui.Gui, v *gocui.View) error
handleClickSelectedItem func(g *gocui.Gui, v *gocui.View) error
gui *Gui
rendersToMainView bool
}
func (lv *listView) handlePrevLine(g *gocui.Gui, v *gocui.View) error {
return lv.handleLineChange(-1)
}
func (lv *listView) handleNextLine(g *gocui.Gui, v *gocui.View) error {
return lv.handleLineChange(1)
}
func (lv *listView) handleLineChange(change int) error {
if !lv.gui.isPopupPanel(lv.viewName) && lv.gui.popupPanelFocused() {
return nil
}
lv.gui.changeSelectedLine(lv.getSelectedLineIdxPtr(), lv.getItemsLength(), change)
if lv.rendersToMainView {
if err := lv.gui.resetOrigin(lv.gui.getMainView()); err != nil {
return err
}
}
view, err := lv.gui.g.View(lv.viewName)
if err != nil {
return err
}
return lv.handleItemSelect(lv.gui.g, view)
}
func (lv *listView) handleClick(g *gocui.Gui, v *gocui.View) error {
if !lv.gui.isPopupPanel(lv.viewName) && lv.gui.popupPanelFocused() {
return nil
}
selectedLineIdxPtr := lv.getSelectedLineIdxPtr()
prevSelectedLineIdx := *selectedLineIdxPtr
newSelectedLineIdx := v.SelectedLineIdx()
if newSelectedLineIdx > lv.getItemsLength()-1 {
return lv.handleFocus(lv.gui.g, v)
}
*selectedLineIdxPtr = newSelectedLineIdx
if prevSelectedLineIdx == newSelectedLineIdx && lv.gui.currentViewName() == lv.viewName && lv.handleClickSelectedItem != nil {
return lv.handleClickSelectedItem(lv.gui.g, v)
}
return lv.handleItemSelect(lv.gui.g, v)
}
func (gui *Gui) getListViews() []*listView {
return []*listView{
{
viewName: "menu",
getItemsLength: func() int { return gui.getMenuView().LinesHeight() },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Menu.SelectedLine },
handleFocus: gui.handleMenuSelect,
handleItemSelect: gui.handleMenuSelect,
// need to add a layer of indirection here because the callback changes during runtime
handleClickSelectedItem: gui.wrappedHandler(func() error { return gui.State.Panels.Menu.OnPress(gui.g, nil) }),
gui: gui,
rendersToMainView: false,
},
{
viewName: "files",
getItemsLength: func() int { return len(gui.State.Files) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Files.SelectedLine },
handleFocus: gui.focusAndSelectFile,
handleItemSelect: gui.focusAndSelectFile,
handleClickSelectedItem: gui.handleFilePress,
gui: gui,
rendersToMainView: true,
},
{
viewName: "branches",
context: "local-branches",
getItemsLength: func() int { return len(gui.State.Branches) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Branches.SelectedLine },
handleFocus: gui.handleBranchSelect,
handleItemSelect: gui.handleBranchSelect,
gui: gui,
rendersToMainView: true,
},
{
viewName: "branches",
context: "remotes",
getItemsLength: func() int { return len(gui.State.Remotes) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Remotes.SelectedLine },
handleFocus: gui.wrappedHandler(gui.renderRemotesWithSelection),
handleItemSelect: gui.handleRemoteSelect,
handleClickSelectedItem: gui.handleRemoteEnter,
gui: gui,
rendersToMainView: true,
},
{
viewName: "branches",
context: "remote-branches",
getItemsLength: func() int { return len(gui.State.RemoteBranches) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.RemoteBranches.SelectedLine },
handleFocus: gui.handleRemoteBranchSelect,
handleItemSelect: gui.handleRemoteBranchSelect,
gui: gui,
rendersToMainView: true,
},
{
viewName: "branches",
context: "tags",
getItemsLength: func() int { return len(gui.State.Tags) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Tags.SelectedLine },
handleFocus: gui.handleTagSelect,
handleItemSelect: gui.handleTagSelect,
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,
handleItemSelect: gui.handleCommitSelect,
handleClickSelectedItem: gui.handleSwitchToCommitFilesPanel,
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) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Stash.SelectedLine },
handleFocus: gui.handleStashEntrySelect,
handleItemSelect: gui.handleStashEntrySelect,
gui: gui,
rendersToMainView: true,
},
{
viewName: "commitFiles",
getItemsLength: func() int { return len(gui.State.CommitFiles) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.CommitFiles.SelectedLine },
handleFocus: gui.handleCommitFileSelect,
handleItemSelect: gui.handleCommitFileSelect,
gui: gui,
rendersToMainView: true,
},
}
}
+4 -30
View File
@@ -14,27 +14,13 @@ func (gui *Gui) handleMenuSelect(g *gocui.Gui, v *gocui.View) error {
return gui.focusPoint(0, gui.State.Panels.Menu.SelectedLine, gui.State.MenuItemCount, v)
}
func (gui *Gui) handleMenuNextLine(g *gocui.Gui, v *gocui.View) error {
panelState := gui.State.Panels.Menu
gui.changeSelectedLine(&panelState.SelectedLine, v.LinesHeight(), false)
return gui.handleMenuSelect(g, v)
}
func (gui *Gui) handleMenuPrevLine(g *gocui.Gui, v *gocui.View) error {
panelState := gui.State.Panels.Menu
gui.changeSelectedLine(&panelState.SelectedLine, v.LinesHeight(), true)
return gui.handleMenuSelect(g, v)
}
// specific functions
func (gui *Gui) renderMenuOptions() error {
optionsMap := map[string]string{
"esc/q": gui.Tr.SLocalize("close"),
"↑ ↓": gui.Tr.SLocalize("navigate"),
"space": gui.Tr.SLocalize("execute"),
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)
}
@@ -87,7 +73,7 @@ func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handl
for _, key := range []gocui.Key{gocui.KeySpace, gocui.KeyEnter, 'y'} {
_ = gui.g.DeleteKeybinding("menu", key, gocui.ModNone)
if err := gui.g.SetKeybinding("menu", key, gocui.ModNone, wrappedHandlePress); err != nil {
if err := gui.g.SetKeybinding("menu", nil, key, gocui.ModNone, wrappedHandlePress); err != nil {
return err
}
}
@@ -103,15 +89,3 @@ func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handl
})
return nil
}
func (gui *Gui) handleMenuClick(g *gocui.Gui, v *gocui.View) error {
itemCount := gui.State.MenuItemCount
handleSelect := gui.handleMenuSelect
selectedLine := &gui.State.Panels.Menu.SelectedLine
if err := gui.handleClick(v, itemCount, selectedLine, handleSelect); err != nil {
return err
}
return gui.State.Panels.Menu.OnPress(g, v)
}
+6 -5
View File
@@ -5,6 +5,7 @@ package gui
import (
"bufio"
"bytes"
"fmt"
"io/ioutil"
"math"
"os"
@@ -243,11 +244,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"),
})
}
+6 -3
View File
@@ -6,6 +6,7 @@ import (
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) getBindings(v *gocui.View) []*Binding {
@@ -13,15 +14,17 @@ func (gui *Gui) getBindings(v *gocui.View) []*Binding {
bindingsGlobal, bindingsPanel []*Binding
)
bindings := gui.GetCurrentKeybindings()
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)
case v.Name():
bindingsPanel = append(bindingsPanel, binding)
if len(binding.Contexts) == 0 || utils.IncludesString(binding.Contexts, v.Context) {
bindingsPanel = append(bindingsPanel, binding)
}
}
}
}
+1 -1
View File
@@ -88,7 +88,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.changeContext("normal")
gui.changeMainViewsContext("normal")
if gui.GitCommand.PatchManager.IsEmpty() {
gui.GitCommand.PatchManager.Reset()
+1 -1
View File
@@ -67,7 +67,7 @@ func (gui *Gui) validateNormalWorkingTreeState() (bool, error) {
}
func (gui *Gui) returnFocusFromLineByLinePanelIfNecessary() error {
if gui.State.Context == "patch-building" {
if gui.State.MainContext == "patch-building" {
return gui.handleEscapePatchBuildingPanel(gui.g, nil)
}
return nil
+94
View File
@@ -0,0 +1,94 @@
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.renderString(g, "main", "No reflog history")
}
if err := gui.focusPoint(0, gui.State.Panels.ReflogCommits.SelectedLine, len(gui.State.ReflogCommits), v); err != nil {
return err
}
commitText, err := gui.GitCommand.Show(commit.Sha)
if err != nil {
return err
}
return gui.renderString(g, "main", commitText)
}
func (gui *Gui) 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
}
+60
View File
@@ -0,0 +1,60 @@
package gui
import (
"fmt"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
)
type reflogResetOption struct {
handler func() error
description string
command string
}
// GetDisplayStrings is a function.
func (r *reflogResetOption) GetDisplayStrings(isFocused bool) []string {
return []string{r.description, color.New(color.FgRed).Sprint(r.command)}
}
func (gui *Gui) handleCreateReflogResetMenu(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedReflogCommit()
resetFunction := func(reset func(string) error) func() error {
return func() error {
if err := reset(commit.Sha); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
gui.State.Panels.ReflogCommits.SelectedLine = 0
return gui.refreshSidePanels(gui.g)
}
}
options := []*reflogResetOption{
{
description: gui.Tr.SLocalize("hardReset"),
command: fmt.Sprintf("reset --hard %s", commit.Sha),
handler: resetFunction(gui.GitCommand.ResetHard),
},
{
description: gui.Tr.SLocalize("softReset"),
command: fmt.Sprintf("reset --soft %s", commit.Sha),
handler: resetFunction(gui.GitCommand.ResetSoft),
},
{
description: gui.Tr.SLocalize("cancel"),
handler: func() error {
return nil
},
},
}
handleMenuPress := func(index int) error {
return options[index].handler()
}
return gui.createMenu("", options, len(options), handleMenuPress)
}
+136
View File
@@ -0,0 +1,136 @@
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
func (gui *Gui) getSelectedRemoteBranch() *commands.RemoteBranch {
selectedLine := gui.State.Panels.RemoteBranches.SelectedLine
if selectedLine == -1 || len(gui.State.RemoteBranches) == 0 {
return nil
}
return gui.State.RemoteBranches[selectedLine]
}
func (gui *Gui) handleRemoteBranchSelect(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 = "Remote Branch"
remote := gui.getSelectedRemote()
remoteBranch := gui.getSelectedRemoteBranch()
if remoteBranch == nil {
return gui.renderString(g, "main", "No branches for this remote")
}
gui.focusPoint(0, gui.State.Panels.Menu.SelectedLine, gui.State.MenuItemCount, v)
if err := gui.focusPoint(0, gui.State.Panels.RemoteBranches.SelectedLine, len(gui.State.RemoteBranches), v); err != nil {
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))
}()
return nil
}
func (gui *Gui) handleRemoteBranchesEscape(g *gocui.Gui, v *gocui.View) error {
return gui.switchBranchesPanelContext("remotes")
}
func (gui *Gui) renderRemoteBranchesWithSelection() error {
branchesView := gui.getBranchesView()
gui.refreshSelectedLine(&gui.State.Panels.RemoteBranches.SelectedLine, len(gui.State.RemoteBranches))
if err := gui.renderListPanel(branchesView, gui.State.RemoteBranches); err != nil {
return err
}
if gui.g.CurrentView() == branchesView && branchesView.Context == "remote-branches" {
if err := gui.handleRemoteBranchSelect(gui.g, branchesView); err != nil {
return err
}
}
return nil
}
func (gui *Gui) handleCheckoutRemoteBranch(g *gocui.Gui, v *gocui.View) error {
remoteBranch := gui.getSelectedRemoteBranch()
if remoteBranch == nil {
return nil
}
if err := gui.handleCheckoutRef(remoteBranch.RemoteName + "/" + remoteBranch.Name); err != nil {
return err
}
return gui.switchBranchesPanelContext("local-branches")
}
func (gui *Gui) handleMergeRemoteBranch(g *gocui.Gui, v *gocui.View) error {
selectedBranchName := gui.getSelectedRemoteBranch().Name
return gui.mergeBranchIntoCheckedOutBranch(selectedBranchName)
}
func (gui *Gui) handleDeleteRemoteBranch(g *gocui.Gui, v *gocui.View) error {
remoteBranch := gui.getSelectedRemoteBranch()
if remoteBranch == nil {
return nil
}
message := fmt.Sprintf("%s '%s/%s'?", gui.Tr.SLocalize("DeleteRemoteBranchMessage"), remoteBranch.RemoteName, remoteBranch.Name)
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("DeleteRemoteBranch"), message, func(*gocui.Gui, *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("DeletingStatus"), func() error {
if err := gui.GitCommand.DeleteRemoteBranch(remoteBranch.RemoteName, remoteBranch.Name); err != nil {
return err
}
return gui.refreshRemotes()
})
}, nil)
}
func (gui *Gui) handleRebaseOntoRemoteBranch(g *gocui.Gui, v *gocui.View) error {
selectedBranchName := gui.getSelectedRemoteBranch().Name
return gui.handleRebaseOntoBranch(selectedBranchName)
}
func (gui *Gui) handleSetBranchUpstream(g *gocui.Gui, v *gocui.View) error {
selectedBranch := gui.getSelectedRemoteBranch()
checkedOutBranch := gui.getCheckedOutBranch()
message := gui.Tr.TemplateLocalize(
"SetUpstreamMessage",
Teml{
"checkedOut": checkedOutBranch.Name,
"selected": selectedBranch.RemoteName + "/" + selectedBranch.Name,
},
)
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("SetUpstreamTitle"), message, func(*gocui.Gui, *gocui.View) error {
if err := gui.GitCommand.SetBranchUpstream(selectedBranch.RemoteName, selectedBranch.Name, checkedOutBranch.Name); err != nil {
return err
}
return gui.refreshSidePanels(gui.g)
}, nil)
}
+195
View File
@@ -0,0 +1,195 @@
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
func (gui *Gui) getSelectedRemote() *commands.Remote {
selectedLine := gui.State.Panels.Remotes.SelectedLine
if selectedLine == -1 || len(gui.State.Remotes) == 0 {
return nil
}
return gui.State.Remotes[selectedLine]
}
func (gui *Gui) handleRemoteSelect(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 = "Remote"
remote := gui.getSelectedRemote()
if remote == nil {
return gui.renderString(g, "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")))
}
func (gui *Gui) refreshRemotes() error {
prevSelectedRemote := gui.getSelectedRemote()
remotes, err := gui.GitCommand.GetRemotes()
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
gui.State.Remotes = remotes
// we need to ensure our selected remote branches aren't now outdated
if prevSelectedRemote != nil && gui.State.RemoteBranches != nil {
// find remote now
for _, remote := range remotes {
if remote.Name == prevSelectedRemote.Name {
gui.State.RemoteBranches = remote.Branches
}
}
}
// TODO: see if this works for deleting remote branches
switch gui.getBranchesView().Context {
case "remotes":
return gui.renderRemotesWithSelection()
case "remote-branches":
return gui.renderRemoteBranchesWithSelection()
}
return nil
}
func (gui *Gui) renderRemotesWithSelection() error {
branchesView := gui.getBranchesView()
gui.refreshSelectedLine(&gui.State.Panels.Remotes.SelectedLine, len(gui.State.Remotes))
if err := gui.renderListPanel(branchesView, gui.State.Remotes); err != nil {
return err
}
if gui.g.CurrentView() == branchesView && branchesView.Context == "remotes" {
if err := gui.handleRemoteSelect(gui.g, branchesView); err != nil {
return err
}
}
return nil
}
func (gui *Gui) handleRemoteEnter(g *gocui.Gui, v *gocui.View) error {
// naive implementation: get the branches and render them to the list, change the context
remote := gui.getSelectedRemote()
if remote == nil {
return nil
}
gui.State.RemoteBranches = remote.Branches
newSelectedLine := 0
if len(remote.Branches) == 0 {
newSelectedLine = -1
}
gui.State.Panels.RemoteBranches.SelectedLine = newSelectedLine
return gui.switchBranchesPanelContext("remote-branches")
}
func (gui *Gui) handleAddRemote(g *gocui.Gui, v *gocui.View) error {
branchesView := gui.getBranchesView()
return gui.createPromptPanel(g, branchesView, gui.Tr.SLocalize("newRemoteName"), "", func(g *gocui.Gui, v *gocui.View) error {
remoteName := gui.trimmedContent(v)
return gui.createPromptPanel(g, branchesView, gui.Tr.SLocalize("newRemoteUrl"), "", func(g *gocui.Gui, v *gocui.View) error {
remoteUrl := gui.trimmedContent(v)
if err := gui.GitCommand.AddRemote(remoteName, remoteUrl); err != nil {
return err
}
return gui.refreshRemotes()
})
})
}
func (gui *Gui) handleRemoveRemote(g *gocui.Gui, v *gocui.View) error {
remote := gui.getSelectedRemote()
if remote == nil {
return nil
}
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("removeRemote"), gui.Tr.SLocalize("removeRemotePrompt")+" '"+remote.Name+"'?", func(*gocui.Gui, *gocui.View) error {
if err := gui.GitCommand.RemoveRemote(remote.Name); err != nil {
return err
}
return gui.refreshRemotes()
}, nil)
}
func (gui *Gui) handleEditRemote(g *gocui.Gui, v *gocui.View) error {
branchesView := gui.getBranchesView()
remote := gui.getSelectedRemote()
if remote == nil {
return nil
}
editNameMessage := gui.Tr.TemplateLocalize(
"editRemoteName",
Teml{
"remoteName": remote.Name,
},
)
return gui.createPromptPanel(g, branchesView, editNameMessage, "", func(g *gocui.Gui, v *gocui.View) error {
updatedRemoteName := gui.trimmedContent(v)
if updatedRemoteName != remote.Name {
if err := gui.GitCommand.RenameRemote(remote.Name, updatedRemoteName); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
}
editUrlMessage := gui.Tr.TemplateLocalize(
"editRemoteUrl",
Teml{
"remoteName": updatedRemoteName,
},
)
return gui.createPromptPanel(g, branchesView, editUrlMessage, "", func(g *gocui.Gui, v *gocui.View) error {
updatedRemoteUrl := gui.trimmedContent(v)
if err := gui.GitCommand.UpdateRemoteUrl(updatedRemoteName, updatedRemoteUrl); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshRemotes()
})
})
}
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()
})
}
-28
View File
@@ -71,34 +71,6 @@ func (gui *Gui) refreshStashEntries(g *gocui.Gui) error {
return nil
}
func (gui *Gui) handleStashNextLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
panelState := gui.State.Panels.Stash
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.StashEntries), false)
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
return gui.handleStashEntrySelect(gui.g, v)
}
func (gui *Gui) handleStashPrevLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
panelState := gui.State.Panels.Stash
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.StashEntries), true)
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
return gui.handleStashEntrySelect(gui.g, v)
}
// specific functions
func (gui *Gui) handleStashApply(g *gocui.Gui, v *gocui.View) error {
+2 -1
View File
@@ -6,6 +6,7 @@ import (
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -34,7 +35,7 @@ func (gui *Gui) refreshStatus(g *gocui.Gui) error {
if len(branches) > 0 {
branch := branches[0]
name := utils.ColoredString(branch.Name, branch.GetColor())
name := utils.ColoredString(branch.Name, commands.GetBranchColor(branch.Name))
repoName := utils.GetCurrentRepoName()
status += fmt.Sprintf(" %s → %s", repoName, name)
}
+151
View File
@@ -0,0 +1,151 @@
package gui
import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
)
// list panel functions
func (gui *Gui) getSelectedTag() *commands.Tag {
selectedLine := gui.State.Panels.Tags.SelectedLine
if selectedLine == -1 || len(gui.State.Tags) == 0 {
return nil
}
return gui.State.Tags[selectedLine]
}
func (gui *Gui) handleTagSelect(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 = "Tag"
tag := gui.getSelectedTag()
if tag == nil {
return gui.renderString(g, "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))
}()
return nil
}
func (gui *Gui) refreshTags() error {
tags, err := gui.GitCommand.GetTags()
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
gui.State.Tags = tags
if gui.getBranchesView().Context == "tags" {
gui.renderTagsWithSelection()
}
return nil
}
func (gui *Gui) renderTagsWithSelection() error {
branchesView := gui.getBranchesView()
gui.refreshSelectedLine(&gui.State.Panels.Tags.SelectedLine, len(gui.State.Tags))
if err := gui.renderListPanel(branchesView, gui.State.Tags); err != nil {
return err
}
if gui.g.CurrentView() == branchesView && branchesView.Context == "tags" {
if err := gui.handleTagSelect(gui.g, branchesView); err != nil {
return err
}
}
return nil
}
func (gui *Gui) handleCheckoutTag(g *gocui.Gui, v *gocui.View) error {
tag := gui.getSelectedTag()
if tag == nil {
return nil
}
if err := gui.handleCheckoutRef(tag.Name); err != nil {
return err
}
return gui.switchBranchesPanelContext("local-branches")
}
func (gui *Gui) handleDeleteTag(g *gocui.Gui, v *gocui.View) error {
tag := gui.getSelectedTag()
if tag == nil {
return nil
}
prompt := gui.Tr.TemplateLocalize(
"DeleteTagPrompt",
Teml{
"tagName": tag.Name,
},
)
return gui.createConfirmationPanel(gui.g, v, true, gui.Tr.SLocalize("DeleteTagTitle"), prompt, func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.DeleteTag(tag.Name); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshTags()
}, nil)
}
func (gui *Gui) handlePushTag(g *gocui.Gui, v *gocui.View) error {
tag := gui.getSelectedTag()
if tag == nil {
return nil
}
title := gui.Tr.TemplateLocalize(
"PushTagTitle",
Teml{
"tagName": tag.Name,
},
)
return gui.createPromptPanel(gui.g, v, title, "origin", func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.PushTag(v.Buffer(), tag.Name); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshTags()
})
}
func (gui *Gui) handleCreateTag(g *gocui.Gui, v *gocui.View) error {
return gui.createPromptPanel(gui.g, v, gui.Tr.SLocalize("CreateTagTitle"), "", func(g *gocui.Gui, v *gocui.View) error {
// leaving commit SHA blank so that we're just creating the tag for the current commit
if err := gui.GitCommand.CreateLightweightTag(v.Buffer(), ""); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshTags()
})
}
+41 -16
View File
@@ -5,6 +5,7 @@ import (
"sort"
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/spkg/bom"
@@ -101,9 +102,21 @@ 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, false)
return gui.focusAndSelectFile(g, v)
case "branches":
return gui.handleBranchSelect(g, v)
branchesView := gui.getBranchesView()
switch branchesView.Context {
case "local-branches":
return gui.handleBranchSelect(g, v)
case "remotes":
return gui.handleRemoteSelect(g, v)
case "remote-branches":
return gui.handleRemoteBranchSelect(g, v)
case "tags":
return gui.handleTagSelect(g, v)
default:
return errors.New("unknown branches panel context: " + branchesView.Context)
}
case "commits":
return gui.handleCommitSelect(g, v)
case "commitFiles":
@@ -117,7 +130,7 @@ func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error {
case "credentials":
return gui.handleCredentialsViewFocused(g, v)
case "main":
if gui.State.Context == "merging" {
if gui.State.MainContext == "merging" {
return gui.refreshMergePanel()
}
v.Highlight = false
@@ -315,6 +328,11 @@ func (gui *Gui) getCommitFilesView() *gocui.View {
return v
}
func (gui *Gui) getMenuView() *gocui.View {
v, _ := gui.g.View("menu")
return v
}
func (gui *Gui) trimmedContent(v *gocui.View) string {
return strings.TrimSpace(v.Buffer())
}
@@ -362,19 +380,17 @@ func (gui *Gui) generalFocusLine(lineNumber int, bottomLine int, v *gocui.View)
return nil
}
func (gui *Gui) changeSelectedLine(line *int, total int, up bool) {
if up {
if *line == -1 || *line == 0 {
return
}
*line--
func (gui *Gui) changeSelectedLine(line *int, total int, change int) {
// TODO: find out why we're doing this
if *line == -1 {
return
}
if *line+change < 0 {
*line = 0
} else if *line+change >= total {
*line = total - 1
} else {
if *line == -1 || *line == total-1 {
return
}
*line++
*line += change
}
}
@@ -406,7 +422,7 @@ func (gui *Gui) renderPanelOptions() error {
case "menu":
return gui.renderMenuOptions()
case "main":
if gui.State.Context == "merging" {
if gui.State.MainContext == "merging" {
return gui.renderMergeOptions()
}
}
@@ -449,3 +465,12 @@ func (gui *Gui) handleClick(v *gocui.View, itemCount int, selectedLine *int, han
return handleSelect(gui.g, v)
}
// often gocui wants functions in the form `func(g *gocui.Gui, v *gocui.View) error`
// but sometimes we just have a function that returns an error, so this is a
// convenience wrapper to give gocui what it wants.
func (gui *Gui) wrappedHandler(f func() error) func(g *gocui.Gui, v *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
return f()
}
}
+100
View File
@@ -0,0 +1,100 @@
package gui
import (
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
)
type workspaceResetOption struct {
handler func() error
description string
command string
}
// GetDisplayStrings is a function.
func (r *workspaceResetOption) GetDisplayStrings(isFocused bool) []string {
return []string{r.description, color.New(color.FgRed).Sprint(r.command)}
}
func (gui *Gui) handleCreateResetMenu(g *gocui.Gui, v *gocui.View) error {
options := []*workspaceResetOption{
{
description: gui.Tr.SLocalize("discardAllChangesToAllFiles"),
command: "reset --hard HEAD && git clean -fd",
handler: func() error {
if err := gui.GitCommand.ResetAndClean(); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshFiles()
},
},
{
description: gui.Tr.SLocalize("discardAnyUnstagedChanges"),
command: "git checkout -- .",
handler: func() error {
if err := gui.GitCommand.DiscardAnyUnstagedFileChanges(); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshFiles()
},
},
{
description: gui.Tr.SLocalize("discardUntrackedFiles"),
command: "git clean -fd",
handler: func() error {
if err := gui.GitCommand.RemoveUntrackedFiles(); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshFiles()
},
},
{
description: gui.Tr.SLocalize("softReset"),
command: "git reset --soft HEAD",
handler: func() error {
if err := gui.GitCommand.ResetSoft("HEAD"); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshFiles()
},
},
{
description: gui.Tr.SLocalize("hardReset"),
command: "git reset --hard HEAD",
handler: func() error {
if err := gui.GitCommand.ResetHard("HEAD"); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshFiles()
},
},
{
description: gui.Tr.SLocalize("hardResetUpstream"),
command: "git reset --hard @{upstream}",
handler: func() error {
if err := gui.GitCommand.ResetHard("@{upstream}"); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshSidePanels(gui.g)
},
},
{
description: gui.Tr.SLocalize("cancel"),
handler: func() error {
return nil
},
},
}
handleMenuPress := func(index int) error {
return options[index].handler()
}
return gui.createMenu("", options, len(options), handleMenuPress)
}
+3 -3
View File
@@ -337,9 +337,6 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "newFocusedViewIs",
Other: "nieuw gefocussed weergave is {{.newFocusedView}}",
}, &i18n.Message{
ID: "CantCloseConfirmationPrompt",
Other: "Kon de bevestiging prompt niet sluiten: {{.error}}",
}, &i18n.Message{
ID: "MergeAborted",
Other: "Merge afgebroken",
@@ -760,6 +757,9 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "EnterUpstream",
Other: `Enter upstream as '<remote> <branchname>'`,
}, &i18n.Message{
ID: "ReturnToRemotesList",
Other: `return to remotes list`,
},
)
}
+106 -4
View File
@@ -147,6 +147,9 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "resolveMergeConflicts",
Other: "resolve merge conflicts",
}, &i18n.Message{
ID: "MergeConflictsTitle",
Other: "Merge Conflicts",
}, &i18n.Message{
ID: "checkout",
Other: "checkout",
@@ -218,7 +221,7 @@ func addEnglish(i18nObject *i18n.Bundle) error {
Other: "{{.selectedBranchName}} is not fully merged. Are you sure you want to delete it?",
}, &i18n.Message{
ID: "rebaseBranch",
Other: "rebase branch",
Other: "rebase checked-out branch onto this branch",
}, &i18n.Message{
ID: "CantRebaseOntoSelf",
Other: "You cannot rebase a branch onto itself",
@@ -396,9 +399,6 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "newFocusedViewIs",
Other: "new focused view is {{.newFocusedView}}",
}, &i18n.Message{
ID: "CantCloseConfirmationPrompt",
Other: "Could not close confirmation prompt: {{.error}}",
}, &i18n.Message{
ID: "NoChangedFiles",
Other: "No changed files",
@@ -753,6 +753,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 +825,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",
@@ -837,6 +843,102 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "EnterUpstream",
Other: `Enter upstream as '<remote> <branchname>'`,
}, &i18n.Message{
ID: "EnterUpstreamWithSlash",
Other: `Enter upstream as '<remote>/<branchname>'`,
}, &i18n.Message{
ID: "notTrackingRemote",
Other: "(not tracking any remote)",
}, &i18n.Message{
ID: "ReturnToRemotesList",
Other: `return to remotes list`,
}, &i18n.Message{
ID: "addNewRemote",
Other: `add new remote`,
}, &i18n.Message{
ID: "newRemoteName",
Other: `New remote name:`,
}, &i18n.Message{
ID: "newRemoteUrl",
Other: `New remote url:`,
}, &i18n.Message{
ID: "editRemoteName",
Other: `Enter updated remote name for {{ .remoteName }}:`,
}, &i18n.Message{
ID: "editRemoteUrl",
Other: `Enter updated remote url for {{ .remoteName }}:`,
}, &i18n.Message{
ID: "removeRemote",
Other: `remove remote`,
}, &i18n.Message{
ID: "removeRemotePrompt",
Other: "Are you sure you want to remove remote",
}, &i18n.Message{
ID: "DeleteRemoteBranch",
Other: "Delete Remote Branch",
}, &i18n.Message{
ID: "DeleteRemoteBranchMessage",
Other: "Are you sure you want to delete remote branch",
}, &i18n.Message{
ID: "setUpstream",
Other: "set as upstream of checked-out branch",
}, &i18n.Message{
ID: "SetUpstreamTitle",
Other: "Set upstream branch",
}, &i18n.Message{
ID: "SetUpstreamMessage",
Other: "Are you sure you want to set the upstream branch of '{{.checkedOut}}' to '{{.selected}}'",
}, &i18n.Message{
ID: "editRemote",
Other: "edit remote",
}, &i18n.Message{
ID: "tagCommit",
Other: "tag commit",
}, &i18n.Message{
ID: "TagNameTitle",
Other: "Tag name:",
}, &i18n.Message{
ID: "deleteTag",
Other: "delete tag",
}, &i18n.Message{
ID: "DeleteTagTitle",
Other: "Delete tag",
}, &i18n.Message{
ID: "DeleteTagPrompt",
Other: "Are you sure you want to delete tag '{{.tagName}}'?",
}, &i18n.Message{
ID: "PushTagTitle",
Other: "remote to push tag '{{.tagName}}' to:",
}, &i18n.Message{
ID: "pushTag",
Other: "push tag",
}, &i18n.Message{
ID: "createTag",
Other: "create tag",
}, &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:",
},
)
}
+3 -3
View File
@@ -329,9 +329,6 @@ func addPolish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "newFocusedViewIs",
Other: "nowy skupiony widok to {{.newFocusedView}}",
}, &i18n.Message{
ID: "CantCloseConfirmationPrompt",
Other: "Nie można zamknąć monitu potwierdzenia: {{.error}}",
}, &i18n.Message{
ID: "MergeAborted",
Other: "Scalanie anulowane",
@@ -743,6 +740,9 @@ func addPolish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "EnterUpstream",
Other: `Enter upstream as '<remote> <branchname>'`,
}, &i18n.Message{
ID: "ReturnToRemotesList",
Other: `return to remotes list`,
},
)
}
+11
View File
@@ -299,3 +299,14 @@ func DifferenceInt(a, b []int) []int {
}
return result
}
// used to keep a number n between 0 and max, allowing for wraparounds
func ModuloWithWrap(n, max int) int {
if n >= max {
return n % max
} else if n < 0 {
return max + n
} else {
return n
}
}
+2
View File
@@ -12,3 +12,5 @@
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/
.idea/
+25 -9
View File
@@ -172,41 +172,57 @@ Other more specific types can also be used as flag types. They will be automati
- time.Duration
- []time.Duration
# Recommended Program Structure
# An Example Program
Best practice when using flaggy includes setting your program's name, description, and version (at build time).
Best practice when using flaggy includes setting your program's name, description, and version (at build time) as shown in this example program.
```go
package main
import "github.com/integrii/flaggy"
// make a variable for the version which will be set at build time
// Make a variable for the version which will be set at build time.
var version = "unknown"
// keep subcommands as globals so you can easily check if they were used later on
// Keep subcommands as globals so you can easily check if they were used later on.
var mySubcommand *flaggy.Subcommand
// Setup the variables you want your incoming flags to set.
var testVar string
// If you would like an environment variable as the default for a value, just populate the flag
// with the value of the environment by default. If the flag corresponding to this value is not
// used, then it will not be changed.
var myVar = os.Getenv("MY_VAR")
func init() {
// Set your program's name and description. These appear in help output.
flaggy.SetName("Test Program")
flaggy.SetDescription("A little example program")
// you can disable various things by changing bools on the default parser
// (or your own parser if you have created one)
// You can disable various things by changing bools on the default parser
// (or your own parser if you have created one).
flaggy.DefaultParser.ShowHelpOnUnexpected = false
// you can set a help prepend or append on the default parser
// You can set a help prepend or append on the default parser.
flaggy.DefaultParser.AdditionalHelpPrepend = "http://github.com/integrii/flaggy"
// Add a flag to the main program (this will be available in all subcommands as well).
flaggy.String(&testVar, "tv", "testVariable", "A variable just for testing things!")
// create any subcommands and set their parameters
// Create any subcommands and set their parameters.
mySubcommand = flaggy.NewSubcommand("mySubcommand")
mySubcommand.Description = "My great subcommand!"
// Add a flag to the subcommand.
mySubcommand.String(&myVar, "mv", "myVariable", "A variable just for me!")
// set the version and parse all inputs into variables
// Set the version and parse all inputs into variables.
flaggy.SetVersion(version)
flaggy.Parse()
}
func main(){
if mySubcommand.Used {
...
+3 -5
View File
@@ -5,22 +5,20 @@ package flaggy
// The return values represent the key being set, and any errors
// returned when setting the key, such as failures to convert the string
// into the appropriate flag value. We stop assigning values as soon
// as we find a parser that accepts it.
// as we find a any parser that accepts it.
func setValueForParsers(key string, value string, parsers ...ArgumentParser) (bool, error) {
var valueWasSet bool
for _, p := range parsers {
valueWasSet, err := p.SetValueForKey(key, value)
if err != nil {
return valueWasSet, err
}
if valueWasSet {
break
return true, nil
}
}
return valueWasSet, nil
return false, nil
}
// ArgumentParser represents a parser or subcommand
+12
View File
@@ -327,6 +327,18 @@ func exitOrPanic(code int) {
os.Exit(code)
}
// ShowHelpOnUnexpectedEnable enables the ShowHelpOnUnexpected behavior on the
// default parser. This causes unknown inputs to error out.
func ShowHelpOnUnexpectedEnable() {
DefaultParser.ShowHelpOnUnexpected = true
}
// ShowHelpOnUnexpectedDisable disables the ShowHelpOnUnexpected behavior on the
// default parser. This causes unknown inputs to error out.
func ShowHelpOnUnexpectedDisable() {
DefaultParser.ShowHelpOnUnexpected = false
}
// AddPositionalValue adds a positional value to the main parser at the global
// context
func AddPositionalValue(assignmentVar *string, name string, relativePosition int, required bool, description string) {
+23
View File
@@ -0,0 +1,23 @@
package flaggy
// parsedValue represents a flag or subcommand that was parsed. Primairily used
// to account for all parsed values in order to determine if unknown values were
// passed to the root parser after all subcommands have been parsed.
type parsedValue struct {
Key string
Value string
IsPositional bool // indicates that this value was positional and not a key/value
}
// newParsedValue creates and returns a new parsedValue struct with the
// supplied values set
func newParsedValue(key string, value string, isPositional bool) parsedValue {
if len(key) == 0 && len(value) == 0 {
panic("cant add parsed value with no key or value")
}
return parsedValue{
Key: key,
Value: value,
IsPositional: isPositional,
}
}
+90 -6
View File
@@ -4,18 +4,20 @@ import (
"errors"
"fmt"
"os"
"strconv"
"text/template"
)
// Parser represents the set of vars and subcommands we are expecting
// from our input args, and the parser than handles them all.
// Parser represents the set of flags and subcommands we are expecting
// from our input arguments. Parser is the top level struct responsible for
// parsing an entire set of subcommands and flags.
type Parser struct {
Subcommand
Version string // the optional version of the parser.
ShowHelpWithHFlag bool // display help when -h or --help passed
ShowVersionWithVersionFlag bool // display the version when --version passed
ShowHelpOnUnexpected bool // display help when an unexpected flag is passed
ShowHelpOnUnexpected bool // display help when an unexpected flag or subcommand is passed
TrailingArguments []string // everything after a -- is placed here
HelpTemplate *template.Template // template for Help output
trailingArgumentsExtracted bool // indicates that trailing args have been parsed and should not be appended again
@@ -46,8 +48,89 @@ func (p *Parser) ParseArgs(args []string) error {
return errors.New("Parser.Parse() called twice on parser with name: " + " " + p.Name + " " + p.ShortName)
}
p.parsed = true
// debugPrint("Kicking off parsing with args:", args)
return p.parse(p, args, 0)
debugPrint("Kicking off parsing with args:", args)
err := p.parse(p, args, 0)
if err != nil {
return err
}
// if we are set to crash on unexpected args, look for those here TODO
if p.ShowHelpOnUnexpected {
parsedValues := p.findAllParsedValues()
debugPrint("parsedValues:", parsedValues)
argsNotParsed := findArgsNotInParsedValues(args, parsedValues)
if len(argsNotParsed) > 0 {
// flatten out unused args for our error message
var argsNotParsedFlat string
for _, a := range argsNotParsed {
argsNotParsedFlat = argsNotParsedFlat + " " + a
}
p.ShowHelpAndExit("Unknown arguments supplied: " + argsNotParsedFlat)
}
}
return nil
}
// findArgsNotInParsedValues finds arguments not used in parsed values. The
// incoming args should be in the order supplied by the user and should not
// include the invoked binary, which is normally the first thing in os.Args.
func findArgsNotInParsedValues(args []string, parsedValues []parsedValue) []string {
var argsNotUsed []string
var skipNext bool
for _, a := range args {
// if the final argument (--) is seen, then we stop checking because all
// further values are trailing arguments.
if determineArgType(a) == argIsFinal {
return argsNotUsed
}
// allow for skipping the next arg when needed
if skipNext {
skipNext = false
continue
}
// strip flag slashes from incoming arguments so they match up with the
// keys from parsedValues.
arg := parseFlagToName(a)
// indicates that we found this arg used in one of the parsed values. Used
// to indicate which values should be added to argsNotUsed.
var foundArgUsed bool
// search all args for a corresponding parsed value
for _, pv := range parsedValues {
// this argumenet was a key
// debugPrint(pv.Key, "==", arg)
debugPrint(pv.Key + "==" + arg + " || (" + strconv.FormatBool(pv.IsPositional) + " && " + pv.Value + " == " + arg + ")")
if pv.Key == arg || (pv.IsPositional && pv.Value == arg) {
debugPrint("Found matching parsed arg for " + pv.Key)
foundArgUsed = true // the arg was used in this parsedValues set
// if the value is not a positional value and the parsed value had a
// value that was not blank, we skip the next value in the argument list
if !pv.IsPositional && len(pv.Value) > 0 {
skipNext = true
break
}
}
// this prevents excessive parsed values from being checked after we find
// the arg used for the first time
if foundArgUsed {
break
}
}
// if the arg was not used in any parsed values, then we add it to the slice
// of arguments not used
if !foundArgUsed {
argsNotUsed = append(argsNotUsed, arg)
}
}
return argsNotUsed
}
// ShowVersionAndExit shows the version of this parser
@@ -105,7 +188,8 @@ func (p *Parser) ShowHelpWithMessage(message string) {
}
}
// Disable show version with --version. It is enabled by default.
// DisableShowVersionWithVersion disables the showing of version information
// with --version. It is enabled by default.
func (p *Parser) DisableShowVersionWithVersion() {
p.ShowVersionWithVersionFlag = false
}
+86 -23
View File
@@ -23,15 +23,20 @@ type Subcommand struct {
Subcommands []*Subcommand
Flags []*Flag
PositionalFlags []*PositionalValue
AdditionalHelpPrepend string // additional prepended message when Help is displayed
AdditionalHelpAppend string // additional appended message when Help is displayed
Used bool // indicates this subcommand was found and parsed
Hidden bool // indicates this subcommand should be hidden from help
ParsedValues []parsedValue // a list of values and positionals parsed
AdditionalHelpPrepend string // additional prepended message when Help is displayed
AdditionalHelpAppend string // additional appended message when Help is displayed
Used bool // indicates this subcommand was found and parsed
Hidden bool // indicates this subcommand should be hidden from help
}
// NewSubcommand creates a new subcommand that can have flags or PositionalFlags
// added to it. The position starts with 1, not 0
func NewSubcommand(name string) *Subcommand {
if len(name) == 0 {
fmt.Fprintln(os.Stderr, "Error creating subcommand (NewSubcommand()). No subcommand name was specified.")
exitOrPanic(2)
}
newSC := &Subcommand{
Name: name,
}
@@ -39,10 +44,11 @@ func NewSubcommand(name string) *Subcommand {
}
// parseAllFlagsFromArgs parses the non-positional flags such as -f or -v=value
// out of the supplied args and returns the positional items in order.
// out of the supplied args and returns the resulting positional items in order,
// all the flag names found (without values), a bool to indicate if help was
// requested, and any errors found during parsing
func (sc *Subcommand) parseAllFlagsFromArgs(p *Parser, args []string) ([]string, bool, error) {
var err error
var positionalOnlyArguments []string
var helpRequested bool // indicates the user has supplied -h and we
// should render help if we are the last subcommand
@@ -58,7 +64,7 @@ func (sc *Subcommand) parseAllFlagsFromArgs(p *Parser, args []string) ([]string,
// find all the normal flags (not positional) and parse them out
for i, a := range args {
debugPrint("parsing arg", 1, a)
debugPrint("parsing arg:", a)
// evaluate if there is a following arg to avoid panics
var nextArgExists bool
@@ -121,62 +127,107 @@ func (sc *Subcommand) parseAllFlagsFromArgs(p *Parser, args []string) ([]string,
// this positional argument into a slice of their own, so that
// we can determine if its a subcommand or positional value later
positionalOnlyArguments = append(positionalOnlyArguments, a)
case argIsFlagWithSpace:
// track this as a parsed value with the subcommand
sc.addParsedPositionalValue(a)
case argIsFlagWithSpace: // a flag with a space. ex) -k v or --key value
a = parseFlagToName(a)
// debugPrint("Arg", i, "is flag with space:", a)
// parse next arg as value to this flag and apply to subcommand flags
// if the flag is a bool flag, then we check for a following positional
// and skip it if necessary
if flagIsBool(sc, p, a) {
debugPrint(sc.Name, "bool flag", a, "next var is:", nextArg)
_, err = setValueForParsers(a, "true", p, sc)
// set the value in this subcommand and its root parser
valueSet, err := setValueForParsers(a, "true", p, sc)
// if an error occurs, just return it and quit parsing
if err != nil {
return []string{}, false, err
}
// by default, we just assign the next argument to the value and continue
// log all values parsed by this subcommand. We leave the value blank
// because the bool value had no explicit true or false supplied
if valueSet {
sc.addParsedFlag(a, "")
}
// we've found and set a standalone bool flag, so we move on to the next
// argument in the list of arguments
continue
}
skipNext = true
debugPrint(sc.Name, "NOT bool flag", a)
// debugPrint(sc.Name, "NOT bool flag", a)
// if the next arg was not found, then show a Help message
if !nextArgExists {
p.ShowHelpWithMessage("Expected a following arg for flag " + a + ", but it did not exist.")
exitOrPanic(2)
}
_, err = setValueForParsers(a, nextArg, p, sc)
valueSet, err := setValueForParsers(a, nextArg, p, sc)
if err != nil {
return []string{}, false, err
}
case argIsFlagWithValue:
// log all parsed values in the subcommand
if valueSet {
sc.addParsedFlag(a, nextArg)
}
case argIsFlagWithValue: // a flag with an equals sign. ex) -k=v or --key=value
// debugPrint("Arg", i, "is flag with value:", a)
a = parseFlagToName(a)
// parse flag into key and value and apply to subcommand flags
key, val := parseArgWithValue(a)
_, err = setValueForParsers(key, val, p, sc)
// set the value in this subcommand and its root parser
valueSet, err := setValueForParsers(key, val, p, sc)
if err != nil {
return []string{}, false, err
}
// if this flag type was found and not set, and the parser is set to show
// Help when an unknown flag is found, then show Help and exit.
}
// log all values parsed by the subcommand
if valueSet {
sc.addParsedFlag(a, val)
}
}
}
return positionalOnlyArguments, helpRequested, nil
}
// Parse causes the argument parser to parse based on the supplied []string.
// depth specifies the non-flag subcommand positional depth
// findAllParsedValues finds all values parsed by all subcommands and this
// subcommand and its child subcommands
func (sc *Subcommand) findAllParsedValues() []parsedValue {
parsedValues := sc.ParsedValues
for _, sc := range sc.Subcommands {
// skip unused subcommands
if !sc.Used {
continue
}
parsedValues = append(parsedValues, sc.findAllParsedValues()...)
}
return parsedValues
}
// parse causes the argument parser to parse based on the supplied []string.
// depth specifies the non-flag subcommand positional depth. A slice of flags
// and subcommands parsed is returned so that the parser can ultimately decide
// if there were any unexpected values supplied by the user
func (sc *Subcommand) parse(p *Parser, args []string, depth int) error {
debugPrint("- Parsing subcommand", sc.Name, "with depth of", depth, "and args", args)
// if a command is parsed, its used
sc.Used = true
debugPrint("used subcommand", sc.Name, sc.ShortName)
if len(sc.Name) > 0 {
sc.addParsedPositionalValue(sc.Name)
}
if len(sc.ShortName) > 0 {
sc.addParsedPositionalValue(sc.ShortName)
}
// as subcommands are used, they become the context of the parser. This helps
// us understand how to display help based on which subcommand is being used
@@ -191,9 +242,10 @@ func (sc *Subcommand) parse(p *Parser, args []string, depth int) error {
sc.ensureNoConflictWithBuiltinVersion()
}
// Parse the normal flags out of the argument list and retain the positionals.
// Apply the flags to the parent parser and the current subcommand context.
// ./command -f -z subcommand someVar -b becomes ./command subcommand somevar
// Parse the normal flags out of the argument list and return the positionals
// (subcommands and positional values), along with the flags used.
// Then the flag values are applied to the parent parser and the current
// subcommand being parsed.
positionalOnlyArguments, helpRequested, err := sc.parseAllFlagsFromArgs(p, args)
if err != nil {
return err
@@ -292,7 +344,7 @@ func (sc *Subcommand) parse(p *Parser, args []string, depth int) error {
}
// find any positionals that were not used on subcommands that were
// found and throw help (unknown argument)
// found and throw help (unknown argument) in the global parse or subcommand
for _, pv := range p.PositionalFlags {
if pv.Required && !pv.Found {
p.ShowHelpWithMessage("Required global positional variable " + pv.Name + " not found at position " + strconv.Itoa(pv.Position))
@@ -309,6 +361,17 @@ func (sc *Subcommand) parse(p *Parser, args []string, depth int) error {
return nil
}
// addParsedFlag makes it easy to append flag values parsed by the subcommand
func (sc *Subcommand) addParsedFlag(key string, value string) {
sc.ParsedValues = append(sc.ParsedValues, newParsedValue(key, value, false))
}
// addParsedPositionalValue makes it easy to append positionals parsed by the
// subcommand
func (sc *Subcommand) addParsedPositionalValue(value string) {
sc.ParsedValues = append(sc.ParsedValues, newParsedValue("", value, true))
}
// FlagExists lets you know if the flag name exists as either a short or long
// name in the (sub)command
func (sc *Subcommand) FlagExists(name string) bool {
+2 -2
View File
@@ -295,14 +295,14 @@ func (g *Gui) CurrentView() *View {
// SetKeybinding creates a new keybinding. If viewname equals to ""
// (empty string) then the keybinding will apply to all views. key must
// be a rune or a Key.
func (g *Gui) SetKeybinding(viewname string, key interface{}, mod Modifier, handler func(*Gui, *View) error) error {
func (g *Gui) SetKeybinding(viewname string, contexts []string, key interface{}, mod Modifier, handler func(*Gui, *View) error) error {
var kb *keybinding
k, ch, err := getKey(key)
if err != nil {
return err
}
kb = newKeybinding(viewname, k, ch, mod, handler)
kb = newKeybinding(viewname, contexts, k, ch, mod, handler)
g.keybindings = append(g.keybindings, kb)
return nil
}
+17 -3
View File
@@ -9,6 +9,7 @@ import "github.com/jesseduffield/termbox-go"
// Keybidings are used to link a given key-press event with a handler.
type keybinding struct {
viewName string
contexts []string
key Key
ch rune
mod Modifier
@@ -16,9 +17,10 @@ type keybinding struct {
}
// newKeybinding returns a new Keybinding object.
func newKeybinding(viewname string, key Key, ch rune, mod Modifier, handler func(*Gui, *View) error) (kb *keybinding) {
func newKeybinding(viewname string, contexts []string, key Key, ch rune, mod Modifier, handler func(*Gui, *View) error) (kb *keybinding) {
kb = &keybinding{
viewName: viewname,
contexts: contexts,
key: key,
ch: ch,
mod: mod,
@@ -32,7 +34,7 @@ func (kb *keybinding) matchKeypress(key Key, ch rune, mod Modifier) bool {
return kb.key == key && kb.ch == ch && kb.mod == mod
}
// matchView returns if the keybinding matches the current view.
// matchView returns if the keybinding matches the current view (and the view's context)
func (kb *keybinding) matchView(v *View) bool {
// if the user is typing in a field, ignore char keys
if v == nil {
@@ -41,7 +43,19 @@ func (kb *keybinding) matchView(v *View) bool {
if v.Editable == true && kb.ch != 0 {
return false
}
return kb.viewName == v.name
if kb.viewName != v.name {
return false
}
// if the keybinding doesn't specify contexts, it applies for all contexts
if len(kb.contexts) == 0 {
return true
}
for _, context := range kb.contexts {
if context == v.Context {
return true
}
}
return false
}
// Key represents special keys or keys combinations.
+2
View File
@@ -103,6 +103,8 @@ type View struct {
// ParentView is the view which catches events bubbled up from the given view if there's no matching handler
ParentView *View
Context string // this is for assigning keybindings to a view only in certain contexts
}
type viewLine struct {
-10
View File
@@ -1,10 +0,0 @@
// +build ignore
package pty
import "C"
type (
_C_int C.int
_C_uint C.uint
)
-17
View File
@@ -1,17 +0,0 @@
// +build ignore
package pty
/*
#define _KERNEL
#include <sys/conf.h>
#include <sys/param.h>
#include <sys/filio.h>
*/
import "C"
const (
_C_SPECNAMELEN = C.SPECNAMELEN /* max length of devicename */
)
type fiodgnameArg C.struct_fiodname_args
-15
View File
@@ -1,15 +0,0 @@
// +build ignore
package pty
/*
#include <sys/param.h>
#include <sys/filio.h>
*/
import "C"
const (
_C_SPECNAMELEN = C.SPECNAMELEN /* max length of devicename */
)
type fiodgnameArg C.struct_fiodgname_arg
-14
View File
@@ -1,14 +0,0 @@
// +build ignore
package pty
/*
#include <sys/time.h>
#include <stdlib.h>
#include <sys/tty.h>
*/
import "C"
type ptmget C.struct_ptmget
var ioctl_PTMGET = C.PTMGET
-39
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
)
+1
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
@@ -0,0 +1,11 @@
// +build linux darwin
package sequences
import (
"fmt"
)
func EnableVirtualTerminalProcessing(stream uintptr, enable bool) error {
return fmt.Errorf("windows only package")
}
+2 -2
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
+2 -4
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=
+1 -1
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
}
+7 -6
View File
@@ -102,11 +102,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 +126,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
+3
View File
@@ -1,2 +1,5 @@
test_program/test_program_bin
fuzz/
cmd/tomll/tomll
cmd/tomljson/tomljson
cmd/tomltestgen/tomltestgen
+132
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
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
+5
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).
+29 -9
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
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'
+2 -3
View File
@@ -1,6 +1,6 @@
#!/bin/bash
set -e
set -ex
reference_ref=${1:-master}
reference_git=${2:-.}
@@ -8,7 +8,6 @@ reference_git=${2:-.}
if ! `hash benchstat 2>/dev/null`; then
echo "Installing benchstat"
go get golang.org/x/perf/cmd/benchstat
go install golang.org/x/perf/cmd/benchstat
fi
tempdir=`mktemp -d /tmp/go-toml-benchmark-XXXXXX`
@@ -29,4 +28,4 @@ go test -bench=. -benchmem | tee ${local_benchmark}
echo ""
echo "=== diff"
benchstat -delta-test=none ${ref_benchmark} ${local_benchmark}
benchstat -delta-test=none ${ref_benchmark} ${local_benchmark}
+1 -1
View File
@@ -1,7 +1,7 @@
// Package toml is a TOML parser and manipulation library.
//
// This version supports the specification as described in
// https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md
// https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.5.0.md
//
// Marshaling
//
+26
View File
@@ -0,0 +1,26 @@
#!/bin/bash
set -xe
# go-fuzz doesn't support modules yet, so ensure we do everything
# in the old style GOPATH way
export GO111MODULE="off"
# install go-fuzz
go get -u github.com/dvyukov/go-fuzz/go-fuzz github.com/dvyukov/go-fuzz/go-fuzz-build
# target name can only contain lower-case letters (a-z), digits (0-9) and a dash (-)
# to add another target, make sure to create it with `fuzzit create target`
# before using `fuzzit create job`
TARGET=toml-fuzzer
go-fuzz-build -libfuzzer -o ${TARGET}.a github.com/pelletier/go-toml
clang -fsanitize=fuzzer ${TARGET}.a -o ${TARGET}
# install fuzzit for talking to fuzzit.dev service
# or latest version:
# https://github.com/fuzzitdev/fuzzit/releases/latest/download/fuzzit_Linux_x86_64
wget -q -O fuzzit https://github.com/fuzzitdev/fuzzit/releases/download/v2.4.52/fuzzit_Linux_x86_64
chmod a+x fuzzit
# TODO: change kkowalczyk to go-toml and create toml-fuzzer target there
./fuzzit create job --type $TYPE go-toml/${TARGET} ${TARGET}
+9
View File
@@ -0,0 +1,9 @@
module github.com/pelletier/go-toml
go 1.12
require (
github.com/BurntSushi/toml v0.3.1
github.com/davecgh/go-spew v1.1.1
gopkg.in/yaml.v2 v2.2.4
)
+11
View File
@@ -0,0 +1,11 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3 h1:fvjTMHxHEw/mxHbtzPi3JCcKXQRAnQTBRo6YCJSVHKI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+82 -54
View File
@@ -3,79 +3,107 @@
package toml
import (
"bytes"
"errors"
"fmt"
"unicode"
)
// Convert the bare key group string to an array.
// The input supports double quotation to allow "." inside the key name,
// The input supports double quotation and single quotation,
// but escape sequences are not supported. Lexers must unescape them beforehand.
func parseKey(key string) ([]string, error) {
groups := []string{}
var buffer bytes.Buffer
inQuotes := false
wasInQuotes := false
ignoreSpace := true
expectDot := false
runes := []rune(key)
var groups []string
for _, char := range key {
if ignoreSpace {
if char == ' ' {
continue
}
ignoreSpace = false
if len(key) == 0 {
return nil, errors.New("empty key")
}
idx := 0
for idx < len(runes) {
for ; idx < len(runes) && isSpace(runes[idx]); idx++ {
// skip leading whitespace
}
switch char {
case '"':
if inQuotes {
groups = append(groups, buffer.String())
buffer.Reset()
wasInQuotes = true
}
inQuotes = !inQuotes
expectDot = false
case '.':
if inQuotes {
buffer.WriteRune(char)
} else {
if !wasInQuotes {
if buffer.Len() == 0 {
return nil, errors.New("empty table key")
if idx >= len(runes) {
break
}
r := runes[idx]
if isValidBareChar(r) {
// parse bare key
startIdx := idx
endIdx := -1
idx++
for idx < len(runes) {
r = runes[idx]
if isValidBareChar(r) {
idx++
} else if r == '.' {
endIdx = idx
break
} else if isSpace(r) {
endIdx = idx
for ; idx < len(runes) && isSpace(runes[idx]); idx++ {
// skip trailing whitespace
}
groups = append(groups, buffer.String())
buffer.Reset()
if idx < len(runes) && runes[idx] != '.' {
return nil, fmt.Errorf("invalid key character after whitespace: %c", runes[idx])
}
break
} else {
return nil, fmt.Errorf("invalid bare key character: %c", r)
}
ignoreSpace = true
expectDot = false
wasInQuotes = false
}
case ' ':
if inQuotes {
buffer.WriteRune(char)
} else {
expectDot = true
if endIdx == -1 {
endIdx = idx
}
default:
if !inQuotes && !isValidBareChar(char) {
return nil, fmt.Errorf("invalid bare character: %c", char)
groups = append(groups, string(runes[startIdx:endIdx]))
} else if r == '\'' {
// parse single quoted key
idx++
startIdx := idx
for {
if idx >= len(runes) {
return nil, fmt.Errorf("unclosed single-quoted key")
}
r = runes[idx]
if r == '\'' {
groups = append(groups, string(runes[startIdx:idx]))
idx++
break
}
idx++
}
if !inQuotes && expectDot {
return nil, errors.New("what?")
} else if r == '"' {
// parse double quoted key
idx++
startIdx := idx
for {
if idx >= len(runes) {
return nil, fmt.Errorf("unclosed double-quoted key")
}
r = runes[idx]
if r == '"' {
groups = append(groups, string(runes[startIdx:idx]))
idx++
break
}
idx++
}
buffer.WriteRune(char)
expectDot = false
} else if r == '.' {
idx++
if idx >= len(runes) {
return nil, fmt.Errorf("unexpected end of key")
}
r = runes[idx]
if !isValidBareChar(r) && r != '\'' && r != '"' && r != ' ' {
return nil, fmt.Errorf("expecting key part after dot")
}
} else {
return nil, fmt.Errorf("invalid key character: %c", r)
}
}
if inQuotes {
return nil, errors.New("mismatched quotes")
}
if buffer.Len() > 0 {
groups = append(groups, buffer.String())
}
if len(groups) == 0 {
return nil, errors.New("empty key")
return nil, fmt.Errorf("empty key")
}
return groups, nil
}
+38 -8
View File
@@ -223,9 +223,12 @@ func (l *tomlLexer) lexRvalue() tomlLexStateFn {
}
possibleDate := l.peekString(35)
dateMatch := dateRegexp.FindString(possibleDate)
if dateMatch != "" {
l.fastForward(len(dateMatch))
dateSubmatches := dateRegexp.FindStringSubmatch(possibleDate)
if dateSubmatches != nil && dateSubmatches[0] != "" {
l.fastForward(len(dateSubmatches[0]))
if dateSubmatches[2] == "" { // no timezone information => local date
return l.lexLocalDate
}
return l.lexDate
}
@@ -247,13 +250,13 @@ func (l *tomlLexer) lexRvalue() tomlLexStateFn {
func (l *tomlLexer) lexLeftCurlyBrace() tomlLexStateFn {
l.next()
l.emit(tokenLeftCurlyBrace)
return l.lexRvalue
return l.lexVoid
}
func (l *tomlLexer) lexRightCurlyBrace() tomlLexStateFn {
l.next()
l.emit(tokenRightCurlyBrace)
return l.lexRvalue
return l.lexVoid
}
func (l *tomlLexer) lexDate() tomlLexStateFn {
@@ -261,6 +264,11 @@ func (l *tomlLexer) lexDate() tomlLexStateFn {
return l.lexRvalue
}
func (l *tomlLexer) lexLocalDate() tomlLexStateFn {
l.emit(tokenLocalDate)
return l.lexRvalue
}
func (l *tomlLexer) lexTrue() tomlLexStateFn {
l.fastForward(4)
l.emit(tokenTrue)
@@ -309,7 +317,7 @@ func (l *tomlLexer) lexKey() tomlLexStateFn {
if err != nil {
return l.errorf(err.Error())
}
growingString += str
growingString += "\"" + str + "\""
l.next()
continue
} else if r == '\'' {
@@ -318,13 +326,15 @@ func (l *tomlLexer) lexKey() tomlLexStateFn {
if err != nil {
return l.errorf(err.Error())
}
growingString += str
growingString += "'" + str + "'"
l.next()
continue
} else if r == '\n' {
return l.errorf("keys cannot contain new lines")
} else if isSpace(r) {
break
} else if r == '.' {
// skip
} else if !isValidBareChar(r) {
return l.errorf("keys cannot contain %c character", r)
}
@@ -731,7 +741,27 @@ func (l *tomlLexer) run() {
}
func init() {
dateRegexp = regexp.MustCompile(`^\d{1,4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,9})?(Z|[+-]\d{2}:\d{2})`)
// Regexp for all date/time formats supported by TOML.
// Group 1: nano precision
// Group 2: timezone
//
// /!\ also matches the empty string
//
// Example matches:
//1979-05-27T07:32:00Z
//1979-05-27T00:32:00-07:00
//1979-05-27T00:32:00.999999-07:00
//1979-05-27 07:32:00Z
//1979-05-27 00:32:00-07:00
//1979-05-27 00:32:00.999999-07:00
//1979-05-27T07:32:00
//1979-05-27T00:32:00.999999
//1979-05-27 07:32:00
//1979-05-27 00:32:00.999999
//1979-05-27
//07:32:00
//00:32:00.999999
dateRegexp = regexp.MustCompile(`^(?:\d{1,4}-\d{2}-\d{2})?(?:[T ]?\d{2}:\d{2}:\d{2}(\.\d{1,9})?(Z|[+-]\d{2}:\d{2})?)?`)
}
// Entry point
+281
View File
@@ -0,0 +1,281 @@
// Implementation of TOML's local date/time.
// Copied over from https://github.com/googleapis/google-cloud-go/blob/master/civil/civil.go
// to avoid pulling all the Google dependencies.
//
// Copyright 2016 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package civil implements types for civil time, a time-zone-independent
// representation of time that follows the rules of the proleptic
// Gregorian calendar with exactly 24-hour days, 60-minute hours, and 60-second
// minutes.
//
// Because they lack location information, these types do not represent unique
// moments or intervals of time. Use time.Time for that purpose.
package toml
import (
"fmt"
"time"
)
// A LocalDate represents a date (year, month, day).
//
// This type does not include location information, and therefore does not
// describe a unique 24-hour timespan.
type LocalDate struct {
Year int // Year (e.g., 2014).
Month time.Month // Month of the year (January = 1, ...).
Day int // Day of the month, starting at 1.
}
// LocalDateOf returns the LocalDate in which a time occurs in that time's location.
func LocalDateOf(t time.Time) LocalDate {
var d LocalDate
d.Year, d.Month, d.Day = t.Date()
return d
}
// ParseLocalDate parses a string in RFC3339 full-date format and returns the date value it represents.
func ParseLocalDate(s string) (LocalDate, error) {
t, err := time.Parse("2006-01-02", s)
if err != nil {
return LocalDate{}, err
}
return LocalDateOf(t), nil
}
// String returns the date in RFC3339 full-date format.
func (d LocalDate) String() string {
return fmt.Sprintf("%04d-%02d-%02d", d.Year, d.Month, d.Day)
}
// IsValid reports whether the date is valid.
func (d LocalDate) IsValid() bool {
return LocalDateOf(d.In(time.UTC)) == d
}
// In returns the time corresponding to time 00:00:00 of the date in the location.
//
// In is always consistent with time.LocalDate, even when time.LocalDate returns a time
// on a different day. For example, if loc is America/Indiana/Vincennes, then both
// time.LocalDate(1955, time.May, 1, 0, 0, 0, 0, loc)
// and
// civil.LocalDate{Year: 1955, Month: time.May, Day: 1}.In(loc)
// return 23:00:00 on April 30, 1955.
//
// In panics if loc is nil.
func (d LocalDate) In(loc *time.Location) time.Time {
return time.Date(d.Year, d.Month, d.Day, 0, 0, 0, 0, loc)
}
// AddDays returns the date that is n days in the future.
// n can also be negative to go into the past.
func (d LocalDate) AddDays(n int) LocalDate {
return LocalDateOf(d.In(time.UTC).AddDate(0, 0, n))
}
// DaysSince returns the signed number of days between the date and s, not including the end day.
// This is the inverse operation to AddDays.
func (d LocalDate) DaysSince(s LocalDate) (days int) {
// We convert to Unix time so we do not have to worry about leap seconds:
// Unix time increases by exactly 86400 seconds per day.
deltaUnix := d.In(time.UTC).Unix() - s.In(time.UTC).Unix()
return int(deltaUnix / 86400)
}
// Before reports whether d1 occurs before d2.
func (d1 LocalDate) Before(d2 LocalDate) bool {
if d1.Year != d2.Year {
return d1.Year < d2.Year
}
if d1.Month != d2.Month {
return d1.Month < d2.Month
}
return d1.Day < d2.Day
}
// After reports whether d1 occurs after d2.
func (d1 LocalDate) After(d2 LocalDate) bool {
return d2.Before(d1)
}
// MarshalText implements the encoding.TextMarshaler interface.
// The output is the result of d.String().
func (d LocalDate) MarshalText() ([]byte, error) {
return []byte(d.String()), nil
}
// UnmarshalText implements the encoding.TextUnmarshaler interface.
// The date is expected to be a string in a format accepted by ParseLocalDate.
func (d *LocalDate) UnmarshalText(data []byte) error {
var err error
*d, err = ParseLocalDate(string(data))
return err
}
// A LocalTime represents a time with nanosecond precision.
//
// This type does not include location information, and therefore does not
// describe a unique moment in time.
//
// This type exists to represent the TIME type in storage-based APIs like BigQuery.
// Most operations on Times are unlikely to be meaningful. Prefer the LocalDateTime type.
type LocalTime struct {
Hour int // The hour of the day in 24-hour format; range [0-23]
Minute int // The minute of the hour; range [0-59]
Second int // The second of the minute; range [0-59]
Nanosecond int // The nanosecond of the second; range [0-999999999]
}
// LocalTimeOf returns the LocalTime representing the time of day in which a time occurs
// in that time's location. It ignores the date.
func LocalTimeOf(t time.Time) LocalTime {
var tm LocalTime
tm.Hour, tm.Minute, tm.Second = t.Clock()
tm.Nanosecond = t.Nanosecond()
return tm
}
// ParseLocalTime parses a string and returns the time value it represents.
// ParseLocalTime accepts an extended form of the RFC3339 partial-time format. After
// the HH:MM:SS part of the string, an optional fractional part may appear,
// consisting of a decimal point followed by one to nine decimal digits.
// (RFC3339 admits only one digit after the decimal point).
func ParseLocalTime(s string) (LocalTime, error) {
t, err := time.Parse("15:04:05.999999999", s)
if err != nil {
return LocalTime{}, err
}
return LocalTimeOf(t), nil
}
// String returns the date in the format described in ParseLocalTime. If Nanoseconds
// is zero, no fractional part will be generated. Otherwise, the result will
// end with a fractional part consisting of a decimal point and nine digits.
func (t LocalTime) String() string {
s := fmt.Sprintf("%02d:%02d:%02d", t.Hour, t.Minute, t.Second)
if t.Nanosecond == 0 {
return s
}
return s + fmt.Sprintf(".%09d", t.Nanosecond)
}
// IsValid reports whether the time is valid.
func (t LocalTime) IsValid() bool {
// Construct a non-zero time.
tm := time.Date(2, 2, 2, t.Hour, t.Minute, t.Second, t.Nanosecond, time.UTC)
return LocalTimeOf(tm) == t
}
// MarshalText implements the encoding.TextMarshaler interface.
// The output is the result of t.String().
func (t LocalTime) MarshalText() ([]byte, error) {
return []byte(t.String()), nil
}
// UnmarshalText implements the encoding.TextUnmarshaler interface.
// The time is expected to be a string in a format accepted by ParseLocalTime.
func (t *LocalTime) UnmarshalText(data []byte) error {
var err error
*t, err = ParseLocalTime(string(data))
return err
}
// A LocalDateTime represents a date and time.
//
// This type does not include location information, and therefore does not
// describe a unique moment in time.
type LocalDateTime struct {
Date LocalDate
Time LocalTime
}
// Note: We deliberately do not embed LocalDate into LocalDateTime, to avoid promoting AddDays and Sub.
// LocalDateTimeOf returns the LocalDateTime in which a time occurs in that time's location.
func LocalDateTimeOf(t time.Time) LocalDateTime {
return LocalDateTime{
Date: LocalDateOf(t),
Time: LocalTimeOf(t),
}
}
// ParseLocalDateTime parses a string and returns the LocalDateTime it represents.
// ParseLocalDateTime accepts a variant of the RFC3339 date-time format that omits
// the time offset but includes an optional fractional time, as described in
// ParseLocalTime. Informally, the accepted format is
// YYYY-MM-DDTHH:MM:SS[.FFFFFFFFF]
// where the 'T' may be a lower-case 't'.
func ParseLocalDateTime(s string) (LocalDateTime, error) {
t, err := time.Parse("2006-01-02T15:04:05.999999999", s)
if err != nil {
t, err = time.Parse("2006-01-02t15:04:05.999999999", s)
if err != nil {
return LocalDateTime{}, err
}
}
return LocalDateTimeOf(t), nil
}
// String returns the date in the format described in ParseLocalDate.
func (dt LocalDateTime) String() string {
return dt.Date.String() + "T" + dt.Time.String()
}
// IsValid reports whether the datetime is valid.
func (dt LocalDateTime) IsValid() bool {
return dt.Date.IsValid() && dt.Time.IsValid()
}
// In returns the time corresponding to the LocalDateTime in the given location.
//
// If the time is missing or ambigous at the location, In returns the same
// result as time.LocalDate. For example, if loc is America/Indiana/Vincennes, then
// both
// time.LocalDate(1955, time.May, 1, 0, 30, 0, 0, loc)
// and
// civil.LocalDateTime{
// civil.LocalDate{Year: 1955, Month: time.May, Day: 1}},
// civil.LocalTime{Minute: 30}}.In(loc)
// return 23:30:00 on April 30, 1955.
//
// In panics if loc is nil.
func (dt LocalDateTime) In(loc *time.Location) time.Time {
return time.Date(dt.Date.Year, dt.Date.Month, dt.Date.Day, dt.Time.Hour, dt.Time.Minute, dt.Time.Second, dt.Time.Nanosecond, loc)
}
// Before reports whether dt1 occurs before dt2.
func (dt1 LocalDateTime) Before(dt2 LocalDateTime) bool {
return dt1.In(time.UTC).Before(dt2.In(time.UTC))
}
// After reports whether dt1 occurs after dt2.
func (dt1 LocalDateTime) After(dt2 LocalDateTime) bool {
return dt2.Before(dt1)
}
// MarshalText implements the encoding.TextMarshaler interface.
// The output is the result of dt.String().
func (dt LocalDateTime) MarshalText() ([]byte, error) {
return []byte(dt.String()), nil
}
// UnmarshalText implements the encoding.TextUnmarshaler interface.
// The datetime is expected to be a string in a format accepted by ParseLocalDateTime
func (dt *LocalDateTime) UnmarshalText(data []byte) error {
var err error
*dt, err = ParseLocalDateTime(string(data))
return err
}
+335 -76
View File
@@ -6,20 +6,28 @@ import (
"fmt"
"io"
"reflect"
"sort"
"strconv"
"strings"
"time"
)
const tagKeyMultiline = "multiline"
const (
tagFieldName = "toml"
tagFieldComment = "comment"
tagCommented = "commented"
tagMultiline = "multiline"
tagDefault = "default"
)
type tomlOpts struct {
name string
comment string
commented bool
multiline bool
include bool
omitempty bool
name string
comment string
commented bool
multiline bool
include bool
omitempty bool
defaultValue string
}
type encOpts struct {
@@ -31,10 +39,40 @@ var encOptsDefaults = encOpts{
quoteMapKeys: false,
}
type annotation struct {
tag string
comment string
commented string
multiline string
defaultValue string
}
var annotationDefault = annotation{
tag: tagFieldName,
comment: tagFieldComment,
commented: tagCommented,
multiline: tagMultiline,
defaultValue: tagDefault,
}
type marshalOrder int
// Orders the Encoder can write the fields to the output stream.
const (
// Sort fields alphabetically.
OrderAlphabetical marshalOrder = iota + 1
// Preserve the order the fields are encountered. For example, the order of fields in
// a struct.
OrderPreserve
)
var timeType = reflect.TypeOf(time.Time{})
var marshalerType = reflect.TypeOf(new(Marshaler)).Elem()
var localDateType = reflect.TypeOf(LocalDate{})
var localTimeType = reflect.TypeOf(LocalTime{})
var localDateTimeType = reflect.TypeOf(LocalDateTime{})
// Check if the given marshall type maps to a Tree primitive
// Check if the given marshal type maps to a Tree primitive
func isPrimitive(mtype reflect.Type) bool {
switch mtype.Kind() {
case reflect.Ptr:
@@ -50,37 +88,41 @@ func isPrimitive(mtype reflect.Type) bool {
case reflect.String:
return true
case reflect.Struct:
return mtype == timeType || isCustomMarshaler(mtype)
return mtype == timeType || mtype == localDateType || mtype == localDateTimeType || mtype == localTimeType || isCustomMarshaler(mtype)
default:
return false
}
}
// Check if the given marshall type maps to a Tree slice
func isTreeSlice(mtype reflect.Type) bool {
switch mtype.Kind() {
case reflect.Slice:
return !isOtherSlice(mtype)
default:
return false
}
}
// Check if the given marshall type maps to a non-Tree slice
func isOtherSlice(mtype reflect.Type) bool {
// Check if the given marshal type maps to a Tree slice or array
func isTreeSequence(mtype reflect.Type) bool {
switch mtype.Kind() {
case reflect.Ptr:
return isOtherSlice(mtype.Elem())
case reflect.Slice:
return isPrimitive(mtype.Elem()) || isOtherSlice(mtype.Elem())
return isTreeSequence(mtype.Elem())
case reflect.Slice, reflect.Array:
return isTree(mtype.Elem())
default:
return false
}
}
// Check if the given marshall type maps to a Tree
// Check if the given marshal type maps to a non-Tree slice or array
func isOtherSequence(mtype reflect.Type) bool {
switch mtype.Kind() {
case reflect.Ptr:
return isOtherSequence(mtype.Elem())
case reflect.Slice, reflect.Array:
return !isTreeSequence(mtype)
default:
return false
}
}
// Check if the given marshal type maps to a Tree
func isTree(mtype reflect.Type) bool {
switch mtype.Kind() {
case reflect.Ptr:
return isTree(mtype.Elem())
case reflect.Map:
return true
case reflect.Struct:
@@ -135,7 +177,9 @@ Tree primitive types and corresponding marshal types:
float64 float32, float64, pointers to same
string string, pointers to same
bool bool, pointers to same
time.Time time.Time{}, pointers to same
time.LocalTime time.LocalTime{}, pointers to same
For additional flexibility, use the Encoder API.
*/
func Marshal(v interface{}) ([]byte, error) {
return NewEncoder(nil).marshal(v)
@@ -145,13 +189,21 @@ func Marshal(v interface{}) ([]byte, error) {
type Encoder struct {
w io.Writer
encOpts
annotation
line int
col int
order marshalOrder
}
// NewEncoder returns a new encoder that writes to w.
func NewEncoder(w io.Writer) *Encoder {
return &Encoder{
w: w,
encOpts: encOptsDefaults,
w: w,
encOpts: encOptsDefaults,
annotation: annotationDefault,
line: 0,
col: 1,
order: OrderAlphabetical,
}
}
@@ -197,11 +249,49 @@ func (e *Encoder) ArraysWithOneElementPerLine(v bool) *Encoder {
return e
}
// Order allows to change in which order fields will be written to the output stream.
func (e *Encoder) Order(ord marshalOrder) *Encoder {
e.order = ord
return e
}
// SetTagName allows changing default tag "toml"
func (e *Encoder) SetTagName(v string) *Encoder {
e.tag = v
return e
}
// SetTagComment allows changing default tag "comment"
func (e *Encoder) SetTagComment(v string) *Encoder {
e.comment = v
return e
}
// SetTagCommented allows changing default tag "commented"
func (e *Encoder) SetTagCommented(v string) *Encoder {
e.commented = v
return e
}
// SetTagMultiline allows changing default tag "multiline"
func (e *Encoder) SetTagMultiline(v string) *Encoder {
e.multiline = v
return e
}
func (e *Encoder) marshal(v interface{}) ([]byte, error) {
mtype := reflect.TypeOf(v)
if mtype.Kind() != reflect.Struct {
return []byte{}, errors.New("Only a struct can be marshaled to TOML")
switch mtype.Kind() {
case reflect.Struct, reflect.Map:
case reflect.Ptr:
if mtype.Elem().Kind() != reflect.Struct {
return []byte{}, errors.New("Only pointer to struct can be marshaled to TOML")
}
default:
return []byte{}, errors.New("Only a struct or map can be marshaled to TOML")
}
sval := reflect.ValueOf(v)
if isCustomMarshaler(mtype) {
return callCustomMarshaler(sval)
@@ -212,22 +302,27 @@ func (e *Encoder) marshal(v interface{}) ([]byte, error) {
}
var buf bytes.Buffer
_, err = t.writeTo(&buf, "", "", 0, e.arraysOneElementPerLine)
_, err = t.writeToOrdered(&buf, "", "", 0, e.arraysOneElementPerLine, e.order)
return buf.Bytes(), err
}
// Create next tree with a position based on Encoder.line
func (e *Encoder) nextTree() *Tree {
return newTreeWithPosition(Position{Line: e.line, Col: 1})
}
// Convert given marshal struct or map value to toml tree
func (e *Encoder) valueToTree(mtype reflect.Type, mval reflect.Value) (*Tree, error) {
if mtype.Kind() == reflect.Ptr {
return e.valueToTree(mtype.Elem(), mval.Elem())
}
tval := newTree()
tval := e.nextTree()
switch mtype.Kind() {
case reflect.Struct:
for i := 0; i < mtype.NumField(); i++ {
mtypef, mvalf := mtype.Field(i), mval.Field(i)
opts := tomlOptions(mtypef)
opts := tomlOptions(mtypef, e.annotation)
if opts.include && (!opts.omitempty || !isZero(mvalf)) {
val, err := e.valueToToml(mtypef.Type, mvalf)
if err != nil {
@@ -242,7 +337,26 @@ func (e *Encoder) valueToTree(mtype reflect.Type, mval reflect.Value) (*Tree, er
}
}
case reflect.Map:
for _, key := range mval.MapKeys() {
keys := mval.MapKeys()
if e.order == OrderPreserve && len(keys) > 0 {
// Sorting []reflect.Value is not straight forward.
//
// OrderPreserve will support deterministic results when string is used
// as the key to maps.
typ := keys[0].Type()
kind := keys[0].Kind()
if kind == reflect.String {
ikeys := make([]string, len(keys))
for i := range keys {
ikeys[i] = keys[i].Interface().(string)
}
sort.Strings(ikeys)
for i := range ikeys {
keys[i] = reflect.ValueOf(ikeys[i]).Convert(typ)
}
}
}
for _, key := range keys {
mvalf := mval.MapIndex(key)
val, err := e.valueToToml(mtype.Elem(), mvalf)
if err != nil {
@@ -290,6 +404,7 @@ func (e *Encoder) valueToOtherSlice(mtype reflect.Type, mval reflect.Value) (int
// Convert given marshal value to toml value
func (e *Encoder) valueToToml(mtype reflect.Type, mval reflect.Value) (interface{}, error) {
e.line++
if mtype.Kind() == reflect.Ptr {
return e.valueToToml(mtype.Elem(), mval.Elem())
}
@@ -298,15 +413,18 @@ func (e *Encoder) valueToToml(mtype reflect.Type, mval reflect.Value) (interface
return callCustomMarshaler(mval)
case isTree(mtype):
return e.valueToTree(mtype, mval)
case isTreeSlice(mtype):
case isTreeSequence(mtype):
return e.valueToTreeSlice(mtype, mval)
case isOtherSlice(mtype):
case isOtherSequence(mtype):
return e.valueToOtherSlice(mtype, mval)
default:
switch mtype.Kind() {
case reflect.Bool:
return mval.Bool(), nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if mtype.Kind() == reflect.Int64 && mtype == reflect.TypeOf(time.Duration(1)) {
return fmt.Sprint(mval), nil
}
return mval.Int(), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return mval.Uint(), nil
@@ -315,7 +433,7 @@ func (e *Encoder) valueToToml(mtype reflect.Type, mval reflect.Value) (interface
case reflect.String:
return mval.String(), nil
case reflect.Struct:
return mval.Interface().(time.Time), nil
return mval.Interface(), nil
default:
return nil, fmt.Errorf("Marshal can't handle %v(%v)", mtype, mtype.Kind())
}
@@ -326,7 +444,7 @@ func (e *Encoder) valueToToml(mtype reflect.Type, mval reflect.Value) (interface
// Neither Unmarshaler interfaces nor UnmarshalTOML functions are supported for
// sub-structs, and only definite types can be unmarshaled.
func (t *Tree) Unmarshal(v interface{}) error {
d := Decoder{tval: t}
d := Decoder{tval: t, tagName: tagFieldName}
return d.unmarshal(v)
}
@@ -334,8 +452,11 @@ func (t *Tree) Unmarshal(v interface{}) error {
// See Marshal() documentation for types mapping table.
func (t *Tree) Marshal() ([]byte, error) {
var buf bytes.Buffer
err := NewEncoder(&buf).Encode(t)
return buf.Bytes(), err
_, err := t.WriteTo(&buf)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// Unmarshal parses the TOML-encoded data and stores the result in the value
@@ -347,6 +468,14 @@ func (t *Tree) Marshal() ([]byte, error) {
// The following struct annotations are supported:
//
// toml:"Field" Overrides the field's name to map to.
// default:"foo" Provides a default value.
//
// For default values, only fields of the following types are supported:
// * string
// * bool
// * int
// * int64
// * float64
//
// See Marshal() documentation for types mapping table.
func Unmarshal(data []byte, v interface{}) error {
@@ -362,6 +491,7 @@ type Decoder struct {
r io.Reader
tval *Tree
encOpts
tagName string
}
// NewDecoder returns a new decoder that reads from r.
@@ -369,6 +499,7 @@ func NewDecoder(r io.Reader) *Decoder {
return &Decoder{
r: r,
encOpts: encOptsDefaults,
tagName: tagFieldName,
}
}
@@ -385,13 +516,29 @@ func (d *Decoder) Decode(v interface{}) error {
return d.unmarshal(v)
}
// SetTagName allows changing default tag "toml"
func (d *Decoder) SetTagName(v string) *Decoder {
d.tagName = v
return d
}
func (d *Decoder) unmarshal(v interface{}) error {
mtype := reflect.TypeOf(v)
if mtype.Kind() != reflect.Ptr || mtype.Elem().Kind() != reflect.Struct {
return errors.New("Only a pointer to struct can be unmarshaled from TOML")
if mtype.Kind() != reflect.Ptr {
return errors.New("only a pointer to struct or map can be unmarshaled from TOML")
}
sval, err := d.valueFromTree(mtype.Elem(), d.tval)
elem := mtype.Elem()
switch elem.Kind() {
case reflect.Struct, reflect.Map:
default:
return errors.New("only a pointer to struct or map can be unmarshaled from TOML")
}
vv := reflect.ValueOf(v).Elem()
sval, err := d.valueFromTree(elem, d.tval, &vv)
if err != nil {
return err
}
@@ -399,34 +546,92 @@ func (d *Decoder) unmarshal(v interface{}) error {
return nil
}
// Convert toml tree to marshal struct or map, using marshal type
func (d *Decoder) valueFromTree(mtype reflect.Type, tval *Tree) (reflect.Value, error) {
// Convert toml tree to marshal struct or map, using marshal type. When mval1
// is non-nil, merge fields into the given value instead of allocating a new one.
func (d *Decoder) valueFromTree(mtype reflect.Type, tval *Tree, mval1 *reflect.Value) (reflect.Value, error) {
if mtype.Kind() == reflect.Ptr {
return d.unwrapPointer(mtype, tval)
return d.unwrapPointer(mtype, tval, mval1)
}
var mval reflect.Value
switch mtype.Kind() {
case reflect.Struct:
mval = reflect.New(mtype).Elem()
if mval1 != nil {
mval = *mval1
} else {
mval = reflect.New(mtype).Elem()
}
for i := 0; i < mtype.NumField(); i++ {
mtypef := mtype.Field(i)
opts := tomlOptions(mtypef)
an := annotation{tag: d.tagName}
opts := tomlOptions(mtypef, an)
if opts.include {
baseKey := opts.name
keysToTry := []string{baseKey, strings.ToLower(baseKey), strings.ToTitle(baseKey)}
keysToTry := []string{
baseKey,
strings.ToLower(baseKey),
strings.ToTitle(baseKey),
strings.ToLower(string(baseKey[0])) + baseKey[1:],
}
found := false
for _, key := range keysToTry {
exists := tval.Has(key)
if !exists {
continue
}
val := tval.Get(key)
mvalf, err := d.valueFromToml(mtypef.Type, val)
fval := mval.Field(i)
mvalf, err := d.valueFromToml(mtypef.Type, val, &fval)
if err != nil {
return mval, formatError(err, tval.GetPosition(key))
}
mval.Field(i).Set(mvalf)
found = true
break
}
if !found && opts.defaultValue != "" {
mvalf := mval.Field(i)
var val interface{}
var err error
switch mvalf.Kind() {
case reflect.Bool:
val, err = strconv.ParseBool(opts.defaultValue)
if err != nil {
return mval.Field(i), err
}
case reflect.Int:
val, err = strconv.Atoi(opts.defaultValue)
if err != nil {
return mval.Field(i), err
}
case reflect.String:
val = opts.defaultValue
case reflect.Int64:
val, err = strconv.ParseInt(opts.defaultValue, 10, 64)
if err != nil {
return mval.Field(i), err
}
case reflect.Float64:
val, err = strconv.ParseFloat(opts.defaultValue, 64)
if err != nil {
return mval.Field(i), err
}
default:
return mval.Field(i), fmt.Errorf("unsuported field type for default option")
}
mval.Field(i).Set(reflect.ValueOf(val))
}
// save the old behavior above and try to check anonymous structs
if !found && opts.defaultValue == "" && mtypef.Anonymous && mtypef.Type.Kind() == reflect.Struct {
v, err := d.valueFromTree(mtypef.Type, tval, nil)
if err != nil {
return v, err
}
mval.Field(i).Set(v)
}
}
}
case reflect.Map:
@@ -434,11 +639,11 @@ func (d *Decoder) valueFromTree(mtype reflect.Type, tval *Tree) (reflect.Value,
for _, key := range tval.Keys() {
// TODO: path splits key
val := tval.GetPath([]string{key})
mvalf, err := d.valueFromToml(mtype.Elem(), val)
mvalf, err := d.valueFromToml(mtype.Elem(), val, nil)
if err != nil {
return mval, formatError(err, tval.GetPosition(key))
}
mval.SetMapIndex(reflect.ValueOf(key), mvalf)
mval.SetMapIndex(reflect.ValueOf(key).Convert(mtype.Key()), mvalf)
}
}
return mval, nil
@@ -448,7 +653,7 @@ func (d *Decoder) valueFromTree(mtype reflect.Type, tval *Tree) (reflect.Value,
func (d *Decoder) valueFromTreeSlice(mtype reflect.Type, tval []*Tree) (reflect.Value, error) {
mval := reflect.MakeSlice(mtype, len(tval), len(tval))
for i := 0; i < len(tval); i++ {
val, err := d.valueFromTree(mtype.Elem(), tval[i])
val, err := d.valueFromTree(mtype.Elem(), tval[i], nil)
if err != nil {
return mval, err
}
@@ -461,7 +666,7 @@ func (d *Decoder) valueFromTreeSlice(mtype reflect.Type, tval []*Tree) (reflect.
func (d *Decoder) valueFromOtherSlice(mtype reflect.Type, tval []interface{}) (reflect.Value, error) {
mval := reflect.MakeSlice(mtype, len(tval), len(tval))
for i := 0; i < len(tval); i++ {
val, err := d.valueFromToml(mtype.Elem(), tval[i])
val, err := d.valueFromToml(mtype.Elem(), tval[i], nil)
if err != nil {
return mval, err
}
@@ -470,33 +675,63 @@ func (d *Decoder) valueFromOtherSlice(mtype reflect.Type, tval []interface{}) (r
return mval, nil
}
// Convert toml value to marshal value, using marshal type
func (d *Decoder) valueFromToml(mtype reflect.Type, tval interface{}) (reflect.Value, error) {
// Convert toml value to marshal value, using marshal type. When mval1 is non-nil
// and the given type is a struct value, merge fields into it.
func (d *Decoder) valueFromToml(mtype reflect.Type, tval interface{}, mval1 *reflect.Value) (reflect.Value, error) {
if mtype.Kind() == reflect.Ptr {
return d.unwrapPointer(mtype, tval)
return d.unwrapPointer(mtype, tval, mval1)
}
switch tval.(type) {
switch t := tval.(type) {
case *Tree:
var mval11 *reflect.Value
if mtype.Kind() == reflect.Struct {
mval11 = mval1
}
if isTree(mtype) {
return d.valueFromTree(mtype, tval.(*Tree))
return d.valueFromTree(mtype, t, mval11)
}
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to a tree", tval, tval)
case []*Tree:
if isTreeSlice(mtype) {
return d.valueFromTreeSlice(mtype, tval.([]*Tree))
if isTreeSequence(mtype) {
return d.valueFromTreeSlice(mtype, t)
}
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to trees", tval, tval)
case []interface{}:
if isOtherSlice(mtype) {
return d.valueFromOtherSlice(mtype, tval.([]interface{}))
if isOtherSequence(mtype) {
return d.valueFromOtherSlice(mtype, t)
}
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to a slice", tval, tval)
default:
switch mtype.Kind() {
case reflect.Bool, reflect.Struct:
val := reflect.ValueOf(tval)
// if this passes for when mtype is reflect.Struct, tval is a time.Time
switch val.Type() {
case localDateType:
localDate := val.Interface().(LocalDate)
switch mtype {
case timeType:
return reflect.ValueOf(time.Date(localDate.Year, localDate.Month, localDate.Day, 0, 0, 0, 0, time.Local)), nil
}
case localDateTimeType:
localDateTime := val.Interface().(LocalDateTime)
switch mtype {
case timeType:
return reflect.ValueOf(time.Date(
localDateTime.Date.Year,
localDateTime.Date.Month,
localDateTime.Date.Day,
localDateTime.Time.Hour,
localDateTime.Time.Minute,
localDateTime.Time.Second,
localDateTime.Time.Nanosecond,
time.Local)), nil
}
}
// if this passes for when mtype is reflect.Struct, tval is a time.LocalTime
if !val.Type().ConvertibleTo(mtype) {
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v", tval, tval, mtype.String())
}
@@ -512,10 +747,17 @@ func (d *Decoder) valueFromToml(mtype reflect.Type, tval interface{}) (reflect.V
return val.Convert(mtype), nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
val := reflect.ValueOf(tval)
if mtype.Kind() == reflect.Int64 && mtype == reflect.TypeOf(time.Duration(1)) && val.Kind() == reflect.String {
d, err := time.ParseDuration(val.String())
if err != nil {
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v. %s", tval, tval, mtype.String(), err)
}
return reflect.ValueOf(d), nil
}
if !val.Type().ConvertibleTo(mtype) {
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v", tval, tval, mtype.String())
}
if reflect.Indirect(reflect.New(mtype)).OverflowInt(val.Int()) {
if reflect.Indirect(reflect.New(mtype)).OverflowInt(val.Convert(mtype).Int()) {
return reflect.ValueOf(nil), fmt.Errorf("%v(%T) would overflow %v", tval, tval, mtype.String())
}
@@ -525,10 +767,11 @@ func (d *Decoder) valueFromToml(mtype reflect.Type, tval interface{}) (reflect.V
if !val.Type().ConvertibleTo(mtype) {
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v", tval, tval, mtype.String())
}
if val.Int() < 0 {
if val.Convert(reflect.TypeOf(int(1))).Int() < 0 {
return reflect.ValueOf(nil), fmt.Errorf("%v(%T) is negative so does not fit in %v", tval, tval, mtype.String())
}
if reflect.Indirect(reflect.New(mtype)).OverflowUint(uint64(val.Int())) {
if reflect.Indirect(reflect.New(mtype)).OverflowUint(uint64(val.Convert(mtype).Uint())) {
return reflect.ValueOf(nil), fmt.Errorf("%v(%T) would overflow %v", tval, tval, mtype.String())
}
@@ -538,7 +781,7 @@ func (d *Decoder) valueFromToml(mtype reflect.Type, tval interface{}) (reflect.V
if !val.Type().ConvertibleTo(mtype) {
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v", tval, tval, mtype.String())
}
if reflect.Indirect(reflect.New(mtype)).OverflowFloat(val.Float()) {
if reflect.Indirect(reflect.New(mtype)).OverflowFloat(val.Convert(mtype).Float()) {
return reflect.ValueOf(nil), fmt.Errorf("%v(%T) would overflow %v", tval, tval, mtype.String())
}
@@ -549,8 +792,15 @@ func (d *Decoder) valueFromToml(mtype reflect.Type, tval interface{}) (reflect.V
}
}
func (d *Decoder) unwrapPointer(mtype reflect.Type, tval interface{}) (reflect.Value, error) {
val, err := d.valueFromToml(mtype.Elem(), tval)
func (d *Decoder) unwrapPointer(mtype reflect.Type, tval interface{}, mval1 *reflect.Value) (reflect.Value, error) {
var melem *reflect.Value
if mval1 != nil && !mval1.IsNil() && mtype.Elem().Kind() == reflect.Struct {
elem := mval1.Elem()
melem = &elem
}
val, err := d.valueFromToml(mtype.Elem(), tval, melem)
if err != nil {
return reflect.ValueOf(nil), err
}
@@ -559,16 +809,25 @@ func (d *Decoder) unwrapPointer(mtype reflect.Type, tval interface{}) (reflect.V
return mval, nil
}
func tomlOptions(vf reflect.StructField) tomlOpts {
tag := vf.Tag.Get("toml")
func tomlOptions(vf reflect.StructField, an annotation) tomlOpts {
tag := vf.Tag.Get(an.tag)
parse := strings.Split(tag, ",")
var comment string
if c := vf.Tag.Get("comment"); c != "" {
if c := vf.Tag.Get(an.comment); c != "" {
comment = c
}
commented, _ := strconv.ParseBool(vf.Tag.Get("commented"))
multiline, _ := strconv.ParseBool(vf.Tag.Get(tagKeyMultiline))
result := tomlOpts{name: vf.Name, comment: comment, commented: commented, multiline: multiline, include: true, omitempty: false}
commented, _ := strconv.ParseBool(vf.Tag.Get(an.commented))
multiline, _ := strconv.ParseBool(vf.Tag.Get(an.multiline))
defaultValue := vf.Tag.Get(tagDefault)
result := tomlOpts{
name: vf.Name,
comment: comment,
commented: commented,
multiline: multiline,
include: true,
omitempty: false,
defaultValue: defaultValue,
}
if parse[0] != "" {
if parse[0] == "-" && len(parse) == 1 {
result.include = false
+39
View File
@@ -0,0 +1,39 @@
title = "TOML Marshal Testing"
[basic_lists]
floats = [12.3,45.6,78.9]
bools = [true,false,true]
dates = [1979-05-27T07:32:00Z,1980-05-27T07:32:00Z]
ints = [8001,8001,8002]
uints = [5002,5003]
strings = ["One","Two","Three"]
[[subdocptrs]]
name = "Second"
[basic_map]
one = "one"
two = "two"
[subdoc]
[subdoc.second]
name = "Second"
[subdoc.first]
name = "First"
[basic]
uint = 5001
bool = true
float = 123.4
float64 = 123.456782132399
int = 5000
string = "Bite me"
date = 1979-05-27T07:32:00Z
[[subdoclist]]
name = "List.First"
[[subdoclist]]
name = "List.Second"
+1
View File
@@ -4,6 +4,7 @@ title = "TOML Marshal Testing"
bool = true
date = 1979-05-27T07:32:00Z
float = 123.4
float64 = 123.456782132399
int = 5000
string = "Bite me"
uint = 5001
+62 -13
View File
@@ -77,8 +77,10 @@ func (p *tomlParser) parseStart() tomlParserStateFn {
return p.parseAssign
case tokenEOF:
return nil
case tokenError:
p.raiseError(tok, "parsing error: %s", tok.String())
default:
p.raiseError(tok, "unexpected token")
p.raiseError(tok, "unexpected token %s", tok.typ)
}
return nil
}
@@ -165,6 +167,11 @@ func (p *tomlParser) parseAssign() tomlParserStateFn {
key := p.getToken()
p.assume(tokenEqual)
parsedKey, err := parseKey(key.val)
if err != nil {
p.raiseError(key, "invalid key: %s", err.Error())
}
value := p.parseRvalue()
var tableKey []string
if len(p.currentTable) > 0 {
@@ -173,6 +180,9 @@ func (p *tomlParser) parseAssign() tomlParserStateFn {
tableKey = []string{}
}
prefixKey := parsedKey[0 : len(parsedKey)-1]
tableKey = append(tableKey, prefixKey...)
// find the table to assign, looking out for arrays of tables
var targetNode *Tree
switch node := p.tree.GetPath(tableKey).(type) {
@@ -180,17 +190,19 @@ func (p *tomlParser) parseAssign() tomlParserStateFn {
targetNode = node[len(node)-1]
case *Tree:
targetNode = node
case nil:
// create intermediate
if err := p.tree.createSubTree(tableKey, key.Position); err != nil {
p.raiseError(key, "could not create intermediate group: %s", err)
}
targetNode = p.tree.GetPath(tableKey).(*Tree)
default:
p.raiseError(key, "Unknown table type for path: %s",
strings.Join(tableKey, "."))
}
// assign value to the found table
keyVals := []string{key.val}
if len(keyVals) != 1 {
p.raiseError(key, "Invalid key")
}
keyVal := keyVals[0]
keyVal := parsedKey[len(parsedKey)-1]
localKey := []string{keyVal}
finalKey := append(tableKey, keyVal)
if targetNode.GetPath(localKey) != nil {
@@ -301,7 +313,41 @@ func (p *tomlParser) parseRvalue() interface{} {
}
return val
case tokenDate:
val, err := time.ParseInLocation(time.RFC3339Nano, tok.val, time.UTC)
layout := time.RFC3339Nano
if !strings.Contains(tok.val, "T") {
layout = strings.Replace(layout, "T", " ", 1)
}
val, err := time.ParseInLocation(layout, tok.val, time.UTC)
if err != nil {
p.raiseError(tok, "%s", err)
}
return val
case tokenLocalDate:
v := strings.Replace(tok.val, " ", "T", -1)
isDateTime := false
isTime := false
for _, c := range v {
if c == 'T' || c == 't' {
isDateTime = true
break
}
if c == ':' {
isTime = true
break
}
}
var val interface{}
var err error
if isDateTime {
val, err = ParseLocalDateTime(v)
} else if isTime {
val, err = ParseLocalTime(v)
} else {
val, err = ParseLocalDate(v)
}
if err != nil {
p.raiseError(tok, "%s", err)
}
@@ -338,18 +384,21 @@ Loop:
case tokenRightCurlyBrace:
p.getToken()
break Loop
case tokenKey:
case tokenKey, tokenInteger, tokenString:
if !tokenIsComma(previous) && previous != nil {
p.raiseError(follow, "comma expected between fields in inline table")
}
key := p.getToken()
p.assume(tokenEqual)
value := p.parseRvalue()
tree.Set(key.val, value)
case tokenComma:
if previous == nil {
p.raiseError(follow, "inline table cannot start with a comma")
parsedKey, err := parseKey(key.val)
if err != nil {
p.raiseError(key, "invalid key: %s", err)
}
value := p.parseRvalue()
tree.SetPath(parsedKey, value)
case tokenComma:
if tokenIsComma(previous) {
p.raiseError(follow, "need field between two commas in inline table")
}
-88
View File
@@ -1,88 +0,0 @@
#!/bin/bash
# fail out of the script if anything here fails
set -e
set -o pipefail
# set the path to the present working directory
export GOPATH=`pwd`
function git_clone() {
path=$1
branch=$2
version=$3
if [ ! -d "src/$path" ]; then
mkdir -p src/$path
git clone https://$path.git src/$path
fi
pushd src/$path
git checkout "$branch"
git reset --hard "$version"
popd
}
# Remove potential previous runs
rm -rf src test_program_bin toml-test
go get github.com/pelletier/go-buffruneio
go get github.com/davecgh/go-spew/spew
go get gopkg.in/yaml.v2
go get github.com/BurntSushi/toml
# get code for BurntSushi TOML validation
# pinning all to 'HEAD' for version 0.3.x work (TODO: pin to commit hash when tests stabilize)
git_clone github.com/BurntSushi/toml master HEAD
git_clone github.com/BurntSushi/toml-test master HEAD #was: 0.2.0 HEAD
# build the BurntSushi test application
go build -o toml-test github.com/BurntSushi/toml-test
# vendorize the current lib for testing
# NOTE: this basically mocks an install without having to go back out to github for code
mkdir -p src/github.com/pelletier/go-toml/cmd
mkdir -p src/github.com/pelletier/go-toml/query
cp *.go *.toml src/github.com/pelletier/go-toml
cp -R cmd/* src/github.com/pelletier/go-toml/cmd
cp -R query/* src/github.com/pelletier/go-toml/query
go build -o test_program_bin src/github.com/pelletier/go-toml/cmd/test_program.go
# Run basic unit tests
go test github.com/pelletier/go-toml -covermode=count -coverprofile=coverage.out
go test github.com/pelletier/go-toml/cmd/tomljson
go test github.com/pelletier/go-toml/query
# run the entire BurntSushi test suite
if [[ $# -eq 0 ]] ; then
echo "Running all BurntSushi tests"
./toml-test ./test_program_bin | tee test_out
else
# run a specific test
test=$1
test_path='src/github.com/BurntSushi/toml-test/tests'
valid_test="$test_path/valid/$test"
invalid_test="$test_path/invalid/$test"
if [ -e "$valid_test.toml" ]; then
echo "Valid Test TOML for $test:"
echo "===="
cat "$valid_test.toml"
echo "Valid Test JSON for $test:"
echo "===="
cat "$valid_test.json"
echo "Go-TOML Output for $test:"
echo "===="
cat "$valid_test.toml" | ./test_program_bin
fi
if [ -e "$invalid_test.toml" ]; then
echo "Invalid Test TOML for $test:"
echo "===="
cat "$invalid_test.toml"
echo "Go-TOML Output for $test:"
echo "===="
echo "go-toml Output:"
cat "$invalid_test.toml" | ./test_program_bin
fi
fi
+3 -10
View File
@@ -2,7 +2,6 @@ package toml
import (
"fmt"
"strconv"
"unicode"
)
@@ -35,6 +34,7 @@ const (
tokenDoubleLeftBracket
tokenDoubleRightBracket
tokenDate
tokenLocalDate
tokenKeyGroup
tokenKeyGroupArray
tokenComma
@@ -68,7 +68,8 @@ var tokenTypeNames = []string{
")",
"]]",
"[[",
"Date",
"LocalDate",
"LocalDate",
"KeyGroup",
"KeyGroupArray",
",",
@@ -95,14 +96,6 @@ func (tt tokenType) String() string {
return "Unknown"
}
func (t token) Int() int {
if result, err := strconv.Atoi(t.val); err != nil {
panic(err)
} else {
return result
}
}
func (t token) String() string {
switch t.typ {
case tokenEOF:
+73 -47
View File
@@ -27,9 +27,13 @@ type Tree struct {
}
func newTree() *Tree {
return newTreeWithPosition(Position{})
}
func newTreeWithPosition(pos Position) *Tree {
return &Tree{
values: make(map[string]interface{}),
position: Position{},
position: pos,
}
}
@@ -194,10 +198,10 @@ func (t *Tree) SetWithOptions(key string, opts SetOptions, value interface{}) {
// formatting instructions to the key, that will be reused by Marshal().
func (t *Tree) SetPathWithOptions(keys []string, opts SetOptions, value interface{}) {
subtree := t
for _, intermediateKey := range keys[:len(keys)-1] {
for i, intermediateKey := range keys[:len(keys)-1] {
nextTree, exists := subtree.values[intermediateKey]
if !exists {
nextTree = newTree()
nextTree = newTreeWithPosition(Position{Line: t.position.Line + i, Col: t.position.Col})
subtree.values[intermediateKey] = nextTree // add new element here
}
switch node := nextTree.(type) {
@@ -207,7 +211,7 @@ func (t *Tree) SetPathWithOptions(keys []string, opts SetOptions, value interfac
// go to most recent element
if len(node) == 0 {
// create element if it does not exist
subtree.values[intermediateKey] = append(node, newTree())
subtree.values[intermediateKey] = append(node, newTreeWithPosition(Position{Line: t.position.Line + i, Col: t.position.Col}))
}
subtree = node[len(node)-1]
}
@@ -215,19 +219,21 @@ func (t *Tree) SetPathWithOptions(keys []string, opts SetOptions, value interfac
var toInsert interface{}
switch value.(type) {
switch v := value.(type) {
case *Tree:
tt := value.(*Tree)
tt.comment = opts.Comment
v.comment = opts.Comment
toInsert = value
case []*Tree:
toInsert = value
case *tomlValue:
tt := value.(*tomlValue)
tt.comment = opts.Comment
toInsert = tt
v.comment = opts.Comment
toInsert = v
default:
toInsert = &tomlValue{value: value, comment: opts.Comment, commented: opts.Commented, multiline: opts.Multiline}
toInsert = &tomlValue{value: value,
comment: opts.Comment,
commented: opts.Commented,
multiline: opts.Multiline,
position: Position{Line: subtree.position.Line + len(subtree.values) + 1, Col: subtree.position.Col}}
}
subtree.values[keys[len(keys)-1]] = toInsert
@@ -256,44 +262,35 @@ func (t *Tree) SetPath(keys []string, value interface{}) {
// SetPathWithComment is the same as SetPath, but allows you to provide comment
// information to the key, that will be reused by Marshal().
func (t *Tree) SetPathWithComment(keys []string, comment string, commented bool, value interface{}) {
subtree := t
for _, intermediateKey := range keys[:len(keys)-1] {
nextTree, exists := subtree.values[intermediateKey]
if !exists {
nextTree = newTree()
subtree.values[intermediateKey] = nextTree // add new element here
}
switch node := nextTree.(type) {
case *Tree:
subtree = node
case []*Tree:
// go to most recent element
if len(node) == 0 {
// create element if it does not exist
subtree.values[intermediateKey] = append(node, newTree())
}
subtree = node[len(node)-1]
}
t.SetPathWithOptions(keys, SetOptions{Comment: comment, Commented: commented}, value)
}
// Delete removes a key from the tree.
// Key is a dot-separated path (e.g. a.b.c).
func (t *Tree) Delete(key string) error {
keys, err := parseKey(key)
if err != nil {
return err
}
return t.DeletePath(keys)
}
var toInsert interface{}
switch value.(type) {
// DeletePath removes a key from the tree.
// Keys is an array of path elements (e.g. {"a","b","c"}).
func (t *Tree) DeletePath(keys []string) error {
keyLen := len(keys)
if keyLen == 1 {
delete(t.values, keys[0])
return nil
}
tree := t.GetPath(keys[:keyLen-1])
item := keys[keyLen-1]
switch node := tree.(type) {
case *Tree:
tt := value.(*Tree)
tt.comment = comment
toInsert = value
case []*Tree:
toInsert = value
case *tomlValue:
tt := value.(*tomlValue)
tt.comment = comment
toInsert = tt
default:
toInsert = &tomlValue{value: value, comment: comment, commented: commented}
delete(node.values, item)
return nil
}
subtree.values[keys[len(keys)-1]] = toInsert
return errors.New("no such key to delete")
}
// createSubTree takes a tree and a key and create the necessary intermediate
@@ -305,10 +302,10 @@ func (t *Tree) SetPathWithComment(keys []string, comment string, commented bool,
// Returns nil on success, error object on failure
func (t *Tree) createSubTree(keys []string, pos Position) error {
subtree := t
for _, intermediateKey := range keys {
for i, intermediateKey := range keys {
nextTree, exists := subtree.values[intermediateKey]
if !exists {
tree := newTree()
tree := newTreeWithPosition(Position{Line: t.position.Line + i, Col: t.position.Col})
tree.position = pos
subtree.values[intermediateKey] = tree
nextTree = tree
@@ -337,10 +334,39 @@ func LoadBytes(b []byte) (tree *Tree, err error) {
err = errors.New(r.(string))
}
}()
if len(b) >= 4 && (hasUTF32BigEndianBOM4(b) || hasUTF32LittleEndianBOM4(b)) {
b = b[4:]
} else if len(b) >= 3 && hasUTF8BOM3(b) {
b = b[3:]
} else if len(b) >= 2 && (hasUTF16BigEndianBOM2(b) || hasUTF16LittleEndianBOM2(b)) {
b = b[2:]
}
tree = parseToml(lexToml(b))
return
}
func hasUTF16BigEndianBOM2(b []byte) bool {
return b[0] == 0xFE && b[1] == 0xFF
}
func hasUTF16LittleEndianBOM2(b []byte) bool {
return b[0] == 0xFF && b[1] == 0xFE
}
func hasUTF8BOM3(b []byte) bool {
return b[0] == 0xEF && b[1] == 0xBB && b[2] == 0xBF
}
func hasUTF32BigEndianBOM4(b []byte) bool {
return b[0] == 0x00 && b[1] == 0x00 && b[2] == 0xFE && b[3] == 0xFF
}
func hasUTF32LittleEndianBOM4(b []byte) bool {
return b[0] == 0xFF && b[1] == 0xFE && b[2] == 0x00 && b[3] == 0x00
}
// LoadReader creates a Tree from any io.Reader.
func LoadReader(reader io.Reader) (tree *Tree, err error) {
inputBytes, err := ioutil.ReadAll(reader)
+226 -84
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"io"
"math"
"math/big"
"reflect"
"sort"
"strconv"
@@ -12,6 +13,18 @@ import (
"time"
)
type valueComplexity int
const (
valueSimple valueComplexity = iota + 1
valueComplex
)
type sortNode struct {
key string
complexity valueComplexity
}
// Encodes a string to a TOML-compliant multi-line string value
// This function is a clone of the existing encodeTomlString function, except that whitespace characters
// are preserved. Quotation marks and backslashes are also not escaped.
@@ -94,12 +107,20 @@ func tomlValueStringRepresentation(v interface{}, indent string, arraysOneElemen
case int64:
return strconv.FormatInt(value, 10), nil
case float64:
// Ensure a round float does contain a decimal point. Otherwise feeding
// the output back to the parser would convert to an integer.
if math.Trunc(value) == value {
return strings.ToLower(strconv.FormatFloat(value, 'f', 1, 32)), nil
// Default bit length is full 64
bits := 64
// Float panics if nan is used
if !math.IsNaN(value) {
// if 32 bit accuracy is enough to exactly show, use 32
_, acc := big.NewFloat(value).Float32()
if acc == big.Exact {
bits = 32
}
}
return strings.ToLower(strconv.FormatFloat(value, 'f', -1, 32)), nil
if math.Trunc(value) == value {
return strings.ToLower(strconv.FormatFloat(value, 'f', 1, bits)), nil
}
return strings.ToLower(strconv.FormatFloat(value, 'f', -1, bits)), nil
case string:
if tv.multiline {
return "\"\"\"\n" + encodeMultilineTomlString(value) + "\"\"\"", nil
@@ -115,6 +136,12 @@ func tomlValueStringRepresentation(v interface{}, indent string, arraysOneElemen
return "false", nil
case time.Time:
return value.Format(time.RFC3339), nil
case LocalDate:
return value.String(), nil
case LocalDateTime:
return value.String(), nil
case LocalTime:
return value.String(), nil
case nil:
return "", nil
}
@@ -153,117 +180,233 @@ func tomlValueStringRepresentation(v interface{}, indent string, arraysOneElemen
return "", fmt.Errorf("unsupported value type %T: %v", v, v)
}
func (t *Tree) writeTo(w io.Writer, indent, keyspace string, bytesCount int64, arraysOneElementPerLine bool) (int64, error) {
simpleValuesKeys := make([]string, 0)
complexValuesKeys := make([]string, 0)
func getTreeArrayLine(trees []*Tree) (line int) {
// get lowest line number that is not 0
for _, tv := range trees {
if tv.position.Line < line || line == 0 {
line = tv.position.Line
}
}
return
}
func sortByLines(t *Tree) (vals []sortNode) {
var (
line int
lines []int
tv *Tree
tom *tomlValue
node sortNode
)
vals = make([]sortNode, 0)
m := make(map[int]sortNode)
for k := range t.values {
v := t.values[k]
switch v.(type) {
case *Tree:
tv = v.(*Tree)
line = tv.position.Line
node = sortNode{key: k, complexity: valueComplex}
case []*Tree:
line = getTreeArrayLine(v.([]*Tree))
node = sortNode{key: k, complexity: valueComplex}
default:
tom = v.(*tomlValue)
line = tom.position.Line
node = sortNode{key: k, complexity: valueSimple}
}
lines = append(lines, line)
vals = append(vals, node)
m[line] = node
}
sort.Ints(lines)
for i, line := range lines {
vals[i] = m[line]
}
return vals
}
func sortAlphabetical(t *Tree) (vals []sortNode) {
var (
node sortNode
simpVals []string
compVals []string
)
vals = make([]sortNode, 0)
m := make(map[string]sortNode)
for k := range t.values {
v := t.values[k]
switch v.(type) {
case *Tree, []*Tree:
complexValuesKeys = append(complexValuesKeys, k)
node = sortNode{key: k, complexity: valueComplex}
compVals = append(compVals, node.key)
default:
simpleValuesKeys = append(simpleValuesKeys, k)
node = sortNode{key: k, complexity: valueSimple}
simpVals = append(simpVals, node.key)
}
vals = append(vals, node)
m[node.key] = node
}
sort.Strings(simpleValuesKeys)
sort.Strings(complexValuesKeys)
for _, k := range simpleValuesKeys {
v, ok := t.values[k].(*tomlValue)
if !ok {
return bytesCount, fmt.Errorf("invalid value type at %s: %T", k, t.values[k])
}
repr, err := tomlValueStringRepresentation(v, indent, arraysOneElementPerLine)
if err != nil {
return bytesCount, err
}
if v.comment != "" {
comment := strings.Replace(v.comment, "\n", "\n"+indent+"#", -1)
start := "# "
if strings.HasPrefix(comment, "#") {
start = ""
}
writtenBytesCountComment, errc := writeStrings(w, "\n", indent, start, comment, "\n")
bytesCount += int64(writtenBytesCountComment)
if errc != nil {
return bytesCount, errc
}
}
var commented string
if v.commented {
commented = "# "
}
writtenBytesCount, err := writeStrings(w, indent, commented, k, " = ", repr, "\n")
bytesCount += int64(writtenBytesCount)
if err != nil {
return bytesCount, err
}
// Simples first to match previous implementation
sort.Strings(simpVals)
i := 0
for _, key := range simpVals {
vals[i] = m[key]
i++
}
for _, k := range complexValuesKeys {
v := t.values[k]
sort.Strings(compVals)
for _, key := range compVals {
vals[i] = m[key]
i++
}
combinedKey := k
if keyspace != "" {
combinedKey = keyspace + "." + combinedKey
}
var commented string
if t.commented {
commented = "# "
}
return vals
}
switch node := v.(type) {
// node has to be of those two types given how keys are sorted above
case *Tree:
tv, ok := t.values[k].(*Tree)
func (t *Tree) writeTo(w io.Writer, indent, keyspace string, bytesCount int64, arraysOneElementPerLine bool) (int64, error) {
return t.writeToOrdered(w, indent, keyspace, bytesCount, arraysOneElementPerLine, OrderAlphabetical)
}
func (t *Tree) writeToOrdered(w io.Writer, indent, keyspace string, bytesCount int64, arraysOneElementPerLine bool, ord marshalOrder) (int64, error) {
var orderedVals []sortNode
switch ord {
case OrderPreserve:
orderedVals = sortByLines(t)
default:
orderedVals = sortAlphabetical(t)
}
for _, node := range orderedVals {
switch node.complexity {
case valueComplex:
k := node.key
v := t.values[k]
combinedKey := k
if keyspace != "" {
combinedKey = keyspace + "." + combinedKey
}
var commented string
if t.commented {
commented = "# "
}
switch node := v.(type) {
// node has to be of those two types given how keys are sorted above
case *Tree:
tv, ok := t.values[k].(*Tree)
if !ok {
return bytesCount, fmt.Errorf("invalid value type at %s: %T", k, t.values[k])
}
if tv.comment != "" {
comment := strings.Replace(tv.comment, "\n", "\n"+indent+"#", -1)
start := "# "
if strings.HasPrefix(comment, "#") {
start = ""
}
writtenBytesCountComment, errc := writeStrings(w, "\n", indent, start, comment)
bytesCount += int64(writtenBytesCountComment)
if errc != nil {
return bytesCount, errc
}
}
writtenBytesCount, err := writeStrings(w, "\n", indent, commented, "[", combinedKey, "]\n")
bytesCount += int64(writtenBytesCount)
if err != nil {
return bytesCount, err
}
bytesCount, err = node.writeToOrdered(w, indent+" ", combinedKey, bytesCount, arraysOneElementPerLine, ord)
if err != nil {
return bytesCount, err
}
case []*Tree:
for _, subTree := range node {
writtenBytesCount, err := writeStrings(w, "\n", indent, commented, "[[", combinedKey, "]]\n")
bytesCount += int64(writtenBytesCount)
if err != nil {
return bytesCount, err
}
bytesCount, err = subTree.writeToOrdered(w, indent+" ", combinedKey, bytesCount, arraysOneElementPerLine, ord)
if err != nil {
return bytesCount, err
}
}
}
default: // Simple
k := node.key
v, ok := t.values[k].(*tomlValue)
if !ok {
return bytesCount, fmt.Errorf("invalid value type at %s: %T", k, t.values[k])
}
if tv.comment != "" {
comment := strings.Replace(tv.comment, "\n", "\n"+indent+"#", -1)
repr, err := tomlValueStringRepresentation(v, indent, arraysOneElementPerLine)
if err != nil {
return bytesCount, err
}
if v.comment != "" {
comment := strings.Replace(v.comment, "\n", "\n"+indent+"#", -1)
start := "# "
if strings.HasPrefix(comment, "#") {
start = ""
}
writtenBytesCountComment, errc := writeStrings(w, "\n", indent, start, comment)
writtenBytesCountComment, errc := writeStrings(w, "\n", indent, start, comment, "\n")
bytesCount += int64(writtenBytesCountComment)
if errc != nil {
return bytesCount, errc
}
}
writtenBytesCount, err := writeStrings(w, "\n", indent, commented, "[", combinedKey, "]\n")
var commented string
if v.commented {
commented = "# "
}
quotedKey := quoteKeyIfNeeded(k)
writtenBytesCount, err := writeStrings(w, indent, commented, quotedKey, " = ", repr, "\n")
bytesCount += int64(writtenBytesCount)
if err != nil {
return bytesCount, err
}
bytesCount, err = node.writeTo(w, indent+" ", combinedKey, bytesCount, arraysOneElementPerLine)
if err != nil {
return bytesCount, err
}
case []*Tree:
for _, subTree := range node {
writtenBytesCount, err := writeStrings(w, "\n", indent, commented, "[[", combinedKey, "]]\n")
bytesCount += int64(writtenBytesCount)
if err != nil {
return bytesCount, err
}
bytesCount, err = subTree.writeTo(w, indent+" ", combinedKey, bytesCount, arraysOneElementPerLine)
if err != nil {
return bytesCount, err
}
}
}
}
return bytesCount, nil
}
// quote a key if it does not fit the bare key format (A-Za-z0-9_-)
// quoted keys use the same rules as strings
func quoteKeyIfNeeded(k string) string {
// when encoding a map with the 'quoteMapKeys' option enabled, the tree will contain
// keys that have already been quoted.
// not an ideal situation, but good enough of a stop gap.
if len(k) >= 2 && k[0] == '"' && k[len(k)-1] == '"' {
return k
}
isBare := true
for _, r := range k {
if !isValidBareChar(r) {
isBare = false
break
}
}
if isBare {
return k
}
return quoteKey(k)
}
func quoteKey(k string) string {
return "\"" + encodeTomlString(k) + "\""
}
func writeStrings(w io.Writer, s ...string) (int, error) {
var n int
for i := range s {
@@ -286,12 +429,11 @@ func (t *Tree) WriteTo(w io.Writer) (int64, error) {
// Output spans multiple lines, and is suitable for ingest by a TOML parser.
// If the conversion cannot be performed, ToString returns a non-nil error.
func (t *Tree) ToTomlString() (string, error) {
var buf bytes.Buffer
_, err := t.WriteTo(&buf)
b, err := t.Marshal()
if err != nil {
return "", err
}
return buf.String(), nil
return string(b), nil
}
// String generates a human-readable representation of the current tree.
+3 -2
View File
@@ -267,7 +267,7 @@ func (u *CopyOnWriteFs) Mkdir(name string, perm os.FileMode) error {
return u.layer.MkdirAll(name, perm)
}
if dir {
return syscall.EEXIST
return ErrFileExists
}
return u.layer.MkdirAll(name, perm)
}
@@ -282,7 +282,8 @@ func (u *CopyOnWriteFs) MkdirAll(name string, perm os.FileMode) error {
return u.layer.MkdirAll(name, perm)
}
if dir {
return syscall.EEXIST
// This is in line with how os.MkdirAll behaves.
return nil
}
return u.layer.MkdirAll(name, perm)
}
+2
View File
@@ -1 +1,3 @@
module github.com/spf13/afero
require golang.org/x/text v0.3.0
+2
View File
@@ -0,0 +1,2 @@
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+17 -2
View File
@@ -155,7 +155,8 @@ var defaultUnionMergeDirsFn = func(lofi, bofi []os.FileInfo) ([]os.FileInfo, err
}
// Readdir will weave the two directories together and
// return a single view of the overlayed directories
// return a single view of the overlayed directories.
// At the end of the directory view, the error is io.EOF if c > 0.
func (f *UnionFile) Readdir(c int) (ofi []os.FileInfo, err error) {
var merge DirsMerger = f.Merger
if merge == nil {
@@ -185,9 +186,23 @@ func (f *UnionFile) Readdir(c int) (ofi []os.FileInfo, err error) {
}
f.files = append(f.files, merged...)
}
if c == -1 {
if c <= 0 && len(f.files) == 0 {
return f.files, nil
}
if f.off >= len(f.files) {
return nil, io.EOF
}
if c <= 0 {
return f.files[f.off:], nil
}
if c > len(f.files) {
c = len(f.files)
}
defer func() { f.off += c }()
return f.files[f.off:c], nil
}
+2
View File
@@ -20,3 +20,5 @@ _cgo_export.*
_testmain.go
*.exe
*.bench
go.sum

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