Compare commits

...
182 Commits
Author SHA1 Message Date
Jesse Duffield e6a6301144 bump version to v0.1.71 2018-08-19 19:47:04 +10:00
Jesse Duffield c5d4024d58 compare error message rather than error itself on no-commits error 2018-08-19 18:51:42 +10:00
Jesse Duffield 81b07daa01 Merge branch 'hotfix/167-ambiguous-name-in-diff' 2018-08-19 15:06:06 +10:00
Jesse Duffield 93266cc2a4 support forked branches when getting project root 2018-08-19 15:05:36 +10:00
Jesse Duffield 60fc24eada Merge pull request #172 from jesseduffield/hotfix/167-ambiguous-name-in-diff
167: Support File names that match Branch names
2018-08-19 14:53:14 +10:00
Jesse Duffield 6978785ccf add user email and config to test repo generators 2018-08-19 14:52:08 +10:00
Jesse Duffield cd9eada0c6 add test for variety of potential git diff situations 2018-08-19 14:48:39 +10:00
Jesse Duffield 7b3679d546 Merge pull request #181 from glvr182/feature/ignore-unused
Cleaned the gitignore and added the linux binary
2018-08-19 13:26:35 +10:00
Jesse Duffield c8e63894b4 Merge pull request #182 from glvr182/feature/docs-images
Feature/docs images
2018-08-19 13:25:23 +10:00
Jesse Duffield af93d04479 Merge pull request #183 from mjarkk/master
Removed logrus logger import from i18n
2018-08-19 13:23:29 +10:00
Mark Kopenga 29f2bdbba3 removed logrus logger import 2018-08-18 14:03:20 +02:00
Mark Kopenga c90e865e34 Merge pull request #10 from jesseduffield/master
Updated to latest master
2018-08-18 14:00:41 +02:00
Jesse Duffield b104dccab2 Merge pull request #163 from dawidd6/translation/pl
Add Polish translation
2018-08-18 21:08:56 +10:00
Glenn Vriesman 2dbd5be24e Removed duplicates 2018-08-18 13:04:44 +02:00
Glenn Vriesman 0a4cf6a544 Docs: Fixed typo 2018-08-18 12:56:15 +02:00
Glenn Vriesman 5f5e275a0e Docs: Made commit-diffs example a local file 2018-08-18 12:55:13 +02:00
Glenn Vriesman 7b85d146af Docs: Made resolving merge conflicts a local file 2018-08-18 12:53:26 +02:00
Glenn Vriesman fd01cdb137 Docs: Made the example gif a local file 2018-08-18 12:51:15 +02:00
Glenn Vriesman 9e8dc37308 Docs: Moved slack image to docs/resources 2018-08-18 12:46:50 +02:00
Jesse Duffield 08666889f4 improve remove file logic 2018-08-18 20:14:44 +10:00
Glenn Vriesman c091401571 Cleaned the gitignore and added the linux binary 2018-08-18 11:49:37 +02:00
Jesse Duffield 0174375562 Merge pull request #180 from jesseduffield/hotfix/174-nil-pointer-reference-on-startup
Hotfix for issue 174: avoid nil pointer reference on startup
2018-08-18 19:45:28 +10:00
Jesse Duffield 1f756d3d0a avoid nil pointer reference on startup 2018-08-18 19:43:58 +10:00
Jesse Duffield 6473e5ca3c use runtime package to get GOOS and GOARCH 2018-08-18 17:28:03 +10:00
Jesse Duffield 896dda3adf bump version to v0.1.70 2018-08-18 17:17:22 +10:00
Jesse Duffield b9ef1a4d67 leave bump commits out of release notes 2018-08-18 17:17:05 +10:00
Jesse Duffield 671c693459 bump version to v0.1.67 2018-08-18 17:12:44 +10:00
Jesse Duffield 41171304b2 add platform/os details to goreleaser ldflags 2018-08-18 17:10:27 +10:00
Jesse Duffield f025b289f0 enforce white text color in prompt panels 2018-08-18 16:23:19 +10:00
Jesse Duffield 3f5c1a4243 bump version to v0.1.66 2018-08-18 16:00:30 +10:00
Jesse Duffield db7e623c0c bump version to v0.1.65 2018-08-18 16:00:12 +10:00
Jesse Duffield ad8f02b986 add 386 architecture to releases 2018-08-18 16:00:00 +10:00
Jesse Duffield 29431ddc8e create dashboard 2018-08-18 15:30:56 +10:00
Jesse Duffield a1a828a781 support opening and editing config file 2018-08-18 14:54:05 +10:00
Jesse Duffield 6b150a4be0 bump dependencies 2018-08-18 14:20:19 +10:00
Jesse Duffield 284c534251 user configurable border colors 2018-08-18 13:53:58 +10:00
Jesse Duffield 10fdb5a609 support writing back to user config 2018-08-18 13:22:05 +10:00
Jesse Duffield 4dc6d40b5a merge master 2018-08-18 12:08:21 +10:00
Jesse Duffield 99d40c2f8e run codecov report after other commands on circle ci 2018-08-18 11:17:31 +10:00
Mark Kopenga 0f145093fe Here i a fix for the error go complain on your pull 2018-08-17 20:43:51 +02:00
Jesse Duffield bd91b9e1e9 add test repo for all the kinds of files that show up when diffing 2018-08-17 22:46:10 +10:00
Dawid Dziurla bf5f3bb972 pl translation 3 2018-08-17 14:27:13 +02:00
Jesse Duffield 03a7e32694 support filenames that match branchnames 2018-08-17 22:25:53 +10:00
Dawid Dziurla b0e08491ac pl translation 2 2018-08-17 14:11:48 +02:00
Jesse Duffield aaa8558de8 Merge pull request #159 from remyabel/158_escape_backticks
#158: escapes backticks, which is a problem in shells like Bash
2018-08-17 21:11:20 +10:00
Mark Kopenga 633553bcf8 Merge pull request #7 from jesseduffield/master
Update to latest master
2018-08-17 10:33:10 +02:00
Jesse Duffield 933aae7da1 bump version to v0.1.64 2018-08-17 11:15:41 +10:00
Jesse Duffield 3b1689727a bump version to v0.1.63 2018-08-17 11:15:19 +10:00
Jesse Duffield c08e6d9999 bump version to v0.1.62 2018-08-17 11:14:54 +10:00
Jesse Duffield dcd3bb6bbd use platform independent command to remove a file or directory 2018-08-17 11:13:21 +10:00
Jesse Duffield bd15f9d27a Merge pull request #164 from dawidd6/ubuntu/release-ppa
Add info about release PPA
2018-08-17 09:05:31 +10:00
Tommy Nguyen 52033b32f7 Use strings.Replace instead of regexp 2018-08-16 17:04:39 -04:00
Mark Kopenga 5c738fa493 Removed duplicates 2018-08-16 20:59:32 +02:00
Mark Kopenga 9a0f29ff5b Merge pull request #5 from jesseduffield/master
Updated to latest master
2018-08-16 20:08:14 +02:00
Dawid Dziurla 36eed80228 add info about release PPA 2018-08-16 19:26:43 +02:00
Dawid Dziurla 2d80a83d27 pl translation 1 2018-08-16 18:56:16 +02:00
Jesse Duffield f4e701611b Merge pull request #162 from ponsfrilus/patch-3
Typo path → patch
2018-08-16 23:40:37 +10:00
Nicolas Borboën ff15d86ced Typo path → patch 2018-08-16 14:15:55 +02:00
Jesse Duffield 8b57b21d79 add circle ci config 2018-08-16 22:07:02 +10:00
Jesse Duffield 563efc41c7 bump dependencies for i18n 2018-08-16 21:48:09 +10:00
Jesse Duffield 090537a1f1 Merge pull request #137 from mjarkk/master
Added a view basic translation functions and translation file
2018-08-16 21:46:42 +10:00
Mark Kopenga fcf616bd62 Fixed it 2018-08-16 13:35:04 +02:00
Tommy Nguyen a7755ab184 reformat 2018-08-16 07:00:13 -04:00
Mark Kopenga faf218f465 Fixed comments from jesseduffield on issue #137 2018-08-16 11:31:50 +02:00
Mark Kopenga 90746502df Fixed comments from jesseduffield on issue #137 2018-08-16 11:31:03 +02:00
Jesse Duffield df7d1df4cb bump dependencies 2018-08-16 19:20:59 +10:00
Jesse Duffield 5819e04c53 use shibukawa/configdir package to follow xdg spec config directory structure 2018-08-16 19:17:38 +10:00
Jesse Duffield fd3ce21576 use yaml for config file rather than json 2018-08-16 18:43:46 +10:00
Mark Kopenga 88e1a815fe Fixed comment on issue #137 from @jesseduffield 2018-08-16 07:16:32 +02:00
Tommy Nguyen db94dde114 fix formatting 2018-08-15 23:58:44 -04:00
Tommy Nguyen ee4660af97 #158: escapes backticks, which is a problem in shells like Bash 2018-08-15 23:55:55 -04:00
Jesse Duffield 59ab38fff6 Update README.md 2018-08-16 10:37:23 +10:00
Jesse Duffield adc906bb87 add slack image file 2018-08-16 10:32:52 +10:00
Mark Kopenga 9abbfe5a43 Fully translated pkg/gui/confirmation_panel.go 2018-08-15 15:12:55 +02:00
Mark Kopenga 9112278ab7 Merge pull request #4 from jesseduffield/master
Update to latest master
2018-08-15 15:03:27 +02:00
Mark Kopenga d00c46a712 Added all english translations to a file and fixed some typos 2018-08-15 14:57:20 +02:00
Jesse Duffield 29ed971558 add user configuration in json file 2018-08-15 22:06:37 +10:00
Jesse Duffield 8d99b400fd factor out code for new popup panels 2018-08-15 21:49:38 +10:00
Jesse Duffield 7c33c02930 dont panic if unable to close confirmation prompt 2018-08-15 21:43:31 +10:00
Mark Kopenga 50b41bfccc Translated pkg/gui/view_helpers.go 2018-08-15 11:49:43 +02:00
Mark Kopenga 295093a432 Translated pkg/gui/stash_panel.go 2018-08-15 11:12:46 +02:00
Mark Kopenga 10c53162ca Merge pull request #3 from jesseduffield/master
Updated to latest master
2018-08-15 10:54:43 +02:00
Mark Kopenga 7e926cf41d Added translation for pkg/gui/ confirmation_panel.go gui.go merge_panel.go 2018-08-15 10:53:05 +02:00
Mark Kopenga d12cc5a74e Fully translated pkg/gui/commits_panel.go 2018-08-15 10:30:29 +02:00
Mark Kopenga 8418fa17a5 Fully translated pkg/gui/commit_message_panel.go 2018-08-15 09:15:31 +02:00
Jesse Duffield 905e6c16ba add credential helper to config of test repo 2018-08-15 15:15:21 +10:00
Jesse Duffield db140842f3 bump version to v0.1.61 2018-08-15 09:56:15 +10:00
Jesse Duffield 7002ee5d9b bump version to v0.1.60 2018-08-15 09:52:23 +10:00
Jesse Duffield 0687c07f50 more robust reading of reflog to get branch name 2018-08-15 09:45:24 +10:00
Jesse Duffield 922f4ed5a4 try rev-parse of git symbolic-ref returns nothing 2018-08-15 09:42:02 +10:00
Mark Kopenga 3dba246029 Added translations for files_panel.go and fixed some typos 2018-08-14 22:29:17 +02:00
Mark Kopenga be3f5846e4 Added more translations 2018-08-14 21:06:50 +02:00
Mark Kopenga 38a1a00cf1 Fixed comment from myself on issue: 137 2018-08-14 16:38:25 +02:00
Mark Kopenga 9c97b75aad Merge remote-tracking branch 'origin/master' 2018-08-14 16:14:11 +02:00
Mark Kopenga 883f436b0f can't go any further because of an error 2018-08-14 16:12:21 +02:00
Jesse Duffield d923796cff Merge branch 'master' of https://github.com/mjarkk/lazygit 2018-08-14 23:48:18 +10:00
Jesse Duffield ba2b6fbf1f pull errors out of package scope and store sentinel errors on the gui struct 2018-08-14 23:47:14 +10:00
Mark Kopenga 73a1682540 fixed package naming and added tr object to file_panel.go 2018-08-14 15:26:25 +02:00
Mark Kopenga 8e22d569a0 Merge pull request #2 from jesseduffield/master
Updated to latest master
2018-08-14 15:10:59 +02:00
Jesse Duffield 4d0702fba5 Merge branch 'master' of https://github.com/mjarkk/lazygit 2018-08-14 22:12:11 +10:00
Jesse Duffield 5cbacb0c67 make local i18n package confirm to project structure 2018-08-14 22:12:07 +10:00
Mark Kopenga 0568b32f0b Added more translations 2018-08-14 13:31:23 +02:00
Mark Kopenga 6e518142b4 added some commands 2018-08-14 12:56:11 +02:00
Mark Kopenga 0c39347224 Added auto detection for the system language 2018-08-14 12:52:26 +02:00
Mark Kopenga dd7e93ac8d Added all the missing translations from dutch.go 2018-08-14 11:35:39 +02:00
Mark Kopenga f792f74f0f Merge remote-tracking branch 'origin/master' 2018-08-14 11:28:11 +02:00
Mark Kopenga 5ad97add08 Added the translation to some words again 2018-08-14 11:27:46 +02:00
Jesse Duffield 652237d48f bump version to v0.1.59 2018-08-14 19:23:05 +10:00
Jesse Duffield 6a3f8eefa5 send version number through app 2018-08-14 19:22:38 +10:00
Mark Kopenga 17dcfbcc6f Merge pull request #1 from jesseduffield/master
Update to latest master
2018-08-14 11:06:34 +02:00
Mark Kopenga dfafb98871 tried to update to latest master 2018-08-14 11:05:26 +02:00
Jesse Duffield 8cc8c4c228 bump version to v0.1.58 2018-08-14 18:49:15 +10:00
Jesse Duffield 574b34930c support files with spaces in name 2018-08-14 18:48:08 +10:00
Jesse Duffield 77080f44a4 bump version to v0.1.57 2018-08-14 18:38:04 +10:00
Jesse Duffield 97daa5eeb2 bump version to v0.1.56 2018-08-14 18:36:51 +10:00
Jesse Duffield c476dfc1cb fix open command 2018-08-14 18:35:47 +10:00
Jesse Duffield bde6182c94 better git squash logic 2018-08-14 18:30:06 +10:00
Jesse Duffield efb049cd24 better error handling for stashing files 2018-08-14 18:29:25 +10:00
Jesse Duffield 88af0fb1b6 add gpg repo generator 2018-08-14 18:12:09 +10:00
Jesse Duffield 5f30f07ea5 Merge branch 'master' into feature/quoted-messages 2018-08-14 18:10:44 +10:00
Jesse Duffield c02f474bf3 update dependencies 2018-08-14 18:05:18 +10:00
Jesse Duffield b32173b0c7 output to development.log in append mode 2018-08-14 18:02:27 +10:00
Jesse Duffield 1c750fdb40 fix reflog command 2018-08-14 18:02:14 +10:00
Jesse Duffield 9ecd7908aa refactor commands to depend less on the shell 2018-08-14 17:47:33 +10:00
Jesse Duffield ad3d332e05 Merge pull request #143 from jesseduffield/feature/clean_up
Platform should only be present once
2018-08-14 12:33:47 +10:00
Andrei Miulescu 842ceec9b0 Platform should only be present once 2018-08-14 12:24:32 +10:00
Jesse Duffield 95c7df4c61 use platform-specific message quoting 2018-08-14 11:16:52 +10:00
Jesse Duffield c0a1f90604 Merge pull request #132 from jesseduffield/feature/project_restructure
Project Restructure Episode 1
2018-08-14 09:02:38 +10:00
Jesse Duffield 15528dc341 Merge branch 'master' into feature/project_restructure 2018-08-14 08:44:39 +10:00
Jesse Duffield 7ecbd7fbb3 update pre-commit hook 2018-08-14 08:42:08 +10:00
Jesse Duffield 45f640941c update dependencies 2018-08-14 08:34:31 +10:00
Jesse Duffield 047892962a centralise subprocess code to gui.go 2018-08-14 08:33:40 +10:00
Jesse Duffield d4f4b46a1f check both local and global config for gpgsign 2018-08-14 08:33:27 +10:00
Jesse Duffield f549ad0f37 use git command with message in subprocess if using gpgsign 2018-08-14 07:27:59 +10:00
Mark Kopenga f2dfcb6e12 Added more messages and text issue: #137 2018-08-13 22:00:44 +02:00
Mark Kopenga 65eb3780a0 fixed typo 2018-08-13 20:48:49 +02:00
Mark Kopenga d9959eb998 fixed @jesseduffield comment #137 2018-08-13 20:46:53 +02:00
Jesse Duffield ebfed34145 add PR #135 keybindings to this branch 2018-08-13 23:46:08 +10:00
Jesse Duffield 450f269f02 Merge pull request #135 from jon-grangien/feature/ctrl-keybindings
Add C-u and C-d keybindings for scrolling up and down diff
2018-08-13 23:43:54 +10:00
Jesse Duffield 95d2b02664 update gitignore 2018-08-13 23:36:06 +10:00
Jesse Duffield 59e5545f38 discard log output when not in debug mode 2018-08-13 23:35:01 +10:00
mjarkk 62fc831407 Added a view basic translation functions + tranlation file 2018-08-13 15:33:45 +02:00
Jesse Duffield fb0004481b correct cursor before returning item position 2018-08-13 21:42:56 +10:00
Jesse Duffield 3bd0246e4d add test repo contents to gitignore 2018-08-13 21:42:31 +10:00
Jesse Duffield 12de0345e4 minor cleanup 2018-08-13 21:35:54 +10:00
Jesse Duffield 9e725ae24e got this bad boy compiling again 2018-08-13 21:16:21 +10:00
Jesse Duffield 97cff65612 progress on refactor 2018-08-13 20:26:02 +10:00
codingInSpace a32fc34a49 Add C-u and C-d keybindings for scrolling up and down diff 2018-08-13 04:09:32 -04:00
Jesse Duffield f9c39ad64b add newline to version output 2018-08-13 17:20:03 +10:00
Jesse Duffield 1571e6b962 add newline to version output 2018-08-13 12:27:26 +10:00
Jesse Duffield 84b27d402d Update issue templates 2018-08-12 23:46:38 +10:00
Jesse Duffield e6beb5d50b no more go-git for committing (reflecting the change in master) 2018-08-12 23:29:58 +10:00
Jesse Duffield 1354885db0 bump version to v0.1.55 2018-08-12 23:20:10 +10:00
Jesse Duffield 360bfa4642 bump version to v0.1.54 2018-08-12 23:17:47 +10:00
Jesse Duffield bb0e580a27 bump version to v0.1.53 2018-08-12 23:17:25 +10:00
Jesse Duffield 1eb0061357 bump dependencies 2018-08-12 23:16:58 +10:00
Jesse Duffield 4cd2d70659 revert to using cli git command rather than go-git for committing 2018-08-12 23:14:37 +10:00
Jesse Duffield 35d695fd0c update gitignore to reflect new test repo directory structure 2018-08-12 23:00:51 +10:00
Andrei Miulescu e8eb78617c Mid refactor change some more stuff 2018-08-12 21:04:47 +10:00
Andrei Miulescu e65ddd7b6f Move some commands around 2018-08-12 20:22:20 +10:00
Jesse Duffield c01bc09442 WIP refactor 2018-08-12 19:50:55 +10:00
Andrei Miulescu dcd461d29f Restrucure project in a way where it is more modular 2018-08-12 19:31:27 +10:00
Jesse Duffield 98c22a36fd rearrange test repo generators 2018-08-12 15:54:59 +10:00
Jesse Duffield 7bfd8155c2 bump version to v0.1.52 2018-08-12 12:01:50 +10:00
Jesse Duffield ed1518fd66 fix CPU issues caused by resizing popup panels 2018-08-12 12:00:30 +10:00
Jesse Duffield 400301e8fa bump gocui fork to support getting view dimensions 2018-08-12 11:39:37 +10:00
Jesse Duffield f97a098c6f Merge branch 'master' of https://github.com/jesseduffield/lazygit 2018-08-12 11:05:24 +10:00
Jesse Duffield 2b3bdc8b8f rename merge conflict test repo 2018-08-12 11:05:21 +10:00
Jesse Duffield 7323e08fdb Merge pull request #127 from ponsfrilus/master
Proposal for a slightly better README.md
2018-08-12 00:51:31 +10:00
Jesse Duffield 838ed4f170 Merge pull request #104 from maxice8/add-void-readme
README.md: Add Void Linux instructions
2018-08-12 00:43:43 +10:00
Jesse Duffield feba6b5318 Merge pull request #129 from ponsfrilus/patch-2
Use <kbd> tags
2018-08-12 00:42:50 +10:00
Nicolas Borboën 27234c1cab Use <kbd> tags
and that close #128
2018-08-11 12:13:14 +02:00
Jesse Duffield 8f6329c402 if you are using gpgsign, must use git editor to commit 2018-08-11 19:28:32 +10:00
Nicolas Borboën 0ac56f5ade Proposal for a slighty bettre README.md
* Text wrapped to 80 chars;
* Kind of table of content;
* Launchpad PPA link added;
* AUR package on a bullet list;
* Missing ponctuation added;
* Youtube tutorial and keybindings on a bullet list.
2018-08-11 11:11:55 +02:00
Jesse Duffield 471a7de418 bump version to v0.1.51 2018-08-11 17:08:20 +10:00
Jesse Duffield 77191ea67c show output line by line in deploy script 2018-08-11 17:07:56 +10:00
Jesse Duffield 176aa62fe0 bump version to v0.1.50 2018-08-11 17:06:50 +10:00
Jesse Duffield 1419be1aab support commit message via git editor using shift+C keybinding 2018-08-11 16:55:39 +10:00
Jesse Duffield 0ac306fe2a fix whitespace 2018-08-11 16:55:15 +10:00
Jesse Duffield f0840c0f46 fallback to vi when trying to edit file and no EDITOR/VISUAL is defined 2018-08-11 16:55:05 +10:00
Jesse Duffield aec125df70 Merge branch 'master' into add-void-readme 2018-08-11 15:50:42 +10:00
maxice8 bf9a53fada README.md: Add Void Linux instructions 2018-08-09 19:39:18 -03:00
283 changed files with 56503 additions and 2870 deletions
+26
View File
@@ -0,0 +1,26 @@
# Golang CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-go/ for more details
version: 2
jobs:
build:
docker:
# specify the version
- image: circleci/golang:1.9
# Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images
# documented at https://circleci.com/docs/2.0/circleci-images/
# - image: circleci/postgres:9.4
#### TEMPLATE_NOTE: go expects specific checkout path representing url
#### expecting it in the form of
#### /go/src/github.com/circleci/go-tool
#### /go/src/bitbucket.org/circleci/go-tool
working_directory: /go/src/github.com/jesseduffield/lazygit
steps:
- checkout
# specify any bash command here prefixed with `run: `
- run: go test -v ./...
- run: bash <(curl -s https://codecov.io/bash)
+28
View File
@@ -0,0 +1,28 @@
---
name: Bug report
about: Create a report to help us improve
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. Windows]
- Lazygit Version [e.g. v0.1.45]
**Additional context**
Add any other context about the problem here.
+17
View File
@@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+14 -3
View File
@@ -1,7 +1,18 @@
development.log
commands.log
# Please do not add personal files
# Logs
*.log
# Extras
extra/lgit.rb
# Notes
notes/go.notes
TODO.notes
TODO.md
test/testrepo/
# Tests
test/repos/repo
# Binaries
lazygit
+23 -19
View File
@@ -1,23 +1,27 @@
# This is an example goreleaser.yaml file with some sane defaults.
# Make sure to check the documentation at http://goreleaser.com
builds:
- env:
- CGO_ENABLED=0
goos:
- freebsd
- windows
- darwin
- linux
goarch:
- amd64
- arm
- arm64
- env:
- CGO_ENABLED=0
goos:
- freebsd
- windows
- darwin
- linux
goarch:
- amd64
- arm
- arm64
- 386
ldflags:
- -s -w -X main.version={{.Version}} -X main.tag={{.Tag}} -X main.date={{.Date}} -X main.commit={{.Commit}} -X main.goos={{.Os}} -X main.arch={{.Arch}} -X main.arm={{.Arm}}
archive:
replacements:
darwin: Darwin
linux: Linux
windows: Windows
386: i386
386: 32-bit
amd64: x86_64
format_overrides:
- goos: windows
@@ -25,13 +29,14 @@ archive:
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ .Tag }}-next"
name_template: '{{ .Tag }}-next'
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
- '^docs:'
- '^test:'
- '^bump'
brew:
# Reporitory to push the tap to.
github:
@@ -40,20 +45,19 @@ brew:
# Your app's homepage.
# Default is empty.
homepage: "https://github.com/jesseduffield/lazygit/"
homepage: 'https://github.com/jesseduffield/lazygit/'
# Your app's description.
# Default is empty.
description: "A simple terminal UI for git commands, written in Go"
description: 'A simple terminal UI for git commands, written in Go'
# # Packages your package depends on.
# dependencies:
# - git
# - zsh
# # Packages that conflict with your package.
# conflicts:
# - svn
# - bash
# test comment to see if goreleaser only releases on new commits
# test comment to see if goreleaser only releases on new commits
Generated
+163 -8
View File
@@ -1,6 +1,22 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
digest = "1:b2339e83ce9b5c4f79405f949429a7f68a9a904fed903c672aac1e7ceb7f5f02"
name = "github.com/Sirupsen/logrus"
packages = ["."]
pruneopts = "NUT"
revision = "3e01752db0189b9157070a0e1668a620f9a85da2"
version = "v1.0.6"
[[projects]]
branch = "master"
digest = "1:cd7ba2b29e93e2a8384e813dfc80ebb0f85d9214762e6ca89bb55a58092eab87"
name = "github.com/cloudfoundry/jibber_jabber"
packages = ["."]
pruneopts = "NUT"
revision = "bcc4c8345a21301bf47c032ff42dd1aae2fe3027"
[[projects]]
digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39"
name = "github.com/davecgh/go-spew"
@@ -32,6 +48,14 @@
revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4"
version = "v1.7.0"
[[projects]]
digest = "1:1b91ae0dc69a41d4c2ed23ea5cffb721ea63f5037ca4b81e6d6771fbb8f45129"
name = "github.com/fsnotify/fsnotify"
packages = ["."]
pruneopts = "NUT"
revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9"
version = "v1.4.7"
[[projects]]
branch = "master"
digest = "1:4a8ed9b8cf22bd03bee5d74179fa06a282e4a73b6de949f7a865ff56cd2537e0"
@@ -40,6 +64,25 @@
pruneopts = "NUT"
revision = "604e922904d35e97f98a774db7881f049cd8d970"
[[projects]]
branch = "master"
digest = "1:11c6c696067d3127ecf332b10f89394d386d9083f82baf71f40f2da31841a009"
name = "github.com/hashicorp/hcl"
packages = [
".",
"hcl/ast",
"hcl/parser",
"hcl/printer",
"hcl/scanner",
"hcl/strconv",
"hcl/token",
"json/parser",
"json/scanner",
"json/token",
]
pruneopts = "NUT"
revision = "ef8a98b0bbce4a65b5aa4c368430a80ddc533168"
[[projects]]
branch = "master"
digest = "1:62fe3a7ea2050ecbd753a71889026f83d73329337ada66325cbafd5dea5f713d"
@@ -50,11 +93,11 @@
[[projects]]
branch = "master"
digest = "1:82b3bbc50ba7b6065b0229ebf0fc990a76e47410acd510294e435aeb3523293e"
digest = "1:c9a848b0484a72da2dae28957b4f67501fe27fa38bc73f4713e454353c0a4a60"
name = "github.com/jesseduffield/gocui"
packages = ["."]
pruneopts = "NUT"
revision = "3c923f53ac9952af649af04a067405843558d56f"
revision = "432b7f6215f81ef1aaa1b2d9b69887822923cf79"
[[projects]]
digest = "1:8021af4dcbd531ae89433c8c3a6520e51064114aaf8eb1724c3cf911c497c9ba"
@@ -64,6 +107,14 @@
revision = "9fc7bb800b555d63157c65a904c86a2cc7b4e795"
version = "0.4"
[[projects]]
digest = "1:d244f8666a838fe6ad70ec8fe77f50ebc29fdc3331a2729ba5886bef8435d10d"
name = "github.com/magiconair/properties"
packages = ["."]
pruneopts = "NUT"
revision = "c2353362d570a7bfa228149c62842019201cfb71"
version = "v1.8.0"
[[projects]]
digest = "1:08c231ec84231a7e23d67e4b58f975e1423695a32467a362ee55a803f9de8061"
name = "github.com/mattn/go-colorable"
@@ -88,6 +139,14 @@
revision = "9e777a8366cce605130a531d2cd6363d07ad7317"
version = "v0.0.2"
[[projects]]
digest = "1:a25c9a6b41e100f4ce164db80260f2b687095ba9d8b46a1d6072d3686cc020db"
name = "github.com/mgutz/str"
packages = ["."]
pruneopts = "NUT"
revision = "968bf66e3da857419e4f6e71b2d5c9ae95682dc4"
version = "v1.2.0"
[[projects]]
branch = "master"
digest = "1:a4df73029d2c42fabcb6b41e327d2f87e685284ec03edf76921c267d9cfc9c23"
@@ -96,6 +155,26 @@
pruneopts = "NUT"
revision = "58046073cbffe2f25d425fe1331102f55cf719de"
[[projects]]
branch = "master"
digest = "1:5fe20cfe4ef484c237cec9f947b2a6fa90bad4b8610fd014f0e4211e13d82d5d"
name = "github.com/mitchellh/mapstructure"
packages = ["."]
pruneopts = "NUT"
revision = "f15292f7a699fcc1a38a80977f80a046874ba8ac"
[[projects]]
digest = "1:2c34c77bf3ec848da26e48af58fc511ed52750961fa848399d122882b8890928"
name = "github.com/nicksnyder/go-i18n"
packages = [
"v2/i18n",
"v2/internal",
"v2/internal/plural",
]
pruneopts = "NUT"
revision = "a16b91a3ba80db3a2301c70d1d302d42251c9079"
version = "v2.0.0-beta.5"
[[projects]]
branch = "master"
digest = "1:34d9354c2c5d916c05864327553047df59fc10e86ff1f408e4136eba0a25a5ec"
@@ -112,6 +191,14 @@
revision = "c37440a7cf42ac63b919c752ca73a85067e05992"
version = "v0.2.0"
[[projects]]
digest = "1:51ea800cff51752ff68e12e04106f5887b4daec6f9356721238c28019f0b42db"
name = "github.com/pelletier/go-toml"
packages = ["."]
pruneopts = "NUT"
revision = "c01d1270ff3e442a8a57cddc1c92dc1138598194"
version = "v1.2.0"
[[projects]]
digest = "1:d917313f309bda80d27274d53985bc65651f81a5b66b820749ac7f8ef061fd04"
name = "github.com/sergi/go-diff"
@@ -120,6 +207,57 @@
revision = "1744e2970ca51c86172c8190fadad617561ed6e7"
version = "v1.0.0"
[[projects]]
branch = "master"
digest = "1:41618aee8828e62dfe62d44f579c06892d0e98907d1c6d5bcd83bfe8536ec5a3"
name = "github.com/shibukawa/configdir"
packages = ["."]
pruneopts = "NUT"
revision = "e180dbdc8da04c4fa04272e875ce64949f38bd3e"
[[projects]]
digest = "1:330e9062b308ac597e28485699c02223bd052437a6eed32a173c9227dcb9d95a"
name = "github.com/spf13/afero"
packages = [
".",
"mem",
]
pruneopts = "NUT"
revision = "787d034dfe70e44075ccc060d346146ef53270ad"
version = "v1.1.1"
[[projects]]
digest = "1:3fa7947ca83b98ae553590d993886e845a4bff19b7b007e869c6e0dd3b9da9cd"
name = "github.com/spf13/cast"
packages = ["."]
pruneopts = "NUT"
revision = "8965335b8c7107321228e3e3702cab9832751bac"
version = "v1.2.0"
[[projects]]
branch = "master"
digest = "1:f29f83301ed096daed24a90f4af591b7560cb14b9cc3e1827abbf04db7269ab5"
name = "github.com/spf13/jwalterweatherman"
packages = ["."]
pruneopts = "NUT"
revision = "14d3d4c518341bea657dd8a226f5121c0ff8c9f2"
[[projects]]
digest = "1:e3707aeaccd2adc89eba6c062fec72116fe1fc1ba71097da85b4d8ae1668a675"
name = "github.com/spf13/pflag"
packages = ["."]
pruneopts = "NUT"
revision = "9a97c102cda95a86cec2345a6f09f55a939babf5"
version = "v1.0.2"
[[projects]]
digest = "1:454979540e2a1582f375a17c106cf4e11e3bcac4baffb4af23e515c87f87de13"
name = "github.com/spf13/viper"
packages = ["."]
pruneopts = "NUT"
revision = "907c19d40d9a6c9bb55f040ff4ae45271a4754b9"
version = "v1.1.0"
[[projects]]
digest = "1:ccca1dcd18bc54e23b517a3c5babeff2e3924a7d8fc1932162225876cfe4bfb0"
name = "github.com/src-d/gcfg"
@@ -151,7 +289,7 @@
[[projects]]
branch = "master"
digest = "1:c76f8b24a4d9b99b502fb7b61ad769125075cb570efff9b9b73e6c428629532d"
digest = "1:dfcb1b2db354cafa48fc3cdafe4905a08bec4a9757919ab07155db0ca23855b4"
name = "golang.org/x/crypto"
packages = [
"cast5",
@@ -170,6 +308,7 @@
"ssh",
"ssh/agent",
"ssh/knownhosts",
"ssh/terminal",
]
pruneopts = "NUT"
revision = "de0752318171da717af4ce24d0a2e8626afaeb11"
@@ -180,26 +319,28 @@
name = "golang.org/x/net"
packages = ["context"]
pruneopts = "NUT"
revision = "f9ce57c11b242f0f1599cf25c89d8cb02c45295a"
revision = "c39426892332e1bb5ec0a434a079bf82f5d30c54"
[[projects]]
branch = "master"
digest = "1:87aef4c266c820591e859450c3ee5ab98f9b8755d7cfd9e4db606959eb5b483d"
digest = "1:ec76a40fbfda0c329ee58f4e3b14b4279a939efce89eca020e934e2e5234eddd"
name = "golang.org/x/sys"
packages = [
"unix",
"windows",
]
pruneopts = "NUT"
revision = "904bdc257025c7b3f43c19360ad3ab85783fad78"
revision = "98c5dad5d1a0e8a73845ecc8897d0bd56586511d"
[[projects]]
digest = "1:8029e9743749d4be5bc9f7d42ea1659471767860f0cdc34d37c3111bd308a295"
digest = "1:a95288ef1ef4dfad6cba7fe30843e1683f71bc28c912ca1ba3f6a539d44db739"
name = "golang.org/x/text"
packages = [
"internal/gen",
"internal/tag",
"internal/triegen",
"internal/ucd",
"language",
"transform",
"unicode/cldr",
"unicode/norm",
@@ -278,18 +419,32 @@
revision = "ec4a0fea49c7b46c2aeb0b51aac55779c607e52b"
version = "v0.1.2"
[[projects]]
digest = "1:7c95b35057a0ff2e19f707173cc1a947fa43a6eb5c4d300d196ece0334046082"
name = "gopkg.in/yaml.v2"
packages = ["."]
pruneopts = "NUT"
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
version = "v2.2.1"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = [
"github.com/Sirupsen/logrus",
"github.com/cloudfoundry/jibber_jabber",
"github.com/davecgh/go-spew/spew",
"github.com/fatih/color",
"github.com/golang-collections/collections/stack",
"github.com/jesseduffield/gocui",
"github.com/mgutz/str",
"github.com/nicksnyder/go-i18n/v2/i18n",
"github.com/shibukawa/configdir",
"github.com/spf13/viper",
"github.com/tcnksm/go-gitconfig",
"golang.org/x/text/language",
"gopkg.in/src-d/go-git.v4",
"gopkg.in/src-d/go-git.v4/plumbing",
"gopkg.in/src-d/go-git.v4/plumbing/object",
]
solver-name = "gps-cdcl"
solver-version = 1
+67 -29
View File
@@ -2,13 +2,20 @@
A simple terminal UI for git commands, written in Go with the [gocui](https://github.com/jroimartin/gocui "gocui") library.
Are YOU tired of typing every git command directly into the terminal, but you're too stubborn to use Sourcetree because you'll never forgive Atlassian for making Jira? This is the app for you!
Are YOU tired of typing every git command directly into the terminal, but you're
too stubborn to use Sourcetree because you'll never forgive Atlassian for making
Jira? This is the app for you!
[Tutorial](https://www.youtube.com/watch?v=VDXvbHZYeKY)
![Gif](https://image.ibb.co/mmeXho/optimisedgif.gif)
![Gif](/docs/resources/lazygit-example.gif)
[Twitch Stream](https://www.twitch.tv/jesseduffield)
* [Installation](https://github.com/jesseduffield/lazygit#installation)
* [Usage](https://github.com/jesseduffield/lazygit#usage),
[Keybindings](https://github.com/jesseduffield/lazygit/blob/master/docs/Keybindings.md)
* [Cool Features](https://github.com/jesseduffield/lazygit#cool-features)
* [Contributing](https://github.com/jesseduffield/lazygit#contributing)
* [Video Tutorial](https://www.youtube.com/watch?v=VDXvbHZYeKY)
* [Twitch Stream](https://www.twitch.tv/jesseduffield)
## Installation
@@ -19,9 +26,21 @@ brew install lazygit
```
### Ubuntu
Packages for Ubuntu 16.04, 18.04 and 18.10 are available via Launchpad PPA.
Packages for Ubuntu 16.04, 18.04 and 18.10 are available via [Launchpad PPA](https://launchpad.net/~lazygit-team).
They are built daily, straight from master branch.
**Release builds**
Built from git tags. Supposed to be more stable.
```sh
sudo add-apt-repository ppa:lazygit-team/release
sudo apt-get update
sudo apt-get install lazygit
```
**Daily builds**
Built from master branch once in 24 hours (or more sometimes).
```sh
sudo add-apt-repository ppa:lazygit-team/daily
@@ -29,22 +48,29 @@ sudo apt-get update
sudo apt-get install lazygit
```
### Void Linux
Packages for Void Linux are available in the distro repo
They follow upstream latest releases
```sh
sudo xbps-install -S lazygit
```
### Arch Linux
Packages for Arch Linux are available via AUR (Arch User Repository).
There are two packages. The stable one which is built with the latest release and the git version which builds from the most recent commit.
There are two packages. The stable one which is built with the latest release
and the git version which builds from the most recent commit.
Stable:
https://aur.archlinux.org/packages/lazygit/
Development:
https://aur.archlinux.org/packages/lazygit-git/
* Stable: https://aur.archlinux.org/packages/lazygit/
* Development: https://aur.archlinux.org/packages/lazygit-git/
Instruction of how to install AUR content can be found here:
https://wiki.archlinux.org/index.php/Arch_User_Repository
### Binary Release (Windows/Linux/OSX)
You can download a binary release [here](https://github.com/jesseduffield/lazygit/releases)
You can download a binary release [here](https://github.com/jesseduffield/lazygit/releases).
### Go
```sh
@@ -52,29 +78,33 @@ go get github.com/jesseduffield/lazygit
```
Please note:
If you get an error claiming that lazygit cannot be found or is not defined, you may need to add `~/go/bin` to your $PATH (MacOS/Linux), or `%HOME%\go\bin` (Windows). Not to be mistaked for `C:\Go\bin` (which is for Go's own binaries, not apps like Lazygit)
If you get an error claiming that lazygit cannot be found or is not defined, you
may need to add `~/go/bin` to your $PATH (MacOS/Linux), or `%HOME%\go\bin`
(Windows). Not to be mistaked for `C:\Go\bin` (which is for Go's own binaries,
not apps like Lazygit).
## Usage
Call `lazygit` in your terminal inside a git repository.
If you want, you can also add an alias for this with `echo "alias lg='lazygit'" >> ~/.zshrc` (or whichever rc file you're using).
Basic tutorial [Here](https://www.youtube.com/watch?v=VDXvbHZYeKY)
Call `lazygit` in your terminal inside a git repository. If you want, you can
also add an alias for this with `echo "alias lg='lazygit'" >> ~/.zshrc` (or
whichever rc file you're using).
[Keybindings](https://github.com/jesseduffield/lazygit/blob/master/docs/Keybindings.md)
* Basic video tutorial [here](https://www.youtube.com/watch?v=VDXvbHZYeKY).
* List of keybindings
[here](/docs/Keybindings.md).
## Cool features
- Adding files easily
- Resolving merge conflicts
- Easily check out recent branches
- Scroll through logs/diffs of branches/commits/stash
- Quick pushing/pulling
- Squash down and rename commits
* Adding files easily
* Resolving merge conflicts
* Easily check out recent branches
* Scroll through logs/diffs of branches/commits/stash
* Quick pushing/pulling
* Squash down and rename commits
### Resolving merge conflicts
![Gif](https://image.ibb.co/iyxUTT/shortermerging.gif)
![Gif](/docs/resources/resolving-merge-conflicts.gif)
### Viewing commit diffs
![Viewing Commit Diffs](https://image.ibb.co/gPD02o/capture.png)
![Viewing Commit Diffs](/docs/resources/viewing-commit-diffs.png)
## Milestones
- [x] Easy Installation (homebrew, release binaries)
@@ -87,9 +117,17 @@ Basic tutorial [Here](https://www.youtube.com/watch?v=VDXvbHZYeKY)
## Contributing
We love your input! Please check out the [contributing guide](CONTRIBUTING.md).
For contributor discussion about things not better discussed here in the repo, join the slack channel
[![Slack](/docs/resources/slack_rgb.png)](https://join.slack.com/t/lazygit/shared_invite/enQtNDE3MjIwNTYyMDA0LTM3Yjk3NzdiYzhhNTA1YjM4Y2M4MWNmNDBkOTI0YTE4YjQ1ZmI2YWRhZTgwNjg2YzhhYjg3NDBlMmQyMTI5N2M)
## Work in progress
This is still a work in progress so there's still bugs to iron out and as this is my first project in Go the code could no doubt use an increase in quality, but I'll be improving on it whenever I find the time. If you have any feedback feel free to [raise an issue](https://github.com/jesseduffield/lazygit/issues)/[submit a PR](https://github.com/jesseduffield/lazygit/pulls).
This is still a work in progress so there's still bugs to iron out and as this
is my first project in Go the code could no doubt use an increase in quality,
but I'll be improving on it whenever I find the time. If you have any feedback
feel free to [raise an issue](https://github.com/jesseduffield/lazygit/issues)/[submit a PR](https://github.com/jesseduffield/lazygit/pulls).
## Social
If you want to see what I (Jesse) am up to in terms of development, follow me on [twitter](https://twitter.com/DuffieldJesse) or watch me program on [twitch](https://www.twitch.tv/jesseduffield)
If you want to see what I (Jesse) am up to in terms of development, follow me on
[twitter](https://twitter.com/DuffieldJesse) or watch me program on
[twitch](https://www.twitch.tv/jesseduffield).
+1 -1
View File
@@ -1 +1 @@
v0.1.49
v0.1.71
-135
View File
@@ -1,135 +0,0 @@
package main
import (
"fmt"
"strings"
"github.com/jesseduffield/gocui"
)
func handleBranchPress(g *gocui.Gui, v *gocui.View) error {
index := getItemPosition(v)
if index == 0 {
return createErrorPanel(g, "You have already checked out this branch")
}
branch := getSelectedBranch(v)
if output, err := gitCheckout(branch.Name, false); err != nil {
createErrorPanel(g, output)
}
return refreshSidePanels(g)
}
func handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
branch := getSelectedBranch(v)
return createConfirmationPanel(g, v, "Force Checkout Branch", "Are you sure you want force checkout? You will lose all local changes", func(g *gocui.Gui, v *gocui.View) error {
if output, err := gitCheckout(branch.Name, true); err != nil {
createErrorPanel(g, output)
}
return refreshSidePanels(g)
}, nil)
}
func handleCheckoutByName(g *gocui.Gui, v *gocui.View) error {
createPromptPanel(g, v, "Branch Name:", func(g *gocui.Gui, v *gocui.View) error {
if output, err := gitCheckout(trimmedContent(v), false); err != nil {
return createErrorPanel(g, output)
}
return refreshSidePanels(g)
})
return nil
}
func handleNewBranch(g *gocui.Gui, v *gocui.View) error {
branch := state.Branches[0]
createPromptPanel(g, v, "New Branch Name (Branch is off of "+branch.Name+")", func(g *gocui.Gui, v *gocui.View) error {
if output, err := gitNewBranch(trimmedContent(v)); err != nil {
return createErrorPanel(g, output)
}
refreshSidePanels(g)
return handleBranchSelect(g, v)
})
return nil
}
func handleDeleteBranch(g *gocui.Gui, v *gocui.View) error {
checkedOutBranch := state.Branches[0]
selectedBranch := getSelectedBranch(v)
if checkedOutBranch.Name == selectedBranch.Name {
return createErrorPanel(g, "You cannot delete the checked out branch!")
}
return createConfirmationPanel(g, v, "Delete Branch", "Are you sure you want delete the branch "+selectedBranch.Name+" ?", func(g *gocui.Gui, v *gocui.View) error {
if output, err := gitDeleteBranch(selectedBranch.Name); err != nil {
return createErrorPanel(g, output)
}
return refreshSidePanels(g)
}, nil)
}
func handleMerge(g *gocui.Gui, v *gocui.View) error {
checkedOutBranch := state.Branches[0]
selectedBranch := getSelectedBranch(v)
defer refreshSidePanels(g)
if checkedOutBranch.Name == selectedBranch.Name {
return createErrorPanel(g, "You cannot merge a branch into itself")
}
if output, err := gitMerge(selectedBranch.Name); err != nil {
return createErrorPanel(g, output)
}
return nil
}
func getSelectedBranch(v *gocui.View) Branch {
lineNumber := getItemPosition(v)
return state.Branches[lineNumber]
}
func renderBranchesOptions(g *gocui.Gui) error {
return renderOptionsMap(g, map[string]string{
"space": "checkout",
"f": "force checkout",
"m": "merge",
"c": "checkout by name",
"n": "new branch",
"d": "delete branch",
"← → ↑ ↓": "navigate",
})
}
// may want to standardise how these select methods work
func handleBranchSelect(g *gocui.Gui, v *gocui.View) error {
if err := renderBranchesOptions(g); err != nil {
return err
}
// This really shouldn't happen: there should always be a master branch
if len(state.Branches) == 0 {
return renderString(g, "main", "No branches for this repo")
}
go func() {
branch := getSelectedBranch(v)
diff, err := getBranchGraph(branch.Name)
if err != nil && strings.HasPrefix(diff, "fatal: ambiguous argument") {
diff = "There is no tracking for this branch"
}
renderString(g, "main", diff)
}()
return nil
}
// refreshStatus is called at the end of this because that's when we can
// be sure there is a state.Branches array to pick the current branch from
func refreshBranches(g *gocui.Gui) error {
g.Update(func(g *gocui.Gui) error {
v, err := g.View("branches")
if err != nil {
panic(err)
}
state.Branches = getGitBranches()
v.Clear()
for _, branch := range state.Branches {
fmt.Fprintln(v, branch.getDisplayString())
}
resetOrigin(v)
return refreshStatus(g)
})
return nil
}
-45
View File
@@ -1,45 +0,0 @@
package main
import "github.com/jesseduffield/gocui"
func handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
message := trimmedContent(v)
if message == "" {
return createErrorPanel(g, "You cannot commit without a commit message")
}
if output, err := gitCommit(g, message); err != nil {
if err == errNoUsername {
return createErrorPanel(g, err.Error())
}
return createErrorPanel(g, output)
}
refreshFiles(g)
v.Clear()
v.SetCursor(0, 0)
g.SetViewOnBottom("commitMessage")
switchFocus(g, v, getFilesView(g))
return refreshCommits(g)
}
func handleCommitClose(g *gocui.Gui, v *gocui.View) error {
g.SetViewOnBottom("commitMessage")
return switchFocus(g, v, getFilesView(g))
}
func handleNewlineCommitMessage(g *gocui.Gui, v *gocui.View) error {
// resising ahead of time so that the top line doesn't get hidden to make
// room for the cursor on the second line
x0, y0, x1, y1 := getConfirmationPanelDimensions(g, v.Buffer())
if _, err := g.SetView("commitMessage", x0, y0, x1, y1+1, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
}
v.EditNewLine()
return nil
}
func handleCommitFocused(g *gocui.Gui, v *gocui.View) error {
return renderString(g, "options", "esc: close, enter: confirm")
}
-176
View File
@@ -1,176 +0,0 @@
package main
import (
"errors"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
)
var (
// ErrNoCommits : When no commits are found for the branch
ErrNoCommits = errors.New("No commits for this branch")
)
func refreshCommits(g *gocui.Gui) error {
g.Update(func(*gocui.Gui) error {
state.Commits = getCommits()
v, err := g.View("commits")
if err != nil {
panic(err)
}
v.Clear()
red := color.New(color.FgRed)
yellow := color.New(color.FgYellow)
white := color.New(color.FgWhite)
shaColor := white
for _, commit := range state.Commits {
if commit.Pushed {
shaColor = red
} else {
shaColor = yellow
}
shaColor.Fprint(v, commit.Sha+" ")
white.Fprintln(v, commit.Name)
}
refreshStatus(g)
if g.CurrentView().Name() == "commits" {
handleCommitSelect(g, v)
}
return nil
})
return nil
}
func handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error {
return createConfirmationPanel(g, commitView, "Reset To Commit", "Are you sure you want to reset to this commit?", func(g *gocui.Gui, v *gocui.View) error {
commit, err := getSelectedCommit(g)
if err != nil {
panic(err)
}
if output, err := gitResetToCommit(commit.Sha); err != nil {
return createErrorPanel(g, output)
}
if err := refreshCommits(g); err != nil {
panic(err)
}
if err := refreshFiles(g); err != nil {
panic(err)
}
resetOrigin(commitView)
return handleCommitSelect(g, nil)
}, nil)
}
func renderCommitsOptions(g *gocui.Gui) error {
return renderOptionsMap(g, map[string]string{
"s": "squash down",
"r": "rename",
"g": "reset to this commit",
"f": "fixup commit",
"← → ↑ ↓": "navigate",
})
}
func handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
if err := renderCommitsOptions(g); err != nil {
return err
}
commit, err := getSelectedCommit(g)
if err != nil {
if err != ErrNoCommits {
return err
}
return renderString(g, "main", "No commits for this branch")
}
commitText := gitShow(commit.Sha)
return renderString(g, "main", commitText)
}
func handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
if getItemPosition(v) != 0 {
return createErrorPanel(g, "Can only squash topmost commit")
}
if len(state.Commits) == 1 {
return createErrorPanel(g, "You have no commits to squash with")
}
commit, err := getSelectedCommit(g)
if err != nil {
return err
}
if output, err := gitSquashPreviousTwoCommits(commit.Name); err != nil {
return createErrorPanel(g, output)
}
if err := refreshCommits(g); err != nil {
panic(err)
}
refreshStatus(g)
return handleCommitSelect(g, v)
}
// TODO: move to files panel
func anyUnStagedChanges(files []GitFile) bool {
for _, file := range files {
if file.Tracked && file.HasUnstagedChanges {
return true
}
}
return false
}
func handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
if len(state.Commits) == 1 {
return createErrorPanel(g, "You have no commits to squash with")
}
objectLog(state.GitFiles)
if anyUnStagedChanges(state.GitFiles) {
return createErrorPanel(g, "Can't fixup while there are unstaged changes")
}
branch := state.Branches[0]
commit, err := getSelectedCommit(g)
if err != nil {
return err
}
createConfirmationPanel(g, v, "Fixup", "Are you sure you want to fixup this commit? The commit beneath will be squashed up into this one", func(g *gocui.Gui, v *gocui.View) error {
if output, err := gitSquashFixupCommit(branch.Name, commit.Sha); err != nil {
return createErrorPanel(g, output)
}
if err := refreshCommits(g); err != nil {
panic(err)
}
return refreshStatus(g)
}, nil)
return nil
}
func handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
if getItemPosition(v) != 0 {
return createErrorPanel(g, "Can only rename topmost commit")
}
createPromptPanel(g, v, "Rename Commit", func(g *gocui.Gui, v *gocui.View) error {
if output, err := gitRenameCommit(v.Buffer()); err != nil {
return createErrorPanel(g, output)
}
if err := refreshCommits(g); err != nil {
panic(err)
}
return handleCommitSelect(g, v)
})
return nil
}
func getSelectedCommit(g *gocui.Gui) (Commit, error) {
v, err := g.View("commits")
if err != nil {
panic(err)
}
if len(state.Commits) == 0 {
return Commit{}, ErrNoCommits
}
lineNumber := getItemPosition(v)
if lineNumber > len(state.Commits)-1 {
devLog("potential error in getSelected Commit (mismatched ui and state)", state.Commits, lineNumber)
return state.Commits[len(state.Commits)-1], nil
}
return state.Commits[lineNumber], nil
}
-159
View File
@@ -1,159 +0,0 @@
// lots of this has been directly ported from one of the example files, will brush up later
// Copyright 2014 The gocui Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
)
func wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.View) error) func(*gocui.Gui, *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
if function != nil {
if err := function(g, v); err != nil {
panic(err)
}
}
return closeConfirmationPrompt(g)
}
}
func closeConfirmationPrompt(g *gocui.Gui) error {
view, err := g.View("confirmation")
if err != nil {
panic(err)
}
if err := returnFocus(g, view); err != nil {
panic(err)
}
g.DeleteKeybindings("confirmation")
return g.DeleteView("confirmation")
}
func getMessageHeight(message string, width int) int {
lines := strings.Split(message, "\n")
lineCount := 0
for _, line := range lines {
lineCount += len(line)/width + 1
}
return lineCount
}
func getConfirmationPanelDimensions(g *gocui.Gui, prompt string) (int, int, int, int) {
width, height := g.Size()
panelWidth := width / 2
panelHeight := getMessageHeight(prompt, panelWidth)
return width/2 - panelWidth/2,
height/2 - panelHeight/2 - panelHeight%2 - 1,
width/2 + panelWidth/2,
height/2 + panelHeight/2
}
func createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, handleConfirm func(*gocui.Gui, *gocui.View) error) error {
g.SetViewOnBottom("commitMessage")
// only need to fit one line
x0, y0, x1, y1 := getConfirmationPanelDimensions(g, "")
if confirmationView, err := g.SetView("confirmation", x0, y0, x1, y1, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
confirmationView.Editable = true
confirmationView.Title = title
switchFocus(g, currentView, confirmationView)
return setKeyBindings(g, handleConfirm, nil)
}
return nil
}
func createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
g.SetViewOnBottom("commitMessage")
g.Update(func(g *gocui.Gui) error {
// delete the existing confirmation panel if it exists
if view, _ := g.View("confirmation"); view != nil {
if err := closeConfirmationPrompt(g); err != nil {
panic(err)
}
}
x0, y0, x1, y1 := getConfirmationPanelDimensions(g, prompt)
if confirmationView, err := g.SetView("confirmation", x0, y0, x1, y1, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
confirmationView.Title = title
confirmationView.FgColor = gocui.ColorWhite
renderString(g, "confirmation", prompt)
switchFocus(g, currentView, confirmationView)
return setKeyBindings(g, handleConfirm, handleClose)
}
return nil
})
return nil
}
func handleNewline(g *gocui.Gui, v *gocui.View) error {
// resising ahead of time so that the top line doesn't get hidden to make
// room for the cursor on the second line
x0, y0, x1, y1 := getConfirmationPanelDimensions(g, v.Buffer())
if _, err := g.SetView("confirmation", x0, y0, x1, y1+1, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
}
v.EditNewLine()
return nil
}
func setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
renderString(g, "options", "esc: close, enter: confirm")
if err := g.SetKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone, wrappedConfirmationFunction(handleConfirm)); err != nil {
return err
}
if err := g.SetKeybinding("confirmation", gocui.KeyTab, gocui.ModNone, handleNewline); err != nil {
return err
}
return g.SetKeybinding("confirmation", gocui.KeyEsc, gocui.ModNone, wrappedConfirmationFunction(handleClose))
}
func createMessagePanel(g *gocui.Gui, currentView *gocui.View, title, prompt string) error {
return createConfirmationPanel(g, currentView, title, prompt, nil, nil)
}
func createErrorPanel(g *gocui.Gui, message string) error {
currentView := g.CurrentView()
colorFunction := color.New(color.FgRed).SprintFunc()
coloredMessage := colorFunction(strings.TrimSpace(message))
return createConfirmationPanel(g, currentView, "Error", coloredMessage, nil, nil)
}
func trimTrailingNewline(str string) string {
if strings.HasSuffix(str, "\n") {
return str[:len(str)-1]
}
return str
}
func resizeConfirmationPanel(g *gocui.Gui, viewName string) error {
// If the confirmation panel is already displayed, just resize the width,
// otherwise continue
g.Update(func(g *gocui.Gui) error {
v, err := g.View(viewName)
if err != nil {
return nil
}
content := trimTrailingNewline(v.Buffer())
x0, y0, x1, y1 := getConfirmationPanelDimensions(g, content)
if _, err := g.SetView(viewName, x0, y0, x1, y1, 0); err != nil {
return err
}
return nil
})
return nil
}
+39
View File
@@ -0,0 +1,39 @@
# User Config:
## Default:
```
gui:
# stuff relating to the UI
scrollHeight: 2 # how many lines you scroll by
theme:
activeBorderColor:
- white
- bold
inactiveBorderColor:
- white
optionsTextColor:
- blue
git:
# stuff relating to git
os:
# stuff relating to the OS
```
## Color Attributes:
For color attributes you can choose an array of attributes (with max one color attribute)
The available attributes are:
- default
- black
- red
- green
- yellow
- blue
- magenta
- cyan
- white
- bold
- reverse # useful for high-contrast
- underline
+59 -35
View File
@@ -2,56 +2,80 @@
## Global:
← → ↑ ↓/h j k l: navigate
PgUp/PgDn: scroll diff panel (use fn+up/down on osx)
q: quit
p: pull
shift+P: push
<pre>
<kbd>←</kbd><kbd>→</kbd><kbd>↑</kbd><kbd>↓</kbd>/<kbd>h</kbd><kbd>j</kbd><kbd>k</kbd><kbd>l</kbd>: navigate
<kbd>PgUp</kbd>/<kbd>PgDn</kbd> or <kbd>ctrl</kbd>+<kbd>u</kbd>/<kbd>ctrl</kbd>+<kbd>d</kbd>: scroll diff panel
(for <kbd>PgUp</kbd> and <kbd>PgDn</kbd>, use <kbd>fn</kbd>+<kbd>up</kbd>/<kbd>fn</kbd>+<kbd>down</kbd> on osx)
<kbd>q</kbd>: quit
<kbd>p</kbd>: pull
<kbd>shift</kbd>+<kbd>P</kbd>: push
</pre>
## Status Panel:
<pre>
<kbd>e</kbd>: edit config file
<kbd>o</kbd>: open config file
</pre>
## Files Panel:
space: toggle staged
c: commit changes
shift+S: stash files
t: add patched (i.e. pick chunks of a file to add)
o: open
e: edit
s: open in sublime (requires 'subl' command)
v: open in vscode (requires 'code' command)
i: add to .gitignore
d: delete if untracked checkout if tracked (aka go away)
shift+R: refresh files
<pre>
<kbd>space</kbd>: toggle staged
<kbd>c</kbd>: commit changes
<kbd>shift</kbd>+<kbd>C</kbd>: commit using git editor
<kbd>shift</kbd>+<kbd>S</kbd>: stash files
<kbd>t</kbd>: add patched (i.e. pick chunks of a file to add)
<kbd>o</kbd>: open
<kbd>e</kbd>: edit
<kbd>s</kbd>: open in sublime (requires 'subl' command)
<kbd>v</kbd>: open in vscode (requires 'code' command)
<kbd>i</kbd>: add to .gitignore
<kbd>d</kbd>: delete if untracked checkout if tracked (aka go away)
<kbd>shift</kbd>+<kbd>R</kbd>: refresh files
</pre>
## Branches Panel:
space: checkout branch
f: force checkout branch
m: merge into currently checked out branch
c: checkout by name
n: new branch
d: delete branch
<pre>
<kbd>space</kbd>: checkout branch
<kbd>f</kbd>: force checkout branch
<kbd>m</kbd>: merge into currently checked out branch
<kbd>c</kbd>: checkout by name
<kbd>n</kbd>: new branch
<kbd>d</kbd>: delete branch
</pre>
## Commits Panel:
s: squash down (only available for topmost commit)
r: rename commit
g: reset to this commit
<pre>
<kbd>s</kbd>: squash down (only available for topmost commit)
<kbd>r</kbd>: rename commit
<kbd>g</kbd>: reset to this commit
</pre>
## Stash Panel:
space: apply
g: pop
d: drop
<pre>
<kbd>space</kbd>: apply
<kbd>g</kbd>: pop
<kbd>d</kbd>: drop
</pre>
## Popup Panel:
esc: close/cancel
enter: confirm
<pre>
<kbd>esc</kbd>: close/cancel
<kbd>enter</kbd>: confirm
<kbd>tab</kbd>: enter newline (if editing)
</pre>
## Resolving Merge Conflicts (Diff Panel):
← →/h l: navigate conflicts
↑ ↓/ k j: select hunk
space: pick hunk
b: pick both hunks
z: undo (only available while still inside diff panel)
<pre>
<kbd>←</kbd><kbd>→</kbd>/<kbd>h</kbd><kbd>l</kbd>: navigate conflicts
<kbd>↑</kbd><kbd>↓</kbd>/<kbd>k</kbd><kbd>j</kbd>: select hunk
<kbd>space</kbd>: pick hunk
<kbd>b</kbd>: pick both hunks
<kbd>z</kbd>: undo (only available while still inside diff panel)
</pre>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

-354
View File
@@ -1,354 +0,0 @@
package main
import (
// "io"
// "io/ioutil"
// "strings"
"errors"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
)
var (
errNoFiles = errors.New("No changed files")
errNoUsername = errors.New(`No username set. Please do: git config --global user.name "Your Name"`)
)
func stagedFiles(files []GitFile) []GitFile {
result := make([]GitFile, 0)
for _, file := range files {
if file.HasStagedChanges {
result = append(result, file)
}
}
return result
}
func stageSelectedFile(g *gocui.Gui) error {
file, err := getSelectedFile(g)
if err != nil {
return err
}
return stageFile(file.Name)
}
func handleFilePress(g *gocui.Gui, v *gocui.View) error {
file, err := getSelectedFile(g)
if err != nil {
if err == errNoFiles {
return nil
}
return err
}
if file.HasMergeConflicts {
return handleSwitchToMerge(g, v)
}
if file.HasUnstagedChanges {
stageFile(file.Name)
} else {
unStageFile(file.Name, file.Tracked)
}
if err := refreshFiles(g); err != nil {
return err
}
return handleFileSelect(g, v)
}
func handleAddPatch(g *gocui.Gui, v *gocui.View) error {
file, err := getSelectedFile(g)
if err != nil {
if err == errNoFiles {
return nil
}
return err
}
if !file.HasUnstagedChanges {
return createErrorPanel(g, "File has no unstaged changes to add")
}
if !file.Tracked {
return createErrorPanel(g, "Cannot git add --patch untracked files")
}
gitAddPatch(g, file.Name)
return err
}
func getSelectedFile(g *gocui.Gui) (GitFile, error) {
if len(state.GitFiles) == 0 {
return GitFile{}, errNoFiles
}
filesView, err := g.View("files")
if err != nil {
panic(err)
}
lineNumber := getItemPosition(filesView)
return state.GitFiles[lineNumber], nil
}
func handleFileRemove(g *gocui.Gui, v *gocui.View) error {
file, err := getSelectedFile(g)
if err != nil {
if err == errNoFiles {
return nil
}
return err
}
var deleteVerb string
if file.Tracked {
deleteVerb = "checkout"
} else {
deleteVerb = "delete"
}
return createConfirmationPanel(g, v, strings.Title(deleteVerb)+" file", "Are you sure you want to "+deleteVerb+" "+file.Name+" (you will lose your changes)?", func(g *gocui.Gui, v *gocui.View) error {
if err := removeFile(file); err != nil {
panic(err)
}
return refreshFiles(g)
}, nil)
}
func handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
file, err := getSelectedFile(g)
if err != nil {
return createErrorPanel(g, err.Error())
}
if file.Tracked {
return createErrorPanel(g, "Cannot ignore tracked files")
}
gitIgnore(file.Name)
return refreshFiles(g)
}
func renderfilesOptions(g *gocui.Gui, gitFile *GitFile) error {
optionsMap := map[string]string{
"← → ↑ ↓": "navigate",
"S": "stash files",
"c": "commit changes",
"o": "open",
"i": "ignore",
"d": "delete",
"space": "toggle staged",
"R": "refresh",
"t": "add patch",
"e": "edit",
"PgUp/PgDn": "scroll",
}
if state.HasMergeConflicts {
optionsMap["a"] = "abort merge"
optionsMap["m"] = "resolve merge conflicts"
}
if gitFile == nil {
return renderOptionsMap(g, optionsMap)
}
if gitFile.Tracked {
optionsMap["d"] = "checkout"
}
return renderOptionsMap(g, optionsMap)
}
func handleFileSelect(g *gocui.Gui, v *gocui.View) error {
gitFile, err := getSelectedFile(g)
if err != nil {
if err != errNoFiles {
return err
}
renderString(g, "main", "No changed files")
return renderfilesOptions(g, nil)
}
renderfilesOptions(g, &gitFile)
var content string
if gitFile.HasMergeConflicts {
return refreshMergePanel(g)
}
content = getDiff(gitFile)
return renderString(g, "main", content)
}
func handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
if len(stagedFiles(state.GitFiles)) == 0 && !state.HasMergeConflicts {
return createErrorPanel(g, "There are no staged files to commit")
}
commitMessageView := getCommitMessageView(g)
g.Update(func(g *gocui.Gui) error {
g.SetViewOnTop("commitMessage")
switchFocus(g, filesView, commitMessageView)
return nil
})
return nil
}
func genericFileOpen(g *gocui.Gui, v *gocui.View, open func(*gocui.Gui, string) (string, error)) error {
file, err := getSelectedFile(g)
if err != nil {
if err != errNoFiles {
return err
}
return nil
}
if _, err := open(g, file.Name); err != nil {
return createErrorPanel(g, err.Error())
}
return nil
}
func handleFileEdit(g *gocui.Gui, v *gocui.View) error {
return genericFileOpen(g, v, editFile)
}
func handleFileOpen(g *gocui.Gui, v *gocui.View) error {
return genericFileOpen(g, v, openFile)
}
func handleSublimeFileOpen(g *gocui.Gui, v *gocui.View) error {
return genericFileOpen(g, v, sublimeOpenFile)
}
func handleVsCodeFileOpen(g *gocui.Gui, v *gocui.View) error {
return genericFileOpen(g, v, vsCodeOpenFile)
}
func handleRefreshFiles(g *gocui.Gui, v *gocui.View) error {
return refreshFiles(g)
}
func refreshStateGitFiles() {
// get files to stage
gitFiles := getGitStatusFiles()
state.GitFiles = mergeGitStatusFiles(state.GitFiles, gitFiles)
updateHasMergeConflictStatus()
}
func updateHasMergeConflictStatus() error {
merging, err := isInMergeState()
if err != nil {
return err
}
state.HasMergeConflicts = merging
return nil
}
func renderGitFile(gitFile GitFile, filesView *gocui.View) {
// potentially inefficient to be instantiating these color
// objects with each render
red := color.New(color.FgRed)
green := color.New(color.FgGreen)
if !gitFile.Tracked && !gitFile.HasStagedChanges {
red.Fprintln(filesView, gitFile.DisplayString)
return
}
green.Fprint(filesView, gitFile.DisplayString[0:1])
red.Fprint(filesView, gitFile.DisplayString[1:3])
if gitFile.HasUnstagedChanges {
red.Fprintln(filesView, gitFile.Name)
} else {
green.Fprintln(filesView, gitFile.Name)
}
}
func catSelectedFile(g *gocui.Gui) (string, error) {
item, err := getSelectedFile(g)
if err != nil {
if err != errNoFiles {
return "", err
}
return "", renderString(g, "main", "No file to display")
}
cat, err := catFile(item.Name)
if err != nil {
panic(err)
}
return cat, nil
}
func refreshFiles(g *gocui.Gui) error {
filesView, err := g.View("files")
if err != nil {
return err
}
refreshStateGitFiles()
filesView.Clear()
for _, gitFile := range state.GitFiles {
renderGitFile(gitFile, filesView)
}
correctCursor(filesView)
if filesView == g.CurrentView() {
handleFileSelect(g, filesView)
}
return nil
}
func pullFiles(g *gocui.Gui, v *gocui.View) error {
createMessagePanel(g, v, "", "Pulling...")
go func() {
if output, err := gitPull(); err != nil {
createErrorPanel(g, output)
} else {
closeConfirmationPrompt(g)
refreshCommits(g)
refreshStatus(g)
}
refreshFiles(g)
}()
return nil
}
func pushFiles(g *gocui.Gui, v *gocui.View) error {
createMessagePanel(g, v, "", "Pushing...")
go func() {
if output, err := gitPush(); err != nil {
createErrorPanel(g, output)
} else {
closeConfirmationPrompt(g)
refreshCommits(g)
refreshStatus(g)
}
}()
return nil
}
func handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error {
mergeView, err := g.View("main")
if err != nil {
return err
}
file, err := getSelectedFile(g)
if err != nil {
if err != errNoFiles {
return err
}
return nil
}
if !file.HasMergeConflicts {
return createErrorPanel(g, "This file has no merge conflicts")
}
switchFocus(g, v, mergeView)
return refreshMergePanel(g)
}
func handleAbortMerge(g *gocui.Gui, v *gocui.View) error {
output, err := gitAbortMerge()
if err != nil {
return createErrorPanel(g, output)
}
createMessagePanel(g, v, "", "Merge aborted")
refreshStatus(g)
return refreshFiles(g)
}
func handleResetHard(g *gocui.Gui, v *gocui.View) error {
return createConfirmationPanel(g, v, "Clear file panel", "Are you sure you want `reset --hard HEAD`? You may lose changes", func(g *gocui.Gui, v *gocui.View) error {
if err := gitResetHard(); err != nil {
createErrorPanel(g, err.Error())
}
return refreshFiles(g)
}, nil)
}
-541
View File
@@ -1,541 +0,0 @@
package main
import (
// "log"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"time"
"github.com/jesseduffield/gocui"
gitconfig "github.com/tcnksm/go-gitconfig"
git "gopkg.in/src-d/go-git.v4"
"gopkg.in/src-d/go-git.v4/plumbing/object"
)
var (
// ErrNoOpenCommand : When we don't know which command to use to open a file
ErrNoOpenCommand = errors.New("Unsure what command to use to open this file")
)
// GitFile : A staged/unstaged file
// TODO: decide whether to give all of these the Git prefix
type GitFile struct {
Name string
HasStagedChanges bool
HasUnstagedChanges bool
Tracked bool
Deleted bool
HasMergeConflicts bool
DisplayString string
}
// Commit : A git commit
type Commit struct {
Sha string
Name string
Pushed bool
DisplayString string
}
// StashEntry : A git stash entry
type StashEntry struct {
Index int
Name string
DisplayString string
}
// Map (from https://gobyexample.com/collection-functions)
func Map(vs []string, f func(string) string) []string {
vsm := make([]string, len(vs))
for i, v := range vs {
vsm[i] = f(v)
}
return vsm
}
func includesString(list []string, a string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
// not sure how to genericise this because []interface{} doesn't accept e.g.
// []int arguments
func includesInt(list []int, a int) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
func mergeGitStatusFiles(oldGitFiles, newGitFiles []GitFile) []GitFile {
if len(oldGitFiles) == 0 {
return newGitFiles
}
appendedIndexes := make([]int, 0)
// retain position of files we already could see
result := make([]GitFile, 0)
for _, oldGitFile := range oldGitFiles {
for newIndex, newGitFile := range newGitFiles {
if oldGitFile.Name == newGitFile.Name {
result = append(result, newGitFile)
appendedIndexes = append(appendedIndexes, newIndex)
break
}
}
}
// append any new files to the end
for index, newGitFile := range newGitFiles {
if !includesInt(appendedIndexes, index) {
result = append(result, newGitFile)
}
}
return result
}
// only to be used when you're already in an error state
func runDirectCommandIgnoringError(command string) string {
output, _ := runDirectCommand(command)
return output
}
func runDirectCommand(command string) (string, error) {
commandLog(command)
cmdOut, err := exec.
Command(state.Platform.shell, state.Platform.shellArg, command).
CombinedOutput()
return sanitisedCommandOutput(cmdOut, err)
}
func branchStringParts(branchString string) (string, string) {
// expect string to be something like '4w master`
splitBranchName := strings.Split(branchString, "\t")
// if we have no \t then we have no recency, so just output that as blank
if len(splitBranchName) == 1 {
return "", branchString
}
return splitBranchName[0], splitBranchName[1]
}
// TODO: DRY up this function and getGitBranches
func getGitStashEntries() []StashEntry {
stashEntries := make([]StashEntry, 0)
rawString, _ := runDirectCommand("git stash list --pretty='%gs'")
for i, line := range splitLines(rawString) {
stashEntries = append(stashEntries, stashEntryFromLine(line, i))
}
return stashEntries
}
func stashEntryFromLine(line string, index int) StashEntry {
return StashEntry{
Name: line,
Index: index,
DisplayString: line,
}
}
func getStashEntryDiff(index int) (string, error) {
return runCommand("git stash show -p --color stash@{" + fmt.Sprint(index) + "}")
}
func includes(array []string, str string) bool {
for _, arrayStr := range array {
if arrayStr == str {
return true
}
}
return false
}
func getGitStatusFiles() []GitFile {
statusOutput, _ := getGitStatus()
statusStrings := splitLines(statusOutput)
gitFiles := make([]GitFile, 0)
for _, statusString := range statusStrings {
change := statusString[0:2]
stagedChange := change[0:1]
unstagedChange := statusString[1:2]
filename := statusString[3:]
tracked := !includes([]string{"??", "A "}, change)
gitFile := GitFile{
Name: filename,
DisplayString: statusString,
HasStagedChanges: !includes([]string{" ", "U", "?"}, stagedChange),
HasUnstagedChanges: unstagedChange != " ",
Tracked: tracked,
Deleted: unstagedChange == "D" || stagedChange == "D",
HasMergeConflicts: change == "UU",
}
gitFiles = append(gitFiles, gitFile)
}
objectLog(gitFiles)
return gitFiles
}
func gitStashDo(index int, method string) (string, error) {
return runCommand("git stash " + method + " stash@{" + fmt.Sprint(index) + "}")
}
func gitStashSave(message string) (string, error) {
output, err := runCommand("git stash save \"" + message + "\"")
if err != nil {
return output, err
}
// if there are no local changes to save, the exit code is 0, but we want
// to raise an error
if output == "No local changes to save\n" {
return output, errors.New(output)
}
return output, nil
}
func gitCheckout(branch string, force bool) (string, error) {
forceArg := ""
if force {
forceArg = "--force "
}
return runCommand("git checkout " + forceArg + branch)
}
func sanitisedCommandOutput(output []byte, err error) (string, error) {
outputString := string(output)
if outputString == "" && err != nil {
return err.Error(), err
}
return outputString, err
}
func runCommand(command string) (string, error) {
commandLog(command)
splitCmd := strings.Split(command, " ")
cmdOut, err := exec.Command(splitCmd[0], splitCmd[1:]...).CombinedOutput()
return sanitisedCommandOutput(cmdOut, err)
}
func vsCodeOpenFile(g *gocui.Gui, filename string) (string, error) {
return runCommand("code -r " + filename)
}
func sublimeOpenFile(g *gocui.Gui, filename string) (string, error) {
return runCommand("subl " + filename)
}
func openFile(g *gocui.Gui, filename string) (string, error) {
cmdName, cmdTrail, err := getOpenCommand()
if err != nil {
return "", err
}
return runCommand(cmdName + " " + filename + cmdTrail)
}
func getOpenCommand() (string, string, error) {
//NextStep open equivalents: xdg-open (linux), cygstart (cygwin), open (OSX)
trailMap := map[string]string{
"xdg-open": " &>/dev/null &",
"cygstart": "",
"open": "",
}
for name, trail := range trailMap {
if out, _ := runCommand("which " + name); out != "exit status 1" {
return name, trail, nil
}
}
return "", "", ErrNoOpenCommand
}
func gitAddPatch(g *gocui.Gui, filename string) {
runSubProcess(g, "git", "add", "--patch", filename)
}
func editFile(g *gocui.Gui, filename string) (string, error) {
editor, _ := gitconfig.Global("core.editor")
if editor == "" {
editor = os.Getenv("VISUAL")
}
if editor == "" {
editor = os.Getenv("EDITOR")
}
if editor == "" {
return "", createErrorPanel(g, "No editor defined in $VISUAL, $EDITOR, or git config.")
}
runSubProcess(g, editor, filename)
return "", nil
}
func runSubProcess(g *gocui.Gui, cmdName string, commandArgs ...string) {
subprocess = exec.Command(cmdName, commandArgs...)
subprocess.Stdin = os.Stdin
subprocess.Stdout = os.Stdout
subprocess.Stderr = os.Stderr
g.Update(func(g *gocui.Gui) error {
return ErrSubprocess
})
}
func getBranchGraph(branch string) (string, error) {
return runCommand("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium -100 " + branch)
}
func verifyInGitRepo() {
if output, err := runCommand("git status"); err != nil {
fmt.Println(output)
os.Exit(1)
}
}
func getCommits() []Commit {
pushables := gitCommitsToPush()
log := getLog()
commits := make([]Commit, 0)
// now we can split it up and turn it into commits
lines := splitLines(log)
for _, line := range lines {
splitLine := strings.Split(line, " ")
sha := splitLine[0]
pushed := includesString(pushables, sha)
commits = append(commits, Commit{
Sha: sha,
Name: strings.Join(splitLine[1:], " "),
Pushed: pushed,
DisplayString: strings.Join(splitLine, " "),
})
}
return commits
}
func getLog() string {
// currently limiting to 30 for performance reasons
// TODO: add lazyloading when you scroll down
result, err := runDirectCommand("git log --oneline -30")
if err != nil {
// assume if there is an error there are no commits yet for this branch
return ""
}
return result
}
func gitIgnore(filename string) {
if _, err := runDirectCommand("echo '" + filename + "' >> .gitignore"); err != nil {
panic(err)
}
}
func gitShow(sha string) string {
result, err := runDirectCommand("git show --color " + sha)
if err != nil {
panic(err)
}
return result
}
func getDiff(file GitFile) string {
cachedArg := ""
if file.HasStagedChanges && !file.HasUnstagedChanges {
cachedArg = "--cached "
}
deletedArg := ""
if file.Deleted {
deletedArg = "-- "
}
trackedArg := ""
if !file.Tracked && !file.HasStagedChanges {
trackedArg = "--no-index /dev/null "
}
command := "git diff --color " + cachedArg + deletedArg + trackedArg + file.Name
// for now we assume an error means the file was deleted
s, _ := runCommand(command)
return s
}
func catFile(file string) (string, error) {
return runDirectCommand("cat " + file)
}
func stageFile(file string) error {
_, err := runCommand("git add " + file)
return err
}
func unStageFile(file string, tracked bool) error {
var command string
if tracked {
command = "git reset HEAD "
} else {
command = "git rm --cached "
}
_, err := runCommand(command + file)
return err
}
func getGitStatus() (string, error) {
return runCommand("git status --untracked-files=all --short")
}
func isInMergeState() (bool, error) {
output, err := runCommand("git status --untracked-files=all")
if err != nil {
return false, err
}
return strings.Contains(output, "conclude merge") || strings.Contains(output, "unmerged paths"), nil
}
func removeFile(file GitFile) error {
// if the file isn't tracked, we assume you want to delete it
if !file.Tracked {
_, err := runCommand("rm -rf ./" + file.Name)
return err
}
// if the file is tracked, we assume you want to just check it out
_, err := runCommand("git checkout " + file.Name)
return err
}
func gitCommit(g *gocui.Gui, message string) (string, error) {
gpgsign, _ := gitconfig.Global("commit.gpgsign")
if gpgsign != "" {
runSubProcess(g, "bash", "-c", "git commit -m \""+message+"\"")
return "", nil
}
userName, err := gitconfig.Username()
if userName == "" {
return "", errNoUsername
}
userEmail, err := gitconfig.Email()
_, err = w.Commit(message, &git.CommitOptions{
Author: &object.Signature{
Name: userName,
Email: userEmail,
When: time.Now(),
},
})
if err != nil {
return err.Error(), err
}
return "", nil
}
func gitPull() (string, error) {
return runDirectCommand("git pull --no-edit")
}
func gitPush() (string, error) {
return runDirectCommand("git push -u origin " + state.Branches[0].Name)
}
func gitSquashPreviousTwoCommits(message string) (string, error) {
return runDirectCommand("git reset --soft HEAD^ && git commit --amend -m \"" + message + "\"")
}
func gitSquashFixupCommit(branchName string, shaValue string) (string, error) {
var err error
commands := []string{
"git checkout -q " + shaValue,
"git reset --soft " + shaValue + "^",
"git commit --amend -C " + shaValue + "^",
"git rebase --onto HEAD " + shaValue + " " + branchName,
}
ret := ""
for _, command := range commands {
devLog(command)
output, err := runDirectCommand(command)
ret += output
if err != nil {
devLog(ret)
break
}
}
if err != nil {
// We are already in an error state here so we're just going to append
// the output of these commands
ret += runDirectCommandIgnoringError("git branch -d " + shaValue)
ret += runDirectCommandIgnoringError("git checkout " + branchName)
}
return ret, err
}
func gitRenameCommit(message string) (string, error) {
return runDirectCommand("git commit --allow-empty --amend -m \"" + message + "\"")
}
func gitFetch() (string, error) {
return runDirectCommand("git fetch")
}
func gitResetToCommit(sha string) (string, error) {
return runDirectCommand("git reset " + sha)
}
func gitNewBranch(name string) (string, error) {
return runDirectCommand("git checkout -b " + name)
}
func gitDeleteBranch(branch string) (string, error) {
return runCommand("git branch -d " + branch)
}
func gitListStash() (string, error) {
return runDirectCommand("git stash list")
}
func gitMerge(branchName string) (string, error) {
return runDirectCommand("git merge --no-edit " + branchName)
}
func gitAbortMerge() (string, error) {
return runDirectCommand("git merge --abort")
}
func gitUpstreamDifferenceCount() (string, string) {
pushableCount, err := runDirectCommand("git rev-list @{u}..head --count")
if err != nil {
return "?", "?"
}
pullableCount, err := runDirectCommand("git rev-list head..@{u} --count")
if err != nil {
return "?", "?"
}
return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount)
}
func gitCommitsToPush() []string {
pushables, err := runDirectCommand("git rev-list @{u}..head --abbrev-commit")
if err != nil {
return make([]string, 0)
}
return splitLines(pushables)
}
func getGitBranches() []Branch {
builder := newBranchListBuilder()
return builder.build()
}
func branchIncluded(branchName string, branches []Branch) bool {
for _, existingBranch := range branches {
if strings.ToLower(existingBranch.Name) == strings.ToLower(branchName) {
return true
}
}
return false
}
func gitResetHard() error {
return w.Reset(&git.ResetOptions{Mode: git.HardReset})
}
-297
View File
@@ -1,297 +0,0 @@
package main
import (
// "io"
// "io/ioutil"
"runtime"
"strings"
"time"
// "strings"
"github.com/golang-collections/collections/stack"
"github.com/jesseduffield/gocui"
)
// OverlappingEdges determines if panel edges overlap
var OverlappingEdges = false
type stateType struct {
GitFiles []GitFile
Branches []Branch
Commits []Commit
StashEntries []StashEntry
PreviousView string
HasMergeConflicts bool
ConflictIndex int
ConflictTop bool
Conflicts []conflict
EditHistory *stack.Stack
Platform platform
}
type conflict struct {
start int
middle int
end int
}
var state = stateType{
GitFiles: make([]GitFile, 0),
PreviousView: "files",
Commits: make([]Commit, 0),
StashEntries: make([]StashEntry, 0),
ConflictIndex: 0,
ConflictTop: true,
Conflicts: make([]conflict, 0),
EditHistory: stack.New(),
Platform: getPlatform(),
}
type platform struct {
os string
shell string
shellArg string
escapedQuote string
}
func getPlatform() platform {
switch runtime.GOOS {
case "windows":
return platform{
os: "windows",
shell: "cmd",
shellArg: "/c",
escapedQuote: "\\\"",
}
default:
return platform{
os: runtime.GOOS,
shell: "bash",
shellArg: "-c",
escapedQuote: "\"",
}
}
}
func scrollUpMain(g *gocui.Gui, v *gocui.View) error {
mainView, _ := g.View("main")
ox, oy := mainView.Origin()
if oy >= 1 {
return mainView.SetOrigin(ox, oy-1)
}
return nil
}
func scrollDownMain(g *gocui.Gui, v *gocui.View) error {
mainView, _ := g.View("main")
ox, oy := mainView.Origin()
if oy < len(mainView.BufferLines()) {
return mainView.SetOrigin(ox, oy+1)
}
return nil
}
func handleRefresh(g *gocui.Gui, v *gocui.View) error {
return refreshSidePanels(g)
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
// layout is called for every screen re-render e.g. when the screen is resized
func layout(g *gocui.Gui) error {
g.Highlight = true
g.SelFgColor = gocui.ColorWhite | gocui.AttrBold
width, height := g.Size()
leftSideWidth := width / 3
statusFilesBoundary := 2
filesBranchesBoundary := 2 * height / 5 // height - 20
commitsBranchesBoundary := 3 * height / 5 // height - 10
commitsStashBoundary := height - 5 // height - 5
minimumHeight := 16
minimumWidth := 10
panelSpacing := 1
if OverlappingEdges {
panelSpacing = 0
}
if height < minimumHeight || width < minimumWidth {
v, err := g.SetView("limit", 0, 0, max(width-1, 2), max(height-1, 2), 0)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = "Not enough space to render panels"
v.Wrap = true
}
return nil
}
g.DeleteView("limit")
optionsTop := height - 2
// hiding options if there's not enough space
if height < 30 {
optionsTop = height - 1
}
v, err := g.SetView("main", leftSideWidth+panelSpacing, 0, width-1, optionsTop, gocui.LEFT)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = "Diff"
v.Wrap = true
v.FgColor = gocui.ColorWhite
}
if v, err := g.SetView("status", 0, 0, leftSideWidth, statusFilesBoundary, gocui.BOTTOM|gocui.RIGHT); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = "Status"
v.FgColor = gocui.ColorWhite
}
filesView, err := g.SetView("files", 0, statusFilesBoundary+panelSpacing, leftSideWidth, filesBranchesBoundary, gocui.TOP|gocui.BOTTOM)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
filesView.Highlight = true
filesView.Title = "Files"
v.FgColor = gocui.ColorWhite
}
if v, err := g.SetView("branches", 0, filesBranchesBoundary+panelSpacing, leftSideWidth, commitsBranchesBoundary, gocui.TOP|gocui.BOTTOM); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = "Branches"
v.FgColor = gocui.ColorWhite
}
if v, err := g.SetView("commits", 0, commitsBranchesBoundary+panelSpacing, leftSideWidth, commitsStashBoundary, gocui.TOP|gocui.BOTTOM); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = "Commits"
v.FgColor = gocui.ColorWhite
}
if v, err := g.SetView("stash", 0, commitsStashBoundary+panelSpacing, leftSideWidth, optionsTop, gocui.TOP|gocui.RIGHT); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = "Stash"
v.FgColor = gocui.ColorWhite
}
if v, err := g.SetView("options", -1, optionsTop, width-len(version)-2, optionsTop+2, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.FgColor = gocui.ColorBlue
v.Frame = false
}
if getCommitMessageView(g) == nil {
// doesn't matter where this view starts because it will be hidden
if commitMessageView, err := g.SetView("commitMessage", 0, 0, width, height, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
g.SetViewOnBottom("commitMessage")
commitMessageView.Title = "Commit message"
commitMessageView.FgColor = gocui.ColorWhite
commitMessageView.Editable = true
}
}
if err = resizeConfirmationPanel(g, "commitMessage"); err != nil {
return err
}
if err = resizeConfirmationPanel(g, "confirmation"); err != nil {
return err
}
if v, err := g.SetView("version", width-len(version)-1, optionsTop, width, optionsTop+2, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.BgColor = gocui.ColorDefault
v.FgColor = gocui.ColorGreen
v.Frame = false
renderString(g, "version", version)
// these are only called once
handleFileSelect(g, filesView)
refreshFiles(g)
refreshBranches(g)
refreshCommits(g)
refreshStashEntries(g)
nextView(g, nil)
}
return nil
}
func fetch(g *gocui.Gui) error {
gitFetch()
refreshStatus(g)
return nil
}
func updateLoader(g *gocui.Gui) error {
if confirmationView, _ := g.View("confirmation"); confirmationView != nil {
content := trimmedContent(confirmationView)
if strings.Contains(content, "...") {
staticContent := strings.Split(content, "...")[0] + "..."
renderString(g, "confirmation", staticContent+" "+loader())
}
}
return nil
}
func goEvery(g *gocui.Gui, interval time.Duration, function func(*gocui.Gui) error) {
go func() {
for range time.Tick(interval) {
function(g)
}
}()
}
func run() (err error) {
g, err := gocui.NewGui(gocui.OutputNormal, OverlappingEdges)
if err != nil {
return
}
defer g.Close()
g.FgColor = gocui.ColorDefault
goEvery(g, time.Second*60, fetch)
goEvery(g, time.Second*10, refreshFiles)
goEvery(g, time.Millisecond*10, updateLoader)
g.SetManagerFunc(layout)
if err = keybindings(g); err != nil {
return
}
err = g.MainLoop()
return
}
func quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}
+19 -92
View File
@@ -1,91 +1,31 @@
package main
import (
"errors"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
"github.com/davecgh/go-spew/spew"
"github.com/jesseduffield/gocui"
git "gopkg.in/src-d/go-git.v4"
"github.com/jesseduffield/lazygit/pkg/app"
"github.com/jesseduffield/lazygit/pkg/config"
)
// ErrSubProcess is raised when we are running a subprocess
var (
ErrSubprocess = errors.New("running subprocess")
subprocess *exec.Cmd
commit string
version = "unversioned"
date string
date string
debuggingFlag = flag.Bool("debug", false, "a boolean")
versionFlag = flag.Bool("v", false, "Print the current version")
w *git.Worktree
r *git.Repository
)
func homeDirectory() string {
usr, err := user.Current()
if err != nil {
log.Fatal(err)
}
return usr.HomeDir
}
func projectPath(path string) string {
gopath := os.Getenv("GOPATH")
return filepath.FromSlash(gopath + "/src/github.com/jesseduffield/lazygit/" + path)
}
func devLog(objects ...interface{}) {
localLog("development.log", objects...)
}
func objectLog(object interface{}) {
if !*debuggingFlag {
return
}
str := spew.Sdump(object)
localLog("development.log", str)
}
func commandLog(objects ...interface{}) {
localLog("commands.log", objects...)
}
func localLog(path string, objects ...interface{}) {
if !*debuggingFlag {
return
}
f, err := os.OpenFile(projectPath(path), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
panic(err.Error())
}
defer f.Close()
log.SetOutput(f)
for _, object := range objects {
log.Println(fmt.Sprint(object))
}
}
func navigateToRepoRootDirectory() {
_, err := os.Stat(".git")
for os.IsNotExist(err) {
devLog("going up a directory to find the root")
os.Chdir("..")
_, err = os.Stat(".git")
}
}
// when building the binary, `version` is set as a compile-time variable, along
// with `date` and `commit`. If this program has been opened directly via go,
// we will populate the `version` with VERSION in the lazygit root directory
@@ -98,41 +38,28 @@ func fallbackVersion() string {
return string(byteVersion)
}
func setupWorktree() {
var err error
r, err = git.PlainOpen(".")
if err != nil {
panic(err)
}
w, err = r.Worktree()
if err != nil {
panic(err)
}
}
func main() {
devLog("\n\n\n\n\n\n\n\n\n\n")
flag.Parse()
if version == "unversioned" {
version = fallbackVersion()
}
if *versionFlag {
fmt.Printf("commit=%s, build date=%s, version=%s", commit, date, version)
fmt.Printf("commit=%s, build date=%s, version=%s, os=%s, arch=%s\n", commit, date, version, runtime.GOOS, runtime.GOARCH)
os.Exit(0)
}
verifyInGitRepo()
navigateToRepoRootDirectory()
setupWorktree()
for {
if err := run(); err != nil {
if err == gocui.ErrQuit {
break
} else if err == ErrSubprocess {
subprocess.Run()
} else {
log.Panicln(err)
}
}
appConfig, err := config.NewAppConfig("lazygit", version, commit, date, debuggingFlag)
if err != nil {
panic(err)
}
app, err := app.NewApp(appConfig)
if err != nil {
// TODO: remove this call to panic after anonymous error reporting
// is setup (right now the call to panic logs nothing to the screen which
// would make debugging difficult
panic(err)
// app.Log.Panic(err.Error())
}
app.GitCommand.SetupGit()
app.Gui.RunWithSubprocesses()
}
-263
View File
@@ -1,263 +0,0 @@
// though this panel is called the merge panel, it's really going to use the main panel. This may change in the future
package main
import (
"bufio"
"bytes"
"io/ioutil"
"math"
"os"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
)
func findConflicts(content string) ([]conflict, error) {
conflicts := make([]conflict, 0)
var newConflict conflict
for i, line := range splitLines(content) {
if line == "<<<<<<< HEAD" || line == "<<<<<<< MERGE_HEAD" || line == "<<<<<<< Updated upstream" {
newConflict = conflict{start: i}
} else if line == "=======" {
newConflict.middle = i
} else if strings.HasPrefix(line, ">>>>>>> ") {
newConflict.end = i
conflicts = append(conflicts, newConflict)
}
}
return conflicts, nil
}
func shiftConflict(conflicts []conflict) (conflict, []conflict) {
return conflicts[0], conflicts[1:]
}
func shouldHighlightLine(index int, conflict conflict, top bool) bool {
return (index >= conflict.start && index <= conflict.middle && top) || (index >= conflict.middle && index <= conflict.end && !top)
}
func coloredConflictFile(content string, conflicts []conflict, conflictIndex int, conflictTop, hasFocus bool) (string, error) {
if len(conflicts) == 0 {
return content, nil
}
conflict, remainingConflicts := shiftConflict(conflicts)
var outputBuffer bytes.Buffer
for i, line := range splitLines(content) {
colourAttr := color.FgWhite
if i == conflict.start || i == conflict.middle || i == conflict.end {
colourAttr = color.FgRed
}
colour := color.New(colourAttr)
if hasFocus && conflictIndex < len(conflicts) && conflicts[conflictIndex] == conflict && shouldHighlightLine(i, conflict, conflictTop) {
colour.Add(color.Bold)
}
if i == conflict.end && len(remainingConflicts) > 0 {
conflict, remainingConflicts = shiftConflict(remainingConflicts)
}
outputBuffer.WriteString(coloredStringDirect(line, colour) + "\n")
}
return outputBuffer.String(), nil
}
func handleSelectTop(g *gocui.Gui, v *gocui.View) error {
state.ConflictTop = true
return refreshMergePanel(g)
}
func handleSelectBottom(g *gocui.Gui, v *gocui.View) error {
state.ConflictTop = false
return refreshMergePanel(g)
}
func handleSelectNextConflict(g *gocui.Gui, v *gocui.View) error {
if state.ConflictIndex >= len(state.Conflicts)-1 {
return nil
}
state.ConflictIndex++
return refreshMergePanel(g)
}
func handleSelectPrevConflict(g *gocui.Gui, v *gocui.View) error {
if state.ConflictIndex <= 0 {
return nil
}
state.ConflictIndex--
return refreshMergePanel(g)
}
func isIndexToDelete(i int, conflict conflict, pick string) bool {
return i == conflict.middle ||
i == conflict.start ||
i == conflict.end ||
pick != "both" &&
(pick == "bottom" && i > conflict.start && i < conflict.middle) ||
(pick == "top" && i > conflict.middle && i < conflict.end)
}
func resolveConflict(g *gocui.Gui, conflict conflict, pick string) error {
gitFile, err := getSelectedFile(g)
if err != nil {
return err
}
file, err := os.Open(gitFile.Name)
if err != nil {
return err
}
defer file.Close()
reader := bufio.NewReader(file)
output := ""
for i := 0; true; i++ {
line, err := reader.ReadString('\n')
if err != nil {
break
}
if !isIndexToDelete(i, conflict, pick) {
output += line
}
}
devLog(output)
return ioutil.WriteFile(gitFile.Name, []byte(output), 0644)
}
func pushFileSnapshot(g *gocui.Gui) error {
gitFile, err := getSelectedFile(g)
if err != nil {
return err
}
content, err := catFile(gitFile.Name)
if err != nil {
return err
}
state.EditHistory.Push(content)
return nil
}
func handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error {
if state.EditHistory.Len() == 0 {
return nil
}
prevContent := state.EditHistory.Pop().(string)
gitFile, err := getSelectedFile(g)
if err != nil {
return err
}
ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644)
return refreshMergePanel(g)
}
func handlePickHunk(g *gocui.Gui, v *gocui.View) error {
conflict := state.Conflicts[state.ConflictIndex]
pushFileSnapshot(g)
pick := "bottom"
if state.ConflictTop {
pick = "top"
}
err := resolveConflict(g, conflict, pick)
if err != nil {
panic(err)
}
refreshMergePanel(g)
return nil
}
func handlePickBothHunks(g *gocui.Gui, v *gocui.View) error {
conflict := state.Conflicts[state.ConflictIndex]
pushFileSnapshot(g)
err := resolveConflict(g, conflict, "both")
if err != nil {
panic(err)
}
return refreshMergePanel(g)
}
func currentViewName(g *gocui.Gui) string {
currentView := g.CurrentView()
return currentView.Name()
}
func refreshMergePanel(g *gocui.Gui) error {
cat, err := catSelectedFile(g)
if err != nil {
return err
}
state.Conflicts, err = findConflicts(cat)
if err != nil {
return err
}
if len(state.Conflicts) == 0 {
return handleCompleteMerge(g)
} else if state.ConflictIndex > len(state.Conflicts)-1 {
state.ConflictIndex = len(state.Conflicts) - 1
}
hasFocus := currentViewName(g) == "main"
if hasFocus {
renderMergeOptions(g)
}
content, err := coloredConflictFile(cat, state.Conflicts, state.ConflictIndex, state.ConflictTop, hasFocus)
if err != nil {
return err
}
if err := scrollToConflict(g); err != nil {
return err
}
return renderString(g, "main", content)
}
func scrollToConflict(g *gocui.Gui) error {
mainView, err := g.View("main")
if err != nil {
return err
}
if len(state.Conflicts) == 0 {
return nil
}
conflict := state.Conflicts[state.ConflictIndex]
ox, _ := mainView.Origin()
_, height := mainView.Size()
conflictMiddle := (conflict.end + conflict.start) / 2
newOriginY := int(math.Max(0, float64(conflictMiddle-(height/2))))
return mainView.SetOrigin(ox, newOriginY)
}
func switchToMerging(g *gocui.Gui) error {
state.ConflictIndex = 0
state.ConflictTop = true
_, err := g.SetCurrentView("main")
if err != nil {
return err
}
return refreshMergePanel(g)
}
func renderMergeOptions(g *gocui.Gui) error {
return renderOptionsMap(g, map[string]string{
"↑ ↓": "select hunk",
"← →": "navigate conflicts",
"space": "pick hunk",
"b": "pick both hunks",
"z": "undo",
})
}
func handleEscapeMerge(g *gocui.Gui, v *gocui.View) error {
filesView, err := g.View("files")
if err != nil {
return err
}
refreshFiles(g)
return switchFocus(g, v, filesView)
}
func handleCompleteMerge(g *gocui.Gui) error {
filesView, err := g.View("files")
if err != nil {
return err
}
stageSelectedFile(g)
refreshFiles(g)
return switchFocus(g, nil, filesView)
}
+79
View File
@@ -0,0 +1,79 @@
package app
import (
"io"
"io/ioutil"
"os"
"github.com/Sirupsen/logrus"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui"
"github.com/jesseduffield/lazygit/pkg/i18n"
)
// App struct
type App struct {
closers []io.Closer
Config config.AppConfigurer
Log *logrus.Logger
OSCommand *commands.OSCommand
GitCommand *commands.GitCommand
Gui *gui.Gui
Tr *i18n.Localizer
}
func newLogger(config config.AppConfigurer) *logrus.Logger {
log := logrus.New()
if !config.GetDebug() {
log.Out = ioutil.Discard
return log
}
file, err := os.OpenFile("development.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
panic("unable to log to file") // TODO: don't panic (also, remove this call to the `panic` function)
}
log.SetOutput(file)
return log
}
// NewApp retruns a new applications
func NewApp(config config.AppConfigurer) (*App, error) {
app := &App{
closers: []io.Closer{},
Config: config,
}
var err error
app.Log = newLogger(config)
app.OSCommand, err = commands.NewOSCommand(app.Log)
if err != nil {
return app, err
}
app.Tr, err = i18n.NewLocalizer(app.Log)
if err != nil {
return app, err
}
app.GitCommand, err = commands.NewGitCommand(app.Log, app.OSCommand)
if err != nil {
return app, err
}
app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config)
if err != nil {
return app, err
}
return app, nil
}
// Close closes any resources
func (app *App) Close() error {
for _, closer := range app.closers {
err := closer.Close()
if err != nil {
return err
}
}
return nil
}
+8 -4
View File
@@ -1,22 +1,26 @@
package main
package commands
import (
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// Branch : A git branch
// duplicating this for now
type Branch struct {
Name string
Recency string
}
func (b *Branch) getDisplayString() string {
return withPadding(b.Recency, 4) + coloredString(b.Name, b.getColor())
// GetDisplayString returns the dispaly string of branch
func (b *Branch) GetDisplayString() string {
return utils.WithPadding(b.Recency, 4) + utils.ColoredString(b.Name, b.GetColor())
}
func (b *Branch) getColor() color.Attribute {
// GetColor branch color
func (b *Branch) GetColor() color.Attribute {
switch b.getType() {
case "feature":
return color.FgGreen
+498
View File
@@ -0,0 +1,498 @@
package commands
import (
"errors"
"fmt"
"os"
"os/exec"
"strings"
"github.com/Sirupsen/logrus"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
gitconfig "github.com/tcnksm/go-gitconfig"
gogit "gopkg.in/src-d/go-git.v4"
)
// GitCommand is our main git interface
type GitCommand struct {
Log *logrus.Logger
OSCommand *OSCommand
Worktree *gogit.Worktree
Repo *gogit.Repository
}
// NewGitCommand it runs git commands
func NewGitCommand(log *logrus.Logger, osCommand *OSCommand) (*GitCommand, error) {
gitCommand := &GitCommand{
Log: log,
OSCommand: osCommand,
}
return gitCommand, nil
}
// SetupGit sets git repo up
func (c *GitCommand) SetupGit() {
c.verifyInGitRepo()
c.navigateToRepoRootDirectory()
c.setupWorktree()
}
// GetStashEntries stash entryies
func (c *GitCommand) GetStashEntries() []StashEntry {
stashEntries := make([]StashEntry, 0)
rawString, _ := c.OSCommand.RunCommandWithOutput("git stash list --pretty='%gs'")
for i, line := range utils.SplitLines(rawString) {
stashEntries = append(stashEntries, stashEntryFromLine(line, i))
}
return stashEntries
}
func stashEntryFromLine(line string, index int) StashEntry {
return StashEntry{
Name: line,
Index: index,
DisplayString: line,
}
}
// GetStashEntryDiff stash diff
func (c *GitCommand) GetStashEntryDiff(index int) (string, error) {
return c.OSCommand.RunCommandWithOutput("git stash show -p --color stash@{" + fmt.Sprint(index) + "}")
}
func includes(array []string, str string) bool {
for _, arrayStr := range array {
if arrayStr == str {
return true
}
}
return false
}
// GetStatusFiles git status files
func (c *GitCommand) GetStatusFiles() []File {
statusOutput, _ := c.GitStatus()
statusStrings := utils.SplitLines(statusOutput)
files := make([]File, 0)
for _, statusString := range statusStrings {
change := statusString[0:2]
stagedChange := change[0:1]
unstagedChange := statusString[1:2]
filename := statusString[3:]
tracked := !includes([]string{"??", "A ", "AM"}, change)
file := File{
Name: filename,
DisplayString: statusString,
HasStagedChanges: !includes([]string{" ", "U", "?"}, stagedChange),
HasUnstagedChanges: unstagedChange != " ",
Tracked: tracked,
Deleted: unstagedChange == "D" || stagedChange == "D",
HasMergeConflicts: change == "UU",
}
files = append(files, file)
}
c.Log.Info(files) // TODO: use a dumper-esque log here
return files
}
// StashDo modify stash
func (c *GitCommand) StashDo(index int, method string) error {
return c.OSCommand.RunCommand("git stash " + method + " stash@{" + fmt.Sprint(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("git stash save " + c.OSCommand.Quote(message))
}
// MergeStatusFiles merge status files
func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []File) []File {
if len(oldFiles) == 0 {
return newFiles
}
appendedIndexes := make([]int, 0)
// retain position of files we already could see
result := make([]File, 0)
for _, oldFile := range oldFiles {
for newIndex, newFile := range newFiles {
if oldFile.Name == newFile.Name {
result = append(result, newFile)
appendedIndexes = append(appendedIndexes, newIndex)
break
}
}
}
// append any new files to the end
for index, newFile := range newFiles {
if !includesInt(appendedIndexes, index) {
result = append(result, newFile)
}
}
return result
}
func (c *GitCommand) verifyInGitRepo() {
if output, err := c.OSCommand.RunCommandWithOutput("git status"); err != nil {
fmt.Println(output)
os.Exit(1)
}
}
// GetBranchName branch name
func (c *GitCommand) GetBranchName() (string, error) {
return c.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD")
}
func (c *GitCommand) navigateToRepoRootDirectory() {
_, err := os.Stat(".git")
for os.IsNotExist(err) {
c.Log.Debug("going up a directory to find the root")
os.Chdir("..")
_, err = os.Stat(".git")
}
}
func (c *GitCommand) setupWorktree() {
r, err := gogit.PlainOpen(".")
if err != nil {
panic(err)
}
c.Repo = r
w, err := r.Worktree()
if err != nil {
panic(err)
}
c.Worktree = w
}
// ResetHard does the equivalent of `git reset --hard HEAD`
func (c *GitCommand) ResetHard() error {
return c.Worktree.Reset(&gogit.ResetOptions{Mode: gogit.HardReset})
}
// UpstreamDifferenceCount checks how many pushables/pullables there are for the
// current branch
func (c *GitCommand) UpstreamDifferenceCount() (string, string) {
pushableCount, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..head --count")
if err != nil {
return "?", "?"
}
pullableCount, err := c.OSCommand.RunCommandWithOutput("git rev-list head..@{u} --count")
if err != nil {
return "?", "?"
}
return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount)
}
// GetCommitsToPush Returns the sha's of the commits that have not yet been pushed
// to the remote branch of the current branch
func (c *GitCommand) GetCommitsToPush() []string {
pushables, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..head --abbrev-commit")
if err != nil {
return make([]string, 0)
}
return utils.SplitLines(pushables)
}
// RenameCommit renames the topmost commit with the given name
func (c *GitCommand) RenameCommit(name string) error {
return c.OSCommand.RunCommand("git commit --allow-empty --amend -m " + c.OSCommand.Quote(name))
}
// Fetch fetch git repo
func (c *GitCommand) Fetch() error {
return c.OSCommand.RunCommand("git fetch")
}
// ResetToCommit reset to commit
func (c *GitCommand) ResetToCommit(sha string) error {
return c.OSCommand.RunCommand("git reset " + sha)
}
// NewBranch create new branch
func (c *GitCommand) NewBranch(name string) error {
return c.OSCommand.RunCommand("git checkout -b " + name)
}
// DeleteBranch delete branch
func (c *GitCommand) DeleteBranch(branch string) error {
return c.OSCommand.RunCommand("git branch -d " + branch)
}
// ListStash list stash
func (c *GitCommand) ListStash() (string, error) {
return c.OSCommand.RunCommandWithOutput("git stash list")
}
// Merge merge
func (c *GitCommand) Merge(branchName string) error {
return c.OSCommand.RunCommand("git merge --no-edit " + branchName)
}
// AbortMerge abort merge
func (c *GitCommand) AbortMerge() error {
return c.OSCommand.RunCommand("git merge --abort")
}
// UsingGpg tells us whether the user has gpg enabled so that we can know
// whether we need to run a subprocess to allow them to enter their password
func (c *GitCommand) UsingGpg() bool {
gpgsign, _ := gitconfig.Global("commit.gpgsign")
if gpgsign == "" {
gpgsign, _ = gitconfig.Local("commit.gpgsign")
}
if gpgsign == "" {
return false
}
return true
}
// Commit commit to git
func (c *GitCommand) Commit(g *gocui.Gui, message string) (*exec.Cmd, error) {
command := "git commit -m " + c.OSCommand.Quote(message)
if c.UsingGpg() {
return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command)
}
return nil, c.OSCommand.RunCommand(command)
}
// Pull pull from repo
func (c *GitCommand) Pull() error {
return c.OSCommand.RunCommand("git pull --no-edit")
}
// Push push to a branch
func (c *GitCommand) Push(branchName string) error {
return c.OSCommand.RunCommand("git push -u origin " + branchName)
}
// SquashPreviousTwoCommits squashes a commit down to the one below it
// retaining the message of the higher commit
func (c *GitCommand) SquashPreviousTwoCommits(message string) error {
// TODO: test this
err := c.OSCommand.RunCommand("git reset --soft HEAD^")
if err != nil {
return err
}
// TODO: if password is required, we need to return a subprocess
return c.OSCommand.RunCommand("git commit --amend -m " + c.OSCommand.Quote(message))
}
// SquashFixupCommit squashes a 'FIXUP' commit into the commit beneath it,
// retaining the commit message of the lower commit
func (c *GitCommand) SquashFixupCommit(branchName string, shaValue string) error {
var err error
commands := []string{
"git checkout -q " + shaValue,
"git reset --soft " + shaValue + "^",
"git commit --amend -C " + shaValue + "^",
"git rebase --onto HEAD " + shaValue + " " + branchName,
}
ret := ""
for _, command := range commands {
c.Log.Info(command)
output, err := c.OSCommand.RunCommandWithOutput(command)
ret += output
if err != nil {
c.Log.Info(ret)
break
}
}
if err != nil {
// We are already in an error state here so we're just going to append
// the output of these commands
output, _ := c.OSCommand.RunCommandWithOutput("git branch -d " + shaValue)
ret += output
output, _ = c.OSCommand.RunCommandWithOutput("git checkout " + branchName)
ret += output
}
if err != nil {
return errors.New(ret)
}
return nil
}
// CatFile obtain the contents of a file
func (c *GitCommand) CatFile(file string) (string, error) {
return c.OSCommand.RunCommandWithOutput("cat " + file)
}
// StageFile stages a file
func (c *GitCommand) StageFile(file string) error {
return c.OSCommand.RunCommand("git add " + c.OSCommand.Quote(file))
}
// UnStageFile unstages a file
func (c *GitCommand) UnStageFile(file string, tracked bool) error {
var command string
if tracked {
command = "git reset HEAD "
} else {
command = "git rm --cached "
}
return c.OSCommand.RunCommand(command + file)
}
// GitStatus returns the plaintext short status of the repo
func (c *GitCommand) GitStatus() (string, error) {
return c.OSCommand.RunCommandWithOutput("git status --untracked-files=all --short")
}
// IsInMergeState states whether we are still mid-merge
func (c *GitCommand) IsInMergeState() (bool, error) {
output, err := c.OSCommand.RunCommandWithOutput("git status --untracked-files=all")
if err != nil {
return false, err
}
return strings.Contains(output, "conclude merge") || strings.Contains(output, "unmerged paths"), nil
}
// RemoveFile directly
func (c *GitCommand) RemoveFile(file File) error {
// if the file isn't tracked, we assume you want to delete it
if file.HasStagedChanges {
if err := c.OSCommand.RunCommand("git reset -- " + file.Name); err != nil {
return err
}
}
if !file.Tracked {
return os.RemoveAll(c.OSCommand.Unquote(file.Name))
}
// if the file is tracked, we assume you want to just check it out
return c.OSCommand.RunCommand("git checkout -- " + file.Name)
}
// Checkout checks out a branch, with --force if you set the force arg to true
func (c *GitCommand) Checkout(branch string, force bool) error {
forceArg := ""
if force {
forceArg = "--force "
}
return c.OSCommand.RunCommand("git checkout " + forceArg + branch)
}
// AddPatch prepares a subprocess for adding a patch by patch
// this will eventually be swapped out for a better solution inside the Gui
func (c *GitCommand) AddPatch(filename string) (*exec.Cmd, error) {
return c.OSCommand.PrepareSubProcess("git", "add", "--patch", filename)
}
// PrepareCommitSubProcess prepares a subprocess for `git commit`
func (c *GitCommand) PrepareCommitSubProcess() (*exec.Cmd, error) {
return c.OSCommand.PrepareSubProcess("git", "commit")
}
// GetBranchGraph gets the color-formatted graph of the log for the given branch
// Currently it limits the result to 100 commits, but when we get async stuff
// working we can do lazy loading
func (c *GitCommand) GetBranchGraph(branchName string) (string, error) {
return c.OSCommand.RunCommandWithOutput("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium -100 " + branchName)
}
// Map (from https://gobyexample.com/collection-functions)
func Map(vs []string, f func(string) string) []string {
vsm := make([]string, len(vs))
for i, v := range vs {
vsm[i] = f(v)
}
return vsm
}
func includesString(list []string, a string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
// not sure how to genericise this because []interface{} doesn't accept e.g.
// []int arguments
func includesInt(list []int, a int) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
// GetCommits obtains the commits of the current branch
func (c *GitCommand) GetCommits() []Commit {
pushables := c.GetCommitsToPush()
log := c.GetLog()
commits := make([]Commit, 0)
// now we can split it up and turn it into commits
lines := utils.SplitLines(log)
for _, line := range lines {
splitLine := strings.Split(line, " ")
sha := splitLine[0]
pushed := includesString(pushables, sha)
commits = append(commits, Commit{
Sha: sha,
Name: strings.Join(splitLine[1:], " "),
Pushed: pushed,
DisplayString: strings.Join(splitLine, " "),
})
}
return commits
}
// GetLog gets the git log (currently limited to 30 commits for performance
// until we work out lazy loading
func (c *GitCommand) GetLog() string {
// currently limiting to 30 for performance reasons
// TODO: add lazyloading when you scroll down
result, err := c.OSCommand.RunCommandWithOutput("git log --oneline -30")
if err != nil {
// assume if there is an error there are no commits yet for this branch
return ""
}
return result
}
// Ignore adds a file to the gitignore for the repo
func (c *GitCommand) Ignore(filename string) {
if _, err := c.OSCommand.RunDirectCommand("echo '" + filename + "' >> .gitignore"); err != nil {
panic(err)
}
}
// Show shows the diff of a commit
func (c *GitCommand) Show(sha string) string {
result, err := c.OSCommand.RunCommandWithOutput("git show --color " + sha)
if err != nil {
panic(err)
}
return result
}
// Diff returns the diff of a file
func (c *GitCommand) Diff(file File) string {
cachedArg := ""
fileName := file.Name
if file.HasStagedChanges && !file.HasUnstagedChanges {
cachedArg = "--cached"
} else {
// if the file is staged and has spaces in it, it comes pre-quoted
fileName = c.OSCommand.Quote(fileName)
}
trackedArg := "--"
if !file.Tracked && !file.HasStagedChanges {
trackedArg = "--no-index /dev/null"
}
command := fmt.Sprintf("%s %s %s %s", "git diff --color ", cachedArg, trackedArg, fileName)
// for now we assume an error means the file was deleted
s, _ := c.OSCommand.RunCommandWithOutput(command)
return s
}
+36
View File
@@ -0,0 +1,36 @@
package commands
// File : A staged/unstaged file
// TODO: decide whether to give all of these the Git prefix
type File struct {
Name string
HasStagedChanges bool
HasUnstagedChanges bool
Tracked bool
Deleted bool
HasMergeConflicts bool
DisplayString string
}
// Commit : A git commit
type Commit struct {
Sha string
Name string
Pushed bool
DisplayString string
}
// StashEntry : A git stash entry
type StashEntry struct {
Index int
Name string
DisplayString string
}
// Conflict : A git conflict with a start middle and end corresponding to line
// numbers in the file where the conflict bars appear
type Conflict struct {
Start int
Middle int
End int
}
+126
View File
@@ -0,0 +1,126 @@
package commands
import (
"io/ioutil"
"strings"
"testing"
"github.com/Sirupsen/logrus"
"github.com/jesseduffield/lazygit/pkg/test"
)
func getDummyLog() *logrus.Logger {
log := logrus.New()
log.Out = ioutil.Discard
return log
}
func getDummyOSCommand() *OSCommand {
return &OSCommand{
Log: getDummyLog(),
Platform: getPlatform(),
}
}
func getDummyGitCommand() *GitCommand {
return &GitCommand{
Log: getDummyLog(),
OSCommand: getDummyOSCommand(),
}
}
func TestDiff(t *testing.T) {
gitCommand := getDummyGitCommand()
if err := test.GenerateRepo("lots_of_diffs.sh"); err != nil {
t.Error(err.Error())
}
files := []File{
{
Name: "deleted_staged",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: true,
Deleted: true,
HasMergeConflicts: false,
DisplayString: " D deleted_staged",
},
{
Name: "\"file with space staged\"",
HasStagedChanges: true,
HasUnstagedChanges: false,
Tracked: false,
Deleted: false,
HasMergeConflicts: false,
DisplayString: "A \"file with space staged\"",
},
{
Name: "file with space unstaged",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: false,
Deleted: false,
HasMergeConflicts: false,
DisplayString: "?? file with space unstaged",
},
{
Name: "modified_unstaged",
HasStagedChanges: true,
HasUnstagedChanges: false,
Tracked: true,
Deleted: false,
HasMergeConflicts: false,
DisplayString: "M modified_unstaged",
},
{
Name: "modified_staged",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: true,
Deleted: false,
HasMergeConflicts: false,
DisplayString: " M modified_staged",
},
{
Name: "renamed_before -> renamed_after",
HasStagedChanges: true,
HasUnstagedChanges: false,
Tracked: true,
Deleted: false,
HasMergeConflicts: false,
DisplayString: "R renamed_before -> renamed_after",
},
{
Name: "untracked_unstaged",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: false,
Deleted: false,
HasMergeConflicts: false,
DisplayString: "?? untracked_unstaged",
},
{
Name: "untracked_staged",
HasStagedChanges: true,
HasUnstagedChanges: false,
Tracked: false,
Deleted: false,
HasMergeConflicts: false,
DisplayString: "A untracked_staged",
},
{
Name: "master",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: false,
Deleted: false,
HasMergeConflicts: false,
DisplayString: "?? master",
},
}
for _, file := range files {
content := gitCommand.Diff(file)
if strings.Contains(content, "error") {
t.Error("Error: diff test failed. File: " + file.Name + ", " + content)
}
}
}
+176
View File
@@ -0,0 +1,176 @@
package commands
import (
"errors"
"os"
"os/exec"
"runtime"
"strings"
"github.com/davecgh/go-spew/spew"
"github.com/mgutz/str"
"github.com/Sirupsen/logrus"
gitconfig "github.com/tcnksm/go-gitconfig"
)
// Platform stores the os state
type Platform struct {
os string
shell string
shellArg string
escapedQuote string
}
// OSCommand holds all the os commands
type OSCommand struct {
Log *logrus.Logger
Platform *Platform
}
// NewOSCommand os command runner
func NewOSCommand(log *logrus.Logger) (*OSCommand, error) {
osCommand := &OSCommand{
Log: log,
Platform: getPlatform(),
}
return osCommand, nil
}
// RunCommandWithOutput wrapper around commands returning their output and error
func (c *OSCommand) RunCommandWithOutput(command string) (string, error) {
c.Log.WithField("command", command).Info("RunCommand")
splitCmd := str.ToArgv(command)
c.Log.Info(splitCmd)
cmdOut, err := exec.Command(splitCmd[0], splitCmd[1:]...).CombinedOutput()
return sanitisedCommandOutput(cmdOut, err)
}
// RunCommand runs a command and just returns the error
func (c *OSCommand) RunCommand(command string) error {
_, err := c.RunCommandWithOutput(command)
return err
}
// RunDirectCommand wrapper around direct commands
func (c *OSCommand) RunDirectCommand(command string) (string, error) {
c.Log.WithField("command", command).Info("RunDirectCommand")
args := str.ToArgv(c.Platform.shellArg + " " + command)
c.Log.Info(spew.Sdump(args))
cmdOut, err := exec.
Command(c.Platform.shell, args...).
CombinedOutput()
return sanitisedCommandOutput(cmdOut, err)
}
func sanitisedCommandOutput(output []byte, err error) (string, error) {
outputString := string(output)
if err != nil {
// errors like 'exit status 1' are not very useful so we'll create an error
// from the combined output
return outputString, errors.New(outputString)
}
return outputString, nil
}
func getPlatform() *Platform {
switch runtime.GOOS {
case "windows":
return &Platform{
os: "windows",
shell: "cmd",
shellArg: "/c",
escapedQuote: "\\\"",
}
default:
return &Platform{
os: runtime.GOOS,
shell: "bash",
shellArg: "-c",
escapedQuote: "\"",
}
}
}
// GetOpenCommand get open command
func (c *OSCommand) GetOpenCommand() (string, string, error) {
//NextStep open equivalents: xdg-open (linux), cygstart (cygwin), open (OSX)
trailMap := map[string]string{
"xdg-open": " &>/dev/null &",
"cygstart": "",
"open": "",
}
for name, trail := range trailMap {
if err := c.RunCommand("which " + name); err == nil {
return name, trail, nil
}
}
return "", "", errors.New("Unsure what command to use to open this file")
}
// VsCodeOpenFile opens the file in code, with the -r flag to open in the
// current window
// each of these open files needs to have the same function signature because
// they're being passed as arguments into another function,
// but only editFile actually returns a *exec.Cmd
func (c *OSCommand) VsCodeOpenFile(filename string) (*exec.Cmd, error) {
return nil, c.RunCommand("code -r " + filename)
}
// SublimeOpenFile opens the filein sublime
// may be deprecated in the future
func (c *OSCommand) SublimeOpenFile(filename string) (*exec.Cmd, error) {
return nil, c.RunCommand("subl " + filename)
}
// OpenFile opens a file with the given
func (c *OSCommand) OpenFile(filename string) (*exec.Cmd, error) {
cmdName, cmdTrail, err := c.GetOpenCommand()
if err != nil {
return nil, err
}
err = c.RunCommand(cmdName + " " + c.Quote(filename) + cmdTrail) // TODO: test on linux
return nil, err
}
// EditFile opens a file in a subprocess using whatever editor is available,
// falling back to core.editor, VISUAL, EDITOR, then vi
func (c *OSCommand) EditFile(filename string) (*exec.Cmd, error) {
editor, _ := gitconfig.Global("core.editor")
if editor == "" {
editor = os.Getenv("VISUAL")
}
if editor == "" {
editor = os.Getenv("EDITOR")
}
if editor == "" {
if err := c.RunCommand("which vi"); err == nil {
editor = "vi"
}
}
if editor == "" {
return nil, errors.New("No editor defined in $VISUAL, $EDITOR, or git config")
}
return c.PrepareSubProcess(editor, filename)
}
// PrepareSubProcess iniPrepareSubProcessrocess then tells the Gui to switch to it
func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) (*exec.Cmd, error) {
subprocess := exec.Command(cmdName, commandArgs...)
return subprocess, nil
}
// Quote wraps a message in platform-specific quotation marks
func (c *OSCommand) Quote(message string) string {
message = strings.Replace(message, "`", "\\`", -1)
return c.Platform.escapedQuote + message + c.Platform.escapedQuote
}
// Unquote removes wrapping quotations marks if they are present
// this is needed for removing quotes from staged filenames with spaces
func (c *OSCommand) Unquote(message string) string {
message = strings.Replace(message, `"`, "", -1)
return message
}
+16
View File
@@ -0,0 +1,16 @@
package commands
import "testing"
func TestQuote(t *testing.T) {
osCommand := &OSCommand{
Log: nil,
Platform: getPlatform(),
}
test := "hello `test`"
expected := osCommand.Platform.escapedQuote + "hello \\`test\\`" + osCommand.Platform.escapedQuote
test = osCommand.Quote(test)
if test != expected {
t.Error("Expected " + expected + ", got " + test)
}
}
+176
View File
@@ -0,0 +1,176 @@
package config
import (
"bytes"
"github.com/shibukawa/configdir"
"github.com/spf13/viper"
)
// AppConfig contains the base configuration fields required for lazygit.
type AppConfig struct {
Debug bool `long:"debug" env:"DEBUG" default:"false"`
Version string `long:"version" env:"VERSION" default:"unversioned"`
Commit string `long:"commit" env:"COMMIT"`
BuildDate string `long:"build-date" env:"BUILD_DATE"`
Name string `long:"name" env:"NAME" default:"lazygit"`
UserConfig *viper.Viper
}
// AppConfigurer interface allows individual app config structs to inherit Fields
// from AppConfig and still be used by lazygit.
type AppConfigurer interface {
GetDebug() bool
GetVersion() string
GetCommit() string
GetBuildDate() string
GetName() string
GetUserConfig() *viper.Viper
InsertToUserConfig(string, string) error
}
// NewAppConfig makes a new app config
func NewAppConfig(name, version, commit, date string, debuggingFlag *bool) (*AppConfig, error) {
userConfig, err := LoadUserConfig()
if err != nil {
panic(err)
}
appConfig := &AppConfig{
Name: "lazygit",
Version: version,
Commit: commit,
BuildDate: date,
Debug: *debuggingFlag,
UserConfig: userConfig,
}
return appConfig, nil
}
// GetDebug returns debug flag
func (c *AppConfig) GetDebug() bool {
return c.Debug
}
// GetVersion returns debug flag
func (c *AppConfig) GetVersion() string {
return c.Version
}
// GetCommit returns debug flag
func (c *AppConfig) GetCommit() string {
return c.Commit
}
// GetBuildDate returns debug flag
func (c *AppConfig) GetBuildDate() string {
return c.BuildDate
}
// GetName returns debug flag
func (c *AppConfig) GetName() string {
return c.Name
}
// GetUserConfig returns the user config
func (c *AppConfig) GetUserConfig() *viper.Viper {
return c.UserConfig
}
func newViper() (*viper.Viper, error) {
v := viper.New()
v.SetConfigType("yaml")
v.SetConfigName("config")
return v, nil
}
// LoadUserConfig gets the user's config
func LoadUserConfig() (*viper.Viper, error) {
v, err := newViper()
if err != nil {
panic(err)
}
if err = LoadDefaultConfig(v); err != nil {
return nil, err
}
if err = LoadUserConfigFromFile(v); err != nil {
return nil, err
}
return v, nil
}
// LoadDefaultConfig loads in the defaults defined in this file
func LoadDefaultConfig(v *viper.Viper) error {
defaults := getDefaultConfig()
return v.ReadConfig(bytes.NewBuffer(defaults))
}
// LoadUserConfigFromFile Loads the user config from their config file, creating
// the file as an empty config if it does not exist
func LoadUserConfigFromFile(v *viper.Viper) error {
// chucking my name there is not for vanity purposes, the xdg spec (and that
// function) requires a vendor name. May as well line up with github
configDirs := configdir.New("jesseduffield", "lazygit")
folder := configDirs.QueryFolderContainsFile("config.yml")
if folder == nil {
// create the file as an empty config and load it
folders := configDirs.QueryFolders(configdir.Global)
if err := folders[0].WriteFile("config.yml", []byte{}); err != nil {
return err
}
folder = configDirs.QueryFolderContainsFile("config.yml")
}
v.AddConfigPath(folder.Path)
if err := v.MergeInConfig(); err != nil {
return err
}
return nil
}
// InsertToUserConfig adds a key/value pair to the user's config and saves it
func (c *AppConfig) InsertToUserConfig(key, value string) error {
// making a new viper object so that we're not writing any defaults back
// to the user's config file
v, err := newViper()
if err != nil {
return err
}
if err = LoadUserConfigFromFile(v); err != nil {
return err
}
v.Set(key, value)
if err := v.WriteConfig(); err != nil {
return err
}
return nil
}
func getDefaultConfig() []byte {
return []byte(`
gui:
## stuff relating to the UI
scrollHeight: 2
theme:
activeBorderColor:
- white
- bold
inactiveBorderColor:
- white
optionsTextColor:
- blue
git:
# stuff relating to git
os:
# stuff relating to the OS
`)
}
// // commenting this out until we use it again
// func homeDirectory() string {
// usr, err := user.Current()
// if err != nil {
// log.Fatal(err)
// }
// return usr.HomeDir
// }
@@ -1,9 +1,14 @@
package main
package git
import (
"regexp"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/Sirupsen/logrus"
"gopkg.in/src-d/go-git.v4/plumbing"
)
@@ -15,53 +20,67 @@ import (
// our safe branches, then add the remaining safe branches, ensuring uniqueness
// along the way
type branchListBuilder struct{}
func newBranchListBuilder() *branchListBuilder {
return &branchListBuilder{}
// BranchListBuilder returns a list of Branch objects for the current repo
type BranchListBuilder struct {
Log *logrus.Logger
GitCommand *commands.GitCommand
}
func (b *branchListBuilder) obtainCurrentBranch() Branch {
// NewBranchListBuilder builds a new branch list builder
func NewBranchListBuilder(log *logrus.Logger, gitCommand *commands.GitCommand) (*BranchListBuilder, error) {
return &BranchListBuilder{
Log: log,
GitCommand: gitCommand,
}, nil
}
func (b *BranchListBuilder) obtainCurrentBranch() commands.Branch {
// I used go-git for this, but that breaks if you've just done a git init,
// even though you're on 'master'
branchName, _ := runDirectCommand("git symbolic-ref --short HEAD")
return Branch{Name: strings.TrimSpace(branchName), Recency: " *"}
branchName, err := b.GitCommand.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD")
if err != nil {
branchName, err = b.GitCommand.OSCommand.RunCommandWithOutput("git rev-parse --short HEAD")
if err != nil {
panic(err.Error())
}
}
return commands.Branch{Name: strings.TrimSpace(branchName), Recency: " *"}
}
func (*branchListBuilder) obtainReflogBranches() []Branch {
branches := make([]Branch, 0)
rawString, err := runDirectCommand("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD")
func (b *BranchListBuilder) obtainReflogBranches() []commands.Branch {
branches := make([]commands.Branch, 0)
rawString, err := b.GitCommand.OSCommand.RunCommandWithOutput("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD")
if err != nil {
return branches
}
branchLines := splitLines(rawString)
branchLines := utils.SplitLines(rawString)
for _, line := range branchLines {
timeNumber, timeUnit, branchName := branchInfoFromLine(line)
timeUnit = abbreviatedTimeUnit(timeUnit)
branch := Branch{Name: branchName, Recency: timeNumber + timeUnit}
branch := commands.Branch{Name: branchName, Recency: timeNumber + timeUnit}
branches = append(branches, branch)
}
return branches
}
func (b *branchListBuilder) obtainSafeBranches() []Branch {
branches := make([]Branch, 0)
func (b *BranchListBuilder) obtainSafeBranches() []commands.Branch {
branches := make([]commands.Branch, 0)
bIter, err := r.Branches()
bIter, err := b.GitCommand.Repo.Branches()
if err != nil {
panic(err)
}
err = bIter.ForEach(func(b *plumbing.Reference) error {
name := b.Name().Short()
branches = append(branches, Branch{Name: name})
branches = append(branches, commands.Branch{Name: name})
return nil
})
return branches
}
func (b *branchListBuilder) appendNewBranches(finalBranches, newBranches, existingBranches []Branch, included bool) []Branch {
func (b *BranchListBuilder) appendNewBranches(finalBranches, newBranches, existingBranches []commands.Branch, included bool) []commands.Branch {
for _, newBranch := range newBranches {
if included == branchIncluded(newBranch.Name, existingBranches) {
finalBranches = append(finalBranches, newBranch)
@@ -70,7 +89,7 @@ func (b *branchListBuilder) appendNewBranches(finalBranches, newBranches, existi
return finalBranches
}
func sanitisedReflogName(reflogBranch Branch, safeBranches []Branch) string {
func sanitisedReflogName(reflogBranch commands.Branch, safeBranches []commands.Branch) string {
for _, safeBranch := range safeBranches {
if strings.ToLower(safeBranch.Name) == strings.ToLower(reflogBranch.Name) {
return safeBranch.Name
@@ -79,15 +98,16 @@ func sanitisedReflogName(reflogBranch Branch, safeBranches []Branch) string {
return reflogBranch.Name
}
func (b *branchListBuilder) build() []Branch {
branches := make([]Branch, 0)
// Build the list of branches for the current repo
func (b *BranchListBuilder) Build() []commands.Branch {
branches := make([]commands.Branch, 0)
head := b.obtainCurrentBranch()
safeBranches := b.obtainSafeBranches()
if len(safeBranches) == 0 {
return append(branches, head)
}
reflogBranches := b.obtainReflogBranches()
reflogBranches = uniqueByName(append([]Branch{head}, reflogBranches...))
reflogBranches = uniqueByName(append([]commands.Branch{head}, reflogBranches...))
for i, reflogBranch := range reflogBranches {
reflogBranches[i].Name = sanitisedReflogName(reflogBranch, safeBranches)
}
@@ -98,8 +118,17 @@ func (b *branchListBuilder) build() []Branch {
return branches
}
func uniqueByName(branches []Branch) []Branch {
finalBranches := make([]Branch, 0)
func branchIncluded(branchName string, branches []commands.Branch) bool {
for _, existingBranch := range branches {
if strings.ToLower(existingBranch.Name) == strings.ToLower(branchName) {
return true
}
}
return false
}
func uniqueByName(branches []commands.Branch) []commands.Branch {
finalBranches := make([]commands.Branch, 0)
for _, branch := range branches {
if branchIncluded(branch.Name, finalBranches) {
continue
@@ -115,7 +144,7 @@ func branchInfoFromLine(line string) (string, string, string) {
r := regexp.MustCompile("\\|.*\\s")
line = r.ReplaceAllString(line, " ")
words := strings.Split(line, " ")
return words[0], words[1], words[3]
return words[0], words[1], words[len(words)-1]
}
func abbreviatedTimeUnit(timeUnit string) string {
+156
View File
@@ -0,0 +1,156 @@
package gui
import (
"fmt"
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/git"
)
func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error {
index := gui.getItemPosition(v)
if index == 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("AlreadyCheckedOutBranch"))
}
branch := gui.getSelectedBranch(v)
if err := gui.GitCommand.Checkout(branch.Name, false); err != nil {
gui.createErrorPanel(g, err.Error())
}
return gui.refreshSidePanels(g)
}
func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
branch := gui.getSelectedBranch(v)
message := gui.Tr.SLocalize("SureForceCheckout")
title := gui.Tr.SLocalize("ForceCheckoutBranch")
return gui.createConfirmationPanel(g, v, title, message, func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.Checkout(branch.Name, true); err != nil {
gui.createErrorPanel(g, err.Error())
}
return gui.refreshSidePanels(g)
}, nil)
}
func (gui *Gui) handleCheckoutByName(g *gocui.Gui, v *gocui.View) error {
gui.createPromptPanel(g, v, gui.Tr.SLocalize("BranchName")+":", func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.Checkout(gui.trimmedContent(v), false); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return gui.refreshSidePanels(g)
})
return nil
}
func (gui *Gui) handleNewBranch(g *gocui.Gui, v *gocui.View) error {
branch := gui.State.Branches[0]
message := gui.Tr.TemplateLocalize(
"NewBranchNameBranchOff",
Teml{
"branchName": branch.Name,
},
)
gui.createPromptPanel(g, v, message, func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.NewBranch(gui.trimmedContent(v)); err != nil {
return gui.createErrorPanel(g, err.Error())
}
gui.refreshSidePanels(g)
return gui.handleBranchSelect(g, v)
})
return nil
}
func (gui *Gui) handleDeleteBranch(g *gocui.Gui, v *gocui.View) error {
checkedOutBranch := gui.State.Branches[0]
selectedBranch := gui.getSelectedBranch(v)
if checkedOutBranch.Name == selectedBranch.Name {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantDeleteCheckOutBranch"))
}
message := gui.Tr.TemplateLocalize(
"DeleteBranchMessage",
Teml{
"selectedBranchName": selectedBranch.Name,
},
)
title := gui.Tr.SLocalize("DeleteBranch")
return gui.createConfirmationPanel(g, v, title, message, func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.DeleteBranch(selectedBranch.Name); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return gui.refreshSidePanels(g)
}, nil)
}
func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error {
checkedOutBranch := gui.State.Branches[0]
selectedBranch := gui.getSelectedBranch(v)
defer gui.refreshSidePanels(g)
if checkedOutBranch.Name == selectedBranch.Name {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantMergeBranchIntoItself"))
}
if err := gui.GitCommand.Merge(selectedBranch.Name); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return nil
}
func (gui *Gui) getSelectedBranch(v *gocui.View) commands.Branch {
lineNumber := gui.getItemPosition(v)
return gui.State.Branches[lineNumber]
}
func (gui *Gui) renderBranchesOptions(g *gocui.Gui) error {
return gui.renderOptionsMap(g, map[string]string{
"space": gui.Tr.SLocalize("checkout"),
"f": gui.Tr.SLocalize("forceCheckout"),
"m": gui.Tr.SLocalize("merge"),
"c": gui.Tr.SLocalize("checkoutByName"),
"n": gui.Tr.SLocalize("newBranch"),
"d": gui.Tr.SLocalize("deleteBranch"),
"← → ↑ ↓": gui.Tr.SLocalize("navigate"),
})
}
// may want to standardise how these select methods work
func (gui *Gui) handleBranchSelect(g *gocui.Gui, v *gocui.View) error {
if err := gui.renderBranchesOptions(g); err != nil {
return err
}
// This really shouldn't happen: there should always be a master branch
if len(gui.State.Branches) == 0 {
return gui.renderString(g, "main", gui.Tr.SLocalize("NoBranchesThisRepo"))
}
go func() {
branch := gui.getSelectedBranch(v)
diff, err := gui.GitCommand.GetBranchGraph(branch.Name)
if err != nil && strings.HasPrefix(diff, "fatal: ambiguous argument") {
diff = gui.Tr.SLocalize("NoTrackingThisBranch")
}
gui.renderString(g, "main", diff)
}()
return nil
}
// gui.refreshStatus is called at the end of this because that's when we can
// be sure there is a state.Branches array to pick the current branch from
func (gui *Gui) refreshBranches(g *gocui.Gui) error {
g.Update(func(g *gocui.Gui) error {
v, err := g.View("branches")
if err != nil {
panic(err)
}
builder, err := git.NewBranchListBuilder(gui.Log, gui.GitCommand)
if err != nil {
return err
}
gui.State.Branches = builder.Build()
v.Clear()
for _, branch := range gui.State.Branches {
fmt.Fprintln(v, branch.GetDisplayString())
}
gui.resetOrigin(v)
return gui.refreshStatus(g)
})
return nil
}
+59
View File
@@ -0,0 +1,59 @@
package gui
import (
"github.com/jesseduffield/gocui"
)
func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
message := gui.trimmedContent(v)
if message == "" {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CommitWithoutMessageErr"))
}
sub, err := gui.GitCommand.Commit(g, message)
if err != nil {
// TODO need to find a way to send through this error
if err != gui.Errors.ErrSubProcess {
return gui.createErrorPanel(g, err.Error())
}
}
if sub != nil {
gui.SubProcess = sub
return gui.Errors.ErrSubProcess
}
gui.refreshFiles(g)
v.Clear()
v.SetCursor(0, 0)
g.SetViewOnBottom("commitMessage")
gui.switchFocus(g, v, gui.getFilesView(g))
return gui.refreshCommits(g)
}
func (gui *Gui) handleCommitClose(g *gocui.Gui, v *gocui.View) error {
g.SetViewOnBottom("commitMessage")
return gui.switchFocus(g, v, gui.getFilesView(g))
}
func (gui *Gui) handleNewlineCommitMessage(g *gocui.Gui, v *gocui.View) error {
// resising ahead of time so that the top line doesn't get hidden to make
// room for the cursor on the second line
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, v.Buffer())
if _, err := g.SetView("commitMessage", x0, y0, x1, y1+1, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
}
v.EditNewLine()
return nil
}
func (gui *Gui) handleCommitFocused(g *gocui.Gui, v *gocui.View) error {
message := gui.Tr.TemplateLocalize(
"CloseConfirm",
Teml{
"keyBindClose": "esc",
"keyBindConfirm": "enter",
},
)
return gui.renderString(g, "options", message)
}
+172
View File
@@ -0,0 +1,172 @@
package gui
import (
"errors"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
)
func (gui *Gui) refreshCommits(g *gocui.Gui) error {
g.Update(func(*gocui.Gui) error {
gui.State.Commits = gui.GitCommand.GetCommits()
v, err := g.View("commits")
if err != nil {
panic(err)
}
v.Clear()
red := color.New(color.FgRed)
yellow := color.New(color.FgYellow)
white := color.New(color.FgWhite)
shaColor := white
for _, commit := range gui.State.Commits {
if commit.Pushed {
shaColor = red
} else {
shaColor = yellow
}
shaColor.Fprint(v, commit.Sha+" ")
white.Fprintln(v, commit.Name)
}
gui.refreshStatus(g)
if g.CurrentView().Name() == "commits" {
gui.handleCommitSelect(g, v)
}
return nil
})
return nil
}
func (gui *Gui) handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error {
return gui.createConfirmationPanel(g, commitView, gui.Tr.SLocalize("ResetToCommit"), gui.Tr.SLocalize("SureResetThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
commit, err := gui.getSelectedCommit(g)
if err != nil {
panic(err)
}
if err := gui.GitCommand.ResetToCommit(commit.Sha); err != nil {
return gui.createErrorPanel(g, err.Error())
}
if err := gui.refreshCommits(g); err != nil {
panic(err)
}
if err := gui.refreshFiles(g); err != nil {
panic(err)
}
gui.resetOrigin(commitView)
return gui.handleCommitSelect(g, nil)
}, nil)
}
func (gui *Gui) renderCommitsOptions(g *gocui.Gui) error {
return gui.renderOptionsMap(g, map[string]string{
"s": gui.Tr.SLocalize("squashDown"),
"r": gui.Tr.SLocalize("rename"),
"g": gui.Tr.SLocalize("resetToThisCommit"),
"f": gui.Tr.SLocalize("fixupCommit"),
"← → ↑ ↓": gui.Tr.SLocalize("navigate"),
})
}
func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
if err := gui.renderCommitsOptions(g); err != nil {
return err
}
commit, err := gui.getSelectedCommit(g)
if err != nil {
if err.Error() != gui.Tr.SLocalize("NoCommitsThisBranch") {
return err
}
return gui.renderString(g, "main", gui.Tr.SLocalize("NoCommitsThisBranch"))
}
commitText := gui.GitCommand.Show(commit.Sha)
return gui.renderString(g, "main", commitText)
}
func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
if gui.getItemPosition(v) != 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlySquashTopmostCommit"))
}
if len(gui.State.Commits) == 1 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash"))
}
commit, err := gui.getSelectedCommit(g)
if err != nil {
return err
}
if err := gui.GitCommand.SquashPreviousTwoCommits(commit.Name); err != nil {
return gui.createErrorPanel(g, err.Error())
}
if err := gui.refreshCommits(g); err != nil {
panic(err)
}
gui.refreshStatus(g)
return gui.handleCommitSelect(g, v)
}
// TODO: move to files panel
func (gui *Gui) anyUnStagedChanges(files []commands.File) bool {
for _, file := range files {
if file.Tracked && file.HasUnstagedChanges {
return true
}
}
return false
}
func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
if len(gui.State.Commits) == 1 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash"))
}
if gui.anyUnStagedChanges(gui.State.Files) {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantFixupWhileUnstagedChanges"))
}
branch := gui.State.Branches[0]
commit, err := gui.getSelectedCommit(g)
if err != nil {
return err
}
message := gui.Tr.SLocalize("SureFixupThisCommit")
gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("Fixup"), message, func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.SquashFixupCommit(branch.Name, commit.Sha); err != nil {
return gui.createErrorPanel(g, err.Error())
}
if err := gui.refreshCommits(g); err != nil {
panic(err)
}
return gui.refreshStatus(g)
}, nil)
return nil
}
func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
if gui.getItemPosition(v) != 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlyRenameTopCommit"))
}
gui.createPromptPanel(g, v, gui.Tr.SLocalize("RenameCommit"), func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.RenameCommit(v.Buffer()); err != nil {
return gui.createErrorPanel(g, err.Error())
}
if err := gui.refreshCommits(g); err != nil {
panic(err)
}
return gui.handleCommitSelect(g, v)
})
return nil
}
func (gui *Gui) getSelectedCommit(g *gocui.Gui) (commands.Commit, error) {
v, err := g.View("commits")
if err != nil {
panic(err)
}
if len(gui.State.Commits) == 0 {
return commands.Commit{}, errors.New(gui.Tr.SLocalize("NoCommitsThisBranch"))
}
lineNumber := gui.getItemPosition(v)
if lineNumber > len(gui.State.Commits)-1 {
gui.Log.Info(gui.Tr.SLocalize("PotentialErrInGetselectedCommit"), gui.State.Commits, lineNumber)
return gui.State.Commits[len(gui.State.Commits)-1], nil
}
return gui.State.Commits[lineNumber], nil
}
+167
View File
@@ -0,0 +1,167 @@
// lots of this has been directly ported from one of the example files, will brush up later
// Copyright 2014 The gocui Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gui
import (
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.View) error) func(*gocui.Gui, *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
if function != nil {
if err := function(g, v); err != nil {
panic(err)
}
}
return gui.closeConfirmationPrompt(g)
}
}
func (gui *Gui) closeConfirmationPrompt(g *gocui.Gui) error {
view, err := g.View("confirmation")
if err != nil {
panic(err)
}
if err := gui.returnFocus(g, view); err != nil {
panic(err)
}
g.DeleteKeybindings("confirmation")
return g.DeleteView("confirmation")
}
func (gui *Gui) getMessageHeight(message string, width int) int {
lines := strings.Split(message, "\n")
lineCount := 0
for _, line := range lines {
lineCount += len(line)/width + 1
}
return lineCount
}
func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, prompt string) (int, int, int, int) {
width, height := g.Size()
panelWidth := width / 2
panelHeight := gui.getMessageHeight(prompt, panelWidth)
return width/2 - panelWidth/2,
height/2 - panelHeight/2 - panelHeight%2 - 1,
width/2 + panelWidth/2,
height/2 + panelHeight/2
}
func (gui *Gui) createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, handleConfirm func(*gocui.Gui, *gocui.View) error) error {
gui.onNewPopupPanel()
// only need to fit one line
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, "")
if confirmationView, err := g.SetView("confirmation", x0, y0, x1, y1, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
confirmationView.Editable = true
confirmationView.Title = title
confirmationView.FgColor = gocui.ColorWhite
gui.switchFocus(g, currentView, confirmationView)
return gui.setKeyBindings(g, handleConfirm, nil)
}
return nil
}
func (gui *Gui) onNewPopupPanel() {
gui.g.SetViewOnBottom("commitMessage")
}
func (gui *Gui) createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
gui.onNewPopupPanel()
g.Update(func(g *gocui.Gui) error {
// delete the existing confirmation panel if it exists
if view, _ := g.View("confirmation"); view != nil {
if err := gui.closeConfirmationPrompt(g); err != nil {
errMessage := gui.Tr.TemplateLocalize(
"CantCloseConfirmationPrompt",
Teml{
"error": err.Error(),
},
)
gui.Log.Error(errMessage)
}
}
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, prompt)
if confirmationView, err := g.SetView("confirmation", x0, y0, x1, y1, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
confirmationView.Title = title
confirmationView.FgColor = gocui.ColorWhite
gui.renderString(g, "confirmation", prompt)
gui.switchFocus(g, currentView, confirmationView)
return gui.setKeyBindings(g, handleConfirm, handleClose)
}
return nil
})
return nil
}
func (gui *Gui) handleNewline(g *gocui.Gui, v *gocui.View) error {
// resising ahead of time so that the top line doesn't get hidden to make
// room for the cursor on the second line
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, v.Buffer())
if _, err := g.SetView("confirmation", x0, y0, x1, y1+1, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
}
v.EditNewLine()
return nil
}
func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
actions := gui.Tr.TemplateLocalize(
"CloseConfirm",
Teml{
"keyBindClose": "esc",
"keyBindConfirm": "enter",
},
)
gui.renderString(g, "options", actions)
if err := g.SetKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone, gui.wrappedConfirmationFunction(handleConfirm)); err != nil {
return err
}
if err := g.SetKeybinding("confirmation", gocui.KeyTab, gocui.ModNone, gui.handleNewline); err != nil {
return err
}
return g.SetKeybinding("confirmation", gocui.KeyEsc, gocui.ModNone, gui.wrappedConfirmationFunction(handleClose))
}
func (gui *Gui) createMessagePanel(g *gocui.Gui, currentView *gocui.View, title, prompt string) error {
return gui.createConfirmationPanel(g, currentView, title, prompt, nil, nil)
}
func (gui *Gui) createErrorPanel(g *gocui.Gui, message string) error {
currentView := g.CurrentView()
colorFunction := color.New(color.FgRed).SprintFunc()
coloredMessage := colorFunction(strings.TrimSpace(message))
return gui.createConfirmationPanel(g, currentView, gui.Tr.SLocalize("Error"), coloredMessage, nil, nil)
}
func (gui *Gui) resizePopupPanel(g *gocui.Gui, v *gocui.View) error {
// If the confirmation panel is already displayed, just resize the width,
// otherwise continue
content := utils.TrimTrailingNewline(v.Buffer())
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, content)
vx0, vy0, vx1, vy1 := v.Dimensions()
if vx0 == x0 && vy0 == y0 && vx1 == x1 && vy1 == y1 {
return nil
}
gui.Log.Info(gui.Tr.SLocalize("resizingPopupPanel"))
_, err := g.SetView(v.Name(), x0, y0, x1, y1, 0)
return err
}
+411
View File
@@ -0,0 +1,411 @@
package gui
import (
// "io"
// "io/ioutil"
// "strings"
"os/exec"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
)
func (gui *Gui) stagedFiles() []commands.File {
files := gui.State.Files
result := make([]commands.File, 0)
for _, file := range files {
if file.HasStagedChanges {
result = append(result, file)
}
}
return result
}
func (gui *Gui) trackedFiles() []commands.File {
files := gui.State.Files
result := make([]commands.File, 0)
for _, file := range files {
if file.Tracked {
result = append(result, file)
}
}
return result
}
func (gui *Gui) stageSelectedFile(g *gocui.Gui) error {
file, err := gui.getSelectedFile(g)
if err != nil {
return err
}
return gui.GitCommand.StageFile(file.Name)
}
func (gui *Gui) handleFilePress(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
if err == gui.Errors.ErrNoFiles {
return nil
}
return err
}
if file.HasMergeConflicts {
return gui.handleSwitchToMerge(g, v)
}
if file.HasUnstagedChanges {
gui.GitCommand.StageFile(file.Name)
} else {
gui.GitCommand.UnStageFile(file.Name, file.Tracked)
}
if err := gui.refreshFiles(g); err != nil {
return err
}
return gui.handleFileSelect(g, v)
}
func (gui *Gui) handleAddPatch(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
if err == gui.Errors.ErrNoFiles {
return nil
}
return err
}
if !file.HasUnstagedChanges {
return gui.createErrorPanel(g, gui.Tr.SLocalize("FileHasNoUnstagedChanges"))
}
if !file.Tracked {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CannotGitAdd"))
}
sub, err := gui.GitCommand.AddPatch(file.Name)
if err != nil {
return err
}
gui.SubProcess = sub
return gui.Errors.ErrSubProcess
}
func (gui *Gui) getSelectedFile(g *gocui.Gui) (commands.File, error) {
if len(gui.State.Files) == 0 {
return commands.File{}, gui.Errors.ErrNoFiles
}
filesView, err := g.View("files")
if err != nil {
panic(err)
}
lineNumber := gui.getItemPosition(filesView)
return gui.State.Files[lineNumber], nil
}
func (gui *Gui) handleFileRemove(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
if err == gui.Errors.ErrNoFiles {
return nil
}
return err
}
var deleteVerb string
if file.Tracked {
deleteVerb = gui.Tr.SLocalize("checkout")
} else {
deleteVerb = gui.Tr.SLocalize("delete")
}
message := gui.Tr.TemplateLocalize(
"SureTo",
Teml{
"deleteVerb": deleteVerb,
"fileName": file.Name,
},
)
return gui.createConfirmationPanel(g, v, strings.Title(deleteVerb)+" file", message, func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.RemoveFile(file); err != nil {
return err
}
return gui.refreshFiles(g)
}, nil)
}
func (gui *Gui) handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
return gui.createErrorPanel(g, err.Error())
}
if file.Tracked {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantIgnoreTrackFiles"))
}
gui.GitCommand.Ignore(file.Name)
return gui.refreshFiles(g)
}
func (gui *Gui) renderfilesOptions(g *gocui.Gui, file *commands.File) error {
optionsMap := map[string]string{
"← → ↑ ↓": gui.Tr.SLocalize("navigate"),
"S": gui.Tr.SLocalize("stashFiles"),
"c": gui.Tr.SLocalize("CommitChanges"),
"o": gui.Tr.SLocalize("open"),
"i": gui.Tr.SLocalize("ignore"),
"d": gui.Tr.SLocalize("delete"),
"space": gui.Tr.SLocalize("toggleStaged"),
"R": gui.Tr.SLocalize("refresh"),
"t": gui.Tr.SLocalize("addPatch"),
"e": gui.Tr.SLocalize("edit"),
"PgUp/PgDn": gui.Tr.SLocalize("scroll"),
}
if gui.State.HasMergeConflicts {
optionsMap["a"] = gui.Tr.SLocalize("abortMerge")
optionsMap["m"] = gui.Tr.SLocalize("resolveMergeConflicts")
}
if file == nil {
return gui.renderOptionsMap(g, optionsMap)
}
if file.Tracked {
optionsMap["d"] = gui.Tr.SLocalize("checkout")
}
return gui.renderOptionsMap(g, optionsMap)
}
func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
}
gui.renderString(g, "main", gui.Tr.SLocalize("NoChangedFiles"))
return gui.renderfilesOptions(g, nil)
}
gui.renderfilesOptions(g, &file)
var content string
if file.HasMergeConflicts {
return gui.refreshMergePanel(g)
}
content = gui.GitCommand.Diff(file)
return gui.renderString(g, "main", content)
}
func (gui *Gui) handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
if len(gui.stagedFiles()) == 0 && !gui.State.HasMergeConflicts {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit"))
}
commitMessageView := gui.getCommitMessageView(g)
g.Update(func(g *gocui.Gui) error {
g.SetViewOnTop("commitMessage")
gui.switchFocus(g, filesView, commitMessageView)
return nil
})
return nil
}
// handleCommitEditorPress - handle when the user wants to commit changes via
// their editor rather than via the popup panel
func (gui *Gui) handleCommitEditorPress(g *gocui.Gui, filesView *gocui.View) error {
if len(gui.stagedFiles()) == 0 && !gui.State.HasMergeConflicts {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit"))
}
gui.PrepareSubProcess(g, "git", "commit")
return nil
}
// PrepareSubProcess - prepare a subprocess for execution and tell the gui to switch to it
func (gui *Gui) PrepareSubProcess(g *gocui.Gui, commands ...string) error {
sub, err := gui.GitCommand.PrepareCommitSubProcess()
if err != nil {
return err
}
gui.SubProcess = sub
g.Update(func(g *gocui.Gui) error {
return gui.Errors.ErrSubProcess
})
return nil
}
func (gui *Gui) genericFileOpen(g *gocui.Gui, v *gocui.View, filename string, open func(string) (*exec.Cmd, error)) error {
sub, err := open(filename)
if err != nil {
return gui.createErrorPanel(g, err.Error())
}
if sub != nil {
gui.SubProcess = sub
return gui.Errors.ErrSubProcess
}
return nil
}
func (gui *Gui) handleFileEdit(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
return err
}
return gui.genericFileOpen(g, v, file.Name, gui.OSCommand.EditFile)
}
func (gui *Gui) handleFileOpen(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
return err
}
return gui.genericFileOpen(g, v, file.Name, gui.OSCommand.OpenFile)
}
func (gui *Gui) handleSublimeFileOpen(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
return err
}
return gui.genericFileOpen(g, v, file.Name, gui.OSCommand.SublimeOpenFile)
}
func (gui *Gui) handleVsCodeFileOpen(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
return err
}
return gui.genericFileOpen(g, v, file.Name, gui.OSCommand.VsCodeOpenFile)
}
func (gui *Gui) handleRefreshFiles(g *gocui.Gui, v *gocui.View) error {
return gui.refreshFiles(g)
}
func (gui *Gui) refreshStateFiles() {
// get files to stage
files := gui.GitCommand.GetStatusFiles()
gui.State.Files = gui.GitCommand.MergeStatusFiles(gui.State.Files, files)
gui.updateHasMergeConflictStatus()
}
func (gui *Gui) updateHasMergeConflictStatus() error {
merging, err := gui.GitCommand.IsInMergeState()
if err != nil {
return err
}
gui.State.HasMergeConflicts = merging
return nil
}
func (gui *Gui) renderFile(file commands.File, filesView *gocui.View) {
// potentially inefficient to be instantiating these color
// objects with each render
red := color.New(color.FgRed)
green := color.New(color.FgGreen)
if !file.Tracked && !file.HasStagedChanges {
red.Fprintln(filesView, file.DisplayString)
return
}
green.Fprint(filesView, file.DisplayString[0:1])
red.Fprint(filesView, file.DisplayString[1:3])
if file.HasUnstagedChanges {
red.Fprintln(filesView, file.Name)
} else {
green.Fprintln(filesView, file.Name)
}
}
func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) {
item, err := gui.getSelectedFile(g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
return "", err
}
return "", gui.renderString(g, "main", gui.Tr.SLocalize("NoFilesDisplay"))
}
cat, err := gui.GitCommand.CatFile(item.Name)
if err != nil {
panic(err)
}
return cat, nil
}
func (gui *Gui) refreshFiles(g *gocui.Gui) error {
filesView, err := g.View("files")
if err != nil {
return err
}
gui.refreshStateFiles()
filesView.Clear()
for _, file := range gui.State.Files {
gui.renderFile(file, filesView)
}
gui.correctCursor(filesView)
if filesView == g.CurrentView() {
gui.handleFileSelect(g, filesView)
}
return nil
}
func (gui *Gui) pullFiles(g *gocui.Gui, v *gocui.View) error {
gui.createMessagePanel(g, v, "", gui.Tr.SLocalize("PullWait"))
go func() {
if err := gui.GitCommand.Pull(); err != nil {
gui.createErrorPanel(g, err.Error())
} else {
gui.closeConfirmationPrompt(g)
gui.refreshCommits(g)
gui.refreshStatus(g)
}
gui.refreshFiles(g)
}()
return nil
}
func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error {
gui.createMessagePanel(g, v, "", gui.Tr.SLocalize("PushWait"))
go func() {
branchName := gui.State.Branches[0].Name
if err := gui.GitCommand.Push(branchName); err != nil {
gui.createErrorPanel(g, err.Error())
} else {
gui.closeConfirmationPrompt(g)
gui.refreshCommits(g)
gui.refreshStatus(g)
}
}()
return nil
}
func (gui *Gui) handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error {
mergeView, err := g.View("main")
if err != nil {
return err
}
file, err := gui.getSelectedFile(g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
}
return nil
}
if !file.HasMergeConflicts {
return gui.createErrorPanel(g, gui.Tr.SLocalize("FileNoMergeCons"))
}
gui.switchFocus(g, v, mergeView)
return gui.refreshMergePanel(g)
}
func (gui *Gui) handleAbortMerge(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.AbortMerge(); err != nil {
return gui.createErrorPanel(g, err.Error())
}
gui.createMessagePanel(g, v, "", gui.Tr.SLocalize("MergeAborted"))
gui.refreshStatus(g)
return gui.refreshFiles(g)
}
func (gui *Gui) handleResetHard(g *gocui.Gui, v *gocui.View) error {
return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("ClearFilePanel"), gui.Tr.SLocalize("SureResetHardHead"), func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.ResetHard(); err != nil {
gui.createErrorPanel(g, err.Error())
}
return gui.refreshFiles(g)
}, nil)
}
+367
View File
@@ -0,0 +1,367 @@
package gui
import (
// "io"
// "io/ioutil"
"errors"
"io/ioutil"
"log"
"os"
"os/exec"
"strings"
"time"
// "strings"
"github.com/Sirupsen/logrus"
"github.com/golang-collections/collections/stack"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n"
)
// OverlappingEdges determines if panel edges overlap
var OverlappingEdges = false
// SentinelErrors are the errors that have special meaning and need to be checked
// by calling functions. The less of these, the better
type SentinelErrors struct {
ErrSubProcess error
ErrNoFiles error
}
// GenerateSentinelErrors makes the sentinel errors for the gui. We're defining it here
// because we can't do package-scoped errors with localization, and also because
// it seems like package-scoped variables are bad in general
// https://dave.cheney.net/2017/06/11/go-without-package-scoped-variables
// In the future it would be good to implement some of the recommendations of
// that article. For now, if we don't need an error to be a sentinel, we will just
// define it inline. This has implications for error messages that pop up everywhere
// in that we'll be duplicating the default values. We may need to look at
// having a default localisation bundle defined, and just using keys-only when
// localising things in the code.
func (gui *Gui) GenerateSentinelErrors() {
gui.Errors = SentinelErrors{
ErrSubProcess: errors.New(gui.Tr.SLocalize("RunningSubprocess")),
ErrNoFiles: errors.New(gui.Tr.SLocalize("NoChangedFiles")),
}
}
// Teml is short for template used to make the required map[string]interface{} shorter when using gui.Tr.SLocalize and gui.Tr.TemplateLocalize
type Teml i18n.Teml
// Gui wraps the gocui Gui object which handles rendering and events
type Gui struct {
g *gocui.Gui
Log *logrus.Logger
GitCommand *commands.GitCommand
OSCommand *commands.OSCommand
SubProcess *exec.Cmd
State guiState
Config config.AppConfigurer
Tr *i18n.Localizer
Errors SentinelErrors
}
type guiState struct {
Files []commands.File
Branches []commands.Branch
Commits []commands.Commit
StashEntries []commands.StashEntry
PreviousView string
HasMergeConflicts bool
ConflictIndex int
ConflictTop bool
Conflicts []commands.Conflict
EditHistory *stack.Stack
Platform commands.Platform
}
// NewGui builds a new gui handler
func NewGui(log *logrus.Logger, gitCommand *commands.GitCommand, oSCommand *commands.OSCommand, tr *i18n.Localizer, config config.AppConfigurer) (*Gui, error) {
initialState := guiState{
Files: make([]commands.File, 0),
PreviousView: "files",
Commits: make([]commands.Commit, 0),
StashEntries: make([]commands.StashEntry, 0),
ConflictIndex: 0,
ConflictTop: true,
Conflicts: make([]commands.Conflict, 0),
EditHistory: stack.New(),
Platform: *oSCommand.Platform,
}
gui := &Gui{
Log: log,
GitCommand: gitCommand,
OSCommand: oSCommand,
State: initialState,
Config: config,
Tr: tr,
}
gui.GenerateSentinelErrors()
return gui, nil
}
func (gui *Gui) scrollUpMain(g *gocui.Gui, v *gocui.View) error {
mainView, _ := g.View("main")
ox, oy := mainView.Origin()
if oy >= 1 {
return mainView.SetOrigin(ox, oy-gui.Config.GetUserConfig().GetInt("gui.scrollHeight"))
}
return nil
}
func (gui *Gui) scrollDownMain(g *gocui.Gui, v *gocui.View) error {
mainView, _ := g.View("main")
ox, oy := mainView.Origin()
if oy < len(mainView.BufferLines()) {
return mainView.SetOrigin(ox, oy+gui.Config.GetUserConfig().GetInt("gui.scrollHeight"))
}
return nil
}
func (gui *Gui) handleRefresh(g *gocui.Gui, v *gocui.View) error {
return gui.refreshSidePanels(g)
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
// layout is called for every screen re-render e.g. when the screen is resized
func (gui *Gui) layout(g *gocui.Gui) error {
g.Highlight = true
width, height := g.Size()
leftSideWidth := width / 3
statusFilesBoundary := 2
filesBranchesBoundary := 2 * height / 5 // height - 20
commitsBranchesBoundary := 3 * height / 5 // height - 10
commitsStashBoundary := height - 5 // height - 5
minimumHeight := 16
minimumWidth := 10
version := gui.Config.GetVersion()
panelSpacing := 1
if OverlappingEdges {
panelSpacing = 0
}
if height < minimumHeight || width < minimumWidth {
v, err := g.SetView("limit", 0, 0, max(width-1, 2), max(height-1, 2), 0)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = gui.Tr.SLocalize("NotEnoughSpace")
v.Wrap = true
}
return nil
}
g.DeleteView("limit")
optionsTop := height - 2
// hiding options if there's not enough space
if height < 30 {
optionsTop = height - 1
}
v, err := g.SetView("main", leftSideWidth+panelSpacing, 0, width-1, optionsTop, gocui.LEFT)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = gui.Tr.SLocalize("DiffTitle")
v.Wrap = true
v.FgColor = gocui.ColorWhite
}
if v, err := g.SetView("status", 0, 0, leftSideWidth, statusFilesBoundary, gocui.BOTTOM|gocui.RIGHT); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = gui.Tr.SLocalize("StatusTitle")
v.FgColor = gocui.ColorWhite
}
filesView, err := g.SetView("files", 0, statusFilesBoundary+panelSpacing, leftSideWidth, filesBranchesBoundary, gocui.TOP|gocui.BOTTOM)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
filesView.Highlight = true
filesView.Title = gui.Tr.SLocalize("FilesTitle")
v.FgColor = gocui.ColorWhite
}
if v, err := g.SetView("branches", 0, filesBranchesBoundary+panelSpacing, leftSideWidth, commitsBranchesBoundary, gocui.TOP|gocui.BOTTOM); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = gui.Tr.SLocalize("BranchesTitle")
v.FgColor = gocui.ColorWhite
}
if v, err := g.SetView("commits", 0, commitsBranchesBoundary+panelSpacing, leftSideWidth, commitsStashBoundary, gocui.TOP|gocui.BOTTOM); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = gui.Tr.SLocalize("CommitsTitle")
v.FgColor = gocui.ColorWhite
}
if v, err := g.SetView("stash", 0, commitsStashBoundary+panelSpacing, leftSideWidth, optionsTop, gocui.TOP|gocui.RIGHT); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = gui.Tr.SLocalize("StashTitle")
v.FgColor = gocui.ColorWhite
}
if v, err := g.SetView("options", -1, optionsTop, width-len(version)-2, optionsTop+2, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Frame = false
if v.FgColor, err = gui.GetOptionsPanelTextColor(); err != nil {
return err
}
}
if gui.getCommitMessageView(g) == nil {
// doesn't matter where this view starts because it will be hidden
if commitMessageView, err := g.SetView("commitMessage", 0, 0, width, height, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
g.SetViewOnBottom("commitMessage")
commitMessageView.Title = gui.Tr.SLocalize("CommitMessage")
commitMessageView.FgColor = gocui.ColorWhite
commitMessageView.Editable = true
}
}
if v, err := g.SetView("version", width-len(version)-1, optionsTop, width, optionsTop+2, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.BgColor = gocui.ColorDefault
v.FgColor = gocui.ColorGreen
v.Frame = false
if err := gui.renderString(g, "version", version); err != nil {
return err
}
// these are only called once
gui.handleFileSelect(g, filesView)
gui.refreshFiles(g)
gui.refreshBranches(g)
gui.refreshCommits(g)
gui.refreshStashEntries(g)
if err := gui.switchFocus(g, nil, filesView); err != nil {
return err
}
}
gui.resizePopupPanels(g)
return nil
}
func (gui *Gui) fetch(g *gocui.Gui) error {
gui.GitCommand.Fetch()
gui.refreshStatus(g)
return nil
}
func (gui *Gui) updateLoader(g *gocui.Gui) error {
if confirmationView, _ := g.View("confirmation"); confirmationView != nil {
content := gui.trimmedContent(confirmationView)
if strings.Contains(content, "...") {
staticContent := strings.Split(content, "...")[0] + "..."
gui.renderString(g, "confirmation", staticContent+" "+gui.loader())
}
}
return nil
}
func (gui *Gui) goEvery(g *gocui.Gui, interval time.Duration, function func(*gocui.Gui) error) {
go func() {
for range time.Tick(interval) {
function(g)
}
}()
}
func (gui *Gui) resizePopupPanels(g *gocui.Gui) error {
v := g.CurrentView()
if v.Name() == "commitMessage" || v.Name() == "confirmation" {
return gui.resizePopupPanel(g, v)
}
return nil
}
// Run setup the gui with keybindings and start the mainloop
func (gui *Gui) Run() error {
g, err := gocui.NewGui(gocui.OutputNormal, OverlappingEdges)
if err != nil {
return err
}
defer g.Close()
gui.g = g // TODO: always use gui.g rather than passing g around everywhere
if err := gui.SetColorScheme(); err != nil {
return err
}
gui.goEvery(g, time.Second*60, gui.fetch)
gui.goEvery(g, time.Second*10, gui.refreshFiles)
gui.goEvery(g, time.Millisecond*10, gui.updateLoader)
g.SetManagerFunc(gui.layout)
if err = gui.keybindings(g); err != nil {
return err
}
err = g.MainLoop()
return err
}
// RunWithSubprocesses loops, instantiating a new gocui.Gui with each iteration
// if the error returned from a run is a ErrSubProcess, it runs the subprocess
// otherwise it handles the error, possibly by quitting the application
func (gui *Gui) RunWithSubprocesses() {
for {
if err := gui.Run(); err != nil {
if err == gocui.ErrQuit {
break
} else if err == gui.Errors.ErrSubProcess {
gui.SubProcess.Stdin = os.Stdin
gui.SubProcess.Stdout = os.Stdout
gui.SubProcess.Stderr = os.Stderr
gui.SubProcess.Run()
gui.SubProcess.Stdout = ioutil.Discard
gui.SubProcess.Stderr = ioutil.Discard
gui.SubProcess.Stdin = nil
gui.SubProcess = nil
} else {
log.Panicln(err)
}
}
}
}
func (gui *Gui) quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}
+66 -61
View File
@@ -1,4 +1,4 @@
package main
package gui
import "github.com/jesseduffield/gocui"
@@ -12,72 +12,77 @@ type Binding struct {
Modifier gocui.Modifier
}
func keybindings(g *gocui.Gui) error {
func (gui *Gui) keybindings(g *gocui.Gui) error {
bindings := []Binding{
{ViewName: "", Key: 'q', Modifier: gocui.ModNone, Handler: quit},
{ViewName: "", Key: gocui.KeyCtrlC, Modifier: gocui.ModNone, Handler: quit},
{ViewName: "", Key: gocui.KeyPgup, Modifier: gocui.ModNone, Handler: scrollUpMain},
{ViewName: "", Key: gocui.KeyPgdn, Modifier: gocui.ModNone, Handler: scrollDownMain},
{ViewName: "", Key: 'P', Modifier: gocui.ModNone, Handler: pushFiles},
{ViewName: "", Key: 'p', Modifier: gocui.ModNone, Handler: pullFiles},
{ViewName: "", Key: 'R', Modifier: gocui.ModNone, Handler: handleRefresh},
{ViewName: "files", Key: 'c', Modifier: gocui.ModNone, Handler: handleCommitPress},
{ViewName: "files", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleFilePress},
{ViewName: "files", Key: 'd', Modifier: gocui.ModNone, Handler: handleFileRemove},
{ViewName: "files", Key: 'm', Modifier: gocui.ModNone, Handler: handleSwitchToMerge},
{ViewName: "files", Key: 'e', Modifier: gocui.ModNone, Handler: handleFileEdit},
{ViewName: "files", Key: 'o', Modifier: gocui.ModNone, Handler: handleFileOpen},
{ViewName: "files", Key: 's', Modifier: gocui.ModNone, Handler: handleSublimeFileOpen},
{ViewName: "files", Key: 'v', Modifier: gocui.ModNone, Handler: handleVsCodeFileOpen},
{ViewName: "files", Key: 'i', Modifier: gocui.ModNone, Handler: handleIgnoreFile},
{ViewName: "files", Key: 'r', Modifier: gocui.ModNone, Handler: handleRefreshFiles},
{ViewName: "files", Key: 'S', Modifier: gocui.ModNone, Handler: handleStashSave},
{ViewName: "files", Key: 'a', Modifier: gocui.ModNone, Handler: handleAbortMerge},
{ViewName: "files", Key: 't', Modifier: gocui.ModNone, Handler: handleAddPatch},
{ViewName: "files", Key: 'D', Modifier: gocui.ModNone, Handler: handleResetHard},
{ViewName: "main", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: handleEscapeMerge},
{ViewName: "main", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handlePickHunk},
{ViewName: "main", Key: 'b', Modifier: gocui.ModNone, Handler: handlePickBothHunks},
{ViewName: "main", Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: handleSelectPrevConflict},
{ViewName: "main", Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: handleSelectNextConflict},
{ViewName: "main", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: handleSelectTop},
{ViewName: "main", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: handleSelectBottom},
{ViewName: "main", Key: 'h', Modifier: gocui.ModNone, Handler: handleSelectPrevConflict},
{ViewName: "main", Key: 'l', Modifier: gocui.ModNone, Handler: handleSelectNextConflict},
{ViewName: "main", Key: 'k', Modifier: gocui.ModNone, Handler: handleSelectTop},
{ViewName: "main", Key: 'j', Modifier: gocui.ModNone, Handler: handleSelectBottom},
{ViewName: "main", Key: 'z', Modifier: gocui.ModNone, Handler: handlePopFileSnapshot},
{ViewName: "branches", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleBranchPress},
{ViewName: "branches", Key: 'c', Modifier: gocui.ModNone, Handler: handleCheckoutByName},
{ViewName: "branches", Key: 'F', Modifier: gocui.ModNone, Handler: handleForceCheckout},
{ViewName: "branches", Key: 'n', Modifier: gocui.ModNone, Handler: handleNewBranch},
{ViewName: "branches", Key: 'd', Modifier: gocui.ModNone, Handler: handleDeleteBranch},
{ViewName: "branches", Key: 'm', Modifier: gocui.ModNone, Handler: handleMerge},
{ViewName: "commits", Key: 's', Modifier: gocui.ModNone, Handler: handleCommitSquashDown},
{ViewName: "commits", Key: 'r', Modifier: gocui.ModNone, Handler: handleRenameCommit},
{ViewName: "commits", Key: 'g', Modifier: gocui.ModNone, Handler: handleResetToCommit},
{ViewName: "commits", Key: 'f', Modifier: gocui.ModNone, Handler: handleCommitFixup},
{ViewName: "stash", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleStashApply},
{ViewName: "stash", Key: 'g', Modifier: gocui.ModNone, Handler: handleStashPop},
{ViewName: "stash", Key: 'd', Modifier: gocui.ModNone, Handler: handleStashDrop},
{ViewName: "commitMessage", Key: gocui.KeyEnter, Modifier: gocui.ModNone, Handler: handleCommitConfirm},
{ViewName: "commitMessage", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: handleCommitClose},
{ViewName: "commitMessage", Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: handleNewlineCommitMessage},
{ViewName: "", Key: 'q', Modifier: gocui.ModNone, Handler: gui.quit},
{ViewName: "", Key: gocui.KeyCtrlC, Modifier: gocui.ModNone, Handler: gui.quit},
{ViewName: "", Key: gocui.KeyPgup, Modifier: gocui.ModNone, Handler: gui.scrollUpMain},
{ViewName: "", Key: gocui.KeyPgdn, Modifier: gocui.ModNone, Handler: gui.scrollDownMain},
{ViewName: "", Key: gocui.KeyCtrlU, Modifier: gocui.ModNone, Handler: gui.scrollUpMain},
{ViewName: "", Key: gocui.KeyCtrlD, Modifier: gocui.ModNone, Handler: gui.scrollDownMain},
{ViewName: "", Key: 'P', Modifier: gocui.ModNone, Handler: gui.pushFiles},
{ViewName: "", Key: 'p', Modifier: gocui.ModNone, Handler: gui.pullFiles},
{ViewName: "", Key: 'R', Modifier: gocui.ModNone, Handler: gui.handleRefresh},
{ViewName: "status", Key: 'e', Modifier: gocui.ModNone, Handler: gui.handleEditConfig},
{ViewName: "status", Key: 'o', Modifier: gocui.ModNone, Handler: gui.handleOpenConfig},
{ViewName: "files", Key: 'c', Modifier: gocui.ModNone, Handler: gui.handleCommitPress},
{ViewName: "files", Key: 'C', Modifier: gocui.ModNone, Handler: gui.handleCommitEditorPress},
{ViewName: "files", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handleFilePress},
{ViewName: "files", Key: 'd', Modifier: gocui.ModNone, Handler: gui.handleFileRemove},
{ViewName: "files", Key: 'm', Modifier: gocui.ModNone, Handler: gui.handleSwitchToMerge},
{ViewName: "files", Key: 'e', Modifier: gocui.ModNone, Handler: gui.handleFileEdit},
{ViewName: "files", Key: 'o', Modifier: gocui.ModNone, Handler: gui.handleFileOpen},
{ViewName: "files", Key: 's', Modifier: gocui.ModNone, Handler: gui.handleSublimeFileOpen},
{ViewName: "files", Key: 'v', Modifier: gocui.ModNone, Handler: gui.handleVsCodeFileOpen},
{ViewName: "files", Key: 'i', Modifier: gocui.ModNone, Handler: gui.handleIgnoreFile},
{ViewName: "files", Key: 'r', Modifier: gocui.ModNone, Handler: gui.handleRefreshFiles},
{ViewName: "files", Key: 'S', Modifier: gocui.ModNone, Handler: gui.handleStashSave},
{ViewName: "files", Key: 'a', Modifier: gocui.ModNone, Handler: gui.handleAbortMerge},
{ViewName: "files", Key: 't', Modifier: gocui.ModNone, Handler: gui.handleAddPatch},
{ViewName: "files", Key: 'D', Modifier: gocui.ModNone, Handler: gui.handleResetHard},
{ViewName: "main", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: gui.handleEscapeMerge},
{ViewName: "main", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handlePickHunk},
{ViewName: "main", Key: 'b', Modifier: gocui.ModNone, Handler: gui.handlePickBothHunks},
{ViewName: "main", Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.handleSelectPrevConflict},
{ViewName: "main", Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: gui.handleSelectNextConflict},
{ViewName: "main", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: gui.handleSelectTop},
{ViewName: "main", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: gui.handleSelectBottom},
{ViewName: "main", Key: 'h', Modifier: gocui.ModNone, Handler: gui.handleSelectPrevConflict},
{ViewName: "main", Key: 'l', Modifier: gocui.ModNone, Handler: gui.handleSelectNextConflict},
{ViewName: "main", Key: 'k', Modifier: gocui.ModNone, Handler: gui.handleSelectTop},
{ViewName: "main", Key: 'j', Modifier: gocui.ModNone, Handler: gui.handleSelectBottom},
{ViewName: "main", Key: 'z', Modifier: gocui.ModNone, Handler: gui.handlePopFileSnapshot},
{ViewName: "branches", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handleBranchPress},
{ViewName: "branches", Key: 'c', Modifier: gocui.ModNone, Handler: gui.handleCheckoutByName},
{ViewName: "branches", Key: 'F', Modifier: gocui.ModNone, Handler: gui.handleForceCheckout},
{ViewName: "branches", Key: 'n', Modifier: gocui.ModNone, Handler: gui.handleNewBranch},
{ViewName: "branches", Key: 'd', Modifier: gocui.ModNone, Handler: gui.handleDeleteBranch},
{ViewName: "branches", Key: 'm', Modifier: gocui.ModNone, Handler: gui.handleMerge},
{ViewName: "commits", Key: 's', Modifier: gocui.ModNone, Handler: gui.handleCommitSquashDown},
{ViewName: "commits", Key: 'r', Modifier: gocui.ModNone, Handler: gui.handleRenameCommit},
{ViewName: "commits", Key: 'g', Modifier: gocui.ModNone, Handler: gui.handleResetToCommit},
{ViewName: "commits", Key: 'f', Modifier: gocui.ModNone, Handler: gui.handleCommitFixup},
{ViewName: "stash", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handleStashApply},
{ViewName: "stash", Key: 'g', Modifier: gocui.ModNone, Handler: gui.handleStashPop},
{ViewName: "stash", Key: 'd', Modifier: gocui.ModNone, Handler: gui.handleStashDrop},
{ViewName: "commitMessage", Key: gocui.KeyEnter, Modifier: gocui.ModNone, Handler: gui.handleCommitConfirm},
{ViewName: "commitMessage", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: gui.handleCommitClose},
{ViewName: "commitMessage", Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.handleNewlineCommitMessage},
}
// Would make these keybindings global but that interferes with editing
// input in the confirmation panel
for _, viewName := range []string{"files", "branches", "commits", "stash"} {
for _, viewName := range []string{"status", "files", "branches", "commits", "stash"} {
bindings = append(bindings, []Binding{
{ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: nextView},
{ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: previousView},
{ViewName: viewName, Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: nextView},
{ViewName: viewName, Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: cursorUp},
{ViewName: viewName, Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: cursorDown},
{ViewName: viewName, Key: 'h', Modifier: gocui.ModNone, Handler: previousView},
{ViewName: viewName, Key: 'l', Modifier: gocui.ModNone, Handler: nextView},
{ViewName: viewName, Key: 'k', Modifier: gocui.ModNone, Handler: cursorUp},
{ViewName: viewName, Key: 'j', Modifier: gocui.ModNone, Handler: cursorDown},
{ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.nextView},
{ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.previousView},
{ViewName: viewName, Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: gui.nextView},
{ViewName: viewName, Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: gui.cursorUp},
{ViewName: viewName, Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: gui.cursorDown},
{ViewName: viewName, Key: 'h', Modifier: gocui.ModNone, Handler: gui.previousView},
{ViewName: viewName, Key: 'l', Modifier: gocui.ModNone, Handler: gui.nextView},
{ViewName: viewName, Key: 'k', Modifier: gocui.ModNone, Handler: gui.cursorUp},
{ViewName: viewName, Key: 'j', Modifier: gocui.ModNone, Handler: gui.cursorDown},
}...)
}
+260
View File
@@ -0,0 +1,260 @@
// though this panel is called the merge panel, it's really going to use the main panel. This may change in the future
package gui
import (
"bufio"
"bytes"
"io/ioutil"
"math"
"os"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) findConflicts(content string) ([]commands.Conflict, error) {
conflicts := make([]commands.Conflict, 0)
var newConflict commands.Conflict
for i, line := range utils.SplitLines(content) {
if line == "<<<<<<< HEAD" || line == "<<<<<<< MERGE_HEAD" || line == "<<<<<<< Updated upstream" {
newConflict = commands.Conflict{Start: i}
} else if line == "=======" {
newConflict.Middle = i
} else if strings.HasPrefix(line, ">>>>>>> ") {
newConflict.End = i
conflicts = append(conflicts, newConflict)
}
}
return conflicts, nil
}
func (gui *Gui) shiftConflict(conflicts []commands.Conflict) (commands.Conflict, []commands.Conflict) {
return conflicts[0], conflicts[1:]
}
func (gui *Gui) shouldHighlightLine(index int, conflict commands.Conflict, top bool) bool {
return (index >= conflict.Start && index <= conflict.Middle && top) || (index >= conflict.Middle && index <= conflict.End && !top)
}
func (gui *Gui) coloredConflictFile(content string, conflicts []commands.Conflict, conflictIndex int, conflictTop, hasFocus bool) (string, error) {
if len(conflicts) == 0 {
return content, nil
}
conflict, remainingConflicts := gui.shiftConflict(conflicts)
var outputBuffer bytes.Buffer
for i, line := range utils.SplitLines(content) {
colourAttr := color.FgWhite
if i == conflict.Start || i == conflict.Middle || i == conflict.End {
colourAttr = color.FgRed
}
colour := color.New(colourAttr)
if hasFocus && conflictIndex < len(conflicts) && conflicts[conflictIndex] == conflict && gui.shouldHighlightLine(i, conflict, conflictTop) {
colour.Add(color.Bold)
}
if i == conflict.End && len(remainingConflicts) > 0 {
conflict, remainingConflicts = gui.shiftConflict(remainingConflicts)
}
outputBuffer.WriteString(utils.ColoredStringDirect(line, colour) + "\n")
}
return outputBuffer.String(), nil
}
func (gui *Gui) handleSelectTop(g *gocui.Gui, v *gocui.View) error {
gui.State.ConflictTop = true
return gui.refreshMergePanel(g)
}
func (gui *Gui) handleSelectBottom(g *gocui.Gui, v *gocui.View) error {
gui.State.ConflictTop = false
return gui.refreshMergePanel(g)
}
func (gui *Gui) handleSelectNextConflict(g *gocui.Gui, v *gocui.View) error {
if gui.State.ConflictIndex >= len(gui.State.Conflicts)-1 {
return nil
}
gui.State.ConflictIndex++
return gui.refreshMergePanel(g)
}
func (gui *Gui) handleSelectPrevConflict(g *gocui.Gui, v *gocui.View) error {
if gui.State.ConflictIndex <= 0 {
return nil
}
gui.State.ConflictIndex--
return gui.refreshMergePanel(g)
}
func (gui *Gui) isIndexToDelete(i int, conflict commands.Conflict, pick string) bool {
return i == conflict.Middle ||
i == conflict.Start ||
i == conflict.End ||
pick != "both" &&
(pick == "bottom" && i > conflict.Start && i < conflict.Middle) ||
(pick == "top" && i > conflict.Middle && i < conflict.End)
}
func (gui *Gui) resolveConflict(g *gocui.Gui, conflict commands.Conflict, pick string) error {
gitFile, err := gui.getSelectedFile(g)
if err != nil {
return err
}
file, err := os.Open(gitFile.Name)
if err != nil {
return err
}
defer file.Close()
reader := bufio.NewReader(file)
output := ""
for i := 0; true; i++ {
line, err := reader.ReadString('\n')
if err != nil {
break
}
if !gui.isIndexToDelete(i, conflict, pick) {
output += line
}
}
gui.Log.Info(output)
return ioutil.WriteFile(gitFile.Name, []byte(output), 0644)
}
func (gui *Gui) pushFileSnapshot(g *gocui.Gui) error {
gitFile, err := gui.getSelectedFile(g)
if err != nil {
return err
}
content, err := gui.GitCommand.CatFile(gitFile.Name)
if err != nil {
return err
}
gui.State.EditHistory.Push(content)
return nil
}
func (gui *Gui) handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error {
if gui.State.EditHistory.Len() == 0 {
return nil
}
prevContent := gui.State.EditHistory.Pop().(string)
gitFile, err := gui.getSelectedFile(g)
if err != nil {
return err
}
ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644)
return gui.refreshMergePanel(g)
}
func (gui *Gui) handlePickHunk(g *gocui.Gui, v *gocui.View) error {
conflict := gui.State.Conflicts[gui.State.ConflictIndex]
gui.pushFileSnapshot(g)
pick := "bottom"
if gui.State.ConflictTop {
pick = "top"
}
err := gui.resolveConflict(g, conflict, pick)
if err != nil {
panic(err)
}
gui.refreshMergePanel(g)
return nil
}
func (gui *Gui) handlePickBothHunks(g *gocui.Gui, v *gocui.View) error {
conflict := gui.State.Conflicts[gui.State.ConflictIndex]
gui.pushFileSnapshot(g)
err := gui.resolveConflict(g, conflict, "both")
if err != nil {
panic(err)
}
return gui.refreshMergePanel(g)
}
func (gui *Gui) refreshMergePanel(g *gocui.Gui) error {
cat, err := gui.catSelectedFile(g)
if err != nil {
return err
}
gui.State.Conflicts, err = gui.findConflicts(cat)
if err != nil {
return err
}
if len(gui.State.Conflicts) == 0 {
return gui.handleCompleteMerge(g)
} else if gui.State.ConflictIndex > len(gui.State.Conflicts)-1 {
gui.State.ConflictIndex = len(gui.State.Conflicts) - 1
}
hasFocus := gui.currentViewName(g) == "main"
if hasFocus {
gui.renderMergeOptions(g)
}
content, err := gui.coloredConflictFile(cat, gui.State.Conflicts, gui.State.ConflictIndex, gui.State.ConflictTop, hasFocus)
if err != nil {
return err
}
if err := gui.scrollToConflict(g); err != nil {
return err
}
return gui.renderString(g, "main", content)
}
func (gui *Gui) scrollToConflict(g *gocui.Gui) error {
mainView, err := g.View("main")
if err != nil {
return err
}
if len(gui.State.Conflicts) == 0 {
return nil
}
conflict := gui.State.Conflicts[gui.State.ConflictIndex]
ox, _ := mainView.Origin()
_, height := mainView.Size()
conflictMiddle := (conflict.End + conflict.Start) / 2
newOriginY := int(math.Max(0, float64(conflictMiddle-(height/2))))
return mainView.SetOrigin(ox, newOriginY)
}
func (gui *Gui) switchToMerging(g *gocui.Gui) error {
gui.State.ConflictIndex = 0
gui.State.ConflictTop = true
_, err := g.SetCurrentView("main")
if err != nil {
return err
}
return gui.refreshMergePanel(g)
}
func (gui *Gui) renderMergeOptions(g *gocui.Gui) error {
return gui.renderOptionsMap(g, map[string]string{
"↑ ↓": gui.Tr.SLocalize("selectHunk"),
"← →": gui.Tr.SLocalize("navigateConflicts"),
"space": gui.Tr.SLocalize("pickHunk"),
"b": gui.Tr.SLocalize("pickBothHunks"),
"z": gui.Tr.SLocalize("undo"),
})
}
func (gui *Gui) handleEscapeMerge(g *gocui.Gui, v *gocui.View) error {
filesView, err := g.View("files")
if err != nil {
return err
}
gui.refreshFiles(g)
return gui.switchFocus(g, v, filesView)
}
func (gui *Gui) handleCompleteMerge(g *gocui.Gui) error {
filesView, err := g.View("files")
if err != nil {
return err
}
gui.stageSelectedFile(g)
gui.refreshFiles(g)
return gui.switchFocus(g, nil, filesView)
}
+105
View File
@@ -0,0 +1,105 @@
package gui
import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
)
func (gui *Gui) refreshStashEntries(g *gocui.Gui) error {
g.Update(func(g *gocui.Gui) error {
v, err := g.View("stash")
if err != nil {
panic(err)
}
gui.State.StashEntries = gui.GitCommand.GetStashEntries()
v.Clear()
for _, stashEntry := range gui.State.StashEntries {
fmt.Fprintln(v, stashEntry.DisplayString)
}
return gui.resetOrigin(v)
})
return nil
}
func (gui *Gui) getSelectedStashEntry(v *gocui.View) *commands.StashEntry {
if len(gui.State.StashEntries) == 0 {
return nil
}
lineNumber := gui.getItemPosition(v)
return &gui.State.StashEntries[lineNumber]
}
func (gui *Gui) renderStashOptions(g *gocui.Gui) error {
return gui.renderOptionsMap(g, map[string]string{
"space": gui.Tr.SLocalize("apply"),
"g": gui.Tr.SLocalize("pop"),
"d": gui.Tr.SLocalize("drop"),
"← → ↑ ↓": gui.Tr.SLocalize("navigate"),
})
}
func (gui *Gui) handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error {
if err := gui.renderStashOptions(g); err != nil {
return err
}
go func() {
stashEntry := gui.getSelectedStashEntry(v)
if stashEntry == nil {
gui.renderString(g, "main", gui.Tr.SLocalize("NoStashEntries"))
return
}
diff, _ := gui.GitCommand.GetStashEntryDiff(stashEntry.Index)
gui.renderString(g, "main", diff)
}()
return nil
}
func (gui *Gui) handleStashApply(g *gocui.Gui, v *gocui.View) error {
return gui.stashDo(g, v, "apply")
}
func (gui *Gui) handleStashPop(g *gocui.Gui, v *gocui.View) error {
return gui.stashDo(g, v, "pop")
}
func (gui *Gui) handleStashDrop(g *gocui.Gui, v *gocui.View) error {
title := gui.Tr.SLocalize("StashDrop")
message := gui.Tr.SLocalize("SureDropStashEntry")
return gui.createConfirmationPanel(g, v, title, message, func(g *gocui.Gui, v *gocui.View) error {
return gui.stashDo(g, v, "drop")
}, nil)
}
func (gui *Gui) stashDo(g *gocui.Gui, v *gocui.View, method string) error {
stashEntry := gui.getSelectedStashEntry(v)
if stashEntry == nil {
errorMessage := gui.Tr.TemplateLocalize(
"NoStashTo",
Teml{
"method": method,
},
)
return gui.createErrorPanel(g, errorMessage)
}
if err := gui.GitCommand.StashDo(stashEntry.Index, method); err != nil {
gui.createErrorPanel(g, err.Error())
}
gui.refreshStashEntries(g)
return gui.refreshFiles(g)
}
func (gui *Gui) handleStashSave(g *gocui.Gui, filesView *gocui.View) error {
if len(gui.trackedFiles()) == 0 && len(gui.stagedFiles()) == 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoTrackedStagedFilesStash"))
}
gui.createPromptPanel(g, filesView, gui.Tr.SLocalize("StashChanges"), func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.StashSave(gui.trimmedContent(v)); err != nil {
gui.createErrorPanel(g, err.Error())
}
gui.refreshStashEntries(g)
return gui.refreshFiles(g)
})
return nil
}
+87
View File
@@ -0,0 +1,87 @@
package gui
import (
"fmt"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) refreshStatus(g *gocui.Gui) error {
v, err := g.View("status")
if err != nil {
panic(err)
}
// for some reason if this isn't wrapped in an update the clear seems to
// be applied after the other things or something like that; the panel's
// contents end up cleared
g.Update(func(*gocui.Gui) error {
v.Clear()
pushables, pullables := gui.GitCommand.UpstreamDifferenceCount()
fmt.Fprint(v, "↑"+pushables+"↓"+pullables)
branches := gui.State.Branches
if err := gui.updateHasMergeConflictStatus(); err != nil {
return err
}
if gui.State.HasMergeConflicts {
fmt.Fprint(v, utils.ColoredString(" (merging)", color.FgYellow))
}
if len(branches) == 0 {
return nil
}
branch := branches[0]
name := utils.ColoredString(branch.Name, branch.GetColor())
repo := utils.GetCurrentRepoName()
fmt.Fprint(v, " "+repo+" → "+name)
return nil
})
return nil
}
func (gui *Gui) renderStatusOptions(g *gocui.Gui) error {
return gui.renderOptionsMap(g, map[string]string{
"o": gui.Tr.SLocalize("OpenConfig"),
"e": gui.Tr.SLocalize("EditConfig"),
})
}
func (gui *Gui) handleStatusSelect(g *gocui.Gui, v *gocui.View) error {
dashboardString := fmt.Sprintf(
"%s\n\n%s\n\n%s\n\n%s\n\n%s",
lazygitTitle(),
"Keybindings: https://github.com/jesseduffield/lazygit/blob/master/docs/Keybindings.md",
"Config Options: https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md",
"Tutorial: https://www.youtube.com/watch?v=VDXvbHZYeKY",
"Raise an Issue: https://github.com/jesseduffield/lazygit/issues",
)
if err := gui.renderString(g, "main", dashboardString); err != nil {
return err
}
return gui.renderStatusOptions(g)
}
func (gui *Gui) handleOpenConfig(g *gocui.Gui, v *gocui.View) error {
filename := gui.Config.GetUserConfig().ConfigFileUsed()
return gui.genericFileOpen(g, v, filename, gui.OSCommand.OpenFile)
}
func (gui *Gui) handleEditConfig(g *gocui.Gui, v *gocui.View) error {
filename := gui.Config.GetUserConfig().ConfigFileUsed()
return gui.genericFileOpen(g, v, filename, gui.OSCommand.EditFile)
}
func lazygitTitle() string {
return `
_ _ _
| | (_) |
| | __ _ _____ _ __ _ _| |_
| |/ _` + "`" + ` |_ / | | |/ _` + "`" + ` | | __|
| | (_| |/ /| |_| | (_| | | |_
|_|\__,_/___|\__, |\__, |_|\__|
__/ | __/ |
|___/ |___/ `
}
+54
View File
@@ -0,0 +1,54 @@
package gui
import (
"github.com/jesseduffield/gocui"
)
// GetAttribute gets the gocui color attribute from the string
func (gui *Gui) GetAttribute(key string) gocui.Attribute {
colorMap := map[string]gocui.Attribute{
"default": gocui.ColorDefault,
"black": gocui.ColorBlack,
"red": gocui.ColorRed,
"green": gocui.ColorGreen,
"yellow": gocui.ColorYellow,
"blue": gocui.ColorBlue,
"magenta": gocui.ColorMagenta,
"cyan": gocui.ColorCyan,
"white": gocui.ColorWhite,
"bold": gocui.AttrBold,
"reverse": gocui.AttrReverse,
"underline": gocui.AttrUnderline,
}
value, present := colorMap[key]
if present {
return value
}
return gocui.ColorWhite
}
// GetColor bitwise OR's a list of attributes obtained via the given keys
func (gui *Gui) GetColor(keys []string) gocui.Attribute {
var attribute gocui.Attribute
for _, key := range keys {
attribute = attribute | gui.GetAttribute(key)
}
return attribute
}
// GetOptionsPanelTextColor gets the color of the options panel text
func (gui *Gui) GetOptionsPanelTextColor() (gocui.Attribute, error) {
userConfig := gui.Config.GetUserConfig()
optionsColor := userConfig.GetStringSlice("gui.theme.optionsTextColor")
return gui.GetColor(optionsColor), nil
}
// SetColorScheme sets the color scheme for the app based on the user config
func (gui *Gui) SetColorScheme() error {
userConfig := gui.Config.GetUserConfig()
activeBorderColor := userConfig.GetStringSlice("gui.theme.activeBorderColor")
inactiveBorderColor := userConfig.GetStringSlice("gui.theme.inactiveBorderColor")
gui.g.FgColor = gui.GetColor(inactiveBorderColor)
gui.g.SelFgColor = gui.GetColor(activeBorderColor)
return nil
}
+80 -44
View File
@@ -1,4 +1,4 @@
package main
package gui
import (
"fmt"
@@ -9,16 +9,16 @@ import (
"github.com/jesseduffield/gocui"
)
var cyclableViews = []string{"files", "branches", "commits", "stash"}
var cyclableViews = []string{"status", "files", "branches", "commits", "stash"}
func refreshSidePanels(g *gocui.Gui) error {
refreshBranches(g)
refreshFiles(g)
refreshCommits(g)
func (gui *Gui) refreshSidePanels(g *gocui.Gui) error {
gui.refreshBranches(g)
gui.refreshFiles(g)
gui.refreshCommits(g)
return nil
}
func nextView(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error {
var focusedViewName string
if v == nil || v.Name() == cyclableViews[len(cyclableViews)-1] {
focusedViewName = cyclableViews[0]
@@ -29,7 +29,13 @@ func nextView(g *gocui.Gui, v *gocui.View) error {
break
}
if i == len(cyclableViews)-1 {
devLog(v.Name() + " is not in the list of views")
message := gui.Tr.TemplateLocalize(
"IssntListOfViews",
Teml{
"name": v.Name(),
},
)
gui.Log.Info(message)
return nil
}
}
@@ -38,10 +44,10 @@ func nextView(g *gocui.Gui, v *gocui.View) error {
if err != nil {
panic(err)
}
return switchFocus(g, v, focusedView)
return gui.switchFocus(g, v, focusedView)
}
func previousView(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error {
var focusedViewName string
if v == nil || v.Name() == cyclableViews[0] {
focusedViewName = cyclableViews[len(cyclableViews)-1]
@@ -52,7 +58,13 @@ func previousView(g *gocui.Gui, v *gocui.View) error {
break
}
if i == len(cyclableViews)-1 {
devLog(v.Name() + " is not in the list of views")
message := gui.Tr.TemplateLocalize(
"IssntListOfViews",
Teml{
"name": v.Name(),
},
)
gui.Log.Info(message)
return nil
}
}
@@ -61,69 +73,84 @@ func previousView(g *gocui.Gui, v *gocui.View) error {
if err != nil {
panic(err)
}
return switchFocus(g, v, focusedView)
return gui.switchFocus(g, v, focusedView)
}
func newLineFocused(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error {
mainView, _ := g.View("main")
mainView.SetOrigin(0, 0)
switch v.Name() {
case "status":
return gui.handleStatusSelect(g, v)
case "files":
return handleFileSelect(g, v)
return gui.handleFileSelect(g, v)
case "branches":
return handleBranchSelect(g, v)
return gui.handleBranchSelect(g, v)
case "confirmation":
return nil
case "commitMessage":
return handleCommitFocused(g, v)
return gui.handleCommitFocused(g, v)
case "main":
// TODO: pull this out into a 'view focused' function
refreshMergePanel(g)
gui.refreshMergePanel(g)
v.Highlight = false
return nil
case "commits":
return handleCommitSelect(g, v)
return gui.handleCommitSelect(g, v)
case "stash":
return handleStashEntrySelect(g, v)
return gui.handleStashEntrySelect(g, v)
default:
panic("No view matching newLineFocused switch statement")
panic(gui.Tr.SLocalize("NoViewMachingNewLineFocusedSwitchStatement"))
}
}
func returnFocus(g *gocui.Gui, v *gocui.View) error {
previousView, err := g.View(state.PreviousView)
func (gui *Gui) returnFocus(g *gocui.Gui, v *gocui.View) error {
previousView, err := g.View(gui.State.PreviousView)
if err != nil {
panic(err)
}
return switchFocus(g, v, previousView)
return gui.switchFocus(g, v, previousView)
}
// pass in oldView = nil if you don't want to be able to return to your old view
func switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error {
func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error {
// we assume we'll never want to return focus to a confirmation panel i.e.
// we should never stack confirmation panels
if oldView != nil && oldView.Name() != "confirmation" {
oldView.Highlight = false
devLog("setting previous view to:", oldView.Name())
state.PreviousView = oldView.Name()
message := gui.Tr.TemplateLocalize(
"settingPreviewsViewTo",
Teml{
"oldViewName": oldView.Name(),
},
)
gui.Log.Info(message)
gui.State.PreviousView = oldView.Name()
}
newView.Highlight = true
devLog("new focused view is " + newView.Name())
message := gui.Tr.TemplateLocalize(
"newFocusedViewIs",
Teml{
"newFocusedView": newView.Name(),
},
)
gui.Log.Info(message)
if _, err := g.SetCurrentView(newView.Name()); err != nil {
return err
}
g.Cursor = newView.Editable
return newLineFocused(g, newView)
return gui.newLineFocused(g, newView)
}
func getItemPosition(v *gocui.View) int {
func (gui *Gui) getItemPosition(v *gocui.View) int {
gui.correctCursor(v)
_, cy := v.Cursor()
_, oy := v.Origin()
return oy + cy
}
func cursorUp(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) cursorUp(g *gocui.Gui, v *gocui.View) error {
// swallowing cursor movements in main
// TODO: pull this out
if v == nil || v.Name() == "main" {
@@ -138,11 +165,11 @@ func cursorUp(g *gocui.Gui, v *gocui.View) error {
}
}
newLineFocused(g, v)
gui.newLineFocused(g, v)
return nil
}
func cursorDown(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) cursorDown(g *gocui.Gui, v *gocui.View) error {
// swallowing cursor movements in main
// TODO: pull this out
if v == nil || v.Name() == "main" {
@@ -159,19 +186,19 @@ func cursorDown(g *gocui.Gui, v *gocui.View) error {
}
}
newLineFocused(g, v)
gui.newLineFocused(g, v)
return nil
}
func resetOrigin(v *gocui.View) error {
func (gui *Gui) resetOrigin(v *gocui.View) error {
if err := v.SetCursor(0, 0); err != nil {
return err
}
return v.SetOrigin(0, 0)
}
// if the cursor down past the last item, move it up one
func correctCursor(v *gocui.View) error {
// if the cursor down past the last item, move it to the last line
func (gui *Gui) correctCursor(v *gocui.View) error {
cx, cy := v.Cursor()
_, oy := v.Origin()
lineCount := len(v.BufferLines()) - 2
@@ -181,7 +208,7 @@ func correctCursor(v *gocui.View) error {
return nil
}
func renderString(g *gocui.Gui, viewName, s string) error {
func (gui *Gui) renderString(g *gocui.Gui, viewName, s string) error {
g.Update(func(*gocui.Gui) error {
v, err := g.View(viewName)
// just in case the view disappeared as this function was called, we'll
@@ -197,7 +224,7 @@ func renderString(g *gocui.Gui, viewName, s string) error {
return nil
}
func optionsMapToString(optionsMap map[string]string) string {
func (gui *Gui) optionsMapToString(optionsMap map[string]string) string {
optionsArray := make([]string, 0)
for key, description := range optionsMap {
optionsArray = append(optionsArray, key+": "+description)
@@ -206,11 +233,11 @@ func optionsMapToString(optionsMap map[string]string) string {
return strings.Join(optionsArray, ", ")
}
func renderOptionsMap(g *gocui.Gui, optionsMap map[string]string) error {
return renderString(g, "options", optionsMapToString(optionsMap))
func (gui *Gui) renderOptionsMap(g *gocui.Gui, optionsMap map[string]string) error {
return gui.renderString(g, "options", gui.optionsMapToString(optionsMap))
}
func loader() string {
func (gui *Gui) loader() string {
characters := "|/-\\"
now := time.Now()
nanos := now.UnixNano()
@@ -219,17 +246,26 @@ func loader() string {
}
// TODO: refactor properly
func getFilesView(g *gocui.Gui) *gocui.View {
func (gui *Gui) getFilesView(g *gocui.Gui) *gocui.View {
v, _ := g.View("files")
return v
}
func getCommitsView(g *gocui.Gui) *gocui.View {
func (gui *Gui) getCommitsView(g *gocui.Gui) *gocui.View {
v, _ := g.View("commits")
return v
}
func getCommitMessageView(g *gocui.Gui) *gocui.View {
func (gui *Gui) getCommitMessageView(g *gocui.Gui) *gocui.View {
v, _ := g.View("commitMessage")
return v
}
func (gui *Gui) trimmedContent(v *gocui.View) string {
return strings.TrimSpace(v.Buffer())
}
func (gui *Gui) currentViewName(g *gocui.Gui) string {
currentView := g.CurrentView()
return currentView.Name()
}
+291
View File
@@ -0,0 +1,291 @@
package i18n
import (
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
)
// addDutch will add all dutch translations
func addDutch(i18nObject *i18n.Bundle) error {
// add the translations
return i18nObject.AddMessages(language.Dutch,
&i18n.Message{
ID: "NotEnoughSpace",
Other: "Niet genoeg ruimte om de panelen te renderen",
}, &i18n.Message{
ID: "DiffTitle",
Other: "Diff",
}, &i18n.Message{
ID: "FilesTitle",
Other: "Bestanden",
}, &i18n.Message{
ID: "BranchesTitle",
Other: "Branches",
}, &i18n.Message{
ID: "CommitsTitle",
Other: "Commits",
}, &i18n.Message{
ID: "StashTitle",
Other: "Stash",
}, &i18n.Message{
ID: "CommitMessage",
Other: "Commit Bericht",
}, &i18n.Message{
ID: "CommitChanges",
Other: "Commit Veranderingen",
}, &i18n.Message{
ID: "StatusTitle",
Other: "Status",
}, &i18n.Message{
ID: "navigate",
Other: "navigeer",
}, &i18n.Message{
ID: "stashFiles",
Other: "stash-bestanden",
}, &i18n.Message{
ID: "open",
Other: "open",
}, &i18n.Message{
ID: "ignore",
Other: "negeren",
}, &i18n.Message{
ID: "delete",
Other: "verwijderen",
}, &i18n.Message{
ID: "toggleStaged",
Other: "toggle staged",
}, &i18n.Message{
ID: "refresh",
Other: "verversen",
}, &i18n.Message{
ID: "addPatch",
Other: "verandering toevoegen",
}, &i18n.Message{
ID: "edit",
Other: "veranderen",
}, &i18n.Message{
ID: "scroll",
Other: "scroll",
}, &i18n.Message{
ID: "abortMerge",
Other: "samenvoegen afbreken",
}, &i18n.Message{
ID: "resolveMergeConflicts",
Other: "verhelp samenvoegen fouten",
}, &i18n.Message{
ID: "checkout",
Other: "uitchecken",
}, &i18n.Message{
ID: "NoChangedFiles",
Other: "Geen Bestanden verandert",
}, &i18n.Message{
ID: "FileHasNoUnstagedChanges",
Other: "Het bestand heeft geen unstaged veranderingen om toe te voegen",
}, &i18n.Message{
ID: "CannotGitAdd",
Other: "Kan commando niet uitvoeren git add --path untracked files",
}, &i18n.Message{
ID: "CantIgnoreTrackFiles",
Other: "Kan gevolgde bestanden niet negeren",
}, &i18n.Message{
ID: "NoStagedFilesToCommit",
Other: "Er zijn geen staged bestanden om te commiten",
}, &i18n.Message{
ID: "NoFilesDisplay",
Other: "Geen bestanden om te laten zien",
}, &i18n.Message{
ID: "PullWait",
Other: "Pulling...",
}, &i18n.Message{
ID: "PushWait",
Other: "Pushing...",
}, &i18n.Message{
ID: "FileNoMergeCons",
Other: "Dit bestand heeft geen merge conflicten",
}, &i18n.Message{
ID: "SureResetHardHead",
Other: "Weet je het zeker dat je `reset --hard HEAD` wil uitvoeren? het kan dat je hierdoor bestanden verliest",
}, &i18n.Message{
ID: "SureTo",
Other: "Weet je het zeker dat je {{.fileName}} wilt {{.deleteVerb}} (je veranderingen zullen worden verwijdert)",
}, &i18n.Message{
ID: "AlreadyCheckedOutBranch",
Other: "Je hebt uitgecheckt op deze branch",
}, &i18n.Message{
ID: "SureForceCheckout",
Other: "Weet je zeker dat je het uitchecken wil forceren? al je locale verandering zullen worden verwijdert",
}, &i18n.Message{
ID: "ForceCheckoutBranch",
Other: "Forceer uitchecken op deze branch",
}, &i18n.Message{
ID: "BranchName",
Other: "Branch naam",
}, &i18n.Message{
ID: "NewBranchNameBranchOff",
Other: "Nieuw branch naam (Branch is afgeleid van {{.branchName}})",
}, &i18n.Message{
ID: "CantDeleteCheckOutBranch",
Other: "Je kan een uitgecheckte branch niet verwijderen!",
}, &i18n.Message{
ID: "DeleteBranch",
Other: "Verwijder branch",
}, &i18n.Message{
ID: "DeleteBranchMessage",
Other: "Weet je zeker dat je {{.selectedBranchName}} branch wil verwijderen?",
}, &i18n.Message{
ID: "CantMergeBranchIntoItself",
Other: "Je kan niet een branch in zichzelf mergen",
}, &i18n.Message{
ID: "forceCheckout",
Other: "forceer checkout",
}, &i18n.Message{
ID: "merge",
Other: "merge",
}, &i18n.Message{
ID: "checkoutByName",
Other: "uitchecken bij naam",
}, &i18n.Message{
ID: "newBranch",
Other: "nieuwe branch",
}, &i18n.Message{
ID: "deleteBranch",
Other: "verwijder branch",
}, &i18n.Message{
ID: "NoBranchesThisRepo",
Other: "Geen branches voor deze repo",
}, &i18n.Message{
ID: "NoTrackingThisBranch",
Other: "deze branch wordt niet gevolgd",
}, &i18n.Message{
ID: "CommitWithoutMessageErr",
Other: "Je kan geen commit maken zonder commit bericht",
}, &i18n.Message{
ID: "CloseConfirm",
Other: "{{.keyBindClose}}: Sluiten, {{.keyBindConfirm}}: Bevestigen",
}, &i18n.Message{
ID: "SureResetThisCommit",
Other: "Weet je het zeker dat je wil resetten naar deze commit?",
}, &i18n.Message{
ID: "ResetToCommit",
Other: "Reset Naar Commit",
}, &i18n.Message{
ID: "squashDown",
Other: "squash beneden",
}, &i18n.Message{
ID: "rename",
Other: "hernoem",
}, &i18n.Message{
ID: "resetToThisCommit",
Other: "reset naar deze commit",
}, &i18n.Message{
ID: "fixupCommit",
Other: "Fixup commit",
}, &i18n.Message{
ID: "NoCommitsThisBranch",
Other: "Er zijn geen commits voor deze branch",
}, &i18n.Message{
ID: "OnlySquashTopmostCommit",
Other: "Kan alleen bovenste commit squashen",
}, &i18n.Message{
ID: "YouNoCommitsToSquash",
Other: "Je hebt geen commits om mee te squashen",
}, &i18n.Message{
ID: "CantFixupWhileUnstagedChanges",
Other: "Kan geen Fixup uitvoeren op unstaged veranderingen",
}, &i18n.Message{
ID: "Fixup",
Other: "Fixup",
}, &i18n.Message{
ID: "SureFixupThisCommit",
Other: "Weet je zeker dat je fixup wil uitvoeren op deze commit? De commit hieronder zol worden squashed in deze",
}, &i18n.Message{
ID: "OnlyRenameTopCommit",
Other: "Je kan alleen de bovenste commit hernoemen",
}, &i18n.Message{
ID: "RenameCommit",
Other: "Hernoem Commit",
}, &i18n.Message{
ID: "PotentialErrInGetselectedCommit",
Other: "Er is mogelijk een error in getSelected Commit (geen match tussen ui en state)",
}, &i18n.Message{
ID: "NoCommitsThisBranch",
Other: "Geen commits voor deze branch",
}, &i18n.Message{
ID: "Error",
Other: "Fout",
}, &i18n.Message{
ID: "resizingPopupPanel",
Other: "resizen popup paneel",
}, &i18n.Message{
ID: "RunningSubprocess",
Other: "subprocess lopend",
}, &i18n.Message{
ID: "selectHunk",
Other: "selecteer Hunk",
}, &i18n.Message{
ID: "navigateConflicts",
Other: "navigeer conflicts",
}, &i18n.Message{
ID: "pickHunk",
Other: "kies Hunk",
}, &i18n.Message{
ID: "pickBothHunks",
Other: "kies bijde hunks",
}, &i18n.Message{
ID: "undo",
Other: "ongedaan maken",
}, &i18n.Message{
ID: "pop",
Other: "pop",
}, &i18n.Message{
ID: "drop",
Other: "drop",
}, &i18n.Message{
ID: "apply",
Other: "toepassen",
}, &i18n.Message{
ID: "NoStashEntries",
Other: "Geen stash items",
}, &i18n.Message{
ID: "StashDrop",
Other: "Stash drop",
}, &i18n.Message{
ID: "SureDropStashEntry",
Other: "Weet je het zeker dat je deze stash entry wil laten vallen?",
}, &i18n.Message{
ID: "NoStashTo",
Other: "Geen stash voor {{.method}}",
}, &i18n.Message{
ID: "NoTrackedStagedFilesStash",
Other: "Je hebt geen tracked/staged bestanden om te laten stashen",
}, &i18n.Message{
ID: "StashChanges",
Other: "Stash veranderingen",
}, &i18n.Message{
ID: "IssntListOfViews",
Other: "{{.name}} is niet in de lijst van weergaves",
}, &i18n.Message{
ID: "NoViewMachingNewLineFocusedSwitchStatement",
Other: "Er machen geen weergave met de newLineFocused switch declaratie",
}, &i18n.Message{
ID: "settingPreviewsViewTo",
Other: "vorige weergave instellen op: {{.oldViewName}}",
}, &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: "NoChangedFiles",
Other: "Geen veranderde files",
}, &i18n.Message{
ID: "ClearFilePanel",
Other: "maak bestandsvenster leeg",
}, &i18n.Message{
ID: "MergeAborted",
Other: "Merge afgebroken",
},
)
}
+305
View File
@@ -0,0 +1,305 @@
/*
Todo list when making a new translation
- Copy this file and rename it to the language you want to translate to like someLanguage.go
- Change the addEnglish() name to the language you want to translate to like addSomeLanguage()
- change the first function argument of i18nObject.AddMessages( to the language you want to translate to like language.SomeLanguage
- Remove this todo and the about section
*/
package i18n
import (
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
)
func addEnglish(i18nObject *i18n.Bundle) error {
return i18nObject.AddMessages(language.English,
&i18n.Message{
ID: "NotEnoughSpace",
Other: "Not enough space to render panels",
}, &i18n.Message{
ID: "DiffTitle",
Other: "Diff",
}, &i18n.Message{
ID: "FilesTitle",
Other: "Files",
}, &i18n.Message{
ID: "BranchesTitle",
Other: "Branches",
}, &i18n.Message{
ID: "CommitsTitle",
Other: "Commits",
}, &i18n.Message{
ID: "StashTitle",
Other: "Stash",
}, &i18n.Message{
ID: "CommitMessage",
Other: "Commit message",
}, &i18n.Message{
ID: "CommitChanges",
Other: "commit changes",
}, &i18n.Message{
ID: "StatusTitle",
Other: "Status",
}, &i18n.Message{
ID: "navigate",
Other: "navigate",
}, &i18n.Message{
ID: "stashFiles",
Other: "stash files",
}, &i18n.Message{
ID: "open",
Other: "open",
}, &i18n.Message{
ID: "ignore",
Other: "ignore",
}, &i18n.Message{
ID: "delete",
Other: "delete",
}, &i18n.Message{
ID: "toggleStaged",
Other: "toggle staged",
}, &i18n.Message{
ID: "refresh",
Other: "refresh",
}, &i18n.Message{
ID: "addPatch",
Other: "add patch",
}, &i18n.Message{
ID: "edit",
Other: "edit",
}, &i18n.Message{
ID: "scroll",
Other: "scroll",
}, &i18n.Message{
ID: "abortMerge",
Other: "abort merge",
}, &i18n.Message{
ID: "resolveMergeConflicts",
Other: "resolve merge conflicts",
}, &i18n.Message{
ID: "checkout",
Other: "checkout",
}, &i18n.Message{
ID: "NoChangedFiles",
Other: "No changed files",
}, &i18n.Message{
ID: "FileHasNoUnstagedChanges",
Other: "File has no unstaged changes to add",
}, &i18n.Message{
ID: "CannotGitAdd",
Other: "Cannot git add --patch untracked files",
}, &i18n.Message{
ID: "CantIgnoreTrackFiles",
Other: "Cannot ignore tracked files",
}, &i18n.Message{
ID: "NoStagedFilesToCommit",
Other: "There are no staged files to commit",
}, &i18n.Message{
ID: "NoFilesDisplay",
Other: "No file to display",
}, &i18n.Message{
ID: "PullWait",
Other: "Pulling...",
}, &i18n.Message{
ID: "PushWait",
Other: "Pushing...",
}, &i18n.Message{
ID: "FileNoMergeCons",
Other: "This file has no merge conflicts",
}, &i18n.Message{
ID: "SureResetHardHead",
Other: "Are you sure you want `reset --hard HEAD`? You may lose changes",
}, &i18n.Message{
ID: "SureTo",
Other: "Are you sure you want to {{.deleteVerb}} {{.fileName}} (you will lose your changes)?",
}, &i18n.Message{
ID: "AlreadyCheckedOutBranch",
Other: "You have already checked out this branch",
}, &i18n.Message{
ID: "SureForceCheckout",
Other: "Are you sure you want force checkout? You will lose all local changes",
}, &i18n.Message{
ID: "ForceCheckoutBranch",
Other: "Force Checkout Branch",
}, &i18n.Message{
ID: "BranchName",
Other: "Branch name",
}, &i18n.Message{
ID: "NewBranchNameBranchOff",
Other: "New Branch Name (Branch is off of {{.branchName}})",
}, &i18n.Message{
ID: "CantDeleteCheckOutBranch",
Other: "You cannot delete the checked out branch!",
}, &i18n.Message{
ID: "DeleteBranch",
Other: "Delete Branch",
}, &i18n.Message{
ID: "DeleteBranchMessage",
Other: "Are you sure you want delete the branch {{.selectedBranchName}} ?",
}, &i18n.Message{
ID: "CantMergeBranchIntoItself",
Other: "You cannot merge a branch into itself",
}, &i18n.Message{
ID: "forceCheckout",
Other: "force checkout",
}, &i18n.Message{
ID: "merge",
Other: "merge",
}, &i18n.Message{
ID: "checkoutByName",
Other: "checkout by name",
}, &i18n.Message{
ID: "newBranch",
Other: "new branch",
}, &i18n.Message{
ID: "deleteBranch",
Other: "delete branch",
}, &i18n.Message{
ID: "NoBranchesThisRepo",
Other: "No branches for this repo",
}, &i18n.Message{
ID: "NoTrackingThisBranch",
Other: "There is no tracking for this branch",
}, &i18n.Message{
ID: "CommitWithoutMessageErr",
Other: "You cannot commit without a commit message",
}, &i18n.Message{
ID: "CloseConfirm",
Other: "{{.keyBindClose}}: close, {{.keyBindConfirm}}: confirm",
}, &i18n.Message{
ID: "SureResetThisCommit",
Other: "Are you sure you want to reset to this commit?",
}, &i18n.Message{
ID: "ResetToCommit",
Other: "Reset To Commit",
}, &i18n.Message{
ID: "squashDown",
Other: "squash down",
}, &i18n.Message{
ID: "rename",
Other: "rename",
}, &i18n.Message{
ID: "resetToThisCommit",
Other: "reset to this commit",
}, &i18n.Message{
ID: "fixupCommit",
Other: "fixup commit",
}, &i18n.Message{
ID: "NoCommitsThisBranch",
Other: "No commits for this branch",
}, &i18n.Message{
ID: "OnlySquashTopmostCommit",
Other: "Can only squash topmost commit",
}, &i18n.Message{
ID: "YouNoCommitsToSquash",
Other: "You have no commits to squash with",
}, &i18n.Message{
ID: "CantFixupWhileUnstagedChanges",
Other: "Can't fixup while there are unstaged changes",
}, &i18n.Message{
ID: "Fixup",
Other: "Fixup",
}, &i18n.Message{
ID: "SureFixupThisCommit",
Other: "Are you sure you want to fixup this commit? The commit beneath will be squashed up into this one",
}, &i18n.Message{
ID: "OnlyRenameTopCommit",
Other: "Can only rename topmost commit",
}, &i18n.Message{
ID: "RenameCommit",
Other: "Rename Commit",
}, &i18n.Message{
ID: "PotentialErrInGetselectedCommit",
Other: "potential error in getSelected Commit (mismatched ui and state)",
}, &i18n.Message{
ID: "NoCommitsThisBranch",
Other: "No commits for this branch",
}, &i18n.Message{
ID: "Error",
Other: "Error",
}, &i18n.Message{
ID: "resizingPopupPanel",
Other: "resizing popup panel",
}, &i18n.Message{
ID: "RunningSubprocess",
Other: "running subprocess",
}, &i18n.Message{
ID: "selectHunk",
Other: "select hunk",
}, &i18n.Message{
ID: "navigateConflicts",
Other: "navigate conflicts",
}, &i18n.Message{
ID: "pickHunk",
Other: "pick hunk",
}, &i18n.Message{
ID: "pickBothHunks",
Other: "pick both hunks",
}, &i18n.Message{
ID: "undo",
Other: "undo",
}, &i18n.Message{
ID: "pop",
Other: "pop",
}, &i18n.Message{
ID: "drop",
Other: "drop",
}, &i18n.Message{
ID: "apply",
Other: "apply",
}, &i18n.Message{
ID: "NoStashEntries",
Other: "No stash entries",
}, &i18n.Message{
ID: "StashDrop",
Other: "Stash drop",
}, &i18n.Message{
ID: "SureDropStashEntry",
Other: "Are you sure you want to drop this stash entry?",
}, &i18n.Message{
ID: "NoStashTo",
Other: "No stash to {{.method}}",
}, &i18n.Message{
ID: "NoTrackedStagedFilesStash",
Other: "You have no tracked/staged files to stash",
}, &i18n.Message{
ID: "StashChanges",
Other: "Stash changes",
}, &i18n.Message{
ID: "IssntListOfViews",
Other: "{{.name}} is not in the list of views",
}, &i18n.Message{
ID: "NoViewMachingNewLineFocusedSwitchStatement",
Other: "No view matching newLineFocused switch statement",
}, &i18n.Message{
ID: "settingPreviewsViewTo",
Other: "setting previous view to: {{.oldViewName}}",
}, &i18n.Message{
ID: "newFocusedViewIs",
Other: "new focused view is {{.newFocusedView}}",
}, &i18n.Message{
ID: "CantCloseConfirmationPrompt",
Other: "Could not close confirmation prompt: {{.error}}",
}, &i18n.Message{
ID: "NoChangedFiles",
Other: "No changed files",
}, &i18n.Message{
ID: "ClearFilePanel",
Other: "Clear file panel",
}, &i18n.Message{
ID: "MergeAborted",
Other: "Merge aborted",
}, &i18n.Message{
ID: "OpenConfig",
Other: "open config file",
}, &i18n.Message{
ID: "EditConfig",
Other: "edit config file",
},
)
}
+95
View File
@@ -0,0 +1,95 @@
package i18n
import (
"github.com/Sirupsen/logrus"
"github.com/cloudfoundry/jibber_jabber"
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
)
// Teml is short for template used to make the required map[string]interface{} shorter when using gui.Tr.SLocalize and gui.Tr.TemplateLocalize
type Teml map[string]interface{}
// Localizer will translate a message into the user's language
type Localizer struct {
i18nLocalizer *i18n.Localizer
language string
Log *logrus.Logger
}
// NewLocalizer creates a new Localizer
func NewLocalizer(log *logrus.Logger) (*Localizer, error) {
// detect the user's language
userLang, err := jibber_jabber.DetectLanguage()
if err != nil {
return nil, err
}
log.Info("language: " + userLang)
// create a i18n bundle that can be used to add translations and other things
i18nBundle := &i18n.Bundle{DefaultLanguage: language.English}
addBundles(log, i18nBundle)
// return the new localizer that can be used to translate text
i18nLocalizer := i18n.NewLocalizer(i18nBundle, userLang)
localizer := &Localizer{
i18nLocalizer: i18nLocalizer,
language: userLang,
Log: log,
}
return localizer, nil
}
// Localize handels the translations
// expects i18n.LocalizeConfig as input: https://godoc.org/github.com/nicksnyder/go-i18n/v2/i18n#Localizer.MustLocalize
// output: translated string
func (l *Localizer) Localize(config *i18n.LocalizeConfig) string {
return l.i18nLocalizer.MustLocalize(config)
}
// SLocalize (short localize) is for 1 line localizations
// ID: The id that is used in the .toml translation files
// Other: the default message it needs to return if there is no translation found or the system is english
func (l *Localizer) SLocalize(ID string) string {
return l.Localize(&i18n.LocalizeConfig{
DefaultMessage: &i18n.Message{
ID: ID,
},
})
}
// TemplateLocalize allows the Other input to be dynamic
func (l *Localizer) TemplateLocalize(ID string, TemplateData map[string]interface{}) string {
return l.Localize(&i18n.LocalizeConfig{
DefaultMessage: &i18n.Message{
ID: ID,
},
TemplateData: TemplateData,
})
}
// GetLanguage returns the currently selected language, e.g 'en'
func (l *Localizer) GetLanguage() string {
return l.language
}
// add translation file(s)
func addBundles(log *logrus.Logger, i18nBundle *i18n.Bundle) {
err := addPolish(i18nBundle)
if err != nil {
log.Fatal(err)
}
err = addDutch(i18nBundle)
if err != nil {
log.Fatal(err)
}
err = addEnglish(i18nBundle)
if err != nil {
log.Fatal(err)
}
}
+289
View File
@@ -0,0 +1,289 @@
package i18n
import (
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
)
func addPolish(i18nObject *i18n.Bundle) error {
return i18nObject.AddMessages(language.Polish,
&i18n.Message{
ID: "NotEnoughSpace",
Other: "Za mało miejsca do wyświetlenia paneli",
}, &i18n.Message{
ID: "DiffTitle",
Other: "Różnice",
}, &i18n.Message{
ID: "FilesTitle",
Other: "Pliki",
}, &i18n.Message{
ID: "BranchesTitle",
Other: "Gałęzie",
}, &i18n.Message{
ID: "CommitsTitle",
Other: "Commity",
}, &i18n.Message{
ID: "StashTitle",
Other: "Schowek",
}, &i18n.Message{
ID: "CommitMessage",
Other: "Wiadomość commita",
}, &i18n.Message{
ID: "CommitChanges",
Other: "commituj zmiany",
}, &i18n.Message{
ID: "StatusTitle",
Other: "Status",
}, &i18n.Message{
ID: "navigate",
Other: "nawiguj",
}, &i18n.Message{
ID: "stashFiles",
Other: "przechowaj pliki",
}, &i18n.Message{
ID: "open",
Other: "otwórz",
}, &i18n.Message{
ID: "ignore",
Other: "ignoruj",
}, &i18n.Message{
ID: "delete",
Other: "usuń",
}, &i18n.Message{
ID: "toggleStaged",
Other: "przełącz zatwierdzenie",
}, &i18n.Message{
ID: "refresh",
Other: "odśwież",
}, &i18n.Message{
ID: "addPatch",
Other: "dodaj łatkę",
}, &i18n.Message{
ID: "edit",
Other: "edytuj",
}, &i18n.Message{
ID: "scroll",
Other: "przewiń",
}, &i18n.Message{
ID: "abortMerge",
Other: "o scalaniu",
}, &i18n.Message{
ID: "resolveMergeConflicts",
Other: "rozwiąż konflikty scalania",
}, &i18n.Message{
ID: "checkout",
Other: "przełącz",
}, &i18n.Message{
ID: "NoChangedFiles",
Other: "Brak zmienionych plików",
}, &i18n.Message{
ID: "FileHasNoUnstagedChanges",
Other: "Plik nie zawiera żadnych nieopublikowanych zmian do dodania",
}, &i18n.Message{
ID: "CannotGitAdd",
Other: "Nie można git add --patch nieśledzonych plików",
}, &i18n.Message{
ID: "CantIgnoreTrackFiles",
Other: "Nie można zignorować nieśledzonych plików",
}, &i18n.Message{
ID: "NoStagedFilesToCommit",
Other: "Brak zatwierdzonych plików do commita",
}, &i18n.Message{
ID: "NoFilesDisplay",
Other: "Brak pliku do wyświetlenia",
}, &i18n.Message{
ID: "PullWait",
Other: "Wciąganie zmian...",
}, &i18n.Message{
ID: "PushWait",
Other: "Wypychanie zmian...",
}, &i18n.Message{
ID: "FileNoMergeCons",
Other: "Ten plik nie powoduje konfliktów scalania",
}, &i18n.Message{
ID: "SureResetHardHead",
Other: "Jesteś pewny, że chcesz wykonać `reset --hard HEAD`? Możesz stracić wprowadzone zmiany",
}, &i18n.Message{
ID: "SureTo",
Other: "Jesteś pewny, że chcesz {{.deleteVerb}} {{.fileName}} (stracisz swoje wprowadzone zmiany)?",
}, &i18n.Message{
ID: "AlreadyCheckedOutBranch",
Other: "Już przęłączono na tą gałąź",
}, &i18n.Message{
ID: "SureForceCheckout",
Other: "Jesteś pewny, że chcesz wymusić przełączenie? Stracisz wszystkie lokalne zmiany",
}, &i18n.Message{
ID: "ForceCheckoutBranch",
Other: "Wymuś przełączenie gałęzi",
}, &i18n.Message{
ID: "BranchName",
Other: "Nazwa gałęzi",
}, &i18n.Message{
ID: "NewBranchNameBranchOff",
Other: "Nazwa nowej gałęzi (gałąź na bazie {{.branchName}})",
}, &i18n.Message{
ID: "CantDeleteCheckOutBranch",
Other: "Nie możesz usunąć obecnej przełączonej gałęzi!",
}, &i18n.Message{
ID: "DeleteBranch",
Other: "Usuń gałąź",
}, &i18n.Message{
ID: "DeleteBranchMessage",
Other: "Jesteś pewien, że chcesz usunąć gałąź {{.selectedBranchName}} ?",
}, &i18n.Message{
ID: "CantMergeBranchIntoItself",
Other: "Nie możesz scalić gałęzi do samej siebie",
}, &i18n.Message{
ID: "forceCheckout",
Other: "wymuś przełączenie",
}, &i18n.Message{
ID: "merge",
Other: "scal",
}, &i18n.Message{
ID: "checkoutByName",
Other: "przełącz używając nazwy",
}, &i18n.Message{
ID: "newBranch",
Other: "nowa gałąź",
}, &i18n.Message{
ID: "deleteBranch",
Other: "usuń gałąź",
}, &i18n.Message{
ID: "NoBranchesThisRepo",
Other: "Brak gałęzi dla tego repozytorium",
}, &i18n.Message{
ID: "NoTrackingThisBranch",
Other: "Brak śledzenia dla tej gałęzi",
}, &i18n.Message{
ID: "CommitWithoutMessageErr",
Other: "Nie możesz commitować bez podania wiadomości",
}, &i18n.Message{
ID: "CloseConfirm",
Other: "{{.keyBindClose}}: zamknij, {{.keyBindConfirm}}: potwierdź",
}, &i18n.Message{
ID: "SureResetThisCommit",
Other: "Jesteś pewny, że chcesz zresetować ten commit?",
}, &i18n.Message{
ID: "ResetToCommit",
Other: "Zresetuj, aby commitować",
}, &i18n.Message{
ID: "squashDown",
Other: "ściśnij w dół",
}, &i18n.Message{
ID: "rename",
Other: "przemianuj",
}, &i18n.Message{
ID: "resetToThisCommit",
Other: "zresetuj do tego commita",
}, &i18n.Message{
ID: "fixupCommit",
Other: "napraw commit",
}, &i18n.Message{
ID: "NoCommitsThisBranch",
Other: "Brak commitów dla tej gałęzi",
}, &i18n.Message{
ID: "OnlySquashTopmostCommit",
Other: "Można tylko ścisnąć najwyższy commit",
}, &i18n.Message{
ID: "YouNoCommitsToSquash",
Other: "Nie masz commitów do ściśnięcia",
}, &i18n.Message{
ID: "CantFixupWhileUnstagedChanges",
Other: "Nie można wykonać naprawy, kiedy istnieją niezatwierdzone zmiany",
}, &i18n.Message{
ID: "Fixup",
Other: "Napraw",
}, &i18n.Message{
ID: "SureFixupThisCommit",
Other: "Jesteś pewny, ze chcesz naprawić ten commit? Commit poniżej zostanie ściśnięty w górę wraz z tym",
}, &i18n.Message{
ID: "OnlyRenameTopCommit",
Other: "Można przmianować tylko najwyższy commit",
}, &i18n.Message{
ID: "RenameCommit",
Other: "Przemianuj commit",
}, &i18n.Message{
ID: "PotentialErrInGetselectedCommit",
Other: "potencjalny błąd w getSelected Commit (niedopasowane ui i stan)",
}, &i18n.Message{
ID: "NoCommitsThisBranch",
Other: "Brak commitów dla tej gałęzi",
}, &i18n.Message{
ID: "Error",
Other: "Błąd",
}, &i18n.Message{
ID: "resizingPopupPanel",
Other: "skalowanie wyskakującego panelu",
}, &i18n.Message{
ID: "RunningSubprocess",
Other: "uruchomiony podproces",
}, &i18n.Message{
ID: "selectHunk",
Other: "wybierz kawałek",
}, &i18n.Message{
ID: "navigateConflicts",
Other: "nawiguj konflikty",
}, &i18n.Message{
ID: "pickHunk",
Other: "wybierz kawałek",
}, &i18n.Message{
ID: "pickBothHunks",
Other: "wybierz oba kawałki",
}, &i18n.Message{
ID: "undo",
Other: "cofnij",
}, &i18n.Message{
ID: "pop",
Other: "wyciągnij",
}, &i18n.Message{
ID: "drop",
Other: "porzuć",
}, &i18n.Message{
ID: "apply",
Other: "zastosuj",
}, &i18n.Message{
ID: "NoStashEntries",
Other: "Brak pozycji w schowku",
}, &i18n.Message{
ID: "StashDrop",
Other: "Porzuć schowek",
}, &i18n.Message{
ID: "SureDropStashEntry",
Other: "Jesteś pewny, że chcesz porzucić tę pozycję w schowku?",
}, &i18n.Message{
ID: "NoStashTo",
Other: "Brak schowka dla {{.method}}",
}, &i18n.Message{
ID: "NoTrackedStagedFilesStash",
Other: "Nie masz śledzonych/zatwierdzonych plików do przechowania",
}, &i18n.Message{
ID: "StashChanges",
Other: "Przechowaj zmiany",
}, &i18n.Message{
ID: "IssntListOfViews",
Other: "{{.name}} nie jest na liście widoków",
}, &i18n.Message{
ID: "NoViewMachingNewLineFocusedSwitchStatement",
Other: "Brak widoku pasującego do instrukcji przełączania newLineFocused",
}, &i18n.Message{
ID: "settingPreviewsViewTo",
Other: "ustawianie poprzedniego widoku na: {{.oldViewName}}",
}, &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: "NoChangedFiles",
Other: "Brak zmienionych plików",
}, &i18n.Message{
ID: "ClearFilePanel",
Other: "Wyczyść panel plików",
}, &i18n.Message{
ID: "MergeAborted",
Other: "Scalanie anulowane",
},
)
}
+25
View File
@@ -0,0 +1,25 @@
package test
import (
"errors"
"os"
"os/exec"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// GenerateRepo generates a repo from test/repos and changes the directory to be
// inside the newly made repo
func GenerateRepo(filename string) error {
testPath := utils.GetProjectRoot() + "/test/repos/"
if err := os.Chdir(testPath); err != nil {
return err
}
if output, err := exec.Command("bash", filename).CombinedOutput(); err != nil {
return errors.New(string(output))
}
if err := os.Chdir(testPath + "repo"); err != nil {
return err
}
return nil
}
+76
View File
@@ -0,0 +1,76 @@
package utils
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"github.com/fatih/color"
)
// SplitLines takes a multiline string and splits it on newlines
// currently we are also stripping \r's which may have adverse effects for
// windows users (but no issues have been raised yet)
func SplitLines(multilineString string) []string {
multilineString = strings.Replace(multilineString, "\r", "", -1)
if multilineString == "" || multilineString == "\n" {
return make([]string, 0)
}
lines := strings.Split(multilineString, "\n")
if lines[len(lines)-1] == "" {
return lines[:len(lines)-1]
}
return lines
}
// WithPadding pads a string as much as you want
func WithPadding(str string, padding int) string {
if padding-len(str) < 0 {
return str
}
return str + strings.Repeat(" ", padding-len(str))
}
// ColoredString takes a string and a colour attribute and returns a colored
// string with that attribute
func ColoredString(str string, colorAttribute color.Attribute) string {
colour := color.New(colorAttribute)
return ColoredStringDirect(str, colour)
}
// ColoredStringDirect used for aggregating a few color attributes rather than
// just sending a single one
func ColoredStringDirect(str string, colour *color.Color) string {
return colour.SprintFunc()(fmt.Sprint(str))
}
// GetCurrentRepoName gets the repo's base name
func GetCurrentRepoName() string {
pwd, err := os.Getwd()
if err != nil {
log.Fatalln(err.Error())
}
return filepath.Base(pwd)
}
// TrimTrailingNewline - Trims the trailing newline
// TODO: replace with `chomp` after refactor
func TrimTrailingNewline(str string) string {
if strings.HasSuffix(str, "\n") {
return str[:len(str)-1]
}
return str
}
// GetProjectRoot returns the path to the root of the project. Only to be used
// in testing contexts, as with binaries it's unlikely this path will exist on
// the machine
func GetProjectRoot() string {
dir, err := os.Getwd()
if err != nil {
panic(err)
}
return strings.Split(dir, "lazygit")[0] + "lazygit"
}
+10 -2
View File
@@ -10,12 +10,14 @@ import (
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"strconv"
"strings"
)
func main() {
version, err := ioutil.ReadFile("VERSION")
if err != nil {
log.Panicln(err.Error())
@@ -47,9 +49,15 @@ func main() {
func runCommand(args ...string) {
fmt.Println(strings.Join(args, " "))
output, err := exec.Command(args[0], args[1:]...).CombinedOutput()
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Start()
if err != nil {
panic(err.Error())
}
err = cmd.Wait()
if err != nil {
panic(err.Error())
}
log.Print(string(output))
}
-93
View File
@@ -1,93 +0,0 @@
package main
import (
"fmt"
"github.com/jesseduffield/gocui"
)
func refreshStashEntries(g *gocui.Gui) error {
g.Update(func(g *gocui.Gui) error {
v, err := g.View("stash")
if err != nil {
panic(err)
}
state.StashEntries = getGitStashEntries()
v.Clear()
for _, stashEntry := range state.StashEntries {
fmt.Fprintln(v, stashEntry.DisplayString)
}
return resetOrigin(v)
})
return nil
}
func getSelectedStashEntry(v *gocui.View) *StashEntry {
if len(state.StashEntries) == 0 {
return nil
}
lineNumber := getItemPosition(v)
return &state.StashEntries[lineNumber]
}
func renderStashOptions(g *gocui.Gui) error {
return renderOptionsMap(g, map[string]string{
"space": "apply",
"g": "pop",
"d": "drop",
"← → ↑ ↓": "navigate",
})
}
func handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error {
if err := renderStashOptions(g); err != nil {
return err
}
go func() {
stashEntry := getSelectedStashEntry(v)
if stashEntry == nil {
renderString(g, "main", "No stash entries")
return
}
diff, _ := getStashEntryDiff(stashEntry.Index)
renderString(g, "main", diff)
}()
return nil
}
func handleStashApply(g *gocui.Gui, v *gocui.View) error {
return stashDo(g, v, "apply")
}
func handleStashPop(g *gocui.Gui, v *gocui.View) error {
return stashDo(g, v, "pop")
}
func handleStashDrop(g *gocui.Gui, v *gocui.View) error {
return createConfirmationPanel(g, v, "Stash drop", "Are you sure you want to drop this stash entry?", func(g *gocui.Gui, v *gocui.View) error {
return stashDo(g, v, "drop")
}, nil)
}
func stashDo(g *gocui.Gui, v *gocui.View, method string) error {
stashEntry := getSelectedStashEntry(v)
if stashEntry == nil {
return createErrorPanel(g, "No stash to "+method)
}
if output, err := gitStashDo(stashEntry.Index, method); err != nil {
createErrorPanel(g, output)
}
refreshStashEntries(g)
return refreshFiles(g)
}
func handleStashSave(g *gocui.Gui, filesView *gocui.View) error {
createPromptPanel(g, filesView, "Stash changes", func(g *gocui.Gui, v *gocui.View) error {
if output, err := gitStashSave(trimmedContent(v)); err != nil {
createErrorPanel(g, output)
}
refreshStashEntries(g)
return refreshFiles(g)
})
return nil
}
-41
View File
@@ -1,41 +0,0 @@
package main
import (
"fmt"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
)
func refreshStatus(g *gocui.Gui) error {
v, err := g.View("status")
if err != nil {
panic(err)
}
// for some reason if this isn't wrapped in an update the clear seems to
// be applied after the other things or something like that; the panel's
// contents end up cleared
g.Update(func(*gocui.Gui) error {
v.Clear()
pushables, pullables := gitUpstreamDifferenceCount()
fmt.Fprint(v, "↑"+pushables+"↓"+pullables)
branches := state.Branches
if err := updateHasMergeConflictStatus(); err != nil {
return err
}
if state.HasMergeConflicts {
fmt.Fprint(v, coloredString(" (merging)", color.FgYellow))
}
if len(branches) == 0 {
return nil
}
branch := branches[0]
name := coloredString(branch.Name, branch.getColor())
repo := getCurrentProject()
fmt.Fprint(v, " "+repo+" → "+name)
return nil
})
return nil
}
-30
View File
@@ -1,30 +0,0 @@
#!/bin/bash
# this script makes a repo with lots of commits
# call this command from the test directory:
# ./lots_of_commits.sh; cd testrepo; gg; cd ..
# -e means exit if something fails
# -x means print out simple commands before running them
set -ex
reponame="case_insensitive_checkouts"
rm -rf ${reponame}
mkdir ${reponame}
cd ${reponame}
git init
touch foo
git add foo
git commit -m "init"
git branch -a
git branch test
git branch TEST
git checkout TEST
git checkout TeST
git checkout TesT
git checkout TEsT
git branch -a
-30
View File
@@ -1,30 +0,0 @@
#!/bin/bash
# this script makes a repo with lots of commits
# call this command from the test directory:
# ./lots_of_commits.sh; cd testrepo; gg; cd ..
# -e means exit if something fails
# -x means print out simple commands before running them
set -ex
reponame="lots_of_commits"
rm -rf ${reponame}
mkdir ${reponame}
cd ${reponame}
git init
i=2
end=100
while [ $i -le $end ]; do
echo "file${i}" > file${i}
git add file${i}
git commit -m file${i}
i=$(($i+1))
done
echo "unstaged change" > file100
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
set -ex; rm -rf repo; mkdir repo; cd repo
git init
git config user.email "test@example.com"
git config user.name "Lazygit Tester"
touch foo
git add foo
git commit -m "init"
git branch -a
git branch test
git branch TEST
git checkout TEST
git checkout TeST
git checkout TesT
git checkout TEsT
git branch -a
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
echo "test1"
sleep 1
echo "test2"
sleep 1
echo "test3"
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
set -ex; rm -rf repo; mkdir repo; cd repo
git init
git config user.email "test@example.com"
git config user.name "Lazygit Tester"
git config gpg.program $(which gpg)
git config user.signingkey E304229F # test key
git config commit.gpgsign true
git config credential.helper store
git config credential.helper cache 1
touch foo
git add foo
touch bar
git add bar
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
set -ex; rm -rf repo; mkdir repo; cd repo
git init
git config user.email "test@example.com"
git config user.name "Lazygit Tester"
i=2
end=100
while [ $i -le $end ]; do
echo "file${i}" > file${i}
git add file${i}
git commit -m file${i}
i=$(($i+1))
done
echo "unstaged change" > file100
+35
View File
@@ -0,0 +1,35 @@
#!/bin/bash
set -ex; rm -rf repo; mkdir repo; cd repo
git init
git config user.email "test@example.com"
git config user.name "Lazygit Tester"
echo "deleted" > deleted_staged
echo "deleted_unstaged" > deleted_unstaged
echo "modified_staged" > modified_staged
echo "modified_unstaged" > modified_unstaged
echo "renamed" > renamed_before
git add .
git commit -m "files to delete"
rm deleted_staged
rm deleted_unstaged
rm renamed_before
echo "renamed" > renamed_after
echo "more" >> modified_staged
echo "more" >> modified_unstaged
echo "untracked_staged" > untracked_staged
echo "untracked_unstaged" > untracked_unstaged
echo "blah" > "file with space staged"
echo "blah" > "file with space unstaged"
echo "same name as branch" > master
git add deleted_staged
git add modified_staged
git add untracked_staged
git add "file with space staged"
git add renamed_before
git add renamed_after
@@ -1,22 +1,10 @@
#!/bin/bash
# this script will make a repo with a master and develop branch, where we end up
# on the master branch and if we try and merge master we get a merge conflict
# call this command from the test directory:
# ./merge_conflict.sh; cd testrepo; gg; cd ..
# -e means exit if something fails
# -x means print out simple commands before running them
set -ex
reponame="testrepo"
rm -rf ${reponame}
mkdir ${reponame}
cd ${reponame}
set -ex; rm -rf repo; mkdir repo; cd repo
git init
git config user.email "test@example.com"
git config user.name "Lazygit Tester"
function add_spacing {
for i in {1..60}
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
set -ex; rm -rf repo; mkdir repo; cd repo
git init
git config user.email "test@example.com"
git config user.name "Lazygit Tester"
cp ../extras/pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
echo "file" > file
git add file
@@ -1,22 +1,10 @@
#!/bin/bash
# this script will make a repo with a master and develop branch, where we end up
# on the master branch and if we try and merge master we get a merge conflict
# call this command from the test directory:
# ./generate_basic_repo.sh; cd testrepo; gg; cd ..
# -e means exit if something fails
# -x means print out simple commands before running them
set -ex
reponame="unicode_characters"
rm -rf ${reponame}
mkdir ${reponame}
cd ${reponame}
set -ex; rm -rf repo; mkdir repo; cd repo
git init
git config user.email "test@example.com"
git config user.name "Lazygit Tester"
# Add some ansi, unicode, zero width joiner caracters
cat <<EOT >> charstest.txt
-54
View File
@@ -1,54 +0,0 @@
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
)
func splitLines(multilineString string) []string {
multilineString = strings.Replace(multilineString, "\r", "", -1)
if multilineString == "" || multilineString == "\n" {
return make([]string, 0)
}
lines := strings.Split(multilineString, "\n")
if lines[len(lines)-1] == "" {
return lines[:len(lines)-1]
}
return lines
}
func trimmedContent(v *gocui.View) string {
return strings.TrimSpace(v.Buffer())
}
func withPadding(str string, padding int) string {
if padding-len(str) < 0 {
return str
}
return str + strings.Repeat(" ", padding-len(str))
}
func coloredString(str string, colorAttribute color.Attribute) string {
colour := color.New(colorAttribute)
return coloredStringDirect(str, colour)
}
// used for aggregating a few color attributes rather than just sending a single one
func coloredStringDirect(str string, colour *color.Color) string {
return colour.SprintFunc()(fmt.Sprint(str))
}
// used to get the project name
func getCurrentProject() string {
pwd, err := os.Getwd()
if err != nil {
log.Fatalln(err.Error())
}
return filepath.Base(pwd)
}
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 Simon Eskildsen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+64
View File
@@ -0,0 +1,64 @@
package logrus
// The following code was sourced and modified from the
// https://github.com/tebeka/atexit package governed by the following license:
//
// Copyright (c) 2012 Miki Tebeka <miki.tebeka@gmail.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import (
"fmt"
"os"
)
var handlers = []func(){}
func runHandler(handler func()) {
defer func() {
if err := recover(); err != nil {
fmt.Fprintln(os.Stderr, "Error: Logrus exit handler error:", err)
}
}()
handler()
}
func runHandlers() {
for _, handler := range handlers {
runHandler(handler)
}
}
// Exit runs all the Logrus atexit handlers and then terminates the program using os.Exit(code)
func Exit(code int) {
runHandlers()
os.Exit(code)
}
// RegisterExitHandler adds a Logrus Exit handler, call logrus.Exit to invoke
// all handlers. The handlers will also be invoked when any Fatal log entry is
// made.
//
// This method is useful when a caller wishes to use logrus to log a fatal
// message but also needs to gracefully shutdown. An example usecase could be
// closing database connections, or sending a alert that the application is
// closing.
func RegisterExitHandler(handler func()) {
handlers = append(handlers, handler)
}
+26
View File
@@ -0,0 +1,26 @@
/*
Package logrus is a structured logger for Go, completely API compatible with the standard library logger.
The simplest way to use Logrus is simply the package-level exported logger:
package main
import (
log "github.com/sirupsen/logrus"
)
func main() {
log.WithFields(log.Fields{
"animal": "walrus",
"number": 1,
"size": 10,
}).Info("A walrus appears")
}
Output:
time="2015-09-07T08:48:33Z" level=info msg="A walrus appears" animal=walrus number=1 size=10
For a full guide visit https://github.com/sirupsen/logrus
*/
package logrus
+300
View File
@@ -0,0 +1,300 @@
package logrus
import (
"bytes"
"fmt"
"os"
"sync"
"time"
)
var bufferPool *sync.Pool
func init() {
bufferPool = &sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
}
// Defines the key when adding errors using WithError.
var ErrorKey = "error"
// An entry is the final or intermediate Logrus logging entry. It contains all
// the fields passed with WithField{,s}. It's finally logged when Debug, Info,
// Warn, Error, Fatal or Panic is called on it. These objects can be reused and
// passed around as much as you wish to avoid field duplication.
type Entry struct {
Logger *Logger
// Contains all the fields set by the user.
Data Fields
// Time at which the log entry was created
Time time.Time
// Level the log entry was logged at: Debug, Info, Warn, Error, Fatal or Panic
// This field will be set on entry firing and the value will be equal to the one in Logger struct field.
Level Level
// Message passed to Debug, Info, Warn, Error, Fatal or Panic
Message string
// When formatter is called in entry.log(), an Buffer may be set to entry
Buffer *bytes.Buffer
}
func NewEntry(logger *Logger) *Entry {
return &Entry{
Logger: logger,
// Default is five fields, give a little extra room
Data: make(Fields, 5),
}
}
// Returns the string representation from the reader and ultimately the
// formatter.
func (entry *Entry) String() (string, error) {
serialized, err := entry.Logger.Formatter.Format(entry)
if err != nil {
return "", err
}
str := string(serialized)
return str, nil
}
// Add an error as single field (using the key defined in ErrorKey) to the Entry.
func (entry *Entry) WithError(err error) *Entry {
return entry.WithField(ErrorKey, err)
}
// Add a single field to the Entry.
func (entry *Entry) WithField(key string, value interface{}) *Entry {
return entry.WithFields(Fields{key: value})
}
// Add a map of fields to the Entry.
func (entry *Entry) WithFields(fields Fields) *Entry {
data := make(Fields, len(entry.Data)+len(fields))
for k, v := range entry.Data {
data[k] = v
}
for k, v := range fields {
data[k] = v
}
return &Entry{Logger: entry.Logger, Data: data, Time: entry.Time}
}
// Overrides the time of the Entry.
func (entry *Entry) WithTime(t time.Time) *Entry {
return &Entry{Logger: entry.Logger, Data: entry.Data, Time: t}
}
// This function is not declared with a pointer value because otherwise
// race conditions will occur when using multiple goroutines
func (entry Entry) log(level Level, msg string) {
var buffer *bytes.Buffer
// Default to now, but allow users to override if they want.
//
// We don't have to worry about polluting future calls to Entry#log()
// with this assignment because this function is declared with a
// non-pointer receiver.
if entry.Time.IsZero() {
entry.Time = time.Now()
}
entry.Level = level
entry.Message = msg
entry.fireHooks()
buffer = bufferPool.Get().(*bytes.Buffer)
buffer.Reset()
defer bufferPool.Put(buffer)
entry.Buffer = buffer
entry.write()
entry.Buffer = nil
// To avoid Entry#log() returning a value that only would make sense for
// panic() to use in Entry#Panic(), we avoid the allocation by checking
// directly here.
if level <= PanicLevel {
panic(&entry)
}
}
func (entry *Entry) fireHooks() {
entry.Logger.mu.Lock()
defer entry.Logger.mu.Unlock()
err := entry.Logger.Hooks.Fire(entry.Level, entry)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err)
}
}
func (entry *Entry) write() {
serialized, err := entry.Logger.Formatter.Format(entry)
entry.Logger.mu.Lock()
defer entry.Logger.mu.Unlock()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err)
} else {
_, err = entry.Logger.Out.Write(serialized)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
}
}
}
func (entry *Entry) Debug(args ...interface{}) {
if entry.Logger.level() >= DebugLevel {
entry.log(DebugLevel, fmt.Sprint(args...))
}
}
func (entry *Entry) Print(args ...interface{}) {
entry.Info(args...)
}
func (entry *Entry) Info(args ...interface{}) {
if entry.Logger.level() >= InfoLevel {
entry.log(InfoLevel, fmt.Sprint(args...))
}
}
func (entry *Entry) Warn(args ...interface{}) {
if entry.Logger.level() >= WarnLevel {
entry.log(WarnLevel, fmt.Sprint(args...))
}
}
func (entry *Entry) Warning(args ...interface{}) {
entry.Warn(args...)
}
func (entry *Entry) Error(args ...interface{}) {
if entry.Logger.level() >= ErrorLevel {
entry.log(ErrorLevel, fmt.Sprint(args...))
}
}
func (entry *Entry) Fatal(args ...interface{}) {
if entry.Logger.level() >= FatalLevel {
entry.log(FatalLevel, fmt.Sprint(args...))
}
Exit(1)
}
func (entry *Entry) Panic(args ...interface{}) {
if entry.Logger.level() >= PanicLevel {
entry.log(PanicLevel, fmt.Sprint(args...))
}
panic(fmt.Sprint(args...))
}
// Entry Printf family functions
func (entry *Entry) Debugf(format string, args ...interface{}) {
if entry.Logger.level() >= DebugLevel {
entry.Debug(fmt.Sprintf(format, args...))
}
}
func (entry *Entry) Infof(format string, args ...interface{}) {
if entry.Logger.level() >= InfoLevel {
entry.Info(fmt.Sprintf(format, args...))
}
}
func (entry *Entry) Printf(format string, args ...interface{}) {
entry.Infof(format, args...)
}
func (entry *Entry) Warnf(format string, args ...interface{}) {
if entry.Logger.level() >= WarnLevel {
entry.Warn(fmt.Sprintf(format, args...))
}
}
func (entry *Entry) Warningf(format string, args ...interface{}) {
entry.Warnf(format, args...)
}
func (entry *Entry) Errorf(format string, args ...interface{}) {
if entry.Logger.level() >= ErrorLevel {
entry.Error(fmt.Sprintf(format, args...))
}
}
func (entry *Entry) Fatalf(format string, args ...interface{}) {
if entry.Logger.level() >= FatalLevel {
entry.Fatal(fmt.Sprintf(format, args...))
}
Exit(1)
}
func (entry *Entry) Panicf(format string, args ...interface{}) {
if entry.Logger.level() >= PanicLevel {
entry.Panic(fmt.Sprintf(format, args...))
}
}
// Entry Println family functions
func (entry *Entry) Debugln(args ...interface{}) {
if entry.Logger.level() >= DebugLevel {
entry.Debug(entry.sprintlnn(args...))
}
}
func (entry *Entry) Infoln(args ...interface{}) {
if entry.Logger.level() >= InfoLevel {
entry.Info(entry.sprintlnn(args...))
}
}
func (entry *Entry) Println(args ...interface{}) {
entry.Infoln(args...)
}
func (entry *Entry) Warnln(args ...interface{}) {
if entry.Logger.level() >= WarnLevel {
entry.Warn(entry.sprintlnn(args...))
}
}
func (entry *Entry) Warningln(args ...interface{}) {
entry.Warnln(args...)
}
func (entry *Entry) Errorln(args ...interface{}) {
if entry.Logger.level() >= ErrorLevel {
entry.Error(entry.sprintlnn(args...))
}
}
func (entry *Entry) Fatalln(args ...interface{}) {
if entry.Logger.level() >= FatalLevel {
entry.Fatal(entry.sprintlnn(args...))
}
Exit(1)
}
func (entry *Entry) Panicln(args ...interface{}) {
if entry.Logger.level() >= PanicLevel {
entry.Panic(entry.sprintlnn(args...))
}
}
// Sprintlnn => Sprint no newline. This is to get the behavior of how
// fmt.Sprintln where spaces are always added between operands, regardless of
// their type. Instead of vendoring the Sprintln implementation to spare a
// string allocation, we do the simplest thing.
func (entry *Entry) sprintlnn(args ...interface{}) string {
msg := fmt.Sprintln(args...)
return msg[:len(msg)-1]
}
+201
View File
@@ -0,0 +1,201 @@
package logrus
import (
"io"
"time"
)
var (
// std is the name of the standard logger in stdlib `log`
std = New()
)
func StandardLogger() *Logger {
return std
}
// SetOutput sets the standard logger output.
func SetOutput(out io.Writer) {
std.SetOutput(out)
}
// SetFormatter sets the standard logger formatter.
func SetFormatter(formatter Formatter) {
std.mu.Lock()
defer std.mu.Unlock()
std.Formatter = formatter
}
// SetLevel sets the standard logger level.
func SetLevel(level Level) {
std.mu.Lock()
defer std.mu.Unlock()
std.SetLevel(level)
}
// GetLevel returns the standard logger level.
func GetLevel() Level {
std.mu.Lock()
defer std.mu.Unlock()
return std.level()
}
// AddHook adds a hook to the standard logger hooks.
func AddHook(hook Hook) {
std.mu.Lock()
defer std.mu.Unlock()
std.Hooks.Add(hook)
}
// WithError creates an entry from the standard logger and adds an error to it, using the value defined in ErrorKey as key.
func WithError(err error) *Entry {
return std.WithField(ErrorKey, err)
}
// WithField creates an entry from the standard logger and adds a field to
// it. If you want multiple fields, use `WithFields`.
//
// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
// or Panic on the Entry it returns.
func WithField(key string, value interface{}) *Entry {
return std.WithField(key, value)
}
// WithFields creates an entry from the standard logger and adds multiple
// fields to it. This is simply a helper for `WithField`, invoking it
// once for each field.
//
// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
// or Panic on the Entry it returns.
func WithFields(fields Fields) *Entry {
return std.WithFields(fields)
}
// WithTime creats an entry from the standard logger and overrides the time of
// logs generated with it.
//
// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
// or Panic on the Entry it returns.
func WithTime(t time.Time) *Entry {
return std.WithTime(t)
}
// Debug logs a message at level Debug on the standard logger.
func Debug(args ...interface{}) {
std.Debug(args...)
}
// Print logs a message at level Info on the standard logger.
func Print(args ...interface{}) {
std.Print(args...)
}
// Info logs a message at level Info on the standard logger.
func Info(args ...interface{}) {
std.Info(args...)
}
// Warn logs a message at level Warn on the standard logger.
func Warn(args ...interface{}) {
std.Warn(args...)
}
// Warning logs a message at level Warn on the standard logger.
func Warning(args ...interface{}) {
std.Warning(args...)
}
// Error logs a message at level Error on the standard logger.
func Error(args ...interface{}) {
std.Error(args...)
}
// Panic logs a message at level Panic on the standard logger.
func Panic(args ...interface{}) {
std.Panic(args...)
}
// Fatal logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
func Fatal(args ...interface{}) {
std.Fatal(args...)
}
// Debugf logs a message at level Debug on the standard logger.
func Debugf(format string, args ...interface{}) {
std.Debugf(format, args...)
}
// Printf logs a message at level Info on the standard logger.
func Printf(format string, args ...interface{}) {
std.Printf(format, args...)
}
// Infof logs a message at level Info on the standard logger.
func Infof(format string, args ...interface{}) {
std.Infof(format, args...)
}
// Warnf logs a message at level Warn on the standard logger.
func Warnf(format string, args ...interface{}) {
std.Warnf(format, args...)
}
// Warningf logs a message at level Warn on the standard logger.
func Warningf(format string, args ...interface{}) {
std.Warningf(format, args...)
}
// Errorf logs a message at level Error on the standard logger.
func Errorf(format string, args ...interface{}) {
std.Errorf(format, args...)
}
// Panicf logs a message at level Panic on the standard logger.
func Panicf(format string, args ...interface{}) {
std.Panicf(format, args...)
}
// Fatalf logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
func Fatalf(format string, args ...interface{}) {
std.Fatalf(format, args...)
}
// Debugln logs a message at level Debug on the standard logger.
func Debugln(args ...interface{}) {
std.Debugln(args...)
}
// Println logs a message at level Info on the standard logger.
func Println(args ...interface{}) {
std.Println(args...)
}
// Infoln logs a message at level Info on the standard logger.
func Infoln(args ...interface{}) {
std.Infoln(args...)
}
// Warnln logs a message at level Warn on the standard logger.
func Warnln(args ...interface{}) {
std.Warnln(args...)
}
// Warningln logs a message at level Warn on the standard logger.
func Warningln(args ...interface{}) {
std.Warningln(args...)
}
// Errorln logs a message at level Error on the standard logger.
func Errorln(args ...interface{}) {
std.Errorln(args...)
}
// Panicln logs a message at level Panic on the standard logger.
func Panicln(args ...interface{}) {
std.Panicln(args...)
}
// Fatalln logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
func Fatalln(args ...interface{}) {
std.Fatalln(args...)
}
+51
View File
@@ -0,0 +1,51 @@
package logrus
import "time"
const defaultTimestampFormat = time.RFC3339
// The Formatter interface is used to implement a custom Formatter. It takes an
// `Entry`. It exposes all the fields, including the default ones:
//
// * `entry.Data["msg"]`. The message passed from Info, Warn, Error ..
// * `entry.Data["time"]`. The timestamp.
// * `entry.Data["level"]. The level the entry was logged at.
//
// Any additional fields added with `WithField` or `WithFields` are also in
// `entry.Data`. Format is expected to return an array of bytes which are then
// logged to `logger.Out`.
type Formatter interface {
Format(*Entry) ([]byte, error)
}
// This is to not silently overwrite `time`, `msg` and `level` fields when
// dumping it. If this code wasn't there doing:
//
// logrus.WithField("level", 1).Info("hello")
//
// Would just silently drop the user provided level. Instead with this code
// it'll logged as:
//
// {"level": "info", "fields.level": 1, "msg": "hello", "time": "..."}
//
// It's not exported because it's still using Data in an opinionated way. It's to
// avoid code duplication between the two default formatters.
func prefixFieldClashes(data Fields, fieldMap FieldMap) {
timeKey := fieldMap.resolve(FieldKeyTime)
if t, ok := data[timeKey]; ok {
data["fields."+timeKey] = t
delete(data, timeKey)
}
msgKey := fieldMap.resolve(FieldKeyMsg)
if m, ok := data[msgKey]; ok {
data["fields."+msgKey] = m
delete(data, msgKey)
}
levelKey := fieldMap.resolve(FieldKeyLevel)
if l, ok := data[levelKey]; ok {
data["fields."+levelKey] = l
delete(data, levelKey)
}
}
+34
View File
@@ -0,0 +1,34 @@
package logrus
// A hook to be fired when logging on the logging levels returned from
// `Levels()` on your implementation of the interface. Note that this is not
// fired in a goroutine or a channel with workers, you should handle such
// functionality yourself if your call is non-blocking and you don't wish for
// the logging calls for levels returned from `Levels()` to block.
type Hook interface {
Levels() []Level
Fire(*Entry) error
}
// Internal type for storing the hooks on a logger instance.
type LevelHooks map[Level][]Hook
// Add a hook to an instance of logger. This is called with
// `log.Hooks.Add(new(MyHook))` where `MyHook` implements the `Hook` interface.
func (hooks LevelHooks) Add(hook Hook) {
for _, level := range hook.Levels() {
hooks[level] = append(hooks[level], hook)
}
}
// Fire all the hooks for the passed level. Used by `entry.log` to fire
// appropriate hooks for a log entry.
func (hooks LevelHooks) Fire(level Level, entry *Entry) error {
for _, hook := range hooks[level] {
if err := hook.Fire(entry); err != nil {
return err
}
}
return nil
}
+89
View File
@@ -0,0 +1,89 @@
package logrus
import (
"encoding/json"
"fmt"
)
type fieldKey string
// FieldMap allows customization of the key names for default fields.
type FieldMap map[fieldKey]string
// Default key names for the default fields
const (
FieldKeyMsg = "msg"
FieldKeyLevel = "level"
FieldKeyTime = "time"
)
func (f FieldMap) resolve(key fieldKey) string {
if k, ok := f[key]; ok {
return k
}
return string(key)
}
// JSONFormatter formats logs into parsable json
type JSONFormatter struct {
// TimestampFormat sets the format used for marshaling timestamps.
TimestampFormat string
// DisableTimestamp allows disabling automatic timestamps in output
DisableTimestamp bool
// DataKey allows users to put all the log entry parameters into a nested dictionary at a given key.
DataKey string
// FieldMap allows users to customize the names of keys for default fields.
// As an example:
// formatter := &JSONFormatter{
// FieldMap: FieldMap{
// FieldKeyTime: "@timestamp",
// FieldKeyLevel: "@level",
// FieldKeyMsg: "@message",
// },
// }
FieldMap FieldMap
}
// Format renders a single log entry
func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
data := make(Fields, len(entry.Data)+3)
for k, v := range entry.Data {
switch v := v.(type) {
case error:
// Otherwise errors are ignored by `encoding/json`
// https://github.com/sirupsen/logrus/issues/137
data[k] = v.Error()
default:
data[k] = v
}
}
if f.DataKey != "" {
newData := make(Fields, 4)
newData[f.DataKey] = data
data = newData
}
prefixFieldClashes(data, f.FieldMap)
timestampFormat := f.TimestampFormat
if timestampFormat == "" {
timestampFormat = defaultTimestampFormat
}
if !f.DisableTimestamp {
data[f.FieldMap.resolve(FieldKeyTime)] = entry.Time.Format(timestampFormat)
}
data[f.FieldMap.resolve(FieldKeyMsg)] = entry.Message
data[f.FieldMap.resolve(FieldKeyLevel)] = entry.Level.String()
serialized, err := json.Marshal(data)
if err != nil {
return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err)
}
return append(serialized, '\n'), nil
}
+337
View File
@@ -0,0 +1,337 @@
package logrus
import (
"io"
"os"
"sync"
"sync/atomic"
"time"
)
type Logger struct {
// The logs are `io.Copy`'d to this in a mutex. It's common to set this to a
// file, or leave it default which is `os.Stderr`. You can also set this to
// something more adventorous, such as logging to Kafka.
Out io.Writer
// Hooks for the logger instance. These allow firing events based on logging
// levels and log entries. For example, to send errors to an error tracking
// service, log to StatsD or dump the core on fatal errors.
Hooks LevelHooks
// All log entries pass through the formatter before logged to Out. The
// included formatters are `TextFormatter` and `JSONFormatter` for which
// TextFormatter is the default. In development (when a TTY is attached) it
// logs with colors, but to a file it wouldn't. You can easily implement your
// own that implements the `Formatter` interface, see the `README` or included
// formatters for examples.
Formatter Formatter
// The logging level the logger should log at. This is typically (and defaults
// to) `logrus.Info`, which allows Info(), Warn(), Error() and Fatal() to be
// logged.
Level Level
// Used to sync writing to the log. Locking is enabled by Default
mu MutexWrap
// Reusable empty entry
entryPool sync.Pool
}
type MutexWrap struct {
lock sync.Mutex
disabled bool
}
func (mw *MutexWrap) Lock() {
if !mw.disabled {
mw.lock.Lock()
}
}
func (mw *MutexWrap) Unlock() {
if !mw.disabled {
mw.lock.Unlock()
}
}
func (mw *MutexWrap) Disable() {
mw.disabled = true
}
// Creates a new logger. Configuration should be set by changing `Formatter`,
// `Out` and `Hooks` directly on the default logger instance. You can also just
// instantiate your own:
//
// var log = &Logger{
// Out: os.Stderr,
// Formatter: new(JSONFormatter),
// Hooks: make(LevelHooks),
// Level: logrus.DebugLevel,
// }
//
// It's recommended to make this a global instance called `log`.
func New() *Logger {
return &Logger{
Out: os.Stderr,
Formatter: new(TextFormatter),
Hooks: make(LevelHooks),
Level: InfoLevel,
}
}
func (logger *Logger) newEntry() *Entry {
entry, ok := logger.entryPool.Get().(*Entry)
if ok {
return entry
}
return NewEntry(logger)
}
func (logger *Logger) releaseEntry(entry *Entry) {
logger.entryPool.Put(entry)
}
// Adds a field to the log entry, note that it doesn't log until you call
// Debug, Print, Info, Warn, Error, Fatal or Panic. It only creates a log entry.
// If you want multiple fields, use `WithFields`.
func (logger *Logger) WithField(key string, value interface{}) *Entry {
entry := logger.newEntry()
defer logger.releaseEntry(entry)
return entry.WithField(key, value)
}
// Adds a struct of fields to the log entry. All it does is call `WithField` for
// each `Field`.
func (logger *Logger) WithFields(fields Fields) *Entry {
entry := logger.newEntry()
defer logger.releaseEntry(entry)
return entry.WithFields(fields)
}
// Add an error as single field to the log entry. All it does is call
// `WithError` for the given `error`.
func (logger *Logger) WithError(err error) *Entry {
entry := logger.newEntry()
defer logger.releaseEntry(entry)
return entry.WithError(err)
}
// Overrides the time of the log entry.
func (logger *Logger) WithTime(t time.Time) *Entry {
entry := logger.newEntry()
defer logger.releaseEntry(entry)
return entry.WithTime(t)
}
func (logger *Logger) Debugf(format string, args ...interface{}) {
if logger.level() >= DebugLevel {
entry := logger.newEntry()
entry.Debugf(format, args...)
logger.releaseEntry(entry)
}
}
func (logger *Logger) Infof(format string, args ...interface{}) {
if logger.level() >= InfoLevel {
entry := logger.newEntry()
entry.Infof(format, args...)
logger.releaseEntry(entry)
}
}
func (logger *Logger) Printf(format string, args ...interface{}) {
entry := logger.newEntry()
entry.Printf(format, args...)
logger.releaseEntry(entry)
}
func (logger *Logger) Warnf(format string, args ...interface{}) {
if logger.level() >= WarnLevel {
entry := logger.newEntry()
entry.Warnf(format, args...)
logger.releaseEntry(entry)
}
}
func (logger *Logger) Warningf(format string, args ...interface{}) {
if logger.level() >= WarnLevel {
entry := logger.newEntry()
entry.Warnf(format, args...)
logger.releaseEntry(entry)
}
}
func (logger *Logger) Errorf(format string, args ...interface{}) {
if logger.level() >= ErrorLevel {
entry := logger.newEntry()
entry.Errorf(format, args...)
logger.releaseEntry(entry)
}
}
func (logger *Logger) Fatalf(format string, args ...interface{}) {
if logger.level() >= FatalLevel {
entry := logger.newEntry()
entry.Fatalf(format, args...)
logger.releaseEntry(entry)
}
Exit(1)
}
func (logger *Logger) Panicf(format string, args ...interface{}) {
if logger.level() >= PanicLevel {
entry := logger.newEntry()
entry.Panicf(format, args...)
logger.releaseEntry(entry)
}
}
func (logger *Logger) Debug(args ...interface{}) {
if logger.level() >= DebugLevel {
entry := logger.newEntry()
entry.Debug(args...)
logger.releaseEntry(entry)
}
}
func (logger *Logger) Info(args ...interface{}) {
if logger.level() >= InfoLevel {
entry := logger.newEntry()
entry.Info(args...)
logger.releaseEntry(entry)
}
}
func (logger *Logger) Print(args ...interface{}) {
entry := logger.newEntry()
entry.Info(args...)
logger.releaseEntry(entry)
}
func (logger *Logger) Warn(args ...interface{}) {
if logger.level() >= WarnLevel {
entry := logger.newEntry()
entry.Warn(args...)
logger.releaseEntry(entry)
}
}
func (logger *Logger) Warning(args ...interface{}) {
if logger.level() >= WarnLevel {
entry := logger.newEntry()
entry.Warn(args...)
logger.releaseEntry(entry)
}
}
func (logger *Logger) Error(args ...interface{}) {
if logger.level() >= ErrorLevel {
entry := logger.newEntry()
entry.Error(args...)
logger.releaseEntry(entry)
}
}
func (logger *Logger) Fatal(args ...interface{}) {
if logger.level() >= FatalLevel {
entry := logger.newEntry()
entry.Fatal(args...)
logger.releaseEntry(entry)
}
Exit(1)
}
func (logger *Logger) Panic(args ...interface{}) {
if logger.level() >= PanicLevel {
entry := logger.newEntry()
entry.Panic(args...)
logger.releaseEntry(entry)
}
}
func (logger *Logger) Debugln(args ...interface{}) {
if logger.level() >= DebugLevel {
entry := logger.newEntry()
entry.Debugln(args...)
logger.releaseEntry(entry)
}
}
func (logger *Logger) Infoln(args ...interface{}) {
if logger.level() >= InfoLevel {
entry := logger.newEntry()
entry.Infoln(args...)
logger.releaseEntry(entry)
}
}
func (logger *Logger) Println(args ...interface{}) {
entry := logger.newEntry()
entry.Println(args...)
logger.releaseEntry(entry)
}
func (logger *Logger) Warnln(args ...interface{}) {
if logger.level() >= WarnLevel {
entry := logger.newEntry()
entry.Warnln(args...)
logger.releaseEntry(entry)
}
}
func (logger *Logger) Warningln(args ...interface{}) {
if logger.level() >= WarnLevel {
entry := logger.newEntry()
entry.Warnln(args...)
logger.releaseEntry(entry)
}
}
func (logger *Logger) Errorln(args ...interface{}) {
if logger.level() >= ErrorLevel {
entry := logger.newEntry()
entry.Errorln(args...)
logger.releaseEntry(entry)
}
}
func (logger *Logger) Fatalln(args ...interface{}) {
if logger.level() >= FatalLevel {
entry := logger.newEntry()
entry.Fatalln(args...)
logger.releaseEntry(entry)
}
Exit(1)
}
func (logger *Logger) Panicln(args ...interface{}) {
if logger.level() >= PanicLevel {
entry := logger.newEntry()
entry.Panicln(args...)
logger.releaseEntry(entry)
}
}
//When file is opened with appending mode, it's safe to
//write concurrently to a file (within 4k message on Linux).
//In these cases user can choose to disable the lock.
func (logger *Logger) SetNoLock() {
logger.mu.Disable()
}
func (logger *Logger) level() Level {
return Level(atomic.LoadUint32((*uint32)(&logger.Level)))
}
func (logger *Logger) SetLevel(level Level) {
atomic.StoreUint32((*uint32)(&logger.Level), uint32(level))
}
func (logger *Logger) SetOutput(out io.Writer) {
logger.mu.Lock()
defer logger.mu.Unlock()
logger.Out = out
}
func (logger *Logger) AddHook(hook Hook) {
logger.mu.Lock()
defer logger.mu.Unlock()
logger.Hooks.Add(hook)
}
+143
View File
@@ -0,0 +1,143 @@
package logrus
import (
"fmt"
"log"
"strings"
)
// Fields type, used to pass to `WithFields`.
type Fields map[string]interface{}
// Level type
type Level uint32
// Convert the Level to a string. E.g. PanicLevel becomes "panic".
func (level Level) String() string {
switch level {
case DebugLevel:
return "debug"
case InfoLevel:
return "info"
case WarnLevel:
return "warning"
case ErrorLevel:
return "error"
case FatalLevel:
return "fatal"
case PanicLevel:
return "panic"
}
return "unknown"
}
// ParseLevel takes a string level and returns the Logrus log level constant.
func ParseLevel(lvl string) (Level, error) {
switch strings.ToLower(lvl) {
case "panic":
return PanicLevel, nil
case "fatal":
return FatalLevel, nil
case "error":
return ErrorLevel, nil
case "warn", "warning":
return WarnLevel, nil
case "info":
return InfoLevel, nil
case "debug":
return DebugLevel, nil
}
var l Level
return l, fmt.Errorf("not a valid logrus Level: %q", lvl)
}
// A constant exposing all logging levels
var AllLevels = []Level{
PanicLevel,
FatalLevel,
ErrorLevel,
WarnLevel,
InfoLevel,
DebugLevel,
}
// These are the different logging levels. You can set the logging level to log
// on your instance of logger, obtained with `logrus.New()`.
const (
// PanicLevel level, highest level of severity. Logs and then calls panic with the
// message passed to Debug, Info, ...
PanicLevel Level = iota
// FatalLevel level. Logs and then calls `os.Exit(1)`. It will exit even if the
// logging level is set to Panic.
FatalLevel
// ErrorLevel level. Logs. Used for errors that should definitely be noted.
// Commonly used for hooks to send errors to an error tracking service.
ErrorLevel
// WarnLevel level. Non-critical entries that deserve eyes.
WarnLevel
// InfoLevel level. General operational entries about what's going on inside the
// application.
InfoLevel
// DebugLevel level. Usually only enabled when debugging. Very verbose logging.
DebugLevel
)
// Won't compile if StdLogger can't be realized by a log.Logger
var (
_ StdLogger = &log.Logger{}
_ StdLogger = &Entry{}
_ StdLogger = &Logger{}
)
// StdLogger is what your logrus-enabled library should take, that way
// it'll accept a stdlib logger and a logrus logger. There's no standard
// interface, this is the closest we get, unfortunately.
type StdLogger interface {
Print(...interface{})
Printf(string, ...interface{})
Println(...interface{})
Fatal(...interface{})
Fatalf(string, ...interface{})
Fatalln(...interface{})
Panic(...interface{})
Panicf(string, ...interface{})
Panicln(...interface{})
}
// The FieldLogger interface generalizes the Entry and Logger types
type FieldLogger interface {
WithField(key string, value interface{}) *Entry
WithFields(fields Fields) *Entry
WithError(err error) *Entry
Debugf(format string, args ...interface{})
Infof(format string, args ...interface{})
Printf(format string, args ...interface{})
Warnf(format string, args ...interface{})
Warningf(format string, args ...interface{})
Errorf(format string, args ...interface{})
Fatalf(format string, args ...interface{})
Panicf(format string, args ...interface{})
Debug(args ...interface{})
Info(args ...interface{})
Print(args ...interface{})
Warn(args ...interface{})
Warning(args ...interface{})
Error(args ...interface{})
Fatal(args ...interface{})
Panic(args ...interface{})
Debugln(args ...interface{})
Infoln(args ...interface{})
Println(args ...interface{})
Warnln(args ...interface{})
Warningln(args ...interface{})
Errorln(args ...interface{})
Fatalln(args ...interface{})
Panicln(args ...interface{})
}
+10
View File
@@ -0,0 +1,10 @@
// +build darwin freebsd openbsd netbsd dragonfly
// +build !appengine,!gopherjs
package logrus
import "golang.org/x/sys/unix"
const ioctlReadTermios = unix.TIOCGETA
type Termios unix.Termios
+11
View File
@@ -0,0 +1,11 @@
// +build appengine gopherjs
package logrus
import (
"io"
)
func checkIfTerminal(w io.Writer) bool {
return true
}
+19
View File
@@ -0,0 +1,19 @@
// +build !appengine,!gopherjs
package logrus
import (
"io"
"os"
"golang.org/x/crypto/ssh/terminal"
)
func checkIfTerminal(w io.Writer) bool {
switch v := w.(type) {
case *os.File:
return terminal.IsTerminal(int(v.Fd()))
default:
return false
}
}
+14
View File
@@ -0,0 +1,14 @@
// Based on ssh/terminal:
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !appengine,!gopherjs
package logrus
import "golang.org/x/sys/unix"
const ioctlReadTermios = unix.TCGETS
type Termios unix.Termios
+195
View File
@@ -0,0 +1,195 @@
package logrus
import (
"bytes"
"fmt"
"sort"
"strings"
"sync"
"time"
)
const (
nocolor = 0
red = 31
green = 32
yellow = 33
blue = 36
gray = 37
)
var (
baseTimestamp time.Time
emptyFieldMap FieldMap
)
func init() {
baseTimestamp = time.Now()
}
// TextFormatter formats logs into text
type TextFormatter struct {
// Set to true to bypass checking for a TTY before outputting colors.
ForceColors bool
// Force disabling colors.
DisableColors bool
// Disable timestamp logging. useful when output is redirected to logging
// system that already adds timestamps.
DisableTimestamp bool
// Enable logging the full timestamp when a TTY is attached instead of just
// the time passed since beginning of execution.
FullTimestamp bool
// TimestampFormat to use for display when a full timestamp is printed
TimestampFormat string
// The fields are sorted by default for a consistent output. For applications
// that log extremely frequently and don't use the JSON formatter this may not
// be desired.
DisableSorting bool
// Disables the truncation of the level text to 4 characters.
DisableLevelTruncation bool
// QuoteEmptyFields will wrap empty fields in quotes if true
QuoteEmptyFields bool
// Whether the logger's out is to a terminal
isTerminal bool
// FieldMap allows users to customize the names of keys for default fields.
// As an example:
// formatter := &TextFormatter{
// FieldMap: FieldMap{
// FieldKeyTime: "@timestamp",
// FieldKeyLevel: "@level",
// FieldKeyMsg: "@message"}}
FieldMap FieldMap
sync.Once
}
func (f *TextFormatter) init(entry *Entry) {
if entry.Logger != nil {
f.isTerminal = checkIfTerminal(entry.Logger.Out)
}
}
// Format renders a single log entry
func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
prefixFieldClashes(entry.Data, f.FieldMap)
keys := make([]string, 0, len(entry.Data))
for k := range entry.Data {
keys = append(keys, k)
}
if !f.DisableSorting {
sort.Strings(keys)
}
var b *bytes.Buffer
if entry.Buffer != nil {
b = entry.Buffer
} else {
b = &bytes.Buffer{}
}
f.Do(func() { f.init(entry) })
isColored := (f.ForceColors || f.isTerminal) && !f.DisableColors
timestampFormat := f.TimestampFormat
if timestampFormat == "" {
timestampFormat = defaultTimestampFormat
}
if isColored {
f.printColored(b, entry, keys, timestampFormat)
} else {
if !f.DisableTimestamp {
f.appendKeyValue(b, f.FieldMap.resolve(FieldKeyTime), entry.Time.Format(timestampFormat))
}
f.appendKeyValue(b, f.FieldMap.resolve(FieldKeyLevel), entry.Level.String())
if entry.Message != "" {
f.appendKeyValue(b, f.FieldMap.resolve(FieldKeyMsg), entry.Message)
}
for _, key := range keys {
f.appendKeyValue(b, key, entry.Data[key])
}
}
b.WriteByte('\n')
return b.Bytes(), nil
}
func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string, timestampFormat string) {
var levelColor int
switch entry.Level {
case DebugLevel:
levelColor = gray
case WarnLevel:
levelColor = yellow
case ErrorLevel, FatalLevel, PanicLevel:
levelColor = red
default:
levelColor = blue
}
levelText := strings.ToUpper(entry.Level.String())
if !f.DisableLevelTruncation {
levelText = levelText[0:4]
}
if f.DisableTimestamp {
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m %-44s ", levelColor, levelText, entry.Message)
} else if !f.FullTimestamp {
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d] %-44s ", levelColor, levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), entry.Message)
} else {
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s] %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), entry.Message)
}
for _, k := range keys {
v := entry.Data[k]
fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=", levelColor, k)
f.appendValue(b, v)
}
}
func (f *TextFormatter) needsQuoting(text string) bool {
if f.QuoteEmptyFields && len(text) == 0 {
return true
}
for _, ch := range text {
if !((ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') ||
ch == '-' || ch == '.' || ch == '_' || ch == '/' || ch == '@' || ch == '^' || ch == '+') {
return true
}
}
return false
}
func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) {
if b.Len() > 0 {
b.WriteByte(' ')
}
b.WriteString(key)
b.WriteByte('=')
f.appendValue(b, value)
}
func (f *TextFormatter) appendValue(b *bytes.Buffer, value interface{}) {
stringVal, ok := value.(string)
if !ok {
stringVal = fmt.Sprint(value)
}
if !f.needsQuoting(stringVal) {
b.WriteString(stringVal)
} else {
b.WriteString(fmt.Sprintf("%q", stringVal))
}
}
+62
View File
@@ -0,0 +1,62 @@
package logrus
import (
"bufio"
"io"
"runtime"
)
func (logger *Logger) Writer() *io.PipeWriter {
return logger.WriterLevel(InfoLevel)
}
func (logger *Logger) WriterLevel(level Level) *io.PipeWriter {
return NewEntry(logger).WriterLevel(level)
}
func (entry *Entry) Writer() *io.PipeWriter {
return entry.WriterLevel(InfoLevel)
}
func (entry *Entry) WriterLevel(level Level) *io.PipeWriter {
reader, writer := io.Pipe()
var printFunc func(args ...interface{})
switch level {
case DebugLevel:
printFunc = entry.Debug
case InfoLevel:
printFunc = entry.Info
case WarnLevel:
printFunc = entry.Warn
case ErrorLevel:
printFunc = entry.Error
case FatalLevel:
printFunc = entry.Fatal
case PanicLevel:
printFunc = entry.Panic
default:
printFunc = entry.Print
}
go entry.writerScanner(reader, printFunc)
runtime.SetFinalizer(writer, writerFinalizer)
return writer
}
func (entry *Entry) writerScanner(reader *io.PipeReader, printFunc func(args ...interface{})) {
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
printFunc(scanner.Text())
}
if err := scanner.Err(); err != nil {
entry.Errorf("Error while reading from Writer: %s", err)
}
reader.Close()
}
func writerFinalizer(writer *io.PipeWriter) {
writer.Close()
}
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2014 Pivotal
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.
+22
View File
@@ -0,0 +1,22 @@
package jibber_jabber
import (
"strings"
)
const (
COULD_NOT_DETECT_PACKAGE_ERROR_MESSAGE = "Could not detect Language"
)
func splitLocale(locale string) (string, string) {
formattedLocale := strings.Split(locale, ".")[0]
formattedLocale = strings.Replace(formattedLocale, "-", "_", -1)
pieces := strings.Split(formattedLocale, "_")
language := pieces[0]
territory := ""
if len(pieces) > 1 {
territory = strings.Split(formattedLocale, "_")[1]
}
return language, territory
}
+57
View File
@@ -0,0 +1,57 @@
// +build darwin freebsd linux netbsd openbsd
package jibber_jabber
import (
"errors"
"os"
"strings"
)
func getLangFromEnv() (locale string) {
locale = os.Getenv("LC_ALL")
if locale == "" {
locale = os.Getenv("LANG")
}
return
}
func getUnixLocale() (unix_locale string, err error) {
unix_locale = getLangFromEnv()
if unix_locale == "" {
err = errors.New(COULD_NOT_DETECT_PACKAGE_ERROR_MESSAGE)
}
return
}
func DetectIETF() (locale string, err error) {
unix_locale, err := getUnixLocale()
if err == nil {
language, territory := splitLocale(unix_locale)
locale = language
if territory != "" {
locale = strings.Join([]string{language, territory}, "-")
}
}
return
}
func DetectLanguage() (language string, err error) {
unix_locale, err := getUnixLocale()
if err == nil {
language, _ = splitLocale(unix_locale)
}
return
}
func DetectTerritory() (territory string, err error) {
unix_locale, err := getUnixLocale()
if err == nil {
_, territory = splitLocale(unix_locale)
}
return
}
+114
View File
@@ -0,0 +1,114 @@
// +build windows
package jibber_jabber
import (
"errors"
"syscall"
"unsafe"
)
const LOCALE_NAME_MAX_LENGTH uint32 = 85
var SUPPORTED_LOCALES = map[uintptr]string{
0x0407: "de-DE",
0x0409: "en-US",
0x0c0a: "es-ES", //or is it 0x040a
0x040c: "fr-FR",
0x0410: "it-IT",
0x0411: "ja-JA",
0x0412: "ko_KR",
0x0416: "pt-BR",
//0x0419: "ru_RU", - Will add support for Russian when nicksnyder/go-i18n supports Russian
0x0804: "zh-CN",
0x0c04: "zh-HK",
0x0404: "zh-TW",
}
func getWindowsLocaleFrom(sysCall string) (locale string, err error) {
buffer := make([]uint16, LOCALE_NAME_MAX_LENGTH)
dll := syscall.MustLoadDLL("kernel32")
proc := dll.MustFindProc(sysCall)
r, _, dllError := proc.Call(uintptr(unsafe.Pointer(&buffer[0])), uintptr(LOCALE_NAME_MAX_LENGTH))
if r == 0 {
err = errors.New(COULD_NOT_DETECT_PACKAGE_ERROR_MESSAGE + ":\n" + dllError.Error())
return
}
locale = syscall.UTF16ToString(buffer)
return
}
func getAllWindowsLocaleFrom(sysCall string) (string, error) {
dll, err := syscall.LoadDLL("kernel32")
if err != nil {
return "", errors.New("Could not find kernel32 dll")
}
proc, err := dll.FindProc(sysCall)
if err != nil {
return "", err
}
locale, _, dllError := proc.Call()
if locale == 0 {
return "", errors.New(COULD_NOT_DETECT_PACKAGE_ERROR_MESSAGE + ":\n" + dllError.Error())
}
return SUPPORTED_LOCALES[locale], nil
}
func getWindowsLocale() (locale string, err error) {
dll, err := syscall.LoadDLL("kernel32")
if err != nil {
return "", errors.New("Could not find kernel32 dll")
}
proc, err := dll.FindProc("GetVersion")
if err != nil {
return "", err
}
v, _, _ := proc.Call()
windowsVersion := byte(v)
isVistaOrGreater := (windowsVersion >= 6)
if isVistaOrGreater {
locale, err = getWindowsLocaleFrom("GetUserDefaultLocaleName")
if err != nil {
locale, err = getWindowsLocaleFrom("GetSystemDefaultLocaleName")
}
} else if !isVistaOrGreater {
locale, err = getAllWindowsLocaleFrom("GetUserDefaultLCID")
if err != nil {
locale, err = getAllWindowsLocaleFrom("GetSystemDefaultLCID")
}
} else {
panic(v)
}
return
}
func DetectIETF() (locale string, err error) {
locale, err = getWindowsLocale()
return
}
func DetectLanguage() (language string, err error) {
windows_locale, err := getWindowsLocale()
if err == nil {
language, _ = splitLocale(windows_locale)
}
return
}
func DetectTerritory() (territory string, err error) {
windows_locale, err := getWindowsLocale()
if err == nil {
_, territory = splitLocale(windows_locale)
}
return
}
+52
View File
@@ -0,0 +1,52 @@
# Names should be added to this file as
# Name or Organization <email address>
# The email address is not required for organizations.
# You can update this list using the following command:
#
# $ git shortlog -se | awk '{print $2 " " $3 " " $4}'
# Please keep the list sorted.
Aaron L <aaron@bettercoder.net>
Adrien Bustany <adrien@bustany.org>
Amit Krishnan <amit.krishnan@oracle.com>
Anmol Sethi <me@anmol.io>
Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Bruno Bigras <bigras.bruno@gmail.com>
Caleb Spare <cespare@gmail.com>
Case Nelson <case@teammating.com>
Chris Howey <chris@howey.me> <howeyc@gmail.com>
Christoffer Buchholz <christoffer.buchholz@gmail.com>
Daniel Wagner-Hall <dawagner@gmail.com>
Dave Cheney <dave@cheney.net>
Evan Phoenix <evan@fallingsnow.net>
Francisco Souza <f@souza.cc>
Hari haran <hariharan.uno@gmail.com>
John C Barstow
Kelvin Fo <vmirage@gmail.com>
Ken-ichirou MATSUZAWA <chamas@h4.dion.ne.jp>
Matt Layher <mdlayher@gmail.com>
Nathan Youngman <git@nathany.com>
Nickolai Zeldovich <nickolai@csail.mit.edu>
Patrick <patrick@dropbox.com>
Paul Hammond <paul@paulhammond.org>
Pawel Knap <pawelknap88@gmail.com>
Pieter Droogendijk <pieter@binky.org.uk>
Pursuit92 <JoshChase@techpursuit.net>
Riku Voipio <riku.voipio@linaro.org>
Rob Figueiredo <robfig@gmail.com>
Rodrigo Chiossi <rodrigochiossi@gmail.com>
Slawek Ligus <root@ooz.ie>
Soge Zhang <zhssoge@gmail.com>
Tiffany Jernigan <tiffany.jernigan@intel.com>
Tilak Sharma <tilaks@google.com>
Tom Payne <twpayne@gmail.com>
Travis Cline <travis.cline@gmail.com>
Tudor Golubenco <tudor.g@gmail.com>
Vahe Khachikyan <vahe@live.ca>
Yukang <moorekang@gmail.com>
bronze1man <bronze1man@gmail.com>
debrando <denis.brandolini@gmail.com>
henrikedwards <henrik.edwards@gmail.com>
铁哥 <guotie.9@gmail.com>
+28
View File
@@ -0,0 +1,28 @@
Copyright (c) 2012 The Go Authors. All rights reserved.
Copyright (c) 2012 fsnotify Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+37
View File
@@ -0,0 +1,37 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build solaris
package fsnotify
import (
"errors"
)
// Watcher watches a set of files, delivering events to a channel.
type Watcher struct {
Events chan Event
Errors chan error
}
// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events.
func NewWatcher() (*Watcher, error) {
return nil, errors.New("FEN based watcher not yet supported for fsnotify\n")
}
// Close removes all watches and closes the events channel.
func (w *Watcher) Close() error {
return nil
}
// Add starts watching the named file or directory (non-recursively).
func (w *Watcher) Add(name string) error {
return nil
}
// Remove stops watching the the named file or directory (non-recursively).
func (w *Watcher) Remove(name string) error {
return nil
}
+66
View File
@@ -0,0 +1,66 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !plan9
// Package fsnotify provides a platform-independent interface for file system notifications.
package fsnotify
import (
"bytes"
"errors"
"fmt"
)
// Event represents a single file system notification.
type Event struct {
Name string // Relative path to the file or directory.
Op Op // File operation that triggered the event.
}
// Op describes a set of file operations.
type Op uint32
// These are the generalized file operations that can trigger a notification.
const (
Create Op = 1 << iota
Write
Remove
Rename
Chmod
)
func (op Op) String() string {
// Use a buffer for efficient string concatenation
var buffer bytes.Buffer
if op&Create == Create {
buffer.WriteString("|CREATE")
}
if op&Remove == Remove {
buffer.WriteString("|REMOVE")
}
if op&Write == Write {
buffer.WriteString("|WRITE")
}
if op&Rename == Rename {
buffer.WriteString("|RENAME")
}
if op&Chmod == Chmod {
buffer.WriteString("|CHMOD")
}
if buffer.Len() == 0 {
return ""
}
return buffer.String()[1:] // Strip leading pipe
}
// String returns a string representation of the event in the form
// "file: REMOVE|WRITE|..."
func (e Event) String() string {
return fmt.Sprintf("%q: %s", e.Name, e.Op.String())
}
// Common errors that can be reported by a watcher
var ErrEventOverflow = errors.New("fsnotify queue overflow")
+337
View File
@@ -0,0 +1,337 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build linux
package fsnotify
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
"unsafe"
"golang.org/x/sys/unix"
)
// Watcher watches a set of files, delivering events to a channel.
type Watcher struct {
Events chan Event
Errors chan error
mu sync.Mutex // Map access
fd int
poller *fdPoller
watches map[string]*watch // Map of inotify watches (key: path)
paths map[int]string // Map of watched paths (key: watch descriptor)
done chan struct{} // Channel for sending a "quit message" to the reader goroutine
doneResp chan struct{} // Channel to respond to Close
}
// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events.
func NewWatcher() (*Watcher, error) {
// Create inotify fd
fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC)
if fd == -1 {
return nil, errno
}
// Create epoll
poller, err := newFdPoller(fd)
if err != nil {
unix.Close(fd)
return nil, err
}
w := &Watcher{
fd: fd,
poller: poller,
watches: make(map[string]*watch),
paths: make(map[int]string),
Events: make(chan Event),
Errors: make(chan error),
done: make(chan struct{}),
doneResp: make(chan struct{}),
}
go w.readEvents()
return w, nil
}
func (w *Watcher) isClosed() bool {
select {
case <-w.done:
return true
default:
return false
}
}
// Close removes all watches and closes the events channel.
func (w *Watcher) Close() error {
if w.isClosed() {
return nil
}
// Send 'close' signal to goroutine, and set the Watcher to closed.
close(w.done)
// Wake up goroutine
w.poller.wake()
// Wait for goroutine to close
<-w.doneResp
return nil
}
// Add starts watching the named file or directory (non-recursively).
func (w *Watcher) Add(name string) error {
name = filepath.Clean(name)
if w.isClosed() {
return errors.New("inotify instance already closed")
}
const agnosticEvents = unix.IN_MOVED_TO | unix.IN_MOVED_FROM |
unix.IN_CREATE | unix.IN_ATTRIB | unix.IN_MODIFY |
unix.IN_MOVE_SELF | unix.IN_DELETE | unix.IN_DELETE_SELF
var flags uint32 = agnosticEvents
w.mu.Lock()
defer w.mu.Unlock()
watchEntry := w.watches[name]
if watchEntry != nil {
flags |= watchEntry.flags | unix.IN_MASK_ADD
}
wd, errno := unix.InotifyAddWatch(w.fd, name, flags)
if wd == -1 {
return errno
}
if watchEntry == nil {
w.watches[name] = &watch{wd: uint32(wd), flags: flags}
w.paths[wd] = name
} else {
watchEntry.wd = uint32(wd)
watchEntry.flags = flags
}
return nil
}
// Remove stops watching the named file or directory (non-recursively).
func (w *Watcher) Remove(name string) error {
name = filepath.Clean(name)
// Fetch the watch.
w.mu.Lock()
defer w.mu.Unlock()
watch, ok := w.watches[name]
// Remove it from inotify.
if !ok {
return fmt.Errorf("can't remove non-existent inotify watch for: %s", name)
}
// We successfully removed the watch if InotifyRmWatch doesn't return an
// error, we need to clean up our internal state to ensure it matches
// inotify's kernel state.
delete(w.paths, int(watch.wd))
delete(w.watches, name)
// inotify_rm_watch will return EINVAL if the file has been deleted;
// the inotify will already have been removed.
// watches and pathes are deleted in ignoreLinux() implicitly and asynchronously
// by calling inotify_rm_watch() below. e.g. readEvents() goroutine receives IN_IGNORE
// so that EINVAL means that the wd is being rm_watch()ed or its file removed
// by another thread and we have not received IN_IGNORE event.
success, errno := unix.InotifyRmWatch(w.fd, watch.wd)
if success == -1 {
// TODO: Perhaps it's not helpful to return an error here in every case.
// the only two possible errors are:
// EBADF, which happens when w.fd is not a valid file descriptor of any kind.
// EINVAL, which is when fd is not an inotify descriptor or wd is not a valid watch descriptor.
// Watch descriptors are invalidated when they are removed explicitly or implicitly;
// explicitly by inotify_rm_watch, implicitly when the file they are watching is deleted.
return errno
}
return nil
}
type watch struct {
wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall)
flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags)
}
// readEvents reads from the inotify file descriptor, converts the
// received events into Event objects and sends them via the Events channel
func (w *Watcher) readEvents() {
var (
buf [unix.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw events
n int // Number of bytes read with read()
errno error // Syscall errno
ok bool // For poller.wait
)
defer close(w.doneResp)
defer close(w.Errors)
defer close(w.Events)
defer unix.Close(w.fd)
defer w.poller.close()
for {
// See if we have been closed.
if w.isClosed() {
return
}
ok, errno = w.poller.wait()
if errno != nil {
select {
case w.Errors <- errno:
case <-w.done:
return
}
continue
}
if !ok {
continue
}
n, errno = unix.Read(w.fd, buf[:])
// If a signal interrupted execution, see if we've been asked to close, and try again.
// http://man7.org/linux/man-pages/man7/signal.7.html :
// "Before Linux 3.8, reads from an inotify(7) file descriptor were not restartable"
if errno == unix.EINTR {
continue
}
// unix.Read might have been woken up by Close. If so, we're done.
if w.isClosed() {
return
}
if n < unix.SizeofInotifyEvent {
var err error
if n == 0 {
// If EOF is received. This should really never happen.
err = io.EOF
} else if n < 0 {
// If an error occurred while reading.
err = errno
} else {
// Read was too short.
err = errors.New("notify: short read in readEvents()")
}
select {
case w.Errors <- err:
case <-w.done:
return
}
continue
}
var offset uint32
// We don't know how many events we just read into the buffer
// While the offset points to at least one whole event...
for offset <= uint32(n-unix.SizeofInotifyEvent) {
// Point "raw" to the event in the buffer
raw := (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset]))
mask := uint32(raw.Mask)
nameLen := uint32(raw.Len)
if mask&unix.IN_Q_OVERFLOW != 0 {
select {
case w.Errors <- ErrEventOverflow:
case <-w.done:
return
}
}
// If the event happened to the watched directory or the watched file, the kernel
// doesn't append the filename to the event, but we would like to always fill the
// the "Name" field with a valid filename. We retrieve the path of the watch from
// the "paths" map.
w.mu.Lock()
name, ok := w.paths[int(raw.Wd)]
// IN_DELETE_SELF occurs when the file/directory being watched is removed.
// This is a sign to clean up the maps, otherwise we are no longer in sync
// with the inotify kernel state which has already deleted the watch
// automatically.
if ok && mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF {
delete(w.paths, int(raw.Wd))
delete(w.watches, name)
}
w.mu.Unlock()
if nameLen > 0 {
// Point "bytes" at the first byte of the filename
bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent]))
// The filename is padded with NULL bytes. TrimRight() gets rid of those.
name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\000")
}
event := newEvent(name, mask)
// Send the events that are not ignored on the events channel
if !event.ignoreLinux(mask) {
select {
case w.Events <- event:
case <-w.done:
return
}
}
// Move to the next event in the buffer
offset += unix.SizeofInotifyEvent + nameLen
}
}
}
// Certain types of events can be "ignored" and not sent over the Events
// channel. Such as events marked ignore by the kernel, or MODIFY events
// against files that do not exist.
func (e *Event) ignoreLinux(mask uint32) bool {
// Ignore anything the inotify API says to ignore
if mask&unix.IN_IGNORED == unix.IN_IGNORED {
return true
}
// If the event is not a DELETE or RENAME, the file must exist.
// Otherwise the event is ignored.
// *Note*: this was put in place because it was seen that a MODIFY
// event was sent after the DELETE. This ignores that MODIFY and
// assumes a DELETE will come or has come if the file doesn't exist.
if !(e.Op&Remove == Remove || e.Op&Rename == Rename) {
_, statErr := os.Lstat(e.Name)
return os.IsNotExist(statErr)
}
return false
}
// newEvent returns an platform-independent Event based on an inotify mask.
func newEvent(name string, mask uint32) Event {
e := Event{Name: name}
if mask&unix.IN_CREATE == unix.IN_CREATE || mask&unix.IN_MOVED_TO == unix.IN_MOVED_TO {
e.Op |= Create
}
if mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF || mask&unix.IN_DELETE == unix.IN_DELETE {
e.Op |= Remove
}
if mask&unix.IN_MODIFY == unix.IN_MODIFY {
e.Op |= Write
}
if mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF || mask&unix.IN_MOVED_FROM == unix.IN_MOVED_FROM {
e.Op |= Rename
}
if mask&unix.IN_ATTRIB == unix.IN_ATTRIB {
e.Op |= Chmod
}
return e
}
+187
View File
@@ -0,0 +1,187 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build linux
package fsnotify
import (
"errors"
"golang.org/x/sys/unix"
)
type fdPoller struct {
fd int // File descriptor (as returned by the inotify_init() syscall)
epfd int // Epoll file descriptor
pipe [2]int // Pipe for waking up
}
func emptyPoller(fd int) *fdPoller {
poller := new(fdPoller)
poller.fd = fd
poller.epfd = -1
poller.pipe[0] = -1
poller.pipe[1] = -1
return poller
}
// Create a new inotify poller.
// This creates an inotify handler, and an epoll handler.
func newFdPoller(fd int) (*fdPoller, error) {
var errno error
poller := emptyPoller(fd)
defer func() {
if errno != nil {
poller.close()
}
}()
poller.fd = fd
// Create epoll fd
poller.epfd, errno = unix.EpollCreate1(0)
if poller.epfd == -1 {
return nil, errno
}
// Create pipe; pipe[0] is the read end, pipe[1] the write end.
errno = unix.Pipe2(poller.pipe[:], unix.O_NONBLOCK)
if errno != nil {
return nil, errno
}
// Register inotify fd with epoll
event := unix.EpollEvent{
Fd: int32(poller.fd),
Events: unix.EPOLLIN,
}
errno = unix.EpollCtl(poller.epfd, unix.EPOLL_CTL_ADD, poller.fd, &event)
if errno != nil {
return nil, errno
}
// Register pipe fd with epoll
event = unix.EpollEvent{
Fd: int32(poller.pipe[0]),
Events: unix.EPOLLIN,
}
errno = unix.EpollCtl(poller.epfd, unix.EPOLL_CTL_ADD, poller.pipe[0], &event)
if errno != nil {
return nil, errno
}
return poller, nil
}
// Wait using epoll.
// Returns true if something is ready to be read,
// false if there is not.
func (poller *fdPoller) wait() (bool, error) {
// 3 possible events per fd, and 2 fds, makes a maximum of 6 events.
// I don't know whether epoll_wait returns the number of events returned,
// or the total number of events ready.
// I decided to catch both by making the buffer one larger than the maximum.
events := make([]unix.EpollEvent, 7)
for {
n, errno := unix.EpollWait(poller.epfd, events, -1)
if n == -1 {
if errno == unix.EINTR {
continue
}
return false, errno
}
if n == 0 {
// If there are no events, try again.
continue
}
if n > 6 {
// This should never happen. More events were returned than should be possible.
return false, errors.New("epoll_wait returned more events than I know what to do with")
}
ready := events[:n]
epollhup := false
epollerr := false
epollin := false
for _, event := range ready {
if event.Fd == int32(poller.fd) {
if event.Events&unix.EPOLLHUP != 0 {
// This should not happen, but if it does, treat it as a wakeup.
epollhup = true
}
if event.Events&unix.EPOLLERR != 0 {
// If an error is waiting on the file descriptor, we should pretend
// something is ready to read, and let unix.Read pick up the error.
epollerr = true
}
if event.Events&unix.EPOLLIN != 0 {
// There is data to read.
epollin = true
}
}
if event.Fd == int32(poller.pipe[0]) {
if event.Events&unix.EPOLLHUP != 0 {
// Write pipe descriptor was closed, by us. This means we're closing down the
// watcher, and we should wake up.
}
if event.Events&unix.EPOLLERR != 0 {
// If an error is waiting on the pipe file descriptor.
// This is an absolute mystery, and should never ever happen.
return false, errors.New("Error on the pipe descriptor.")
}
if event.Events&unix.EPOLLIN != 0 {
// This is a regular wakeup, so we have to clear the buffer.
err := poller.clearWake()
if err != nil {
return false, err
}
}
}
}
if epollhup || epollerr || epollin {
return true, nil
}
return false, nil
}
}
// Close the write end of the poller.
func (poller *fdPoller) wake() error {
buf := make([]byte, 1)
n, errno := unix.Write(poller.pipe[1], buf)
if n == -1 {
if errno == unix.EAGAIN {
// Buffer is full, poller will wake.
return nil
}
return errno
}
return nil
}
func (poller *fdPoller) clearWake() error {
// You have to be woken up a LOT in order to get to 100!
buf := make([]byte, 100)
n, errno := unix.Read(poller.pipe[0], buf)
if n == -1 {
if errno == unix.EAGAIN {
// Buffer is empty, someone else cleared our wake.
return nil
}
return errno
}
return nil
}
// Close all poller file descriptors, but not the one passed to it.
func (poller *fdPoller) close() {
if poller.pipe[1] != -1 {
unix.Close(poller.pipe[1])
}
if poller.pipe[0] != -1 {
unix.Close(poller.pipe[0])
}
if poller.epfd != -1 {
unix.Close(poller.epfd)
}
}
+521
View File
@@ -0,0 +1,521 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build freebsd openbsd netbsd dragonfly darwin
package fsnotify
import (
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sync"
"time"
"golang.org/x/sys/unix"
)
// Watcher watches a set of files, delivering events to a channel.
type Watcher struct {
Events chan Event
Errors chan error
done chan struct{} // Channel for sending a "quit message" to the reader goroutine
kq int // File descriptor (as returned by the kqueue() syscall).
mu sync.Mutex // Protects access to watcher data
watches map[string]int // Map of watched file descriptors (key: path).
externalWatches map[string]bool // Map of watches added by user of the library.
dirFlags map[string]uint32 // Map of watched directories to fflags used in kqueue.
paths map[int]pathInfo // Map file descriptors to path names for processing kqueue events.
fileExists map[string]bool // Keep track of if we know this file exists (to stop duplicate create events).
isClosed bool // Set to true when Close() is first called
}
type pathInfo struct {
name string
isDir bool
}
// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events.
func NewWatcher() (*Watcher, error) {
kq, err := kqueue()
if err != nil {
return nil, err
}
w := &Watcher{
kq: kq,
watches: make(map[string]int),
dirFlags: make(map[string]uint32),
paths: make(map[int]pathInfo),
fileExists: make(map[string]bool),
externalWatches: make(map[string]bool),
Events: make(chan Event),
Errors: make(chan error),
done: make(chan struct{}),
}
go w.readEvents()
return w, nil
}
// Close removes all watches and closes the events channel.
func (w *Watcher) Close() error {
w.mu.Lock()
if w.isClosed {
w.mu.Unlock()
return nil
}
w.isClosed = true
// copy paths to remove while locked
var pathsToRemove = make([]string, 0, len(w.watches))
for name := range w.watches {
pathsToRemove = append(pathsToRemove, name)
}
w.mu.Unlock()
// unlock before calling Remove, which also locks
for _, name := range pathsToRemove {
w.Remove(name)
}
// send a "quit" message to the reader goroutine
close(w.done)
return nil
}
// Add starts watching the named file or directory (non-recursively).
func (w *Watcher) Add(name string) error {
w.mu.Lock()
w.externalWatches[name] = true
w.mu.Unlock()
_, err := w.addWatch(name, noteAllEvents)
return err
}
// Remove stops watching the the named file or directory (non-recursively).
func (w *Watcher) Remove(name string) error {
name = filepath.Clean(name)
w.mu.Lock()
watchfd, ok := w.watches[name]
w.mu.Unlock()
if !ok {
return fmt.Errorf("can't remove non-existent kevent watch for: %s", name)
}
const registerRemove = unix.EV_DELETE
if err := register(w.kq, []int{watchfd}, registerRemove, 0); err != nil {
return err
}
unix.Close(watchfd)
w.mu.Lock()
isDir := w.paths[watchfd].isDir
delete(w.watches, name)
delete(w.paths, watchfd)
delete(w.dirFlags, name)
w.mu.Unlock()
// Find all watched paths that are in this directory that are not external.
if isDir {
var pathsToRemove []string
w.mu.Lock()
for _, path := range w.paths {
wdir, _ := filepath.Split(path.name)
if filepath.Clean(wdir) == name {
if !w.externalWatches[path.name] {
pathsToRemove = append(pathsToRemove, path.name)
}
}
}
w.mu.Unlock()
for _, name := range pathsToRemove {
// Since these are internal, not much sense in propagating error
// to the user, as that will just confuse them with an error about
// a path they did not explicitly watch themselves.
w.Remove(name)
}
}
return nil
}
// Watch all events (except NOTE_EXTEND, NOTE_LINK, NOTE_REVOKE)
const noteAllEvents = unix.NOTE_DELETE | unix.NOTE_WRITE | unix.NOTE_ATTRIB | unix.NOTE_RENAME
// keventWaitTime to block on each read from kevent
var keventWaitTime = durationToTimespec(100 * time.Millisecond)
// addWatch adds name to the watched file set.
// The flags are interpreted as described in kevent(2).
// Returns the real path to the file which was added, if any, which may be different from the one passed in the case of symlinks.
func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
var isDir bool
// Make ./name and name equivalent
name = filepath.Clean(name)
w.mu.Lock()
if w.isClosed {
w.mu.Unlock()
return "", errors.New("kevent instance already closed")
}
watchfd, alreadyWatching := w.watches[name]
// We already have a watch, but we can still override flags.
if alreadyWatching {
isDir = w.paths[watchfd].isDir
}
w.mu.Unlock()
if !alreadyWatching {
fi, err := os.Lstat(name)
if err != nil {
return "", err
}
// Don't watch sockets.
if fi.Mode()&os.ModeSocket == os.ModeSocket {
return "", nil
}
// Don't watch named pipes.
if fi.Mode()&os.ModeNamedPipe == os.ModeNamedPipe {
return "", nil
}
// Follow Symlinks
// Unfortunately, Linux can add bogus symlinks to watch list without
// issue, and Windows can't do symlinks period (AFAIK). To maintain
// consistency, we will act like everything is fine. There will simply
// be no file events for broken symlinks.
// Hence the returns of nil on errors.
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
name, err = filepath.EvalSymlinks(name)
if err != nil {
return "", nil
}
w.mu.Lock()
_, alreadyWatching = w.watches[name]
w.mu.Unlock()
if alreadyWatching {
return name, nil
}
fi, err = os.Lstat(name)
if err != nil {
return "", nil
}
}
watchfd, err = unix.Open(name, openMode, 0700)
if watchfd == -1 {
return "", err
}
isDir = fi.IsDir()
}
const registerAdd = unix.EV_ADD | unix.EV_CLEAR | unix.EV_ENABLE
if err := register(w.kq, []int{watchfd}, registerAdd, flags); err != nil {
unix.Close(watchfd)
return "", err
}
if !alreadyWatching {
w.mu.Lock()
w.watches[name] = watchfd
w.paths[watchfd] = pathInfo{name: name, isDir: isDir}
w.mu.Unlock()
}
if isDir {
// Watch the directory if it has not been watched before,
// or if it was watched before, but perhaps only a NOTE_DELETE (watchDirectoryFiles)
w.mu.Lock()
watchDir := (flags&unix.NOTE_WRITE) == unix.NOTE_WRITE &&
(!alreadyWatching || (w.dirFlags[name]&unix.NOTE_WRITE) != unix.NOTE_WRITE)
// Store flags so this watch can be updated later
w.dirFlags[name] = flags
w.mu.Unlock()
if watchDir {
if err := w.watchDirectoryFiles(name); err != nil {
return "", err
}
}
}
return name, nil
}
// readEvents reads from kqueue and converts the received kevents into
// Event values that it sends down the Events channel.
func (w *Watcher) readEvents() {
eventBuffer := make([]unix.Kevent_t, 10)
loop:
for {
// See if there is a message on the "done" channel
select {
case <-w.done:
break loop
default:
}
// Get new events
kevents, err := read(w.kq, eventBuffer, &keventWaitTime)
// EINTR is okay, the syscall was interrupted before timeout expired.
if err != nil && err != unix.EINTR {
select {
case w.Errors <- err:
case <-w.done:
break loop
}
continue
}
// Flush the events we received to the Events channel
for len(kevents) > 0 {
kevent := &kevents[0]
watchfd := int(kevent.Ident)
mask := uint32(kevent.Fflags)
w.mu.Lock()
path := w.paths[watchfd]
w.mu.Unlock()
event := newEvent(path.name, mask)
if path.isDir && !(event.Op&Remove == Remove) {
// Double check to make sure the directory exists. This can happen when
// we do a rm -fr on a recursively watched folders and we receive a
// modification event first but the folder has been deleted and later
// receive the delete event
if _, err := os.Lstat(event.Name); os.IsNotExist(err) {
// mark is as delete event
event.Op |= Remove
}
}
if event.Op&Rename == Rename || event.Op&Remove == Remove {
w.Remove(event.Name)
w.mu.Lock()
delete(w.fileExists, event.Name)
w.mu.Unlock()
}
if path.isDir && event.Op&Write == Write && !(event.Op&Remove == Remove) {
w.sendDirectoryChangeEvents(event.Name)
} else {
// Send the event on the Events channel.
select {
case w.Events <- event:
case <-w.done:
break loop
}
}
if event.Op&Remove == Remove {
// Look for a file that may have overwritten this.
// For example, mv f1 f2 will delete f2, then create f2.
if path.isDir {
fileDir := filepath.Clean(event.Name)
w.mu.Lock()
_, found := w.watches[fileDir]
w.mu.Unlock()
if found {
// make sure the directory exists before we watch for changes. When we
// do a recursive watch and perform rm -fr, the parent directory might
// have gone missing, ignore the missing directory and let the
// upcoming delete event remove the watch from the parent directory.
if _, err := os.Lstat(fileDir); err == nil {
w.sendDirectoryChangeEvents(fileDir)
}
}
} else {
filePath := filepath.Clean(event.Name)
if fileInfo, err := os.Lstat(filePath); err == nil {
w.sendFileCreatedEventIfNew(filePath, fileInfo)
}
}
}
// Move to next event
kevents = kevents[1:]
}
}
// cleanup
err := unix.Close(w.kq)
if err != nil {
// only way the previous loop breaks is if w.done was closed so we need to async send to w.Errors.
select {
case w.Errors <- err:
default:
}
}
close(w.Events)
close(w.Errors)
}
// newEvent returns an platform-independent Event based on kqueue Fflags.
func newEvent(name string, mask uint32) Event {
e := Event{Name: name}
if mask&unix.NOTE_DELETE == unix.NOTE_DELETE {
e.Op |= Remove
}
if mask&unix.NOTE_WRITE == unix.NOTE_WRITE {
e.Op |= Write
}
if mask&unix.NOTE_RENAME == unix.NOTE_RENAME {
e.Op |= Rename
}
if mask&unix.NOTE_ATTRIB == unix.NOTE_ATTRIB {
e.Op |= Chmod
}
return e
}
func newCreateEvent(name string) Event {
return Event{Name: name, Op: Create}
}
// watchDirectoryFiles to mimic inotify when adding a watch on a directory
func (w *Watcher) watchDirectoryFiles(dirPath string) error {
// Get all files
files, err := ioutil.ReadDir(dirPath)
if err != nil {
return err
}
for _, fileInfo := range files {
filePath := filepath.Join(dirPath, fileInfo.Name())
filePath, err = w.internalWatch(filePath, fileInfo)
if err != nil {
return err
}
w.mu.Lock()
w.fileExists[filePath] = true
w.mu.Unlock()
}
return nil
}
// sendDirectoryEvents searches the directory for newly created files
// and sends them over the event channel. This functionality is to have
// the BSD version of fsnotify match Linux inotify which provides a
// create event for files created in a watched directory.
func (w *Watcher) sendDirectoryChangeEvents(dirPath string) {
// Get all files
files, err := ioutil.ReadDir(dirPath)
if err != nil {
select {
case w.Errors <- err:
case <-w.done:
return
}
}
// Search for new files
for _, fileInfo := range files {
filePath := filepath.Join(dirPath, fileInfo.Name())
err := w.sendFileCreatedEventIfNew(filePath, fileInfo)
if err != nil {
return
}
}
}
// sendFileCreatedEvent sends a create event if the file isn't already being tracked.
func (w *Watcher) sendFileCreatedEventIfNew(filePath string, fileInfo os.FileInfo) (err error) {
w.mu.Lock()
_, doesExist := w.fileExists[filePath]
w.mu.Unlock()
if !doesExist {
// Send create event
select {
case w.Events <- newCreateEvent(filePath):
case <-w.done:
return
}
}
// like watchDirectoryFiles (but without doing another ReadDir)
filePath, err = w.internalWatch(filePath, fileInfo)
if err != nil {
return err
}
w.mu.Lock()
w.fileExists[filePath] = true
w.mu.Unlock()
return nil
}
func (w *Watcher) internalWatch(name string, fileInfo os.FileInfo) (string, error) {
if fileInfo.IsDir() {
// mimic Linux providing delete events for subdirectories
// but preserve the flags used if currently watching subdirectory
w.mu.Lock()
flags := w.dirFlags[name]
w.mu.Unlock()
flags |= unix.NOTE_DELETE | unix.NOTE_RENAME
return w.addWatch(name, flags)
}
// watch file to mimic Linux inotify
return w.addWatch(name, noteAllEvents)
}
// kqueue creates a new kernel event queue and returns a descriptor.
func kqueue() (kq int, err error) {
kq, err = unix.Kqueue()
if kq == -1 {
return kq, err
}
return kq, nil
}
// register events with the queue
func register(kq int, fds []int, flags int, fflags uint32) error {
changes := make([]unix.Kevent_t, len(fds))
for i, fd := range fds {
// SetKevent converts int to the platform-specific types:
unix.SetKevent(&changes[i], fd, unix.EVFILT_VNODE, flags)
changes[i].Fflags = fflags
}
// register the events
success, err := unix.Kevent(kq, changes, nil, nil)
if success == -1 {
return err
}
return nil
}
// read retrieves pending events, or waits until an event occurs.
// A timeout of nil blocks indefinitely, while 0 polls the queue.
func read(kq int, events []unix.Kevent_t, timeout *unix.Timespec) ([]unix.Kevent_t, error) {
n, err := unix.Kevent(kq, nil, events, timeout)
if err != nil {
return nil, err
}
return events[0:n], nil
}
// durationToTimespec prepares a timeout value
func durationToTimespec(d time.Duration) unix.Timespec {
return unix.NsecToTimespec(d.Nanoseconds())
}
+11
View File
@@ -0,0 +1,11 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build freebsd openbsd netbsd dragonfly
package fsnotify
import "golang.org/x/sys/unix"
const openMode = unix.O_NONBLOCK | unix.O_RDONLY
+12
View File
@@ -0,0 +1,12 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build darwin
package fsnotify
import "golang.org/x/sys/unix"
// note: this constant is not defined on BSD
const openMode = unix.O_EVTONLY
+561
View File
@@ -0,0 +1,561 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build windows
package fsnotify
import (
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"sync"
"syscall"
"unsafe"
)
// Watcher watches a set of files, delivering events to a channel.
type Watcher struct {
Events chan Event
Errors chan error
isClosed bool // Set to true when Close() is first called
mu sync.Mutex // Map access
port syscall.Handle // Handle to completion port
watches watchMap // Map of watches (key: i-number)
input chan *input // Inputs to the reader are sent on this channel
quit chan chan<- error
}
// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events.
func NewWatcher() (*Watcher, error) {
port, e := syscall.CreateIoCompletionPort(syscall.InvalidHandle, 0, 0, 0)
if e != nil {
return nil, os.NewSyscallError("CreateIoCompletionPort", e)
}
w := &Watcher{
port: port,
watches: make(watchMap),
input: make(chan *input, 1),
Events: make(chan Event, 50),
Errors: make(chan error),
quit: make(chan chan<- error, 1),
}
go w.readEvents()
return w, nil
}
// Close removes all watches and closes the events channel.
func (w *Watcher) Close() error {
if w.isClosed {
return nil
}
w.isClosed = true
// Send "quit" message to the reader goroutine
ch := make(chan error)
w.quit <- ch
if err := w.wakeupReader(); err != nil {
return err
}
return <-ch
}
// Add starts watching the named file or directory (non-recursively).
func (w *Watcher) Add(name string) error {
if w.isClosed {
return errors.New("watcher already closed")
}
in := &input{
op: opAddWatch,
path: filepath.Clean(name),
flags: sysFSALLEVENTS,
reply: make(chan error),
}
w.input <- in
if err := w.wakeupReader(); err != nil {
return err
}
return <-in.reply
}
// Remove stops watching the the named file or directory (non-recursively).
func (w *Watcher) Remove(name string) error {
in := &input{
op: opRemoveWatch,
path: filepath.Clean(name),
reply: make(chan error),
}
w.input <- in
if err := w.wakeupReader(); err != nil {
return err
}
return <-in.reply
}
const (
// Options for AddWatch
sysFSONESHOT = 0x80000000
sysFSONLYDIR = 0x1000000
// Events
sysFSACCESS = 0x1
sysFSALLEVENTS = 0xfff
sysFSATTRIB = 0x4
sysFSCLOSE = 0x18
sysFSCREATE = 0x100
sysFSDELETE = 0x200
sysFSDELETESELF = 0x400
sysFSMODIFY = 0x2
sysFSMOVE = 0xc0
sysFSMOVEDFROM = 0x40
sysFSMOVEDTO = 0x80
sysFSMOVESELF = 0x800
// Special events
sysFSIGNORED = 0x8000
sysFSQOVERFLOW = 0x4000
)
func newEvent(name string, mask uint32) Event {
e := Event{Name: name}
if mask&sysFSCREATE == sysFSCREATE || mask&sysFSMOVEDTO == sysFSMOVEDTO {
e.Op |= Create
}
if mask&sysFSDELETE == sysFSDELETE || mask&sysFSDELETESELF == sysFSDELETESELF {
e.Op |= Remove
}
if mask&sysFSMODIFY == sysFSMODIFY {
e.Op |= Write
}
if mask&sysFSMOVE == sysFSMOVE || mask&sysFSMOVESELF == sysFSMOVESELF || mask&sysFSMOVEDFROM == sysFSMOVEDFROM {
e.Op |= Rename
}
if mask&sysFSATTRIB == sysFSATTRIB {
e.Op |= Chmod
}
return e
}
const (
opAddWatch = iota
opRemoveWatch
)
const (
provisional uint64 = 1 << (32 + iota)
)
type input struct {
op int
path string
flags uint32
reply chan error
}
type inode struct {
handle syscall.Handle
volume uint32
index uint64
}
type watch struct {
ov syscall.Overlapped
ino *inode // i-number
path string // Directory path
mask uint64 // Directory itself is being watched with these notify flags
names map[string]uint64 // Map of names being watched and their notify flags
rename string // Remembers the old name while renaming a file
buf [4096]byte
}
type indexMap map[uint64]*watch
type watchMap map[uint32]indexMap
func (w *Watcher) wakeupReader() error {
e := syscall.PostQueuedCompletionStatus(w.port, 0, 0, nil)
if e != nil {
return os.NewSyscallError("PostQueuedCompletionStatus", e)
}
return nil
}
func getDir(pathname string) (dir string, err error) {
attr, e := syscall.GetFileAttributes(syscall.StringToUTF16Ptr(pathname))
if e != nil {
return "", os.NewSyscallError("GetFileAttributes", e)
}
if attr&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 {
dir = pathname
} else {
dir, _ = filepath.Split(pathname)
dir = filepath.Clean(dir)
}
return
}
func getIno(path string) (ino *inode, err error) {
h, e := syscall.CreateFile(syscall.StringToUTF16Ptr(path),
syscall.FILE_LIST_DIRECTORY,
syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE,
nil, syscall.OPEN_EXISTING,
syscall.FILE_FLAG_BACKUP_SEMANTICS|syscall.FILE_FLAG_OVERLAPPED, 0)
if e != nil {
return nil, os.NewSyscallError("CreateFile", e)
}
var fi syscall.ByHandleFileInformation
if e = syscall.GetFileInformationByHandle(h, &fi); e != nil {
syscall.CloseHandle(h)
return nil, os.NewSyscallError("GetFileInformationByHandle", e)
}
ino = &inode{
handle: h,
volume: fi.VolumeSerialNumber,
index: uint64(fi.FileIndexHigh)<<32 | uint64(fi.FileIndexLow),
}
return ino, nil
}
// Must run within the I/O thread.
func (m watchMap) get(ino *inode) *watch {
if i := m[ino.volume]; i != nil {
return i[ino.index]
}
return nil
}
// Must run within the I/O thread.
func (m watchMap) set(ino *inode, watch *watch) {
i := m[ino.volume]
if i == nil {
i = make(indexMap)
m[ino.volume] = i
}
i[ino.index] = watch
}
// Must run within the I/O thread.
func (w *Watcher) addWatch(pathname string, flags uint64) error {
dir, err := getDir(pathname)
if err != nil {
return err
}
if flags&sysFSONLYDIR != 0 && pathname != dir {
return nil
}
ino, err := getIno(dir)
if err != nil {
return err
}
w.mu.Lock()
watchEntry := w.watches.get(ino)
w.mu.Unlock()
if watchEntry == nil {
if _, e := syscall.CreateIoCompletionPort(ino.handle, w.port, 0, 0); e != nil {
syscall.CloseHandle(ino.handle)
return os.NewSyscallError("CreateIoCompletionPort", e)
}
watchEntry = &watch{
ino: ino,
path: dir,
names: make(map[string]uint64),
}
w.mu.Lock()
w.watches.set(ino, watchEntry)
w.mu.Unlock()
flags |= provisional
} else {
syscall.CloseHandle(ino.handle)
}
if pathname == dir {
watchEntry.mask |= flags
} else {
watchEntry.names[filepath.Base(pathname)] |= flags
}
if err = w.startRead(watchEntry); err != nil {
return err
}
if pathname == dir {
watchEntry.mask &= ^provisional
} else {
watchEntry.names[filepath.Base(pathname)] &= ^provisional
}
return nil
}
// Must run within the I/O thread.
func (w *Watcher) remWatch(pathname string) error {
dir, err := getDir(pathname)
if err != nil {
return err
}
ino, err := getIno(dir)
if err != nil {
return err
}
w.mu.Lock()
watch := w.watches.get(ino)
w.mu.Unlock()
if watch == nil {
return fmt.Errorf("can't remove non-existent watch for: %s", pathname)
}
if pathname == dir {
w.sendEvent(watch.path, watch.mask&sysFSIGNORED)
watch.mask = 0
} else {
name := filepath.Base(pathname)
w.sendEvent(filepath.Join(watch.path, name), watch.names[name]&sysFSIGNORED)
delete(watch.names, name)
}
return w.startRead(watch)
}
// Must run within the I/O thread.
func (w *Watcher) deleteWatch(watch *watch) {
for name, mask := range watch.names {
if mask&provisional == 0 {
w.sendEvent(filepath.Join(watch.path, name), mask&sysFSIGNORED)
}
delete(watch.names, name)
}
if watch.mask != 0 {
if watch.mask&provisional == 0 {
w.sendEvent(watch.path, watch.mask&sysFSIGNORED)
}
watch.mask = 0
}
}
// Must run within the I/O thread.
func (w *Watcher) startRead(watch *watch) error {
if e := syscall.CancelIo(watch.ino.handle); e != nil {
w.Errors <- os.NewSyscallError("CancelIo", e)
w.deleteWatch(watch)
}
mask := toWindowsFlags(watch.mask)
for _, m := range watch.names {
mask |= toWindowsFlags(m)
}
if mask == 0 {
if e := syscall.CloseHandle(watch.ino.handle); e != nil {
w.Errors <- os.NewSyscallError("CloseHandle", e)
}
w.mu.Lock()
delete(w.watches[watch.ino.volume], watch.ino.index)
w.mu.Unlock()
return nil
}
e := syscall.ReadDirectoryChanges(watch.ino.handle, &watch.buf[0],
uint32(unsafe.Sizeof(watch.buf)), false, mask, nil, &watch.ov, 0)
if e != nil {
err := os.NewSyscallError("ReadDirectoryChanges", e)
if e == syscall.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 {
// Watched directory was probably removed
if w.sendEvent(watch.path, watch.mask&sysFSDELETESELF) {
if watch.mask&sysFSONESHOT != 0 {
watch.mask = 0
}
}
err = nil
}
w.deleteWatch(watch)
w.startRead(watch)
return err
}
return nil
}
// readEvents reads from the I/O completion port, converts the
// received events into Event objects and sends them via the Events channel.
// Entry point to the I/O thread.
func (w *Watcher) readEvents() {
var (
n, key uint32
ov *syscall.Overlapped
)
runtime.LockOSThread()
for {
e := syscall.GetQueuedCompletionStatus(w.port, &n, &key, &ov, syscall.INFINITE)
watch := (*watch)(unsafe.Pointer(ov))
if watch == nil {
select {
case ch := <-w.quit:
w.mu.Lock()
var indexes []indexMap
for _, index := range w.watches {
indexes = append(indexes, index)
}
w.mu.Unlock()
for _, index := range indexes {
for _, watch := range index {
w.deleteWatch(watch)
w.startRead(watch)
}
}
var err error
if e := syscall.CloseHandle(w.port); e != nil {
err = os.NewSyscallError("CloseHandle", e)
}
close(w.Events)
close(w.Errors)
ch <- err
return
case in := <-w.input:
switch in.op {
case opAddWatch:
in.reply <- w.addWatch(in.path, uint64(in.flags))
case opRemoveWatch:
in.reply <- w.remWatch(in.path)
}
default:
}
continue
}
switch e {
case syscall.ERROR_MORE_DATA:
if watch == nil {
w.Errors <- errors.New("ERROR_MORE_DATA has unexpectedly null lpOverlapped buffer")
} else {
// The i/o succeeded but the buffer is full.
// In theory we should be building up a full packet.
// In practice we can get away with just carrying on.
n = uint32(unsafe.Sizeof(watch.buf))
}
case syscall.ERROR_ACCESS_DENIED:
// Watched directory was probably removed
w.sendEvent(watch.path, watch.mask&sysFSDELETESELF)
w.deleteWatch(watch)
w.startRead(watch)
continue
case syscall.ERROR_OPERATION_ABORTED:
// CancelIo was called on this handle
continue
default:
w.Errors <- os.NewSyscallError("GetQueuedCompletionPort", e)
continue
case nil:
}
var offset uint32
for {
if n == 0 {
w.Events <- newEvent("", sysFSQOVERFLOW)
w.Errors <- errors.New("short read in readEvents()")
break
}
// Point "raw" to the event in the buffer
raw := (*syscall.FileNotifyInformation)(unsafe.Pointer(&watch.buf[offset]))
buf := (*[syscall.MAX_PATH]uint16)(unsafe.Pointer(&raw.FileName))
name := syscall.UTF16ToString(buf[:raw.FileNameLength/2])
fullname := filepath.Join(watch.path, name)
var mask uint64
switch raw.Action {
case syscall.FILE_ACTION_REMOVED:
mask = sysFSDELETESELF
case syscall.FILE_ACTION_MODIFIED:
mask = sysFSMODIFY
case syscall.FILE_ACTION_RENAMED_OLD_NAME:
watch.rename = name
case syscall.FILE_ACTION_RENAMED_NEW_NAME:
if watch.names[watch.rename] != 0 {
watch.names[name] |= watch.names[watch.rename]
delete(watch.names, watch.rename)
mask = sysFSMOVESELF
}
}
sendNameEvent := func() {
if w.sendEvent(fullname, watch.names[name]&mask) {
if watch.names[name]&sysFSONESHOT != 0 {
delete(watch.names, name)
}
}
}
if raw.Action != syscall.FILE_ACTION_RENAMED_NEW_NAME {
sendNameEvent()
}
if raw.Action == syscall.FILE_ACTION_REMOVED {
w.sendEvent(fullname, watch.names[name]&sysFSIGNORED)
delete(watch.names, name)
}
if w.sendEvent(fullname, watch.mask&toFSnotifyFlags(raw.Action)) {
if watch.mask&sysFSONESHOT != 0 {
watch.mask = 0
}
}
if raw.Action == syscall.FILE_ACTION_RENAMED_NEW_NAME {
fullname = filepath.Join(watch.path, watch.rename)
sendNameEvent()
}
// Move to the next event in the buffer
if raw.NextEntryOffset == 0 {
break
}
offset += raw.NextEntryOffset
// Error!
if offset >= n {
w.Errors <- errors.New("Windows system assumed buffer larger than it is, events have likely been missed.")
break
}
}
if err := w.startRead(watch); err != nil {
w.Errors <- err
}
}
}
func (w *Watcher) sendEvent(name string, mask uint64) bool {
if mask == 0 {
return false
}
event := newEvent(name, uint32(mask))
select {
case ch := <-w.quit:
w.quit <- ch
case w.Events <- event:
}
return true
}
func toWindowsFlags(mask uint64) uint32 {
var m uint32
if mask&sysFSACCESS != 0 {
m |= syscall.FILE_NOTIFY_CHANGE_LAST_ACCESS
}
if mask&sysFSMODIFY != 0 {
m |= syscall.FILE_NOTIFY_CHANGE_LAST_WRITE
}
if mask&sysFSATTRIB != 0 {
m |= syscall.FILE_NOTIFY_CHANGE_ATTRIBUTES
}
if mask&(sysFSMOVE|sysFSCREATE|sysFSDELETE) != 0 {
m |= syscall.FILE_NOTIFY_CHANGE_FILE_NAME | syscall.FILE_NOTIFY_CHANGE_DIR_NAME
}
return m
}
func toFSnotifyFlags(action uint32) uint64 {
switch action {
case syscall.FILE_ACTION_ADDED:
return sysFSCREATE
case syscall.FILE_ACTION_REMOVED:
return sysFSDELETE
case syscall.FILE_ACTION_MODIFIED:
return sysFSMODIFY
case syscall.FILE_ACTION_RENAMED_OLD_NAME:
return sysFSMOVEDFROM
case syscall.FILE_ACTION_RENAMED_NEW_NAME:
return sysFSMOVEDTO
}
return 0
}
+354
View File
@@ -0,0 +1,354 @@
Mozilla Public License, version 2.0
1. Definitions
1.1. “Contributor”
means each individual or legal entity that creates, contributes to the
creation of, or owns Covered Software.
1.2. “Contributor Version”
means the combination of the Contributions of others (if any) used by a
Contributor and that particular Contributor’s Contribution.
1.3. “Contribution”
means Covered Software of a particular Contributor.
1.4. “Covered Software”
means Source Code Form to which the initial Contributor has attached the
notice in Exhibit A, the Executable Form of such Source Code Form, and
Modifications of such Source Code Form, in each case including portions
thereof.
1.5. “Incompatible With Secondary Licenses”
means
a. that the initial Contributor has attached the notice described in
Exhibit B to the Covered Software; or
b. that the Covered Software was made available under the terms of version
1.1 or earlier of the License, but not also under the terms of a
Secondary License.
1.6. “Executable Form”
means any form of the work other than Source Code Form.
1.7. “Larger Work”
means a work that combines Covered Software with other material, in a separate
file or files, that is not Covered Software.
1.8. “License”
means this document.
1.9. “Licensable”
means having the right to grant, to the maximum extent possible, whether at the
time of the initial grant or subsequently, any and all of the rights conveyed by
this License.
1.10. “Modifications”
means any of the following:
a. any file in Source Code Form that results from an addition to, deletion
from, or modification of the contents of Covered Software; or
b. any new file in Source Code Form that contains any Covered Software.
1.11. “Patent Claims” of a Contributor
means any patent claim(s), including without limitation, method, process,
and apparatus claims, in any patent Licensable by such Contributor that
would be infringed, but for the grant of the License, by the making,
using, selling, offering for sale, having made, import, or transfer of
either its Contributions or its Contributor Version.
1.12. “Secondary License”
means either the GNU General Public License, Version 2.0, the GNU Lesser
General Public License, Version 2.1, the GNU Affero General Public
License, Version 3.0, or any later versions of those licenses.
1.13. “Source Code Form”
means the form of the work preferred for making modifications.
1.14. “You” (or “Your”)
means an individual or a legal entity exercising rights under this
License. For legal entities, “You” includes any entity that controls, is
controlled by, or is under common control with You. For purposes of this
definition, “control” means (a) the power, direct or indirect, to cause
the direction or management of such entity, whether by contract or
otherwise, or (b) ownership of more than fifty percent (50%) of the
outstanding shares or beneficial ownership of such entity.
2. License Grants and Conditions
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
a. under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or as
part of a Larger Work; and
b. under Patent Claims of such Contributor to make, use, sell, offer for
sale, have made, import, and otherwise transfer either its Contributions
or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution become
effective for each Contribution on the date the Contributor first distributes
such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under this
License. No additional rights or licenses will be implied from the distribution
or licensing of Covered Software under this License. Notwithstanding Section
2.1(b) above, no patent license is granted by a Contributor:
a. for any code that a Contributor has removed from Covered Software; or
b. for infringements caused by: (i) Your and any other third party’s
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
c. under Patent Claims infringed by Covered Software in the absence of its
Contributions.
This License does not grant any rights in the trademarks, service marks, or
logos of any Contributor (except as may be necessary to comply with the
notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this License
(see Section 10.2) or under the terms of a Secondary License (if permitted
under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its Contributions
are its original creation(s) or it has sufficient rights to grant the
rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under applicable
copyright doctrines of fair use, fair dealing, or other equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
Section 2.1.
3. Responsibilities
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under the
terms of this License. You must inform recipients that the Source Code Form
of the Covered Software is governed by the terms of this License, and how
they can obtain a copy of this License. You may not attempt to alter or
restrict the recipients’ rights in the Source Code Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
a. such Covered Software must also be made available in Source Code Form,
as described in Section 3.1, and You must inform recipients of the
Executable Form how they can obtain a copy of such Source Code Form by
reasonable means in a timely manner, at a charge no more than the cost
of distribution to the recipient; and
b. You may distribute such Executable Form under the terms of this License,
or sublicense it under different terms, provided that the license for
the Executable Form does not attempt to limit or alter the recipients’
rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for the
Covered Software. If the Larger Work is a combination of Covered Software
with a work governed by one or more Secondary Licenses, and the Covered
Software is not Incompatible With Secondary Licenses, this License permits
You to additionally distribute such Covered Software under the terms of
such Secondary License(s), so that the recipient of the Larger Work may, at
their option, further distribute the Covered Software under the terms of
either this License or such Secondary License(s).
3.4. Notices
You may not remove or alter the substance of any license notices (including
copyright notices, patent notices, disclaimers of warranty, or limitations
of liability) contained within the Source Code Form of the Covered
Software, except that You may alter any license notices to the extent
required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on behalf
of any Contributor. You must make it absolutely clear that any such
warranty, support, indemnity, or liability obligation is offered by You
alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
If it is impossible for You to comply with any of the terms of this License
with respect to some or all of the Covered Software due to statute, judicial
order, or regulation then You must: (a) comply with the terms of this License
to the maximum extent possible; and (b) describe the limitations and the code
they affect. Such description must be placed in a text file included with all
distributions of the Covered Software under this License. Except to the
extent prohibited by statute or regulation, such description must be
sufficiently detailed for a recipient of ordinary skill to be able to
understand it.
5. Termination
5.1. The rights granted under this License will terminate automatically if You
fail to comply with any of its terms. However, if You become compliant,
then the rights granted under this License from a particular Contributor
are reinstated (a) provisionally, unless and until such Contributor
explicitly and finally terminates Your grants, and (b) on an ongoing basis,
if such Contributor fails to notify You of the non-compliance by some
reasonable means prior to 60 days after You have come back into compliance.
Moreover, Your grants from a particular Contributor are reinstated on an
ongoing basis if such Contributor notifies You of the non-compliance by
some reasonable means, this is the first time You have received notice of
non-compliance with this License from such Contributor, and You become
compliant prior to 30 days after Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions, counter-claims,
and cross-claims) alleging that a Contributor Version directly or
indirectly infringes any patent, then the rights granted to You by any and
all Contributors for the Covered Software under Section 2.1 of this License
shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
license agreements (excluding distributors and resellers) which have been
validly granted by You or Your distributors under this License prior to
termination shall survive termination.
6. Disclaimer of Warranty
Covered Software is provided under this License on an “as is” basis, without
warranty of any kind, either expressed, implied, or statutory, including,
without limitation, warranties that the Covered Software is free of defects,
merchantable, fit for a particular purpose or non-infringing. The entire
risk as to the quality and performance of the Covered Software is with You.
Should any Covered Software prove defective in any respect, You (not any
Contributor) assume the cost of any necessary servicing, repair, or
correction. This disclaimer of warranty constitutes an essential part of this
License. No use of any Covered Software is authorized under this License
except under this disclaimer.
7. Limitation of Liability
Under no circumstances and under no legal theory, whether tort (including
negligence), contract, or otherwise, shall any Contributor, or anyone who
distributes Covered Software as permitted above, be liable to You for any
direct, indirect, special, incidental, or consequential damages of any
character including, without limitation, damages for lost profits, loss of
goodwill, work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses, even if such party shall have been
informed of the possibility of such damages. This limitation of liability
shall not apply to liability for death or personal injury resulting from such
party’s negligence to the extent applicable law prohibits such limitation.
Some jurisdictions do not allow the exclusion or limitation of incidental or
consequential damages, so this exclusion and limitation may not apply to You.
8. Litigation
Any litigation relating to this License may be brought only in the courts of
a jurisdiction where the defendant maintains its principal place of business
and such litigation shall be governed by laws of that jurisdiction, without
reference to its conflict-of-law provisions. Nothing in this Section shall
prevent a party’s ability to bring cross-claims or counter-claims.
9. Miscellaneous
This License represents the complete agreement concerning the subject matter
hereof. If any provision of this License is held to be unenforceable, such
provision shall be reformed only to the extent necessary to make it
enforceable. Any law or regulation which provides that the language of a
contract shall be construed against the drafter shall not be used to construe
this License against a Contributor.
10. Versions of the License
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version of
the License under which You originally received the Covered Software, or
under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a modified
version of this License if you rename the license and remove any
references to the name of the license steward (except to note that such
modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular file, then
You may include the notice in a location (such as a LICENSE file in a relevant
directory) where a recipient would be likely to look for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - “Incompatible With Secondary Licenses” Notice
This Source Code Form is “Incompatible
With Secondary Licenses”, as defined by
the Mozilla Public License, v. 2.0.
+729
View File
@@ -0,0 +1,729 @@
package hcl
import (
"errors"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"github.com/hashicorp/hcl/hcl/ast"
"github.com/hashicorp/hcl/hcl/parser"
"github.com/hashicorp/hcl/hcl/token"
)
// This is the tag to use with structures to have settings for HCL
const tagName = "hcl"
var (
// nodeType holds a reference to the type of ast.Node
nodeType reflect.Type = findNodeType()
)
// Unmarshal accepts a byte slice as input and writes the
// data to the value pointed to by v.
func Unmarshal(bs []byte, v interface{}) error {
root, err := parse(bs)
if err != nil {
return err
}
return DecodeObject(v, root)
}
// Decode reads the given input and decodes it into the structure
// given by `out`.
func Decode(out interface{}, in string) error {
obj, err := Parse(in)
if err != nil {
return err
}
return DecodeObject(out, obj)
}
// DecodeObject is a lower-level version of Decode. It decodes a
// raw Object into the given output.
func DecodeObject(out interface{}, n ast.Node) error {
val := reflect.ValueOf(out)
if val.Kind() != reflect.Ptr {
return errors.New("result must be a pointer")
}
// If we have the file, we really decode the root node
if f, ok := n.(*ast.File); ok {
n = f.Node
}
var d decoder
return d.decode("root", n, val.Elem())
}
type decoder struct {
stack []reflect.Kind
}
func (d *decoder) decode(name string, node ast.Node, result reflect.Value) error {
k := result
// If we have an interface with a valid value, we use that
// for the check.
if result.Kind() == reflect.Interface {
elem := result.Elem()
if elem.IsValid() {
k = elem
}
}
// Push current onto stack unless it is an interface.
if k.Kind() != reflect.Interface {
d.stack = append(d.stack, k.Kind())
// Schedule a pop
defer func() {
d.stack = d.stack[:len(d.stack)-1]
}()
}
switch k.Kind() {
case reflect.Bool:
return d.decodeBool(name, node, result)
case reflect.Float32, reflect.Float64:
return d.decodeFloat(name, node, result)
case reflect.Int, reflect.Int32, reflect.Int64:
return d.decodeInt(name, node, result)
case reflect.Interface:
// When we see an interface, we make our own thing
return d.decodeInterface(name, node, result)
case reflect.Map:
return d.decodeMap(name, node, result)
case reflect.Ptr:
return d.decodePtr(name, node, result)
case reflect.Slice:
return d.decodeSlice(name, node, result)
case reflect.String:
return d.decodeString(name, node, result)
case reflect.Struct:
return d.decodeStruct(name, node, result)
default:
return &parser.PosError{
Pos: node.Pos(),
Err: fmt.Errorf("%s: unknown kind to decode into: %s", name, k.Kind()),
}
}
}
func (d *decoder) decodeBool(name string, node ast.Node, result reflect.Value) error {
switch n := node.(type) {
case *ast.LiteralType:
if n.Token.Type == token.BOOL {
v, err := strconv.ParseBool(n.Token.Text)
if err != nil {
return err
}
result.Set(reflect.ValueOf(v))
return nil
}
}
return &parser.PosError{
Pos: node.Pos(),
Err: fmt.Errorf("%s: unknown type %T", name, node),
}
}
func (d *decoder) decodeFloat(name string, node ast.Node, result reflect.Value) error {
switch n := node.(type) {
case *ast.LiteralType:
if n.Token.Type == token.FLOAT || n.Token.Type == token.NUMBER {
v, err := strconv.ParseFloat(n.Token.Text, 64)
if err != nil {
return err
}
result.Set(reflect.ValueOf(v).Convert(result.Type()))
return nil
}
}
return &parser.PosError{
Pos: node.Pos(),
Err: fmt.Errorf("%s: unknown type %T", name, node),
}
}
func (d *decoder) decodeInt(name string, node ast.Node, result reflect.Value) error {
switch n := node.(type) {
case *ast.LiteralType:
switch n.Token.Type {
case token.NUMBER:
v, err := strconv.ParseInt(n.Token.Text, 0, 0)
if err != nil {
return err
}
if result.Kind() == reflect.Interface {
result.Set(reflect.ValueOf(int(v)))
} else {
result.SetInt(v)
}
return nil
case token.STRING:
v, err := strconv.ParseInt(n.Token.Value().(string), 0, 0)
if err != nil {
return err
}
if result.Kind() == reflect.Interface {
result.Set(reflect.ValueOf(int(v)))
} else {
result.SetInt(v)
}
return nil
}
}
return &parser.PosError{
Pos: node.Pos(),
Err: fmt.Errorf("%s: unknown type %T", name, node),
}
}
func (d *decoder) decodeInterface(name string, node ast.Node, result reflect.Value) error {
// When we see an ast.Node, we retain the value to enable deferred decoding.
// Very useful in situations where we want to preserve ast.Node information
// like Pos
if result.Type() == nodeType && result.CanSet() {
result.Set(reflect.ValueOf(node))
return nil
}
var set reflect.Value
redecode := true
// For testing types, ObjectType should just be treated as a list. We
// set this to a temporary var because we want to pass in the real node.
testNode := node
if ot, ok := node.(*ast.ObjectType); ok {
testNode = ot.List
}
switch n := testNode.(type) {
case *ast.ObjectList:
// If we're at the root or we're directly within a slice, then we
// decode objects into map[string]interface{}, otherwise we decode
// them into lists.
if len(d.stack) == 0 || d.stack[len(d.stack)-1] == reflect.Slice {
var temp map[string]interface{}
tempVal := reflect.ValueOf(temp)
result := reflect.MakeMap(
reflect.MapOf(
reflect.TypeOf(""),
tempVal.Type().Elem()))
set = result
} else {
var temp []map[string]interface{}
tempVal := reflect.ValueOf(temp)
result := reflect.MakeSlice(
reflect.SliceOf(tempVal.Type().Elem()), 0, len(n.Items))
set = result
}
case *ast.ObjectType:
// If we're at the root or we're directly within a slice, then we
// decode objects into map[string]interface{}, otherwise we decode
// them into lists.
if len(d.stack) == 0 || d.stack[len(d.stack)-1] == reflect.Slice {
var temp map[string]interface{}
tempVal := reflect.ValueOf(temp)
result := reflect.MakeMap(
reflect.MapOf(
reflect.TypeOf(""),
tempVal.Type().Elem()))
set = result
} else {
var temp []map[string]interface{}
tempVal := reflect.ValueOf(temp)
result := reflect.MakeSlice(
reflect.SliceOf(tempVal.Type().Elem()), 0, 1)
set = result
}
case *ast.ListType:
var temp []interface{}
tempVal := reflect.ValueOf(temp)
result := reflect.MakeSlice(
reflect.SliceOf(tempVal.Type().Elem()), 0, 0)
set = result
case *ast.LiteralType:
switch n.Token.Type {
case token.BOOL:
var result bool
set = reflect.Indirect(reflect.New(reflect.TypeOf(result)))
case token.FLOAT:
var result float64
set = reflect.Indirect(reflect.New(reflect.TypeOf(result)))
case token.NUMBER:
var result int
set = reflect.Indirect(reflect.New(reflect.TypeOf(result)))
case token.STRING, token.HEREDOC:
set = reflect.Indirect(reflect.New(reflect.TypeOf("")))
default:
return &parser.PosError{
Pos: node.Pos(),
Err: fmt.Errorf("%s: cannot decode into interface: %T", name, node),
}
}
default:
return fmt.Errorf(
"%s: cannot decode into interface: %T",
name, node)
}
// Set the result to what its supposed to be, then reset
// result so we don't reflect into this method anymore.
result.Set(set)
if redecode {
// Revisit the node so that we can use the newly instantiated
// thing and populate it.
if err := d.decode(name, node, result); err != nil {
return err
}
}
return nil
}
func (d *decoder) decodeMap(name string, node ast.Node, result reflect.Value) error {
if item, ok := node.(*ast.ObjectItem); ok {
node = &ast.ObjectList{Items: []*ast.ObjectItem{item}}
}
if ot, ok := node.(*ast.ObjectType); ok {
node = ot.List
}
n, ok := node.(*ast.ObjectList)
if !ok {
return &parser.PosError{
Pos: node.Pos(),
Err: fmt.Errorf("%s: not an object type for map (%T)", name, node),
}
}
// If we have an interface, then we can address the interface,
// but not the slice itself, so get the element but set the interface
set := result
if result.Kind() == reflect.Interface {
result = result.Elem()
}
resultType := result.Type()
resultElemType := resultType.Elem()
resultKeyType := resultType.Key()
if resultKeyType.Kind() != reflect.String {
return &parser.PosError{
Pos: node.Pos(),
Err: fmt.Errorf("%s: map must have string keys", name),
}
}
// Make a map if it is nil
resultMap := result
if result.IsNil() {
resultMap = reflect.MakeMap(
reflect.MapOf(resultKeyType, resultElemType))
}
// Go through each element and decode it.
done := make(map[string]struct{})
for _, item := range n.Items {
if item.Val == nil {
continue
}
// github.com/hashicorp/terraform/issue/5740
if len(item.Keys) == 0 {
return &parser.PosError{
Pos: node.Pos(),
Err: fmt.Errorf("%s: map must have string keys", name),
}
}
// Get the key we're dealing with, which is the first item
keyStr := item.Keys[0].Token.Value().(string)
// If we've already processed this key, then ignore it
if _, ok := done[keyStr]; ok {
continue
}
// Determine the value. If we have more than one key, then we
// get the objectlist of only these keys.
itemVal := item.Val
if len(item.Keys) > 1 {
itemVal = n.Filter(keyStr)
done[keyStr] = struct{}{}
}
// Make the field name
fieldName := fmt.Sprintf("%s.%s", name, keyStr)
// Get the key/value as reflection values
key := reflect.ValueOf(keyStr)
val := reflect.Indirect(reflect.New(resultElemType))
// If we have a pre-existing value in the map, use that
oldVal := resultMap.MapIndex(key)
if oldVal.IsValid() {
val.Set(oldVal)
}
// Decode!
if err := d.decode(fieldName, itemVal, val); err != nil {
return err
}
// Set the value on the map
resultMap.SetMapIndex(key, val)
}
// Set the final map if we can
set.Set(resultMap)
return nil
}
func (d *decoder) decodePtr(name string, node ast.Node, result reflect.Value) error {
// Create an element of the concrete (non pointer) type and decode
// into that. Then set the value of the pointer to this type.
resultType := result.Type()
resultElemType := resultType.Elem()
val := reflect.New(resultElemType)
if err := d.decode(name, node, reflect.Indirect(val)); err != nil {
return err
}
result.Set(val)
return nil
}
func (d *decoder) decodeSlice(name string, node ast.Node, result reflect.Value) error {
// If we have an interface, then we can address the interface,
// but not the slice itself, so get the element but set the interface
set := result
if result.Kind() == reflect.Interface {
result = result.Elem()
}
// Create the slice if it isn't nil
resultType := result.Type()
resultElemType := resultType.Elem()
if result.IsNil() {
resultSliceType := reflect.SliceOf(resultElemType)
result = reflect.MakeSlice(
resultSliceType, 0, 0)
}
// Figure out the items we'll be copying into the slice
var items []ast.Node
switch n := node.(type) {
case *ast.ObjectList:
items = make([]ast.Node, len(n.Items))
for i, item := range n.Items {
items[i] = item
}
case *ast.ObjectType:
items = []ast.Node{n}
case *ast.ListType:
items = n.List
default:
return &parser.PosError{
Pos: node.Pos(),
Err: fmt.Errorf("unknown slice type: %T", node),
}
}
for i, item := range items {
fieldName := fmt.Sprintf("%s[%d]", name, i)
// Decode
val := reflect.Indirect(reflect.New(resultElemType))
// if item is an object that was decoded from ambiguous JSON and
// flattened, make sure it's expanded if it needs to decode into a
// defined structure.
item := expandObject(item, val)
if err := d.decode(fieldName, item, val); err != nil {
return err
}
// Append it onto the slice
result = reflect.Append(result, val)
}
set.Set(result)
return nil
}
// expandObject detects if an ambiguous JSON object was flattened to a List which
// should be decoded into a struct, and expands the ast to properly deocode.
func expandObject(node ast.Node, result reflect.Value) ast.Node {
item, ok := node.(*ast.ObjectItem)
if !ok {
return node
}
elemType := result.Type()
// our target type must be a struct
switch elemType.Kind() {
case reflect.Ptr:
switch elemType.Elem().Kind() {
case reflect.Struct:
//OK
default:
return node
}
case reflect.Struct:
//OK
default:
return node
}
// A list value will have a key and field name. If it had more fields,
// it wouldn't have been flattened.
if len(item.Keys) != 2 {
return node
}
keyToken := item.Keys[0].Token
item.Keys = item.Keys[1:]
// we need to un-flatten the ast enough to decode
newNode := &ast.ObjectItem{
Keys: []*ast.ObjectKey{
&ast.ObjectKey{
Token: keyToken,
},
},
Val: &ast.ObjectType{
List: &ast.ObjectList{
Items: []*ast.ObjectItem{item},
},
},
}
return newNode
}
func (d *decoder) decodeString(name string, node ast.Node, result reflect.Value) error {
switch n := node.(type) {
case *ast.LiteralType:
switch n.Token.Type {
case token.NUMBER:
result.Set(reflect.ValueOf(n.Token.Text).Convert(result.Type()))
return nil
case token.STRING, token.HEREDOC:
result.Set(reflect.ValueOf(n.Token.Value()).Convert(result.Type()))
return nil
}
}
return &parser.PosError{
Pos: node.Pos(),
Err: fmt.Errorf("%s: unknown type for string %T", name, node),
}
}
func (d *decoder) decodeStruct(name string, node ast.Node, result reflect.Value) error {
var item *ast.ObjectItem
if it, ok := node.(*ast.ObjectItem); ok {
item = it
node = it.Val
}
if ot, ok := node.(*ast.ObjectType); ok {
node = ot.List
}
// Handle the special case where the object itself is a literal. Previously
// the yacc parser would always ensure top-level elements were arrays. The new
// parser does not make the same guarantees, thus we need to convert any
// top-level literal elements into a list.
if _, ok := node.(*ast.LiteralType); ok && item != nil {
node = &ast.ObjectList{Items: []*ast.ObjectItem{item}}
}
list, ok := node.(*ast.ObjectList)
if !ok {
return &parser.PosError{
Pos: node.Pos(),
Err: fmt.Errorf("%s: not an object type for struct (%T)", name, node),
}
}
// This slice will keep track of all the structs we'll be decoding.
// There can be more than one struct if there are embedded structs
// that are squashed.
structs := make([]reflect.Value, 1, 5)
structs[0] = result
// Compile the list of all the fields that we're going to be decoding
// from all the structs.
type field struct {
field reflect.StructField
val reflect.Value
}
fields := []field{}
for len(structs) > 0 {
structVal := structs[0]
structs = structs[1:]
structType := structVal.Type()
for i := 0; i < structType.NumField(); i++ {
fieldType := structType.Field(i)
tagParts := strings.Split(fieldType.Tag.Get(tagName), ",")
// Ignore fields with tag name "-"
if tagParts[0] == "-" {
continue
}
if fieldType.Anonymous {
fieldKind := fieldType.Type.Kind()
if fieldKind != reflect.Struct {
return &parser.PosError{
Pos: node.Pos(),
Err: fmt.Errorf("%s: unsupported type to struct: %s",
fieldType.Name, fieldKind),
}
}
// We have an embedded field. We "squash" the fields down
// if specified in the tag.
squash := false
for _, tag := range tagParts[1:] {
if tag == "squash" {
squash = true
break
}
}
if squash {
structs = append(
structs, result.FieldByName(fieldType.Name))
continue
}
}
// Normal struct field, store it away
fields = append(fields, field{fieldType, structVal.Field(i)})
}
}
usedKeys := make(map[string]struct{})
decodedFields := make([]string, 0, len(fields))
decodedFieldsVal := make([]reflect.Value, 0)
unusedKeysVal := make([]reflect.Value, 0)
for _, f := range fields {
field, fieldValue := f.field, f.val
if !fieldValue.IsValid() {
// This should never happen
panic("field is not valid")
}
// If we can't set the field, then it is unexported or something,
// and we just continue onwards.
if !fieldValue.CanSet() {
continue
}
fieldName := field.Name
tagValue := field.Tag.Get(tagName)
tagParts := strings.SplitN(tagValue, ",", 2)
if len(tagParts) >= 2 {
switch tagParts[1] {
case "decodedFields":
decodedFieldsVal = append(decodedFieldsVal, fieldValue)
continue
case "key":
if item == nil {
return &parser.PosError{
Pos: node.Pos(),
Err: fmt.Errorf("%s: %s asked for 'key', impossible",
name, fieldName),
}
}
fieldValue.SetString(item.Keys[0].Token.Value().(string))
continue
case "unusedKeys":
unusedKeysVal = append(unusedKeysVal, fieldValue)
continue
}
}
if tagParts[0] != "" {
fieldName = tagParts[0]
}
// Determine the element we'll use to decode. If it is a single
// match (only object with the field), then we decode it exactly.
// If it is a prefix match, then we decode the matches.
filter := list.Filter(fieldName)
prefixMatches := filter.Children()
matches := filter.Elem()
if len(matches.Items) == 0 && len(prefixMatches.Items) == 0 {
continue
}
// Track the used key
usedKeys[fieldName] = struct{}{}
// Create the field name and decode. We range over the elements
// because we actually want the value.
fieldName = fmt.Sprintf("%s.%s", name, fieldName)
if len(prefixMatches.Items) > 0 {
if err := d.decode(fieldName, prefixMatches, fieldValue); err != nil {
return err
}
}
for _, match := range matches.Items {
var decodeNode ast.Node = match.Val
if ot, ok := decodeNode.(*ast.ObjectType); ok {
decodeNode = &ast.ObjectList{Items: ot.List.Items}
}
if err := d.decode(fieldName, decodeNode, fieldValue); err != nil {
return err
}
}
decodedFields = append(decodedFields, field.Name)
}
if len(decodedFieldsVal) > 0 {
// Sort it so that it is deterministic
sort.Strings(decodedFields)
for _, v := range decodedFieldsVal {
v.Set(reflect.ValueOf(decodedFields))
}
}
return nil
}
// findNodeType returns the type of ast.Node
func findNodeType() reflect.Type {
var nodeContainer struct {
Node ast.Node
}
value := reflect.ValueOf(nodeContainer).FieldByName("Node")
return value.Type()
}
+11
View File
@@ -0,0 +1,11 @@
// Package hcl decodes HCL into usable Go structures.
//
// hcl input can come in either pure HCL format or JSON format.
// It can be parsed into an AST, and then decoded into a structure,
// or it can be decoded directly from a string into a structure.
//
// If you choose to parse HCL into a raw AST, the benefit is that you
// can write custom visitor implementations to implement custom
// semantic checks. By default, HCL does not perform any semantic
// checks.
package hcl
+219
View File
@@ -0,0 +1,219 @@
// Package ast declares the types used to represent syntax trees for HCL
// (HashiCorp Configuration Language)
package ast
import (
"fmt"
"strings"
"github.com/hashicorp/hcl/hcl/token"
)
// Node is an element in the abstract syntax tree.
type Node interface {
node()
Pos() token.Pos
}
func (File) node() {}
func (ObjectList) node() {}
func (ObjectKey) node() {}
func (ObjectItem) node() {}
func (Comment) node() {}
func (CommentGroup) node() {}
func (ObjectType) node() {}
func (LiteralType) node() {}
func (ListType) node() {}
// File represents a single HCL file
type File struct {
Node Node // usually a *ObjectList
Comments []*CommentGroup // list of all comments in the source
}
func (f *File) Pos() token.Pos {
return f.Node.Pos()
}
// ObjectList represents a list of ObjectItems. An HCL file itself is an
// ObjectList.
type ObjectList struct {
Items []*ObjectItem
}
func (o *ObjectList) Add(item *ObjectItem) {
o.Items = append(o.Items, item)
}
// Filter filters out the objects with the given key list as a prefix.
//
// The returned list of objects contain ObjectItems where the keys have
// this prefix already stripped off. This might result in objects with
// zero-length key lists if they have no children.
//
// If no matches are found, an empty ObjectList (non-nil) is returned.
func (o *ObjectList) Filter(keys ...string) *ObjectList {
var result ObjectList
for _, item := range o.Items {
// If there aren't enough keys, then ignore this
if len(item.Keys) < len(keys) {
continue
}
match := true
for i, key := range item.Keys[:len(keys)] {
key := key.Token.Value().(string)
if key != keys[i] && !strings.EqualFold(key, keys[i]) {
match = false
break
}
}
if !match {
continue
}
// Strip off the prefix from the children
newItem := *item
newItem.Keys = newItem.Keys[len(keys):]
result.Add(&newItem)
}
return &result
}
// Children returns further nested objects (key length > 0) within this
// ObjectList. This should be used with Filter to get at child items.
func (o *ObjectList) Children() *ObjectList {
var result ObjectList
for _, item := range o.Items {
if len(item.Keys) > 0 {
result.Add(item)
}
}
return &result
}
// Elem returns items in the list that are direct element assignments
// (key length == 0). This should be used with Filter to get at elements.
func (o *ObjectList) Elem() *ObjectList {
var result ObjectList
for _, item := range o.Items {
if len(item.Keys) == 0 {
result.Add(item)
}
}
return &result
}
func (o *ObjectList) Pos() token.Pos {
// always returns the uninitiliazed position
return o.Items[0].Pos()
}
// ObjectItem represents a HCL Object Item. An item is represented with a key
// (or keys). It can be an assignment or an object (both normal and nested)
type ObjectItem struct {
// keys is only one length long if it's of type assignment. If it's a
// nested object it can be larger than one. In that case "assign" is
// invalid as there is no assignments for a nested object.
Keys []*ObjectKey
// assign contains the position of "=", if any
Assign token.Pos
// val is the item itself. It can be an object,list, number, bool or a
// string. If key length is larger than one, val can be only of type
// Object.
Val Node
LeadComment *CommentGroup // associated lead comment
LineComment *CommentGroup // associated line comment
}
func (o *ObjectItem) Pos() token.Pos {
// I'm not entirely sure what causes this, but removing this causes
// a test failure. We should investigate at some point.
if len(o.Keys) == 0 {
return token.Pos{}
}
return o.Keys[0].Pos()
}
// ObjectKeys are either an identifier or of type string.
type ObjectKey struct {
Token token.Token
}
func (o *ObjectKey) Pos() token.Pos {
return o.Token.Pos
}
// LiteralType represents a literal of basic type. Valid types are:
// token.NUMBER, token.FLOAT, token.BOOL and token.STRING
type LiteralType struct {
Token token.Token
// comment types, only used when in a list
LeadComment *CommentGroup
LineComment *CommentGroup
}
func (l *LiteralType) Pos() token.Pos {
return l.Token.Pos
}
// ListStatement represents a HCL List type
type ListType struct {
Lbrack token.Pos // position of "["
Rbrack token.Pos // position of "]"
List []Node // the elements in lexical order
}
func (l *ListType) Pos() token.Pos {
return l.Lbrack
}
func (l *ListType) Add(node Node) {
l.List = append(l.List, node)
}
// ObjectType represents a HCL Object Type
type ObjectType struct {
Lbrace token.Pos // position of "{"
Rbrace token.Pos // position of "}"
List *ObjectList // the nodes in lexical order
}
func (o *ObjectType) Pos() token.Pos {
return o.Lbrace
}
// Comment node represents a single //, # style or /*- style commment
type Comment struct {
Start token.Pos // position of / or #
Text string
}
func (c *Comment) Pos() token.Pos {
return c.Start
}
// CommentGroup node represents a sequence of comments with no other tokens and
// no empty lines between.
type CommentGroup struct {
List []*Comment // len(List) > 0
}
func (c *CommentGroup) Pos() token.Pos {
return c.List[0].Pos()
}
//-------------------------------------------------------------------
// GoStringer
//-------------------------------------------------------------------
func (o *ObjectKey) GoString() string { return fmt.Sprintf("*%#v", *o) }
func (o *ObjectList) GoString() string { return fmt.Sprintf("*%#v", *o) }
+52
View File
@@ -0,0 +1,52 @@
package ast
import "fmt"
// WalkFunc describes a function to be called for each node during a Walk. The
// returned node can be used to rewrite the AST. Walking stops the returned
// bool is false.
type WalkFunc func(Node) (Node, bool)
// Walk traverses an AST in depth-first order: It starts by calling fn(node);
// node must not be nil. If fn returns true, Walk invokes fn recursively for
// each of the non-nil children of node, followed by a call of fn(nil). The
// returned node of fn can be used to rewrite the passed node to fn.
func Walk(node Node, fn WalkFunc) Node {
rewritten, ok := fn(node)
if !ok {
return rewritten
}
switch n := node.(type) {
case *File:
n.Node = Walk(n.Node, fn)
case *ObjectList:
for i, item := range n.Items {
n.Items[i] = Walk(item, fn).(*ObjectItem)
}
case *ObjectKey:
// nothing to do
case *ObjectItem:
for i, k := range n.Keys {
n.Keys[i] = Walk(k, fn).(*ObjectKey)
}
if n.Val != nil {
n.Val = Walk(n.Val, fn)
}
case *LiteralType:
// nothing to do
case *ListType:
for i, l := range n.List {
n.List[i] = Walk(l, fn)
}
case *ObjectType:
n.List = Walk(n.List, fn).(*ObjectList)
default:
// should we panic here?
fmt.Printf("unknown type: %T\n", n)
}
fn(nil)
return rewritten
}
+17
View File
@@ -0,0 +1,17 @@
package parser
import (
"fmt"
"github.com/hashicorp/hcl/hcl/token"
)
// PosError is a parse error that contains a position.
type PosError struct {
Pos token.Pos
Err error
}
func (e *PosError) Error() string {
return fmt.Sprintf("At %s: %s", e.Pos, e.Err)
}

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