Compare commits

...

1365 Commits

Author SHA1 Message Date
Jesse Duffield
3eb124c732 easier hiding of command log 2021-10-23 12:54:57 +11:00
Jesse Duffield
ef544e6ce9 add more suggestions 2021-10-23 12:29:52 +11:00
Jesse Duffield
629494144f show suggestions when typing in an origin 2021-10-23 12:29:52 +11:00
Jesse Duffield
b6a5e9d615 use cached git config 2021-10-23 10:26:47 +11:00
Jesse Duffield
5011cac7ea show filetree by default 2021-10-22 22:39:17 +11:00
Sam Burville
5df0475612 Add variable to simplify JumpToBlock keybinding
This removes the magic '5' and instead uses the number of windows.
2021-10-22 22:38:26 +11:00
Sam Burville
f6e316dfe5 Improve JumpToBlock keybinding functionality
Improve experience when yaml file has != 5 keybindings and change view
helper to use the length of the array instead of hardcoded value.
2021-10-22 22:38:26 +11:00
Sam Burville
91e8765d9c Add JumpToBlock keybinding
This should allow users to decide their own keybinding for jumping
between blocks/panels.
E.g. A user could choose 5-9 instead of 1-5.
2021-10-22 22:38:26 +11:00
Jesse Duffield
80a8e9b04d fix merge conflict scrolling 2021-10-22 22:16:52 +11:00
Jesse Duffield
2008c39516 add tests for dealing with remotes 2021-10-22 21:33:17 +11:00
Jesse Duffield
6388af70ac simplify pull logic 2021-10-22 21:33:17 +11:00
Jesse Duffield
5ee559b896 fix issue where upstream origin and branch were quoted together
fix issue where upstream origin and branch were quoted together
2021-10-20 09:29:17 +11:00
Jesse Duffield
ca7252ef8e suggest files when picking a path to filter on
async fetching of suggestions

remove limit

cache the trie for future use

more

more
2021-10-19 09:02:42 +11:00
Mark Kopenga
a496858c62 Merge pull request #1529 from Ryooooooga/feature/backward-compatibility
Improve backward compatibility
2021-10-18 18:58:46 +02:00
Ryooooooga
40bc3aa5a9 Improve backward compatibility 2021-10-18 22:44:01 +09:00
Jesse Duffield
e4888e924e update docs 2021-10-18 22:24:51 +11:00
Jesse Duffield
71fdc5c038 better title rendering 2021-10-18 09:21:33 +11:00
Jesse Duffield
a05f22efa2 support home/end keys in editors 2021-10-17 20:14:31 +11:00
Jesse Duffield
c0cd9dd835 stop opening suggestions tab when no suggestions present 2021-10-17 20:05:09 +11:00
Jesse Duffield
305f211615 surface error when trying to set upstream 2021-10-17 09:00:08 +00:00
Jesse Duffield
d672b7342f stop resetting scroll all the time 2021-10-17 19:45:57 +11:00
Jesse Duffield
e7c27b6f4a small fixes 2021-10-17 06:41:21 +00:00
Jesse Duffield
345c90ac05 fix editor 2021-10-17 04:17:59 +00:00
Ryooooooga
7564e506b5 Enable/disable os specific tests at compile time 2021-10-17 11:00:20 +11:00
Ryooooooga
1e50764b4d Fix tests 2021-10-17 11:00:20 +11:00
Ryooooooga
9619d3447f Run tests on Windows 2021-10-17 11:00:20 +11:00
Ryooooooga
4171b7613c Use replacer 2021-10-16 22:40:50 +11:00
Ryooooooga
92f03a7872 Escape special characters 2021-10-16 22:40:50 +11:00
Ryooooooga
2dc8396deb Fix test 2021-10-16 22:40:50 +11:00
Ryooooooga
7b615e3186 Fix open link command in Windows 2021-10-16 22:40:50 +11:00
Adam Łyskawa
a2108362de Update polish.go
Previous translation was completely unacceptable. As a native Polish speaker I consulted various Polish documentation files and translated almost all content.
My translation may contain minor errors due to the possible lack of context for some text.
2021-10-16 22:32:34 +11:00
Jesse Duffield
87e9d9bdc2 minor changes 2021-10-16 21:18:43 +11:00
Hrishikesh Hiraskar
b6454755ca copy selected text to clipboard 2021-10-16 21:18:43 +11:00
Jesse Duffield
3621084096 small changes 2021-10-16 12:22:34 +11:00
Jesse Duffield
8c25aaa687 extra comment 2021-10-16 12:22:34 +11:00
Jesse Duffield
d02e52989e small changes 2021-10-16 12:22:34 +11:00
mjarkk
913a2fd065 Allow having multiple config files 2021-10-16 12:22:34 +11:00
Mark Kopenga
db736896bc Merge pull request #1501 from Ryooooooga/feature/fix-command
Improved command execution
2021-10-09 11:10:29 +02:00
Ryooooooga
154b6b09cb Quote ref names and branches 2021-10-09 12:55:00 +09:00
Ryooooooga
292b780bd8 Quote branch names and remote names 2021-10-08 18:36:05 +09:00
Mark Kopenga
c421f396af Merge pull request #1473 from black-desk/tmp
Improve Chinese translation
2021-10-08 09:53:48 +02:00
black_desk
a1ae2aa277 Improve Chinese translation 2021-10-08 08:49:22 +08:00
Ryooooooga
e19b4fe369 Fix git-remote commands 2021-10-06 23:20:19 +09:00
Ryooooooga
eb7531b206 Fix error prompt when new tag name starts with '--' 2021-10-06 22:57:02 +09:00
Ryooooooga
428ce2d0f2 Fix crash when new submodule url contains double quotes 2021-10-06 22:51:24 +09:00
Ryooooooga
f1fbf1e9f5 Fix crash when try to ignore tracked files 2021-10-06 22:43:30 +09:00
Sam Burville
268d4080b3 Fix text formatting 2021-09-30 01:26:05 +10:00
Jesse Duffield
2c72990838 Update pkg/theme/theme.go 2021-09-30 01:26:05 +10:00
Jesse Duffield
046edd8120 Update pkg/theme/theme.go 2021-09-30 01:26:05 +10:00
samburville
c4552aad28 Use simpler short variable declaration
Co-authored-by: Jesse Duffield <jessedduffield@gmail.com>
2021-09-30 01:26:05 +10:00
Sam Burville
5c57c973d6 Tidy of spacing on GetDefaultConfig in user_config 2021-09-30 01:26:05 +10:00
Sam Burville
c5f7ad5adb Make cherry pick commit color customisable
Two new settings in the config, which allow the cherry picked
foreground and background to be custom colors.

Issue #856
2021-09-30 01:26:05 +10:00
Ryooooooga
663c036ca5 Save patch files in TempDir 2021-09-29 22:05:58 +10:00
Jesse Duffield
c8e9d1b4fc bump gocui 2021-09-27 19:58:24 +10:00
Jesse Duffield
ab0117c416 fix some encodings 2021-09-27 19:58:24 +10:00
Jesse Duffield
652c97d239 honour options menu press 2021-09-27 19:41:38 +10:00
Jesse Duffield
bd67bba751 Update README.md 2021-09-25 13:14:26 +10:00
Mark Kopenga
5193353020 Merge pull request #1478 from black-desk/remove-unused-strings
Remove unused strings
2021-09-23 22:23:52 +02:00
Mark Kopenga
a5719c530a Merge pull request #1481 from Ryooooooga/feature/fix-remove-tracked-files
Fix crash on remove tracked files #1480
2021-09-21 12:18:28 +02:00
Ryooooooga
add3e8783e Fix crash on remove tracked files #1480 2021-09-21 18:51:18 +09:00
black_desk
5eff56b557 Remove unused strings 2021-09-19 19:05:11 +08:00
Mark Kopenga
60c87b3e70 Merge pull request #1479 from black-desk/format-code
Format code to pass lint
2021-09-19 11:40:49 +02:00
black_desk
66d0fd2133 Format code to pass lint 2021-09-16 21:38:43 +08:00
Mark Kopenga
f44ae68e99 Merge pull request #1463 from Ryooooooga/feature/fix-delete-branch 2021-09-04 20:43:48 +02:00
Ryooooooga
57f7051590 Fix deletion of unmerged branches in languages other than English 2021-09-04 21:01:38 +09:00
Mark Kopenga
0543d43f10 Merge pull request #1459 from btwise/master
fix chinese translate
2021-09-02 09:27:46 +02:00
Mark Kopenga
ab8f2b7cc4 Merge pull request #1461 from mjarkk/remove-ununsed-gomod-replace
Remove unused dep replacement in go.mod
2021-09-02 09:19:34 +02:00
mjarkk
16dbb6f76e remove unused dep replacement in go.mod 2021-09-02 09:13:18 +02:00
Mark Kopenga
51383f24bf Merge pull request #1458 from codesoap/master
Fix misspells
2021-09-02 09:09:24 +02:00
btwise
151486dcfb fix chinese translate 2021-09-02 12:16:53 +08:00
codesoap
c1d2aa61f3 Fix misspells 2021-09-01 22:51:24 +02:00
Dwarven YANG
63072af5bc allow user to configure the gui language 2021-08-30 09:12:29 +10:00
Jesse Duffield
44d08edfb0 Address feedback 2021-08-25 22:23:55 +10:00
Jesse Duffield
f08fdb2873 Minor refactor 2021-08-25 22:23:55 +10:00
Ryooooooga
df4eb70ba2 Fix translations 2021-08-25 22:23:55 +10:00
Ryooooooga
6ca42ff720 Fix pick all hunks 2021-08-25 22:23:55 +10:00
Ryooooooga
a533f8e1a5 simplify merge panel logic 2021-08-25 22:23:55 +10:00
Ryooooooga
cf8ded0b79 add mergeConflict#hasAncestor 2021-08-25 22:23:55 +10:00
Ryooooooga
73548fa15f Fix conflict resolution 2021-08-25 22:23:55 +10:00
Ryooooooga
a0e7604f61 Support git config merge.conflictStyle diff3 2021-08-25 22:23:55 +10:00
Daniel Bast
aedeba4fe3 Limit to security updates 2021-08-25 21:43:58 +10:00
Daniel Bast
7033a4bd58 Add dependabot config for dependency updates 2021-08-25 21:43:58 +10:00
Ryooooooga
c3d7de1c18 Change not to use cat command 2021-08-25 21:32:48 +10:00
Liberatys
711bd5a670 Lint 2021-08-25 20:13:50 +10:00
Liberatys
6b68f4f25d Update as per review and add tests 2021-08-25 20:13:50 +10:00
Liberatys
89ee0a1dee Move field names to translation 2021-08-25 20:13:50 +10:00
Liberatys
2dc6f5f079 Implement state filtering for commit files 2021-08-25 20:13:50 +10:00
Mark Kopenga
bdea3b7dcf Merge pull request #1448 from Ryooooooga/feature/fix-open-files-in-windows
Fix open command in Windows #1403
2021-08-23 15:37:07 +02:00
Ryooooooga
51f05ce08b Fix open command in Windows 2021-08-23 21:21:34 +09:00
Mark Kopenga
487ad196a7 Merge pull request #1413 from Ryooooooga/feature/edit-line
Make os.editCommand customizable using template
2021-08-23 10:15:38 +02:00
Ryoga
44140adb92 Update docs/Config.md
Co-authored-by: Jesse Duffield <jessedduffield@gmail.com>
2021-08-23 08:33:05 +09:00
Mark Kopenga
508af269fb Merge pull request #1446 from Ryooooooga/feature/fix-panic-in-merge-conflict
Fix panic during merge conflict #1375
2021-08-21 18:07:21 +02:00
Ryooooooga
0af0e66586 Fix panic in merge conflict 2021-08-21 18:34:30 +09:00
Ryooooooga
821a59f21d apply suggestion for the document of editCommandTemplate 2021-08-21 01:01:34 +09:00
Ryooooooga
5ea3dc7579 add documentation of editCommandTemplate 2021-08-20 22:39:08 +09:00
Mark Kopenga
2eeff1257b Merge pull request #1432 from black-desk/moved-submodule
fix moved submodule
2021-08-17 10:10:22 +02:00
Mark Kopenga
c878f34ff1 Merge pull request #1438 from Ryooooooga/feature/rename-files-with-modification
Fix staged renamed file with unstaged in file pane #1408
2021-08-16 17:29:08 +02:00
Mark Kopenga
f8db3592e3 Merge pull request #1440 from Ryooooooga/feature/quote-git-C-path
Fix stash submodule #1436
2021-08-16 16:43:12 +02:00
Ryooooooga
d073932cec Fix stash submodule #1436 2021-08-16 23:36:16 +09:00
Ryooooooga
a2f7fcd730 Remove unused constant 2021-08-16 23:21:46 +09:00
Ryooooooga
f96674b24b Fix error when filename contains -> 2021-08-16 23:15:37 +09:00
Ryooooooga
a553f7fb77 Fix staged renamed file with unstaged changes displays incorrectly in Files view #1408 2021-08-16 20:05:59 +09:00
Mark Kopenga
6c415d1341 Merge pull request #1434 from Ryooooooga/feature/fix-quote-in-filename
the patch panel would crash if the filename contained an odd number of double quotes
2021-08-16 09:52:06 +02:00
Mark Kopenga
617e8a05ee Merge pull request #1437 from Ryooooooga/feature/fix-submodule-update
Fix submodule command escaping #1436
2021-08-16 09:48:50 +02:00
Ryooooooga
b21ac990ea fix submodule command escaping #1436 2021-08-16 12:34:52 +09:00
Ryooooooga
0740409f43 fix test 2021-08-13 22:15:06 +09:00
Ryooooooga
37700908cc fix checkout file command 2021-08-13 21:49:40 +09:00
Ryooooooga
488c43aaa2 fix crash when double quotes in filename #1433 2021-08-13 21:39:38 +09:00
black_desk
bb4fe2653b fix moved submodule 2021-08-13 17:03:37 +08:00
Mark Kopenga
a2ee52142c Merge pull request #1433 from black-desk/fix-space-in-filename
fix empty patch panel when spaces in filename
2021-08-13 10:47:40 +02:00
black_desk
66d735acb5 Update pkg/commands/files.go
Co-authored-by: Mark Kopenga <mkopenga@gmail.com>
2021-08-13 16:41:23 +08:00
black_desk
d51b065f2a fix empty patch panel when spaces in filename 2021-08-13 14:02:11 +08:00
Mark Kopenga
a3a14e9ff4 Merge pull request #1423 from FoamScience/feature/colorsInMenuFromCommand
Support match colors in `labelFormat` entry in menuFromCommand prompts
2021-08-09 21:13:32 +02:00
mjarkk
e58376f9f7 add tests for TemplateFuncMapAddColors 2021-08-09 21:09:52 +02:00
Elwardi
e8e4fa5957 Add color functions to templates funcMaps 2021-08-09 11:52:00 +01:00
Elwardi
b5d8849c06 Support match colors in labelFormat entry in menuFromCommand prompts 2021-08-07 16:06:36 +01:00
Mark Kopenga
5d1a9639b6 Merge pull request #1416 from FoamScience/feature_menuOptions 2021-08-07 15:24:42 +02:00
mjarkk
ea136e4e77 Improve code quality
- Make CommandMenuEntry private
- create candidates only once we really need it
- Use only 1 buffer
- Clearify CommandMenuEntry creation fields
2021-08-06 21:50:58 +02:00
Elwardi
dcd3b7c058 Show only labels in menuFromCommand prompts 2021-08-06 18:38:26 +01:00
Mark Kopenga
fd8cb6e6d7 Merge pull request #1419 from mrgarelli/1418_bug_tests_ForceSetColorLevel
1418 Fixing test failure due to ColorLevel in test renderers
2021-08-06 14:30:26 +02:00
Elwardi
906ec30cac Minor changes to menuFromCommand prompts 2021-08-06 10:53:32 +01:00
Matthew Garelli
46c146a8c1 fixed test failing due to ForceSetColorLevel in pkg/gui/style/style_test.go 2021-08-06 02:18:04 -07:00
Elwardi
a8ec044f0e Make menuFromCommand format menu items and their description 2021-08-05 15:45:18 +01:00
Ryooooooga
ac609bd37c fix backward compatibility 2021-08-04 18:43:34 +09:00
Ryooooooga
67cc65930a fix out of range error 2021-08-03 22:00:28 +09:00
Ryooooooga
4f66093335 introduce edit command template to open a specifig line of a file 2021-08-03 21:42:14 +09:00
Jesse Duffield
d626bcac00 color fixups 2021-08-01 16:14:56 +10:00
Jesse Duffield
123d624141 make import explicit 2021-08-01 13:23:59 +10:00
Jesse Duffield
e798aa4b15 more color tests 2021-08-01 13:21:06 +10:00
Mark Kopenga
04e474aa66 Merge pull request #1401 from jesseduffield/switch-text-color-library
Switch to gookit/color for terminal text styling
2021-07-31 20:58:40 +02:00
mjarkk
0662733ad9 add tests for color changes 2021-07-31 20:53:49 +02:00
Mark Kopenga
3c78ba7ed3 Merge pull request #1409 from jesseduffield/jesse-switch-text-color-library 2021-07-31 11:02:16 +02:00
Jesse Duffield
550c0fd4dc refactor 2021-07-31 17:56:47 +10:00
Jesse Duffield
0bc0e4ac88 more efficient 2021-07-31 17:33:20 +10:00
Jesse Duffield
117c0bd4f7 simplify code a bit 2021-07-31 17:33:13 +10:00
mjarkk
79848087bc Switch to github.com/gookit/color for terminal colors 2021-07-30 15:14:46 +02:00
mjarkk
a3b820fb5f add missing universal keybindings to doc 2021-07-29 14:15:26 +02:00
Mark Kopenga
de5133ff90 Merge pull request #1402 from jesseduffield/ci-build-less-binaries
PRs CI Build less types of go binaries
2021-07-28 15:45:51 +02:00
mjarkk
1183de151a revert changes from bfc9881 2021-07-28 15:42:37 +02:00
mjarkk
bfc9881213 added changes that should fail the ci on windows 2021-07-28 15:41:23 +02:00
mjarkk
3db40a79fe Sperate gh action build step 2021-07-28 15:40:06 +02:00
Jesse Duffield
62393cf28a more treeish files 2021-07-27 21:52:42 +10:00
Jesse Duffield
ec82f8099c update keybindings 2021-07-27 21:30:08 +10:00
Jesse Duffield
b81bac3d65 more i18n 2021-07-27 21:30:08 +10:00
Jesse Duffield
58ddbae4d1 Minor refactor 2021-07-27 21:30:08 +10:00
Denis Palashevskii
3802b563b0 Add error message if target branch not found with prompt 2021-07-27 21:30:08 +10:00
Denis Palashevskii
d1134daa53 review fixes: PR URL refactoring, target branch selection prompt 2021-07-27 21:30:08 +10:00
Denis Palashevskii
63cb304a82 Fix translations, make formatter happy 2021-07-27 21:30:08 +10:00
Denis Palashevskii
6e579dc6e4 Update localized Keybinding file 2021-07-27 21:30:08 +10:00
Denis Palashevskii
d5ec0fdcd1 Remove doubled string formatting in pull request URL generation 2021-07-27 21:30:08 +10:00
Denis Palashevskii
0a63f701e5 Apply suggestions from code review
Co-authored-by: Mark Kopenga <mkopenga@gmail.com>
2021-07-27 21:30:08 +10:00
Denis Palashevskii
bccf203a18 Fix menu item color 2021-07-27 21:30:08 +10:00
Denis Palashevskii
b590397dce Update docs 2021-07-27 21:30:08 +10:00
Denis Palashevskii
755cc9f8d8 Add tests 2021-07-27 21:30:08 +10:00
Denis Palashevskii
0e6598adbd Implement pull request options menu 2021-07-27 21:30:08 +10:00
Denis Palashevskii
f2645da16a Extract git service URL formatting to a separate method 2021-07-27 21:30:08 +10:00
Francisco Miamoto
f8f596d097 add tests for open file cmd on linux 2021-07-27 20:28:00 +10:00
Francisco Miamoto
028cb2be2f add extra quoting for shell cmd string on linux
This solves an issue where we could not open files with names that contained
spaces and single quotes.
It also  solves an issue of variable expansion for files with some kind
of environment variables on the name e.g. '$USER.txt'
2021-07-27 20:28:00 +10:00
Evan Boehs
fb69bfd20d Update english.go 2021-07-27 14:57:41 +10:00
Mark Kopenga
f4874bbb74 Merge pull request #1396 from mjarkk/fix-1385
branches check for split parts length
2021-07-26 11:22:33 +02:00
Mark Kopenga
eec20b845d Merge pull request #1392 from mjarkk/parcally-fix-1385
Change the way file statuses are loaded
2021-07-26 11:22:14 +02:00
mjarkk
3a0a9ec33b branches check for split parts length 2021-07-26 11:07:42 +02:00
Mark Kopenga
9b57b73f41 Merge pull request #1395 from mjarkk/allow-hex-theme-colors
Allow hex theme colors
2021-07-26 10:48:25 +02:00
mjarkk
4fca89bc52 Allow hex theme colors 2021-07-26 10:38:45 +02:00
mjarkk
fc76b44b45 correctly show files with special chars in commit 2021-07-23 12:04:23 +02:00
mjarkk
9a087d04eb Change the way file statuses are loaded
This makes it so file statuses recived from git no longer get joined
before spliting them again.
2021-07-22 22:12:43 +02:00
Mark Kopenga
c005b0d92b Merge pull request #1390 from FoamScience/menu_from_cmd
Generate menu options from a Git Command with a filter
2021-07-22 19:54:14 +02:00
mjarkk
713fae3e32 format code 2021-07-22 19:45:43 +02:00
Elwardi
148bf2c070 Add test for GenerateMenuCandidates from Custom Commands 2021-07-22 15:44:16 +01:00
Elwardi
edfb0a26b2 Refactor code around handleCustomCommandKeybinding 2021-07-20 20:59:03 +01:00
Elwardi
f70435a20f Better format error catching in menuFromCommand prompts 2021-07-19 13:41:42 +01:00
Elwardi
b92ff3ee3f Consider first match only in menuFromCommand prompt 2021-07-19 13:06:00 +01:00
Elwardi
f1ced5539a Add option to format filter matches to menuFromCommand prompts 2021-07-19 11:46:29 +01:00
Elwardi
77e9ee64a4 Apply suggestions from @mjarkk for menyFromCommands 2021-07-18 18:42:42 +01:00
Elwardi
9daa47fb2d Add docs for menuFromCommand prompts 2021-07-18 10:36:01 +01:00
Elwardi
d18c8c8dc3 Add prompt type: menuFromCommand 2021-07-18 10:36:00 +01:00
Mark Kopenga
1573a449f8 Merge pull request #1389 from mjarkk/parse-emoji-update-docs
Add parse github emoji to config docs
2021-07-16 21:14:21 +02:00
mjarkk
7b19c5ad95 add parse github emoji to docs 2021-07-16 21:13:01 +02:00
Mark Kopenga
b363b75534 Merge pull request #1387 from mjarkk/parse-emoji-2
parse github emoji config option
2021-07-16 21:01:14 +02:00
mjarkk
fc066d2f2e parse github emoji config option 2021-07-16 14:06:01 +02:00
Davyd McColl
53ea7df655 🚚 move only the platform-specific part of log tailing into platform-specific files 2021-07-01 17:13:14 +10:00
Davyd McColl
533817bda3 🐛 should be TailLogs 2021-07-01 17:13:14 +10:00
Davyd McColl
35f1ccdb1b ♻️ temporarily bypass ignore whitespace for diff view instead of turning the toggle off completely 2021-07-01 17:13:14 +10:00
Davyd McColl
3dc3174d85 🔥 remove erroneous user_config fields 2021-07-01 17:13:14 +10:00
Davyd McColl
ae2496cf80 🎨 prefer the long switch over the short one for easier reading 2021-07-01 17:13:14 +10:00
Davyd McColl
2ac33bb83d 🎨 split out platform-dependent logging for compile-time selection 2021-07-01 17:13:14 +10:00
Davyd McColl
2b4048ebff 🐛 shouldn't hammer the file continually, have a nap instead 2021-07-01 17:13:14 +10:00
Davyd McColl
31bcd632c7 🎨 observe the error, if there is one 2021-07-01 17:13:14 +10:00
Davyd McColl
aa9ef12d43 make log-watching work on windows 2021-07-01 17:13:14 +10:00
Davyd McColl
b80fafef02 🎨 properly ignore the result 2021-07-01 17:13:14 +10:00
Davyd McColl
130480555f always show whitespace in diffs when entering line-by-line staging 2021-07-01 17:13:14 +10:00
Davyd McColl
92cc6e883d 🚚 move whitespace toggle out of quitting.go 2021-07-01 17:13:14 +10:00
Davyd McColl
107503c903 🎨 alternative syntax 2021-07-01 17:13:14 +10:00
Davyd McColl
7ae106d4df 🎨 run formatter 2021-07-01 17:13:14 +10:00
Davyd McColl
16dcc8f4db implement feedback when toggling whitespace 2021-07-01 17:13:14 +10:00
Davyd McColl
eb10ddfccc add a test around ignoring whitespace 2021-07-01 17:13:13 +10:00
Davyd McColl
22a6771e51 🎨 run go fmt against the file directly ftw 2021-07-01 17:13:13 +10:00
Davyd McColl
3f96537380 update test to pass in default ignore-whitespace flag (false) 2021-07-01 17:13:13 +10:00
Davyd McColl
a9f04d3925 facilitate toggling whitespace in the diff view with a hotkey (c-w by default) 2021-07-01 17:13:13 +10:00
Mark Kopenga
83834a2c2e Merge pull request #1373 from danielebra/master
Fix typo in random tip
2021-06-28 10:00:25 +02:00
Daniel Ebrahimian
0c3132c6f0 Fix typo in random tip 2021-06-28 11:03:54 +10:00
Cristian Betivu
b28569a593 Fix a format issue 2021-06-16 15:00:17 +10:00
Cristian Betivu
1aa45b0142 Update tests 2021-06-16 15:00:17 +10:00
Cristian Betivu
39c8577074 Use static context 2021-06-16 15:00:17 +10:00
Jesse Duffield
23285eab40 more resilient test 2021-06-16 15:00:17 +10:00
Cristian Betivu
0c2d90a444 Add comment 2021-06-16 15:00:17 +10:00
Cristian Betivu
d65c018875 Add integration test 2021-06-16 15:00:17 +10:00
Cristian Betivu
0c135515a5 Use parent view for tab navigation 2021-06-16 15:00:17 +10:00
Jesse Duffield
2b9df0ea06 fix up cheatsheet 2021-06-15 08:37:56 +10:00
Jesse Duffield
b7b30191f1 update cheatsheet 2021-06-15 08:37:10 +10:00
Stefan Teunissen
7d1b76a349 Update dutch.go 2021-06-15 08:37:03 +10:00
Emiliano Ruiz Carletti
40f10c3388 Update config.md 2021-06-15 08:31:07 +10:00
Emiliano Ruiz Carletti
01e4467d76 Add test cases for pull mode 2021-06-15 08:31:07 +10:00
Emiliano Ruiz Carletti
b4e6850f98 Fix wrong ff-only configuration 2021-06-15 08:31:07 +10:00
Emiliano Ruiz Carletti
c57a0077d0 Read pull mode from gitconfig lazily 2021-06-15 08:31:07 +10:00
Emiliano Ruiz Carletti
46e500dc28 Revert "Read pull mode from git configuration"
This reverts commit e69e240a31.
2021-06-15 08:31:07 +10:00
Emiliano Ruiz Carletti
d7865b3882 Read pull mode from git configuration 2021-06-15 08:31:07 +10:00
Jesse Duffield
0aad68acf0 Merge branch 'btwise-master' 2021-06-15 08:25:24 +10:00
Jesse Duffield
4969e9ce0a gofmt 2021-06-15 08:25:07 +10:00
Jesse Duffield
17770b9f9b go mod vendor 2021-06-15 08:13:45 +10:00
Jesse Duffield
3dd88d6138 bump dependencies 2021-06-15 08:12:38 +10:00
Jesse Duffield
ce7cbe58a0 naming change 2021-06-14 18:17:08 +10:00
Andrei Yangabishev
7588d5290b ShowTotal flag 2021-06-10 12:43:05 +03:00
Jesse Duffield
9fdf92b226 more refactoring
WIP

WIP
2021-06-06 09:12:49 +10:00
Jesse Duffield
93bf691fd6 refactoring 2021-06-06 09:12:49 +10:00
Jesse Duffield
82022615dd bump tcell 2021-06-06 09:12:42 +10:00
Jesse Duffield
fb395bca6e support reverting merge commits 2021-06-05 22:15:51 +10:00
Jesse Duffield
f91adf026b fix lbl scrolling 2021-06-05 13:54:05 +10:00
Jesse Duffield
6d91661d5e prevent closure issue 2021-06-05 13:54:05 +10:00
Jesse Duffield
90983aae65 not importing regexp 2021-06-05 13:53:25 +10:00
Jesse Duffield
f71b23b890 more explicit 2021-06-05 13:53:25 +10:00
Cristian Betivu
05a23f0e1e Discard value after END marker 2021-06-05 13:53:25 +10:00
Cristian Betivu
fd38ad8096 More generic merge conflict detection 2021-06-05 13:53:25 +10:00
Jesse Duffield
d502c43ae8 fix tests 2021-06-05 10:58:36 +10:00
Jesse Duffield
0df02dacc2 minor changes 2021-06-05 10:58:09 +10:00
caojoshua
3258c24fb3 Better english for Configuring File Editing. 2021-06-05 10:58:09 +10:00
caojoshua
e7c657fba0 Docs for EditCommand. 2021-06-05 10:58:09 +10:00
caojoshua
60468d2e17 Edit command as user OS config option 2021-06-05 10:58:09 +10:00
Robert Verst
cb78cf7de4 Simplify sorting of git tags by using git's functions 2021-06-05 10:56:46 +10:00
Robert Verst
94b52af661 Remove config, make default sort order descending 2021-06-05 10:56:46 +10:00
Robert Verst
472288c81b Add user config to change the sort order of tags 2021-06-05 10:56:46 +10:00
Jesse Duffield
258eedb38c refactor 2021-06-02 20:33:52 +10:00
Jérémy Pagé
bc044c64b2 Remove origin prefix when creating local branch based from origin 2021-05-30 15:29:56 +10:00
Harrison Jones
e478c254d4 Handle alternate merge conflict format; add tests 2021-05-30 13:50:42 +10:00
Liberatys
44f7fc6f7c Add global binding to open recent repos 2021-05-30 13:25:44 +10:00
btwise
a13e919d3d add chinese for i18n 2021-05-19 17:55:26 +08:00
Dawid Dziurla
f92fcfbb47 cd: remove ppa job
Deprecated
2021-05-07 00:08:22 +02:00
Dawid Dziurla
6ccf58c224 README: deprecate Ubuntu PPA 2021-05-07 00:06:38 +02:00
Petróczi Zoltán
9190e9beac Fix englishIntroPopupMessage typo in english.go 2021-04-20 21:08:29 +10:00
Jesse Duffield
a99e6ba071 update release notes 2021-04-20 18:34:47 +10:00
Jesse Duffield
604ee02cd9 ignore east asian width setting to avoid broken frame rendering 2021-04-19 23:06:05 +10:00
Jesse Duffield
926a48a65b smarter sizing of command log panel 2021-04-19 18:09:01 +10:00
Jesse Duffield
98375dc902 refactor merge panel 2021-04-18 18:58:09 +10:00
Jesse Duffield
e73de332a1 refactor line by line panel 2021-04-18 16:55:09 +10:00
Jesse Duffield
b28b2d05bd force cursor to be at end of line when opening confirmation panel 2021-04-17 21:15:54 +10:00
Jesse Duffield
9e5f031553 bubble up tracked files in flat file view 2021-04-17 10:04:49 +10:00
Peijun Ma
dab5ba363c Fix typo: scrool -> scroll 2021-04-14 19:11:30 +10:00
Jesse Duffield
e42387d0da update keybindings 2021-04-12 23:40:20 +10:00
Jesse Duffield
730a03a3b2 fix race condition 2021-04-12 23:40:20 +10:00
Jesse Duffield
7d195b97c2 better squash description 2021-04-12 21:57:13 +10:00
Jesse Duffield
4fb2dba587 allow hiding random tip 2021-04-12 21:48:08 +10:00
Jesse Duffield
76697280c9 fix rendering issues caused by resizing 2021-04-12 21:48:08 +10:00
Jesse Duffield
0df6ac6140 bump gocui to fix resizing issue 2021-04-12 21:48:08 +10:00
Jesse Duffield
5453b71fd1 linting 2021-04-12 21:48:08 +10:00
Jesse Duffield
1f3070c882 fix up doc 2021-04-12 21:48:08 +10:00
Jesse Duffield
3b7e7a7f56 add random tip to command log 2021-04-12 21:48:08 +10:00
Jesse Duffield
06a8eb115c make command log size configurable 2021-04-11 23:36:34 +10:00
Jesse Duffield
e4f0a470e9 print header for command log 2021-04-11 23:36:34 +10:00
Jesse Duffield
adee0b8ccb add spans to i18n 2021-04-11 23:36:34 +10:00
Jesse Duffield
0bebfe454e pull out function 2021-04-11 23:36:34 +10:00
Jesse Duffield
84b0c3df4f ask question button 2021-04-11 22:07:29 +10:00
Jesse Duffield
764bd556f3 prettify config.md 2021-04-11 21:42:41 +10:00
Jesse Duffield
069c7c9d35 fix test 2021-04-11 17:07:49 +10:00
Jesse Duffield
393ce05860 allow focusing on command log view 2021-04-11 17:07:49 +10:00
Jesse Duffield
cf78b86cb5 more support for command log and more code reuse for contexts 2021-04-11 17:07:49 +10:00
Jesse Duffield
4f03d7733a allow showing, hiding, and scrolling the extras panel 2021-04-11 17:07:49 +10:00
Jesse Duffield
e3a14d546a support static boxes that go outside the available size 2021-04-11 17:07:49 +10:00
Jesse Duffield
f2007f4d95 support scrolling extras view 2021-04-11 17:07:49 +10:00
Jesse Duffield
8969464b00 log TODO content when interactive rebasing 2021-04-11 17:07:49 +10:00
Jesse Duffield
6137d66914 no need to log this 2021-04-11 17:07:49 +10:00
Jesse Duffield
6fbe660f96 full coverage for logging commands 2021-04-11 17:07:49 +10:00
Jesse Duffield
74320f0075 more logging of commands 2021-04-11 17:07:49 +10:00
Jesse Duffield
bfad972f0c fix bug where mixed reset is actually a soft reset 2021-04-11 17:07:49 +10:00
Jesse Duffield
bb918b579a start adding support for logging of commands 2021-04-11 17:07:49 +10:00
Jesse Duffield
e145090046 add cmdLog panel 2021-04-11 17:07:49 +10:00
Jesse Duffield
70b5c822bb update config.md to explain situation with config paths 2021-04-11 10:25:37 +10:00
tafryn
f2df77a4f1 Fix path for Linux config file
The listed config file directory for Linux is incorrect.
2021-04-11 10:22:26 +10:00
Jesse Duffield
8d416634ba update release notes 2021-04-11 10:21:53 +10:00
Jesse Duffield
9f4433d8b5 allow opening merge tool 2021-04-11 10:21:53 +10:00
Jesse Duffield
2d8f7d2a7b better way of scrolling to a merge conflict 2021-04-11 10:21:53 +10:00
Jesse Duffield
a9fbc9eda1 fix merge conflict panel not rendering 2021-04-11 10:21:53 +10:00
Jesse Duffield
e092da5f78 pause background threads when running subprocess 2021-04-10 12:16:45 +10:00
Jesse Duffield
e42e7e5cbd fix commit amend 2021-04-10 11:54:38 +10:00
Jesse Duffield
93fac1f312 reduce flicker without worrying about carriage returns 2021-04-09 22:50:55 +10:00
Jesse Duffield
d5504fa5d0 potentially fix credentials issue 2021-04-09 00:39:04 +10:00
Jesse Duffield
273aba38d4 stricter CI 2021-04-09 00:15:48 +10:00
Jesse Duffield
cab0aa462c fix crash at start 2021-04-09 00:10:35 +10:00
Jesse Duffield
b03e2270a0 revert no-flicker due to carriage return weirdness 2021-04-08 23:17:27 +10:00
Jesse Duffield
21049be233 support file tree mode on windows 2021-04-08 21:33:17 +10:00
Jesse Duffield
f89c47b83d add test for building tree 2021-04-08 21:33:17 +10:00
Jesse Duffield
44f1f22068 close commit message panel after returning from subprocess 2021-04-08 20:17:16 +10:00
Jesse Duffield
a229547048 fix CI 2021-04-07 22:59:53 +10:00
Jesse Duffield
4f700c23ba fix crash on first open 2021-04-07 22:59:53 +10:00
Emiliano Ruiz Carletti
b69fc19b35 fix broken link to old AUR package 2021-04-07 08:51:07 +10:00
Jesse Duffield
cd1d1996df add 0.27 release notes 2021-04-06 19:34:32 +10:00
Jesse Duffield
963fcc1444 don't kill the index.lock file until I decide whether it's actually a good idea 2021-04-06 19:34:32 +10:00
Jesse Duffield
c6825e3d0d skip some tests that are failing on CI for some reason 2021-04-06 19:34:32 +10:00
Jesse Duffield
20bdba15f6 amend reword test 2021-04-06 19:34:32 +10:00
Jesse Duffield
e636857057 prevent adding staged files when renaming top commit 2021-04-06 19:34:32 +10:00
Jesse Duffield
1ae8523098 restore contents on resume from subprocess
The proper fix will come out of this PR but it's not yet merged:
https://github.com/gdamore/tcell/pull/439/files

In the meantime I'm just going to directly edit this from my vendor
directory. If it ends up stretching a while I'll fork tcell properly
and use the fork.
2021-04-06 19:34:32 +10:00
Jesse Duffield
8eb802d3a0 fix flicker issue in main view 2021-04-06 19:34:32 +10:00
Jesse Duffield
6fc031c523 hide patch panel if we're in the commits panel and we refresh and it's now exited 2021-04-06 19:34:32 +10:00
Jesse Duffield
8c93289a72 reduce chance of deadlock by using a RW mutex on the context stack 2021-04-06 19:34:32 +10:00
Jesse Duffield
b1df0fafa2 remove junk test 2021-04-06 19:34:32 +10:00
Jesse Duffield
15046a0454 more tests for branches 2021-04-06 19:34:32 +10:00
Jesse Duffield
fb9b6314a0 ensure we're passing the right testing struct pointer around 2021-04-06 19:34:32 +10:00
Jesse Duffield
0719a3e36e stop checking out branches when doing a rename. Instead just move the cursor to the new position 2021-04-06 19:34:32 +10:00
Jesse Duffield
a3b0efb82e branch rename test 2021-04-06 19:34:32 +10:00
Jesse Duffield
bde324820d more tests 2021-04-06 19:34:32 +10:00
Jesse Duffield
bbdbbd0b1b more tests 2021-04-06 19:34:32 +10:00
Jesse Duffield
d4f3b292e6 even slower retries for CI 2021-04-06 19:34:32 +10:00
Jesse Duffield
39eb937830 update test descriptions 2021-04-06 19:34:32 +10:00
Jesse Duffield
fbab5bd444 do not refresh patch panel unless commit files panel is the current side panel 2021-04-06 19:34:32 +10:00
Jesse Duffield
12ca922a41 add tests for diffing 2021-04-06 19:34:32 +10:00
Jesse Duffield
f4e552f982 prevent deadlocks.
Hard to choose between the lock with a defer unlock in an anonymous function
vs just having an explicit unlock at the end with additional unlocks before
any early returns. The former is less error prone, but the former is much more
readable, especially if the anonymous function would have needed to return
an error value.
2021-04-06 19:34:32 +10:00
Jesse Duffield
94d26d00ba move suggestions view behind confirmation view 2021-04-06 19:34:32 +10:00
Jesse Duffield
d80d1f8493 more tests 2021-04-06 19:34:32 +10:00
Jesse Duffield
ace4350319 update snapshots to include tags comparison 2021-04-06 19:34:32 +10:00
Jesse Duffield
4441cf1045 fix bug with tags panel 2021-04-06 19:34:32 +10:00
Jesse Duffield
cf99b47ec0 another filter path test 2021-04-06 19:34:32 +10:00
Jesse Duffield
546eb50bac add another filter path test 2021-04-06 19:34:32 +10:00
Jesse Duffield
5e094c8a7c marginally better logic for searching 2021-04-06 19:34:32 +10:00
Jesse Duffield
c683f2c96c allow opening diff menu panel when other popup is open 2021-04-06 19:34:32 +10:00
Jesse Duffield
e5a372fa2d allow opening filter menu panel when other popup is open 2021-04-06 19:34:32 +10:00
Jesse Duffield
02f45b679f do not double-append contexts to the stack 2021-04-06 19:34:32 +10:00
Jesse Duffield
b1cda65dcf show error when user attempts to commit when no files are present 2021-04-06 19:34:32 +10:00
Jesse Duffield
74ce65d9ff update keybindings 2021-04-06 19:34:32 +10:00
Jesse Duffield
ccebe5e069 change language 2021-04-06 19:34:32 +10:00
Jesse Duffield
b6ec667de0 add comment 2021-04-06 19:34:32 +10:00
Jesse Duffield
390b7ddc5e change order of filtering and patch building so that esc key exits patch building mode first 2021-04-06 19:34:32 +10:00
Jesse Duffield
38739b16bc add filter path test 2021-04-06 19:34:32 +10:00
Jesse Duffield
27525f1d42 support passing extra command args in integration tests 2021-04-06 19:34:32 +10:00
Jesse Duffield
43a9dc48e0 default to not quitting when hitting esc at the top level.
I've been using this config option for years now so I don't think much of it,
but newcomers are going to find it annoying that hitting escape gets you out
of filtering/cherry-picking/patch-building mode, but also quits the app. So
if you want to exit all the modes you're in, you need to take care not to
press the key one too many times or the app will close.

We'll see if anybody gets mad about this change, but I think it's reasonable.
The only downside is that you won't be able to always quit by spamming the escape
key. If you're in a prompt panel, you'll need to hit escape to exit that, and
then 'q' at the top level. Or CTRL+C of course.
2021-04-06 19:34:32 +10:00
Jesse Duffield
440eb387d7 much cleaner integration test code 2021-04-06 19:34:32 +10:00
Jesse Duffield
28ffaf9348 tiny refactor 2021-04-06 19:34:32 +10:00
Jesse Duffield
d7da6dde0e allow decimal replay speeds for integration tests 2021-04-06 19:34:32 +10:00
Jesse Duffield
e000620cdf fix windows compilation issue 2021-04-06 19:34:32 +10:00
Jesse Duffield
f09309485a remove time limit 2021-04-06 19:34:32 +10:00
Jesse Duffield
e04e2ebab5 try better logging for CI 2021-04-06 19:34:32 +10:00
Jesse Duffield
91a107eb6f retry flakey tests 2021-04-06 19:34:32 +10:00
Jesse Duffield
5ce9e0193a add retry logic for running git commands to avoid index.lock problems 2021-04-06 19:34:32 +10:00
Jesse Duffield
4c71c26593 speed up test 2021-04-06 19:34:32 +10:00
Jesse Duffield
abdd2455bb allow playing and updating snapshots 2021-04-06 19:34:32 +10:00
Jesse Duffield
c33f8d2790 prevent git from prompting user if program is run directly 2021-04-06 19:34:32 +10:00
Jesse Duffield
8e9d08bc10 minor cleanup of integration code 2021-04-06 19:34:32 +10:00
Jesse Duffield
9593129e6a remove caching of styles in gocui 2021-04-06 19:34:32 +10:00
Jesse Duffield
267da3b4db fix issue when switching repos while files refresh 2021-04-06 19:34:32 +10:00
Jesse Duffield
c9ded489c9 bump gocui 2021-04-06 19:34:32 +10:00
Jesse Duffield
4c73d070ac ignore clicks on invisible views 2021-04-06 19:34:32 +10:00
Jesse Duffield
121b9d0715 update comment 2021-04-06 19:34:32 +10:00
Jesse Duffield
fbb33b7abc remove code that I'm pretty sure isn't needed 2021-04-06 19:34:32 +10:00
Jesse Duffield
7178bab6b4 only re-use repo state when jumping in and out of submodules 2021-04-06 19:34:32 +10:00
Jesse Duffield
2d7452bfaa Revert "see how CI goes running these tests in parallel"
This reverts commit d271cbc138.
2021-04-06 19:34:32 +10:00
Jesse Duffield
b0f3bfef27 see how CI goes running these tests in parallel 2021-04-06 19:34:32 +10:00
Jesse Duffield
7bc6dc5cf3 show branches context when starting in filtering mode 2021-04-06 19:34:32 +10:00
Jesse Duffield
ee7b634dce how about using pty 2021-04-06 19:34:32 +10:00
Jesse Duffield
b0bd752180 maybe this will fix CI 2021-04-06 19:34:32 +10:00
Jesse Duffield
4d14af5d4b more lint fixes 2021-04-06 19:34:32 +10:00
Jesse Duffield
7953e58c74 try this 2021-04-06 19:34:32 +10:00
Jesse Duffield
549d73a0b1 fix lint issues 2021-04-06 19:34:32 +10:00
Jesse Duffield
8301bba8ad make it more likely for CI to work 2021-04-06 19:34:32 +10:00
Jesse Duffield
78f17aa541 update squash integration test 2021-04-06 19:34:32 +10:00
Jesse Duffield
7578a7466f update searching tests 2021-04-06 19:34:32 +10:00
Jesse Duffield
8681a6b4e2 update patch building with filetree test 2021-04-06 19:34:32 +10:00
Jesse Duffield
efed313721 update patch building 2 test 2021-04-06 19:34:32 +10:00
Jesse Duffield
795cf39ddf update patch building test 2021-04-06 19:34:32 +10:00
Jesse Duffield
f08f248cb7 update merge conflict test 2021-04-06 19:34:32 +10:00
Jesse Duffield
3c20425649 update merge conflict undo test 2021-04-06 19:34:32 +10:00
Jesse Duffield
dfc689411b no need for debug flag because it writes to a different log anyway 2021-04-06 19:34:32 +10:00
Jesse Duffield
2295407a45 update discard file changes test 2021-04-06 19:34:32 +10:00
Jesse Duffield
828a2acd26 update branch autocomplete integration test 2021-04-06 19:34:32 +10:00
Jesse Duffield
843b8ceab0 support tcell simulation screen 2021-04-06 19:34:32 +10:00
Jesse Duffield
011451464f working on integration tests working again 2021-04-06 19:34:32 +10:00
Jesse Duffield
32d170621c remove mutex lock that caused deadlock 2021-04-06 19:34:32 +10:00
Jesse Duffield
464d022a86 minor refactor 2021-04-06 19:34:32 +10:00
Jesse Duffield
6a0066253f move recording code into gocui 2021-04-06 19:34:32 +10:00
Jesse Duffield
d627b3bfc8 more refactoring 2021-04-06 19:34:32 +10:00
Jesse Duffield
952c62df37 fix bug where searching through view got stuck if you went over the upper bound 2021-04-06 19:34:32 +10:00
Jesse Duffield
b6cc1c9492 small refactor 2021-04-06 19:34:32 +10:00
Jesse Duffield
39ae122304 more refactoring 2021-04-06 19:34:32 +10:00
Jesse Duffield
c34c6926d5 fix some things up 2021-04-06 19:34:32 +10:00
Jesse Duffield
4fe512ff3a test
type safe view access
2021-04-06 19:34:32 +10:00
Jesse Duffield
4197921465 WIP 2021-04-06 19:34:32 +10:00
Jesse Duffield
4b69ab08c1 WIP 2021-04-06 19:34:32 +10:00
Jesse Duffield
f3a0058eb9 WIP 2021-04-06 19:34:32 +10:00
Jesse Duffield
633b6f596d WIP 2021-04-06 19:34:32 +10:00
Jesse Duffield
e6274c0757 remove sentinel errors 2021-04-06 19:34:32 +10:00
Jesse Duffield
0898a7bb57 refactor 2021-04-06 19:34:32 +10:00
Jesse Duffield
fafd5234bd refactor to get view tab context map into gui state 2021-04-06 19:34:32 +10:00
Jesse Duffield
8cb10f76e4 refresh main panel when switching between tree and flat mode 2021-04-06 19:34:32 +10:00
Jesse Duffield
f1d7f59e49 switching repos without restarting the gui 2021-04-06 19:34:32 +10:00
Jesse Duffield
bc9a99387f refactor of contexts and filtering 2021-04-06 19:34:32 +10:00
Jesse Duffield
5289d49f75 more efficient gocui 2021-04-06 19:34:32 +10:00
Jesse Duffield
69e9f6d29d use suspense rather than close the gui when switching to a subprocess 2021-04-06 19:34:32 +10:00
Jesse Duffield
0b42437052 fix comment 2021-04-06 19:34:32 +10:00
Jesse Duffield
ae0f750770 fix bug where you couldn't change tabs 2021-04-06 19:34:32 +10:00
Jesse Duffield
9fe7e0d63d fix bug where we had two sets of contexts with their own state 2021-04-06 19:34:32 +10:00
Jesse Duffield
8935794e28 reset origin when clicking new item 2021-04-06 19:34:32 +10:00
Jesse Duffield
d44ff447bd fix panic 2021-04-06 19:34:32 +10:00
Jesse Duffield
798d3e2d54 get rid of these positively ghastly method signatures 2021-04-06 19:34:32 +10:00
Jesse Duffield
e8f99c3326 better scroll support 2021-04-06 19:34:32 +10:00
Jesse Duffield
1a5f380c00 support alt-enter for inserting newline when typing commit message within the app 2021-04-06 19:34:32 +10:00
Jesse Duffield
b4827a98ca fix commit message panel 2021-04-06 19:34:32 +10:00
Jesse Duffield
3ea5e4d4b2 allow scrolling when staging lines or building patch 2021-04-06 19:34:32 +10:00
Jesse Duffield
5f77ac8d6f bump gocui 2021-04-06 19:34:32 +10:00
Jesse Duffield
5d0cf3d919 prioritise keybindings on editors 2021-04-06 19:34:32 +10:00
Jesse Duffield
4b1da0cf3c bump gocui again 2021-04-06 19:34:32 +10:00
Jesse Duffield
862ced3bd0 bump gocui 2021-04-06 19:34:32 +10:00
Jesse Duffield
79b256a0fa remove 24 bit color delta arg from docs 2021-04-06 19:34:32 +10:00
Jesse Duffield
0d6ff7d1b7 support backtab key 2021-04-06 19:34:32 +10:00
Jesse Duffield
ecc5fe24a9 get tcell to cleanup the terminal if we panic 2021-04-06 19:34:32 +10:00
Jesse Duffield
1fb2317bac use true output mode 2021-04-06 19:34:32 +10:00
Jesse Duffield
6246eb9717 go mod tidy 2021-04-06 19:34:32 +10:00
Jesse Duffield
8f763c42b6 bum pgocui 2021-04-06 19:34:32 +10:00
Jesse Duffield
6472bda29e bump gocui 2021-04-06 19:34:32 +10:00
Jesse Duffield
c0cad91cb6 no more termbox 2021-04-06 19:34:32 +10:00
Jesse Duffield
1149dea4b2 stop referencing termbox 2021-04-06 19:34:32 +10:00
Jesse Duffield
6a6024e38f use tcell via porting over code from awesome-gocui 2021-04-06 19:34:32 +10:00
Jesse Duffield
8901d11674 fix merge conflict cat issue on windows 2021-04-02 13:15:07 +11:00
Jesse Duffield
8b7f7cbc30 linting 2021-04-02 11:09:12 +11:00
Jesse Duffield
b6d0bdfa2d another integration test 2021-04-02 11:09:12 +11:00
Jesse Duffield
44896bcd51 safer code 2021-04-02 11:09:12 +11:00
Jesse Duffield
bdf2b2d5c4 add merge conflict undo integration test 2021-04-02 11:09:12 +11:00
Jesse Duffield
035726f650 add integration UI to make the integration process easier 2021-04-02 11:09:12 +11:00
Jesse Duffield
1abb3cd566 more thorough merge conflict integration test 2021-04-02 11:09:12 +11:00
Jesse Duffield
f7772f00c4 do not jump cursor around when fixing merge conflicts 2021-04-02 11:09:12 +11:00
Jesse Duffield
216b5341ae better handling of scrolling for conflicted files 2021-04-02 11:09:12 +11:00
Jesse Duffield
eeeef9ca86 refactor 2021-04-02 11:09:12 +11:00
Jesse Duffield
cc9293b386 add mutex to prevent crashes with merge conflicts 2021-04-02 11:09:12 +11:00
Jesse Duffield
efe43077bc fix name 2021-04-02 11:00:15 +11:00
Jesse Duffield
949c7726d1 fix bug caused by interface 2021-04-02 11:00:15 +11:00
Jesse Duffield
0b7bda291c remove dead code 2021-04-02 11:00:15 +11:00
Jesse Duffield
872cf0d726 hide commit files view upon losing focus because you probably don't want it lingering anyway 2021-04-02 11:00:15 +11:00
Jesse Duffield
af09223dd5 refactor 2021-04-02 11:00:15 +11:00
Jesse Duffield
7d62f103e4 big refactor to give our enums actual types 2021-04-02 11:00:15 +11:00
Jesse Duffield
9e85d37fb9 refactor to no longer call these things file changes 2021-04-02 11:00:15 +11:00
Jesse Duffield
8dee06f83a allow toggling tree view for commit files panel 2021-04-02 11:00:15 +11:00
Jesse Duffield
82fe4aa6c0 disallow editing commit file directory 2021-04-02 11:00:15 +11:00
Jesse Duffield
50c169e0a3 better colouring for directories for when adding a patch 2021-04-02 11:00:15 +11:00
Jesse Duffield
7364525bf5 do not show commit files of another parent as added to the patch 2021-04-02 11:00:15 +11:00
Jesse Duffield
54910fdb76 refactor 2021-04-02 11:00:15 +11:00
Jesse Duffield
332a3c4cbf file tree for commit files 2021-04-02 11:00:15 +11:00
Jesse Duffield
ac41c41809 refactor to support commit file tree 2021-04-02 11:00:15 +11:00
Jesse Duffield
96a9df04ed Update README.md 2021-04-01 20:56:04 +11:00
Jesse Duffield
b7cc4158d5 Update Config.md 2021-04-01 20:52:56 +11:00
Jesse Duffield
2bbe6269cd Update Config.md 2021-04-01 20:44:27 +11:00
Jesse Duffield
eb54189683 support GIT_EDITOR 2021-04-01 20:40:02 +11:00
Jesse Duffield
e8e59306fc shell out custom commands 2021-04-01 20:25:30 +11:00
Jesse Duffield
8af3fe3b4a faster startup 2021-04-01 09:13:29 +11:00
Jesse Duffield
3103247e8f refactor 2021-03-30 21:57:00 +11:00
Jesse Duffield
1629a7d280 same for renames 2021-03-30 21:57:00 +11:00
Jesse Duffield
b5a5169372 expand to path when switching to tree mode 2021-03-30 21:57:00 +11:00
Jesse Duffield
4b4bfae4f4 fix background colour on selected line 2021-03-30 21:57:00 +11:00
Jesse Duffield
d5639e6e95 refactor 2021-03-30 21:57:00 +11:00
Jesse Duffield
9e67f74ca3 prevent staging directory containing files with inline merge conflicts 2021-03-30 21:57:00 +11:00
Jesse Duffield
e3ddfbf2b8 rename function 2021-03-30 21:57:00 +11:00
Jesse Duffield
1ea78c7ae7 make fields private 2021-03-30 21:57:00 +11:00
Jesse Duffield
e7af3bf55d refactor 2021-03-30 21:57:00 +11:00
Jesse Duffield
e52cec9cdf small refactor 2021-03-30 21:57:00 +11:00
Jesse Duffield
5bb48b51a0 rename 2021-03-30 21:57:00 +11:00
Jesse Duffield
d2e1b35eee small fixes 2021-03-30 21:57:00 +11:00
Jesse Duffield
ef204b0adf remove collapsed field 2021-03-30 21:57:00 +11:00
Jesse Duffield
f742434043 fix spec 2021-03-30 21:57:00 +11:00
Jesse Duffield
d3b34ce323 fix spec 2021-03-30 21:57:00 +11:00
Jesse Duffield
89c2f4f2ff fix spec 2021-03-30 21:57:00 +11:00
Jesse Duffield
5a0f23e6d6 update cheatsheets 2021-03-30 21:57:00 +11:00
Jesse Duffield
5e05e8b62b fix comment 2021-03-30 21:57:00 +11:00
Jesse Duffield
1f7273af23 better way to check if a node is a leaf 2021-03-30 21:57:00 +11:00
Jesse Duffield
2b8302bced refactor 2021-03-30 21:57:00 +11:00
Jesse Duffield
1b94462410 rename some things 2021-03-30 21:57:00 +11:00
Jesse Duffield
120bb443fe small thing 2021-03-30 21:57:00 +11:00
Jesse Duffield
6fc3c03c4b allow configuring to show file tree on startup 2021-03-30 21:57:00 +11:00
Jesse Duffield
46b79c7c61 drop Name field from status line node 2021-03-30 21:57:00 +11:00
Jesse Duffield
4782d8aa1f bring merge conflicts to top 2021-03-30 21:57:00 +11:00
Jesse Duffield
fe4e305410 safer code 2021-03-30 21:57:00 +11:00
Jesse Duffield
040c1fc302 more functional approach 2021-03-30 21:57:00 +11:00
Jesse Duffield
5edea5a8dc better handling of cursor relocation 2021-03-30 21:57:00 +11:00
Jesse Duffield
d2b65537f6 handle nothing selected 2021-03-30 21:57:00 +11:00
Jesse Duffield
1183f68e19 better handling of refreshed files 2021-03-30 21:57:00 +11:00
Jesse Duffield
da6fe01eca allow toggling on/off file tree mode 2021-03-30 21:57:00 +11:00
Jesse Duffield
c27cea6f30 more file tree improvements 2021-03-30 21:57:00 +11:00
Jesse Duffield
cd0532b4d6 allow ignoring directories 2021-03-30 21:57:00 +11:00
Jesse Duffield
c9de6c003b support some more things 2021-03-30 21:57:00 +11:00
Jesse Duffield
418621a9ff support discarding changes in dir 2021-03-30 21:57:00 +11:00
Jesse Duffield
f871724ae6 update wording 2021-03-30 21:57:00 +11:00
Jesse Duffield
def68ddc8f fix bug for combining directories with single child 2021-03-30 21:57:00 +11:00
Jesse Duffield
a31db3df9c support toggling collapsed 2021-03-30 21:57:00 +11:00
Jesse Duffield
64217a8a5b fix spacing 2021-03-30 21:57:00 +11:00
Jesse Duffield
79079b54ea combining nodes when only one child exists 2021-03-30 21:57:00 +11:00
Jesse Duffield
77a7619690 showing changes for directories 2021-03-30 21:57:00 +11:00
Jesse Duffield
9f2d7adb8e more improvements 2021-03-30 21:57:00 +11:00
Jesse Duffield
07dd9c6bc8 better tree formatting 2021-03-30 21:57:00 +11:00
Jesse Duffield
45939171ea WIP
start moving to new interface

WIP

WIP

WIP

WIP

WIP
2021-03-30 21:57:00 +11:00
Jesse Duffield
049849264e defend against race condition in editors 2021-03-30 09:26:06 +11:00
Francisco Miamoto
7e0d48c2a1 fix panic for unprintable key presses
Checking is the associated rune of a key is printable solves our problem
of having panics whenever keys like `Home` or `Ctrl-W` are pressed.

Fixes #1175
2021-03-21 11:24:36 +11:00
Jesse Duffield
ad1468f66f better handling of discarding files 2021-03-20 12:46:27 +11:00
Jesse Duffield
058bcddc53 fix renamed files looking wrong 2021-03-14 13:24:51 +11:00
Jesse Duffield
8288de0c84 update release notes 2021-03-13 11:46:48 +11:00
Ryooooooga
1da2afd450 Fix edit remote name message 2021-03-13 11:04:38 +11:00
Jesse Duffield
03de51747e remove redundant addition 2021-03-13 11:03:34 +11:00
Ryooooooga
3d698cd7c1 Fix tests 2021-03-13 11:02:31 +11:00
Ryooooooga
a48cc245e7 Support multibyte characters in pane 2021-03-13 11:02:31 +11:00
Ryooooooga
9ed3a8ee05 Fix staging/unstaging filenames that starts with - or -- 2021-03-13 11:02:31 +11:00
Ryooooooga
64daf1310d Fix staging/unstaging files containing " in paths 2021-03-13 11:02:31 +11:00
Ryooooooga
e5ba0d9d9c Support multibyte characters in Files pane 2021-03-13 11:02:31 +11:00
Ryooooooga
50e4e9d58d fix command escaping 2021-03-13 10:49:40 +11:00
István Donkó
03b9db5e0a Fix the linux config path (related: #913, #1059) 2021-03-12 12:45:48 +11:00
Jesse Duffield
043cb2ea44 reload config whenever returning to gui 2021-02-24 02:45:05 -08:00
Dawid Dziurla
a62d70fbd5 Merge pull request #1172 from jesseduffield/dawidd6-patch-1
gui: ReplaceAll -> Replace
2021-02-24 00:14:24 +01:00
Dawid Dziurla
053e80a08e gui: ReplaceAll -> Replace 2021-02-24 00:09:05 +01:00
Daniel Bast
b726dcc770 Switch to Go 1.16 to support macOS arm64 2021-02-23 03:04:49 -08:00
Matthias Küch
9df133ed8c Fix pattern in commitPrefix example 2021-02-16 13:57:28 -08:00
1jz
50dd7b00c3 add colors to differentiate action and menu commands 2021-02-16 13:52:04 -08:00
Rui Pires
ccbd2c924b Fixed whitespace format issue 2021-02-09 14:45:33 -08:00
Rui Pires
52d5c3beeb Added initialContent on branch rename 2021-02-09 14:45:33 -08:00
Jesse Duffield
c43416891e update cheatsheet 2021-02-09 20:23:20 +11:00
Jesse Duffield
56a573de86 support wide characters in the editor 2021-02-08 22:55:11 +00:00
Jesse Duffield
e7fff2529c fix lint error 2021-02-08 14:40:30 -08:00
Jesse Duffield
78867647d1 remove go-gitconfig package 2021-02-08 14:40:30 -08:00
Jesse Duffield
09f32d4f84 add secureexec file for getting around windows checking for a binary first in the current dir 2021-02-08 14:40:30 -08:00
Nick Flueckiger
6f0f70bd92 Adding setup and config 2021-02-08 14:25:24 -08:00
caquillo07
6df15ddf6e added support for using spaces on branch names when creating new ones. 2021-02-08 14:23:54 -08:00
unknown
922c0887f1 fix type: executable not found error when there is a merge conflict on windows 2021-01-01 13:17:29 -08:00
Dawid Dziurla
d7c9243880 workflows: setup git before PPA repo updating 2020-12-24 10:32:50 +01:00
Dawid Dziurla
f42a595aba Merge pull request #1130 from jesseduffield/dawidd6-patch-2
gui: ReplaceAll -> Replace
2020-12-24 10:24:49 +01:00
Dawid Dziurla
797722ec12 Merge pull request #1129 from jesseduffield/dawidd6-patch-1
workflows: split CD into separate jobs
2020-12-24 10:24:30 +01:00
Dawid Dziurla
bb4bf23c5c gui: ReplaceAll -> Replace 2020-12-24 10:21:54 +01:00
Dawid Dziurla
f3aacbd253 workflows: split CD into separate jobs 2020-12-24 09:51:50 +01:00
Jeff Hertzler
106fce26b5 Update Custom_Command_Keybindings.md 2020-12-23 18:54:26 -08:00
Jesse Duffield
caf208b0a4 v24 release notes 2020-12-24 13:52:29 +11:00
Jesse Duffield
13b9a8bc9a add integration test for branch checkout autocomplete 2020-11-28 20:48:17 +11:00
Jesse Duffield
14ce230683 refactor 2020-11-28 20:48:17 +11:00
Jesse Duffield
f31fbc10f6 soft code finding of suggestions 2020-11-28 20:48:17 +11:00
Jesse Duffield
be404068ff support labels for suggestions which are distinct from values 2020-11-28 20:48:17 +11:00
Jesse Duffield
5671ec5f58 refactor prompt opts 2020-11-28 20:48:17 +11:00
Jesse Duffield
da3b0bf7c8 Start on supporting auto-suggestions when checking out a branch
switch to other fuzzy package with no dependencies
2020-11-28 20:48:17 +11:00
Yuki Osaki
90ade3225f Add lc prefix 2020-11-28 19:19:47 +11:00
Yuki Osaki
4928d1d490 Visualize the commits for all branches 2020-11-28 19:19:47 +11:00
Kalvin Pearce
9c52eb9d6f Add notARepository description to config docs 2020-11-28 10:51:34 +11:00
Kalvin Pearce
0a58cb2877 Fix formatting on notARepository checks 2020-11-28 10:51:34 +11:00
Kalvin Pearce
7581830e70 Add notARepository to config docs 2020-11-28 10:51:34 +11:00
Kalvin Pearce
d468866746 Add config option for notInRepo behaviour. 2020-11-28 10:51:34 +11:00
Jesse Duffield
999e170f1d standardise how we read from the config 2020-11-28 10:45:30 +11:00
Nick Flueckiger
7513bfb13a Implement suggestions 2020-11-28 10:42:38 +11:00
Nick Flueckiger
1f27002b84 Switch the directory check 2020-11-28 10:42:38 +11:00
Nick Flueckiger
669bfe763a A small change that enables direct lazygit directory config 2020-11-28 10:42:38 +11:00
Davyd McColl
860370a845 👌 update as per PR commentary 2020-11-28 10:27:28 +11:00
Davyd McColl
196761a40a 🐛 should only stage all if configured to do so _and_ there are no items staged 2020-11-28 10:27:28 +11:00
Davyd McColl
26d5444919 implement quick commit when no files staged, if configured to do so 2020-11-28 10:27:28 +11:00
Nathan Bell
e05c41828c added tests and fixed bug found in tests 2020-11-25 08:41:22 +11:00
Nathan Bell
c4cce58464 Allow --follow-tags to be disabled if push.followTags is configured to false 2020-11-25 08:41:22 +11:00
Jesse Duffield
f7e6d4e724 fix updater 2020-11-22 10:00:35 +11:00
Jesse Duffield
d02e992265 Update Custom_Command_Keybindings.md 2020-11-21 17:35:51 +11:00
Jesse Duffield
3e13936e08 notify user upon copying something to clipboard 2020-11-21 17:31:08 +11:00
Jesse Duffield
a3dfcd5a95 toast notifications 2020-11-21 17:31:08 +11:00
Jesse Duffield
ce928dc6c8 Update docs/README.md 2020-11-21 14:14:40 +11:00
Nils Andresen
1dea988cd6 Added a reference to chocolatey in README
and also added a simple overview of documentation under docs/README.md
2020-11-21 14:14:40 +11:00
Farzad Majidfayyaz
74bb6f0012 Change copy PR mapping to <c-y> and use gui.Tr for the message 2020-11-19 09:43:51 +11:00
Farzad Majidfayyaz
79888d3bde Add mapping to copy a pull request URL to the clipboard 2020-11-19 09:43:51 +11:00
Jaime Gomes
4e1d3e45a3 add minimum macos version 10.10 to the README 2020-11-18 08:36:50 +11:00
Jesse Duffield
682db77401 fix lint errors 2020-11-18 08:36:19 +11:00
Dawid Dziurla
6faed08d9d workflows: clean up linting 2020-11-16 10:02:57 +11:00
Sean Stiglitz
62b200a4be Add GolangCI action. 2020-11-16 10:02:57 +11:00
Dawid Dziurla
f7bab5fdc0 workflows: update apt cache before installing pkgs 2020-11-05 11:47:21 +01:00
Jesse Duffield
5ff0ac2816 prevent crash when removing remote with no urls 2020-11-05 21:32:08 +11:00
Dawid Dziurla
7c1889cd70 Merge pull request #1051 from jesseduffield/go-1.10-compat
gui: fix go-1.10 compatibility
2020-10-14 12:53:29 +02:00
Dawid Dziurla
5669cc0002 gui: fix go-1.10 compatibility 2020-10-14 12:43:31 +02:00
Dawid Dziurla
d2ea5dd8b7 workflows: don't sign commit 2020-10-14 00:01:19 +02:00
Dawid Dziurla
e0381b5920 workflows: run CD on Ubuntu 20.04
So we can get newer git-buildpackage
2020-10-13 23:51:40 +02:00
Dawid Dziurla
dac3978983 Merge pull request #1049 from jesseduffield/cd-update-ppa
workflows: update PPA repo as part of CD process
2020-10-13 23:33:19 +02:00
Dawid Dziurla
7074cc28b8 Merge pull request #1050 from jesseduffield/dawidd6-patch-1
utils: ReplaceAll -> Replace
2020-10-13 17:28:33 +02:00
Dawid Dziurla
327b6ad097 utils: ReplaceAll -> Replace
Fix compatibility with older Go compiler versions
2020-10-13 17:25:37 +02:00
Dawid Dziurla
1dc837527f workflows: update PPA repo as part of CD process 2020-10-13 13:43:14 +02:00
Jesse Duffield
b1dd3c4866 support rebinding confirm/newline keys in editor 2020-10-13 08:21:09 +11:00
Jesse Duffield
624fb8da21 preserve width of side panel when main view split unless window is wide enough 2020-10-13 07:31:14 +11:00
nullawhale
1ff405edd8 Copy a commit message to clipboard: Changes to latest version 2020-10-12 21:04:01 +11:00
Jesse Duffield
031e97ef91 more password checks on commands that talk to the remote 2020-10-12 19:07:40 +11:00
Jesse Duffield
3df0a9f132 update in-app release notes 2020-10-12 09:08:47 +11:00
Jesse Duffield
1e79ab78dd return default config when dealing with read only filesystem rather than create new config file 2020-10-12 08:48:28 +11:00
Jesse Duffield
1e48afeb8f quote config file when editing 2020-10-12 08:47:12 +11:00
Jesse Duffield
b8ad1883f5 fix delta 2020-10-12 08:26:31 +11:00
band-a-prend
582fd24d78 Add SSH key passphrase prompt to pull/push from/to remote git repo
This commit resolves issue with absence of ssh key prompting
to pull from or push to remote git repository.

I checked lazygit with this patch for successfully pull from
and push to https://gitweb.gentoo.org/repo/proj/guru.git repository.
While for lazygit-0.23.1 I'm not able to do that.

The check for Passphrase follows the Password because of
more long time before SSH key is prompt in terminal.
Otherwise after timeout "Password" prompt is appears.

Excuse me for google translated i18n dutch lines.

Bug: https://github.com/jesseduffield/lazygit/issues/534

Signed-off-by: band-a-prend <torokhov-s-a@yandex.ru>
2020-10-10 17:58:23 +11:00
Jesse Duffield
a0963f8036 fix up intro text even more 2020-10-10 10:12:52 +11:00
Jesse Duffield
7d002474d7 fix up intro text 2020-10-10 09:58:59 +11:00
Jesse Duffield
ef77d7c608 fix submodule tab colour 2020-10-10 00:23:01 +11:00
Jesse Duffield
63f6d0c036 release notes in status panel 2020-10-10 00:23:01 +11:00
Jesse Duffield
aa5001f661 for some reason the commit files view was on top 2020-10-10 00:23:01 +11:00
Jesse Duffield
b01ea26719 fix go.mod and go.sum 2020-10-10 00:23:01 +11:00
Jesse Duffield
c1a6229c2c install lazygit at beginning of test suite 2020-10-10 00:23:01 +11:00
Jesse Duffield
4c9ec88be5 fix mutex deadlock 2020-10-10 00:23:01 +11:00
Jesse Duffield
9011271a01 fix another panic error 2020-10-10 00:23:01 +11:00
Jesse Duffield
777ec0b36c fix nil view keybinding panic 2020-10-10 00:23:01 +11:00
Jesse Duffield
795e4da8b8 do not put mutexes on state else we might unlock an unlocked mutex 2020-10-10 00:23:01 +11:00
Jesse Duffield
79e59d5460 add some safe goroutines
WIP
2020-10-10 00:23:01 +11:00
Jesse Duffield
ba4c3e5bc4 small changes 2020-10-10 00:23:01 +11:00
Jesse Duffield
88f2a66a51 store everything you need to know about a test in its directory 2020-10-10 00:23:01 +11:00
Jesse Duffield
bb081ca764 more mutex safety with staging panel 2020-10-10 00:23:01 +11:00
CI
a9049b4a82 stop using snapshots 2020-10-10 00:23:01 +11:00
CI
ae352a5d8c configurable speeds 2020-10-10 00:23:01 +11:00
CI
e2ad503bda stop using snapshot just store the actual resultant repo 2020-10-10 00:23:01 +11:00
CI
2657060aa2 support running integration tests in parallel 2020-10-10 00:23:01 +11:00
Jesse Duffield
2724f3888a fix CI 2020-10-10 00:23:01 +11:00
Jesse Duffield
dc953ea680 fall back to slower speed if test fails 2020-10-10 00:23:01 +11:00
Jesse Duffield
08f8472db3 fix loop logic 2020-10-10 00:23:01 +11:00
Jesse Duffield
3f5e52f774 another integration test 2020-10-10 00:23:01 +11:00
Jesse Duffield
2e05ac0c90 paging keybindings for line by line panel
support searching in line by line panel

move mutexes into their own struct

add line by line panel mutex

apply LBL panel mutex

bump gocui to prevent crashing when search item count decreases
2020-10-10 00:23:01 +11:00
Jesse Duffield
40c5cd4b4b another integration test 2020-10-10 00:23:01 +11:00
Jesse Duffield
18f8c3d00a add merge conflicts integration test 2020-10-10 00:23:01 +11:00
Jesse Duffield
074fbf6f25 heed gocui stopping 2020-10-10 00:23:01 +11:00
Jesse Duffield
a482f20ba3 kill process if nothing happens two seconds after final event 2020-10-10 00:23:01 +11:00
Jesse Duffield
c36349f460 add another integration test 2020-10-10 00:23:01 +11:00
Jesse Duffield
485f6d5386 support configurable config 2020-10-10 00:23:01 +11:00
Jesse Duffield
778ca8e6f9 better interface 2020-10-10 00:23:01 +11:00
Jesse Duffield
b64c6a3ac7 this is so cool 2020-10-10 00:23:01 +11:00
Jesse Duffield
f76196937a support integration testing
WIP
2020-10-10 00:23:01 +11:00
Jesse Duffield
ece93e5eef support recording sessions for testing purposes 2020-10-10 00:23:01 +11:00
Jesse Duffield
37bb89dac3 type i18n 2020-10-10 00:23:01 +11:00
Jesse Duffield
7d9aa97f96 have typed default config 2020-10-10 00:23:01 +11:00
Jesse Duffield
ca31e5258f store popup version in state not config so that we never need to write to the user config 2020-10-10 00:23:01 +11:00
Jesse Duffield
4912205adb remove viper
WIP
2020-10-10 00:23:01 +11:00
Jesse Duffield
9440dcf9de Create Integration_Tests.md 2020-10-09 23:14:17 +11:00
Jesse Duffield
0aed47737c bump go-git to fix invalid merge error 2020-10-06 21:58:41 +11:00
Jesse Duffield
6e076472b8 switch to fork of go-git 2020-10-06 21:58:41 +11:00
kobutomo
3e15ae3211 Add error panel. 2020-10-06 21:55:01 +11:00
kobutomo
26cb209af2 Ignore "i" command if the filename is .gitignore 2020-10-06 21:55:01 +11:00
Jesse Duffield
76f7726c47 dont close over loop variables ugh I hate this language feature 2020-10-02 20:05:45 +10:00
Jesse Duffield
9763fa9997 fix windows CI 2020-10-02 08:09:42 +10:00
Jesse Duffield
7be474bd83 update keybindings 2020-10-02 08:09:42 +10:00
Jesse Duffield
30b3478611 fix test 2020-10-02 08:09:42 +10:00
Jesse Duffield
f77ce209e0 use path not name 2020-10-02 08:09:42 +10:00
Jesse Duffield
a61356d018 dont really need this 2020-10-02 08:09:42 +10:00
Jesse Duffield
2dc848506c bulk submodule menu 2020-10-02 08:09:42 +10:00
Jesse Duffield
9125e3c0c6 stop refreshing item when at end of list 2020-10-02 08:09:42 +10:00
Jesse Duffield
86dd9d87dd allow updating submodule 2020-10-02 08:09:42 +10:00
Jesse Duffield
da3e00823f allow submodule init and show submodule diff with a prefix 2020-10-02 08:09:42 +10:00
Jesse Duffield
f3be2b3e68 improved command for deleting a submodule 2020-10-02 08:09:42 +10:00
Jesse Duffield
988176e073 manually update submodule url 2020-10-02 08:09:42 +10:00
Jesse Duffield
5d128adee1 add mutexes for when looping through views 2020-10-02 08:09:42 +10:00
Jesse Duffield
71d4c552af allow updating submodule url 2020-10-02 08:09:42 +10:00
Jesse Duffield
d4ab607d0d allow adding a submodule 2020-10-02 08:09:42 +10:00
Jesse Duffield
ea307c8d94 add more submodule commands 2020-10-02 08:09:42 +10:00
Jesse Duffield
7b4a0f20b2 add submodules context 2020-10-02 08:09:42 +10:00
Jesse Duffield
3b93b5dde4 make it easier to add a tab to a view 2020-10-02 08:09:42 +10:00
Jesse Duffield
7ddb916a18 Update README.md 2020-10-02 06:46:51 +10:00
Mrityunjay Saxena
faba40554a Limitations Section sentence mixup corrected
> If you are mid-rebase, undo/redo is not supported, because the reflog doesn't enough contain information about what specific things have happened inside that rebase.

changed to

> If you are mid-rebase, undo/redo is not supported, because the reflog doesn't contain enough information about what specific things have happened inside that rebase.
2020-10-01 17:41:32 +10:00
Jesse Duffield
c12752cf53 add mutex to views array 2020-10-01 07:01:39 +10:00
Jesse Duffield
ca105692cf fix windows build 2020-09-29 20:48:49 +10:00
Jesse Duffield
ce6f8ed1bc move models folder into commands folder 2020-09-29 20:48:49 +10:00
Jesse Duffield
83748d78f8 fix tests 2020-09-29 20:48:49 +10:00
Jesse Duffield
72af7e4177 factor out code from git.go 2020-09-29 20:48:49 +10:00
Jesse Duffield
1767f91047 factor out code for loading models 2020-09-29 20:48:49 +10:00
Jesse Duffield
1759ddf247 move OS commands into their own package 2020-09-29 20:48:49 +10:00
Jesse Duffield
f9643448a4 move commit files 2020-09-29 20:48:49 +10:00
Jesse Duffield
91f0b0e28f move stash panel 2020-09-29 20:48:49 +10:00
Jesse Duffield
8d2af5cc61 move file and submodule 2020-09-29 20:48:49 +10:00
Jesse Duffield
eda4619a4f move remotes and remote branches 2020-09-29 20:48:49 +10:00
Jesse Duffield
e849ca3372 move tags 2020-09-29 20:48:49 +10:00
Jesse Duffield
630e446989 move commits model into models package 2020-09-29 20:48:49 +10:00
Jesse Duffield
44248d9ab0 pull branch model out into models package 2020-09-29 20:48:49 +10:00
Jesse Duffield
c87b2c02fa fix tests 2020-09-29 18:21:59 +10:00
Jesse Duffield
6e80371535 tell users we're going to reset submodules 2020-09-29 18:21:59 +10:00
Jesse Duffield
b4a350259d format code 2020-09-29 18:21:59 +10:00
Jesse Duffield
914fb36173 allow entering and returning from submodule 2020-09-29 18:21:59 +10:00
Jesse Duffield
b882ac9e06 support nuking all submodules 2020-09-29 18:21:59 +10:00
Jesse Duffield
b8da166ab1 support discarding submodule changes 2020-09-29 18:21:59 +10:00
Jesse Duffield
ca437a6504 support submodules 2020-09-29 18:21:59 +10:00
Jesse Duffield
72a31aed76 support opening lazygit in a symlinked submodule 2020-09-29 17:48:21 +10:00
Jesse Duffield
59e117738d missed a spot 2020-09-29 17:42:07 +10:00
Jesse Duffield
75598ea2a1 move git dir env stuff into a centralised package 2020-09-29 17:42:07 +10:00
Jesse Duffield
e873816160 do not include bare repos in recent repos list 2020-09-29 17:42:07 +10:00
Jesse Duffield
23626755d7 unset GIT_WORK_TREE and GIT_DIR when switching repos 2020-09-29 17:42:07 +10:00
Jesse Duffield
97af7e677b support bare repositories 2020-09-29 17:42:07 +10:00
Jesse Duffield
f9f7f74efb Update Config.md 2020-09-27 11:59:25 +10:00
Jesse Duffield
de482262e1 support setting description in custom command 2020-09-27 11:32:54 +10:00
Jesse Duffield
1b39c829ac Update Custom_Command_Keybindings.md 2020-09-27 11:15:08 +10:00
Jesse Duffield
fb09fb4472 bump gocui 2020-09-27 11:11:55 +10:00
Jesse Duffield
12f9b1416f better handling of global custom keybindings 2020-09-27 11:11:55 +10:00
Jesse Duffield
4dad7064cc Update Custom_Command_Keybindings.md 2020-09-27 11:11:44 +10:00
Jesse Duffield
628abc412e Update README.md 2020-09-27 10:36:18 +10:00
Jesse Duffield
84a899c38a remove resources folder now that we're using the assets branch 2020-09-27 10:32:44 +10:00
Jesse Duffield
21d4e46bf1 point to assets branch 2020-09-27 10:31:53 +10:00
Jesse Duffield
c603691a98 Update Config.md 2020-09-27 10:31:16 +10:00
Jesse Duffield
efbbc5c6bc Update Undoing.md 2020-09-27 10:30:42 +10:00
Jesse Duffield
8dcc148a22 Update Custom_Command_Keybindings.md 2020-09-27 10:28:39 +10:00
Jesse Duffield
becd4cc3c0 Update README.md 2020-09-27 09:58:49 +10:00
Jesse Duffield
e0ea2b75a1 add custom command keybindings doc 2020-09-27 09:56:33 +10:00
Jesse Duffield
a09bb5d4d8 better validation messages 2020-09-27 09:49:30 +10:00
Jesse Duffield
7cd17d3a73 support custom command loading text 2020-09-27 09:49:30 +10:00
Jesse Duffield
8a59a4404b rename prompt to input 2020-09-27 09:49:30 +10:00
Jesse Duffield
5724fa534a fallback to value if name not given 2020-09-27 09:49:30 +10:00
Jesse Duffield
e7210dd249 better template support for menus and prompts 2020-09-27 09:49:30 +10:00
Jesse Duffield
7d39cc75b2 support menus in custom commands 2020-09-27 09:49:30 +10:00
Jesse Duffield
b5066f1d8e support prompts in custom commands 2020-09-27 09:49:30 +10:00
Jesse Duffield
266d8bf0d5 minor fixup 2020-09-27 09:49:30 +10:00
Jesse Duffield
da8eac5538 better interface 2020-09-27 09:49:30 +10:00
Jesse Duffield
67bbeb195b support custom keybindings 2020-09-27 09:49:30 +10:00
Jesse Duffield
92183de29e more lenient handling of views not existing 2020-09-26 19:33:22 +10:00
Jesse Duffield
8dae54ab8c fix panic 2020-09-26 11:56:22 +10:00
Jesse Duffield
62a31c27e1 refresh commit files view when needed 2020-09-26 11:52:38 +10:00
Jesse Duffield
dd29ee7288 convert to string in a better way because I'm pretty sure alpine needs it 2020-09-26 11:12:47 +10:00
Jesse Duffield
fe64f2f4c9 use --no-ext-diff flag for git diff 2020-09-26 11:03:38 +10:00
Jesse Duffield
03ea4a884a tidy go.sum 2020-09-26 11:00:50 +10:00
Jesse Duffield
d3c7cbeea7 better documentation 2020-09-26 11:00:50 +10:00
Jesse Duffield
f0a1544ebd more logging 2020-09-26 11:00:50 +10:00
Jesse Duffield
077f113618 add in-built logging support for a better dev experience 2020-09-26 11:00:50 +10:00
Jesse Duffield
0c6cbe7746 Update README.md 2020-09-26 10:57:04 +10:00
Jesse Duffield
3bdfb2875f Update README.md 2020-09-26 10:56:22 +10:00
Yuki Osaki
cc7ea736bb Change the descriptions to lowercase 2020-09-21 15:35:50 +10:00
Yuki Osaki
9dfe1bbadf Update pkg/i18n/english.go
Co-authored-by: Jesse Duffield <jessedduffield@gmail.com>
2020-09-21 15:35:50 +10:00
Yuki Osaki
1fd89b4f46 Be able to copy file name 2020-09-21 15:35:50 +10:00
Jesse Duffield
307d051ec2 smarter checking of git version 2020-09-18 21:02:27 +10:00
Jesse Duffield
3a668011fa better flag description 2020-09-18 08:40:30 +10:00
Jesse Duffield
14c8b80494 show loading state when amending top commit 2020-09-18 07:58:16 +10:00
Lakshay Garg
10dde518bc Fix numbering in Dutch IntroPopupMessage 2020-09-18 07:47:01 +10:00
Lakshay Garg
1e1c90c92e Fix numbering in English IntroPopupMessage 2020-09-18 07:47:01 +10:00
Jesse Duffield
4954791443 fix test 2020-09-18 07:46:12 +10:00
Jesse Duffield
c471f4927a fix test 2020-09-02 20:55:53 +10:00
Jesse Duffield
9eba98302e ensure that when a branch name is ambiguous we still show the correct colours 2020-09-02 10:40:50 +00:00
Francisco Miamoto
250fe740b2 use GetBool instead of casting 2020-08-31 09:22:39 +10:00
Francisco Miamoto
70eda031dc implement config option for disabling force pushing 2020-08-31 09:22:39 +10:00
Francisco Miamoto
86f296a898 add config for disabling force pushing 2020-08-31 09:22:39 +10:00
Jesse Duffield
71ff18318d fast UI update when moving commits in rebase mode 2020-08-29 00:19:31 +00:00
Jesse Duffield
46cce28758 restore donate link 2020-08-28 09:52:56 +10:00
Jesse Duffield
5611d9a3ef gracefully fail due to git version less than 2.0 2020-08-27 12:21:37 +00:00
Jesse Duffield
40bec49de8 more efficient refreshing of rebase commits 2020-08-27 21:51:07 +10:00
Jesse Duffield
f99d5f74d4 drop merge commits when interactive rebasing just like git CLI 2020-08-27 21:51:07 +10:00
Jesse Duffield
30a066aa41 remove redundant test 2020-08-27 19:29:22 +10:00
Jesse Duffield
1dcc3363d0 support branches with no upstream 2020-08-27 17:05:07 +10:00
Jesse Duffield
c6948582e6 better way of knowing which commits are unpushed 2020-08-26 22:45:55 +00:00
Jesse Duffield
196c83d058 fix bug where cancelling search in menu caused issue 2020-08-26 09:32:57 +00:00
Jesse Duffield
806bee9646 try again 2020-08-26 08:57:32 +10:00
Jesse Duffield
45a0378c01 do not create error panel for sentinel errors 2020-08-25 22:21:15 +00:00
Jesse Duffield
afd669194a use clipboard package to handle clipboard stuff 2020-08-26 07:53:43 +10:00
Jesse Duffield
1494a3863d Remove tab keybinding for cycling tab
This keybinding has been more pain than it's worth. Having a tab keybinding
to cycle tabs implies that you can shift+tab and when you shift+tab the
application exits because termbox, our dependency, doesn't know how to
interpret the escape sequence (so it takes it for an actual ESC key which
will exit lazygit at the top level).

If people get mad at me they can set nextBlock-alt to <tab> and they'll have
the functionality back :)
2020-08-25 10:48:13 +00:00
Jesse Duffield
f5c55f066b use new branch logic when 'checking out' remote branch 2020-08-25 09:25:17 +00:00
Dawid Dziurla
bd8f198beb workflows: run CI on master or pull request only 2020-08-25 09:18:29 +02:00
Dawid Dziurla
f05adb4f99 workflows: match every branch
Previously, branches with `/` in the name wasn't being matched
2020-08-25 09:16:13 +02:00
Jesse Duffield
3ebb91c07a better keybinding ('W') for viewing diff 2020-08-24 23:08:05 +00:00
Jesse Duffield
771e87ebeb do not reset cursor unless previous file has moved position 2020-08-24 22:39:01 +00:00
Jesse Duffield
2598ce1d4b bump creack 2020-08-24 22:16:38 +00:00
Jesse Duffield
e2f3b2b41f add log when git status errors 2020-08-25 08:04:45 +10:00
Jesse Duffield
7ebb8343d1 ignore warning messages about files when obtaining file statuses 2020-08-24 11:53:17 +00:00
Jesse Duffield
42479a75af prevent moving cursor past last character in prompt modal 2020-08-24 20:11:32 +10:00
Jesse Duffield
22c7110349 prevent moving cursor past last character in prompt modal 2020-08-24 10:02:08 +00:00
Jesse Duffield
44ee28bb2e support alacritty 2020-08-24 09:19:56 +10:00
Jesse Duffield
f172f20219 Return whether the context has a parent or not along with that parent
There has got to be a better way around this but if we're returning a Context
from a function (Context is an interface, not a concrete type), even if we
return nil, on the calling end it won't be equal to nil because an interface
value is a tuple of the type and the value meaning it's never itself nil,
unless both values in the tuple are nil.

So we're explicitly returning whether or not the underlying concrete type is nil.
2020-08-23 22:30:32 +00:00
Jesse Duffield
0f7003d939 allow spamming the p key 2020-08-23 11:30:29 +00:00
Jesse Duffield
d2d88fe64e fix focus change on merge popup return 2020-08-23 19:28:59 +10:00
Jesse Duffield
fa2a385a0c when in the remote branches view, prefill name for creating branch off of remote branch 2020-08-23 19:27:34 +10:00
Jesse Duffield
bd9579983e bump gocui to ensure no crash on startup 2020-08-23 17:49:58 +10:00
Jesse Duffield
66bd86b9b7 set keybindings after initialising views 2020-08-23 17:49:58 +10:00
Jesse Duffield
364bdcf532 safer getting of branch 2020-08-23 17:49:58 +10:00
Jesse Duffield
ba7e098373 check for missing view when scrolling 2020-08-23 16:05:20 +10:00
Jesse Duffield
9f71c8d2b9 rename Status to PatchStatus 2020-08-23 15:11:06 +10:00
Jesse Duffield
fce7cdcc0a enlargen stash window when its focused 2020-08-23 15:11:06 +10:00
Jesse Duffield
4fb52ce2ab better handling of there being no commit files 2020-08-23 15:11:06 +10:00
Jesse Duffield
2915134007 show file statuses in commit files view 2020-08-23 15:11:06 +10:00
Jesse Duffield
2f893bf361 format 2020-08-23 14:29:18 +10:00
Jesse Duffield
f815c5607c prefill remote edit prompts 2020-08-23 14:29:18 +10:00
Jesse Duffield
59d61f00a6 hide secondary view when escaping patch building panel 2020-08-23 14:29:18 +10:00
Jesse Duffield
262ff24c5b always reset branch selected index when creating new branch 2020-08-23 14:29:18 +10:00
Jesse Duffield
1189c2fab7 we've now flipped the boolean 2020-08-23 14:29:18 +10:00
Jesse Duffield
3eb3de3edc allow explicitly managing focus 2020-08-23 14:29:18 +10:00
Jesse Duffield
94601b4dc9 use context to return to the correct view 2020-08-23 14:29:18 +10:00
Jesse Duffield
9ca0073cd7 attempt at fixing bad lazyloading 2020-08-23 14:29:18 +10:00
Jesse Duffield
55e6366529 run task for appropriate view 2020-08-23 14:29:18 +10:00
Jesse Duffield
bd66162972 fix up patch manager 2020-08-23 14:29:18 +10:00
Jesse Duffield
5cdfd41dca prevent spamming pull or push buttons 2020-08-23 14:29:18 +10:00
Jesse Duffield
a95fd581fd fix logic for entering merging panel 2020-08-23 14:29:18 +10:00
Jesse Duffield
fda9f4ea7a centralise logic for rendering options map 2020-08-23 14:29:18 +10:00
Jesse Duffield
f876d8fdc8 use constants 2020-08-23 14:29:18 +10:00
Jesse Duffield
4198bbae6c ensure there is always a current context 2020-08-23 14:29:18 +10:00
Jesse Duffield
ade54b38c1 cleanup 2020-08-23 14:29:18 +10:00
Jesse Duffield
0dd2c869a8 minor refactor 2020-08-23 14:29:18 +10:00
Jesse Duffield
ed85ea69bd cleanup of list context file 2020-08-23 14:29:18 +10:00
Jesse Duffield
953298de74 remove dead code 2020-08-23 14:29:18 +10:00
Jesse Duffield
628404e114 use actual keys 2020-08-23 14:29:18 +10:00
Jesse Duffield
5638a40007 carry more mode state across after returning from subprocess 2020-08-23 14:29:18 +10:00
Jesse Duffield
d6005dc0eb more accurate comment 2020-08-23 14:29:18 +10:00
Jesse Duffield
b3a7acbdad more standardising modes 2020-08-23 14:29:18 +10:00
Jesse Duffield
88ae550b93 unused method 2020-08-23 14:29:18 +10:00
Jesse Duffield
2c3f5be093 comment these things out because we're not using them yet 2020-08-23 14:29:18 +10:00
Jesse Duffield
95a4ca6f8e remove todo comment 2020-08-23 14:29:18 +10:00
Jesse Duffield
23432dd909 remove test 2020-08-23 14:29:18 +10:00
Jesse Duffield
148f601bcb cleanup now that we're always using the same diff command 2020-08-23 14:29:18 +10:00
Jesse Duffield
43d891b8d6 support creating patches from files in diff mode 2020-08-23 14:29:18 +10:00
Jesse Duffield
2eee079d3a minor rename 2020-08-23 14:29:18 +10:00
Jesse Duffield
30a555b108 don't needlessly load every file 2020-08-23 14:29:18 +10:00
Jesse Duffield
8be970e688 stop loading all the diffs at once now that we load them as we go 2020-08-23 14:29:18 +10:00
Jesse Duffield
12bf851c7d faster patch manager 2020-08-23 14:29:18 +10:00
Jesse Duffield
c837c54c39 handle diffing and filtering by file in commit files view 2020-08-23 14:29:18 +10:00
Jesse Duffield
5874529f43 deal with the fact that a nil wrapped in an interface is not equal to nil 2020-08-23 14:29:18 +10:00
Jesse Duffield
e290710f67 support drilling down into the files of a diff 2020-08-23 14:29:18 +10:00
Jesse Duffield
438abd6003 centralise code for copying to clipboard 2020-08-23 14:29:18 +10:00
Jesse Duffield
442f6cd854 more cherry picking stuff, mostly around the reflog 2020-08-23 14:29:18 +10:00
Jesse Duffield
c2b154acad better handling of our different modes and also cherry picking 2020-08-23 14:29:18 +10:00
Jesse Duffield
fbd61fcd17 refactor how we handle different modes 2020-08-23 14:29:18 +10:00
Jesse Duffield
b1529f19ad more cherry picking code into its own file 2020-08-23 14:29:18 +10:00
Jesse Duffield
134566ed49 move into more appropriate file 2020-08-23 14:29:18 +10:00
Jesse Duffield
8da93fd762 add description field to ListItem interface 2020-08-23 14:29:18 +10:00
Jesse Duffield
63209ef71e try allowing creating branches off the stash too 2020-08-23 14:29:18 +10:00
Jesse Duffield
f63ec38aae genericise creating new branches off things 2020-08-23 14:29:18 +10:00
Jesse Duffield
f858c8e750 rename to make way for a generic function name 2020-08-23 14:29:18 +10:00
Jesse Duffield
26f80087dd when toggling files reset patch manager if patch ends up empty 2020-08-23 14:29:18 +10:00
Jesse Duffield
0ac402792b allow getting the current item generically 2020-08-23 14:29:18 +10:00
Jesse Duffield
974c6510b8 add sub commit context 2020-08-23 14:29:18 +10:00
Jesse Duffield
41df63cdc4 show when building patch 2020-08-23 14:29:18 +10:00
Jesse Duffield
4080e9b501 only return focus if we already have it 2020-08-23 14:29:18 +10:00
Jesse Duffield
53da858c06 escape patch building mode on hitting escape at the top level 2020-08-23 14:29:18 +10:00
Jesse Duffield
50c9ae863a remove sdump 2020-08-23 14:29:18 +10:00
Jesse Duffield
ce20d1b482 remove clipboard option for now because we need a better way of doing it 2020-08-23 14:29:18 +10:00
Jesse Duffield
fcf916d138 don't panic 2020-08-23 14:29:18 +10:00
Jesse Duffield
f3c87bde88 more 2020-08-23 14:29:18 +10:00
Jesse Duffield
3f7136fc7d missed a spot 2020-08-23 14:29:18 +10:00
Jesse Duffield
59f5f5c1af refactor 2020-08-23 14:29:18 +10:00
Jesse Duffield
1956301b1c better menu item name 2020-08-23 14:29:18 +10:00
Jesse Duffield
1fd0f31682 only show rebasey commands on a local commit when patch building 2020-08-23 14:29:18 +10:00
Jesse Duffield
e6a1bd6566 generalise patch building stuff 2020-08-23 14:29:18 +10:00
Jesse Duffield
609f3f4bfa rename Sha to parent now that we're also considering stash entries 2020-08-23 14:29:18 +10:00
Jesse Duffield
9b42cd2214 slightly better 2020-08-23 14:29:18 +10:00
Jesse Duffield
2d90e1e8ee commit files kind of generalised 2020-08-23 14:29:18 +10:00
Jesse Duffield
ddf25e14af allowing commit files to be viewed in reflog as well 2020-08-23 14:29:18 +10:00
Jesse Duffield
48f1adad49 stop logging stack 2020-08-23 14:29:18 +10:00
Jesse Duffield
379d37a255 remove unnecessary function 2020-08-23 14:29:18 +10:00
Jesse Duffield
a59ac064d2 statically define context keys 2020-08-23 14:29:18 +10:00
Jesse Duffield
433d54fcec WIP constants for context keys 2020-08-23 14:29:18 +10:00
Jesse Duffield
146722beb8 rename to SelectedLineIdx 2020-08-23 14:29:18 +10:00
Jesse Duffield
eb5e54e9fd use interface for panel state rather than pointer 2020-08-23 14:29:18 +10:00
Jesse Duffield
99707a527d WIP 2020-08-23 14:29:18 +10:00
Jesse Duffield
9ee7793782 remove comment 2020-08-23 14:29:18 +10:00
Jesse Duffield
bc410d8e4a use camelCase 2020-08-23 14:29:18 +10:00
Jesse Duffield
7561f5aa32 some more standardisation for diffing 2020-08-23 14:29:18 +10:00
Jesse Duffield
2855b5b4d5 standardise diffmode 2020-08-23 14:29:18 +10:00
Jesse Duffield
419cb9feb8 more standardisation 2020-08-23 14:29:18 +10:00
Jesse Duffield
dbf6bb5f27 some more things 2020-08-23 14:29:18 +10:00
Jesse Duffield
f601108c5d update naming to refer to context 2020-08-23 14:29:18 +10:00
Jesse Duffield
b77abdc5e1 WIP 2020-08-23 14:29:18 +10:00
Jesse Duffield
2fac2f9f1f WIP 2020-08-23 14:29:18 +10:00
Jesse Duffield
e4beaf4de9 more stuff 2020-08-23 14:29:18 +10:00
Jesse Duffield
d4f134c6c7 WIP 2020-08-23 14:29:18 +10:00
Jesse Duffield
7ebed76d16 WIP 2020-08-23 14:29:18 +10:00
Jesse Duffield
2b812b01e9 more standardisation of rendering 2020-08-23 14:29:18 +10:00
Jesse Duffield
2f5d5034db good progress 2020-08-23 14:29:18 +10:00
Jesse Duffield
a32947e7a7 prepare for OnRender prop 2020-08-23 14:29:18 +10:00
Jesse Duffield
2fdadd383a introduce new approach to handling tab states 2020-08-23 14:29:18 +10:00
Jesse Duffield
9a2dc3fe15 stop crash due to context stack not being initialized 2020-08-23 14:29:18 +10:00
Jesse Duffield
f0c3d3fc4d centralise setting of main views context 2020-08-23 14:29:18 +10:00
Jesse Duffield
2488e0044d concurrent-safe handling of context state 2020-08-23 14:29:18 +10:00
Jesse Duffield
9c866fd49c more standardisation 2020-08-23 14:29:18 +10:00
Jesse Duffield
6c270b6e26 WIP 2020-08-23 14:29:18 +10:00
Jesse Duffield
ae1c4536e6 WIP 2020-08-23 14:29:18 +10:00
Jesse Duffield
f5b22d94d9 WIP 2020-08-23 14:29:18 +10:00
Jesse Duffield
3c87ff4eff WIP: standardising how we render to main 2020-08-23 14:29:18 +10:00
Jesse Duffield
0f7b2c45d7 centralise split main panel code 2020-08-23 14:29:18 +10:00
Jesse Duffield
a12d18146c better logic for taking focus away from popup panels 2020-08-23 14:29:18 +10:00
Jesse Duffield
119d5be1a4 move into list context file 2020-08-23 14:29:18 +10:00
Jesse Duffield
fcdc0174d9 rename context file 2020-08-23 14:29:18 +10:00
Jesse Duffield
4f4df8f9cc move context specific keybindings into context file 2020-08-23 14:29:18 +10:00
Jesse Duffield
c730271e09 minor update 2020-08-23 14:29:18 +10:00
Jesse Duffield
ac0eedda91 lots more stuff 2020-08-23 14:29:18 +10:00
Jesse Duffield
e87635295a dont check for error when sending view to bottom 2020-08-23 14:29:18 +10:00
Jesse Duffield
62a662054b hide view if not specified in dimensions object 2020-08-23 14:29:18 +10:00
Jesse Duffield
dc183c0d82 no need to set views on top anymore 2020-08-23 14:29:18 +10:00
Jesse Duffield
08e039bea9 return nil when no file selected 2020-08-23 14:29:18 +10:00
Jesse Duffield
88d329c52a WIP 2020-08-23 14:29:18 +10:00
Jesse Duffield
fd8a455aff small things
WIP
2020-08-23 14:29:18 +10:00
Jesse Duffield
ed4574bda9 standardise getting selected item 2020-08-23 14:29:18 +10:00
Jesse Duffield
c9ae54a8c8 remove previous view 2020-08-23 14:29:18 +10:00
Jesse Duffield
6fb83b740b WIP 2020-08-23 14:29:18 +10:00
Jesse Duffield
7f89113245 WIP 2020-08-23 14:29:18 +10:00
Jesse Duffield
0ea0c48631 WIP 2020-08-23 14:29:18 +10:00
Jesse Duffield
cec4cb48cb centralise some list view code 2020-08-23 14:29:18 +10:00
mjarkk
b211a14a66 Add GitUI to alternatives in readme 2020-08-23 14:05:55 +10:00
Jesse Duffield
a3d1455c83 Update README.md 2020-08-22 20:46:00 +10:00
Jesse Duffield
1716de3b59 remove space as keybinding for confirmation panel 2020-08-17 20:30:10 +10:00
Jesse Duffield
44d8b3e8f3 allow overriding default confirm/escape keybindings 2020-08-17 18:22:57 +10:00
Jesse Duffield
4f4bb40ea6 support opening lazygit outside a git directory 2020-08-16 22:59:58 +10:00
Jesse Duffield
db826b3c87 add keybinding to create new branch off of commit
retain focus in commits panel

surface prompt errors

better description
2020-08-16 22:24:54 +10:00
Jesse Duffield
be658e7d64 support multi word editor config 2020-08-16 20:37:40 +10:00
Jesse Duffield
53f06f6a4e prefill commit reword editor 2020-08-16 20:37:24 +10:00
Jesse Duffield
c8add47fe7 move cursor to right when using auto prefix 2020-08-16 18:44:39 +10:00
Jesse Duffield
28cd827cea better popups 2020-08-16 09:07:54 +10:00
Jesse Duffield
ffda2839e0 remove anonymous reporting popup cos we dont do it anymore anyway 2020-08-16 09:07:54 +10:00
Jesse Duffield
28208e8364 refactor list view 2020-08-15 18:01:43 +10:00
Jesse Duffield
9b7a6934b3 more removing of g 2020-08-15 18:01:43 +10:00
Jesse Duffield
15229bbdab more removing of g and v 2020-08-15 18:01:43 +10:00
Jesse Duffield
63e6eea9ec files view 2020-08-15 18:01:43 +10:00
Jesse Duffield
50d5b9e8e7 status view 2020-08-15 18:01:43 +10:00
Jesse Duffield
cc872b0444 menu view 2020-08-15 18:01:43 +10:00
Jesse Duffield
17b84e09c0 fix remote branches select sig 2020-08-15 18:01:43 +10:00
Jesse Duffield
43f8bae267 fix remotes select sig 2020-08-15 18:01:43 +10:00
Jesse Duffield
b0fe963f8a fix branches select sig 2020-08-15 18:01:43 +10:00
Jesse Duffield
0822a9296c rename 2020-08-15 18:01:43 +10:00
Jesse Duffield
d9fa02c53b clean up interface for popup panels 2020-08-15 18:01:43 +10:00
Jesse Duffield
c44ee71ad4 update cheatsheet 2020-08-15 11:41:37 +10:00
Jesse Duffield
826d1660c9 move patch stuff into its own package 2020-08-15 11:41:37 +10:00
Jesse Duffield
291a8e4de0 allow opening files on the selected line in the staging panel 2020-08-15 11:41:37 +10:00
Jesse Duffield
f02ccca0e0 add specs to boxlayout package 2020-08-15 09:04:40 +10:00
Jesse Duffield
1e12a60b34 move box layout stuff into its own package 2020-08-15 09:04:40 +10:00
Jesse Duffield
8430b04492 allow configurable main panel split 2020-08-13 21:50:23 +10:00
Jesse Duffield
35b72420ad support accordian mode i.e. expanding focused side panels 2020-08-13 21:50:23 +10:00
Jesse Duffield
28ba142fd6 set minimum confirmation box width 2020-08-13 21:50:23 +10:00
Jesse Duffield
b39bcd5c61 more lenient for switching into portrait mode 2020-08-13 21:50:23 +10:00
Jesse Duffield
1fd35f3824 centralise logic for information section
WIP
2020-08-13 21:50:23 +10:00
Jesse Duffield
e73937c2bd more work on new layout functionality 2020-08-13 21:50:23 +10:00
Jesse Duffield
b51ad4fcea softcode cyclable views 2020-08-13 21:50:23 +10:00
Jesse Duffield
d1a7c7283f some more changes 2020-08-13 21:50:23 +10:00
Jesse Duffield
b641ecdc74 move some things around 2020-08-13 21:50:23 +10:00
Jesse Duffield
13f567ff4c add portrait mode for when the window is really tall 2020-08-13 21:50:23 +10:00
Jesse Duffield
771d4b5811 refactor how we handle layouts 2020-08-13 21:50:23 +10:00
Jesse Duffield
3c944e0351 support force push after failure 2020-08-12 21:11:24 +10:00
Jesse Duffield
e26af258d6 allow rebasing onto remote branch 2020-08-12 20:58:34 +10:00
Jesse Duffield
76e5ec6d45 immediately quit when pressing q in diff or filter mode 2020-08-12 20:44:29 +10:00
Jesse Duffield
27cd12e2d9 accept umlaut keybindings 2020-08-12 20:07:56 +10:00
Jesse Duffield
bfaf1c4f70 use remote prefixed branch name when merging remote branch 2020-08-12 20:07:46 +10:00
Jesse Duffield
2d18d089ce allow entering a password when fast forwarding another branch 2020-08-12 18:47:16 +10:00
Jesse Duffield
9c7e40906d rename arg 2020-08-12 18:47:16 +10:00
Jesse Duffield
401f291c3b lowercase function name 2020-08-12 18:47:16 +10:00
Jesse Duffield
bea2ae5ff5 stop pulling in general 2020-08-12 18:47:16 +10:00
Jesse Duffield
f49e4946f2 minor refactor 2020-08-12 18:47:16 +10:00
Jesse Duffield
8ff74072f8 update config 2020-08-12 18:47:16 +10:00
Jesse Duffield
fcd5aea04e support multiple modes of git pull 2020-08-12 18:47:16 +10:00
Jesse Duffield
1c0da2967c update naming 2020-08-12 18:47:16 +10:00
Jesse Duffield
1b78a42b80 pass callback directly 2020-08-12 18:47:16 +10:00
Jesse Duffield
79e73d2eff minor cleanup
WIP
2020-08-12 18:47:16 +10:00
Jesse Duffield
23299f88e9 simplify patch modifier interface 2020-08-09 15:42:20 +10:00
mjarkk
ef744e45c1 Update dutch translations 2020-08-08 14:25:11 +10:00
Jesse Duffield
660cc2f3d1 follow cursor when staging and unstaging a file rename 2020-08-07 18:59:56 +10:00
Jesse Duffield
469ac116ef allow renames to be discarded 2020-08-07 18:01:26 +10:00
Jesse Duffield
a86103479b cleanup 2020-08-07 18:01:26 +10:00
Axel Navarro
d49e75bd3e Add tab keybinding in commit message 2020-07-26 16:28:01 +10:00
Jesse Duffield
f4718a9047 allow editing commit files 2020-07-21 18:24:39 +10:00
Jesse Duffield
7d5fe4b66c better logic for staging a renamed file 2020-07-19 14:11:32 +10:00
Jesse Duffield
845c80721f Decouple escaping from quitting
When a user is not entering text into a prompt, the 'q' key should immediately
quit the application. On the other hand, the 'esc' key should cancel/close/go-back
to the previous context.

If we're at the surface level (nothing to cancel/close) and the user hits the
escape key, the default behaviour is to close the app, however we now have a
`quitOnTopLevelReturn` config key to override this.

I actually think from the beginning we should have made this config option
default to false rather than true which is the default this PR gives it,
but I don't want to anger too many people familiar with the existing behaviour.
2020-07-18 20:00:48 +10:00
Gadzhi Kharkharov
0e65db10d8 add solus linux installation info 2020-07-18 19:48:05 +10:00
Jesse Duffield
a9cc321981 prompt to create new branch if branch not found 2020-07-17 09:20:50 +10:00
Jesse Duffield
6349214f00 prompt to commit all files if committing with no staged files 2020-07-17 09:01:40 +10:00
Randshot
96f821b841 fix TestGitCommandCommit test
Signed-off-by: Randshot <randshot@norealm.xyz>
2020-07-15 09:41:16 +10:00
Randshot
964e3872c1 revert changes to 'os_default_platform.go' and 'os_windows.go'
Signed-off-by: Randshot <randshot@norealm.xyz>
2020-07-15 09:41:16 +10:00
Randshot
5dfa26ea8b use strconv for quoting in 'GitCommand.Commit' and 'OSCommand.ShellCommandFromString'
use raw strings for the escaped quotes in 'os_default_platform.go' and 'os_windows.go'

Signed-off-by: Randshot <randshot@norealm.xyz>
2020-07-15 09:41:16 +10:00
Dawid Dziurla
dbf042b8ad goreleaser: fix deprecation and comment 2020-07-14 09:13:43 +02:00
Randshot
014e06eefd factor out duplicate code into 'ShellCommandFromString'
Signed-off-by: Randshot <randshot@norealm.xyz>
2020-07-14 08:26:53 +10:00
Randshot
39a2122dc0 add quotes around the git commit command on non-windows systems
Signed-off-by: Randshot <randshot@norealm.xyz>
2020-07-14 08:26:53 +10:00
Randshot
fe6d8d62c5 add overrideGpg switch to Config.md
Signed-off-by: Randshot <randshot@norealm.xyz>
2020-07-12 11:50:12 +02:00
Randshot
570d27ffaa Merge branch 'master' into add-overrideGpg-switch
Signed-off-by: Randshot <randshot@norealm.xyz>
2020-07-12 11:47:35 +02:00
Pranav Shikarpur
7b69aa1fda Added ENTRYPOINT to Dockerfile to jump directly into lazy git while running the docker container 2020-07-12 14:10:04 +10:00
Randshot
21e478dd59 fix 'Amend commit using gpg' test
Signed-off-by: Randshot <randshot@norealm.xyz>
2020-07-12 14:06:53 +10:00
Randshot
d14fb36cb9 fix 'Commit using gpg' test
Signed-off-by: Randshot <randshot@norealm.xyz>
2020-07-12 14:06:53 +10:00
Randshot
19a808642f fix platform specific quoting when using GPG
fixes #620

Signed-off-by: Randshot <randshot@norealm.xyz>
2020-07-12 14:06:53 +10:00
Jasper Mendiola
e921ba0910 Remove getLocalGitConfig 2020-07-10 18:55:00 +10:00
Jasper Mendiola
0f5a073d57 Rename appconfig to config 2020-07-10 18:55:00 +10:00
Jasper Mendiola
cb0bdd89c0 fix tests 2020-07-10 18:55:00 +10:00
Jasper Mendiola
e89bf5d06b add oneline-graph 2020-07-10 18:55:00 +10:00
David Chen
e82d2f37a1 Update example keybinding config for Colemak users 2020-06-03 22:14:21 +10:00
Randshot
65e955c622 add overrideGpg switch, which prevents lazygit from spawning a separate process when using GPG
Signed-off-by: Randshot <randshot@norealm.xyz>
2020-05-30 23:39:07 +02:00
Dima Kotik
e73f4c6b7e Better CWD check for a git repository. 2020-05-30 00:31:58 +10:00
Jesse Duffield
cf5cefb2d6 allow user to scroll themselves inside merge panel 2020-05-19 18:44:53 +10:00
Jesse Duffield
36ac764133 fix race condition when scrolling to merge conflict 2020-05-19 18:05:14 +10:00
Jesse Duffield
003e45d2f5 allow creating branches off of remote branches 2020-05-19 09:57:37 +10:00
Jesse Duffield
04e93317b8 fix https://github.com/jesseduffield/lazygit/issues/848 2020-05-19 09:57:37 +10:00
Jesse Duffield
f8dedb710b additional password prompt regex 2020-05-15 22:18:07 +10:00
Jesse Duffield
1c259f69f6 check if user has configured to push to current by default 2020-05-15 21:41:23 +10:00
Jesse Duffield
913f17ee3e prevent flicker from bolding background of selected line 2020-05-15 21:12:12 +10:00
Dawid Dziurla
6291c53966 workflows: update bumping action to v3 2020-05-13 13:36:59 +02:00
Jesse Duffield
267730bc00 standardise how we handle background colours 2020-05-13 21:24:25 +10:00
Jesse Duffield
d5db02a899 bump gocui to be on 'simple' branch.
The master branch of gocui contains stuff I added for lazynpm which changes how
the cursor is used. This will provide some benefits to lazygit as well but I
don't yet have the motivation to make the required changed in lazygit to support it.

So we're gonna be on the branch named 'simple' rather than master until I fix that up.
2020-05-13 21:24:25 +10:00
Gary Yendell
7ed8ee160d Add option to split patch into a new commit
Add GetHeadCommitMessage to read the subject of the HEAD commit
Create PullPatchIntoNewCommit based heavily on PullPatchIntoIndex to
  split the current patch from its commit and apply it in a separate
  commit immediately after.

WIP to Squash - Fill format string with format string

WIP
2020-05-09 11:59:37 +10:00
Josh Soref
3dd33b65a0 Minor fixes
* Windows
* Use backticks
* Italicize git config
2020-05-08 09:48:13 +10:00
Dawid Dziurla
b85048f616 workflows: update CI triggers
So it would run on pull requests from forks
2020-05-04 20:03:27 +02:00
Mike Palmer
0852f53455 Add path to config file on Windows 2020-04-27 19:15:06 +10:00
Glenn Vriesman
10fa119ab3 fix: fixed readme link
Signed-off-by: Glenn Vriesman <glenn.vriesman@gmail.com>
2020-04-27 19:14:43 +10:00
Tyler Davis
b5404c6159 fix issue #640 add catCmd and OS-specific values
Add a catCmd to the Platform struct and set the value to "cat" for
non-windows builds and "type" for windows builds.
2020-04-27 19:14:18 +10:00
Lars E
42d21c4bb6 Add FreeBSD installation instructions 2020-04-22 19:51:01 +10:00
Jesse Duffield
cc13ae252a totally screwed up the last commit 2020-04-22 11:21:20 +10:00
Jesse Duffield
b97f844a3e handle comments in todo files 2020-04-22 11:15:41 +10:00
Glenn Vriesman
1d6eb015c1 fix: fixed yaml typo
Signed-off-by: Glenn Vriesman <glenn.vriesman@gmail.com>
2020-04-22 08:52:08 +10:00
Jesse Duffield
07a8ae8c3e add handler for searching in menu 2020-04-21 19:28:31 +10:00
Jesse Duffield
f05a5e531e warnings for stash actions 2020-04-20 18:57:08 +10:00
Kristijan Husak
68586ec49a Handle regex compilation errors and show them to the user. 2020-04-20 18:47:50 +10:00
Kristijan Husak
6cf75af0af Add option to set predefined commit message prefix. Fixes #760. 2020-04-20 18:47:50 +10:00
Jesse Duffield
304607ae5d support configurable merge args 2020-04-20 18:40:49 +10:00
Jesse Duffield
e9f28855a2 add bugfix git flow option 2020-04-20 18:31:13 +10:00
Glenn Vriesman
66d7d5f312 fix: fixed gpg breaking terminal
Signed-off-by: Glenn Vriesman <glenn.vriesman@gmail.com>
2020-04-20 18:30:57 +10:00
Jesse Duffield
59734f1069 whoops 2020-04-17 09:27:23 +10:00
Jesse Duffield
2974a57943 support copying stuff to clipboard 2020-04-15 10:44:56 +00:00
Adwin Ying
fcdcd1c335 fix config docs typo 2020-04-03 17:44:15 +11:00
Dawid Dziurla
4a35f9fcdb Merge pull request #775 from jesseduffield/dawidd6-patch-1
workflows: update homebrew bumping action
2020-04-02 23:51:22 +02:00
Dawid Dziurla
674b14802e workflows: update homebrew bumping action 2020-04-02 23:43:26 +02:00
Jesse Duffield
3e36affa69 remove trash files 2020-03-29 21:53:25 +00:00
Jesse Duffield
97d7a8ad0c add reverse patch option 2020-03-29 21:53:25 +00:00
Jesse Duffield
b89ba365d0 unbold diff info 2020-03-29 18:31:19 +11:00
Jesse Duffield
47ff388549 some more UI logic 2020-03-29 18:26:24 +11:00
Jesse Duffield
647ab9bf0f better keybinding 2020-03-29 18:26:24 +11:00
Jesse Duffield
76431b4673 simplify things 2020-03-29 18:26:24 +11:00
Jesse Duffield
be0dd29e3a don't support files until we understand the use case 2020-03-29 18:26:24 +11:00
Jesse Duffield
40fbce91ce add new diff mode
WIP

WIP

WIP

WIP

WIP

WIP

WIP
2020-03-29 18:26:24 +11:00
Jesse Duffield
33d287d2f0 remove old diff mode code 2020-03-29 18:26:24 +11:00
Jesse Duffield
9eb1cbc514 reset main's origin when cycling views 2020-03-29 02:36:01 +00:00
Jesse Duffield
40b173118a fix conflict race condition 2020-03-29 02:36:01 +00:00
Jesse Duffield
8822c409e2 split reflog commits into ReflogCommits and FilteredReflogCommits 2020-03-29 11:37:29 +11:00
Jesse Duffield
aa750c0819 load reflog commits manually when in filter mode for branches panel 2020-03-29 11:37:29 +11:00
Jesse Duffield
d90d9d7330 reset state on each Run() call 2020-03-29 11:37:29 +11:00
Jesse Duffield
a8db672ffb refactor gui.go 2020-03-29 11:37:29 +11:00
Jesse Duffield
76b66ae26f properly reset gui state when restarting or coming back from a subprocess 2020-03-29 11:37:29 +11:00
Jesse Duffield
a2790cfe8e rename to filtered mode 2020-03-29 11:37:29 +11:00
Jesse Duffield
624ae45ebb allow scoped mode where the commits/reflog/stash panels are scoped to a file
WIP

restrict certain actions in scoped mode

WIP
2020-03-29 11:37:29 +11:00
Jesse Duffield
2756b82f57 fix width of half screen mode 2020-03-29 11:37:29 +11:00
Jesse Duffield
52f41ab0d5 update cheatsheet 2020-03-28 03:16:44 +00:00
Jesse Duffield
fbb767893e support lazyloading in commits view 2020-03-28 14:02:53 +11:00
Jesse Duffield
229f5ee48c add keybindings for paging in list panels and jumping to top/bottom 2020-03-28 14:02:53 +11:00
Jesse Duffield
96c7741ba0 add workflow for auto-merging 2020-03-28 13:22:30 +11:00
Jesse Duffield
517b7d0283 fix up some things with the patch handling stuff 2020-03-28 13:19:35 +11:00
Jesse Duffield
0c0231c3e8 autostash changes when pulling file into index 2020-03-28 13:19:35 +11:00
Jesse Duffield
a9559a5c87 move working tree state function into git.go 2020-03-28 13:19:35 +11:00
Jesse Duffield
814ee24c8d better error handling 2020-03-28 11:59:45 +11:00
Jesse Duffield
7876cddf4a remove dead code 2020-03-28 11:59:45 +11:00
Jesse Duffield
e9051355a1 fix test 2020-03-28 11:59:45 +11:00
Jesse Duffield
29316a528a better documentation 2020-03-28 11:59:45 +11:00
Jesse Duffield
036b53acf8 in fact we don't need any of these options 2020-03-28 11:59:45 +11:00
Jesse Duffield
919463ff02 actually don't even bother limiting 2020-03-28 11:59:45 +11:00
Jesse Duffield
3f7ec3f3b8 load reflog commits in two stages to speed up startup time 2020-03-28 11:59:45 +11:00
Jesse Duffield
19604214d7 discard old reflog commits when in new context 2020-03-28 11:59:45 +11:00
Jesse Duffield
f7add8d788 smarter refreshing for tags and remotes 2020-03-28 11:59:45 +11:00
Jesse Duffield
d97c230747 stop switching focus to commit files view while staging line by line 2020-03-28 11:59:45 +11:00
Jesse Duffield
906a49049e smart refreshing files 2020-03-28 11:59:45 +11:00
Jesse Duffield
c1a4bd0482 more smart refreshing
WIP

WIP

WIP

WIP

WIP

fix how diff entries are handled

WIP

WIP

WIP

WIP

WIP

WIP
2020-03-28 11:59:45 +11:00
Jesse Duffield
d0336fe16f better presentation of remotes 2020-03-28 11:59:45 +11:00
Jesse Duffield
61b4bbf74e clean up signature 2020-03-28 11:59:45 +11:00
Jesse Duffield
384c2e13d7 better refreshing for stash 2020-03-28 11:59:45 +11:00
Jesse Duffield
198d237679 more centralised handling of refreshing 2020-03-28 11:59:45 +11:00
Jesse Duffield
39315ca1e2 use wait groups when refreshing 2020-03-28 11:59:45 +11:00
Jesse Duffield
efb51eee96 more efficient refreshing 2020-03-28 11:59:45 +11:00
Jesse Duffield
fbbd16bd82 use reflogs from state to work out branch recencies 2020-03-28 11:59:45 +11:00
Jesse Duffield
bd2c1eef53 remove redundant fetch of reflog 2020-03-28 11:59:45 +11:00
Jesse Duffield
d1395b15bb use GIT_EDITOR 2020-03-27 19:26:14 +11:00
Máximo Cuadros
2d8ed5e274 *: update go-git import 2020-03-27 19:06:21 +11:00
Máximo Cuadros
6a5d8ba859 vendor: replace go-git package 2020-03-27 19:06:21 +11:00
Francisco Miamoto
320e2a6536 fix links on toc 2020-03-27 09:14:04 +11:00
Dawid Dziurla
3858118340 Merge pull request #751 from Semro/patch-1
Fix link in README
2020-03-26 16:59:04 +01:00
Semro
6420068569 Fix link in README 2020-03-26 18:46:26 +03:00
Jesse Duffield
95b147079f allow applying patch directly 2020-03-26 21:44:45 +11:00
Jesse Duffield
83757f1065 limit size of menu panel 2020-03-26 21:44:33 +11:00
Jesse Duffield
f2036b42e5 only load new reflog entries 2020-03-26 21:44:33 +11:00
Jesse Duffield
21b7d41845 relax limit on commit list and reset on branch change 2020-03-26 21:44:33 +11:00
Jesse Duffield
91a404d033 separate commits from cherry pick state 2020-03-26 21:44:33 +11:00
Jesse Duffield
d027cf969c better handling of current branch name 2020-03-26 20:37:06 +11:00
Jesse Duffield
c7f68a2ef9 delete unused assets 2020-03-26 19:18:43 +11:00
Jesse Duffield
78e55a05c1 another staging gif 2020-03-26 19:15:15 +11:00
Jesse Duffield
ca71555d0b Update README.md 2020-03-26 19:10:49 +11:00
Jesse Duffield
77fdac01ff better staging gif 2020-03-26 19:10:37 +11:00
Jesse Duffield
8301fae01e Update README.md 2020-03-26 19:04:48 +11:00
Jesse Duffield
e9161ad702 add staging gif 2020-03-26 19:04:26 +11:00
Jesse Duffield
a0a139da1f add rebasing gif 2020-03-26 18:43:10 +11:00
Francisco Miamoto
8f13d1da91 change binary releases order 2020-03-26 18:32:38 +11:00
Francisco Miamoto
d5fe9ce2c7 add table of contents to readme 2020-03-26 18:32:38 +11:00
Jesse Duffield
37acc17cf3 more lenient getting of short shas 2020-03-26 18:30:02 +11:00
Jesse Duffield
569ec5919c Update README.md 2020-03-26 09:14:14 +11:00
Dawid Dziurla
19719becf5 workflows: run goreleaser as a build step for CI 2020-03-25 21:26:15 +11:00
Dawid Dziurla
e64057b803 workflows: install gox in separate step in /tmp directory
avoid Go trying to add a dependency to go.mod file
2020-03-25 21:26:15 +11:00
Dawid Dziurla
672667aa3e goreleaser: stop ignoring arm64 build for freebsd 2020-03-25 21:26:15 +11:00
Dawid Dziurla
8a06b6067e go mod vendor 2020-03-25 21:26:15 +11:00
Dawid Dziurla
2dcc52abd0 go mod tidy 2020-03-25 21:26:15 +11:00
Dawid Dziurla
c831ad39c9 pkg: use upstream pty package 2020-03-25 21:26:15 +11:00
Jesse Duffield
0cf78ea9ad Update Undoing.md 2020-03-25 20:35:10 +11:00
Jesse Duffield
3d51fbf354 Update README.md 2020-03-25 20:32:44 +11:00
Jesse Duffield
e7a2c7cc3e update cheatsheet 2020-03-25 20:24:03 +11:00
Jesse Duffield
708a078412 document undo 2020-03-25 20:17:46 +11:00
Jesse Duffield
bbcc4b7b70 just disallow undo/redo while rebasing because you need more info than just the reflog 2020-03-25 09:39:04 +11:00
Jesse Duffield
45bba0a3c5 ignore redundant actions when undoing and redoing 2020-03-25 09:39:04 +11:00
Jesse Duffield
d105e2690a vastly improve the logic for undo and redo 2020-03-25 09:39:04 +11:00
Jesse Duffield
32d3e497c3 fix tests 2020-03-25 09:39:04 +11:00
Jesse Duffield
30a5d1b486 move into undoing file 2020-03-25 09:39:04 +11:00
Jesse Duffield
6b3ea56add refactor undo and redo 2020-03-25 09:39:04 +11:00
Jesse Duffield
c3aefdb98e stateless undos and redos 2020-03-25 09:39:04 +11:00
Jesse Duffield
094939451d more explicit env vars 2020-03-25 09:39:04 +11:00
Jesse Duffield
0e23f44b84 support reflog action prefix 2020-03-25 09:39:04 +11:00
Jesse Duffield
daecdd7c2b redoing 2020-03-25 09:39:04 +11:00
Jesse Duffield
7c8df28d01 add waiting status to checkout ref handler 2020-03-25 09:39:04 +11:00
Jesse Duffield
65917272a2 undoing status 2020-03-25 09:39:04 +11:00
Jesse Duffield
137fd80fdb note that undo functionality is experimental 2020-03-25 09:39:04 +11:00
Jesse Duffield
98fbc61221 better formatted reflog list 2020-03-25 09:39:04 +11:00
Jesse Duffield
f80d15062b use reflog undo history pointer 2020-03-25 09:39:04 +11:00
Jesse Duffield
b1b0219f04 autostash changes when hard resetting 2020-03-25 09:39:04 +11:00
Jesse Duffield
b1941c33f7 undo via rebase 2020-03-25 09:39:04 +11:00
J. B. Rainsberger
a15a7b607d Made the humor more concise and clear. 2020-03-25 09:18:25 +11:00
J. B. Rainsberger
d50283f5ee improved spelling 2020-03-25 09:18:25 +11:00
J. B. Rainsberger
6508d3b872 inject more humor into the README 2020-03-25 09:18:25 +11:00
J. B. Rainsberger
65b8cef1b8 Fixed an incorrect criticism of git in the README.
It's true that parts of git are genuinely difficult to use, so
we don't need to overstate that difficulty in order to state the
value proposition of lazygit. If `git add -p` can't split a hunk
any further, one is not _completely_ stuck, but one does need to
edit the hunk in a way that, after years, I still need a few
attempts to get right. The fact that many otherwise-experienced
git users don't even know that one can do that is itself a
testament to the UX problems that lazygit is trying to address.
2020-03-25 09:18:25 +11:00
Jesse Duffield
5d460e1e5e add tab keybindings 2020-03-23 23:25:00 +11:00
Jesse Duffield
3d3e0be7bd more compatible commands 2020-03-23 22:33:17 +11:00
Dawid Dziurla
c06c0b7133 workflows: git fetch --unshallow before goreleaser step 2020-03-22 21:49:41 +11:00
Dawid Dziurla
91f6630907 README: remove codecov badge 2020-03-22 21:32:06 +11:00
Dawid Dziurla
60085cf679 workflows: use get-tag action 2020-03-22 21:31:53 +11:00
Dawid Dziurla
389480b8fc goreleaser: ignore arm64 build for freebsd 2020-03-22 21:31:37 +11:00
Dawid Dziurla
b5c4f78e9d Remove redundant semicolon 2020-03-21 12:55:44 +11:00
Dawid Dziurla
59b0e2d70a Add GOBIN to PATH 2020-03-21 12:55:44 +11:00
Dawid Dziurla
39bd1a4628 Wording 2020-03-21 12:55:44 +11:00
Dawid Dziurla
1c1445c896 Cache builds 2020-03-21 12:55:44 +11:00
Dawid Dziurla
1e8ade2431 Use setup-go Action instead of container 2020-03-21 12:55:44 +11:00
Dawid Dziurla
a990fbc3eb Don't run codecov or golangci Actions 2020-03-21 12:55:44 +11:00
Dawid Dziurla
e5574e7fe5 Continue if lint step fails 2020-03-21 12:55:44 +11:00
Dawid Dziurla
6c8a924fad Run 4 builds in parallel 2020-03-21 12:55:44 +11:00
Dawid Dziurla
64706257ca Add golangci-lint Action 2020-03-21 12:55:44 +11:00
Dawid Dziurla
6183d92315 Fix typo in script name 2020-03-21 12:55:44 +11:00
Dawid Dziurla
31823a7405 Modify CI badge in README 2020-03-21 12:55:44 +11:00
Dawid Dziurla
85ddd623f6 Add CI workflow 2020-03-21 12:55:44 +11:00
Dawid Dziurla
9212dda9c3 Add CD workflow 2020-03-21 12:55:44 +11:00
Dawid Dziurla
93d7b37c8d Remove homebrew workflow
Will be integrated with another
2020-03-21 12:55:44 +11:00
Dawid Dziurla
8470bcd71d Remove circleci config 2020-03-21 12:55:44 +11:00
Jesse Duffield
3aab37611a show status of selected cherry picked commits 2020-03-19 21:42:21 +11:00
Jesse Duffield
8fbcc36331 allow resetting cherry picked commits selection 2020-03-19 21:42:21 +11:00
Jesse Duffield
dadb646252 fix branch building 2020-03-19 12:04:17 +11:00
Jesse Duffield
0227b93409 fix branch parser 2020-03-18 23:26:02 +11:00
Jesse Duffield
b0ec0821d5 fix docs 2020-03-18 22:50:35 +11:00
hitsuji_no_shippo
13a7806cac add opne menu keybindings in docs 2020-03-18 22:50:35 +11:00
hitsuji_no_shippo
41c76fb748 add close menu keybindings in docs 2020-03-18 22:50:35 +11:00
hitsuji_no_shippo
ac0c3b9f92 fix search keybindings in docs 2020-03-18 22:50:35 +11:00
Jesse Duffield
1be0ff8da7 better upstream tracking and allow renaming a branch 2020-03-18 21:29:06 +11:00
hitsuji_no_shippo
2169b5109f add search keybings in docs 2020-03-11 19:43:22 +11:00
hitsuji_no_shippo
4a2292a53c fix keybindings of main panel in docs 2020-03-11 19:43:22 +11:00
Jesse Duffield
7df4b736cf be a bit more lenient 2020-03-09 12:41:41 +11:00
Jesse Duffield
e47ad846c4 big golangci-lint cleanup 2020-03-09 12:23:13 +11:00
Jesse Duffield
8f68ac2129 case insensitive search
By default, search is now case insensitive.
If you include uppercase characters in your search string, the search
will become case sensitive. This means there is no way to do a case-
insensitive search of all-lowercase strings. We could add more support
for this but we'll wait until we come across the use case
2020-03-09 11:17:50 +11:00
Dawid Dziurla
1ea2825a54 add homebrew bump formula workflow 2020-03-08 20:13:16 +11:00
Jesse Duffield
19146d61b1 use selected branch as base when creating a new branch 2020-03-08 18:44:15 +11:00
skwerlman
e541b809ce update tests to match changed command 2020-03-06 09:25:31 +11:00
skwerlman
6ca08c6519 make branches and files non-ambiguous for git-log
fixes #694
2020-03-06 09:25:31 +11:00
Dawid Dziurla
b43540820b Merge pull request #695 from chenrui333/go-1.14
Bump golang to v1.14
2020-03-04 16:38:56 +01:00
Rui Chen
3d57da71eb Add gox to vendor list 2020-03-04 10:32:07 -05:00
Rui Chen
0130fd3666 go mod vendor 2020-03-03 18:16:09 -05:00
Rui Chen
395afc4a8d Bump golang to v1.14 2020-03-03 18:14:49 -05:00
Jesse Duffield
31e201ca52 allow configuring side panel width 2020-03-04 00:12:23 +11:00
Jesse Duffield
0abd7ad6be update config 2020-03-04 00:12:23 +11:00
Jesse Duffield
b3522c48d9 refactor 2020-03-04 00:12:23 +11:00
Jesse Duffield
0fc58a7986 fix test 2020-03-04 00:12:23 +11:00
Jesse Duffield
54241d8ab9 more generic way of supporting custom pagers 2020-03-04 00:12:23 +11:00
Jesse Duffield
355f1615ab supporing custom pagers step 1 2020-03-04 00:12:23 +11:00
Jesse Duffield
113252b0ae bump gocui 2020-03-04 00:12:23 +11:00
Jesse Duffield
1cd7d14029 Update README.md 2020-03-04 00:11:32 +11:00
Jesse Duffield
87c2fb6a4a Update Custom_Pagers.md 2020-03-04 00:06:49 +11:00
Jesse Duffield
9912998bb7 Create Custom_Pagers.md 2020-03-03 23:07:12 +11:00
Patrick DeVivo
e223d3d8de Add TODOs badge to README
Closes #685
2020-03-02 22:39:19 +11:00
William Wagner Moraes Artero
ec31fc4cc7 docs: moved services conf docs to config.md 2020-03-01 10:57:12 +11:00
William Wagner Moraes Artero
3ce2b9b79a chore: keeping coverage up :D 2020-03-01 10:57:12 +11:00
William Wagner Moraes Artero
a79182e50d fix: accidentally escaped %s 2020-03-01 10:57:12 +11:00
William Wagner Moraes Artero
6f4c595dde docs: configuration and pull request services' settings 2020-03-01 10:57:12 +11:00
William Wagner Moraes Artero
0eb3090ad6 fix: owner groups (GitLab) 2020-03-01 10:57:12 +11:00
William Wagner Moraes Artero
6ea25bd259 feat: flexible service configuration 2020-03-01 10:57:12 +11:00
William Wagner Moraes Artero
fe5f087f9c feat: configurable services 2020-03-01 10:57:12 +11:00
Jesse Duffield
79299be3b2 better keybindings for patch building mode 2020-02-29 18:48:10 +11:00
Jesse Duffield
4c9b620bd0 better keybindings for staging by line 2020-02-29 18:48:10 +11:00
Jesse Duffield
a7508a5dfd fix cheatsheet script to support different contexts 2020-02-29 17:46:00 +11:00
hitsuji no shippo
1a3d765c4c fix keybinds document 2020-02-28 23:08:14 +11:00
Dawid Dziurla
4058c71ca0 Merge pull request #684 from ueberBrot/add-scoop-install-option
Add scoop install option to README.
2020-02-27 18:31:21 +01:00
Maurice de Bruyn
3fc22a6010 Add scoop install option to README.
Adds the install option scoop on Windows to the README.
2020-02-27 18:12:23 +01:00
David Chen
a9fe0b8000 set --abbrev-commit to return 8-digit hash strings 2020-02-27 18:05:41 +11:00
David Chen
5af7b0235e fix #680: unpushed commits still appear to be green instead of red 2020-02-27 18:05:41 +11:00
Corentin Rossignon
bf946200e9 Fix OutOfBound array access when looking for ReflogCommits
refs #679
2020-02-27 09:34:40 +11:00
Jesse Duffield
890cc87724 fix bug where commits appeared as green despite not being pushed 2020-02-27 09:33:09 +11:00
Jesse Duffield
8eb0b0f4ca do not close over variables in a function 2020-02-25 22:09:43 +11:00
Jesse Duffield
e6a8dc0bcf better logic for checking if we're rebasing 2020-02-25 22:09:43 +11:00
Jesse Duffield
02c497fad6 show file list when diffing commits 2020-02-25 21:38:38 +11:00
Jesse Duffield
d0ab747479 color active frames green by default 2020-02-25 21:27:50 +11:00
Jesse Duffield
f94d0be2c9 refactor the way we render lists 2020-02-25 21:21:07 +11:00
Jesse Duffield
9fd9fd6816 better commit lines in fullscreen mode 2020-02-25 21:21:07 +11:00
David Chen
b8717d750a keybinding doc for nextMatch/prevMatch in Config.md (#659) 2020-02-25 09:37:28 +11:00
Jesse Duffield
8ad01fe32f refresh commits when adding a tag 2020-02-25 09:10:23 +11:00
Jesse Duffield
fdb543fa7d add half and fullscreen modes 2020-02-25 08:45:30 +11:00
Jesse Duffield
52b5a6410c show item counts in frames 2020-02-25 07:19:46 +11:00
Jesse Duffield
0034cfef5c show tags in commits panel 2020-02-24 23:13:54 +11:00
Jesse Duffield
78b62be96f better handling of clearing the search 2020-02-24 22:18:04 +11:00
Jesse Duffield
1f5ccab1ce eagerload commits when searching 2020-02-24 22:18:04 +11:00
Jesse Duffield
46be280c92 support searching in side panels
For now we're just doing side panels, because it will take more work
to support this in the various main panel contexts
2020-02-24 22:18:04 +11:00
Jesse Duffield
2a5763a771 switch custom command keybinding to ':' 2020-02-24 22:04:39 +11:00
Jesse Duffield
370cec098b show diff stat 2020-02-24 09:20:50 +11:00
Dawid Dziurla
49a2f0191f tasks: don't use a function that requires Go 1.12 2020-02-24 09:09:27 +11:00
Jesse Duffield
fabdda0492 allow customizing background color in staging mode 2020-02-23 18:37:19 +11:00
Glenn Vriesman
6fc3290a05 Reflog: Use 20 sha digits instead of 7
Signed-off-by: Glenn Vriesman <glenn.vriesman@gmail.com>
2020-02-20 08:34:01 +11:00
Jesse Duffield
66e6369c28 allow fastforwarding the current branch 2020-02-18 23:07:38 +11:00
Jesse Duffield
0f0da9c32a fix wording 2020-02-16 09:57:49 +11:00
Jesse Duffield
0a69c1a02d add reset to reflog commit menu 2020-02-16 09:57:49 +11:00
Jesse Duffield
feaf98bd01 add reset to upstream option on files panel 2020-02-16 09:57:49 +11:00
Jesse Duffield
0fe9c15ce8 add mixed option to HEAD resetting, remove @{upstream} 2020-02-16 09:57:49 +11:00
Jesse Duffield
f528e12c83 allow resetting to tag 2020-02-16 09:57:49 +11:00
Jesse Duffield
8ca9f93ccf allow resetting to remote branch 2020-02-16 09:57:49 +11:00
Jesse Duffield
73d8064837 allow resetting to branch 2020-02-16 09:57:49 +11:00
Jesse Duffield
5b1f60b7eb refactor create reset menu logic 2020-02-16 09:57:49 +11:00
Jesse Duffield
2e1344f611 fix specs 2020-02-15 08:47:36 +11:00
Jesse Duffield
5b9996b16f remove old createMenu function 2020-02-15 08:47:36 +11:00
Jesse Duffield
6fdc1791e4 refactor stash options menu 2020-02-15 08:47:36 +11:00
Jesse Duffield
fd4f37b5c3 refactor git flow menu 2020-02-15 08:47:36 +11:00
Jesse Duffield
d76e8887e5 refactor patch options menu panel 2020-02-15 08:47:36 +11:00
Jesse Duffield
eb9134685a refactor rebase menu panel 2020-02-15 08:47:36 +11:00
Jesse Duffield
d929b84786 refactor recent repos menu panel 2020-02-15 08:47:36 +11:00
Jesse Duffield
8ef3297b11 refactor reflog reset options panel 2020-02-15 08:47:36 +11:00
Jesse Duffield
27c7aeb117 refactor workspace reset options panel 2020-02-15 08:47:36 +11:00
Jesse Duffield
c9714600e8 refactor commit reset menu 2020-02-15 08:47:36 +11:00
Jesse Duffield
665fdded14 continue refactor of menu panel 2020-02-15 08:47:36 +11:00
Jesse Duffield
814a0ea36f begin refactor of menu panel 2020-02-15 08:47:36 +11:00
Jesse Duffield
71e018a3dd get whole commit SHA from rebase commits 2020-02-13 18:10:14 +11:00
Jesse Duffield
efb26f8b60 refresh current branch graph when side panels refresh 2020-02-10 19:05:55 +11:00
Glenn Vriesman
d9eb6e2682 Fixed tests
Signed-off-by: Glenn Vriesman <glenn.vriesman@gmail.com>
2020-02-09 23:47:22 +11:00
Glenn Vriesman
b74107f2ba Use 8 instead of 7 digit long sha
Signed-off-by: Glenn Vriesman <glenn.vriesman@gmail.com>
2020-02-09 23:47:22 +11:00
Glenn Vriesman
0cd91a10c6 Increase internal sha size
This does not change the sha size that is displayed to the user

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

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

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

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

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

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

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

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

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

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

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

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

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

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

This fix adjusts the time format to be the `stampNano` format with "."
subsituted for ":".
2020-01-05 20:01:20 +00:00
David Chen
86b101c410 Merge branch 'master' into custom-keybindings 2020-01-04 08:12:36 +00:00
Dawid Dziurla
96ca7262e4 Merge pull request #572 from matejcik/master
make Ctrl+P visible
2020-01-03 15:13:48 +01:00
matejcik
0a31edecb6 make Ctrl+P visible 2020-01-03 15:09:59 +01:00
Dawid Dziurla
6ac6d142c0 Merge pull request #573 from jesseduffield/ubuntu-readme-update
README: update ubuntu info
2020-01-03 14:58:29 +01:00
Dawid Dziurla
5bb9900220 README: update ubuntu info 2020-01-03 14:49:51 +01:00
David Chen
f99a200db0 Merge branch 'master' into custom-keybindings 2019-12-14 10:57:46 -08:00
Dawid Dziurla
24e73cdd8b Merge pull request #564 from jesseduffield/update-deps
update dependencies
2019-12-13 11:36:05 +01:00
Dawid Dziurla
be8f589c32 update dependencies 2019-12-13 11:31:04 +01:00
Dawid Dziurla
3074ae99ea Merge pull request #560 from tim77/tim77-patch-1
Fedora installation instructions
2019-12-13 11:24:22 +01:00
Artem Polishchuk
873fe41ab3 Fedora installation instructions 2019-12-13 11:19:42 +01:00
David Chen
029de4ac86 re-position key names so that the menu will show 'enter' instead of 'ctrl-m', or 'esc' instead of 'ctrl-[' 2019-12-08 14:57:29 -08:00
David Chen
5f21f190b9 Merge branch 'master' into custom-keybindings 2019-12-08 14:40:11 -08:00
Jesse Duffield
dab78c8a63 stop the files panel from stealing focus whenever files are refreshed 2019-12-08 21:27:28 +11:00
David Chen
0d1230a959 added keybinding for fetchRemote 2019-12-07 09:26:17 -08:00
David Chen
c507e5f562 Merge branch 'master' into custom-keybindings 2019-12-07 09:19:43 -08:00
David Chen
63e353ad6a updated docs/examples 2019-12-06 22:48:19 -08:00
David Chen
7194dfa43c better error messages 2019-12-06 22:39:41 -08:00
David Chen
e425f1df87 suggested keybinding improvements 2019-12-06 22:36:52 -08:00
Jesse Duffield
3f4613feb0 allow fetching remotes with 'f' 2019-12-07 16:23:04 +11:00
Jesse Duffield
033c21754b fix commit message char count 2019-12-07 16:21:26 +11:00
David Chen
c89c35c6b3 bug fix: ctrl+combinations was not showing up in help menu 2019-12-06 10:26:39 -08:00
David Chen
1dbfea54bc better error handling 2019-12-04 19:16:47 -08:00
David Chen
0af8784707 added all possible custom keybindings to the documentation 2019-12-04 18:46:00 -08:00
David Chen
2ca5766f56 included default config file path 2019-12-04 18:35:29 -08:00
David Chen
a0b842204c exmaple keybindings for colemak users 2019-12-04 18:35:07 -08:00
David Chen
0a26050b47 fix 2019-12-04 18:33:00 -08:00
David Chen
c50ab9872d update documentation for custom keybindings 2019-12-04 18:18:28 -08:00
David Chen
fa6893fda9 feature: custom keybindings 2019-12-04 18:01:06 -08:00
Dawid Dziurla
710abded64 Merge pull request #554 from jesseduffield/alias-V-v-keybinding
keybindings: alias V -> v
2019-11-27 20:40:17 +01:00
Dawid Dziurla
1c38db1fc7 keybindings: alias V -> v 2019-11-27 20:37:07 +01:00
Jesse Duffield
339e1b5dcf lenient sorting of tags on startup 2019-11-26 21:39:40 +11:00
Jesse Duffield
7113ed73d4 support older versions of git when getting remote branches 2019-11-26 21:36:07 +11:00
Jesse Duffield
3dd1daacdc unescape another string 2019-11-21 22:17:18 +11:00
Jesse Duffield
bad06bb634 fix typo 2019-11-21 22:09:02 +11:00
Jesse Duffield
e18e81f5eb don't pass single commands directly to RunCommand (or equivalent function)
when it contains percentages.

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

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

package main

import "fmt"

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

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

swallow error when adding file to watcher
2019-11-14 22:22:47 +11:00
Jesse Duffield
7995d56a85 allow editing or opening a file while resolving merge conflicts 2019-11-14 22:22:47 +11:00
Jesse Duffield
30aed94aa8 update go git 2019-11-14 09:41:56 +11:00
Jesse Duffield
3b1d705473 show upstream branch for branch 2019-11-13 22:25:42 +11:00
Jesse Duffield
f43ba728e3 prompt to set upstream when pulling on untracked branch
prompt to set upstream when pulling on untracked branch
2019-11-13 21:36:16 +11:00
Dawid Dziurla
b662362570 Merge pull request #538 from jesseduffield/dawidd6-patch-1
README: fix typo
2019-11-12 23:46:27 +01:00
Dawid Dziurla
99ece6fc35 README: fix typo
945fccd211
2019-11-12 23:42:57 +01:00
Jesse Duffield
8287659584 fix up pty fork 2019-11-12 22:58:01 +11:00
Jesse Duffield
cf95ab9a28 go mod vendor 2019-11-12 22:58:01 +11:00
Jesse Duffield
b907c74386 remove go-getter 2019-11-12 22:58:01 +11:00
Dawid Dziurla
a68fb4fb8f Merge pull request #533 from JaanJah/patch-1
Fix typo in README `.zhsrc` -> `.zshrc`
2019-11-11 18:09:30 +01:00
jaanjahimees
945fccd211 Fix typo in README .zhsrc -> .zshrc
Fix typo in `Changing Directory On Exit` section
2019-11-11 18:07:11 +02:00
4740 changed files with 193945 additions and 315126 deletions

View File

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

1
.github/FUNDING.yml vendored
View File

@@ -1,5 +1,4 @@
# These are supported funding model platforms
github: [jesseduffield]
ko_fi: jesseduffield
custom: ['https://donorbox.org/lazygit']

9
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,9 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
allowed_updates:
- match:
update_type: "security"

28
.github/workflows/automerge.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: automerge
on:
pull_request:
types:
- labeled
- unlabeled
- synchronize
- opened
- edited
- ready_for_review
- reopened
- unlocked
pull_request_review:
types:
- submitted
check_suite:
types:
- completed
status: {}
jobs:
automerge:
runs-on: ubuntu-latest
steps:
- name: automerge
uses: "pascalgn/automerge-action@135f0bdb927d9807b5446f7ca9ecc2c51de03c4a"
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
MERGE_METHOD: rebase

31
.github/workflows/cd.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: Continuous Delivery
on:
push:
tags:
- 'v*'
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Unshallow repo
run: git fetch --prune --unshallow
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.16.x
- name: Run goreleaser
uses: goreleaser/goreleaser-action@v1
env:
GITHUB_TOKEN: ${{secrets.GITHUB_API_TOKEN}}
homebrew:
runs-on: macos-latest
steps:
- name: Bump Homebrew formula
uses: dawidd6/action-homebrew-bump-formula@v3
with:
token: ${{secrets.GITHUB_API_TOKEN}}
formula: lazygit

65
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,65 @@
name: Continuous Integration
on:
push:
branches:
- master
pull_request:
jobs:
ci:
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- windows-latest
name: ci - ${{matrix.os}}
runs-on: ${{matrix.os}}
env:
GOFLAGS: -mod=vendor
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.16.x
- name: Cache build
uses: actions/cache@v1
with:
path: ~/.cache/go-build
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-test
restore-keys: |
${{runner.os}}-go-
- name: Test code
run: |
bash ./test.sh
build:
runs-on: ubuntu-latest
env:
GOFLAGS: -mod=vendor
GOARCH: amd64
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.16.x
- name: Cache build
uses: actions/cache@v1
with:
path: ~/.cache/go-build
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-build
restore-keys: |
${{runner.os}}-go-
- name: Build linux binary
run: |
GOOS=linux go build
- name: Build windows binary
run: |
GOOS=windows go build
- name: Build darwin binary
run: |
GOOS=darwin go build

20
.github/workflows/lint.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: Lint
on: pull_request
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Lint
uses: golangci/golangci-lint-action@v2
with:
version: latest
- name: Format code
run: |
if [ $(find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;|wc -l) -gt 0 ]; then
find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;
exit 1
fi

10
.gitignore vendored
View File

@@ -25,4 +25,12 @@ lazygit
!.circleci/
!.github/
test/git_server/data
test/git_server/data
test/integration/*/actual/
test/integration/*/actual_remote/
test/integration/*/used_config/
# these sample hooks waste too much space
test/integration/*/expected/.git_keep/hooks/
test/integration/*/expected_remote/hooks/
!.git_keep/
lazygit.exe

View File

@@ -17,16 +17,16 @@ builds:
ldflags:
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.buildSource=binaryRelease
archive:
replacements:
darwin: Darwin
linux: Linux
windows: Windows
386: 32-bit
amd64: x86_64
format_overrides:
- goos: windows
format: zip
archives:
- replacements:
darwin: Darwin
linux: Linux
windows: Windows
386: 32-bit
amd64: x86_64
format_overrides:
- goos: windows
format: zip
checksum:
name_template: 'checksums.txt'
snapshot:
@@ -38,27 +38,26 @@ changelog:
- '^docs:'
- '^test:'
- '^bump'
brew:
# Reporitory to push the tap to.
github:
owner: jesseduffield
name: homebrew-lazygit
brews:
-
# Repository to push the tap to.
tap:
owner: jesseduffield
name: homebrew-lazygit
# Your app's homepage.
# Default is empty.
homepage: 'https://github.com/jesseduffield/lazygit/'
# Your app's homepage.
# Default is empty.
homepage: 'https://github.com/jesseduffield/lazygit/'
# Your app's description.
# Default is empty.
description: 'A simple terminal UI for git commands, written in Go'
# Your app's description.
# Default is empty.
description: 'A simple terminal UI for git commands, written in Go'
# # Packages your package depends on.
# dependencies:
# - git
# - zsh
# # Packages that conflict with your package.
# conflicts:
# - svn
# - bash
# test comment to see if goreleaser only releases on new commits
# # Packages your package depends on.
# dependencies:
# - git
# - zsh
# # Packages that conflict with your package.
# conflicts:
# - svn
# - bash

View File

@@ -1,14 +1,13 @@
# Contributing
♥ We love pull requests from everyone !
When contributing to this repository, please first discuss the change you wish
to make via issue, email, or any other method with the owners of this repository
before making a change.
before making a change.
## So all code changes happen through Pull Requests
Pull requests are the best way to propose changes to the codebase. We actively
welcome your pull requests:
@@ -21,15 +20,33 @@ welcome your pull requests:
7. Issue that pull request!
## Code of conduct
Please note by participating in this project, you agree to abide by the [code of conduct].
[code of conduct]: https://github.com/jesseduffield/lazygit/blob/master/CODE-OF-CONDUCT.md
## Any contributions you make will be under the MIT Software License
In short, when you submit code changes, your submissions are understood to be
under the same [MIT License](http://choosealicense.com/licenses/mit/) that
covers the project. Feel free to contact the maintainers if that's a concern.
## Report bugs using Github's [issues](https://github.com/jesseduffield/lazygit/issues)
We use GitHub issues to track public bugs. Report a bug by [opening a new
issue](https://github.com/jesseduffield/lazygit/issues/new); it's that easy!
## Updating Gocui
Sometimes you will need to make a change in the gocui fork (https://github.com/jesseduffield/gocui). Gocui is the package responsible for rending windows and handling user input. Here's the typical process to follow:
1. Make the changes in gocui inside the vendor directory so it's easy to test against lazygit
2. Copy the changes over to the actual gocui repo (clone it if you haven't already, and use the `awesome` branch, not `master`)
3. Raise a PR on the gocui repo with your changes
4. After that PR is merged, make a PR in lazygit bumping the gocui version. You can bump the version by running the following at the lazygit repo root:
```sh
./bump_gocui.sh
```
5. Raise a PR in lazygit with those changes

View File

@@ -1,15 +1,17 @@
# run with:
# docker build -t lazygit .
# docker run -it lazygit:latest /bin/sh -l
# docker run -it lazygit:latest /bin/sh
FROM golang:1.13-alpine3.10
FROM golang:1.14-alpine3.11
WORKDIR /go/src/github.com/jesseduffield/lazygit/
COPY ./ .
RUN CGO_ENABLED=0 GOOS=linux go build
FROM alpine:3.10
FROM alpine:3.11
RUN apk add -U git xdg-utils
WORKDIR /go/src/github.com/jesseduffield/lazygit/
COPY --from=0 /go/src/github.com/jesseduffield/lazygit /go/src/github.com/jesseduffield/lazygit
COPY --from=0 /go/src/github.com/jesseduffield/lazygit/lazygit /bin/
RUN echo "alias gg=lazygit" >> ~/.profile
ENTRYPOINT [ "lazygit" ]

218
README.md
View File

@@ -1,32 +1,65 @@
# lazygit [![CircleCI](https://circleci.com/gh/jesseduffield/lazygit.svg?style=svg)](https://circleci.com/gh/jesseduffield/lazygit) [![codecov](https://codecov.io/gh/jesseduffield/lazygit/branch/master/graph/badge.svg)](https://codecov.io/gh/jesseduffield/lazygit) [![Go Report Card](https://goreportcard.com/badge/github.com/jesseduffield/lazygit)](https://goreportcard.com/report/github.com/jesseduffield/lazygit) [![GolangCI](https://golangci.com/badges/github.com/jesseduffield/lazygit.svg)](https://golangci.com) [![GoDoc](https://godoc.org/github.com/jesseduffield/lazygit?status.svg)](http://godoc.org/github.com/jesseduffield/lazygit) [![GitHub tag](https://img.shields.io/github/tag/jesseduffield/lazygit.svg)]()
<p align="center">
<img src="https://i.imgur.com/oYB7Cj8.png">
</p>
A simple terminal UI for git commands, written in Go with the [gocui](https://github.com/jroimartin/gocui 'gocui') library.
![CI](https://github.com/jesseduffield/lazygit/workflows/Continuous%20Integration/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/jesseduffield/lazygit)](https://goreportcard.com/report/github.com/jesseduffield/lazygit) [![GolangCI](https://golangci.com/badges/github.com/jesseduffield/lazygit.svg)](https://golangci.com) [![GoDoc](https://godoc.org/github.com/jesseduffield/lazygit?status.svg)](http://godoc.org/github.com/jesseduffield/lazygit) [![GitHub tag](https://img.shields.io/github/tag/jesseduffield/lazygit.svg)]() [![TODOs](https://badgen.net/https/api.tickgit.com/badgen/github.com/jesseduffield/lazygit)](https://www.tickgit.com/browse?repo=github.com/jesseduffield/lazygit)
Rant time: You've heard it before, git is _powerful_, but what good is that power when everything is so damn hard to do? Interactive rebasing requires you to edit a goddamn TODO file in your editor? *Are you kidding me?* To stage part of a file you need to use a command line program stepping through each hunk and if a hunk can't be split down any further but contains code you don't want to stage, bad luck? *Are you KIDDING me?!* Sometimes you get asked to stash your changes when switching branches only to realise that after you switch and unstash that there weren't even any conflicts and it would have been fine to just checkout the branch directly? *YOU HAVE GOT TO BE KIDDING ME!*
A simple terminal UI for git commands, written in Go with the [gocui](https://github.com/jroimartin/gocui "gocui") library.
Rant time: You've heard it before, git is _powerful_, but what good is that power when everything is so damn hard to do? Interactive rebasing requires you to edit a goddamn TODO file in your editor? *Are you kidding me?* To stage part of a file you need to use a command line program to step through each hunk and if a hunk can't be split down any further but contains code you don't want to stage, you have to edit an arcane patch file _by hand_? *Are you KIDDING me?!* Sometimes you get asked to stash your changes when switching branches only to realise that after you switch and unstash that there weren't even any conflicts and it would have been fine to just checkout the branch directly? *YOU HAVE GOT TO BE KIDDING ME!*
If you're a mere mortal like me and you're tired of hearing how powerful git is when in your daily life it's a powerful pain in your ass, lazygit might be for you.
![Gif](/docs/resources/lazygit-example.gif)
![Gif](../assets/staging.gif)
- [Installation](https://github.com/jesseduffield/lazygit#installation)
- [Usage](https://github.com/jesseduffield/lazygit#usage),
[Keybindings](/docs/keybindings)
- [Cool Features](https://github.com/jesseduffield/lazygit#cool-features)
- [Contributing](https://github.com/jesseduffield/lazygit#contributing)
- [Video Tutorial](https://youtu.be/VDXvbHZYeKY)
- [Rebase Magic Video Tutorial](https://youtu.be/4XaToVut_hs)
- [Twitch Stream](https://www.twitch.tv/jesseduffield)
## Table of contents
[<img src="https://i.imgur.com/sVEktDn.png">](https://youtu.be/CPLdltN7wgE)
- [Installation](#installation)
- [Binary releases](#binary-releases)
- [Homebrew](#homebrew)
- [MacPorts](#macports)
- [Void Linux](#void-linux)
- [Scoop (Windows)](#scoop-windows)
- [Arch Linux](#arch-linux)
- [Fedora and CentOS 7](#fedora-and-centos-7)
- [Solus Linux](#solus-linux)
- [FreeBSD](#freebsd)
- [Conda](#conda)
- [Go](#go)
- [Chocolatey (Windows)](#chocolatey-windows)
- [Manual](#manual)
- [Usage](#usage)
- [Keybindings](#keybindings)
- [Changing directory on exit](#changing-directory-on-exit)
- [Undo/Redo](#undoredo)
- [Configuration](#configuration)
- [Custom pagers](#configuration)
- [Custom commands](#configuration)
- [Tutorials](#tutorials)
- [Cool Features](#cool-features)
- [Contributing](#contributing)
- [Donate](#donate)
- [Alternatives](#alternatives)
Github Sponsors is matching all donations dollar-for-dollar for 12 months so if you're feeling generous consider [sponsoring me](https://github.com/sponsors/jesseduffield)
[<img src="https://i.imgur.com/sVEktDn.png">](https://youtu.be/CPLdltN7wgE)
## Installation
### Binary Releases
For Windows, Mac OS(10.12+) or Linux, you can download a binary release [here](../../releases).
### Homebrew
Normally the lazygit formula can be found in the Homebrew core but we suggest you tap our formula to get the frequently updated one. It works with Linux, too.
Tap:
```
brew install jesseduffield/lazygit/lazygit
```
@@ -37,13 +70,20 @@ Core:
brew install lazygit
```
### MacPorts
Latest version built from github releases.
Tap:
```
sudo port install lazygit
```
### Ubuntu
Packages for Ubuntu 16.04, 18.04 and 18.10 are available via [Launchpad PPA](https://launchpad.net/~lazygit-team).
**Deprecated**: will no longer receive updates.
**Release builds**
Built from git tags. Supposed to be more stable.
Packages for Ubuntu are available via [Launchpad PPA](https://launchpad.net/~lazygit-team).
```sh
sudo add-apt-repository ppa:lazygit-team/release
@@ -51,16 +91,6 @@ sudo apt-get update
sudo apt-get install lazygit
```
**Daily builds**
Built from master branch once in 24 hours (or more sometimes).
```sh
sudo add-apt-repository ppa:lazygit-team/daily
sudo apt-get update
sudo apt-get install lazygit
```
### Void Linux
Packages for Void Linux are available in the distro repo
@@ -71,31 +101,61 @@ They follow upstream latest releases
sudo xbps-install -S lazygit
```
### Scoop (Windows)
You can install `lazygit` using [scoop](https://scoop.sh/). It's in the `extras` bucket:
```sh
# Add the extras bucket
scoop bucket add extras
# Install lazygit
scoop install lazygit
```
### Arch Linux
Packages for Arch Linux are available via AUR (Arch User Repository).
Packages for Arch Linux are available via pacman and AUR (Arch User Repository).
There are two packages. The stable one which is built with the latest release
and the git version which builds from the most recent commit.
- Stable: https://aur.archlinux.org/packages/lazygit/
- Development: https://aur.archlinux.org/packages/lazygit-git/
- Stable: `sudo pacman -S lazygit`
- Development: <https://aur.archlinux.org/packages/lazygit-git/>
Instruction of how to install AUR content can be found here:
https://wiki.archlinux.org/index.php/Arch_User_Repository
<https://wiki.archlinux.org/index.php/Arch_User_Repository>
### Fedora and CentOS 7
Packages for Fedora and CentOS 7 are available via [Copr](https://copr.fedorainfracloud.org/coprs/atim/lazygit/) (Cool Other Package Repo).
```sh
sudo dnf copr enable atim/lazygit -y
sudo dnf install lazygit
```
### Solus Linux
```sh
sudo eopkg install lazygit
```
### FreeBSD
```sh
pkg install lazygit
```
### Conda
Released versions are available for different platforms, see https://anaconda.org/conda-forge/lazygit
Released versions are available for different platforms, see <https://anaconda.org/conda-forge/lazygit>
```sh
conda install -c conda-forge lazygit
```
### Binary Release (Windows/Linux/OSX)
You can download a binary release [here](https://github.com/jesseduffield/lazygit/releases).
### Go
```sh
@@ -108,20 +168,46 @@ 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).
### Chocolatey (Windows)
You can install `lazygit` using [Chocolatey](https://chocolatey.org/):
```sh
choco install lazygit
```
### Manual
You'll need to [install Go](https://golang.org/doc/install)
```
git clone https://github.com/jesseduffield/lazygit.git
cd lazygit
go install
```
You can also use `go run main.go` to compile and run in one go (pun definitely intended)
## Usage
Call `lazygit` in your terminal inside a git repository. If you want, you can
Call `lazygit` in your terminal inside a git repository.
```sh
$ lazygit
```
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 video tutorial [here](https://youtu.be/VDXvbHZYeKY).
- Rebase Magic tutorial [here](https://youtu.be/4XaToVut_hs)
- List of keybindings
[here](/docs/keybindings).
## Changing Directory On Exit
### Keybindings
You can check out the list of keybindings [here](/docs/keybindings).
### Changing Directory On Exit
If you change repos in lazygit and want your shell to change directory into that repo on exiting lazygit, add this to your `~/.zshrc` (or other rc file):
If you change repos in lazygit and want your shell to change directory into that repo on exiting lazygit, add this to your `~./zhsrc` (or other rc file):
```
lg()
{
@@ -135,8 +221,34 @@ lg()
fi
}
```
Then `source ~/.zshrc` and from now on when you call `lg` and exit you'll switch directories to whatever you were in inside lazyigt. To override this behaviour you can exit using `shift+Q` rather than just `q`.
### Undo/Redo
See the [docs](/docs/Undoing.md)
## Configuration
Check out the [configuration docs](docs/Config.md).
### Custom Pagers
See the [docs](docs/Custom_Pagers.md)
### Custom Commands
If lazygit is missing a feature, there's a good chance you can implement it yourself with a custom command!
See the [docs](docs/Custom_Command_Keybindings.md)
## Tutorials
- [Video Tutorial](https://youtu.be/VDXvbHZYeKY)
- [Rebase Magic Video Tutorial](https://youtu.be/4XaToVut_hs)
- [Twitch Stream](https://www.twitch.tv/jesseduffield)
## Cool features
- Adding files easily
@@ -148,29 +260,30 @@ Then `source ~/.zshrc` and from now on when you call `lg` and exit you'll switch
### Resolving merge conflicts
![Gif](/docs/resources/resolving-merge-conflicts.gif)
![Gif](../assets/resolving-merge-conflicts.gif)
### Interactive Rebasing
![Interactive Rebasing](/docs/resources/interactive-rebase.png)
![Interactive Rebasing](../assets/rebase.gif)
## 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)
[![Slack](../assets/slack_rgb.png)](https://join.slack.com/t/lazygit/shared_invite/zt-5bo2clzo-hB8ZTVN5dWUCqj5QFiQVLA)
### Debugging Locally
Run `lazygit --debug` in one terminal tab and `lazygit --logs` in another to view the program and its log output side by side
## Donate
If you would like to support the development of lazygit, consider [sponsoring me](https://github.com/sponsors/jesseduffield) (github is matching all donations dollar-for-dollar for 12 months)
## Work in progress
## FAQ
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).
### I'm struggling to see the selected line
see [here](https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#struggling-to-see-selected-line)
## Social
@@ -182,4 +295,5 @@ If you want to see what I (Jesse) am up to in terms of development, follow me on
If you find that lazygit doesn't quite satisfy your requirements, these may be a better fit:
- [GitUI](https://github.com/Extrawurst/gitui)
- [tig](https://github.com/jonas/tig)

5
bump_gocui.sh Executable file
View File

@@ -0,0 +1,5 @@
# Go's proxy servers are not very up-to-date so that's why we use `GOPROXY=direct`
# We specify the `awesome` branch to avoid the default behaviour of looking for a semver tag.
GOPROXY=direct go get -u github.com/jesseduffield/gocui@awesome && go mod vendor
# Note to self if you ever want to fork a repo be sure to use this same approach: it's important to use the branch name (e.g. master)

View File

@@ -1,75 +1,299 @@
# User Config:
# User Config
## Default:
Default path for the config file:
- Linux: `~/.config/lazygit/config.yml`
- MacOS: `~/Library/Application Support/lazygit/config.yml`
- Windows: `%APPDATA%\lazygit\config.yml`
For old installations (slightly embarrassing: I didn't realise at the time that you didn't need to supply a vendor name to the path so I just used my name):
- Linux: `~/.config/jesseduffield/lazygit/config.yml`
- MacOS: `~/Library/Application Support/jesseduffield/lazygit/config.yml`
- Windows: `%APPDATA%\jesseduffield\lazygit\config.yml`
## Default
```yaml
gui:
# stuff relating to the UI
scrollHeight: 2 # how many lines you scroll by
scrollPastBottom: true # enable scrolling past the bottom
theme:
lightTheme: false # For terminals with a light background
activeBorderColor:
- white
- bold
inactiveBorderColor:
- white
optionsTextColor:
- blue
commitLength:
show: true
mouseEvents: true
git:
merging:
# only applicable to unix users
manualCommit: false
skipHookPrefix: WIP
autoFetch: true
update:
method: prompt # can be: prompt | background | never
days: 14 # how often an update is checked for
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
confirmOnQuit: false
gui:
# stuff relating to the UI
scrollHeight: 2 # how many lines you scroll by
scrollPastBottom: true # enable scrolling past the bottom
sidePanelWidth: 0.3333 # number from 0 to 1
expandFocusedSidePanel: false
mainPanelSplitMode: 'flexible' # one of 'horizontal' | 'flexible' | 'vertical'
language: 'auto' # one of 'auto' | 'en' | 'zh' | 'pl' | 'nl'
theme:
lightTheme: false # For terminals with a light background
activeBorderColor:
- white
- bold
inactiveBorderColor:
- green
optionsTextColor:
- blue
selectedLineBgColor:
- default
selectedRangeBgColor:
- blue
cherryPickedCommitBgColor:
- blue
cherryPickedCommitFgColor:
- cyan
commitLength:
show: true
mouseEvents: true
skipUnstageLineWarning: false
skipStashWarning: true
showFileTree: false # for rendering changes files in a tree format
showListFooter: true # for seeing the '5 of 20' message in list panels
showRandomTip: true
showCommandLog: true
commandLogSize: 8
git:
paging:
colorArg: always
useConfig: false
merging:
# only applicable to unix users
manualCommit: false
# extra args passed to `git merge`, e.g. --no-ff
args: ''
skipHookPrefix: WIP
autoFetch: true
branchLogCmd: 'git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --'
allBranchesLogCmd: 'git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium'
overrideGpg: false # prevents lazygit from spawning a separate process when using GPG
disableForcePushing: false
parseEmoji: false
os:
editCommand: '' # see 'Configuring File Editing' section
editCommandTemplate: '{{editor}} {{filename}}'
openCommand: ''
refresher:
refreshInterval: 10 # file/submodule refresh interval in seconds
fetchInterval: 60 # re-fetch interval in seconds
update:
method: prompt # can be: prompt | background | never
days: 14 # how often an update is checked for
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
confirmOnQuit: false
# determines whether hitting 'esc' will quit the application when there is nothing to cancel/close
quitOnTopLevelReturn: false
disableStartupPopups: false
notARepository: 'prompt' # one of: 'prompt' | 'create' | 'skip'
keybinding:
universal:
quit: 'q'
quit-alt1: '<c-c>' # alternative/alias of quit
return: '<esc>' # return to previous menu, will quit if there's nowhere to return
quitWithoutChangingDirectory: 'Q'
togglePanel: '<tab>' # goto the next panel
prevItem: '<up>' # go one line up
nextItem: '<down>' # go one line down
prevItem-alt: 'k' # go one line up
nextItem-alt: 'j' # go one line down
prevPage: ',' # go to next page in list
nextPage: '.' # go to previous page in list
gotoTop: '<' # go to top of list
gotoBottom: '>' # go to bottom of list
prevBlock: '<left>' # goto the previous block / panel
nextBlock: '<right>' # goto the next block / panel
prevBlock-alt: 'h' # goto the previous block / panel
nextBlock-alt: 'l' # goto the next block / panel
jumpToBlock: ["1", "2", "3", "4", "5"] # goto the Nth block / panel
nextMatch: 'n'
prevMatch: 'N'
optionMenu: 'x' # show help menu
optionMenu-alt1: '?' # show help menu
select: '<space>'
goInto: '<enter>'
openRecentRepos: '<c-r>'
confirm: '<enter>'
confirm-alt1: 'y'
remove: 'd'
new: 'n'
edit: 'e'
openFile: 'o'
scrollUpMain: '<pgup>' # main panel scroll up
scrollDownMain: '<pgdown>' # main panel scroll down
scrollUpMain-alt1: 'K' # main panel scroll up
scrollDownMain-alt1: 'J' # main panel scroll down
scrollUpMain-alt2: '<c-u>' # main panel scroll up
scrollDownMain-alt2: '<c-d>' # main panel scroll down
executeCustomCommand: ':'
createRebaseOptionsMenu: 'm'
pushFiles: 'P'
pullFiles: 'p'
refresh: 'R'
createPatchOptionsMenu: '<c-p>'
nextTab: ']'
prevTab: '['
nextScreenMode: '+'
prevScreenMode: '_'
undo: 'z'
redo: '<c-z>'
filteringMenu: '<c-s>'
diffingMenu: 'W'
diffingMenu-alt: '<c-e>' # deprecated
copyToClipboard: '<c-o>'
submitEditorText: '<enter>'
appendNewline: '<a-enter>'
extrasMenu: '@'
toggleWhitespaceInDiffView: '<c-w>'
status:
checkForUpdate: 'u'
recentRepos: '<enter>'
files:
commitChanges: 'c'
commitChangesWithoutHook: 'w' # commit changes without pre-commit hook
amendLastCommit: 'A'
commitChangesWithEditor: 'C'
ignoreFile: 'i'
refreshFiles: 'r'
stashAllChanges: 's'
viewStashOptions: 'S'
toggleStagedAll: 'a' # stage/unstage all
viewResetOptions: 'D'
fetch: 'f'
toggleTreeView: '`'
branches:
createPullRequest: 'o'
viewPullRequestOptions: 'O'
checkoutBranchByName: 'c'
forceCheckoutBranch: 'F'
rebaseBranch: 'r'
mergeIntoCurrentBranch: 'M'
viewGitFlowOptions: 'i'
fastForward: 'f' # fast-forward this branch from its upstream
pushTag: 'P'
setUpstream: 'u' # set as upstream of checked-out branch
fetchRemote: 'f'
commits:
squashDown: 's'
renameCommit: 'r'
renameCommitWithEditor: 'R'
viewResetOptions: 'g'
markCommitAsFixup: 'f'
createFixupCommit: 'F' # create fixup commit for this commit
squashAboveCommits: 'S'
moveDownCommit: '<c-j>' # move commit down one
moveUpCommit: '<c-k>' # move commit up one
amendToCommit: 'A'
pickCommit: 'p' # pick commit (when mid-rebase)
revertCommit: 't'
cherryPickCopy: 'c'
cherryPickCopyRange: 'C'
pasteCommits: 'v'
tagCommit: 'T'
checkoutCommit: '<space>'
resetCherryPick: '<c-R>'
copyCommitMessageToClipboard: '<c-y>'
stash:
popStash: 'g'
commitFiles:
checkoutCommitFile: 'c'
main:
toggleDragSelect: 'v'
toggleDragSelect-alt: 'V'
toggleSelectHunk: 'a'
pickBothHunks: 'b'
submodules:
init: 'i'
update: 'u'
bulkMenu: 'b'
```
## Platform Defaults:
## Platform Defaults
### Windows:
### Windows
```yaml
os:
openCommand: 'cmd /c "start "" {{filename}}"'
os:
openCommand: 'start "" {{filename}}'
```
### Linux:
### Linux
```yaml
os:
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'
os:
openCommand: 'xdg-open {{filename}} >/dev/null'
```
### OSX:
### OSX
```yaml
os:
openCommand: 'open {{filename}}'
os:
openCommand: 'open {{filename}}'
```
### Recommended Config Values:
### Configuring File Editing
Lazygit will edit a file with the first set editor in the following:
1. config.yaml
```yaml
os:
editCommand: 'vim' # as an example
```
2. \$(git config core.editor)
3. \$GIT_EDITOR
4. \$VISUAL
5. \$EDITOR
6. \$(which vi)
Lazygit will log an error if none of these options are set.
You can specify a line number you are currently at when in the line-by-line mode.
```yaml
os:
editCommand: 'vim'
editCommandTemplate: '{{editor}} +{{line}} {{filename}}'
```
or
```yaml
os:
editCommand: 'code'
editCommandTemplate: '{{editor}} --goto {{filename}}:{{line}}'
```
`{{editor}}` in `editCommandTemplate` is replaced with the value of `editCommand`.
### Overriding default config file location
To override the default config directory, use `$CONFIG_DIR="~/.config/lazygit"`. This directory contains the config file in addition to some other files lazygit uses to keep track of state across sessions.
To override the individual config file used, use the `--use-config-file` arg or the `LG_CONFIG_FILE` env var.
If you want to merge a specific config file into a more general config file, perhaps for the sake of setting some theme-specific options, you can supply a list of comma-separated config file paths, like so:
```sh
lazygit --use-config-file=~/.base_lg_conf,~/.light_theme_lg_conf
or
LG_CONFIG_FILE="~/.base_lg_conf,~/.light_theme_lg_conf" lazygit
```
### Recommended Config Values
for users of VSCode
```yaml
os:
openCommand: 'code -r {{filename}}'
os:
openCommand: 'code -rg {{filename}}'
```
## Color Attributes:
## Color Attributes
For color attributes you can choose an array of attributes (with max one color attribute)
The available attributes are:
- default
**Colors**
- black
- red
- green
@@ -78,25 +302,169 @@ The available attributes are:
- magenta
- cyan
- white
- '#ff00ff'
**Modifiers**
- bold
- default
- reverse # useful for high-contrast
- underline
## Light terminal theme:
## Light terminal theme
If you have issues with a light terminal theme where you can't read / see the text add these settings
```yaml
gui:
theme:
lightTheme: true
activeBorderColor:
- black
- bold
inactiveBorderColor:
- black
gui:
theme:
lightTheme: true
activeBorderColor:
- black
- bold
inactiveBorderColor:
- black
selectedLineBgColor:
- default
```
## Example Coloring:
## Struggling to see selected line
![border example](/docs/resources/colored-border-example.png)
If you struggle to see the selected line I recommend using the reverse attribute on selected lines like so:
```yaml
gui:
theme:
selectedLineBgColor:
- reverse
selectedRangeBgColor:
- reverse
```
The following has also worked for a couple of people:
```yaml
gui:
theme:
activeBorderColor:
- white
- bold
inactiveBorderColor:
- white
selectedLineBgColor:
- reverse
- blue
```
Alternatively you may have bold fonts disabled in your terminal, in which case enabling bold fonts should solve the problem.
If you're still having trouble please raise an issue.
## Example Coloring
![border example](../../assets/colored-border-example.png)
## Keybindings
For all possible keybinding options, check [Custom_Keybindings.md](https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md)
### Example Keybindings For Colemak Users
```yaml
keybinding:
universal:
prevItem-alt: 'u'
nextItem-alt: 'e'
prevBlock-alt: 'n'
nextBlock-alt: 'i'
nextMatch: '='
prevMatch: '-'
new: 'k'
edit: 'o'
openFile: 'O'
scrollUpMain-alt1: 'U'
scrollDownMain-alt1: 'E'
scrollUpMain-alt2: '<c-u>'
scrollDownMain-alt2: '<c-e>'
undo: 'l'
redo: '<c-r>'
diffingMenu: 'M'
filteringMenu: '<c-f>'
files:
ignoreFile: 'I'
commits:
moveDownCommit: '<c-e>'
moveUpCommit: '<c-u>'
branches:
viewGitFlowOptions: 'I'
setUpstream: 'U'
```
## Custom pull request URLs
Some git provider setups (e.g. on-premises GitLab) can have distinct URLs for git-related calls and
the web interface/API itself. To work with those, Lazygit needs to know where it needs to create
the pull request. You can do so on your `config.yml` file using the following syntax:
```yaml
services:
'<gitDomain>': '<provider>:<webDomain>'
```
Where:
- `gitDomain` stands for the domain used by git itself (i.e. the one present on clone URLs), e.g. `git.work.com`
- `provider` is one of `github`, `bitbucket` or `gitlab`
- `webDomain` is the URL where your git service exposes a web interface and APIs, e.g. `gitservice.work.com`
## Predefined commit message prefix
In situations where certain naming pattern is used for branches and commits, pattern can be used to populate
commit message with prefix that is parsed from the branch name.
Example:
- Branch name: feature/AB-123
- Commit message: [AB-123] Adding feature
```yaml
git:
commitPrefixes:
my_project: # This is repository folder name
pattern: "^\\w+\\/(\\w+-\\w+).*"
replace: '[$1] '
```
## Custom git log command
You can override the `git log` command that's used to render the log of the selected branch like so:
```
git:
branchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium --oneline {{branchName}} --"
```
Result:
![](https://i.imgur.com/Nibq35B.png)
## Launching not in a repository behaviour
By default, when launching lazygit from a directory that is not a repository,
you will be prompted to choose if you would like to initialize a repo. You can
override this behaviour in the config with one of the following:
```yaml
# for default prompting behaviour
notARepository: 'prompt'
```
```yaml
# to skip and initialize a new repo
notARepository: 'create'
```
```yaml
# to skip without creating a new repo
notARepository: 'skip'
```

View File

@@ -0,0 +1,168 @@
# Custom Command Keybindings
You can add custom command keybindings in your config.yml (accessible by pressing 'o' on the status panel from within lazygit) like so:
```yml
customCommands:
- key: '<c-r>'
command: 'hub browse -- "commit/{{.SelectedLocalCommit.Sha}}"'
context: 'commits'
- key: 'a'
command: "git {{if .SelectedFile.HasUnstagedChanges}} add {{else}} reset {{end}} {{.SelectedFile.Name}}"
context: 'files'
description: 'toggle file staged'
- key: 'C'
command: "git commit"
context: 'global'
subprocess: true
- key: 'n'
prompts:
- type: 'menu'
title: 'What kind of branch is it?'
options:
- name: 'feature'
description: 'a feature branch'
value: 'feature'
- name: 'hotfix'
description: 'a hotfix branch'
value: 'hotfix'
- name: 'release'
description: 'a release branch'
value: 'release'
- type: 'input'
title: 'What is the new branch name?'
initialValue: ''
command: "git flow {{index .PromptResponses 0}} start {{index .PromptResponses 1}}"
context: 'localBranches'
loadingText: 'creating branch'
- key : 'r'
description: 'Checkout a remote branch as FETCH_HEAD'
command: "git fetch {{index .PromptResponses 0}} {{index .PromptResponses 1}} && git checkout FETCH_HEAD"
context: 'remotes'
prompts:
- type: 'input'
title: 'Remote:'
initialValue: "{{index .SelectedRemote.Name }}"
- type: 'menuFromCommand'
title: 'Remote branch:'
command: 'git branch -r --list {{index .PromptResponses 0}}/*'
filter: '.*{{index .PromptResponses 0}}/(?P<branch>.*)'
valueFormat: '{{ .branch }}'
labelFormat: '{{ .branch | green }}'
```
Looking at the command assigned to the 'n' key, here's what the result looks like:
![](../../assets/custom-command-keybindings.gif)
Custom command keybindings will appear alongside inbuilt keybindings when you view the options menu by pressing 'x':
![](https://i.imgur.com/QB21FPx.png)
For a given custom command, here are the allowed fields:
| _field_ | _description_ | required |
|-----------------|----------------------|-|
| key | the key to trigger the command. Use a single letter or one of the values from [here](https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md) | yes |
| command | the command to run | yes |
| context | the context in which to listen for the key (see below) | yes |
| subprocess | whether you want the command to run in a subprocess (necessary if you want to view the output of the command or provide user input) | no |
| prompts | a list of prompts that will request user input before running the final command | no |
| loadingText | text to display while waiting for command to finish | no |
| description | text to display in the keybindings menu that appears when you press 'x' | no |
### Contexts
The permitted contexts are:
| _context_ | _description_ |
| -------------- | -------------------------------------------------------------------------------------------------------- |
| status | the 'Status' tab |
| files | the 'Files' tab |
| localBranches | the 'Local Branches' tab |
| remotes | the 'Remotes' tab |
| remoteBranches | the context you get when pressing enter on a remote in the remotes tab |
| tags | the 'Tags' tab |
| commits | the 'Commits' tab |
| reflogCommits | the 'Reflog' tab |
| subCommits | the context you see when pressing enter on a branch |
| commitFiles | the context you see when pressing enter on a commit or stash entry (warning, might be renamed in future) |
| stash | the 'Stash' tab |
| global | this keybinding will take affect everywhere |
### Prompts
The permitted prompt fields are:
| _field_ | _description_ | _required_ |
| ------------ | -------------------------------------------------------------------------------- | ---------- |
| type | one of 'input' or 'menu' | yes |
| title | the title to display in the popup panel | no |
| initialValue | (only applicable to 'input' prompts) the initial value to appear in the text box | no |
| options | (only applicable to 'menu' prompts) the options to display in the menu | no |
| command | (only applicable to 'menuFromCommand' prompts) the command to run to generate | yes |
| | menu options | |
| filter | (only applicable to 'menuFromCommand' prompts) the regexp to run specifying | yes |
| | groups which are going to be kept from the command's output | |
| valueFormat | (only applicable to 'menuFromCommand' prompts) how to format matched groups from | yes |
| | the filter to construct a menu item's value (What gets appended to prompt | |
| | responses when the item is selected). You can use named groups, | |
| | or `{{ .group_GROUPID }}`. | |
| | PS: named groups keep first match only | |
| labelFormat | (only applicable to 'menuFromCommand' prompts) how to format matched groups from | no |
| | the filter to construct the item's label (What's shown on screen). You can use | |
| | named groups, or `{{ .group_GROUPID }}`. You can also color each match with | |
| | `{{ .group_GROUPID | colorname }}` (Color names from | |
| | [here](https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md)) | |
| | If `labelFormat` is not specified, `valueFormat` is shown instead. | |
| | PS: named groups keep first match only | |
The permitted option fields are:
| _field_ | _description_ | _required_ |
|-----------------|----------------------|-|
| name | the string which will appear first on the line | no |
| description | the string which will appear second on the line | no |
| value | the value that will be stored in `.PromptResponses` if the option is selected | yes |
If an option has no name the value will be displayed to the user in place of the name, so you're allowed to only include the value like so:
```yml
prompts:
- type: 'menu'
title: 'What kind of branch is it?'
options:
- value: 'feature'
- value: 'hotfix'
- value: 'release'
```
### Placeholder values
Your commands can contain placeholder strings using Go's [template syntax](https://jan.newmarch.name/go/template/chapter-template.html). The template syntax is pretty powerful, letting you do things like conditionals if you want, but for the most part you'll simply want to be accessing the fields on the following objects:
```
SelectedLocalCommit
SelectedReflogCommit
SelectedSubCommit
SelectedFile
SelectedLocalBranch
SelectedRemoteBranch
SelectedRemote
SelectedTag
SelectedStashEntry
SelectedCommitFile
CheckedOutBranch
```
To see what fields are available on e.g. the `SelectedFile`, see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/commands/models/file.go) (all the modelling lives in the same directory). Note that the custom commands feature does not guarantee backwards compatibility (until we hit lazygit version 1.0 of course) which means a field you're accessing on an object may no longer be available from one release to the next. Typically however, all you'll need is `{{.SelectedFile.Name}}`, `{{.SelectedLocalCommit.Sha}}` and `{{.SelectedBranch.Name}}`. In the future we will likely introduce a tighter interface that exposes a limited set of fields for each model.
### Keybinding collisions
If your custom keybinding collides with an inbuilt keybinding that is defined for the same context, only the custom keybinding will be executed. This also applies to the global context. However, one caveat is that if you have a custom keybinding defined on the global context for some key, and there is an in-built keybinding defined for the same key and for a specific context (say the 'files' context), then the in-built keybinding will take precedence. See how to change in-built keybindings [here](https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#keybindings)
### Debugging
If you want to verify that your command actually does what you expect, you can wrap it in an 'echo' call and set `subprocess: true` so that it doesn't actually execute the command but you can see how the placeholders were resolved. Alternatively you can run lazygit in debug mode with `lazygit --debug` and in another terminal window run `lazygit --logs` to see which commands are actually run
### More Examples
See the [wiki](https://github.com/jesseduffield/lazygit/wiki/Custom-Commands-Compendium) page for more examples, and feel free to add your own custom commands to this page so others can benefit!

64
docs/Custom_Pagers.md Normal file
View File

@@ -0,0 +1,64 @@
# Custom Pagers
Lazygit supports custom pagers, [configured](/docs/Config.md) in the config.yml file (which can be opened by pressing `o` in the Status panel).
Support does not extend to Windows users, because we're making use of a package which doesn't have Windows support.
## Default:
```yaml
git:
paging:
colorArg: always
useConfig: false
```
the `colorArg` key is for whether you want the `--color=always` arg in your `git diff` command. Some pagers want it set to `always`, others want it set to `never`.
## Delta:
```yaml
git:
paging:
colorArg: always
pager: delta --dark --paging=never
```
![](https://i.imgur.com/QJpQkF3.png)
## Diff-so-fancy
```yaml
git:
paging:
colorArg: always
pager: diff-so-fancy
```
![](https://i.imgur.com/rjH1TpT.png)
## ydiff
```yaml
gui:
sidePanelWidth: 0.2 # gives you more space to show things side-by-side
git:
paging:
colorArg: never
pager: ydiff -p cat -s --wrap --width={{columnWidth}}
```
![](https://i.imgur.com/vaa8z0H.png)
Be careful with this one, I think the homebrew and pip versions are behind master. I needed to directly download the ydiff script to get the no-pager functionality working.
## Using git config
```yaml
git:
paging:
colorArg: always
useConfig: true
```
If you set `useConfig: true`, lazygit will use whatever pager is specified in `$GIT_PAGER`, `$PAGER`, or your *git config*. If the pager ends with something like ` | less` we will strip that part out, because less doesn't play nice with our rendering approach. If the custom pager uses less under the hood, that will also break rendering (hence the `--paging=never` flag for the `delta` pager).

88
docs/Integration_Tests.md Normal file
View File

@@ -0,0 +1,88 @@
# How To Make And Run Integration Tests For lazygit
Integration tests are located in `test/integration`. Each test will run a bash script to prepare a test repo, then replay a recorded lazygit session from within that repo, and then the resultant repo will be compared to an expected repo that was created upon the initial recording. Each integration test lives in its own directory, and the name of the directory becomes the name of the test. Within the directory must be the following files:
### `test.json`
An example of a `test.json` is:
```
{ "description": "stage a file and commit the change", "speed": 20 }
```
The `speed` key refers to the playback speed as a multiple of the original recording speed. So 20 means the test will run 20 times faster than the original recording speed. If a test fails for a given speed, it will drop the speed and re-test, until finally attempting the test at the original speed. If you omit the speed, it will default to 10.
### `setup.sh`
This is a bash script containing the instructions for creating the test repo from scratch. For example:
```
#!/bin/sh
cd $1
git init
git config user.email "CI@example.com"
git config user.name "CI"
echo test1 > myfile1
git add .
git commit -am "myfile1"
```
## Running tests
To run all tests
```
go test pkg/gui/gui_test.go
```
To run them in parallel
```
PARALLEL=true go test pkg/gui/gui_test.go
```
To run a single test
```
go test pkg/gui/gui_test.go -run /<test name>
```
To run a test at a certain speed
```
SPEED=2 go test pkg/gui/gui_test.go -run /<test name>
```
To update a snapshot
```
UPDATE_SNAPSHOTS=true go test pkg/gui/gui_test.go -run /<test name>
```
## Creating a new test
To create a new test:
1) Copy and paste an existing test directory and rename the new directory to whatever you want the test name to be. Update the test.json file's description to describe your test.
2) Update the `setup.sh` any way you like
3) If you want to have a config folder for just that test, create a `config` directory to contain a `config.yml` and optionally a `state.yml` file. Otherwise, the `test/default_test_config` directory will be used.
4) From the lazygit root directory, run:
```
RECORD_EVENTS=true go test pkg/gui/gui_test.go -run /<test name>
```
5) Feel free to re-attempt recording as many times as you like. In the absence of a proper testing framework, the more deliberate your keypresses, the better!
6) Once satisfied with the recording, stage all the newly created files: `test.json`, `setup.sh`, `recording.json` and the `expected` directory that contains a copy of the repo you created.
The resulting directory will look like:
```
actual/ (the resulting repo after running the test, ignored by git)
expected/ (the 'snapshot' repo)
config/ (need not be present)
test.json
setup.sh
recording.json
```
Feel free to create a hierarchy of directories in the `test/integration` directory to group tests by feature.
## Feedback
If you think this process can be improved, let me know! It shouldn't be too hard to change things.

7
docs/README.md Normal file
View File

@@ -0,0 +1,7 @@
# Documentation Overview
* [Configuration](./Config.md).
* [Custom Commands](./Custom_Command_Keybindings.md)
* [Custom Pagers](./Custom_Pagers.md)
* [Keybindings](./keybindings)
* [Undo/Redo](./Undoing.md)

24
docs/Undoing.md Normal file
View File

@@ -0,0 +1,24 @@
# Undo/Redo in lazygit
![Gif](../../assets/undo2.gif)
## Keybindings:
'z' to undo, 'ctrl+z' to redo
## How it works
If you're as clumsy as me you'll probably have felt the pain of botching an interactive rebase or doing a hard reset onto the wrong commit. Luckily, the reflog allows you to trace your steps and make things right again, but I personally can't stand trying to make sense of the reflog.
Lazygit can read through your reflog for you and walk back action by action so that you don't even need to read the reflog. If lazygit finds a reflog entry where you checked out a branch, we'll checkout the original branch. If the entry is from a commit being applied, we'll go back to the commit before that. If we hit an interactive rebase, we'll go back to the commit you were on just before you started it.
## You can even undo things you did outside of lazygit!
Because lazygit just uses the reflog to keep track of things, it doesn't matter whether you're trying to undo something you did in lazygit or directly on the command line. You can open lazygit for the first time and start undoing thing in your repo! Likewise, lazygit marks its undos/redos in the reflog so if you quit the application and come back, lazygit still knows where you're up to.
## Limitations
There are limitations: firstly, lazygit can only undo things that are recorded in the reflog. That means changes to your working tree or stash aren't covered. Secondly, anything permanent you do like pushing to a remote can't be undone. Thirdly, actions like creating a branch won't be undone, because they're not stored in the reflog.
If you are mid-rebase, undo/redo is not supported, because the reflog doesn't contain enough information about what specific things have happened inside that rebase. If you want to undo out of a rebase, it's best to abort the rebase (the default keybinding for bringing up rebase options is 'm').
Undo/Redo is a new feature so if you find a bug let us know. The worst case scenario is that you'll just need to look at your reflog and manually put yourself back on track.

View File

@@ -0,0 +1,59 @@
## Possible keybindings
| Put in | You will get |
|---------------|----------------|
| `<f1>` | F1 |
| `<f2>` | F2 |
| `<f3>` | F3 |
| `<f4>` | F4 |
| `<f5>` | F5 |
| `<f6>` | F6 |
| `<f7>` | F7 |
| `<f8>` | F8 |
| `<f9>` | F9 |
| `<f10>` | F10 |
| `<f11>` | F11 |
| `<f12>` | F12 |
| `<insert>` | Insert |
| `<delete>` | Delete |
| `<home>` | Home |
| `<end>` | End |
| `<pgup>` | Pgup |
| `<pgdown>` | Pgdn |
| `<up>` | ArrowUp |
| `<down>` | ArrowDown |
| `<left>` | ArrowLeft |
| `<right>` | ArrowRight |
| `<tab>` | Tab |
| `<enter>` | Enter |
| `<esc>` | Esc |
| `<backspace>` | Backspace |
| `<c-space>` | CtrlSpace |
| `<c-/>` | CtrlSlash |
| `<space>` | Space |
| `<c-a>` | CtrlA |
| `<c-b>` | CtrlB |
| `<c-c>` | CtrlC |
| `<c-d>` | CtrlD |
| `<c-e>` | CtrlE |
| `<c-f>` | CtrlF |
| `<c-g>` | CtrlG |
| `<c-j>` | CtrlJ |
| `<c-k>` | CtrlK |
| `<c-l>` | CtrlL |
| `<c-n>` | CtrlN |
| `<c-o>` | CtrlO |
| `<c-p>` | CtrlP |
| `<c-q>` | CtrlQ |
| `<c-r>` | CtrlR |
| `<c-s>` | CtrlS |
| `<c-t>` | CtrlT |
| `<c-u>` | CtrlU |
| `<c-v>` | CtrlV |
| `<c-w>` | CtrlW |
| `<c-x>` | CtrlX |
| `<c-y>` | CtrlY |
| `<c-z>` | CtrlZ |
| `<c-4>` | Ctrl4 |
| `<c-5>` | Ctrl5 |
| `<c-6>` | Ctrl6 |
| `<c-8>` | Ctrl8 |

View File

@@ -1,24 +1,169 @@
# Lazygit menu
# Lazygit Keybindings
## Global
## Global Keybindings
<pre>
<kbd>ctrl+r</kbd>: switch to a recent repo (<c-r>)
<kbd>pgup</kbd>: scroll up main panel (fn+up)
<kbd>pgdown</kbd>: scroll down main panel (fn+down)
<kbd>m</kbd>: view merge/rebase options
<kbd>ctrl+p</kbd>: view custom patch options
<kbd>P</kbd>: push
<kbd>p</kbd>: pull
<kbd>R</kbd>: refresh
<kbd>x</kbd>: open menu
<kbd>z</kbd>: undo (via reflog) (experimental)
<kbd>ctrl+z</kbd>: redo (via reflog) (experimental)
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode
<kbd>:</kbd>: execute custom command
<kbd>ctrl+s</kbd>: view filter-by-path options
<kbd>W</kbd>: open diff menu
<kbd>ctrl+e</kbd>: open diff menu
<kbd>@</kbd>: open command log menu
</pre>
## Status
## List Panel Navigation
<pre>
<kbd>e</kbd>: edit config file
<kbd>o</kbd>: open config file
<kbd>u</kbd>: check for update
<kbd>s</kbd>: switch to a recent repo
<kbd>.</kbd>: next page
<kbd>,</kbd>: previous page
<kbd><</kbd>: scroll to top
<kbd>></kbd>: scroll to bottom
<kbd>/</kbd>: start search
<kbd>]</kbd>: next tab
<kbd>[</kbd>: previous tab
</pre>
## Files
## Branches Panel (Branches Tab)
<pre>
<kbd>space</kbd>: checkout
<kbd>o</kbd>: create pull request
<kbd>O</kbd>: create pull request options
<kbd>ctrl+y</kbd>: copy pull request URL to clipboard
<kbd>c</kbd>: checkout by name
<kbd>F</kbd>: force checkout
<kbd>n</kbd>: new branch
<kbd>d</kbd>: delete branch
<kbd>r</kbd>: rebase checked-out branch onto this branch
<kbd>M</kbd>: merge into currently checked out branch
<kbd>i</kbd>: show git-flow options
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>g</kbd>: view reset options
<kbd>R</kbd>: rename branch
<kbd>ctrl+o</kbd>: copy branch name to clipboard
<kbd>enter</kbd>: view commits
</pre>
## Branches Panel (Remote Branches (in Remotes tab))
<pre>
<kbd>esc</kbd>: Return to remotes list
<kbd>g</kbd>: view reset options
<kbd>enter</kbd>: view commits
<kbd>space</kbd>: checkout
<kbd>n</kbd>: new branch
<kbd>M</kbd>: merge into currently checked out branch
<kbd>d</kbd>: delete branch
<kbd>r</kbd>: rebase checked-out branch onto this branch
<kbd>u</kbd>: set as upstream of checked-out branch
</pre>
## Branches Panel (Remotes Tab)
<pre>
<kbd>f</kbd>: fetch remote
<kbd>n</kbd>: add new remote
<kbd>d</kbd>: remove remote
<kbd>e</kbd>: edit remote
</pre>
## Branches Panel (Sub-commits)
<pre>
<kbd>enter</kbd>: view commit's files
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: view reset options
<kbd>n</kbd>: new branch
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
</pre>
## Branches Panel (Tags Tab)
<pre>
<kbd>space</kbd>: checkout
<kbd>d</kbd>: delete tag
<kbd>P</kbd>: push tag
<kbd>n</kbd>: create tag
<kbd>g</kbd>: view reset options
<kbd>enter</kbd>: view commits
</pre>
## Commit Files Panel
<pre>
<kbd>ctrl+o</kbd>: copy the committed file name to the clipboard
<kbd>c</kbd>: checkout file
<kbd>d</kbd>: discard this commit's changes to this file
<kbd>o</kbd>: open file
<kbd>e</kbd>: edit file
<kbd>space</kbd>: toggle file included in patch
<kbd>enter</kbd>: enter file to add selected lines to the patch (or toggle directory collapsed)
<kbd>`</kbd>: toggle file tree view
</pre>
## Commits Panel (Commits)
<pre>
<kbd>s</kbd>: squash down
<kbd>r</kbd>: reword commit
<kbd>R</kbd>: rename commit with editor
<kbd>g</kbd>: reset to this commit
<kbd>f</kbd>: fixup commit
<kbd>F</kbd>: create fixup commit for this commit
<kbd>S</kbd>: squash all 'fixup!' commits above selected commit (autosquash)
<kbd>d</kbd>: delete commit
<kbd>ctrl+j</kbd>: move commit down one
<kbd>ctrl+k</kbd>: move commit up one
<kbd>e</kbd>: edit commit
<kbd>A</kbd>: amend commit with staged changes
<kbd>p</kbd>: pick commit (when mid-rebase)
<kbd>t</kbd>: revert commit
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>v</kbd>: paste commits (cherry-pick)
<kbd>enter</kbd>: view commit's files
<kbd>space</kbd>: checkout commit
<kbd>n</kbd>: create new branch off of commit
<kbd>T</kbd>: tag commit
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>ctrl+y</kbd>: copy commit message to clipboard
</pre>
## Commits Panel (Reflog Tab)
<pre>
<kbd>enter</kbd>: view commit's files
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: view reset options
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
</pre>
## Extras Panel
<pre>
<kbd>@</kbd>: open command log menu
</pre>
## Files Panel (Files)
<pre>
<kbd>c</kbd>: commit changes
@@ -31,98 +176,112 @@
<kbd>o</kbd>: open file
<kbd>i</kbd>: add to .gitignore
<kbd>r</kbd>: refresh files
<kbd>S</kbd>: stash files
<kbd>s</kbd>: stash changes
<kbd>S</kbd>: view stash options
<kbd>a</kbd>: stage/unstage all
<kbd>t</kbd>: add patch
<kbd>D</kbd>: view reset options
<kbd>enter</kbd>: stage individual hunks/lines
<kbd>enter</kbd>: stage individual hunks/lines for file, or collapse/expand for directory
<kbd>f</kbd>: fetch
<kbd>X</kbd>: execute custom command
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
<kbd>g</kbd>: view upstream reset options
<kbd>`</kbd>: toggle file tree view
<kbd>M</kbd>: open external merge tool (git mergetool)
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
</pre>
## Branches
## Files Panel (Submodules)
<pre>
<kbd>space</kbd>: checkout
<kbd>o</kbd>: create pull request
<kbd>c</kbd>: checkout by name
<kbd>F</kbd>: force checkout
<kbd>n</kbd>: new branch
<kbd>d</kbd>: delete branch
<kbd>r</kbd>: rebase branch
<kbd>M</kbd>: merge into currently checked out branch
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>ctrl+o</kbd>: copy submodule name to clipboard
<kbd>enter</kbd>: enter submodule
<kbd>d</kbd>: view reset and remove submodule options
<kbd>u</kbd>: update submodule
<kbd>n</kbd>: add new submodule
<kbd>e</kbd>: update submodule URL
<kbd>i</kbd>: initialize submodule
<kbd>b</kbd>: view bulk submodule options
</pre>
## Commits
<pre>
<kbd>s</kbd>: squash down
<kbd>r</kbd>: reword commit
<kbd>R</kbd>: rename commit with editor
<kbd>g</kbd>: reset to this commit
<kbd>f</kbd>: fixup commit
<kbd>F</kbd>: create fixup commit for this commit
<kbd>S</kbd>: squash above commits
<kbd>d</kbd>: delete commit
<kbd>ctrl+j</kbd>: move commit down one
<kbd>ctrl+k</kbd>: move commit up one
<kbd>e</kbd>: edit commit
<kbd>A</kbd>: amend commit with staged changes
<kbd>p</kbd>: pick commit (when mid-rebase)
<kbd>t</kbd>: revert commit
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>v</kbd>: paste commits (cherry-pick)
<kbd>enter</kbd>: view commit's files
<kbd>space</kbd>: select commit to diff with another commit
</pre>
## Stash
<pre>
<kbd>space</kbd>: apply
<kbd>g</kbd>: pop
<kbd>d</kbd>: drop
</pre>
## Commit files
<pre>
<kbd>esc</kbd>: go back
<kbd>c</kbd>: checkout file
<kbd>d</kbd>: discard this commit's changes to this file
<kbd>o</kbd>: open file
</pre>
## Main (Normal)
<pre>
<kbd>PgDn</kbd>: scroll down (fn+up)
<kbd>PgUp</kbd>: scroll up (fn+down)
</pre>
## Main (Staging)
## Main Panel (Merging)
<pre>
<kbd>esc</kbd>: return to files panel
<kbd>M</kbd>: open external merge tool (git mergetool)
<kbd>space</kbd>: pick hunk
<kbd>b</kbd>: pick all hunks
<kbd>◄</kbd>: select previous conflict
<kbd>►</kbd>: select next conflict
<kbd>▲</kbd>: select previous hunk
<kbd>▼</kbd>: select next hunk
<kbd>z</kbd>: undo
</pre>
## Main Panel (Normal)
<pre>
<kbd>Ő</kbd>: scroll down (fn+up)
<kbd>ő</kbd>: scroll up (fn+down)
</pre>
## Main Panel (Patch Building)
<pre>
<kbd>esc</kbd>: exit line-by-line mode
<kbd>o</kbd>: open file
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>space</kbd>: stage line
<kbd>a</kbd>: stage hunk
<kbd>space</kbd>: add/remove line(s) to patch
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
</pre>
## Main (Merging)
## Main Panel (Staging)
<pre>
<kbd>esc</kbd>: return to files panel
<kbd>space</kbd>: pick hunk
<kbd>b</kbd>: pick both hunks
<kbd></kbd>: select previous conflict
<kbd></kbd>: select next conflict
<kbd>▲</kbd>: select top hunk
<kbd>▼</kbd>: select bottom hunk
<kbd>z</kbd>: undo
<kbd>space</kbd>: toggle line staged / unstaged
<kbd>d</kbd>: delete change (git reset)
<kbd>tab</kbd>: switch to other panel
<kbd>o</kbd>: open file
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd></kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>e</kbd>: edit file
<kbd>o</kbd>: open file
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
<kbd>c</kbd>: commit changes
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>C</kbd>: commit changes using git editor
</pre>
## Menu Panel
<pre>
<kbd>esc</kbd>: close menu
</pre>
## Stash Panel
<pre>
<kbd>enter</kbd>: view stash entry's files
<kbd>space</kbd>: apply
<kbd>g</kbd>: pop
<kbd>d</kbd>: drop
<kbd>n</kbd>: new branch
</pre>
## Status Panel
<pre>
<kbd>e</kbd>: edit config file
<kbd>o</kbd>: open config file
<kbd>u</kbd>: check for update
<kbd>enter</kbd>: switch to a recent repo
<kbd>a</kbd>: show all branch logs
</pre>

View File

@@ -1,24 +1,169 @@
# Lazygit menu
# Lazygit Sneltoetsen
## Global
## Globale Sneltoetsen
<pre>
<kbd>ctrl+r</kbd>: wissel naar een recente repo (<c-r>)
<kbd>pgup</kbd>: scroll naar beneden vanaf hoofdpaneel (fn+up)
<kbd>pgdown</kbd>: scroll naar beneden vanaf hoofdpaneel (fn+down)
<kbd>m</kbd>: bekijk merge/rebase opties
<kbd>ctrl+p</kbd>: bekijk aangepaste patch opties
<kbd>P</kbd>: push
<kbd>p</kbd>: pull
<kbd>R</kbd>: verversen
<kbd>x</kbd>: open menu
<kbd>z</kbd>: ongedaan maken (via reflog) (experimenteel)
<kbd>ctrl+z</kbd>: redo (via reflog) (experimenteel)
<kbd>+</kbd>: volgende scherm modus (normaal/half/groot)
<kbd>_</kbd>: vorige scherm modus
<kbd>:</kbd>: voor aangepaste commando uit
<kbd>ctrl+s</kbd>: bekijk scoping opties
<kbd>W</kbd>: open diff menu
<kbd>ctrl+e</kbd>: open diff menu
<kbd>@</kbd>: open command log menu
</pre>
## Status
## Lijstpaneel Navigatie
<pre>
<kbd>e</kbd>: verander config file
<kbd>o</kbd>: open config file
<kbd>u</kbd>: check voor updates
<kbd>s</kbd>: wissel naar een recente repo
<kbd>.</kbd>: volgende pagina
<kbd>,</kbd>: vorige pagina
<kbd><</kbd>: scroll naar boven
<kbd>></kbd>: scroll naar beneden
<kbd>/</kbd>: start met zoeken
<kbd>]</kbd>: volgende tabblad
<kbd>[</kbd>: vorige tabblad
</pre>
## Bestanden
## Branches Paneel (Branches Tabblad)
<pre>
<kbd>space</kbd>: uitchecken
<kbd>o</kbd>: maak een pull-request
<kbd>O</kbd>: bekijk opties voor pull-aanvraag
<kbd>ctrl+y</kbd>: kopieer de URL van het pull-verzoek naar het klembord
<kbd>c</kbd>: uitchecken bij naam
<kbd>F</kbd>: forceer checkout
<kbd>n</kbd>: nieuwe branch
<kbd>d</kbd>: verwijder branch
<kbd>r</kbd>: rebase branch
<kbd>M</kbd>: merge in met huidige checked out branch
<kbd>i</kbd>: laat git-flow opties zien
<kbd>f</kbd>: fast-forward deze branch vanaf zijn upstream
<kbd>g</kbd>: bekijk reset opties
<kbd>R</kbd>: hernoem branch
<kbd>ctrl+o</kbd>: kopieer branch name naar klembord
<kbd>enter</kbd>: bekijk commits
</pre>
## Branches Paneel (Remote Branches (in Remotes tabblad))
<pre>
<kbd>esc</kbd>: Ga terug naar remotes lijst
<kbd>g</kbd>: bekijk reset opties
<kbd>enter</kbd>: bekijk commits
<kbd>space</kbd>: uitchecken
<kbd>n</kbd>: nieuwe branch
<kbd>M</kbd>: merge in met huidige checked out branch
<kbd>d</kbd>: verwijder branch
<kbd>r</kbd>: rebase branch
<kbd>u</kbd>: stel in als upstream van uitgecheckte branch
</pre>
## Branches Paneel (Remotes Tabblad)
<pre>
<kbd>f</kbd>: fetch remote
<kbd>n</kbd>: voeg een nieuwe remote toe
<kbd>d</kbd>: verwijder remote
<kbd>e</kbd>: wijzig remote
</pre>
## Branches Paneel (Sub-commits)
<pre>
<kbd>enter</kbd>: bekijk gecommite bestanden
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: bekijk reset opties
<kbd>n</kbd>: nieuwe branch
<kbd>c</kbd>: kopieer commit (cherry-pick)
<kbd>C</kbd>: kopieer commit reeks (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
</pre>
## Branches Paneel (Tags Tabblad)
<pre>
<kbd>space</kbd>: uitchecken
<kbd>d</kbd>: verwijder tag
<kbd>P</kbd>: push tag
<kbd>n</kbd>: creëer tag
<kbd>g</kbd>: bekijk reset opties
<kbd>enter</kbd>: bekijk commits
</pre>
## Commit bestanden Paneel
<pre>
<kbd>ctrl+o</kbd>: kopieer de vastgelegde bestandsnaam naar het klembord
<kbd>c</kbd>: bestand uitchecken
<kbd>d</kbd>: uitsluit deze commit zijn veranderingen aan dit bestand
<kbd>o</kbd>: open bestand
<kbd>e</kbd>: verander bestand
<kbd>space</kbd>: toggle bestand inbegrepen in patch
<kbd>enter</kbd>: enter bestand om geselecteerde regels toe te voegen aan de patch
<kbd>`</kbd>: toggle bestandsboom weergave
</pre>
## Commits Paneel (Commits)
<pre>
<kbd>s</kbd>: squash beneden
<kbd>r</kbd>: hernoem commit
<kbd>R</kbd>: hernoem commit met editor
<kbd>g</kbd>: reset naar deze commit
<kbd>f</kbd>: Fixup commit
<kbd>F</kbd>: creëer fixup commit voor deze commit
<kbd>S</kbd>: squash bovenstaande commits
<kbd>d</kbd>: verwijder commit
<kbd>ctrl+j</kbd>: verplaats commit 1 naar beneden
<kbd>ctrl+k</kbd>: verplaats commit 1 naar boven
<kbd>e</kbd>: wijzig commit
<kbd>A</kbd>: wijzig commit met staged veranderingen
<kbd>p</kbd>: kies commit (wanneer midden in rebase)
<kbd>t</kbd>: commit ongedaan maken
<kbd>c</kbd>: kopieer commit (cherry-pick)
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
<kbd>C</kbd>: kopieer commit reeks (cherry-pick)
<kbd>v</kbd>: plak commits (cherry-pick)
<kbd>enter</kbd>: bekijk gecommite bestanden
<kbd>space</kbd>: checkout commit
<kbd>n</kbd>: creëer nieuwe branch van commit
<kbd>T</kbd>: tag commit
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
<kbd>ctrl+y</kbd>: kopieer commit bericht naar klembord
</pre>
## Commits Paneel (Reflog Tabblad)
<pre>
<kbd>enter</kbd>: bekijk gecommite bestanden
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: bekijk reset opties
<kbd>c</kbd>: kopieer commit (cherry-pick)
<kbd>C</kbd>: kopieer commit reeks (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
</pre>
## Extras Paneel
<pre>
<kbd>@</kbd>: open command log menu
</pre>
## Bestanden Paneel (Bestanden)
<pre>
<kbd>c</kbd>: Commit veranderingen
@@ -31,88 +176,39 @@
<kbd>o</kbd>: open bestand
<kbd>i</kbd>: voeg toe aan .gitignore
<kbd>r</kbd>: refresh bestanden
<kbd>S</kbd>: stash-bestanden
<kbd>s</kbd>: stash-bestanden
<kbd>S</kbd>: bekijk stash opties
<kbd>a</kbd>: toggle staged alle
<kbd>t</kbd>: bewerkingen toevoegen
<kbd>D</kbd>: bekijk reset opties
<kbd>enter</kbd>: stage individuele hunks/lijnen
<kbd>f</kbd>: fetch
<kbd>X</kbd>: voor aangepast commando uit
<kbd>ctrl+o</kbd>: kopieer de bestandsnaam naar het klembord
<kbd>g</kbd>: bekijk upstream reset opties
<kbd>`</kbd>: toggle bestandsboom weergave
<kbd>M</kbd>: open external merge tool (git mergetool)
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
</pre>
## Branches
## Bestanden Paneel (Submodules)
<pre>
<kbd>space</kbd>: uitchecken
<kbd>o</kbd>: maak een pull-aanvraag
<kbd>c</kbd>: uitchecken bij naam
<kbd>F</kbd>: forceer checkout
<kbd>n</kbd>: nieuwe branch
<kbd>d</kbd>: verwijder branch
<kbd>r</kbd>: rebase branch
<kbd>M</kbd>: merge in met huidige checked out branch
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>ctrl+o</kbd>: kopieer submodule naam naar klembord
<kbd>enter</kbd>: enter submodule
<kbd>d</kbd>: bekijk reset en verwijder submodule opties
<kbd>u</kbd>: update submodule
<kbd>n</kbd>: voeg nieuwe submodule toe
<kbd>e</kbd>: update submodule URL
<kbd>i</kbd>: initialiseer submodule
<kbd>b</kbd>: bekijk bulk submodule opties
</pre>
## Commits
<pre>
<kbd>s</kbd>: squash beneden
<kbd>r</kbd>: hernoem commit
<kbd>R</kbd>: rename commit with editor
<kbd>g</kbd>: reset naar deze commit
<kbd>f</kbd>: Fixup commit
<kbd>F</kbd>: creëer fixup commit voor deze commit
<kbd>S</kbd>: squash bovenstaande commits
<kbd>d</kbd>: verwijder commit
<kbd>ctrl+j</kbd>: verplaats commit 1 omlaag
<kbd>ctrl+k</kbd>: verplaats commit 1 omhoog
<kbd>e</kbd>: verander commit
<kbd>A</kbd>: wijzig commit met staged veranderingen
<kbd>p</kbd>: pick commit (when mid-rebase)
<kbd>t</kbd>: commit omgedaan maken
<kbd>c</kbd>: kopiëer commit (cherry-pick)
<kbd>C</kbd>: kopiëer commit reeks (cherry-pick)
<kbd>v</kbd>: plak commits (cherry-pick)
<kbd>enter</kbd>: bekijk gecommite bestanden
<kbd>space</kbd>: select commit to diff with another commit
</pre>
## Stash
<pre>
<kbd>space</kbd>: toepassen
<kbd>g</kbd>: pop
<kbd>d</kbd>: drop
</pre>
## Commit bestanden
<pre>
<kbd>esc</kbd>: ga terug
<kbd>c</kbd>: bestand uitchecken
<kbd>d</kbd>: uitsluit deze commit zijn veranderingen aan dit bestand
<kbd>o</kbd>: open bestand
</pre>
## Hoofd (Stage Lines/Hunks)
## Hoofd Paneel (Mergen)
<pre>
<kbd>esc</kbd>: ga terug naar het bestanden paneel
<kbd></kbd>: selecteer de vorige lijn
<kbd></kbd>: selecteer de volgende lijn
<kbd></kbd>: selecteer de vorige hunk
<kbd>►</kbd>: selecteer de volgende hunk
<kbd>space</kbd>: stage lijn
<kbd>a</kbd>: stage hunk
</pre>
## Hoofd (Merging)
<pre>
<kbd>esc</kbd>: ga terug naar het bestanden paneel
<kbd>space</kbd>: pick hunk
<kbd>b</kbd>: pick beide hunks
<kbd>M</kbd>: open external merge tool (git mergetool)
<kbd>space</kbd>: kies hunk
<kbd>b</kbd>: kies bijde hunks
<kbd>◄</kbd>: selecteer voorgaand conflict
<kbd>►</kbd>: selecteer volgende conflict
<kbd>▲</kbd>: selecteer bovenste hunk
@@ -120,9 +216,72 @@
<kbd>z</kbd>: ongedaan maken
</pre>
## Hoofd (Normaal)
## Hoofd Paneel (Normaal)
<pre>
<kbd>PgDn</kbd>: scroll omlaag (fn+up)
<kbd>PgUp</kbd>: scroll omhoog (fn+down)
<kbd>Ő</kbd>: scroll omlaag (fn+up)
<kbd>ő</kbd>: scroll omhoog (fn+down)
</pre>
## Hoofd Paneel (Patch Bouwen)
<pre>
<kbd>esc</kbd>: sluit lijn-bij-lijn modus
<kbd>o</kbd>: open bestand
<kbd>▲</kbd>: selecteer de vorige lijn
<kbd>▼</kbd>: selecteer de volgende lijn
<kbd>◄</kbd>: selecteer de vorige hunk
<kbd>►</kbd>: selecteer de volgende hunk
<kbd>space</kbd>: voeg toe/verwijder lijn(en) in patch
<kbd>v</kbd>: toggle drag selecteer
<kbd>V</kbd>: toggle drag selecteer
<kbd>a</kbd>: toggle selecteer hunk
</pre>
## Hoofd Paneel (Staging)
<pre>
<kbd>esc</kbd>: ga terug naar het bestanden paneel
<kbd>space</kbd>: toggle lijnen staged / unstaged
<kbd>d</kbd>: verwijdert change (git reset)
<kbd>tab</kbd>: ga naar een ander paneel
<kbd>o</kbd>: open bestand
<kbd>▲</kbd>: selecteer de vorige lijn
<kbd>▼</kbd>: selecteer de volgende lijn
<kbd>◄</kbd>: selecteer de vorige hunk
<kbd>►</kbd>: selecteer de volgende hunk
<kbd>e</kbd>: verander bestand
<kbd>o</kbd>: open bestand
<kbd>v</kbd>: toggle drag selecteer
<kbd>V</kbd>: toggle drag selecteer
<kbd>a</kbd>: toggle selecteer hunk
<kbd>c</kbd>: Commit veranderingen
<kbd>w</kbd>: commit veranderingen zonder pre-commit hook
<kbd>C</kbd>: commit veranderingen met de git editor
</pre>
## Menu Paneel
<pre>
<kbd>esc</kbd>: sluit menu
</pre>
## Stash Paneel
<pre>
<kbd>enter</kbd>: bekijk bestanden van stash entry
<kbd>space</kbd>: toepassen
<kbd>g</kbd>: pop
<kbd>d</kbd>: laten vallen
<kbd>n</kbd>: nieuwe branch
</pre>
## Status Paneel
<pre>
<kbd>e</kbd>: verander config bestand
<kbd>o</kbd>: open config bestand
<kbd>u</kbd>: check voor updates
<kbd>enter</kbd>: wissel naar een recente repo
<kbd>a</kbd>: alle logs van de branch laten zien
</pre>

View File

@@ -1,24 +1,169 @@
# Lazygit menu
# Lazygit Keybindings
## Globalne
<pre>
<kbd>ctrl+r</kbd>: switch to a recent repo (<c-r>)
<kbd>pgup</kbd>: scroll up main panel (fn+up)
<kbd>pgdown</kbd>: scroll down main panel (fn+down)
<kbd>m</kbd>: view merge/rebase options
<kbd>ctrl+p</kbd>: view custom patch options
<kbd>P</kbd>: push
<kbd>p</kbd>: pull
<kbd>R</kbd>: odśwież
<kbd>x</kbd>: open menu
<kbd>z</kbd>: undo (via reflog) (experimental)
<kbd>ctrl+z</kbd>: redo (via reflog) (experimental)
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode
<kbd>:</kbd>: execute custom command
<kbd>ctrl+s</kbd>: view filter-by-path options
<kbd>W</kbd>: open diff menu
<kbd>ctrl+e</kbd>: open diff menu
<kbd>@</kbd>: open command log menu
</pre>
## Status
## List Panel Navigation
<pre>
<kbd>e</kbd>: edytuj plik konfiguracyjny
<kbd>o</kbd>: otwórz plik konfiguracyjny
<kbd>u</kbd>: sprawdź aktualizacje
<kbd>s</kbd>: switch to a recent repo
<kbd>.</kbd>: next page
<kbd>,</kbd>: previous page
<kbd><</kbd>: scroll to top
<kbd>></kbd>: scroll to bottom
<kbd>/</kbd>: start search
<kbd>]</kbd>: next tab
<kbd>[</kbd>: previous tab
</pre>
## Pliki
## Gałęzie Panel (Branches Tab)
<pre>
<kbd>space</kbd>: przełącz
<kbd>o</kbd>: utwórz żądanie wyciągnięcia
<kbd>O</kbd>: utwórz opcje żądania ściągnięcia
<kbd>ctrl+y</kbd>: skopiuj adres URL żądania ściągnięcia do schowka
<kbd>c</kbd>: przełącz używając nazwy
<kbd>F</kbd>: wymuś przełączenie
<kbd>n</kbd>: nowa gałąź
<kbd>d</kbd>: usuń gałąź
<kbd>r</kbd>: rebase branch
<kbd>M</kbd>: scal do obecnej gałęzi
<kbd>i</kbd>: show git-flow options
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>g</kbd>: view reset options
<kbd>R</kbd>: rename branch
<kbd>ctrl+o</kbd>: copy branch name to clipboard
<kbd>enter</kbd>: view commits
</pre>
## Gałęzie Panel (Remote Branches (in Remotes tab))
<pre>
<kbd>esc</kbd>: return to remotes list
<kbd>g</kbd>: view reset options
<kbd>enter</kbd>: view commits
<kbd>space</kbd>: przełącz
<kbd>n</kbd>: nowa gałąź
<kbd>M</kbd>: scal do obecnej gałęzi
<kbd>d</kbd>: usuń gałąź
<kbd>r</kbd>: rebase branch
<kbd>u</kbd>: set as upstream of checked-out branch
</pre>
## Gałęzie Panel (Remotes Tab)
<pre>
<kbd>f</kbd>: fetch remote
<kbd>n</kbd>: add new remote
<kbd>d</kbd>: remove remote
<kbd>e</kbd>: edit remote
</pre>
## Gałęzie Panel (Sub-commits)
<pre>
<kbd>enter</kbd>: view commit's files
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: view reset options
<kbd>n</kbd>: nowa gałąź
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
</pre>
## Gałęzie Panel (Tags Tab)
<pre>
<kbd>space</kbd>: przełącz
<kbd>d</kbd>: delete tag
<kbd>P</kbd>: push tag
<kbd>n</kbd>: create tag
<kbd>g</kbd>: view reset options
<kbd>enter</kbd>: view commits
</pre>
## Commit files Panel
<pre>
<kbd>ctrl+o</kbd>: copy the committed file name to the clipboard
<kbd>c</kbd>: checkout file
<kbd>d</kbd>: discard this commit's changes to this file
<kbd>o</kbd>: otwórz plik
<kbd>e</kbd>: edytuj plik
<kbd>space</kbd>: toggle file included in patch
<kbd>enter</kbd>: enter file to add selected lines to the patch (or toggle directory collapsed)
<kbd>`</kbd>: toggle file tree view
</pre>
## Commity Panel (Commity)
<pre>
<kbd>s</kbd>: ściśnij w dół
<kbd>r</kbd>: przemianuj commit
<kbd>R</kbd>: przemianuj commit w edytorze
<kbd>g</kbd>: zresetuj do tego commita
<kbd>f</kbd>: napraw commit
<kbd>F</kbd>: create fixup commit for this commit
<kbd>S</kbd>: squash all 'fixup!' commits above selected commits (autosquash)
<kbd>d</kbd>: delete commit
<kbd>ctrl+j</kbd>: move commit down one
<kbd>ctrl+k</kbd>: move commit up one
<kbd>e</kbd>: edit commit
<kbd>A</kbd>: amend commit with staged changes
<kbd>p</kbd>: pick commit (when mid-rebase)
<kbd>t</kbd>: revert commit
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>v</kbd>: paste commits (cherry-pick)
<kbd>enter</kbd>: view commit's files
<kbd>space</kbd>: checkout commit
<kbd>n</kbd>: create new branch off of commit
<kbd>T</kbd>: tag commit
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>ctrl+y</kbd>: copy commit message to clipboard
</pre>
## Commity Panel (Reflog Tab)
<pre>
<kbd>enter</kbd>: view commit's files
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: view reset options
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
</pre>
## Extras Panel
<pre>
<kbd>@</kbd>: open command log menu
</pre>
## Pliki Panel (Pliki)
<pre>
<kbd>c</kbd>: commituj zmiany
@@ -31,98 +176,112 @@
<kbd>o</kbd>: otwórz plik
<kbd>i</kbd>: dodaj do .gitignore
<kbd>r</kbd>: odśwież pliki
<kbd>S</kbd>: przechowaj pliki
<kbd>s</kbd>: przechowaj pliki
<kbd>S</kbd>: view stash options
<kbd>a</kbd>: przełącz wszystkie zatwierdzenia
<kbd>t</kbd>: dodaj łatkę
<kbd>D</kbd>: view reset options
<kbd>enter</kbd>: zatwierdź pojedyncze linie
<kbd>f</kbd>: fetch
<kbd>X</kbd>: execute custom command
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
<kbd>g</kbd>: view upstream reset options
<kbd>`</kbd>: toggle file tree view
<kbd>M</kbd>: open external merge tool (git mergetool)
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
</pre>
## Gałęzie
## Pliki Panel (Submodules)
<pre>
<kbd>space</kbd>: przełącz
<kbd>o</kbd>: utwórz żądanie wyciągnięcia
<kbd>c</kbd>: przełącz używając nazwy
<kbd>F</kbd>: wymuś przełączenie
<kbd>n</kbd>: nowa gałąź
<kbd>d</kbd>: usuń gałąź
<kbd>r</kbd>: rebase branch
<kbd>M</kbd>: scal do obecnej gałęzi
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>ctrl+o</kbd>: copy submodule name to clipboard
<kbd>enter</kbd>: enter submodule
<kbd>d</kbd>: view reset and remove submodule options
<kbd>u</kbd>: update submodule
<kbd>n</kbd>: add new submodule
<kbd>e</kbd>: update submodule URL
<kbd>i</kbd>: initialize submodule
<kbd>b</kbd>: view bulk submodule options
</pre>
## Commity
<pre>
<kbd>s</kbd>: ściśnij w dół
<kbd>r</kbd>: przemianuj commit
<kbd>R</kbd>: przemianuj commit w edytorze
<kbd>g</kbd>: zresetuj do tego commita
<kbd>f</kbd>: napraw commit
<kbd>F</kbd>: create fixup commit for this commit
<kbd>S</kbd>: squash above commits
<kbd>d</kbd>: delete commit
<kbd>ctrl+j</kbd>: move commit down one
<kbd>ctrl+k</kbd>: move commit up one
<kbd>e</kbd>: edit commit
<kbd>A</kbd>: amend commit with staged changes
<kbd>p</kbd>: pick commit (when mid-rebase)
<kbd>t</kbd>: revert commit
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>v</kbd>: paste commits (cherry-pick)
<kbd>enter</kbd>: view commit's files
<kbd>space</kbd>: select commit to diff with another commit
</pre>
## Schowek
<pre>
<kbd>space</kbd>: zastosuj
<kbd>g</kbd>: wyciągnij
<kbd>d</kbd>: porzuć
</pre>
## Commit files
<pre>
<kbd>esc</kbd>: go back
<kbd>c</kbd>: checkout file
<kbd>d</kbd>: discard this commit's changes to this file
<kbd>o</kbd>: otwórz plik
</pre>
## Main (Normal)
<pre>
<kbd>PgDn</kbd>: scroll down (fn+up)
<kbd>PgUp</kbd>: scroll up (fn+down)
</pre>
## Main (Zatwierdzanie)
## Main Panel (Merging)
<pre>
<kbd>esc</kbd>: wróć do panelu plików
<kbd>M</kbd>: open external merge tool (git mergetool)
<kbd>space</kbd>: pick hunk
<kbd>b</kbd>: pick all hunks
<kbd>◄</kbd>: select previous conflict
<kbd>►</kbd>: select next conflict
<kbd>▲</kbd>: select previous hunk
<kbd>▼</kbd>: select next hunk
<kbd>z</kbd>: cofnij
</pre>
## Main Panel (Normal)
<pre>
<kbd>Ő</kbd>: scroll down (fn+up)
<kbd>ő</kbd>: scroll up (fn+down)
</pre>
## Main Panel (Patch Building)
<pre>
<kbd>esc</kbd>: exit line-by-line mode
<kbd>o</kbd>: otwórz plik
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>space</kbd>: zatwierdź linię
<kbd>a</kbd>: zatwierdź kawałek
<kbd>space</kbd>: add/remove line(s) to patch
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
</pre>
## Main (Merging)
## Main Panel (Zatwierdzanie)
<pre>
<kbd>esc</kbd>: wróć do panelu plików
<kbd>space</kbd>: pick hunk
<kbd>b</kbd>: pick both hunks
<kbd></kbd>: select previous conflict
<kbd></kbd>: select next conflict
<kbd>▲</kbd>: select top hunk
<kbd>▼</kbd>: select bottom hunk
<kbd>z</kbd>: undo
<kbd>space</kbd>: toggle line staged / unstaged
<kbd>d</kbd>: delete change (git reset)
<kbd>tab</kbd>: switch to other panel
<kbd>o</kbd>: otwórz plik
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd></kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>e</kbd>: edytuj plik
<kbd>o</kbd>: otwórz plik
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
<kbd>c</kbd>: commituj zmiany
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>C</kbd>: commituj zmiany używając edytora z gita
</pre>
## Menu Panel
<pre>
<kbd>esc</kbd>: close menu
</pre>
## Schowek Panel
<pre>
<kbd>enter</kbd>: view stash entry's files
<kbd>space</kbd>: zastosuj
<kbd>g</kbd>: wyciągnij
<kbd>d</kbd>: porzuć
<kbd>n</kbd>: nowa gałąź
</pre>
## Status Panel
<pre>
<kbd>e</kbd>: edytuj plik konfiguracyjny
<kbd>o</kbd>: otwórz plik konfiguracyjny
<kbd>u</kbd>: sprawdź aktualizacje
<kbd>enter</kbd>: switch to a recent repo
<kbd>a</kbd>: pokazywać wszystkie logi branżowe
</pre>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

61
go.mod
View File

@@ -1,39 +1,50 @@
module github.com/jesseduffield/lazygit
go 1.13
go 1.14
require (
github.com/aws/aws-sdk-go v1.25.31 // indirect
github.com/OpenPeeDeeP/xdg v1.0.0
github.com/atotto/clipboard v0.1.2
github.com/aybabtme/humanlog v0.4.1
github.com/cli/safeexec v1.0.0
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
github.com/fatih/color v1.7.0
github.com/go-errors/errors v1.0.1
github.com/creack/pty v1.1.11
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.9.0 // indirect
github.com/fsnotify/fsnotify v1.4.7
github.com/go-errors/errors v1.4.1
github.com/go-logfmt/logfmt v0.5.0 // indirect
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
github.com/golang/protobuf v1.3.2 // indirect
github.com/google/go-cmp v0.3.1 // indirect
github.com/hashicorp/go-cleanhttp v0.5.1 // indirect
github.com/hashicorp/go-getter v1.4.0 // indirect
github.com/hashicorp/go-version v1.2.0 // indirect
github.com/integrii/flaggy v1.3.0
github.com/jesseduffield/go-getter v0.0.0-20180822080847-906e15686e63
github.com/jesseduffield/gocui v0.3.1-0.20191110053728-01cdcccd0508
github.com/jesseduffield/pty v1.1.3
github.com/jesseduffield/rollrus v0.0.0-20190701125922-dd028cb0bfd7
github.com/jesseduffield/termbox-go v0.0.0-20190630083001-9dd53af7214e // indirect
github.com/gookit/color v1.4.2
github.com/imdario/mergo v0.3.11
github.com/integrii/flaggy v1.4.0
github.com/iriri/minimal/gitignore v0.3.2 // indirect
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4
github.com/jesseduffield/gocui v0.3.1-0.20211017220056-b2fc03c74a6f
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
github.com/jesseduffield/yaml v2.1.0+incompatible
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/mattn/go-colorable v0.1.4 // indirect
github.com/mattn/go-isatty v0.0.10 // indirect
github.com/mattn/go-runewidth v0.0.6 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/kyokomi/emoji/v2 v2.2.8
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.7 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mgutz/str v1.2.0
github.com/nicksnyder/go-i18n/v2 v2.0.2
github.com/onsi/ginkgo v1.10.3 // indirect
github.com/onsi/gomega v1.7.1 // indirect
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0
github.com/ozeidan/fuzzy-patricia v3.0.0+incompatible // indirect
github.com/sahilm/fuzzy v0.1.0
github.com/sirupsen/logrus v1.4.2
github.com/spf13/viper v1.5.0
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
github.com/stretchr/testify v1.4.0
github.com/tcnksm/go-gitconfig v0.1.2
github.com/ulikunitz/xz v0.5.6 // indirect
golang.org/x/text v0.3.2
gopkg.in/src-d/go-git.v4 v4.13.1
gopkg.in/yaml.v2 v2.2.5
github.com/stretchr/testify v1.7.0
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 // indirect
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c // indirect
golang.org/x/sys v0.0.0-20211015200801-69063c4bb744 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0
)

411
go.sum
View File

@@ -1,370 +1,223 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/OpenPeeDeeP/xdg v1.0.0 h1:UDLmNjCGFZZCaVMB74DqYEtXkHxnTxcr4FeJVF9uCn8=
github.com/OpenPeeDeeP/xdg v1.0.0/go.mod h1:tMoSueLQlMf0TCldjrJLNIjAc5qAOIcHt5REi88/Ygo=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM=
github.com/aws/aws-sdk-go v1.25.31 h1:14mdh3HsTgRekePPkYcCbAaEXJknc3mN7f4XfsiMMDA=
github.com/aws/aws-sdk-go v1.25.31/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY=
github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aybabtme/humanlog v0.4.1 h1:D8d9um55rrthJsP8IGSHBcti9lTb/XknmDAX6Zy8tek=
github.com/aybabtme/humanlog v0.4.1/go.mod h1:B0bnQX4FTSU3oftPMTTPvENCy8LqixLDvYJA9TUCAGo=
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI=
github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.7.1-0.20180516100307-2d684516a886/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.4.0 h1:W6dxJEmaxYvhICFoTY3WrLLEXsQ11SaFnKGVEXW57KM=
github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.4.1 h1:IvVlgbzSsaUNudsw5dcXSzF3EWyXTi5XrAdngnuhRyg=
github.com/go-errors/errors v1.4.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM=
github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12 h1:PbKy9zOy4aAKrJ5pibIRpVO2BXnK1Tlcg+caKI7Ox5M=
github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 h1:zN2lZNZRflqFyxVaTIU61KNKQ9C0055u9CAfpmqUvo4=
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3/go.mod h1:nPpo7qLxd6XL3hWJG/O60sR8ZKfMCiIoNap5GvD12KU=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-getter v1.4.0 h1:ENHNi8494porjD0ZhIrjlAHnveSFhY7hvOJrV/fsKkw=
github.com/hashicorp/go-getter v1.4.0/go.mod h1:7qxyCd8rBfcShwsvxgIguu4KbS3l8bUCwg2Umn7RjeY=
github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo=
github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I=
github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/gookit/color v1.4.2 h1:tXy44JFSFkKnELV6WaMo/lLfu/meqITX3iAV52do7lk=
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/integrii/flaggy v1.3.0 h1:8I5Qqz22C6+EwUqJuaN5ITh77obI8VSg6RwYLe2VB7o=
github.com/integrii/flaggy v1.3.0/go.mod h1:tnTxHeTJbah0gQ6/K0RW0J7fMUBk9MCF5blhm43LNpI=
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/integrii/flaggy v1.4.0 h1:A1x7SYx4jqu5NSrY14z8Z+0UyX2S5ygfJJrfolWR3zM=
github.com/integrii/flaggy v1.4.0/go.mod h1:tnTxHeTJbah0gQ6/K0RW0J7fMUBk9MCF5blhm43LNpI=
github.com/iriri/minimal v0.0.0-20180828191352-9b2348d09c1a h1:mCZYG6QcX0dz/J0rFc1tcRYGeixlDcCGSPXuPMbiS5U=
github.com/iriri/minimal/gitignore v0.3.2 h1:MnTVH89iuwiyZ/a1pByw/mAU2ShWai1yvv0tgHSq5Ww=
github.com/iriri/minimal/gitignore v0.3.2/go.mod h1:v7YhsYBAInyAnQligwCIGRuQmtwQyYxkVy5vEdy2wPU=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jesseduffield/go-getter v0.0.0-20180822080847-906e15686e63 h1:tbm85YuPi3d1LFAUr6yZyzZ4vR96ygV98rezZZ+ywbg=
github.com/jesseduffield/go-getter v0.0.0-20180822080847-906e15686e63/go.mod h1:fNqjRf+4XnTo2PrGN1JRb79b/BeoHwP4lU00f39SQY0=
github.com/jesseduffield/gocui v0.3.1-0.20191110053728-01cdcccd0508 h1:8CPQLUe+0QXxnbnUfaMxh1UGxg3rYCqCvbxecC9rrIY=
github.com/jesseduffield/gocui v0.3.1-0.20191110053728-01cdcccd0508/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/pty v1.1.3 h1:JTPQ/bbGH3IBwQhxKZiFxfD4wv7cYrY7LNTwbucsZ64=
github.com/jesseduffield/pty v1.1.3/go.mod h1:7jlS40+UhOqkZJDIG1B/H21xnuET/+fvbbnHCa8wSIo=
github.com/jesseduffield/roll v0.0.0-20190629104057-695be2e62b00 h1:+JaOkfBNYQYlGD7dgru8mCwYNEc5tRRI8mThlVANhSM=
github.com/jesseduffield/roll v0.0.0-20190629104057-695be2e62b00/go.mod h1:cWNQljQAWYBp4wchyGfql4q2jRNZXxiE1KhVQgz+JaM=
github.com/jesseduffield/rollrus v0.0.0-20190701125922-dd028cb0bfd7 h1:CRD7bVjlGIiV+M0jlsa+XWpneW0KY0e7Y4z3GWb5S4o=
github.com/jesseduffield/rollrus v0.0.0-20190701125922-dd028cb0bfd7/go.mod h1:VspA3aTkEo0Q7TPCLmX1uHNP+Wb4iSDX09hmTRo1QYc=
github.com/jesseduffield/termbox-go v0.0.0-20190630083001-9dd53af7214e h1:tth7wr6+sfSbdpRWWrwvLYyS56HyIRVfq0Qcl2h28wM=
github.com/jesseduffield/termbox-go v0.0.0-20190630083001-9dd53af7214e/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4 h1:GOQrmaE8i+KEdB8NzAegKYd4tPn/inM0I1uo0NXFerg=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
github.com/jesseduffield/gocui v0.3.1-0.20211017035223-b68948e63cc3 h1:J5s/4Y860tas8J0AMQ3gJKCbJPx8zNpiTm5UjEgPQfY=
github.com/jesseduffield/gocui v0.3.1-0.20211017035223-b68948e63cc3/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
github.com/jesseduffield/gocui v0.3.1-0.20211017041119-0ec562dfd23b h1:kepukaDQfZ6LBSvHUYReFvVSW5Lx5ZQZDgGhXj0Mx7U=
github.com/jesseduffield/gocui v0.3.1-0.20211017041119-0ec562dfd23b/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
github.com/jesseduffield/gocui v0.3.1-0.20211017063715-c74848d8ad00 h1:5TusU8ir9OHg3By2PPmLwa2y+2G9F+16QRK8bpofsC0=
github.com/jesseduffield/gocui v0.3.1-0.20211017063715-c74848d8ad00/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
github.com/jesseduffield/gocui v0.3.1-0.20211017091015-8bf4a4666b77 h1:MQUxSxVBTZQpSYybEiFA4+oIi02ycTKGCqgHItYi/20=
github.com/jesseduffield/gocui v0.3.1-0.20211017091015-8bf4a4666b77/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
github.com/jesseduffield/gocui v0.3.1-0.20211017220056-b2fc03c74a6f h1:JHrb78pj+gYC3KiJKL1WW6lYzlatBIF46oREn68plTM=
github.com/jesseduffield/gocui v0.3.1-0.20211017220056-b2fc03c74a6f/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
github.com/jesseduffield/minimal v0.0.0-20211018110810-9cde264e6b1e h1:WZc73tBVMMhcO6zXyZBItLEF4jgBpBH0lFCZzDgrjDg=
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e h1:uw/oo+kg7t/oeMs6sqlAwr85ND/9cpO3up3VxphxY0U=
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e/go.mod h1:u60qdFGXRd36jyEXxetz0vQceQIxzI13lIo3EFUDf4I=
github.com/jesseduffield/yaml v2.1.0+incompatible h1:HWQJ1gIv2zHKbDYNp0Jwjlj24K8aqpFHnMCynY1EpmE=
github.com/jesseduffield/yaml v2.1.0+incompatible/go.mod h1:w0xGhOSIJCGYYW+hnFPTutCy5aACpkcwbmORt5axGqk=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 h1:12VvqtR6Aowv3l/EQUlocDHW2Cp4G9WJVH7uyH8QFJE=
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/kyokomi/emoji/v2 v2.2.8 h1:jcofPxjHWEkJtkIbcLHvZhxKgCPl6C7MyjTrD4KDqUE=
github.com/kyokomi/emoji/v2 v2.2.8/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.6 h1:V2iyH+aX9C5fsYCpK60U8BYIvmhqxuOL3JZcqc1NB7k=
github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mgutz/str v1.2.0 h1:4IzWSdIz9qPQWLfKZ0rJcV0jcUDpxvP4JVZ4GXQyvSw=
github.com/mgutz/str v1.2.0/go.mod h1:w1v0ofgLaJdoD0HpQ3fycxKD1WtxpjSo151pK/31q6w=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nicksnyder/go-i18n/v2 v2.0.2 h1:KsHGcTByIM0mHZKQGy0nlJLOjPNjQ6MVib/3PvsBDNY=
github.com/nicksnyder/go-i18n/v2 v2.0.2/go.mod h1:JXS4+OKhbcwDoVTEj0sLFWL1vOwec2g/YBAxZ9owJqY=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY=
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/ozeidan/fuzzy-patricia v1.0.1 h1:YExnavqXH3OvCCqE2TunuJJHdFcFQdVEfUoWzrnPxSg=
github.com/ozeidan/fuzzy-patricia v3.0.0+incompatible h1:Pl61eMyfJqgY/wytiI4vamqPYribq6d8VxeP1CNyg9M=
github.com/ozeidan/fuzzy-patricia v3.0.0+incompatible/go.mod h1:zgvuCcYS7wB7fVCGblsaFFmEe8+aAH13dTYm8FbrpsM=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 h1:Xuk8ma/ibJ1fOy4Ee11vHhUFHQNpHhrBneOCNHVXS5w=
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0/go.mod h1:7AwjWCpdPhkSmNAgUv5C7EJ4AbmjEB3r047r3DXWu3Y=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.5.0 h1:GpsTwfsQ27oS/Aha/6d1oD7tpKIqWnOA6tgOX9HHkt4=
github.com/spf13/viper v1.5.0/go.mod h1:AkYRkVJF8TkSG/xet6PzXX+l39KhhXa2pdqVSxnTcn4=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4=
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/urfave/cli v1.20.1-0.20180226030253-8e01ec4cd3e2/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c h1:dk0ukUIHmGHqASjP0iue2261isepFCC6XRCSd1nHgDw=
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c/go.mod h1:iQL9McJNjoIa5mjH6nYTCTZXUN6RP+XW3eib7Ya3XcI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20170407050850-f3918c30c5c2/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e h1:D5TXcfTk7xF7hvieo4QErS3qqCB4teTffacDWr7CI+0=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be h1:QAcqgptGM8IQBC9K/RC4o+O9YmqEm0diQn9QmZw/0mU=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211015200801-69063c4bb744 h1:KzbpndAYEM+4oHRp9JmB2ewj0NHHxO3Z0g7Gus2O1kk=
golang.org/x/sys v0.0.0-20211015200801-69063c4bb744/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg=
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE=
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0 h1:KzcWKJ0nMAmGoBhYVMnkWc1rXjB42lKy5aIys4TdLOA=
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0/go.mod h1:XoytMOotjRRJVkIsQdxsPIioRLYFISEaY9a4tftOXAo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

89
main.go
View File

@@ -1,6 +1,7 @@
package main
import (
"bytes"
"fmt"
"log"
"os"
@@ -11,6 +12,9 @@ import (
"github.com/integrii/flaggy"
"github.com/jesseduffield/lazygit/pkg/app"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/constants"
"github.com/jesseduffield/lazygit/pkg/env"
yaml "github.com/jesseduffield/yaml"
)
var (
@@ -20,16 +24,14 @@ var (
buildSource = "unknown"
)
func projectPath(path string) string {
gopath := os.Getenv("GOPATH")
return filepath.FromSlash(gopath + "/src/github.com/jesseduffield/lazygit/" + path)
}
func main() {
flaggy.DefaultParser.ShowVersionWithVersionFlag = false
repoPath := "."
flaggy.String(&repoPath, "p", "path", "Path of git repo")
repoPath := ""
flaggy.String(&repoPath, "p", "path", "Path of git repo. (equivalent to --work-tree=<path> --git-dir=<path>/.git/)")
filterPath := ""
flaggy.String(&filterPath, "f", "filter", "Path to filter on in `git log -- <path>`. When in filter mode, the commits, reflog, and stash are filtered based on the given path, and some operations are restricted")
dump := ""
flaggy.AddPositionalValue(&dump, "gitargs", 1, false, "Todo file")
@@ -39,25 +41,84 @@ func main() {
flaggy.Bool(&versionFlag, "v", "version", "Print the current version")
debuggingFlag := false
flaggy.Bool(&debuggingFlag, "d", "debug", "Run in debug mode with logging")
flaggy.Bool(&debuggingFlag, "d", "debug", "Run in debug mode with logging (see --logs flag below). Use the LOG_LEVEL env var to set the log level (debug/info/warn/error)")
logFlag := false
flaggy.Bool(&logFlag, "l", "logs", "Tail lazygit logs (intended to be used when `lazygit --debug` is called in a separate terminal tab)")
configFlag := false
flaggy.Bool(&configFlag, "c", "config", "Print the current default config")
flaggy.Bool(&configFlag, "c", "config", "Print the default config")
configDirFlag := false
flaggy.Bool(&configDirFlag, "cd", "print-config-dir", "Print the config directory")
useConfigDir := ""
flaggy.String(&useConfigDir, "ucd", "use-config-dir", "override default config directory with provided directory")
workTree := ""
flaggy.String(&workTree, "w", "work-tree", "equivalent of the --work-tree git argument")
gitDir := ""
flaggy.String(&gitDir, "g", "git-dir", "equivalent of the --git-dir git argument")
customConfig := ""
flaggy.String(&customConfig, "ucf", "use-config-file", "Comma seperated list to custom config file(s)")
flaggy.Parse()
if repoPath != "" {
if workTree != "" || gitDir != "" {
log.Fatal("--path option is incompatible with the --work-tree and --git-dir options")
}
workTree = repoPath
gitDir = filepath.Join(repoPath, ".git")
}
if customConfig != "" {
os.Setenv("LG_CONFIG_FILE", customConfig)
}
if useConfigDir != "" {
os.Setenv("CONFIG_DIR", useConfigDir)
}
if workTree != "" {
env.SetGitWorkTreeEnv(workTree)
}
if gitDir != "" {
env.SetGitDirEnv(gitDir)
}
if versionFlag {
fmt.Printf("commit=%s, build date=%s, build source=%s, version=%s, os=%s, arch=%s\n", commit, date, buildSource, version, runtime.GOOS, runtime.GOARCH)
os.Exit(0)
}
if configFlag {
fmt.Printf("%s\n", config.GetDefaultConfig())
var buf bytes.Buffer
encoder := yaml.NewEncoder(&buf)
err := encoder.Encode(config.GetDefaultConfig())
if err != nil {
log.Fatal(err.Error())
}
fmt.Printf("%s\n", buf.String())
os.Exit(0)
}
if repoPath != "." {
if err := os.Chdir(repoPath); err != nil {
if configDirFlag {
fmt.Printf("%s\n", config.ConfigDir())
os.Exit(0)
}
if logFlag {
app.TailLogs()
os.Exit(0)
}
if workTree != "" {
if err := os.Chdir(workTree); err != nil {
log.Fatal(err.Error())
}
}
@@ -67,7 +128,7 @@ func main() {
log.Fatal(err.Error())
}
app, err := app.NewApp(appConfig)
app, err := app.NewApp(appConfig, filterPath)
if err == nil {
err = app.Run()
@@ -81,6 +142,6 @@ func main() {
stackTrace := newErr.ErrorStack()
app.Log.Error(stackTrace)
log.Fatal(fmt.Sprintf("%s\n\n%s", app.Tr.SLocalize("ErrorOccurred"), stackTrace))
log.Fatal(fmt.Sprintf("%s: %s\n\n%s", app.Tr.ErrorOccurred, constants.Links.Issues, stackTrace))
}
}

View File

@@ -2,20 +2,26 @@ package app
import (
"bufio"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/aybabtme/humanlog"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/env"
"github.com/jesseduffield/lazygit/pkg/gui"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/updates"
"github.com/jesseduffield/rollrus"
"github.com/shibukawa/configdir"
"github.com/sirupsen/logrus"
)
@@ -25,10 +31,10 @@ type App struct {
Config config.AppConfigurer
Log *logrus.Entry
OSCommand *commands.OSCommand
OSCommand *oscommands.OSCommand
GitCommand *commands.GitCommand
Gui *gui.Gui
Tr *i18n.Localizer
Tr *i18n.TranslationSet
Updater *updates.Updater // may only need this on the Gui
ClientContext string
}
@@ -45,12 +51,6 @@ func newProductionLogger(config config.AppConfigurer) *logrus.Logger {
return log
}
func globalConfigDir() string {
configDirs := configdir.New("jesseduffield", "lazygit")
configDir := configDirs.QueryFolders(configdir.Global)[0]
return configDir.Path
}
func getLogLevel() logrus.Level {
strLevel := os.Getenv("LOG_LEVEL")
level, err := logrus.ParseLevel(strLevel)
@@ -60,22 +60,24 @@ func getLogLevel() logrus.Level {
return level
}
func newDevelopmentLogger(config config.AppConfigurer) *logrus.Logger {
log := logrus.New()
log.SetLevel(getLogLevel())
file, err := os.OpenFile(filepath.Join(globalConfigDir(), "development.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
func newDevelopmentLogger(configurer config.AppConfigurer) *logrus.Logger {
logger := logrus.New()
logger.SetLevel(getLogLevel())
logPath, err := config.LogPath()
if err != nil {
log.Fatal(err)
}
file, err := os.OpenFile(logPath, 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
logger.SetOutput(file)
return logger
}
func newLogger(config config.AppConfigurer) *logrus.Entry {
var log *logrus.Logger
environment := "production"
if config.GetDebug() || os.Getenv("DEBUG") == "TRUE" {
environment = "development"
log = newDevelopmentLogger(config)
} else {
log = newProductionLogger(config)
@@ -85,11 +87,6 @@ func newLogger(config config.AppConfigurer) *logrus.Entry {
// https://github.com/aybabtme/humanlog
log.Formatter = &logrus.JSONFormatter{}
if config.GetUserConfig().GetString("reporting") == "on" {
// this isn't really a secret token: it only has permission to push new rollbar items
hook := rollrus.NewHook("23432119147a4367abf7c0de2aa99a2d", environment)
log.Hooks.Add(hook)
}
return log.WithFields(logrus.Fields{
"debug": config.GetDebug(),
"version": config.GetVersion(),
@@ -99,14 +96,18 @@ func newLogger(config config.AppConfigurer) *logrus.Entry {
}
// NewApp bootstrap a new application
func NewApp(config config.AppConfigurer) (*App, error) {
func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
app := &App{
closers: []io.Closer{},
Config: config,
}
var err error
app.Log = newLogger(config)
app.Tr = i18n.NewLocalizer(app.Log)
app.Tr, err = i18n.NewTranslationSetFromConfig(app.Log, config.GetUserConfig().Gui.Language)
if err != nil {
return app, err
}
// if we are being called in 'demon' mode, we can just return here
app.ClientContext = os.Getenv("LAZYGIT_CLIENT_COMMAND")
@@ -114,44 +115,122 @@ func NewApp(config config.AppConfigurer) (*App, error) {
return app, nil
}
app.OSCommand = commands.NewOSCommand(app.Log, config)
app.OSCommand = oscommands.NewOSCommand(app.Log, config)
app.Updater, err = updates.NewUpdater(app.Log, config, app.OSCommand, app.Tr)
if err != nil {
return app, err
}
if err := app.setupRepo(); err != nil {
return app, err
}
app.GitCommand, err = commands.NewGitCommand(app.Log, app.OSCommand, app.Tr, app.Config)
showRecentRepos, err := app.setupRepo()
if err != nil {
return app, err
}
app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config, app.Updater)
app.GitCommand, err = commands.NewGitCommand(app.Log, app.OSCommand, app.Tr, app.Config, git_config.NewStdCachedGitConfig(app.Log))
if err != nil {
return app, err
}
app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config, app.Updater, filterPath, showRecentRepos)
if err != nil {
return app, err
}
return app, nil
}
func (app *App) setupRepo() error {
func (app *App) validateGitVersion() error {
output, err := app.OSCommand.RunCommandWithOutput("git --version")
// if we get an error anywhere here we'll show the same status
minVersionError := errors.New(app.Tr.MinGitVersionError)
if err != nil {
return minVersionError
}
if isGitVersionValid(output) {
return nil
}
return minVersionError
}
func isGitVersionValid(versionStr string) bool {
// output should be something like: 'git version 2.23.0 (blah)'
re := regexp.MustCompile(`[^\d]+([\d\.]+)`)
matches := re.FindStringSubmatch(versionStr)
if len(matches) == 0 {
return false
}
gitVersion := matches[1]
majorVersion, err := strconv.Atoi(gitVersion[0:1])
if err != nil {
return false
}
if majorVersion < 2 {
return false
}
return true
}
func (app *App) setupRepo() (bool, error) {
if err := app.validateGitVersion(); err != nil {
return false, err
}
if env.GetGitDirEnv() != "" {
// we've been given the git dir directly. We'll verify this dir when initializing our GitCommand object
return false, nil
}
// if we are not in a git repo, we ask if we want to `git init`
if err := app.OSCommand.RunCommand("git status"); err != nil {
if !strings.Contains(err.Error(), "Not a git repository") {
return err
if err := commands.VerifyInGitRepo(app.OSCommand); err != nil {
cwd, err := os.Getwd()
if err != nil {
return false, err
}
fmt.Print(app.Tr.SLocalize("CreateRepo"))
response, _ := bufio.NewReader(os.Stdin).ReadString('\n')
if strings.Trim(response, " \n") != "y" {
info, _ := os.Stat(filepath.Join(cwd, ".git"))
if info != nil && info.IsDir() {
return false, err // Current directory appears to be a git repository.
}
shouldInitRepo := true
notARepository := app.Config.GetUserConfig().NotARepository
if notARepository == "prompt" {
// Offer to initialize a new repository in current directory.
fmt.Print(app.Tr.CreateRepo)
response, _ := bufio.NewReader(os.Stdin).ReadString('\n')
if strings.Trim(response, " \n") != "y" {
shouldInitRepo = false
}
} else if notARepository == "skip" {
shouldInitRepo = false
}
if !shouldInitRepo {
// check if we have a recent repo we can open
recentRepos := app.Config.GetAppState().RecentRepos
if len(recentRepos) > 0 {
var err error
// try opening each repo in turn, in case any have been deleted
for _, repoDir := range recentRepos {
if err = os.Chdir(repoDir); err == nil {
return true, nil
}
}
return false, err
}
os.Exit(1)
}
if err := app.OSCommand.RunCommand("git init"); err != nil {
return err
return false, err
}
}
return nil
return false, nil
}
func (app *App) Run() error {
@@ -163,10 +242,18 @@ func (app *App) Run() error {
os.Exit(0)
}
err := app.Gui.RunWithSubprocesses()
err := app.Gui.RunAndHandleError()
return err
}
func gitDir() string {
dir := env.GetGitDirEnv()
if dir == "" {
return ".git"
}
return dir
}
// Rebase contains logic for when we've been run in demon mode, meaning we've
// given lazygit as a command for git to call e.g. to edit a file
func (app *App) Rebase() error {
@@ -178,7 +265,7 @@ func (app *App) Rebase() error {
return err
}
} else if strings.HasSuffix(os.Args[1], ".git/COMMIT_EDITMSG") {
} else if strings.HasSuffix(os.Args[1], filepath.Join(gitDir(), "COMMIT_EDITMSG")) { // TODO: test
// if we are rebasing and squashing, we'll see a COMMIT_EDITMSG
// but in this case we don't need to edit it, so we'll just return
} else {
@@ -203,10 +290,18 @@ func (app *App) Close() error {
func (app *App) KnownError(err error) (string, bool) {
errorMessage := err.Error()
knownErrorMessages := []string{app.Tr.MinGitVersionError}
for _, message := range knownErrorMessages {
if errorMessage == message {
return message, true
}
}
mappings := []errorMapping{
{
originalError: "fatal: not a git repository (or any of the parent directories): .git",
newError: app.Tr.SLocalize("notARepository"),
originalError: "fatal: not a git repository",
newError: app.Tr.NotARepository,
},
}
@@ -217,3 +312,25 @@ func (app *App) KnownError(err error) (string, bool) {
}
return "", false
}
func TailLogs() {
logFilePath, err := config.LogPath()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Tailing log file %s\n\n", logFilePath)
opts := humanlog.DefaultOptions
opts.Truncates = false
_, err = os.Stat(logFilePath)
if err != nil {
if os.IsNotExist(err) {
log.Fatal("Log file does not exist. Run `lazygit --debug` first to create the log file")
}
log.Fatal(err)
}
TailLogsForPlatform(logFilePath, opts)
}

44
pkg/app/app_test.go Normal file
View File

@@ -0,0 +1,44 @@
package app
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsGitVersionValid(t *testing.T) {
type scenario struct {
versionStr string
expectedResult bool
}
scenarios := []scenario{
{
"",
false,
},
{
"git version 1.9.0",
false,
},
{
"git version 1.9.0 (Apple Git-128)",
false,
},
{
"git version 2.4.0",
true,
},
{
"git version 2.24.3 (Apple Git-128)",
true,
},
}
for _, s := range scenarios {
t.Run(s.versionStr, func(t *testing.T) {
result := isGitVersionValid(s.versionStr)
assert.Equal(t, result, s.expectedResult)
})
}
}

30
pkg/app/logging.go Normal file
View File

@@ -0,0 +1,30 @@
//go:build !windows
// +build !windows
package app
import (
"github.com/aybabtme/humanlog"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"log"
"os"
)
func TailLogsForPlatform(logFilePath string, opts *humanlog.HandlerOptions) {
cmd := secureexec.Command("tail", "-f", logFilePath)
stdout, _ := cmd.StdoutPipe()
if err := cmd.Start(); err != nil {
log.Fatal(err)
}
if err := humanlog.Scanner(stdout, os.Stdout, opts); err != nil {
log.Fatal(err)
}
if err := cmd.Wait(); err != nil {
log.Fatal(err)
}
os.Exit(0)
}

View File

@@ -0,0 +1,72 @@
//go:build windows
// +build windows
package app
import (
"bufio"
"github.com/aybabtme/humanlog"
"log"
"os"
"strings"
"time"
)
func TailLogsForPlatform(logFilePath string, opts *humanlog.HandlerOptions) {
var lastModified int64 = 0
var lastOffset int64 = 0
for {
stat, err := os.Stat(logFilePath)
if err != nil {
log.Fatal(err)
}
if stat.ModTime().Unix() > lastModified {
err = TailFrom(lastOffset, logFilePath, opts)
if err != nil {
log.Fatal(err)
}
}
lastOffset = stat.Size()
time.Sleep(1 * time.Second)
}
}
func OpenAndSeek(filepath string, offset int64) (*os.File, error) {
file, err := os.Open(filepath)
if err != nil {
return nil, err
}
_, err = file.Seek(offset, 0)
if err != nil {
_ = file.Close()
return nil, err
}
return file, nil
}
func TailFrom(lastOffset int64, logFilePath string, opts *humanlog.HandlerOptions) error {
file, err := OpenAndSeek(logFilePath, lastOffset)
if err != nil {
return err
}
fileScanner := bufio.NewScanner(file)
var lines []string
for fileScanner.Scan() {
lines = append(lines, fileScanner.Text())
}
file.Close()
lineCount := len(lines)
lastTen := lines
if lineCount > 10 {
lastTen = lines[lineCount-10:]
}
for _, line := range lastTen {
reader := strings.NewReader(line)
if err := humanlog.Scanner(reader, os.Stdout, opts); err != nil {
log.Fatal(err)
}
}
return nil
}

View File

@@ -1,50 +0,0 @@
package commands
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// Branch : A git branch
// duplicating this for now
type Branch struct {
Name string
Recency string
Pushables string
Pullables string
Selected bool
}
// GetDisplayStrings returns the display string of branch
func (b *Branch) GetDisplayStrings(isFocused bool) []string {
displayName := utils.ColoredString(b.Name, b.GetColor())
if isFocused && b.Selected && b.Pushables != "" && b.Pullables != "" {
displayName = fmt.Sprintf("%s ↑%s↓%s", displayName, b.Pushables, b.Pullables)
}
return []string{b.Recency, displayName}
}
// GetColor branch color
func (b *Branch) GetColor() color.Attribute {
switch b.getType() {
case "feature":
return color.FgGreen
case "bugfix":
return color.FgYellow
case "hotfix":
return color.FgRed
default:
return theme.DefaultTextColor
}
}
// expected to return feature/bugfix/hotfix or blank string
func (b *Branch) getType() string {
return strings.Split(b.Name, "/")[0]
}

View File

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

161
pkg/commands/branches.go Normal file
View File

@@ -0,0 +1,161 @@
package commands
import (
"fmt"
"regexp"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// NewBranch create new branch
func (c *GitCommand) NewBranch(name string, base string) error {
return c.RunCommand("git checkout -b %s %s", c.OSCommand.Quote(name), c.OSCommand.Quote(base))
}
// CurrentBranchName get the current branch name and displayname.
// the first returned string is the name and the second is the displayname
// e.g. name is 123asdf and displayname is '(HEAD detached at 123asdf)'
func (c *GitCommand) CurrentBranchName() (string, string, error) {
branchName, err := c.RunCommandWithOutput("git symbolic-ref --short HEAD")
if err == nil && branchName != "HEAD\n" {
trimmedBranchName := strings.TrimSpace(branchName)
return trimmedBranchName, trimmedBranchName, nil
}
output, err := c.RunCommandWithOutput("git branch --contains")
if err != nil {
return "", "", err
}
for _, line := range utils.SplitLines(output) {
re := regexp.MustCompile(CurrentBranchNameRegex)
match := re.FindStringSubmatch(line)
if len(match) > 0 {
branchName = match[1]
displayBranchName := match[0][2:]
return branchName, displayBranchName, nil
}
}
return "HEAD", "HEAD", nil
}
// DeleteBranch delete branch
func (c *GitCommand) DeleteBranch(branch string, force bool) error {
command := "git branch -d"
if force {
command = "git branch -D"
}
return c.OSCommand.RunCommand("%s %s", command, c.OSCommand.Quote(branch))
}
// Checkout checks out a branch (or commit), with --force if you set the force arg to true
type CheckoutOptions struct {
Force bool
EnvVars []string
}
func (c *GitCommand) Checkout(branch string, options CheckoutOptions) error {
forceArg := ""
if options.Force {
forceArg = " --force"
}
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git checkout%s %s", forceArg, c.OSCommand.Quote(branch)), oscommands.RunCommandOptions{EnvVars: options.EnvVars})
}
// 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) {
cmdStr := c.GetBranchGraphCmdStr(branchName)
return c.OSCommand.RunCommandWithOutput(cmdStr)
}
func (c *GitCommand) GetUpstreamForBranch(branchName string) (string, error) {
output, err := c.RunCommandWithOutput("git rev-parse --abbrev-ref --symbolic-full-name %s@{u}", c.OSCommand.Quote(branchName))
return strings.TrimSpace(output), err
}
func (c *GitCommand) GetBranchGraphCmdStr(branchName string) string {
branchLogCmdTemplate := c.Config.GetUserConfig().Git.BranchLogCmd
templateValues := map[string]string{
"branchName": c.OSCommand.Quote(branchName),
}
return utils.ResolvePlaceholderString(branchLogCmdTemplate, templateValues)
}
func (c *GitCommand) SetUpstreamBranch(upstream string) error {
return c.RunCommand("git branch -u %s", c.OSCommand.Quote(upstream))
}
func (c *GitCommand) SetBranchUpstream(remoteName string, remoteBranchName string, branchName string) error {
return c.RunCommand("git branch --set-upstream-to=%s/%s %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(remoteBranchName), c.OSCommand.Quote(branchName))
}
func (c *GitCommand) GetCurrentBranchUpstreamDifferenceCount() (string, string) {
return c.GetCommitDifferences("HEAD", "HEAD@{u}")
}
func (c *GitCommand) GetBranchUpstreamDifferenceCount(branchName string) (string, string) {
return c.GetCommitDifferences(branchName, branchName+"@{u}")
}
// GetCommitDifferences checks how many pushables/pullables there are for the
// current branch
func (c *GitCommand) GetCommitDifferences(from, to string) (string, string) {
command := "git rev-list %s..%s --count"
pushableCount, err := c.OSCommand.RunCommandWithOutput(command, to, from)
if err != nil {
return "?", "?"
}
pullableCount, err := c.OSCommand.RunCommandWithOutput(command, from, to)
if err != nil {
return "?", "?"
}
return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount)
}
type MergeOpts struct {
FastForwardOnly bool
}
// Merge merge
func (c *GitCommand) Merge(branchName string, opts MergeOpts) error {
mergeArgs := c.Config.GetUserConfig().Git.Merging.Args
command := fmt.Sprintf("git merge --no-edit %s %s", mergeArgs, c.OSCommand.Quote(branchName))
if opts.FastForwardOnly {
command = fmt.Sprintf("%s --ff-only", command)
}
return c.OSCommand.RunCommand(command)
}
// AbortMerge abort merge
func (c *GitCommand) AbortMerge() error {
return c.RunCommand("git merge --abort")
}
func (c *GitCommand) IsHeadDetached() bool {
err := c.RunCommand("git symbolic-ref -q HEAD")
return err != nil
}
// ResetHardHead runs `git reset --hard`
func (c *GitCommand) ResetHard(ref string) error {
return c.RunCommand("git reset --hard " + c.OSCommand.Quote(ref))
}
// ResetSoft runs `git reset --soft HEAD`
func (c *GitCommand) ResetSoft(ref string) error {
return c.RunCommand("git reset --soft " + c.OSCommand.Quote(ref))
}
func (c *GitCommand) ResetMixed(ref string) error {
return c.RunCommand("git reset --mixed " + c.OSCommand.Quote(ref))
}
func (c *GitCommand) RenameBranch(oldName string, newName string) error {
return c.RunCommand("git branch --move %s %s", c.OSCommand.Quote(oldName), c.OSCommand.Quote(newName))
}

View File

@@ -0,0 +1,338 @@
package commands
import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/jesseduffield/lazygit/pkg/test"
"github.com/stretchr/testify/assert"
)
// TestGitCommandGetCommitDifferences is a function.
func TestGitCommandGetCommitDifferences(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(string, string)
}
scenarios := []scenario{
{
"Can't retrieve pushable count",
func(string, ...string) *exec.Cmd {
return secureexec.Command("test")
},
func(pushableCount string, pullableCount string) {
assert.EqualValues(t, "?", pushableCount)
assert.EqualValues(t, "?", pullableCount)
},
},
{
"Can't retrieve pullable count",
func(cmd string, args ...string) *exec.Cmd {
if args[1] == "HEAD..@{u}" {
return secureexec.Command("test")
}
return secureexec.Command("echo")
},
func(pushableCount string, pullableCount string) {
assert.EqualValues(t, "?", pushableCount)
assert.EqualValues(t, "?", pullableCount)
},
},
{
"Retrieve pullable and pushable count",
func(cmd string, args ...string) *exec.Cmd {
if args[1] == "HEAD..@{u}" {
return secureexec.Command("echo", "10")
}
return secureexec.Command("echo", "11")
},
func(pushableCount string, pullableCount string) {
assert.EqualValues(t, "11", pushableCount)
assert.EqualValues(t, "10", pullableCount)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
s.test(gitCmd.GetCommitDifferences("HEAD", "@{u}"))
})
}
}
// TestGitCommandNewBranch is a function.
func TestGitCommandNewBranch(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"checkout", "-b", "test", "master"}, args)
return secureexec.Command("echo")
}
assert.NoError(t, gitCmd.NewBranch("test", "master"))
}
// TestGitCommandDeleteBranch is a function.
func TestGitCommandDeleteBranch(t *testing.T) {
type scenario struct {
testName string
branch string
force bool
command func(string, ...string) *exec.Cmd
test func(error)
}
scenarios := []scenario{
{
"Delete a branch",
"test",
false,
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"branch", "-d", "test"}, args)
return secureexec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
},
{
"Force delete a branch",
"test",
true,
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"branch", "-D", "test"}, args)
return secureexec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
s.test(gitCmd.DeleteBranch(s.branch, s.force))
})
}
}
// TestGitCommandMerge is a function.
func TestGitCommandMerge(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"merge", "--no-edit", "test"}, args)
return secureexec.Command("echo")
}
assert.NoError(t, gitCmd.Merge("test", MergeOpts{}))
}
// TestGitCommandCheckout is a function.
func TestGitCommandCheckout(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(error)
force bool
}
scenarios := []scenario{
{
"Checkout",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"checkout", "test"}, args)
return secureexec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
false,
},
{
"Checkout forced",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"checkout", "--force", "test"}, args)
return secureexec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
true,
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
s.test(gitCmd.Checkout("test", CheckoutOptions{Force: s.force}))
})
}
}
// TestGitCommandGetBranchGraph is a function.
func TestGitCommandGetBranchGraph(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"log", "--graph", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium", "test", "--"}, args)
return secureexec.Command("echo")
}
_, err := gitCmd.GetBranchGraph("test")
assert.NoError(t, err)
}
func TestGitCommandGetAllBranchGraph(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"log", "--graph", "--all", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium"}, args)
return secureexec.Command("echo")
}
cmdStr := gitCmd.Config.GetUserConfig().Git.AllBranchesLogCmd
_, err := gitCmd.OSCommand.RunCommandWithOutput(cmdStr)
assert.NoError(t, err)
}
// TestGitCommandCurrentBranchName is a function.
func TestGitCommandCurrentBranchName(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(string, string, error)
}
scenarios := []scenario{
{
"says we are on the master branch if we are",
func(cmd string, args ...string) *exec.Cmd {
assert.Equal(t, "git", cmd)
return secureexec.Command("echo", "master")
},
func(name string, displayname string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "master", name)
assert.EqualValues(t, "master", displayname)
},
},
{
"falls back to git `git branch --contains` if symbolic-ref fails",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return secureexec.Command("test")
case "branch":
assert.EqualValues(t, []string{"branch", "--contains"}, args)
return secureexec.Command("echo", "* master")
}
return nil
},
func(name string, displayname string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "master", name)
assert.EqualValues(t, "master", displayname)
},
},
{
"handles a detached head",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return secureexec.Command("test")
case "branch":
assert.EqualValues(t, []string{"branch", "--contains"}, args)
return secureexec.Command("echo", "* (HEAD detached at 123abcd)")
}
return nil
},
func(name string, displayname string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "123abcd", name)
assert.EqualValues(t, "(HEAD detached at 123abcd)", displayname)
},
},
{
"bubbles up error if there is one",
func(cmd string, args ...string) *exec.Cmd {
assert.Equal(t, "git", cmd)
return secureexec.Command("test")
},
func(name string, displayname string, err error) {
assert.Error(t, err)
assert.EqualValues(t, "", name)
assert.EqualValues(t, "", displayname)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
s.test(gitCmd.CurrentBranchName())
})
}
}
// TestGitCommandResetHard is a function.
func TestGitCommandResetHard(t *testing.T) {
type scenario struct {
testName string
ref string
command func(string, ...string) *exec.Cmd
test func(error)
}
scenarios := []scenario{
{
"valid case",
"HEAD",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: `git reset --hard HEAD`,
Replace: "echo",
},
}),
func(err error) {
assert.NoError(t, err)
},
},
}
gitCmd := NewDummyGitCommand()
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd.OSCommand.Command = s.command
s.test(gitCmd.ResetHard(s.ref))
})
}
}

View File

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

View File

@@ -1,42 +0,0 @@
package commands
import (
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/theme"
)
// CommitFile : A git commit file
type CommitFile struct {
Sha string
Name string
DisplayString string
Status int // one of 'WHOLE' 'PART' 'NONE'
}
const (
// UNSELECTED is for when the commit file has not been added to the patch in any way
UNSELECTED = iota
// WHOLE is for when you want to add the whole diff of a file to the patch,
// including e.g. if it was deleted
WHOLE = iota
// PART is for when you're only talking about specific lines that have been modified
PART
)
// GetDisplayStrings is a function.
func (f *CommitFile) GetDisplayStrings(isFocused bool) []string {
yellow := color.New(color.FgYellow)
green := color.New(color.FgGreen)
defaultColor := color.New(theme.DefaultTextColor)
var colour *color.Color
switch f.Status {
case UNSELECTED:
colour = defaultColor
case WHOLE:
colour = green
case PART:
colour = yellow
}
return []string{colour.Sprint(f.DisplayString)}
}

View File

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

View File

@@ -1,314 +0,0 @@
package commands
import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/stretchr/testify/assert"
)
// NewDummyCommitListBuilder creates a new dummy CommitListBuilder for testing
func NewDummyCommitListBuilder() *CommitListBuilder {
osCommand := NewDummyOSCommand()
return &CommitListBuilder{
Log: NewDummyLog(),
GitCommand: NewDummyGitCommandWithOSCommand(osCommand),
OSCommand: osCommand,
Tr: i18n.NewLocalizer(NewDummyLog()),
CherryPickedCommits: []*Commit{},
}
}
// TestCommitListBuilderGetUnpushedCommits is a function.
func TestCommitListBuilderGetUnpushedCommits(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(map[string]bool)
}
scenarios := []scenario{
{
"Can't retrieve pushable commits",
func(string, ...string) *exec.Cmd {
return exec.Command("test")
},
func(pushables map[string]bool) {
assert.EqualValues(t, map[string]bool{}, pushables)
},
},
{
"Retrieve pushable commits",
func(cmd string, args ...string) *exec.Cmd {
return exec.Command("echo", "8a2bb0e\n78976bc")
},
func(pushables map[string]bool) {
assert.Len(t, pushables, 2)
assert.EqualValues(t, map[string]bool{"8a2bb0e": true, "78976bc": true}, pushables)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.getUnpushedCommits())
})
}
}
// TestCommitListBuilderGetMergeBase is a function.
func TestCommitListBuilderGetMergeBase(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(string, error)
}
scenarios := []scenario{
{
"swallows an error if the call to merge-base returns an error",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("test")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "", output)
},
},
{
"returns the commit when master",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("echo", "blah")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.Equal(t, "blah\n", output)
},
},
{
"checks against develop when a feature branch",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "feature/test")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "develop"}, args)
return exec.Command("echo", "blah")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.Equal(t, "blah\n", output)
},
},
{
"bubbles up error if there is one",
func(cmd string, args ...string) *exec.Cmd {
return exec.Command("test")
},
func(output string, err error) {
assert.Error(t, err)
assert.Equal(t, "", output)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.getMergeBase())
})
}
}
// TestCommitListBuilderGetLog is a function.
func TestCommitListBuilderGetLog(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(string)
}
scenarios := []scenario{
{
"Retrieves logs",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
return exec.Command("echo", "6f0b32f commands/git : add GetCommits tests refactor\n9d9d775 circle : remove new line")
},
func(output string) {
assert.EqualValues(t, "6f0b32f commands/git : add GetCommits tests refactor\n9d9d775 circle : remove new line\n", output)
},
},
{
"An error occurred when retrieving logs",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
return exec.Command("test")
},
func(output string) {
assert.Empty(t, output)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.getLog())
})
}
}
// TestCommitListBuilderGetCommits is a function.
func TestCommitListBuilderGetCommits(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func([]*Commit, error)
}
scenarios := []scenario{
{
"No data found",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "rev-list":
assert.EqualValues(t, []string{"rev-list", "@{u}..HEAD", "--abbrev-commit"}, args)
return exec.Command("echo")
case "log":
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
return exec.Command("echo")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("test")
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
}
return nil
},
func(commits []*Commit, err error) {
assert.NoError(t, err)
assert.Len(t, commits, 0)
},
},
{
"GetCommits returns 2 commits, 1 unpushed, the other merged",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "rev-list":
assert.EqualValues(t, []string{"rev-list", "@{u}..HEAD", "--abbrev-commit"}, args)
return exec.Command("echo", "8a2bb0e")
case "log":
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
return exec.Command("echo", "8a2bb0e commit 1\n78976bc commit 2")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("echo", "78976bc")
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
}
return nil
},
func(commits []*Commit, err error) {
assert.NoError(t, err)
assert.Len(t, commits, 2)
assert.EqualValues(t, []*Commit{
{
Sha: "8a2bb0e",
Name: "commit 1",
Status: "unpushed",
DisplayString: "8a2bb0e commit 1",
},
{
Sha: "78976bc",
Name: "commit 2",
Status: "merged",
DisplayString: "78976bc commit 2",
},
}, commits)
},
},
{
"GetCommits bubbles up an error from setCommitMergedStatuses",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "rev-list":
assert.EqualValues(t, []string{"rev-list", "@{u}..HEAD", "--abbrev-commit"}, args)
return exec.Command("echo", "8a2bb0e")
case "log":
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
return exec.Command("echo", "8a2bb0e commit 1\n78976bc commit 2")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("echo", "78976bc")
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
// here's where we are returning the error
return exec.Command("test")
case "rev-parse":
assert.EqualValues(t, []string{"rev-parse", "--short", "HEAD"}, args)
// here too
return exec.Command("test")
}
return nil
},
func(commits []*Commit, err error) {
assert.Error(t, err)
assert.Len(t, commits, 0)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.GetCommits())
})
}
}

98
pkg/commands/commits.go Normal file
View File

@@ -0,0 +1,98 @@
package commands
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
)
// RenameCommit renames the topmost commit with the given name
func (c *GitCommand) RenameCommit(name string) error {
return c.RunCommand("git commit --allow-empty --amend --only -m %s", c.OSCommand.Quote(name))
}
// ResetToCommit reset to commit
func (c *GitCommand) ResetToCommit(sha string, strength string, options oscommands.RunCommandOptions) error {
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git reset --%s %s", strength, sha), options)
}
func (c *GitCommand) CommitCmdStr(message string, flags string) string {
splitMessage := strings.Split(message, "\n")
lineArgs := ""
for _, line := range splitMessage {
lineArgs += fmt.Sprintf(" -m %s", c.OSCommand.Quote(line))
}
flagsStr := ""
if flags != "" {
flagsStr = fmt.Sprintf(" %s", flags)
}
return fmt.Sprintf("git commit%s%s", flagsStr, lineArgs)
}
// Get the subject of the HEAD commit
func (c *GitCommand) GetHeadCommitMessage() (string, error) {
cmdStr := "git log -1 --pretty=%s"
message, err := c.OSCommand.RunCommandWithOutput(cmdStr)
return strings.TrimSpace(message), err
}
func (c *GitCommand) GetCommitMessage(commitSha string) (string, error) {
cmdStr := "git rev-list --format=%B --max-count=1 " + commitSha
messageWithHeader, err := c.OSCommand.RunCommandWithOutput(cmdStr)
message := strings.Join(strings.SplitAfter(messageWithHeader, "\n")[1:], "\n")
return strings.TrimSpace(message), err
}
func (c *GitCommand) GetCommitMessageFirstLine(sha string) (string, error) {
return c.RunCommandWithOutput("git show --no-patch --pretty=format:%%s %s", sha)
}
// AmendHead amends HEAD with whatever is staged in your working tree
func (c *GitCommand) AmendHead() error {
return c.OSCommand.RunCommand(c.AmendHeadCmdStr())
}
func (c *GitCommand) AmendHeadCmdStr() string {
return "git commit --amend --no-edit --allow-empty"
}
func (c *GitCommand) ShowCmdStr(sha string, filterPath string) string {
filterPathArg := ""
if filterPath != "" {
filterPathArg = fmt.Sprintf(" -- %s", c.OSCommand.Quote(filterPath))
}
return fmt.Sprintf("git show --submodule --color=%s --no-renames --stat -p %s %s", c.colorArg(), sha, filterPathArg)
}
// Revert reverts the selected commit by sha
func (c *GitCommand) Revert(sha string) error {
return c.RunCommand("git revert %s", sha)
}
func (c *GitCommand) RevertMerge(sha string, parentNumber int) error {
return c.RunCommand("git revert %s -m %d", sha, parentNumber)
}
// CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD
func (c *GitCommand) CherryPickCommits(commits []*models.Commit) error {
todo := ""
for _, commit := range commits {
todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo
}
cmd, err := c.PrepareInteractiveRebaseCommand("HEAD", todo, false)
if err != nil {
return err
}
return c.OSCommand.RunPreparedCommand(cmd)
}
// CreateFixupCommit creates a commit that fixes up a previous commit
func (c *GitCommand) CreateFixupCommit(sha string) error {
return c.RunCommand("git commit --fixup=%s", sha)
}

View File

@@ -0,0 +1,112 @@
package commands
import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/jesseduffield/lazygit/pkg/test"
"github.com/stretchr/testify/assert"
)
// TestGitCommandRenameCommit is a function.
func TestGitCommandRenameCommit(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, args)
return secureexec.Command("echo")
}
assert.NoError(t, gitCmd.RenameCommit("test"))
}
// TestGitCommandResetToCommit is a function.
func TestGitCommandResetToCommit(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"reset", "--hard", "78976bc"}, args)
return secureexec.Command("echo")
}
assert.NoError(t, gitCmd.ResetToCommit("78976bc", "hard", oscommands.RunCommandOptions{}))
}
// TestGitCommandCommitStr is a function.
func TestGitCommandCommitStr(t *testing.T) {
gitCmd := NewDummyGitCommand()
type scenario struct {
testName string
message string
flags string
expected string
}
scenarios := []scenario{
{
testName: "Commit",
message: "test",
flags: "",
expected: "git commit -m " + gitCmd.OSCommand.Quote("test"),
},
{
testName: "Commit with --no-verify flag",
message: "test",
flags: "--no-verify",
expected: "git commit --no-verify -m " + gitCmd.OSCommand.Quote("test"),
},
{
testName: "Commit with multiline message",
message: "line1\nline2",
flags: "",
expected: "git commit -m " + gitCmd.OSCommand.Quote("line1") + " -m " + gitCmd.OSCommand.Quote("line2"),
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
cmdStr := gitCmd.CommitCmdStr(s.message, s.flags)
assert.Equal(t, s.expected, cmdStr)
})
}
}
// TestGitCommandCreateFixupCommit is a function.
func TestGitCommandCreateFixupCommit(t *testing.T) {
type scenario struct {
testName string
sha string
command func(string, ...string) *exec.Cmd
test func(error)
}
scenarios := []scenario{
{
"valid case",
"12345",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: `git commit --fixup=12345`,
Replace: "echo",
},
}),
func(err error) {
assert.NoError(t, err)
},
},
}
gitCmd := NewDummyGitCommand()
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd.OSCommand.Command = s.command
s.test(gitCmd.CreateFixupCommit(s.sha))
})
}
}

50
pkg/commands/config.go Normal file
View File

@@ -0,0 +1,50 @@
package commands
import (
"os"
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (c *GitCommand) ConfiguredPager() string {
if os.Getenv("GIT_PAGER") != "" {
return os.Getenv("GIT_PAGER")
}
if os.Getenv("PAGER") != "" {
return os.Getenv("PAGER")
}
output := c.GitConfig.Get("core.pager")
return strings.Split(output, "\n")[0]
}
func (c *GitCommand) GetPager(width int) string {
useConfig := c.Config.GetUserConfig().Git.Paging.UseConfig
if useConfig {
pager := c.ConfiguredPager()
return strings.Split(pager, "| less")[0]
}
templateValues := map[string]string{
"columnWidth": strconv.Itoa(width/2 - 6),
}
pagerTemplate := c.Config.GetUserConfig().Git.Paging.Pager
return utils.ResolvePlaceholderString(pagerTemplate, templateValues)
}
func (c *GitCommand) colorArg() string {
return c.Config.GetUserConfig().Git.Paging.ColorArg
}
// 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 {
overrideGpg := c.Config.GetUserConfig().Git.OverrideGpg
if overrideGpg {
return false
}
return c.GitConfig.GetBool("commit.gpgsign")
}

View File

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

View File

@@ -1,14 +0,0 @@
package commands
import "github.com/go-errors/errors"
// WrapError wraps an error for the sake of showing a stack trace at the top level
// the go-errors package, for some reason, does not return nil when you try to wrap
// a non-error, so we're just doing it here
func WrapError(err error) error {
if err == nil {
return err
}
return errors.Wrap(err, 0)
}

View File

@@ -1,9 +0,0 @@
// +build windows
package commands
// RunCommandWithOutputLiveWrapper runs a command live but because of windows compatibility this command can't be ran there
// TODO: Remove this hack and replace it with a proper way to run commands live on windows
func RunCommandWithOutputLiveWrapper(c *OSCommand, command string, output func(string) string) error {
return c.RunCommand(command)
}

View File

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

363
pkg/commands/files.go Normal file
View File

@@ -0,0 +1,363 @@
package commands
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"time"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// CatFile obtains the content of a file
func (c *GitCommand) CatFile(fileName string) (string, error) {
buf, err := ioutil.ReadFile(fileName)
if err != nil {
return "", nil
}
return string(buf), nil
}
func (c *GitCommand) OpenMergeToolCmd() string {
return "git mergetool"
}
func (c *GitCommand) OpenMergeTool() error {
return c.OSCommand.RunCommand("git mergetool")
}
// StageFile stages a file
func (c *GitCommand) StageFile(fileName string) error {
return c.RunCommand("git add -- %s", c.OSCommand.Quote(fileName))
}
// StageAll stages all files
func (c *GitCommand) StageAll() error {
return c.RunCommand("git add -A")
}
// UnstageAll unstages all files
func (c *GitCommand) UnstageAll() error {
return c.RunCommand("git reset")
}
// UnStageFile unstages a file
// we accept an array of filenames for the cases where a file has been renamed i.e.
// we accept the current name and the previous name
func (c *GitCommand) UnStageFile(fileNames []string, reset bool) error {
command := "git rm --cached --force -- %s"
if reset {
command = "git reset HEAD -- %s"
}
for _, name := range fileNames {
if err := c.OSCommand.RunCommand(command, c.OSCommand.Quote(name)); err != nil {
return err
}
}
return nil
}
func (c *GitCommand) BeforeAndAfterFileForRename(file *models.File) (*models.File, *models.File, error) {
if !file.IsRename() {
return nil, nil, errors.New("Expected renamed file")
}
// we've got a file that represents a rename from one file to another. Here we will refetch
// all files, passing the --no-renames flag and then recursively call the function
// again for the before file and after file.
filesWithoutRenames := c.GetStatusFiles(GetStatusFileOptions{NoRenames: true})
var beforeFile *models.File
var afterFile *models.File
for _, f := range filesWithoutRenames {
if f.Name == file.PreviousName {
beforeFile = f
}
if f.Name == file.Name {
afterFile = f
}
}
if beforeFile == nil || afterFile == nil {
return nil, nil, errors.New("Could not find deleted file or new file for file rename")
}
if beforeFile.IsRename() || afterFile.IsRename() {
// probably won't happen but we want to ensure we don't get an infinite loop
return nil, nil, errors.New("Nested rename found")
}
return beforeFile, afterFile, nil
}
// DiscardAllFileChanges directly
func (c *GitCommand) DiscardAllFileChanges(file *models.File) error {
if file.IsRename() {
beforeFile, afterFile, err := c.BeforeAndAfterFileForRename(file)
if err != nil {
return err
}
if err := c.DiscardAllFileChanges(beforeFile); err != nil {
return err
}
if err := c.DiscardAllFileChanges(afterFile); err != nil {
return err
}
return nil
}
quotedFileName := c.OSCommand.Quote(file.Name)
if file.ShortStatus == "AA" {
if err := c.RunCommand("git checkout --ours -- %s", quotedFileName); err != nil {
return err
}
if err := c.RunCommand("git add -- %s", quotedFileName); err != nil {
return err
}
return nil
}
if file.ShortStatus == "DU" {
return c.RunCommand("git rm -- %s", quotedFileName)
}
// if the file isn't tracked, we assume you want to delete it
if file.HasStagedChanges || file.HasMergeConflicts {
if err := c.RunCommand("git reset -- %s", quotedFileName); err != nil {
return err
}
}
if file.ShortStatus == "DD" || file.ShortStatus == "AU" {
return nil
}
if file.Added {
return c.OSCommand.RemoveFile(file.Name)
}
return c.DiscardUnstagedFileChanges(file)
}
func (c *GitCommand) DiscardAllDirChanges(node *filetree.FileNode) error {
// this could be more efficient but we would need to handle all the edge cases
return node.ForEachFile(c.DiscardAllFileChanges)
}
func (c *GitCommand) DiscardUnstagedDirChanges(node *filetree.FileNode) error {
if err := c.RemoveUntrackedDirFiles(node); err != nil {
return err
}
quotedPath := c.OSCommand.Quote(node.GetPath())
if err := c.RunCommand("git checkout -- %s", quotedPath); err != nil {
return err
}
return nil
}
func (c *GitCommand) RemoveUntrackedDirFiles(node *filetree.FileNode) error {
untrackedFilePaths := node.GetPathsMatching(
func(n *filetree.FileNode) bool { return n.File != nil && !n.File.GetIsTracked() },
)
for _, path := range untrackedFilePaths {
err := os.Remove(path)
if err != nil {
return err
}
}
return nil
}
// DiscardUnstagedFileChanges directly
func (c *GitCommand) DiscardUnstagedFileChanges(file *models.File) error {
quotedFileName := c.OSCommand.Quote(file.Name)
return c.RunCommand("git checkout -- %s", quotedFileName)
}
// Ignore adds a file to the gitignore for the repo
func (c *GitCommand) Ignore(filename string) error {
return c.OSCommand.AppendLineToFile(".gitignore", filename)
}
// WorktreeFileDiff returns the diff of a file
func (c *GitCommand) WorktreeFileDiff(file *models.File, plain bool, cached bool, ignoreWhitespace bool) string {
// for now we assume an error means the file was deleted
s, _ := c.OSCommand.RunCommandWithOutput(c.WorktreeFileDiffCmdStr(file, plain, cached, ignoreWhitespace))
return s
}
func (c *GitCommand) WorktreeFileDiffCmdStr(node models.IFile, plain bool, cached bool, ignoreWhitespace bool) string {
cachedArg := ""
trackedArg := "--"
colorArg := c.colorArg()
quotedPath := c.OSCommand.Quote(node.GetPath())
ignoreWhitespaceArg := ""
if cached {
cachedArg = "--cached"
}
if !node.GetIsTracked() && !node.GetHasStagedChanges() && !cached {
trackedArg = "--no-index -- /dev/null"
}
if plain {
colorArg = "never"
}
if ignoreWhitespace {
ignoreWhitespaceArg = "--ignore-all-space"
}
return fmt.Sprintf("git diff --submodule --no-ext-diff --color=%s %s %s %s %s", colorArg, ignoreWhitespaceArg, cachedArg, trackedArg, quotedPath)
}
func (c *GitCommand) ApplyPatch(patch string, flags ...string) error {
filepath := filepath.Join(c.Config.GetTempDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch")
c.Log.Infof("saving temporary patch to %s", filepath)
if err := c.OSCommand.CreateFileWithContent(filepath, patch); err != nil {
return err
}
flagStr := ""
for _, flag := range flags {
flagStr += " --" + flag
}
return c.RunCommand("git apply %s %s", flagStr, c.OSCommand.Quote(filepath))
}
// ShowFileDiff get the diff of specified from and to. Typically this will be used for a single commit so it'll be 123abc^..123abc
// but when we're in diff mode it could be any 'from' to any 'to'. The reverse flag is also here thanks to diff mode.
func (c *GitCommand) ShowFileDiff(from string, to string, reverse bool, fileName string, plain bool) (string, error) {
cmdStr := c.ShowFileDiffCmdStr(from, to, reverse, fileName, plain)
return c.OSCommand.RunCommandWithOutput(cmdStr)
}
func (c *GitCommand) ShowFileDiffCmdStr(from string, to string, reverse bool, fileName string, plain bool) string {
colorArg := c.colorArg()
if plain {
colorArg = "never"
}
reverseFlag := ""
if reverse {
reverseFlag = " -R "
}
return fmt.Sprintf("git diff --submodule --no-ext-diff --no-renames --color=%s %s %s %s -- %s", colorArg, from, to, reverseFlag, c.OSCommand.Quote(fileName))
}
// CheckoutFile checks out the file for the given commit
func (c *GitCommand) CheckoutFile(commitSha, fileName string) error {
return c.RunCommand("git checkout %s -- %s", commitSha, c.OSCommand.Quote(fileName))
}
// DiscardOldFileChanges discards changes to a file from an old commit
func (c *GitCommand) DiscardOldFileChanges(commits []*models.Commit, commitIndex int, fileName string) error {
if err := c.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil {
return err
}
// check if file exists in previous commit (this command returns an error if the file doesn't exist)
if err := c.RunCommand("git cat-file -e HEAD^:%s", c.OSCommand.Quote(fileName)); err != nil {
if err := c.OSCommand.Remove(fileName); err != nil {
return err
}
if err := c.StageFile(fileName); err != nil {
return err
}
} else if err := c.CheckoutFile("HEAD^", fileName); err != nil {
return err
}
// amend the commit
err := c.AmendHead()
if err != nil {
return err
}
// continue
return c.GenericMergeOrRebaseAction("rebase", "continue")
}
// DiscardAnyUnstagedFileChanges discards any unstages file changes via `git checkout -- .`
func (c *GitCommand) DiscardAnyUnstagedFileChanges() error {
return c.RunCommand("git checkout -- .")
}
// RemoveTrackedFiles will delete the given file(s) even if they are currently tracked
func (c *GitCommand) RemoveTrackedFiles(name string) error {
return c.RunCommand("git rm -r --cached -- %s", c.OSCommand.Quote(name))
}
// RemoveUntrackedFiles runs `git clean -fd`
func (c *GitCommand) RemoveUntrackedFiles() error {
return c.RunCommand("git clean -fd")
}
// ResetAndClean removes all unstaged changes and removes all untracked files
func (c *GitCommand) ResetAndClean() error {
submoduleConfigs, err := c.GetSubmoduleConfigs()
if err != nil {
return err
}
if len(submoduleConfigs) > 0 {
if err := c.ResetSubmodules(submoduleConfigs); err != nil {
return err
}
}
if err := c.ResetHard("HEAD"); err != nil {
return err
}
return c.RemoveUntrackedFiles()
}
func (c *GitCommand) EditFileCmdStr(filename string, lineNumber int) (string, error) {
editor := c.Config.GetUserConfig().OS.EditCommand
if editor == "" {
editor = c.GitConfig.Get("core.editor")
}
if editor == "" {
editor = c.OSCommand.Getenv("GIT_EDITOR")
}
if editor == "" {
editor = c.OSCommand.Getenv("VISUAL")
}
if editor == "" {
editor = c.OSCommand.Getenv("EDITOR")
}
if editor == "" {
if err := c.OSCommand.RunCommand("which vi"); err == nil {
editor = "vi"
}
}
if editor == "" {
return "", errors.New("No editor defined in config file, $GIT_EDITOR, $VISUAL, $EDITOR, or git config")
}
templateValues := map[string]string{
"editor": editor,
"filename": c.OSCommand.Quote(filename),
"line": strconv.Itoa(lineNumber),
}
editCmdTemplate := c.Config.GetUserConfig().OS.EditCommandTemplate
return utils.ResolvePlaceholderString(editCmdTemplate, templateValues), nil
}

878
pkg/commands/files_test.go Normal file
View File

@@ -0,0 +1,878 @@
package commands
import (
"fmt"
"io/ioutil"
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/jesseduffield/lazygit/pkg/test"
"github.com/stretchr/testify/assert"
)
// TestGitCommandStageFile is a function.
func TestGitCommandStageFile(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"add", "--", "test.txt"}, args)
return secureexec.Command("echo")
}
assert.NoError(t, gitCmd.StageFile("test.txt"))
}
// TestGitCommandUnstageFile is a function.
func TestGitCommandUnstageFile(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(error)
reset bool
}
scenarios := []scenario{
{
"Remove an untracked file from staging",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"rm", "--cached", "--force", "--", "test.txt"}, args)
return secureexec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
false,
},
{
"Remove a tracked file from staging",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"reset", "HEAD", "--", "test.txt"}, args)
return secureexec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
true,
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
s.test(gitCmd.UnStageFile([]string{"test.txt"}, s.reset))
})
}
}
// TestGitCommandDiscardAllFileChanges is a function.
// these tests don't cover everything, in part because we already have an integration
// test which does cover everything. I don't want to unnecessarily assert on the 'how'
// when the 'what' is what matters
func TestGitCommandDiscardAllFileChanges(t *testing.T) {
type scenario struct {
testName string
command func() (func(string, ...string) *exec.Cmd, *[][]string)
test func(*[][]string, error)
file *models.File
removeFile func(string) error
}
scenarios := []scenario{
{
"An error occurred when resetting",
func() (func(string, ...string) *exec.Cmd, *[][]string) {
cmdsCalled := [][]string{}
return func(cmd string, args ...string) *exec.Cmd {
cmdsCalled = append(cmdsCalled, args)
return secureexec.Command("test")
}, &cmdsCalled
},
func(cmdsCalled *[][]string, err error) {
assert.Error(t, err)
assert.Len(t, *cmdsCalled, 1)
assert.EqualValues(t, *cmdsCalled, [][]string{
{"reset", "--", "test"},
})
},
&models.File{
Name: "test",
HasStagedChanges: true,
},
func(string) error {
return nil
},
},
{
"An error occurred when removing file",
func() (func(string, ...string) *exec.Cmd, *[][]string) {
cmdsCalled := [][]string{}
return func(cmd string, args ...string) *exec.Cmd {
cmdsCalled = append(cmdsCalled, args)
return secureexec.Command("test")
}, &cmdsCalled
},
func(cmdsCalled *[][]string, err error) {
assert.Error(t, err)
assert.EqualError(t, err, "an error occurred when removing file")
assert.Len(t, *cmdsCalled, 0)
},
&models.File{
Name: "test",
Tracked: false,
Added: true,
},
func(string) error {
return fmt.Errorf("an error occurred when removing file")
},
},
{
"An error occurred with checkout",
func() (func(string, ...string) *exec.Cmd, *[][]string) {
cmdsCalled := [][]string{}
return func(cmd string, args ...string) *exec.Cmd {
cmdsCalled = append(cmdsCalled, args)
return secureexec.Command("test")
}, &cmdsCalled
},
func(cmdsCalled *[][]string, err error) {
assert.Error(t, err)
assert.Len(t, *cmdsCalled, 1)
assert.EqualValues(t, *cmdsCalled, [][]string{
{"checkout", "--", "test"},
})
},
&models.File{
Name: "test",
Tracked: true,
HasStagedChanges: false,
},
func(string) error {
return nil
},
},
{
"Checkout only",
func() (func(string, ...string) *exec.Cmd, *[][]string) {
cmdsCalled := [][]string{}
return func(cmd string, args ...string) *exec.Cmd {
cmdsCalled = append(cmdsCalled, args)
return secureexec.Command("echo")
}, &cmdsCalled
},
func(cmdsCalled *[][]string, err error) {
assert.NoError(t, err)
assert.Len(t, *cmdsCalled, 1)
assert.EqualValues(t, *cmdsCalled, [][]string{
{"checkout", "--", "test"},
})
},
&models.File{
Name: "test",
Tracked: true,
HasStagedChanges: false,
},
func(string) error {
return nil
},
},
{
"Reset and checkout staged changes",
func() (func(string, ...string) *exec.Cmd, *[][]string) {
cmdsCalled := [][]string{}
return func(cmd string, args ...string) *exec.Cmd {
cmdsCalled = append(cmdsCalled, args)
return secureexec.Command("echo")
}, &cmdsCalled
},
func(cmdsCalled *[][]string, err error) {
assert.NoError(t, err)
assert.Len(t, *cmdsCalled, 2)
assert.EqualValues(t, *cmdsCalled, [][]string{
{"reset", "--", "test"},
{"checkout", "--", "test"},
})
},
&models.File{
Name: "test",
Tracked: true,
HasStagedChanges: true,
},
func(string) error {
return nil
},
},
{
"Reset and checkout merge conflicts",
func() (func(string, ...string) *exec.Cmd, *[][]string) {
cmdsCalled := [][]string{}
return func(cmd string, args ...string) *exec.Cmd {
cmdsCalled = append(cmdsCalled, args)
return secureexec.Command("echo")
}, &cmdsCalled
},
func(cmdsCalled *[][]string, err error) {
assert.NoError(t, err)
assert.Len(t, *cmdsCalled, 2)
assert.EqualValues(t, *cmdsCalled, [][]string{
{"reset", "--", "test"},
{"checkout", "--", "test"},
})
},
&models.File{
Name: "test",
Tracked: true,
HasMergeConflicts: true,
},
func(string) error {
return nil
},
},
{
"Reset and remove",
func() (func(string, ...string) *exec.Cmd, *[][]string) {
cmdsCalled := [][]string{}
return func(cmd string, args ...string) *exec.Cmd {
cmdsCalled = append(cmdsCalled, args)
return secureexec.Command("echo")
}, &cmdsCalled
},
func(cmdsCalled *[][]string, err error) {
assert.NoError(t, err)
assert.Len(t, *cmdsCalled, 1)
assert.EqualValues(t, *cmdsCalled, [][]string{
{"reset", "--", "test"},
})
},
&models.File{
Name: "test",
Tracked: false,
Added: true,
HasStagedChanges: true,
},
func(filename string) error {
assert.Equal(t, "test", filename)
return nil
},
},
{
"Remove only",
func() (func(string, ...string) *exec.Cmd, *[][]string) {
cmdsCalled := [][]string{}
return func(cmd string, args ...string) *exec.Cmd {
cmdsCalled = append(cmdsCalled, args)
return secureexec.Command("echo")
}, &cmdsCalled
},
func(cmdsCalled *[][]string, err error) {
assert.NoError(t, err)
assert.Len(t, *cmdsCalled, 0)
},
&models.File{
Name: "test",
Tracked: false,
Added: true,
HasStagedChanges: false,
},
func(filename string) error {
assert.Equal(t, "test", filename)
return nil
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
var cmdsCalled *[][]string
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command, cmdsCalled = s.command()
gitCmd.OSCommand.SetRemoveFile(s.removeFile)
s.test(cmdsCalled, gitCmd.DiscardAllFileChanges(s.file))
})
}
}
// TestGitCommandDiff is a function.
func TestGitCommandDiff(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
file *models.File
plain bool
cached bool
ignoreWhitespace bool
}
scenarios := []scenario{
{
"Default case",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=always", "--", "test.txt"}, args)
return secureexec.Command("echo")
},
&models.File{
Name: "test.txt",
HasStagedChanges: false,
Tracked: true,
},
false,
false,
false,
},
{
"cached",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=always", "--cached", "--", "test.txt"}, args)
return secureexec.Command("echo")
},
&models.File{
Name: "test.txt",
HasStagedChanges: false,
Tracked: true,
},
false,
true,
false,
},
{
"plain",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=never", "--", "test.txt"}, args)
return secureexec.Command("echo")
},
&models.File{
Name: "test.txt",
HasStagedChanges: false,
Tracked: true,
},
true,
false,
false,
},
{
"File not tracked and file has no staged changes",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=always", "--no-index", "--", "/dev/null", "test.txt"}, args)
return secureexec.Command("echo")
},
&models.File{
Name: "test.txt",
HasStagedChanges: false,
Tracked: false,
},
false,
false,
false,
},
{
"Default case (ignore whitespace)",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=always", "--ignore-all-space", "--", "test.txt"}, args)
return secureexec.Command("echo")
},
&models.File{
Name: "test.txt",
HasStagedChanges: false,
Tracked: true,
},
false,
false,
true,
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
gitCmd.WorktreeFileDiff(s.file, s.plain, s.cached, s.ignoreWhitespace)
})
}
}
// TestGitCommandCheckoutFile is a function.
func TestGitCommandCheckoutFile(t *testing.T) {
type scenario struct {
testName string
commitSha string
fileName string
command func(string, ...string) *exec.Cmd
test func(error)
}
scenarios := []scenario{
{
"typical case",
"11af912",
"test999.txt",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: "git checkout 11af912 -- test999.txt",
Replace: "echo",
},
}),
func(err error) {
assert.NoError(t, err)
},
},
{
"returns error if there is one",
"11af912",
"test999.txt",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: "git checkout 11af912 -- test999.txt",
Replace: "test",
},
}),
func(err error) {
assert.Error(t, err)
},
},
}
gitCmd := NewDummyGitCommand()
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd.OSCommand.Command = s.command
s.test(gitCmd.CheckoutFile(s.commitSha, s.fileName))
})
}
}
func TestGitCommandApplyPatch(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(error)
}
scenarios := []scenario{
{
"valid case",
func(cmd string, args ...string) *exec.Cmd {
assert.Equal(t, "git", cmd)
assert.EqualValues(t, []string{"apply", "--cached"}, args[0:2])
filename := args[2]
content, err := ioutil.ReadFile(filename)
assert.NoError(t, err)
assert.Equal(t, "test", string(content))
return secureexec.Command("echo", "done")
},
func(err error) {
assert.NoError(t, err)
},
},
{
"command returns error",
func(cmd string, args ...string) *exec.Cmd {
assert.Equal(t, "git", cmd)
assert.EqualValues(t, []string{"apply", "--cached"}, args[0:2])
filename := args[2]
// TODO: Ideally we want to mock out OSCommand here so that we're not
// double handling testing it's CreateTempFile functionality,
// but it is going to take a bit of work to make a proper mock for it
// so I'm leaving it for another PR
content, err := ioutil.ReadFile(filename)
assert.NoError(t, err)
assert.Equal(t, "test", string(content))
return secureexec.Command("test")
},
func(err error) {
assert.Error(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
s.test(gitCmd.ApplyPatch("test", "cached"))
})
}
}
func TestGitCommandDiscardOldFileChanges(t *testing.T) {
type scenario struct {
testName string
gitConfigMockResponses map[string]string
commits []*models.Commit
commitIndex int
fileName string
command func(string, ...string) *exec.Cmd
test func(error)
}
scenarios := []scenario{
{
"returns error when index outside of range of commits",
nil,
[]*models.Commit{},
0,
"test999.txt",
nil,
func(err error) {
assert.Error(t, err)
},
},
{
"returns error when using gpg",
map[string]string{"commit.gpgsign": "true"},
[]*models.Commit{{Name: "commit", Sha: "123456"}},
0,
"test999.txt",
nil,
func(err error) {
assert.Error(t, err)
},
},
{
"checks out file if it already existed",
nil,
[]*models.Commit{
{Name: "commit", Sha: "123456"},
{Name: "commit2", Sha: "abcdef"},
},
0,
"test999.txt",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: "git rebase --interactive --autostash --keep-empty abcdef",
Replace: "echo",
},
{
Expect: "git cat-file -e HEAD^:test999.txt",
Replace: "echo",
},
{
Expect: "git checkout HEAD^ -- test999.txt",
Replace: "echo",
},
{
Expect: "git commit --amend --no-edit --allow-empty",
Replace: "echo",
},
{
Expect: "git rebase --continue",
Replace: "echo",
},
}),
func(err error) {
assert.NoError(t, err)
},
},
// test for when the file was created within the commit requires a refactor to support proper mocks
// currently we'd need to mock out the os.Remove function and that's gonna introduce tech debt
}
gitCmd := NewDummyGitCommand()
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd.OSCommand.Command = s.command
gitCmd.GitConfig = git_config.NewFakeGitConfig(s.gitConfigMockResponses)
s.test(gitCmd.DiscardOldFileChanges(s.commits, s.commitIndex, s.fileName))
})
}
}
// TestGitCommandDiscardUnstagedFileChanges is a function.
func TestGitCommandDiscardUnstagedFileChanges(t *testing.T) {
type scenario struct {
testName string
file *models.File
command func(string, ...string) *exec.Cmd
test func(error)
}
scenarios := []scenario{
{
"valid case",
&models.File{Name: "test.txt"},
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: `git checkout -- "test.txt"`,
Replace: "echo",
},
}),
func(err error) {
assert.NoError(t, err)
},
},
}
gitCmd := NewDummyGitCommand()
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd.OSCommand.Command = s.command
s.test(gitCmd.DiscardUnstagedFileChanges(s.file))
})
}
}
// TestGitCommandDiscardAnyUnstagedFileChanges is a function.
func TestGitCommandDiscardAnyUnstagedFileChanges(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(error)
}
scenarios := []scenario{
{
"valid case",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: `git checkout -- .`,
Replace: "echo",
},
}),
func(err error) {
assert.NoError(t, err)
},
},
}
gitCmd := NewDummyGitCommand()
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd.OSCommand.Command = s.command
s.test(gitCmd.DiscardAnyUnstagedFileChanges())
})
}
}
// TestGitCommandRemoveUntrackedFiles is a function.
func TestGitCommandRemoveUntrackedFiles(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(error)
}
scenarios := []scenario{
{
"valid case",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: `git clean -fd`,
Replace: "echo",
},
}),
func(err error) {
assert.NoError(t, err)
},
},
}
gitCmd := NewDummyGitCommand()
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd.OSCommand.Command = s.command
s.test(gitCmd.RemoveUntrackedFiles())
})
}
}
// TestEditFileCmdStr is a function.
func TestEditFileCmdStr(t *testing.T) {
gitCmd := NewDummyGitCommand()
type scenario struct {
filename string
configEditCommand string
configEditCommandTemplate string
command func(string, ...string) *exec.Cmd
getenv func(string) string
gitConfigMockResponses map[string]string
test func(string, error)
}
scenarios := []scenario{
{
"test",
"",
"{{editor}} {{filename}}",
func(name string, arg ...string) *exec.Cmd {
return secureexec.Command("exit", "1")
},
func(env string) string {
return ""
},
nil,
func(cmdStr string, err error) {
assert.EqualError(t, err, "No editor defined in config file, $GIT_EDITOR, $VISUAL, $EDITOR, or git config")
},
},
{
"test",
"nano",
"{{editor}} {{filename}}",
func(name string, args ...string) *exec.Cmd {
assert.Equal(t, "which", name)
return secureexec.Command("echo")
},
func(env string) string {
return ""
},
nil,
func(cmdStr string, err error) {
assert.NoError(t, err)
assert.Equal(t, "nano "+gitCmd.OSCommand.Quote("test"), cmdStr)
},
},
{
"test",
"",
"{{editor}} {{filename}}",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "which", name)
return secureexec.Command("exit", "1")
},
func(env string) string {
return ""
},
map[string]string{"core.editor": "nano"},
func(cmdStr string, err error) {
assert.NoError(t, err)
assert.Equal(t, "nano "+gitCmd.OSCommand.Quote("test"), cmdStr)
},
},
{
"test",
"",
"{{editor}} {{filename}}",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "which", name)
return secureexec.Command("exit", "1")
},
func(env string) string {
if env == "VISUAL" {
return "nano"
}
return ""
},
nil,
func(cmdStr string, err error) {
assert.NoError(t, err)
},
},
{
"test",
"",
"{{editor}} {{filename}}",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "which", name)
return secureexec.Command("exit", "1")
},
func(env string) string {
if env == "EDITOR" {
return "emacs"
}
return ""
},
nil,
func(cmdStr string, err error) {
assert.NoError(t, err)
assert.Equal(t, "emacs "+gitCmd.OSCommand.Quote("test"), cmdStr)
},
},
{
"test",
"",
"{{editor}} {{filename}}",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "which", name)
return secureexec.Command("echo")
},
func(env string) string {
return ""
},
nil,
func(cmdStr string, err error) {
assert.NoError(t, err)
assert.Equal(t, "vi "+gitCmd.OSCommand.Quote("test"), cmdStr)
},
},
{
"file/with space",
"",
"{{editor}} {{filename}}",
func(name string, args ...string) *exec.Cmd {
assert.Equal(t, "which", name)
return secureexec.Command("echo")
},
func(env string) string {
return ""
},
nil,
func(cmdStr string, err error) {
assert.NoError(t, err)
assert.Equal(t, "vi "+gitCmd.OSCommand.Quote("file/with space"), cmdStr)
},
},
{
"open file/at line",
"vim",
"{{editor}} +{{line}} {{filename}}",
func(name string, args ...string) *exec.Cmd {
assert.Equal(t, "which", name)
return secureexec.Command("echo")
},
func(env string) string {
return ""
},
nil,
func(cmdStr string, err error) {
assert.NoError(t, err)
assert.Equal(t, "vim +1 "+gitCmd.OSCommand.Quote("open file/at line"), cmdStr)
},
},
}
for _, s := range scenarios {
gitCmd.Config.GetUserConfig().OS.EditCommand = s.configEditCommand
gitCmd.Config.GetUserConfig().OS.EditCommandTemplate = s.configEditCommandTemplate
gitCmd.OSCommand.Command = s.command
gitCmd.OSCommand.Getenv = s.getenv
gitCmd.GitConfig = git_config.NewFakeGitConfig(s.gitConfigMockResponses)
s.test(gitCmd.EditFileCmdStr(s.filename, 1))
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,59 @@
package git_config
import (
"strings"
"github.com/sirupsen/logrus"
)
type IGitConfig interface {
Get(string) string
GetBool(string) bool
}
type CachedGitConfig struct {
cache map[string]string
getKey func(string) (string, error)
log *logrus.Entry
}
func NewStdCachedGitConfig(log *logrus.Entry) *CachedGitConfig {
return NewCachedGitConfig(getGitConfigValue, log)
}
func NewCachedGitConfig(getKey func(string) (string, error), log *logrus.Entry) *CachedGitConfig {
return &CachedGitConfig{
cache: make(map[string]string),
getKey: getKey,
log: log,
}
}
func (self *CachedGitConfig) Get(key string) string {
if value, ok := self.cache[key]; ok {
self.log.Debugf("using cache for key " + key)
return value
}
value := self.getAux(key)
self.cache[key] = value
return value
}
func (self *CachedGitConfig) getAux(key string) string {
value, err := self.getKey(key)
if err != nil {
self.log.Debugf("Error getting git config value for key: " + key + ". Error: " + err.Error())
return ""
}
return strings.TrimSpace(value)
}
func (self *CachedGitConfig) GetBool(key string) bool {
return isTruthy(self.Get(key))
}
func isTruthy(value string) bool {
lcValue := strings.ToLower(value)
return lcValue == "true" || lcValue == "1" || lcValue == "yes" || lcValue == "on"
}

View File

@@ -0,0 +1,116 @@
package git_config
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
)
func TestGetBool(t *testing.T) {
type scenario struct {
testName string
mockResponses map[string]string
expected bool
}
scenarios := []scenario{
{
"Option global and local config commit.gpgsign is not set",
map[string]string{},
false,
},
{
"Some other random key is set",
map[string]string{"blah": "blah"},
false,
},
{
"Option commit.gpgsign is true",
map[string]string{"commit.gpgsign": "True"},
true,
},
{
"Option commit.gpgsign is on",
map[string]string{"commit.gpgsign": "ON"},
true,
},
{
"Option commit.gpgsign is yes",
map[string]string{"commit.gpgsign": "YeS"},
true,
},
{
"Option commit.gpgsign is 1",
map[string]string{"commit.gpgsign": "1"},
true,
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
fake := NewFakeGitConfig(s.mockResponses)
real := NewCachedGitConfig(
func(key string) (string, error) {
return fake.Get(key), nil
},
utils.NewDummyLog(),
)
result := real.GetBool("commit.gpgsign")
assert.Equal(t, s.expected, result)
})
}
}
func TestGet(t *testing.T) {
type scenario struct {
testName string
mockResponses map[string]string
expected string
}
scenarios := []scenario{
{
"not set",
map[string]string{},
"",
},
{
"is set",
map[string]string{"commit.gpgsign": "blah"},
"blah",
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
fake := NewFakeGitConfig(s.mockResponses)
real := NewCachedGitConfig(
func(key string) (string, error) {
return fake.Get(key), nil
},
utils.NewDummyLog(),
)
result := real.Get("commit.gpgsign")
assert.Equal(t, s.expected, result)
})
}
// verifying that the cache is used
count := 0
real := NewCachedGitConfig(
func(key string) (string, error) {
count++
assert.Equal(t, "commit.gpgsign", key)
return "blah", nil
},
utils.NewDummyLog(),
)
result := real.Get("commit.gpgsign")
assert.Equal(t, "blah", result)
result = real.Get("commit.gpgsign")
assert.Equal(t, "blah", result)
assert.Equal(t, 1, count)
}

View File

@@ -0,0 +1,22 @@
package git_config
type FakeGitConfig struct {
mockResponses map[string]string
}
func NewFakeGitConfig(mockResponses map[string]string) *FakeGitConfig {
return &FakeGitConfig{
mockResponses: mockResponses,
}
}
func (self *FakeGitConfig) Get(key string) string {
if self.mockResponses == nil {
return ""
}
return self.mockResponses[key]
}
func (self *FakeGitConfig) GetBool(key string) bool {
return isTruthy(self.Get(key))
}

View File

@@ -0,0 +1,56 @@
package git_config
import (
"bytes"
"fmt"
"io/ioutil"
"os/exec"
"strings"
"syscall"
"github.com/jesseduffield/lazygit/pkg/secureexec"
)
// including license from https://github.com/tcnksm/go-gitconfig because this file is an adaptation of that repo's code
// Copyright (c) 2014 tcnksm
// MIT License
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
func getGitConfigValue(key string) (string, error) {
gitArgs := []string{"config", "--get", "--null", key}
var stdout bytes.Buffer
cmd := secureexec.Command("git", gitArgs...)
cmd.Stdout = &stdout
cmd.Stderr = ioutil.Discard
err := cmd.Run()
if exitError, ok := err.(*exec.ExitError); ok {
if waitStatus, ok := exitError.Sys().(syscall.WaitStatus); ok {
if waitStatus.ExitStatus() == 1 {
return "", fmt.Errorf("the key `%s` is not found", key)
}
}
return "", err
}
return strings.TrimRight(stdout.String(), "\000"), nil
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,167 @@
package commands
import (
"regexp"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
// context:
// we want to only show 'safe' branches (ones that haven't e.g. been deleted)
// which `git branch -a` gives us, but we also want the recency data that
// git reflog gives us.
// So we get the HEAD, then append get the reflog branches that intersect with
// our safe branches, then add the remaining safe branches, ensuring uniqueness
// along the way
// if we find out we need to use one of these functions in the git.go file, we
// can just pull them out of here and put them there and then call them from in here
// BranchListBuilder returns a list of Branch objects for the current repo
type BranchListBuilder struct {
Log *logrus.Entry
GitCommand *GitCommand
ReflogCommits []*models.Commit
}
// NewBranchListBuilder builds a new branch list builder
func NewBranchListBuilder(log *logrus.Entry, gitCommand *GitCommand, reflogCommits []*models.Commit) (*BranchListBuilder, error) {
return &BranchListBuilder{
Log: log,
GitCommand: gitCommand,
ReflogCommits: reflogCommits,
}, nil
}
func (b *BranchListBuilder) obtainBranches() []*models.Branch {
cmdStr := `git for-each-ref --sort=-committerdate --format="%(HEAD)|%(refname:short)|%(upstream:short)|%(upstream:track)" refs/heads`
output, err := b.GitCommand.OSCommand.RunCommandWithOutput(cmdStr)
if err != nil {
panic(err)
}
trimmedOutput := strings.TrimSpace(output)
outputLines := strings.Split(trimmedOutput, "\n")
branches := make([]*models.Branch, 0, len(outputLines))
for _, line := range outputLines {
if line == "" {
continue
}
split := strings.Split(line, SEPARATION_CHAR)
if len(split) != 4 {
// Ignore line if it isn't separated into 4 parts
// This is probably a warning message, for more info see:
// https://github.com/jesseduffield/lazygit/issues/1385#issuecomment-885580439
continue
}
name := strings.TrimPrefix(split[1], "heads/")
branch := &models.Branch{
Name: name,
Pullables: "?",
Pushables: "?",
Head: split[0] == "*",
}
upstreamName := split[2]
if upstreamName == "" {
branches = append(branches, branch)
continue
}
branch.UpstreamName = upstreamName
track := split[3]
re := regexp.MustCompile(`ahead (\d+)`)
match := re.FindStringSubmatch(track)
if len(match) > 1 {
branch.Pushables = match[1]
} else {
branch.Pushables = "0"
}
re = regexp.MustCompile(`behind (\d+)`)
match = re.FindStringSubmatch(track)
if len(match) > 1 {
branch.Pullables = match[1]
} else {
branch.Pullables = "0"
}
branches = append(branches, branch)
}
return branches
}
// Build the list of branches for the current repo
func (b *BranchListBuilder) Build() []*models.Branch {
branches := b.obtainBranches()
reflogBranches := b.obtainReflogBranches()
// loop through reflog branches. If there is a match, merge them, then remove it from the branches and keep it in the reflog branches
branchesWithRecency := make([]*models.Branch, 0)
outer:
for _, reflogBranch := range reflogBranches {
for j, branch := range branches {
if branch.Head {
continue
}
if strings.EqualFold(reflogBranch.Name, branch.Name) {
branch.Recency = reflogBranch.Recency
branchesWithRecency = append(branchesWithRecency, branch)
branches = append(branches[0:j], branches[j+1:]...)
continue outer
}
}
}
branches = append(branchesWithRecency, branches...)
foundHead := false
for i, branch := range branches {
if branch.Head {
foundHead = true
branch.Recency = " *"
branches = append(branches[0:i], branches[i+1:]...)
branches = append([]*models.Branch{branch}, branches...)
break
}
}
if !foundHead {
currentBranchName, currentBranchDisplayName, err := b.GitCommand.CurrentBranchName()
if err != nil {
panic(err)
}
branches = append([]*models.Branch{{Name: currentBranchName, DisplayName: currentBranchDisplayName, Head: true, Recency: " *"}}, branches...)
}
return branches
}
// TODO: only look at the new reflog commits, and otherwise store the recencies in
// int form against the branch to recalculate the time ago
func (b *BranchListBuilder) obtainReflogBranches() []*models.Branch {
foundBranchesMap := map[string]bool{}
re := regexp.MustCompile(`checkout: moving from ([\S]+) to ([\S]+)`)
reflogBranches := make([]*models.Branch, 0, len(b.ReflogCommits))
for _, commit := range b.ReflogCommits {
if match := re.FindStringSubmatch(commit.Name); len(match) == 3 {
recency := utils.UnixToTimeAgo(commit.UnixTimestamp)
for _, branchName := range match[1:] {
if !foundBranchesMap[branchName] {
foundBranchesMap[branchName] = true
reflogBranches = append(reflogBranches, &models.Branch{
Recency: recency,
Name: branchName,
})
}
}
}
}
return reflogBranches
}

View File

@@ -0,0 +1,42 @@
package commands
import (
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
)
// GetFilesInDiff get the specified commit files
func (c *GitCommand) GetFilesInDiff(from string, to string, reverse bool) ([]*models.CommitFile, error) {
reverseFlag := ""
if reverse {
reverseFlag = " -R "
}
filenames, err := c.RunCommandWithOutput("git diff --submodule --no-ext-diff --name-status -z --no-renames %s %s %s", reverseFlag, from, to)
if err != nil {
return nil, err
}
return c.getCommitFilesFromFilenames(filenames), nil
}
// filenames string is something like "file1\nfile2\nfile3"
func (c *GitCommand) getCommitFilesFromFilenames(filenames string) []*models.CommitFile {
commitFiles := make([]*models.CommitFile, 0)
lines := strings.Split(strings.TrimRight(filenames, "\x00"), "\x00")
n := len(lines)
for i := 0; i < n-1; i += 2 {
// typical result looks like 'A my_file' meaning my_file was added
changeStatus := lines[i]
name := lines[i+1]
commitFiles = append(commitFiles, &models.CommitFile{
Name: name,
ChangeStatus: changeStatus,
})
}
return commitFiles
}

View File

@@ -0,0 +1,376 @@
package commands
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/sirupsen/logrus"
)
// context:
// here we get the commits from git log but format them to show whether they're
// unpushed/pushed/merged into the base branch or not, or if they're yet to
// be processed as part of a rebase (these won't appear in git log but we
// grab them from the rebase-related files in the .git directory to show them
// if we find out we need to use one of these functions in the git.go file, we
// can just pull them out of here and put them there and then call them from in here
const SEPARATION_CHAR = "|"
// CommitListBuilder returns a list of Branch objects for the current repo
type CommitListBuilder struct {
Log *logrus.Entry
GitCommand *GitCommand
OSCommand *oscommands.OSCommand
Tr *i18n.TranslationSet
}
// NewCommitListBuilder builds a new commit list builder
func NewCommitListBuilder(log *logrus.Entry, gitCommand *GitCommand, osCommand *oscommands.OSCommand, tr *i18n.TranslationSet) *CommitListBuilder {
return &CommitListBuilder{
Log: log,
GitCommand: gitCommand,
OSCommand: osCommand,
Tr: tr,
}
}
// extractCommitFromLine takes a line from a git log and extracts the sha, message, date, and tag if present
// then puts them into a commit object
// example input:
// 8ad01fe32fcc20f07bc6693f87aa4977c327f1e1|10 hours ago|Jesse Duffield| (HEAD -> master, tag: v0.15.2)|refresh commits when adding a tag
func (c *CommitListBuilder) extractCommitFromLine(line string) *models.Commit {
split := strings.Split(line, SEPARATION_CHAR)
sha := split[0]
unixTimestamp := split[1]
author := split[2]
extraInfo := strings.TrimSpace(split[3])
parentHashes := split[4]
message := strings.Join(split[5:], SEPARATION_CHAR)
tags := []string{}
if extraInfo != "" {
re := regexp.MustCompile(`tag: ([^,\)]+)`)
tagMatch := re.FindStringSubmatch(extraInfo)
if len(tagMatch) > 1 {
tags = append(tags, tagMatch[1])
}
}
unitTimestampInt, _ := strconv.Atoi(unixTimestamp)
return &models.Commit{
Sha: sha,
Name: message,
Tags: tags,
ExtraInfo: extraInfo,
UnixTimestamp: int64(unitTimestampInt),
Author: author,
Parents: strings.Split(parentHashes, " "),
}
}
type GetCommitsOptions struct {
Limit bool
FilterPath string
IncludeRebaseCommits bool
RefName string // e.g. "HEAD" or "my_branch"
}
func (c *CommitListBuilder) MergeRebasingCommits(commits []*models.Commit) ([]*models.Commit, error) {
// chances are we have as many commits as last time so we'll set the capacity to be the old length
result := make([]*models.Commit, 0, len(commits))
for i, commit := range commits {
if commit.Status != "rebasing" { // removing the existing rebase commits so we can add the refreshed ones
result = append(result, commits[i:]...)
break
}
}
rebaseMode, err := c.GitCommand.RebaseMode()
if err != nil {
return nil, err
}
if rebaseMode == "" {
// not in rebase mode so return original commits
return result, nil
}
rebasingCommits, err := c.getRebasingCommits(rebaseMode)
if err != nil {
return nil, err
}
if len(rebasingCommits) > 0 {
result = append(rebasingCommits, result...)
}
return result, nil
}
// GetCommits obtains the commits of the current branch
func (c *CommitListBuilder) GetCommits(opts GetCommitsOptions) ([]*models.Commit, error) {
commits := []*models.Commit{}
var rebasingCommits []*models.Commit
rebaseMode, err := c.GitCommand.RebaseMode()
if err != nil {
return nil, err
}
if opts.IncludeRebaseCommits && opts.FilterPath == "" {
var err error
rebasingCommits, err = c.MergeRebasingCommits(commits)
if err != nil {
return nil, err
}
commits = append(commits, rebasingCommits...)
}
passedFirstPushedCommit := false
firstPushedCommit, err := c.getFirstPushedCommit(opts.RefName)
if err != nil {
// must have no upstream branch so we'll consider everything as pushed
passedFirstPushedCommit = true
}
cmd := c.getLogCmd(opts)
err = oscommands.RunLineOutputCmd(cmd, func(line string) (bool, error) {
if strings.Split(line, " ")[0] != "gpg:" {
commit := c.extractCommitFromLine(line)
if commit.Sha == firstPushedCommit {
passedFirstPushedCommit = true
}
commit.Status = map[bool]string{true: "unpushed", false: "pushed"}[!passedFirstPushedCommit]
commits = append(commits, commit)
}
return false, nil
})
if err != nil {
return nil, err
}
if rebaseMode != "" {
currentCommit := commits[len(rebasingCommits)]
youAreHere := style.FgYellow.Sprintf("<-- %s ---", c.Tr.YouAreHere)
currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name)
}
commits, err = c.setCommitMergedStatuses(opts.RefName, commits)
if err != nil {
return nil, err
}
return commits, nil
}
// getRebasingCommits obtains the commits that we're in the process of rebasing
func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*models.Commit, error) {
switch rebaseMode {
case REBASE_MODE_MERGING:
return c.getNormalRebasingCommits()
case REBASE_MODE_INTERACTIVE:
return c.getInteractiveRebasingCommits()
default:
return nil, nil
}
}
func (c *CommitListBuilder) getNormalRebasingCommits() ([]*models.Commit, error) {
rewrittenCount := 0
bytesContent, err := ioutil.ReadFile(filepath.Join(c.GitCommand.DotGitDir, "rebase-apply/rewritten"))
if err == nil {
content := string(bytesContent)
rewrittenCount = len(strings.Split(content, "\n"))
}
// we know we're rebasing, so lets get all the files whose names have numbers
commits := []*models.Commit{}
err = filepath.Walk(filepath.Join(c.GitCommand.DotGitDir, "rebase-apply"), func(path string, f os.FileInfo, err error) error {
if rewrittenCount > 0 {
rewrittenCount--
return nil
}
if err != nil {
return err
}
re := regexp.MustCompile(`^\d+$`)
if !re.MatchString(f.Name()) {
return nil
}
bytesContent, err := ioutil.ReadFile(path)
if err != nil {
return err
}
content := string(bytesContent)
commit, err := c.commitFromPatch(content)
if err != nil {
return err
}
commits = append([]*models.Commit{commit}, commits...)
return nil
})
if err != nil {
return nil, err
}
return commits, nil
}
// git-rebase-todo example:
// pick ac446ae94ee560bdb8d1d057278657b251aaef17 ac446ae
// pick afb893148791a2fbd8091aeb81deba4930c73031 afb8931
// git-rebase-todo.backup example:
// pick 49cbba374296938ea86bbd4bf4fee2f6ba5cccf6 third commit on master
// pick ac446ae94ee560bdb8d1d057278657b251aaef17 blah commit on master
// pick afb893148791a2fbd8091aeb81deba4930c73031 fourth commit on master
// getInteractiveRebasingCommits takes our git-rebase-todo and our git-rebase-todo.backup files
// and extracts out the sha and names of commits that we still have to go
// in the rebase:
func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*models.Commit, error) {
bytesContent, err := ioutil.ReadFile(filepath.Join(c.GitCommand.DotGitDir, "rebase-merge/git-rebase-todo"))
if err != nil {
c.Log.Error(fmt.Sprintf("error occurred reading git-rebase-todo: %s", err.Error()))
// we assume an error means the file doesn't exist so we just return
return nil, nil
}
commits := []*models.Commit{}
lines := strings.Split(string(bytesContent), "\n")
for _, line := range lines {
if line == "" || line == "noop" {
return commits, nil
}
if strings.HasPrefix(line, "#") {
continue
}
splitLine := strings.Split(line, " ")
commits = append([]*models.Commit{{
Sha: splitLine[1],
Name: strings.Join(splitLine[2:], " "),
Status: "rebasing",
Action: splitLine[0],
}}, commits...)
}
return commits, nil
}
// assuming the file starts like this:
// From e93d4193e6dd45ca9cf3a5a273d7ba6cd8b8fb20 Mon Sep 17 00:00:00 2001
// From: Lazygit Tester <test@example.com>
// Date: Wed, 5 Dec 2018 21:03:23 +1100
// Subject: second commit on master
func (c *CommitListBuilder) commitFromPatch(content string) (*models.Commit, error) {
lines := strings.Split(content, "\n")
sha := strings.Split(lines[0], " ")[1]
name := strings.TrimPrefix(lines[3], "Subject: ")
return &models.Commit{
Sha: sha,
Name: name,
Status: "rebasing",
}, nil
}
func (c *CommitListBuilder) setCommitMergedStatuses(refName string, commits []*models.Commit) ([]*models.Commit, error) {
ancestor, err := c.getMergeBase(refName)
if err != nil {
return nil, err
}
if ancestor == "" {
return commits, nil
}
passedAncestor := false
for i, commit := range commits {
if strings.HasPrefix(ancestor, commit.Sha) {
passedAncestor = true
}
if commit.Status != "pushed" {
continue
}
if passedAncestor {
commits[i].Status = "merged"
}
}
return commits, nil
}
func (c *CommitListBuilder) getMergeBase(refName string) (string, error) {
currentBranch, _, err := c.GitCommand.CurrentBranchName()
if err != nil {
return "", err
}
baseBranch := "master"
if strings.HasPrefix(currentBranch, "feature/") {
baseBranch = "develop"
}
// swallowing error because it's not a big deal; probably because there are no commits yet
output, _ := c.OSCommand.RunCommandWithOutput("git merge-base %s %s", c.OSCommand.Quote(refName), c.OSCommand.Quote(baseBranch))
return ignoringWarnings(output), nil
}
func ignoringWarnings(commandOutput string) string {
trimmedOutput := strings.TrimSpace(commandOutput)
split := strings.Split(trimmedOutput, "\n")
// need to get last line in case the first line is a warning about how the error is ambiguous.
// At some point we should find a way to make it unambiguous
lastLine := split[len(split)-1]
return lastLine
}
// getFirstPushedCommit returns the first commit SHA which has been pushed to the ref's upstream.
// all commits above this are deemed unpushed and marked as such.
func (c *CommitListBuilder) getFirstPushedCommit(refName string) (string, error) {
output, err := c.OSCommand.RunCommandWithOutput("git merge-base %s %s@{u}", c.OSCommand.Quote(refName), c.OSCommand.Quote(refName))
if err != nil {
return "", err
}
return ignoringWarnings(output), nil
}
// getLog gets the git log.
func (c *CommitListBuilder) getLogCmd(opts GetCommitsOptions) *exec.Cmd {
limitFlag := ""
if opts.Limit {
limitFlag = "-300"
}
filterFlag := ""
if opts.FilterPath != "" {
filterFlag = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(opts.FilterPath))
}
return c.OSCommand.ExecutableFromString(
fmt.Sprintf(
"git log %s --oneline --pretty=format:\"%%H%s%%at%s%%aN%s%%d%s%%p%s%%s\" %s --abbrev=%d %s",
c.OSCommand.Quote(opts.RefName),
SEPARATION_CHAR,
SEPARATION_CHAR,
SEPARATION_CHAR,
SEPARATION_CHAR,
SEPARATION_CHAR,
limitFlag,
20,
filterFlag,
),
)
}

View File

@@ -0,0 +1,114 @@
package commands
import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
)
// NewDummyCommitListBuilder creates a new dummy CommitListBuilder for testing
func NewDummyCommitListBuilder() *CommitListBuilder {
osCommand := oscommands.NewDummyOSCommand()
return &CommitListBuilder{
Log: utils.NewDummyLog(),
GitCommand: NewDummyGitCommandWithOSCommand(osCommand),
OSCommand: osCommand,
Tr: i18n.NewTranslationSet(utils.NewDummyLog(), "auto"),
}
}
// TestCommitListBuilderGetMergeBase is a function.
func TestCommitListBuilderGetMergeBase(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(string, error)
}
scenarios := []scenario{
{
"swallows an error if the call to merge-base returns an error",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return secureexec.Command("echo", "master")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return secureexec.Command("test")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "", output)
},
},
{
"returns the commit when master",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return secureexec.Command("echo", "master")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return secureexec.Command("echo", "blah")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.Equal(t, "blah", output)
},
},
{
"checks against develop when a feature branch",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return secureexec.Command("echo", "feature/test")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "develop"}, args)
return secureexec.Command("echo", "blah")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.Equal(t, "blah", output)
},
},
{
"bubbles up error if there is one",
func(cmd string, args ...string) *exec.Cmd {
return secureexec.Command("test")
},
func(output string, err error) {
assert.Error(t, err)
assert.Equal(t, "", output)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.getMergeBase("HEAD"))
})
}
}

View File

@@ -0,0 +1,116 @@
package commands
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// GetStatusFiles git status files
type GetStatusFileOptions struct {
NoRenames bool
}
func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
// check if config wants us ignoring untracked files
untrackedFilesSetting := c.GitConfig.Get("status.showUntrackedFiles")
if untrackedFilesSetting == "" {
untrackedFilesSetting = "all"
}
untrackedFilesArg := fmt.Sprintf("--untracked-files=%s", untrackedFilesSetting)
statuses, err := c.GitStatus(GitStatusOptions{NoRenames: opts.NoRenames, UntrackedFilesArg: untrackedFilesArg})
if err != nil {
c.Log.Error(err)
}
files := []*models.File{}
for _, status := range statuses {
if strings.HasPrefix(status.StatusString, "warning") {
c.Log.Warningf("warning when calling git status: %s", status.StatusString)
continue
}
change := status.Change
stagedChange := change[0:1]
unstagedChange := change[1:2]
untracked := utils.IncludesString([]string{"??", "A ", "AM"}, change)
hasNoStagedChanges := utils.IncludesString([]string{" ", "U", "?"}, stagedChange)
hasMergeConflicts := utils.IncludesString([]string{"DD", "AA", "UU", "AU", "UA", "UD", "DU"}, change)
hasInlineMergeConflicts := utils.IncludesString([]string{"UU", "AA"}, change)
file := &models.File{
Name: status.Name,
PreviousName: status.PreviousName,
DisplayString: status.StatusString,
HasStagedChanges: !hasNoStagedChanges,
HasUnstagedChanges: unstagedChange != " ",
Tracked: !untracked,
Deleted: unstagedChange == "D" || stagedChange == "D",
Added: unstagedChange == "A" || untracked,
HasMergeConflicts: hasMergeConflicts,
HasInlineMergeConflicts: hasInlineMergeConflicts,
Type: c.OSCommand.FileType(status.Name),
ShortStatus: change,
}
files = append(files, file)
}
return files
}
// GitStatus returns the file status of the repo
type GitStatusOptions struct {
NoRenames bool
UntrackedFilesArg string
}
type FileStatus struct {
StatusString string
Change string // ??, MM, AM, ...
Name string
PreviousName string
}
func (c *GitCommand) GitStatus(opts GitStatusOptions) ([]FileStatus, error) {
noRenamesFlag := ""
if opts.NoRenames {
noRenamesFlag = "--no-renames"
}
statusLines, err := c.RunCommandWithOutput("git status %s --porcelain -z %s", opts.UntrackedFilesArg, noRenamesFlag)
if err != nil {
return []FileStatus{}, err
}
splitLines := strings.Split(statusLines, "\x00")
response := []FileStatus{}
for i := 0; i < len(splitLines); i++ {
original := splitLines[i]
if len(original) < 3 {
continue
}
status := FileStatus{
StatusString: original,
Change: original[:2],
Name: original[3:],
PreviousName: "",
}
if strings.HasPrefix(status.Change, "R") {
// if a line starts with 'R' then the next line is the original file.
status.PreviousName = strings.TrimSpace(splitLines[i+1])
status.StatusString = fmt.Sprintf("%s %s -> %s", status.Change, status.PreviousName, status.Name)
i++
}
response = append(response, status)
}
return response, nil
}

View File

@@ -0,0 +1,227 @@
package commands
import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/stretchr/testify/assert"
)
// TestGitCommandGetStatusFiles is a function.
func TestGitCommandGetStatusFiles(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func([]*models.File)
}
scenarios := []scenario{
{
"No files found",
func(cmd string, args ...string) *exec.Cmd {
return secureexec.Command("echo")
},
func(files []*models.File) {
assert.Len(t, files, 0)
},
},
{
"Several files found",
func(cmd string, args ...string) *exec.Cmd {
return secureexec.Command(
"printf",
`MM file1.txt\0A file3.txt\0AM file2.txt\0?? file4.txt\0UU file5.txt`,
)
},
func(files []*models.File) {
assert.Len(t, files, 5)
expected := []*models.File{
{
Name: "file1.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "MM file1.txt",
Type: "other",
ShortStatus: "MM",
},
{
Name: "file3.txt",
HasStagedChanges: true,
HasUnstagedChanges: false,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "A file3.txt",
Type: "other",
ShortStatus: "A ",
},
{
Name: "file2.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "AM file2.txt",
Type: "other",
ShortStatus: "AM",
},
{
Name: "file4.txt",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "?? file4.txt",
Type: "other",
ShortStatus: "??",
},
{
Name: "file5.txt",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: true,
HasInlineMergeConflicts: true,
DisplayString: "UU file5.txt",
Type: "other",
ShortStatus: "UU",
},
}
assert.EqualValues(t, expected, files)
},
},
{
"File with new line char",
func(cmd string, args ...string) *exec.Cmd {
return secureexec.Command(
"printf",
`MM a\nb.txt`,
)
},
func(files []*models.File) {
assert.Len(t, files, 1)
expected := []*models.File{
{
Name: "a\nb.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "MM a\nb.txt",
Type: "other",
ShortStatus: "MM",
},
}
assert.EqualValues(t, expected, files)
},
},
{
"Renamed files",
func(cmd string, args ...string) *exec.Cmd {
return secureexec.Command(
"printf",
`R after1.txt\0before1.txt\0RM after2.txt\0before2.txt`,
)
},
func(files []*models.File) {
assert.Len(t, files, 2)
expected := []*models.File{
{
Name: "after1.txt",
PreviousName: "before1.txt",
HasStagedChanges: true,
HasUnstagedChanges: false,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "R before1.txt -> after1.txt",
Type: "other",
ShortStatus: "R ",
},
{
Name: "after2.txt",
PreviousName: "before2.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "RM before2.txt -> after2.txt",
Type: "other",
ShortStatus: "RM",
},
}
assert.EqualValues(t, expected, files)
},
},
{
"File with arrow in name",
func(cmd string, args ...string) *exec.Cmd {
return secureexec.Command(
"printf",
`?? a -> b.txt`,
)
},
func(files []*models.File) {
assert.Len(t, files, 1)
expected := []*models.File{
{
Name: "a -> b.txt",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "?? a -> b.txt",
Type: "other",
ShortStatus: "??",
},
}
assert.EqualValues(t, expected, files)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
s.test(gitCmd.GetStatusFiles(GetStatusFileOptions{}))
})
}
}

View File

@@ -0,0 +1,53 @@
package commands
import (
"fmt"
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
)
// GetReflogCommits only returns the new reflog commits since the given lastReflogCommit
// if none is passed (i.e. it's value is nil) then we get all the reflog commits
func (c *GitCommand) GetReflogCommits(lastReflogCommit *models.Commit, filterPath string) ([]*models.Commit, bool, error) {
commits := make([]*models.Commit, 0)
filterPathArg := ""
if filterPath != "" {
filterPathArg = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(filterPath))
}
cmd := c.OSCommand.ExecutableFromString(fmt.Sprintf(`git log -g --abbrev=20 --format="%%h %%ct %%gs" %s`, filterPathArg))
onlyObtainedNewReflogCommits := false
err := oscommands.RunLineOutputCmd(cmd, func(line string) (bool, error) {
fields := strings.SplitN(line, " ", 3)
if len(fields) <= 2 {
return false, nil
}
unixTimestamp, _ := strconv.Atoi(fields[1])
commit := &models.Commit{
Sha: fields[0],
Name: fields[2],
UnixTimestamp: int64(unixTimestamp),
Status: "reflog",
}
if lastReflogCommit != nil && commit.Sha == lastReflogCommit.Sha && commit.UnixTimestamp == lastReflogCommit.UnixTimestamp {
onlyObtainedNewReflogCommits = true
// after this point we already have these reflogs loaded so we'll simply return the new ones
return true, nil
}
commits = append(commits, commit)
return false, nil
})
if err != nil {
return nil, false, err
}
return commits, onlyObtainedNewReflogCommits, nil
}

View File

@@ -0,0 +1,60 @@
package commands
import (
"fmt"
"regexp"
"sort"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
)
func (c *GitCommand) GetRemotes() ([]*models.Remote, error) {
// get remote branches
unescaped := "git branch -r"
remoteBranchesStr, err := c.OSCommand.RunCommandWithOutput(unescaped)
if err != nil {
return nil, err
}
goGitRemotes, err := c.Repo.Remotes()
if err != nil {
return nil, err
}
// first step is to get our remotes from go-git
remotes := make([]*models.Remote, len(goGitRemotes))
for i, goGitRemote := range goGitRemotes {
remoteName := goGitRemote.Config().Name
re := regexp.MustCompile(fmt.Sprintf(`%s\/([\S]+)`, remoteName))
matches := re.FindAllStringSubmatch(remoteBranchesStr, -1)
branches := make([]*models.RemoteBranch, len(matches))
for j, match := range matches {
branches[j] = &models.RemoteBranch{
Name: match[1],
RemoteName: remoteName,
}
}
remotes[i] = &models.Remote{
Name: goGitRemote.Config().Name,
Urls: goGitRemote.Config().URLs,
Branches: branches,
}
}
// now lets sort our remotes by name alphabetically
sort.Slice(remotes, func(i, j int) bool {
// we want origin at the top because we'll be most likely to want it
if remotes[i].Name == "origin" {
return true
}
if remotes[j].Name == "origin" {
return false
}
return strings.ToLower(remotes[i].Name) < strings.ToLower(remotes[j].Name)
})
return remotes, nil
}

View File

@@ -0,0 +1,65 @@
package commands
import (
"regexp"
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (c *GitCommand) getUnfilteredStashEntries() []*models.StashEntry {
unescaped := "git stash list --pretty='%gs'"
rawString, _ := c.OSCommand.RunCommandWithOutput(unescaped)
stashEntries := []*models.StashEntry{}
for i, line := range utils.SplitLines(rawString) {
stashEntries = append(stashEntries, stashEntryFromLine(line, i))
}
return stashEntries
}
// GetStashEntries stash entries
func (c *GitCommand) GetStashEntries(filterPath string) []*models.StashEntry {
if filterPath == "" {
return c.getUnfilteredStashEntries()
}
rawString, err := c.RunCommandWithOutput("git stash list --name-only")
if err != nil {
return c.getUnfilteredStashEntries()
}
stashEntries := []*models.StashEntry{}
var currentStashEntry *models.StashEntry
lines := utils.SplitLines(rawString)
isAStash := func(line string) bool { return strings.HasPrefix(line, "stash@{") }
re := regexp.MustCompile(`stash@\{(\d+)\}`)
outer:
for i := 0; i < len(lines); i++ {
if !isAStash(lines[i]) {
continue
}
match := re.FindStringSubmatch(lines[i])
idx, err := strconv.Atoi(match[1])
if err != nil {
return c.getUnfilteredStashEntries()
}
currentStashEntry = stashEntryFromLine(lines[i], idx)
for i+1 < len(lines) && !isAStash(lines[i+1]) {
i++
if lines[i] == filterPath {
stashEntries = append(stashEntries, currentStashEntry)
continue outer
}
}
}
return stashEntries
}
func stashEntryFromLine(line string, index int) *models.StashEntry {
return &models.StashEntry{
Name: line,
Index: index,
}
}

View File

@@ -0,0 +1,61 @@
package commands
import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/stretchr/testify/assert"
)
// TestGitCommandGetStashEntries is a function.
func TestGitCommandGetStashEntries(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func([]*models.StashEntry)
}
scenarios := []scenario{
{
"No stash entries found",
func(string, ...string) *exec.Cmd {
return secureexec.Command("echo")
},
func(entries []*models.StashEntry) {
assert.Len(t, entries, 0)
},
},
{
"Several stash entries found",
func(string, ...string) *exec.Cmd {
return secureexec.Command("echo", "WIP on add-pkg-commands-test: 55c6af2 increase parallel build\nWIP on master: bb86a3f update github template")
},
func(entries []*models.StashEntry) {
expected := []*models.StashEntry{
{
Index: 0,
Name: "WIP on add-pkg-commands-test: 55c6af2 increase parallel build",
},
{
Index: 1,
Name: "WIP on master: bb86a3f update github template",
},
}
assert.Len(t, entries, 2)
assert.EqualValues(t, expected, entries)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
s.test(gitCmd.GetStashEntries(""))
})
}
}

View File

@@ -0,0 +1,34 @@
package commands
import (
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (c *GitCommand) GetTags() ([]*models.Tag, error) {
// get remote branches, sorted by creation date (descending)
// see: https://git-scm.com/docs/git-tag#Documentation/git-tag.txt---sortltkeygt
remoteBranchesStr, err := c.OSCommand.RunCommandWithOutput(`git tag --list --sort=-creatordate`)
if err != nil {
return nil, err
}
content := utils.TrimTrailingNewline(remoteBranchesStr)
if content == "" {
return nil, nil
}
split := strings.Split(content, "\n")
// first step is to get our remotes from go-git
tags := make([]*models.Tag, len(split))
for i, tagName := range split {
tags[i] = &models.Tag{
Name: tagName,
}
}
return tags, nil
}

View File

@@ -0,0 +1,49 @@
package models
// Branch : A git branch
// duplicating this for now
type Branch struct {
Name string
// the displayname is something like '(HEAD detached at 123asdf)', whereas in that case the name would be '123asdf'
DisplayName string
Recency string
Pushables string
Pullables string
UpstreamName string
Head bool
}
func (b *Branch) RefName() string {
return b.Name
}
func (b *Branch) ID() string {
return b.RefName()
}
func (b *Branch) Description() string {
return b.RefName()
}
// this method does not consider the case where the git config states that a branch is tracking the config.
// The Pullables value here is based on whether or not we saw an upstream when doing `git branch`
func (b *Branch) IsTrackingRemote() bool {
return b.IsRealBranch() && b.Pullables != "?"
}
func (b *Branch) MatchesUpstream() bool {
return b.IsRealBranch() && b.Pushables == "0" && b.Pullables == "0"
}
func (b *Branch) HasCommitsToPush() bool {
return b.IsRealBranch() && b.Pushables != "0"
}
func (b *Branch) HasCommitsToPull() bool {
return b.IsRealBranch() && b.Pullables != "0"
}
// for when we're in a detached head state
func (b *Branch) IsRealBranch() bool {
return b.Pushables != "" && b.Pullables != ""
}

View File

@@ -0,0 +1,41 @@
package models
import "fmt"
// Commit : A git commit
type Commit struct {
Sha string
Name string
Status string // one of "unpushed", "pushed", "merged", "rebasing" or "selected"
Action string // one of "", "pick", "edit", "squash", "reword", "drop", "fixup"
Tags []string
ExtraInfo string // something like 'HEAD -> master, tag: v0.15.2'
Author string
UnixTimestamp int64
// SHAs of parent commits (will be multiple if it's a merge commit)
Parents []string
}
func (c *Commit) ShortSha() string {
if len(c.Sha) < 8 {
return c.Sha
}
return c.Sha[:8]
}
func (c *Commit) RefName() string {
return c.Sha
}
func (c *Commit) ID() string {
return c.RefName()
}
func (c *Commit) Description() string {
return fmt.Sprintf("%s %s", c.Sha[:7], c.Name)
}
func (c *Commit) IsMerge() bool {
return len(c.Parents) > 1
}

View File

@@ -0,0 +1,17 @@
package models
// CommitFile : A git commit file
type CommitFile struct {
// TODO: rename this to Path
Name string
ChangeStatus string // e.g. 'A' for added or 'M' for modified. This is based on the result from git diff --name-status
}
func (f *CommitFile) ID() string {
return f.Name
}
func (f *CommitFile) Description() string {
return f.Name
}

View File

@@ -0,0 +1,87 @@
package models
import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
// File : A file from git status
// duplicating this for now
type File struct {
Name string
PreviousName string
HasStagedChanges bool
HasUnstagedChanges bool
Tracked bool
Added bool
Deleted bool
HasMergeConflicts bool
HasInlineMergeConflicts bool
DisplayString string
Type string // one of 'file', 'directory', and 'other'
ShortStatus string // e.g. 'AD', ' A', 'M ', '??'
}
// sometimes we need to deal with either a node (which contains a file) or an actual file
type IFile interface {
GetHasUnstagedChanges() bool
GetHasStagedChanges() bool
GetIsTracked() bool
GetPath() string
}
func (f *File) IsRename() bool {
return f.PreviousName != ""
}
// Names returns an array containing just the filename, or in the case of a rename, the after filename and the before filename
func (f *File) Names() []string {
result := []string{f.Name}
if f.PreviousName != "" {
result = append(result, f.PreviousName)
}
return result
}
// returns true if the file names are the same or if a a file rename includes the filename of the other
func (f *File) Matches(f2 *File) bool {
return utils.StringArraysOverlap(f.Names(), f2.Names())
}
func (f *File) ID() string {
return f.Name
}
func (f *File) Description() string {
return f.Name
}
func (f *File) IsSubmodule(configs []*SubmoduleConfig) bool {
return f.SubmoduleConfig(configs) != nil
}
func (f *File) SubmoduleConfig(configs []*SubmoduleConfig) *SubmoduleConfig {
for _, config := range configs {
if f.Name == config.Path {
return config
}
}
return nil
}
func (f *File) GetHasUnstagedChanges() bool {
return f.HasUnstagedChanges
}
func (f *File) GetHasStagedChanges() bool {
return f.HasStagedChanges
}
func (f *File) GetIsTracked() bool {
return f.Tracked
}
func (f *File) GetPath() string {
// TODO: remove concept of name; just use path
return f.Name
}

View File

@@ -0,0 +1,20 @@
package models
// Remote : A git remote
type Remote struct {
Name string
Urls []string
Branches []*RemoteBranch
}
func (r *Remote) RefName() string {
return r.Name
}
func (r *Remote) ID() string {
return r.RefName()
}
func (r *Remote) Description() string {
return r.RefName()
}

View File

@@ -0,0 +1,23 @@
package models
// Remote Branch : A git remote branch
type RemoteBranch struct {
Name string
RemoteName string
}
func (r *RemoteBranch) FullName() string {
return r.RemoteName + "/" + r.Name
}
func (r *RemoteBranch) RefName() string {
return r.FullName()
}
func (r *RemoteBranch) ID() string {
return r.RefName()
}
func (r *RemoteBranch) Description() string {
return r.RefName()
}

View File

@@ -0,0 +1,21 @@
package models
import "fmt"
// StashEntry : A git stash entry
type StashEntry struct {
Index int
Name string
}
func (s *StashEntry) RefName() string {
return fmt.Sprintf("stash@{%d}", s.Index)
}
func (s *StashEntry) ID() string {
return s.RefName()
}
func (s *StashEntry) Description() string {
return s.RefName() + ": " + s.Name
}

View File

@@ -0,0 +1,19 @@
package models
type SubmoduleConfig struct {
Name string
Path string
Url string
}
func (r *SubmoduleConfig) RefName() string {
return r.Name
}
func (r *SubmoduleConfig) ID() string {
return r.RefName()
}
func (r *SubmoduleConfig) Description() string {
return r.RefName()
}

View File

@@ -0,0 +1,18 @@
package models
// Tag : A git tag
type Tag struct {
Name string
}
func (t *Tag) RefName() string {
return t.Name
}
func (t *Tag) ID() string {
return t.RefName()
}
func (t *Tag) Description() string {
return "tag " + t.Name
}

View File

@@ -1,385 +0,0 @@
package commands
import (
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"sync"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/mgutz/str"
"github.com/sirupsen/logrus"
gitconfig "github.com/tcnksm/go-gitconfig"
)
// Platform stores the os state
type Platform struct {
os string
shell string
shellArg string
escapedQuote string
openCommand string
openLinkCommand string
fallbackEscapedQuote string
}
// OSCommand holds all the os commands
type OSCommand struct {
Log *logrus.Entry
Platform *Platform
Config config.AppConfigurer
command func(string, ...string) *exec.Cmd
getGlobalGitConfig func(string) (string, error)
getenv func(string) string
}
// NewOSCommand os command runner
func NewOSCommand(log *logrus.Entry, config config.AppConfigurer) *OSCommand {
return &OSCommand{
Log: log,
Platform: getPlatform(),
Config: config,
command: exec.Command,
getGlobalGitConfig: gitconfig.Global,
getenv: os.Getenv,
}
}
// SetCommand sets the command function used by the struct.
// To be used for testing only
func (c *OSCommand) SetCommand(cmd func(string, ...string) *exec.Cmd) {
c.command = cmd
}
// RunCommandWithOutput wrapper around commands returning their output and error
func (c *OSCommand) RunCommandWithOutput(command string) (string, error) {
c.Log.WithField("command", command).Info("RunCommand")
cmd := c.ExecutableFromString(command)
return sanitisedCommandOutput(cmd.CombinedOutput())
}
// RunExecutableWithOutput runs an executable file and returns its output
func (c *OSCommand) RunExecutableWithOutput(cmd *exec.Cmd) (string, error) {
return sanitisedCommandOutput(cmd.CombinedOutput())
}
// RunExecutable runs an executable file and returns an error if there was one
func (c *OSCommand) RunExecutable(cmd *exec.Cmd) error {
_, err := c.RunExecutableWithOutput(cmd)
return err
}
// ExecutableFromString takes a string like `git status` and returns an executable command for it
func (c *OSCommand) ExecutableFromString(commandStr string) *exec.Cmd {
splitCmd := str.ToArgv(commandStr)
cmd := c.command(splitCmd[0], splitCmd[1:]...)
cmd.Env = append(os.Environ(), "GIT_OPTIONAL_LOCKS=0")
return cmd
}
// RunCommandWithOutputLive runs RunCommandWithOutputLiveWrapper
func (c *OSCommand) RunCommandWithOutputLive(command string, output func(string) string) error {
return RunCommandWithOutputLiveWrapper(c, command, output)
}
// DetectUnamePass detect a username / password question in a command
// ask is a function that gets executen when this function detect you need to fillin a password
// The ask argument will be "username" or "password" and expects the user's password or username back
func (c *OSCommand) DetectUnamePass(command string, ask func(string) string) error {
ttyText := ""
errMessage := c.RunCommandWithOutputLive(command, func(word string) string {
ttyText = ttyText + " " + word
prompts := map[string]string{
"password": `Password\s*for\s*'.+':`,
"username": `Username\s*for\s*'.+':`,
}
for askFor, pattern := range prompts {
if match, _ := regexp.MatchString(pattern, ttyText); match {
ttyText = ""
return ask(askFor)
}
}
return ""
})
return errMessage
}
// RunCommand runs a command and just returns the error
func (c *OSCommand) RunCommand(command string) error {
_, err := c.RunCommandWithOutput(command)
return err
}
// FileType tells us if the file is a file, directory or other
func (c *OSCommand) FileType(path string) string {
fileInfo, err := os.Stat(path)
if err != nil {
return "other"
}
if fileInfo.IsDir() {
return "directory"
}
return "file"
}
// RunDirectCommand wrapper around direct commands
func (c *OSCommand) RunDirectCommand(command string) (string, error) {
c.Log.WithField("command", command).Info("RunDirectCommand")
return sanitisedCommandOutput(
c.command(c.Platform.shell, c.Platform.shellArg, command).
CombinedOutput(),
)
}
func sanitisedCommandOutput(output []byte, err error) (string, error) {
outputString := string(output)
if err != nil {
// errors like 'exit status 1' are not very useful so we'll create an error
// from the combined output
if outputString == "" {
return "", WrapError(err)
}
return outputString, errors.New(outputString)
}
return outputString, nil
}
// OpenFile opens a file with the given
func (c *OSCommand) OpenFile(filename string) error {
commandTemplate := c.Config.GetUserConfig().GetString("os.openCommand")
templateValues := map[string]string{
"filename": c.Quote(filename),
}
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
err := c.RunCommand(command)
return err
}
// OpenLink opens a file with the given
func (c *OSCommand) OpenLink(link string) error {
commandTemplate := c.Config.GetUserConfig().GetString("os.openLinkCommand")
templateValues := map[string]string{
"link": c.Quote(link),
}
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
err := c.RunCommand(command)
return err
}
// EditFile opens a file in a subprocess using whatever editor is available,
// falling back to core.editor, VISUAL, EDITOR, then vi
func (c *OSCommand) EditFile(filename string) (*exec.Cmd, error) {
editor, _ := c.getGlobalGitConfig("core.editor")
if editor == "" {
editor = c.getenv("VISUAL")
}
if editor == "" {
editor = c.getenv("EDITOR")
}
if editor == "" {
if err := c.RunCommand("which vi"); err == nil {
editor = "vi"
}
}
if editor == "" {
return nil, errors.New("No editor defined in $VISUAL, $EDITOR, or git config")
}
return c.PrepareSubProcess(editor, filename), nil
}
// PrepareSubProcess iniPrepareSubProcessrocess then tells the Gui to switch to it
// TODO: see if this needs to exist, given that ExecutableFromString does the same things
func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *exec.Cmd {
cmd := c.command(cmdName, commandArgs...)
if cmd != nil {
cmd.Env = append(os.Environ(), "GIT_OPTIONAL_LOCKS=0")
}
return cmd
}
// Quote wraps a message in platform-specific quotation marks
func (c *OSCommand) Quote(message string) string {
message = strings.Replace(message, "`", "\\`", -1)
escapedQuote := c.Platform.escapedQuote
if strings.Contains(message, c.Platform.escapedQuote) {
escapedQuote = c.Platform.fallbackEscapedQuote
}
return escapedQuote + message + escapedQuote
}
// Unquote removes wrapping quotations marks if they are present
// this is needed for removing quotes from staged filenames with spaces
func (c *OSCommand) Unquote(message string) string {
return strings.Replace(message, `"`, "", -1)
}
// AppendLineToFile adds a new line in file
func (c *OSCommand) AppendLineToFile(filename, line string) error {
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
return WrapError(err)
}
defer f.Close()
_, err = f.WriteString("\n" + line)
if err != nil {
return WrapError(err)
}
return nil
}
// CreateTempFile writes a string to a new temp file and returns the file's name
func (c *OSCommand) CreateTempFile(filename, content string) (string, error) {
tmpfile, err := ioutil.TempFile("", filename)
if err != nil {
c.Log.Error(err)
return "", WrapError(err)
}
if _, err := tmpfile.WriteString(content); err != nil {
c.Log.Error(err)
return "", WrapError(err)
}
if err := tmpfile.Close(); err != nil {
c.Log.Error(err)
return "", WrapError(err)
}
return tmpfile.Name(), nil
}
// CreateFileWithContent creates a file with the given content
func (c *OSCommand) CreateFileWithContent(path string, content string) error {
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
c.Log.Error(err)
return err
}
if err := ioutil.WriteFile(path, []byte(content), 0644); err != nil {
c.Log.Error(err)
return WrapError(err)
}
return nil
}
// Remove removes a file or directory at the specified path
func (c *OSCommand) Remove(filename string) error {
err := os.RemoveAll(filename)
return WrapError(err)
}
// FileExists checks whether a file exists at the specified path
func (c *OSCommand) FileExists(path string) (bool, error) {
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
return true, nil
}
// RunPreparedCommand takes a pointer to an exec.Cmd and runs it
// this is useful if you need to give your command some environment variables
// before running it
func (c *OSCommand) RunPreparedCommand(cmd *exec.Cmd) error {
out, err := cmd.CombinedOutput()
outString := string(out)
c.Log.Info(outString)
if err != nil {
if len(outString) == 0 {
return err
}
return errors.New(outString)
}
return nil
}
// GetLazygitPath returns the path of the currently executed file
func (c *OSCommand) GetLazygitPath() string {
ex, err := os.Executable() // get the executable path for git to use
if err != nil {
ex = os.Args[0] // fallback to the first call argument if needed
}
return filepath.ToSlash(ex)
}
// RunCustomCommand returns the pointer to a custom command
func (c *OSCommand) RunCustomCommand(command string) *exec.Cmd {
return c.PrepareSubProcess(c.Platform.shell, c.Platform.shellArg, command)
}
// PipeCommands runs a heap of commands and pipes their inputs/outputs together like A | B | C
func (c *OSCommand) PipeCommands(commandStrings ...string) error {
cmds := make([]*exec.Cmd, len(commandStrings))
for i, str := range commandStrings {
cmds[i] = c.ExecutableFromString(str)
}
for i := 0; i < len(cmds)-1; i++ {
stdout, err := cmds[i].StdoutPipe()
if err != nil {
return err
}
cmds[i+1].Stdin = stdout
}
// keeping this here in case I adapt this code for some other purpose in the future
// cmds[len(cmds)-1].Stdout = os.Stdout
finalErrors := []string{}
wg := sync.WaitGroup{}
wg.Add(len(cmds))
for _, cmd := range cmds {
currentCmd := cmd
go func() {
stderr, err := currentCmd.StderrPipe()
if err != nil {
c.Log.Error(err)
}
if err := currentCmd.Start(); err != nil {
c.Log.Error(err)
}
if b, err := ioutil.ReadAll(stderr); err == nil {
if len(b) > 0 {
finalErrors = append(finalErrors, string(b))
}
}
if err := currentCmd.Wait(); err != nil {
c.Log.Error(err)
}
wg.Done()
}()
}
wg.Wait()
if len(finalErrors) > 0 {
return errors.New(strings.Join(finalErrors, "\n"))
}
return nil
}

View File

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

View File

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

View File

@@ -1,11 +0,0 @@
package commands
func getPlatform() *Platform {
return &Platform{
os: "windows",
shell: "cmd",
shellArg: "/c",
escapedQuote: `\"`,
fallbackEscapedQuote: "\\'",
}
}

View File

@@ -0,0 +1,33 @@
package oscommands
import (
"os/exec"
)
// A command object is a general way to represent a command to be run on the
// command line. If you want to log the command you'll use .ToString() and
// if you want to run it you'll use .GetCmd()
type ICmdObj interface {
GetCmd() *exec.Cmd
ToString() string
AddEnvVars(...string) ICmdObj
}
type CmdObj struct {
cmdStr string
cmd *exec.Cmd
}
func (self *CmdObj) GetCmd() *exec.Cmd {
return self.cmd
}
func (self *CmdObj) ToString() string {
return self.cmdStr
}
func (self *CmdObj) AddEnvVars(vars ...string) ICmdObj {
self.cmd.Env = append(self.cmd.Env, vars...)
return self
}

View File

@@ -0,0 +1,137 @@
package oscommands
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
)
/* MIT License
*
* Copyright (c) 2017 Roland Singer [roland.singer@desertbit.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.
*/
// CopyFile copies the contents of the file named src to the file named
// by dst. The file will be created if it does not already exist. If the
// destination file exists, all it's contents will be replaced by the contents
// of the source file. The file mode will be copied from the source and
// the copied data is synced/flushed to stable storage.
func CopyFile(src, dst string) (err error) {
in, err := os.Open(src)
if err != nil {
return
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return
}
defer func() {
if e := out.Close(); e != nil {
err = e
}
}()
_, err = io.Copy(out, in)
if err != nil {
return
}
err = out.Sync()
if err != nil {
return
}
si, err := os.Stat(src)
if err != nil {
return
}
err = os.Chmod(dst, si.Mode())
if err != nil {
return
}
return
}
// CopyDir recursively copies a directory tree, attempting to preserve permissions.
// Source directory must exist. If destination already exists we'll clobber it.
// Symlinks are ignored and skipped.
func CopyDir(src string, dst string) (err error) {
src = filepath.Clean(src)
dst = filepath.Clean(dst)
si, err := os.Stat(src)
if err != nil {
return err
}
if !si.IsDir() {
return fmt.Errorf("source is not a directory")
}
_, err = os.Stat(dst)
if err != nil && !os.IsNotExist(err) {
return
}
if err == nil {
// it exists so let's remove it
if err := os.RemoveAll(dst); err != nil {
return err
}
}
err = os.MkdirAll(dst, si.Mode())
if err != nil {
return
}
entries, err := ioutil.ReadDir(src)
if err != nil {
return
}
for _, entry := range entries {
srcPath := filepath.Join(src, entry.Name())
dstPath := filepath.Join(dst, entry.Name())
if entry.IsDir() {
err = CopyDir(srcPath, dstPath)
if err != nil {
return
}
} else {
// Skip symlinks.
if entry.Mode()&os.ModeSymlink != 0 {
continue
}
err = CopyFile(srcPath, dstPath)
if err != nil {
return
}
}
}
return
}

View File

@@ -0,0 +1,11 @@
package oscommands
import (
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// NewDummyOSCommand creates a new dummy OSCommand for testing
func NewDummyOSCommand() *OSCommand {
return NewOSCommand(utils.NewDummyLog(), config.NewDummyAppConfig())
}

View File

@@ -1,6 +1,7 @@
//go:build !windows
// +build !windows
package commands
package oscommands
import (
"bufio"
@@ -9,17 +10,19 @@ import (
"unicode/utf8"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/jesseduffield/pty"
"github.com/creack/pty"
)
// RunCommandWithOutputLiveWrapper runs a command and return every word that gets written in stdout
// Output is a function that executes by every word that gets read by bufio
// As return of output you need to give a string that will be written to stdin
// NOTE: If the return data is empty it won't written anything to stdin
func RunCommandWithOutputLiveWrapper(c *OSCommand, command string, output func(string) string) error {
cmd := c.ExecutableFromString(command)
cmd.Env = append(cmd.Env, "LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8")
func RunCommandWithOutputLiveWrapper(c *OSCommand, cmdObj ICmdObj, output func(string) string) error {
c.Log.WithField("command", cmdObj.ToString()).Info("RunCommand")
c.LogCommand(cmdObj.ToString(), true)
cmd := cmdObj.AddEnvVars("LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8").GetCmd()
var stderr bytes.Buffer
cmd.Stderr = &stderr
@@ -30,14 +33,14 @@ func RunCommandWithOutputLiveWrapper(c *OSCommand, command string, output func(s
return err
}
go func() {
go utils.Safe(func() {
scanner := bufio.NewScanner(ptmx)
scanner.Split(scanWordsWithNewLines)
for scanner.Scan() {
toOutput := strings.Trim(scanner.Text(), " ")
_, _ = ptmx.WriteString(output(toOutput))
}
}()
})
err = cmd.Wait()
ptmx.Close()

View File

@@ -0,0 +1,10 @@
//go:build windows
// +build windows
package oscommands
// RunCommandWithOutputLiveWrapper runs a command live but because of windows compatibility this command can't be ran there
// TODO: Remove this hack and replace it with a proper way to run commands live on windows
func RunCommandWithOutputLiveWrapper(c *OSCommand, cmdObj ICmdObj, output func(string) string) error {
return c.RunCommand(cmdObj.ToString())
}

View File

@@ -0,0 +1,592 @@
package oscommands
import (
"bufio"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"sync"
"github.com/go-errors/errors"
"github.com/atotto/clipboard"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/mgutz/str"
"github.com/sirupsen/logrus"
)
// Platform stores the os state
type Platform struct {
OS string
Shell string
ShellArg string
OpenCommand string
OpenLinkCommand string
}
// OSCommand holds all the os commands
type OSCommand struct {
Log *logrus.Entry
Platform *Platform
Config config.AppConfigurer
Command func(string, ...string) *exec.Cmd
BeforeExecuteCmd func(*exec.Cmd)
Getenv func(string) string
// callback to run before running a command, i.e. for the purposes of logging
onRunCommand func(CmdLogEntry)
// something like 'Staging File': allows us to group cmd logs under a single title
CmdLogSpan string
removeFile func(string) error
}
// TODO: make these fields private
type CmdLogEntry struct {
// e.g. 'git commit -m "haha"'
cmdStr string
// Span is something like 'Staging File'. Multiple commands can be grouped under the same
// span
span string
// sometimes our command is direct like 'git commit', and sometimes it's a
// command to remove a file but through Go's standard library rather than the
// command line
commandLine bool
}
func (e CmdLogEntry) GetCmdStr() string {
return e.cmdStr
}
func (e CmdLogEntry) GetSpan() string {
return e.span
}
func (e CmdLogEntry) GetCommandLine() bool {
return e.commandLine
}
func NewCmdLogEntry(cmdStr string, span string, commandLine bool) CmdLogEntry {
return CmdLogEntry{cmdStr: cmdStr, span: span, commandLine: commandLine}
}
// NewOSCommand os command runner
func NewOSCommand(log *logrus.Entry, config config.AppConfigurer) *OSCommand {
return &OSCommand{
Log: log,
Platform: getPlatform(),
Config: config,
Command: secureexec.Command,
BeforeExecuteCmd: func(*exec.Cmd) {},
Getenv: os.Getenv,
removeFile: os.RemoveAll,
}
}
func (c *OSCommand) WithSpan(span string) *OSCommand {
// sometimes .WithSpan(span) will be called where span actually is empty, in
// which case we don't need to log anything so we can just return early here
// with the original struct
if span == "" {
return c
}
newOSCommand := &OSCommand{}
*newOSCommand = *c
newOSCommand.CmdLogSpan = span
return newOSCommand
}
func (c *OSCommand) LogExecCmd(cmd *exec.Cmd) {
c.LogCommand(strings.Join(cmd.Args, " "), true)
}
func (c *OSCommand) LogCommand(cmdStr string, commandLine bool) {
c.Log.WithField("command", cmdStr).Info("RunCommand")
if c.onRunCommand != nil && c.CmdLogSpan != "" {
c.onRunCommand(NewCmdLogEntry(cmdStr, c.CmdLogSpan, commandLine))
}
}
func (c *OSCommand) SetOnRunCommand(f func(CmdLogEntry)) {
c.onRunCommand = f
}
// SetCommand sets the command function used by the struct.
// To be used for testing only
func (c *OSCommand) SetCommand(cmd func(string, ...string) *exec.Cmd) {
c.Command = cmd
}
// To be used for testing only
func (c *OSCommand) SetRemoveFile(f func(string) error) {
c.removeFile = f
}
func (c *OSCommand) SetBeforeExecuteCmd(cmd func(*exec.Cmd)) {
c.BeforeExecuteCmd = cmd
}
type RunCommandOptions struct {
EnvVars []string
}
func (c *OSCommand) RunCommandWithOutputWithOptions(command string, options RunCommandOptions) (string, error) {
c.LogCommand(command, true)
cmd := c.ExecutableFromString(command)
cmd.Env = append(cmd.Env, "GIT_TERMINAL_PROMPT=0") // prevents git from prompting us for input which would freeze the program
cmd.Env = append(cmd.Env, options.EnvVars...)
return sanitisedCommandOutput(cmd.CombinedOutput())
}
func (c *OSCommand) RunCommandWithOptions(command string, options RunCommandOptions) error {
_, err := c.RunCommandWithOutputWithOptions(command, options)
return err
}
// RunCommandWithOutput wrapper around commands returning their output and error
// NOTE: If you don't pass any formatArgs we'll just use the command directly,
// however there's a bizarre compiler error/warning when you pass in a formatString
// with a percent sign because it thinks it's supposed to be a formatString when
// in that case it's not. To get around that error you'll need to define the string
// in a variable and pass the variable into RunCommandWithOutput.
func (c *OSCommand) RunCommandWithOutput(formatString string, formatArgs ...interface{}) (string, error) {
command := formatString
if formatArgs != nil {
command = fmt.Sprintf(formatString, formatArgs...)
}
cmd := c.ExecutableFromString(command)
c.LogExecCmd(cmd)
output, err := sanitisedCommandOutput(cmd.CombinedOutput())
if err != nil {
c.Log.WithField("command", command).Error(output)
}
return output, err
}
// RunExecutableWithOutput runs an executable file and returns its output
func (c *OSCommand) RunExecutableWithOutput(cmd *exec.Cmd) (string, error) {
c.LogExecCmd(cmd)
c.BeforeExecuteCmd(cmd)
return sanitisedCommandOutput(cmd.CombinedOutput())
}
// RunExecutable runs an executable file and returns an error if there was one
func (c *OSCommand) RunExecutable(cmd *exec.Cmd) error {
_, err := c.RunExecutableWithOutput(cmd)
return err
}
// ExecutableFromString takes a string like `git status` and returns an executable command for it
func (c *OSCommand) ExecutableFromString(commandStr string) *exec.Cmd {
splitCmd := str.ToArgv(commandStr)
cmd := c.Command(splitCmd[0], splitCmd[1:]...)
cmd.Env = append(os.Environ(), "GIT_OPTIONAL_LOCKS=0")
return cmd
}
// ShellCommandFromString takes a string like `git commit` and returns an executable shell command for it
func (c *OSCommand) ShellCommandFromString(commandStr string) *exec.Cmd {
quotedCommand := ""
// Windows does not seem to like quotes around the command
if c.Platform.OS == "windows" {
quotedCommand = strings.NewReplacer(
"^", "^^",
"&", "^&",
"|", "^|",
"<", "^<",
">", "^>",
"%", "^%",
).Replace(commandStr)
} else {
quotedCommand = c.Quote(commandStr)
}
shellCommand := fmt.Sprintf("%s %s %s", c.Platform.Shell, c.Platform.ShellArg, quotedCommand)
return c.ExecutableFromString(shellCommand)
}
// RunCommandWithOutputLive runs RunCommandWithOutputLiveWrapper
func (c *OSCommand) RunCommandWithOutputLive(cmdObj ICmdObj, output func(string) string) error {
return RunCommandWithOutputLiveWrapper(c, cmdObj, output)
}
// DetectUnamePass detect a username / password / passphrase question in a command
// promptUserForCredential is a function that gets executed when this function detect you need to fillin a password or passphrase
// The promptUserForCredential argument will be "username", "password" or "passphrase" and expects the user's password/passphrase or username back
func (c *OSCommand) DetectUnamePass(cmdObj ICmdObj, promptUserForCredential func(string) string) error {
ttyText := ""
errMessage := c.RunCommandWithOutputLive(cmdObj, func(word string) string {
ttyText = ttyText + " " + word
prompts := map[string]string{
`.+'s password:`: "password",
`Password\s*for\s*'.+':`: "password",
`Username\s*for\s*'.+':`: "username",
`Enter\s*passphrase\s*for\s*key\s*'.+':`: "passphrase",
}
for pattern, askFor := range prompts {
if match, _ := regexp.MatchString(pattern, ttyText); match {
ttyText = ""
return promptUserForCredential(askFor)
}
}
return ""
})
return errMessage
}
// RunCommand runs a command and just returns the error
func (c *OSCommand) RunCommand(formatString string, formatArgs ...interface{}) error {
_, err := c.RunCommandWithOutput(formatString, formatArgs...)
return err
}
// RunShellCommand runs shell commands i.e. 'sh -c <command>'. Good for when you
// need access to the shell
func (c *OSCommand) RunShellCommand(command string) error {
cmd := c.ShellCommandFromString(command)
c.LogExecCmd(cmd)
_, err := sanitisedCommandOutput(cmd.CombinedOutput())
return err
}
// FileType tells us if the file is a file, directory or other
func (c *OSCommand) FileType(path string) string {
fileInfo, err := os.Stat(path)
if err != nil {
return "other"
}
if fileInfo.IsDir() {
return "directory"
}
return "file"
}
func sanitisedCommandOutput(output []byte, err error) (string, error) {
outputString := string(output)
if err != nil {
// errors like 'exit status 1' are not very useful so we'll create an error
// from the combined output
if outputString == "" {
return "", utils.WrapError(err)
}
return outputString, errors.New(outputString)
}
return outputString, nil
}
// OpenFile opens a file with the given
func (c *OSCommand) OpenFile(filename string) error {
commandTemplate := c.Config.GetUserConfig().OS.OpenCommand
templateValues := map[string]string{
"filename": c.Quote(filename),
}
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
err := c.RunShellCommand(command)
return err
}
// OpenLink opens a file with the given
func (c *OSCommand) OpenLink(link string) error {
c.LogCommand(fmt.Sprintf("Opening link '%s'", link), false)
commandTemplate := c.Config.GetUserConfig().OS.OpenLinkCommand
templateValues := map[string]string{
"link": c.Quote(link),
}
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
err := c.RunShellCommand(command)
return err
}
// PrepareSubProcess iniPrepareSubProcessrocess then tells the Gui to switch to it
// TODO: see if this needs to exist, given that ExecutableFromString does the same things
func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *exec.Cmd {
cmd := c.Command(cmdName, commandArgs...)
if cmd != nil {
cmd.Env = append(os.Environ(), "GIT_OPTIONAL_LOCKS=0")
}
c.LogExecCmd(cmd)
return cmd
}
// PrepareShellSubProcess returns the pointer to a custom command
func (c *OSCommand) PrepareShellSubProcess(command string) *exec.Cmd {
return c.PrepareSubProcess(c.Platform.Shell, c.Platform.ShellArg, command)
}
// Quote wraps a message in platform-specific quotation marks
func (c *OSCommand) Quote(message string) string {
var quote string
if c.Platform.OS == "windows" {
quote = `\"`
message = strings.NewReplacer(
`"`, `"'"'"`,
`\"`, `\\"`,
).Replace(message)
} else {
quote = `"`
message = strings.NewReplacer(
`\`, `\\`,
`"`, `\"`,
`$`, `\$`,
"`", "\\`",
).Replace(message)
}
return quote + message + quote
}
// AppendLineToFile adds a new line in file
func (c *OSCommand) AppendLineToFile(filename, line string) error {
c.LogCommand(fmt.Sprintf("Appending '%s' to file '%s'", line, filename), false)
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
return utils.WrapError(err)
}
defer f.Close()
_, err = f.WriteString("\n" + line)
if err != nil {
return utils.WrapError(err)
}
return nil
}
// CreateTempFile writes a string to a new temp file and returns the file's name
func (c *OSCommand) CreateTempFile(filename, content string) (string, error) {
tmpfile, err := ioutil.TempFile("", filename)
if err != nil {
c.Log.Error(err)
return "", utils.WrapError(err)
}
c.LogCommand(fmt.Sprintf("Creating temp file '%s'", tmpfile.Name()), false)
if _, err := tmpfile.WriteString(content); err != nil {
c.Log.Error(err)
return "", utils.WrapError(err)
}
if err := tmpfile.Close(); err != nil {
c.Log.Error(err)
return "", utils.WrapError(err)
}
return tmpfile.Name(), nil
}
// CreateFileWithContent creates a file with the given content
func (c *OSCommand) CreateFileWithContent(path string, content string) error {
c.LogCommand(fmt.Sprintf("Creating file '%s'", path), false)
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
c.Log.Error(err)
return err
}
if err := ioutil.WriteFile(path, []byte(content), 0644); err != nil {
c.Log.Error(err)
return utils.WrapError(err)
}
return nil
}
// Remove removes a file or directory at the specified path
func (c *OSCommand) Remove(filename string) error {
c.LogCommand(fmt.Sprintf("Removing '%s'", filename), false)
err := os.RemoveAll(filename)
return utils.WrapError(err)
}
// FileExists checks whether a file exists at the specified path
func (c *OSCommand) FileExists(path string) (bool, error) {
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
return true, nil
}
// RunPreparedCommand takes a pointer to an exec.Cmd and runs it
// this is useful if you need to give your command some environment variables
// before running it
func (c *OSCommand) RunPreparedCommand(cmd *exec.Cmd) error {
c.BeforeExecuteCmd(cmd)
c.LogExecCmd(cmd)
out, err := cmd.CombinedOutput()
outString := string(out)
c.Log.Info(outString)
if err != nil {
if len(outString) == 0 {
return err
}
return errors.New(outString)
}
return nil
}
// GetLazygitPath returns the path of the currently executed file
func (c *OSCommand) GetLazygitPath() string {
ex, err := os.Executable() // get the executable path for git to use
if err != nil {
ex = os.Args[0] // fallback to the first call argument if needed
}
return `"` + filepath.ToSlash(ex) + `"`
}
// PipeCommands runs a heap of commands and pipes their inputs/outputs together like A | B | C
func (c *OSCommand) PipeCommands(commandStrings ...string) error {
cmds := make([]*exec.Cmd, len(commandStrings))
logCmdStr := ""
for i, str := range commandStrings {
if i > 0 {
logCmdStr += " | "
}
logCmdStr += str
cmds[i] = c.ExecutableFromString(str)
}
c.LogCommand(logCmdStr, true)
for i := 0; i < len(cmds)-1; i++ {
stdout, err := cmds[i].StdoutPipe()
if err != nil {
return err
}
cmds[i+1].Stdin = stdout
}
// keeping this here in case I adapt this code for some other purpose in the future
// cmds[len(cmds)-1].Stdout = os.Stdout
finalErrors := []string{}
wg := sync.WaitGroup{}
wg.Add(len(cmds))
for _, cmd := range cmds {
currentCmd := cmd
go utils.Safe(func() {
stderr, err := currentCmd.StderrPipe()
if err != nil {
c.Log.Error(err)
}
if err := currentCmd.Start(); err != nil {
c.Log.Error(err)
}
if b, err := ioutil.ReadAll(stderr); err == nil {
if len(b) > 0 {
finalErrors = append(finalErrors, string(b))
}
}
if err := currentCmd.Wait(); err != nil {
c.Log.Error(err)
}
wg.Done()
})
}
wg.Wait()
if len(finalErrors) > 0 {
return errors.New(strings.Join(finalErrors, "\n"))
}
return nil
}
func Kill(cmd *exec.Cmd) error {
if cmd.Process == nil {
// somebody got to it before we were able to, poor bastard
return nil
}
return cmd.Process.Kill()
}
func RunLineOutputCmd(cmd *exec.Cmd, onLine func(line string) (bool, error)) error {
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
return err
}
scanner := bufio.NewScanner(stdoutPipe)
scanner.Split(bufio.ScanLines)
if err := cmd.Start(); err != nil {
return err
}
for scanner.Scan() {
line := scanner.Text()
stop, err := onLine(line)
if err != nil {
return err
}
if stop {
_ = cmd.Process.Kill()
break
}
}
_ = cmd.Wait()
return nil
}
func (c *OSCommand) CopyToClipboard(str string) error {
escaped := strings.Replace(str, "\n", "\\n", -1)
truncated := utils.TruncateWithEllipsis(escaped, 40)
c.LogCommand(fmt.Sprintf("Copying '%s' to clipboard", truncated), false)
return clipboard.WriteAll(str)
}
func (c *OSCommand) RemoveFile(path string) error {
c.LogCommand(fmt.Sprintf("Deleting path '%s'", path), false)
return c.removeFile(path)
}
func (c *OSCommand) NewCmdObjFromStr(cmdStr string) ICmdObj {
args := str.ToArgv(cmdStr)
cmd := c.Command(args[0], args[1:]...)
cmd.Env = os.Environ()
return &CmdObj{
cmdStr: cmdStr,
cmd: cmd,
}
}
func (c *OSCommand) NewCmdObjFromArgs(args []string) ICmdObj {
cmd := c.Command(args[0], args[1:]...)
return &CmdObj{
cmdStr: strings.Join(args, " "),
cmd: cmd,
}
}
func (c *OSCommand) NewCmdObj(cmd *exec.Cmd) ICmdObj {
return &CmdObj{
cmdStr: strings.Join(cmd.Args, " "),
cmd: cmd,
}
}

View File

@@ -0,0 +1,18 @@
//go:build !windows
// +build !windows
package oscommands
import (
"runtime"
)
func getPlatform() *Platform {
return &Platform{
OS: runtime.GOOS,
Shell: "bash",
ShellArg: "-c",
OpenCommand: "open {{filename}}",
OpenLinkCommand: "open {{link}}",
}
}

View File

@@ -0,0 +1,138 @@
//go:build !windows
// +build !windows
package oscommands
import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/stretchr/testify/assert"
)
// TestOSCommandOpenFileDarwin is a function.
func TestOSCommandOpenFileDarwin(t *testing.T) {
type scenario struct {
filename string
command func(string, ...string) *exec.Cmd
test func(error)
}
scenarios := []scenario{
{
"test",
func(name string, arg ...string) *exec.Cmd {
return secureexec.Command("exit", "1")
},
func(err error) {
assert.Error(t, err)
},
},
{
"test",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "bash", name)
assert.Equal(t, []string{"-c", `open "test"`}, arg)
return secureexec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
},
{
"filename with spaces",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "bash", name)
assert.Equal(t, []string{"-c", `open "filename with spaces"`}, arg)
return secureexec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
OSCmd := NewDummyOSCommand()
OSCmd.Platform.OS = "darwin"
OSCmd.Command = s.command
OSCmd.Config.GetUserConfig().OS.OpenCommand = "open {{filename}}"
s.test(OSCmd.OpenFile(s.filename))
}
}
// TestOSCommandOpenFileLinux tests the OpenFile command on Linux
func TestOSCommandOpenFileLinux(t *testing.T) {
type scenario struct {
filename string
command func(string, ...string) *exec.Cmd
test func(error)
}
scenarios := []scenario{
{
"test",
func(name string, arg ...string) *exec.Cmd {
return secureexec.Command("exit", "1")
},
func(err error) {
assert.Error(t, err)
},
},
{
"test",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "bash", name)
assert.Equal(t, []string{"-c", `xdg-open "test" > /dev/null`}, arg)
return secureexec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
},
{
"filename with spaces",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "bash", name)
assert.Equal(t, []string{"-c", `xdg-open "filename with spaces" > /dev/null`}, arg)
return secureexec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
},
{
"let's_test_with_single_quote",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "bash", name)
assert.Equal(t, []string{"-c", `xdg-open "let's_test_with_single_quote" > /dev/null`}, arg)
return secureexec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
},
{
"$USER.txt",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "bash", name)
assert.Equal(t, []string{"-c", `xdg-open "\$USER.txt" > /dev/null`}, arg)
return secureexec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
OSCmd := NewDummyOSCommand()
OSCmd.Command = s.command
OSCmd.Platform.OS = "linux"
OSCmd.Config.GetUserConfig().OS.OpenCommand = `xdg-open {{filename}} > /dev/null`
s.test(OSCmd.OpenFile(s.filename))
}
}

View File

@@ -0,0 +1,199 @@
package oscommands
import (
"io/ioutil"
"os"
"testing"
"github.com/stretchr/testify/assert"
)
// TestOSCommandRunCommandWithOutput is a function.
func TestOSCommandRunCommandWithOutput(t *testing.T) {
type scenario struct {
command string
test func(string, error)
}
scenarios := []scenario{
{
"echo -n '123'",
func(output string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "123", output)
},
},
{
"rmdir unexisting-folder",
func(output string, err error) {
assert.Regexp(t, "rmdir.*unexisting-folder.*", err.Error())
},
},
}
for _, s := range scenarios {
s.test(NewDummyOSCommand().RunCommandWithOutput(s.command))
}
}
// TestOSCommandRunCommand is a function.
func TestOSCommandRunCommand(t *testing.T) {
type scenario struct {
command string
test func(error)
}
scenarios := []scenario{
{
"rmdir unexisting-folder",
func(err error) {
assert.Regexp(t, "rmdir.*unexisting-folder.*", err.Error())
},
},
}
for _, s := range scenarios {
s.test(NewDummyOSCommand().RunCommand(s.command))
}
}
// TestOSCommandQuote is a function.
func TestOSCommandQuote(t *testing.T) {
osCommand := NewDummyOSCommand()
osCommand.Platform.OS = "linux"
actual := osCommand.Quote("hello `test`")
expected := "\"hello \\`test\\`\""
assert.EqualValues(t, expected, actual)
}
// TestOSCommandQuoteSingleQuote tests the quote function with ' quotes explicitly for Linux
func TestOSCommandQuoteSingleQuote(t *testing.T) {
osCommand := NewDummyOSCommand()
osCommand.Platform.OS = "linux"
actual := osCommand.Quote("hello 'test'")
expected := `"hello 'test'"`
assert.EqualValues(t, expected, actual)
}
// TestOSCommandQuoteDoubleQuote tests the quote function with " quotes explicitly for Linux
func TestOSCommandQuoteDoubleQuote(t *testing.T) {
osCommand := NewDummyOSCommand()
osCommand.Platform.OS = "linux"
actual := osCommand.Quote(`hello "test"`)
expected := `"hello \"test\""`
assert.EqualValues(t, expected, actual)
}
// TestOSCommandQuoteWindows tests the quote function for Windows
func TestOSCommandQuoteWindows(t *testing.T) {
osCommand := NewDummyOSCommand()
osCommand.Platform.OS = "windows"
actual := osCommand.Quote(`hello "test" 'test2'`)
expected := `\"hello "'"'"test"'"'" 'test2'\"`
assert.EqualValues(t, expected, actual)
}
// TestOSCommandFileType is a function.
func TestOSCommandFileType(t *testing.T) {
type scenario struct {
path string
setup func()
test func(string)
}
scenarios := []scenario{
{
"testFile",
func() {
if _, err := os.Create("testFile"); err != nil {
panic(err)
}
},
func(output string) {
assert.EqualValues(t, "file", output)
},
},
{
"file with spaces",
func() {
if _, err := os.Create("file with spaces"); err != nil {
panic(err)
}
},
func(output string) {
assert.EqualValues(t, "file", output)
},
},
{
"testDirectory",
func() {
if err := os.Mkdir("testDirectory", 0644); err != nil {
panic(err)
}
},
func(output string) {
assert.EqualValues(t, "directory", output)
},
},
{
"nonExistant",
func() {},
func(output string) {
assert.EqualValues(t, "other", output)
},
},
}
for _, s := range scenarios {
s.setup()
s.test(NewDummyOSCommand().FileType(s.path))
_ = os.RemoveAll(s.path)
}
}
func TestOSCommandCreateTempFile(t *testing.T) {
type scenario struct {
testName string
filename string
content string
test func(string, error)
}
scenarios := []scenario{
{
"valid case",
"filename",
"content",
func(path string, err error) {
assert.NoError(t, err)
content, err := ioutil.ReadFile(path)
assert.NoError(t, err)
assert.Equal(t, "content", string(content))
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
s.test(NewDummyOSCommand().CreateTempFile(s.filename, s.content))
})
}
}

View File

@@ -0,0 +1,9 @@
package oscommands
func getPlatform() *Platform {
return &Platform{
OS: "windows",
Shell: "cmd",
ShellArg: "/c",
}
}

View File

@@ -0,0 +1,86 @@
//go:build windows
// +build windows
package oscommands
import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/stretchr/testify/assert"
)
// TestOSCommandOpenFileWindows tests the OpenFile command on Linux
func TestOSCommandOpenFileWindows(t *testing.T) {
type scenario struct {
filename string
command func(string, ...string) *exec.Cmd
test func(error)
}
scenarios := []scenario{
{
"test",
func(name string, arg ...string) *exec.Cmd {
return secureexec.Command("exit", "1")
},
func(err error) {
assert.Error(t, err)
},
},
{
"test",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "cmd", name)
assert.Equal(t, []string{"/c", "start", "", "test"}, arg)
return secureexec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
},
{
"filename with spaces",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "cmd", name)
assert.Equal(t, []string{"/c", "start", "", "filename with spaces"}, arg)
return secureexec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
},
{
"let's_test_with_single_quote",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "cmd", name)
assert.Equal(t, []string{"/c", "start", "", "let's_test_with_single_quote"}, arg)
return secureexec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
},
{
"$USER.txt",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "cmd", name)
assert.Equal(t, []string{"/c", "start", "", "$USER.txt"}, arg)
return secureexec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
OSCmd := NewDummyOSCommand()
OSCmd.Command = s.command
OSCmd.Platform.OS = "windows"
OSCmd.Config.GetUserConfig().OS.OpenCommand = `start "" {{filename}}`
s.test(OSCmd.OpenFile(s.filename))
}
}

142
pkg/commands/patch/hunk.go Normal file
View File

@@ -0,0 +1,142 @@
package patch
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type PatchHunk struct {
FirstLineIdx int
oldStart int
newStart int
heading string
bodyLines []string
}
func (hunk *PatchHunk) LastLineIdx() int {
return hunk.FirstLineIdx + len(hunk.bodyLines)
}
func newHunk(lines []string, firstLineIdx int) *PatchHunk {
header := lines[0]
bodyLines := lines[1:]
oldStart, newStart, heading := headerInfo(header)
return &PatchHunk{
oldStart: oldStart,
newStart: newStart,
heading: heading,
FirstLineIdx: firstLineIdx,
bodyLines: bodyLines,
}
}
func headerInfo(header string) (int, int, string) {
match := hunkHeaderRegexp.FindStringSubmatch(header)
oldStart := utils.MustConvertToInt(match[1])
newStart := utils.MustConvertToInt(match[2])
heading := match[3]
return oldStart, newStart, heading
}
func (hunk *PatchHunk) updatedLines(lineIndices []int, reverse bool) []string {
skippedNewlineMessageIndex := -1
newLines := []string{}
lineIdx := hunk.FirstLineIdx
for _, line := range hunk.bodyLines {
lineIdx++ // incrementing at the start to skip the header line
if line == "" {
break
}
isLineSelected := utils.IncludesInt(lineIndices, lineIdx)
firstChar, content := line[:1], line[1:]
transformedFirstChar := transformedFirstChar(firstChar, reverse, isLineSelected)
if isLineSelected || (transformedFirstChar == "\\" && skippedNewlineMessageIndex != lineIdx) || transformedFirstChar == " " {
newLines = append(newLines, transformedFirstChar+content)
continue
}
if transformedFirstChar == "+" {
// we don't want to include the 'newline at end of file' line if it involves an addition we're not including
skippedNewlineMessageIndex = lineIdx + 1
}
}
return newLines
}
func transformedFirstChar(firstChar string, reverse bool, isLineSelected bool) string {
if reverse {
if !isLineSelected && firstChar == "+" {
return " "
} else if firstChar == "-" {
return "+"
} else if firstChar == "+" {
return "-"
} else {
return firstChar
}
}
if !isLineSelected && firstChar == "-" {
return " "
}
return firstChar
}
func (hunk *PatchHunk) formatHeader(oldStart int, oldLength int, newStart int, newLength int, heading string) string {
return fmt.Sprintf("@@ -%d,%d +%d,%d @@%s\n", oldStart, oldLength, newStart, newLength, heading)
}
func (hunk *PatchHunk) formatWithChanges(lineIndices []int, reverse bool, startOffset int) (int, string) {
bodyLines := hunk.updatedLines(lineIndices, reverse)
startOffset, header, ok := hunk.updatedHeader(bodyLines, startOffset, reverse)
if !ok {
return startOffset, ""
}
return startOffset, header + strings.Join(bodyLines, "")
}
func (hunk *PatchHunk) updatedHeader(newBodyLines []string, startOffset int, reverse bool) (int, string, bool) {
changeCount := nLinesWithPrefix(newBodyLines, []string{"+", "-"})
oldLength := nLinesWithPrefix(newBodyLines, []string{" ", "-"})
newLength := nLinesWithPrefix(newBodyLines, []string{"+", " "})
if changeCount == 0 {
// if nothing has changed we just return nothing
return startOffset, "", false
}
var oldStart int
if reverse {
oldStart = hunk.newStart
} else {
oldStart = hunk.oldStart
}
var newStartOffset int
// if the hunk went from zero to positive length, we need to increment the starting point by one
// if the hunk went from positive to zero length, we need to decrement the starting point by one
if oldLength == 0 {
newStartOffset = 1
} else if newLength == 0 {
newStartOffset = -1
} else {
newStartOffset = 0
}
newStart := oldStart + startOffset + newStartOffset
newStartOffset = startOffset + newLength - oldLength
formattedHeader := hunk.formatHeader(oldStart, oldLength, newStart, newLength, hunk.heading)
return newStartOffset, formattedHeader, true
}

View File

@@ -0,0 +1,305 @@
package patch
import (
"sort"
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
type PatchStatus int
const (
// UNSELECTED is for when the commit file has not been added to the patch in any way
UNSELECTED PatchStatus = iota
// WHOLE is for when you want to add the whole diff of a file to the patch,
// including e.g. if it was deleted
WHOLE
// PART is for when you're only talking about specific lines that have been modified
PART
)
type fileInfo struct {
mode PatchStatus
includedLineIndices []int
diff string
}
type applyPatchFunc func(patch string, flags ...string) error
type loadFileDiffFunc func(from string, to string, reverse bool, filename string, plain bool) (string, error)
// PatchManager manages the building of a patch for a commit to be applied to another commit (or the working tree, or removed from the current commit). We also support building patches from things like stashes, for which there is less flexibility
type PatchManager struct {
// To is the commit sha if we're dealing with files of a commit, or a stash ref for a stash
To string
From string
Reverse bool
// CanRebase tells us whether we're allowed to modify our commits. CanRebase should be true for commits of the currently checked out branch and false for everything else
// TODO: move this out into a proper mode struct in the gui package: it doesn't really belong here
CanRebase bool
// fileInfoMap starts empty but you add files to it as you go along
fileInfoMap map[string]*fileInfo
Log *logrus.Entry
ApplyPatch applyPatchFunc
// LoadFileDiff loads the diff of a file, for a given to (typically a commit SHA)
LoadFileDiff loadFileDiffFunc
}
// NewPatchManager returns a new PatchManager
func NewPatchManager(log *logrus.Entry, applyPatch applyPatchFunc, loadFileDiff loadFileDiffFunc) *PatchManager {
return &PatchManager{
Log: log,
ApplyPatch: applyPatch,
LoadFileDiff: loadFileDiff,
}
}
// NewPatchManager returns a new PatchManager
func (p *PatchManager) Start(from, to string, reverse bool, canRebase bool) {
p.To = to
p.From = from
p.Reverse = reverse
p.CanRebase = canRebase
p.fileInfoMap = map[string]*fileInfo{}
}
func (p *PatchManager) addFileWhole(info *fileInfo) {
info.mode = WHOLE
lineCount := len(strings.Split(info.diff, "\n"))
info.includedLineIndices = make([]int, lineCount)
// add every line index
for i := 0; i < lineCount; i++ {
info.includedLineIndices[i] = i
}
}
func (p *PatchManager) removeFile(info *fileInfo) {
info.mode = UNSELECTED
info.includedLineIndices = nil
}
func (p *PatchManager) AddFileWhole(filename string) error {
info, err := p.getFileInfo(filename)
if err != nil {
return err
}
p.addFileWhole(info)
return nil
}
func (p *PatchManager) RemoveFile(filename string) error {
info, err := p.getFileInfo(filename)
if err != nil {
return err
}
p.removeFile(info)
return nil
}
func getIndicesForRange(first, last int) []int {
indices := []int{}
for i := first; i <= last; i++ {
indices = append(indices, i)
}
return indices
}
func (p *PatchManager) getFileInfo(filename string) (*fileInfo, error) {
info, ok := p.fileInfoMap[filename]
if ok {
return info, nil
}
diff, err := p.LoadFileDiff(p.From, p.To, p.Reverse, filename, true)
if err != nil {
return nil, err
}
info = &fileInfo{
mode: UNSELECTED,
diff: diff,
}
p.fileInfoMap[filename] = info
return info, nil
}
func (p *PatchManager) AddFileLineRange(filename string, firstLineIdx, lastLineIdx int) error {
info, err := p.getFileInfo(filename)
if err != nil {
return err
}
info.mode = PART
info.includedLineIndices = utils.UnionInt(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx))
return nil
}
func (p *PatchManager) RemoveFileLineRange(filename string, firstLineIdx, lastLineIdx int) error {
info, err := p.getFileInfo(filename)
if err != nil {
return err
}
info.mode = PART
info.includedLineIndices = utils.DifferenceInt(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx))
if len(info.includedLineIndices) == 0 {
p.removeFile(info)
}
return nil
}
func (p *PatchManager) renderPlainPatchForFile(filename string, reverse bool, keepOriginalHeader bool) string {
info, err := p.getFileInfo(filename)
if err != nil {
p.Log.Error(err)
return ""
}
switch info.mode {
case WHOLE:
// use the whole diff
// the reverse flag is only for part patches so we're ignoring it here
return info.diff
case PART:
// generate a new diff with just the selected lines
return ModifiedPatchForLines(p.Log, filename, info.diff, info.includedLineIndices, reverse, keepOriginalHeader)
default:
return ""
}
}
func (p *PatchManager) RenderPatchForFile(filename string, plain bool, reverse bool, keepOriginalHeader bool) string {
patch := p.renderPlainPatchForFile(filename, reverse, keepOriginalHeader)
if plain {
return patch
}
parser := NewPatchParser(p.Log, patch)
// not passing included lines because we don't want to see them in the secondary panel
return parser.Render(-1, -1, nil)
}
func (p *PatchManager) renderEachFilePatch(plain bool) []string {
// sort files by name then iterate through and render each patch
filenames := make([]string, len(p.fileInfoMap))
index := 0
for filename := range p.fileInfoMap {
filenames[index] = filename
index++
}
sort.Strings(filenames)
output := []string{}
for _, filename := range filenames {
patch := p.RenderPatchForFile(filename, plain, false, true)
if patch != "" {
output = append(output, patch)
}
}
return output
}
func (p *PatchManager) RenderAggregatedPatchColored(plain bool) string {
result := ""
for _, patch := range p.renderEachFilePatch(plain) {
if patch != "" {
result += patch + "\n"
}
}
return result
}
func (p *PatchManager) GetFileStatus(filename string, parent string) PatchStatus {
if parent != p.To {
return UNSELECTED
}
info, ok := p.fileInfoMap[filename]
if !ok {
return UNSELECTED
}
return info.mode
}
func (p *PatchManager) GetFileIncLineIndices(filename string) ([]int, error) {
info, err := p.getFileInfo(filename)
if err != nil {
return nil, err
}
return info.includedLineIndices, nil
}
func (p *PatchManager) ApplyPatches(reverse bool) error {
// for whole patches we'll apply the patch in reverse
// but for part patches we'll apply a reverse patch forwards
for filename, info := range p.fileInfoMap {
if info.mode == UNSELECTED {
continue
}
applyFlags := []string{"index", "3way"}
reverseOnGenerate := false
if reverse {
if info.mode == WHOLE {
applyFlags = append(applyFlags, "reverse")
} else {
reverseOnGenerate = true
}
}
var err error
// first run we try with the original header, then without
for _, keepOriginalHeader := range []bool{true, false} {
patch := p.RenderPatchForFile(filename, true, reverseOnGenerate, keepOriginalHeader)
if patch == "" {
continue
}
if err = p.ApplyPatch(patch, applyFlags...); err != nil {
continue
}
break
}
if err != nil {
return err
}
}
return nil
}
// clears the patch
func (p *PatchManager) Reset() {
p.To = ""
p.fileInfoMap = map[string]*fileInfo{}
}
func (p *PatchManager) Active() bool {
return p.To != ""
}
func (p *PatchManager) IsEmpty() bool {
for _, fileInfo := range p.fileInfoMap {
if fileInfo.mode == WHOLE || (fileInfo.mode == PART && len(fileInfo.includedLineIndices) > 0) {
return false
}
}
return true
}
// if any of these things change we'll need to reset and start a new patch
func (p *PatchManager) NewPatchRequired(from string, to string, reverse bool) bool {
return from != p.From || to != p.To || reverse != p.Reverse
}

View File

@@ -0,0 +1,165 @@
package patch
import (
"fmt"
"regexp"
"strings"
"github.com/sirupsen/logrus"
)
var hunkHeaderRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)[^\+]+\+(\d+)[^@]+@@(.*)$`)
var patchHeaderRegexp = regexp.MustCompile(`(?ms)(^diff.*?)^@@`)
func GetHeaderFromDiff(diff string) string {
match := patchHeaderRegexp.FindStringSubmatch(diff)
if len(match) <= 1 {
return ""
}
return match[1]
}
func GetHunksFromDiff(diff string) []*PatchHunk {
hunks := []*PatchHunk{}
firstLineIdx := -1
var hunkLines []string
pastDiffHeader := false
for lineIdx, line := range strings.SplitAfter(diff, "\n") {
isHunkHeader := strings.HasPrefix(line, "@@ -")
if isHunkHeader {
if pastDiffHeader { // we need to persist the current hunk
hunks = append(hunks, newHunk(hunkLines, firstLineIdx))
}
pastDiffHeader = true
firstLineIdx = lineIdx
hunkLines = []string{line}
continue
}
if !pastDiffHeader { // skip through the stuff that precedes the first hunk
continue
}
hunkLines = append(hunkLines, line)
}
if pastDiffHeader {
hunks = append(hunks, newHunk(hunkLines, firstLineIdx))
}
return hunks
}
type PatchModifier struct {
Log *logrus.Entry
filename string
hunks []*PatchHunk
header string
}
func NewPatchModifier(log *logrus.Entry, filename string, diffText string) *PatchModifier {
return &PatchModifier{
Log: log,
filename: filename,
hunks: GetHunksFromDiff(diffText),
header: GetHeaderFromDiff(diffText),
}
}
func (d *PatchModifier) ModifiedPatchForLines(lineIndices []int, reverse bool, keepOriginalHeader bool) string {
// step one is getting only those hunks which we care about
hunksInRange := []*PatchHunk{}
outer:
for _, hunk := range d.hunks {
// if there is any line in our lineIndices array that the hunk contains, we append it
for _, lineIdx := range lineIndices {
if lineIdx >= hunk.FirstLineIdx && lineIdx <= hunk.LastLineIdx() {
hunksInRange = append(hunksInRange, hunk)
continue outer
}
}
}
// step 2 is collecting all the hunks with new headers
startOffset := 0
formattedHunks := ""
var formattedHunk string
for _, hunk := range hunksInRange {
startOffset, formattedHunk = hunk.formatWithChanges(lineIndices, reverse, startOffset)
formattedHunks += formattedHunk
}
if formattedHunks == "" {
return ""
}
var fileHeader string
// for staging/unstaging lines we don't want the original header because
// it makes git confused e.g. when dealing with deleted/added files
// but with building and applying patches the original header gives git
// information it needs to cleanly apply patches
if keepOriginalHeader {
fileHeader = d.header
} else {
fileHeader = fmt.Sprintf("--- a/%s\n+++ b/%s\n", d.filename, d.filename)
}
return fileHeader + formattedHunks
}
func (d *PatchModifier) ModifiedPatchForRange(firstLineIdx int, lastLineIdx int, reverse bool, keepOriginalHeader bool) string {
// generate array of consecutive line indices from our range
selectedLines := []int{}
for i := firstLineIdx; i <= lastLineIdx; i++ {
selectedLines = append(selectedLines, i)
}
return d.ModifiedPatchForLines(selectedLines, reverse, keepOriginalHeader)
}
func (d *PatchModifier) OriginalPatchLength() int {
if len(d.hunks) == 0 {
return 0
}
return d.hunks[len(d.hunks)-1].LastLineIdx()
}
func ModifiedPatchForRange(log *logrus.Entry, filename string, diffText string, firstLineIdx int, lastLineIdx int, reverse bool, keepOriginalHeader bool) string {
p := NewPatchModifier(log, filename, diffText)
return p.ModifiedPatchForRange(firstLineIdx, lastLineIdx, reverse, keepOriginalHeader)
}
func ModifiedPatchForLines(log *logrus.Entry, filename string, diffText string, includedLineIndices []int, reverse bool, keepOriginalHeader bool) string {
p := NewPatchModifier(log, filename, diffText)
return p.ModifiedPatchForLines(includedLineIndices, reverse, keepOriginalHeader)
}
// I want to know, given a hunk, what line a given index is on
func (hunk *PatchHunk) LineNumberOfLine(idx int) int {
n := idx - hunk.FirstLineIdx - 1
if n < 0 {
n = 0
} else if n >= len(hunk.bodyLines) {
n = len(hunk.bodyLines) - 1
}
lines := hunk.bodyLines[0:n]
offset := nLinesWithPrefix(lines, []string{"+", " "})
return hunk.newStart + offset
}
func nLinesWithPrefix(lines []string, chars []string) int {
result := 0
for _, line := range lines {
for _, char := range chars {
if line[:1] == char {
result++
}
}
}
return result
}

View File

@@ -1,7 +1,8 @@
package commands
package patch
import (
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
@@ -88,6 +89,15 @@ index e69de29..c6568ea 100644
\ No newline at end of file
`
const exampleHunk = `@@ -1,5 +1,5 @@
apple
-grape
+orange
...
...
...
`
// TestModifyPatchForRange is a function.
func TestModifyPatchForRange(t *testing.T) {
type scenario struct {
@@ -509,3 +519,30 @@ func TestModifyPatchForRange(t *testing.T) {
})
}
}
func TestLineNumberOfLine(t *testing.T) {
type scenario struct {
testName string
hunk *PatchHunk
idx int
expected int
}
scenarios := []scenario{
{
testName: "nothing selected",
hunk: newHunk(strings.SplitAfter(exampleHunk, "\n"), 10),
idx: 15,
expected: 3,
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
result := s.hunk.LineNumberOfLine(s.idx)
if !assert.Equal(t, s.expected, result) {
fmt.Println(result)
}
})
}
}

View File

@@ -1,17 +1,19 @@
package commands
package patch
import (
"regexp"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
type PatchLineKind int
const (
PATCH_HEADER = iota
PATCH_HEADER PatchLineKind = iota
COMMIT_SHA
COMMIT_DESCRIPTION
HUNK_HEADER
@@ -24,7 +26,7 @@ const (
// the job of this file is to parse a diff, find out where the hunks begin and end, which lines are stageable, and how to find the next hunk from the current position or the next stageable line from the current position.
type PatchLine struct {
Kind int
Kind PatchLineKind
Content string // something like '+ hello' (note the first character is not removed)
}
@@ -37,11 +39,8 @@ type PatchParser struct {
}
// NewPatchParser builds a new branch list builder
func NewPatchParser(log *logrus.Entry, patch string) (*PatchParser, error) {
hunkStarts, stageableLines, patchLines, err := parsePatch(patch)
if err != nil {
return nil, err
}
func NewPatchParser(log *logrus.Entry, patch string) *PatchParser {
hunkStarts, stageableLines, patchLines := parsePatch(patch)
patchHunks := GetHunksFromDiff(patch)
@@ -51,7 +50,7 @@ func NewPatchParser(log *logrus.Entry, patch string) (*PatchParser, error) {
StageableLines: stageableLines,
PatchLines: patchLines,
PatchHunks: patchHunks,
}, nil
}
}
// GetHunkContainingLine takes a line index and an offset and finds the hunk
@@ -63,7 +62,7 @@ func (p *PatchParser) GetHunkContainingLine(lineIndex int, offset int) *PatchHun
}
for index, hunk := range p.PatchHunks {
if lineIndex >= hunk.FirstLineIdx && lineIndex <= hunk.LastLineIdx {
if lineIndex >= hunk.FirstLineIdx && lineIndex <= hunk.LastLineIdx() {
resultIndex := index + offset
if resultIndex < 0 {
resultIndex = 0
@@ -75,7 +74,7 @@ func (p *PatchParser) GetHunkContainingLine(lineIndex int, offset int) *PatchHun
}
// if your cursor is past the last hunk, select the last hunk
if lineIndex > p.PatchHunks[len(p.PatchHunks)-1].LastLineIdx {
if lineIndex > p.PatchHunks[len(p.PatchHunks)-1].LastLineIdx() {
return p.PatchHunks[len(p.PatchHunks)-1]
}
@@ -96,55 +95,49 @@ func (l *PatchLine) render(selected bool, included bool) string {
if l.Kind == HUNK_HEADER {
re := regexp.MustCompile("(@@.*?@@)(.*)")
match := re.FindStringSubmatch(content)
return coloredString(color.FgCyan, match[1], selected, included) + coloredString(theme.DefaultTextColor, match[2], selected, false)
return coloredString(style.FgCyan, match[1], selected, included) + coloredString(theme.DefaultTextColor, match[2], selected, false)
}
var colorAttr color.Attribute
textStyle := theme.DefaultTextColor
switch l.Kind {
case PATCH_HEADER:
colorAttr = color.Bold
textStyle = textStyle.SetBold()
case ADDITION:
colorAttr = color.FgGreen
textStyle = style.FgGreen
case DELETION:
colorAttr = color.FgRed
textStyle = style.FgRed
case COMMIT_SHA:
colorAttr = color.FgYellow
default:
colorAttr = theme.DefaultTextColor
textStyle = style.FgYellow
}
return coloredString(colorAttr, content, selected, included)
return coloredString(textStyle, content, selected, included)
}
func coloredString(colorAttr color.Attribute, str string, selected bool, included bool) string {
var cl *color.Color
attributes := []color.Attribute{colorAttr}
func coloredString(textStyle style.TextStyle, str string, selected bool, included bool) string {
if selected {
attributes = append(attributes, color.BgBlue)
textStyle = textStyle.MergeStyle(theme.SelectedRangeBgColor)
}
cl = color.New(attributes...)
var clIncluded *color.Color
firstCharStyle := textStyle
if included {
clIncluded = color.New(append(attributes, color.BgGreen)...)
} else {
clIncluded = color.New(attributes...)
firstCharStyle = firstCharStyle.MergeStyle(style.BgGreen)
}
if len(str) < 2 {
return utils.ColoredStringDirect(str, clIncluded)
return firstCharStyle.Sprint(str)
}
return utils.ColoredStringDirect(str[:1], clIncluded) + utils.ColoredStringDirect(str[1:], cl)
return firstCharStyle.Sprint(str[:1]) + textStyle.Sprint(str[1:])
}
func parsePatch(patch string) ([]int, []int, []*PatchLine, error) {
func parsePatch(patch string) ([]int, []int, []*PatchLine) {
lines := strings.Split(patch, "\n")
hunkStarts := []int{}
stageableLines := []int{}
pastFirstHunkHeader := false
pastCommitDescription := true
patchLines := make([]*PatchLine, len(lines))
var lineKind int
var lineKind PatchLineKind
var firstChar string
for index, line := range lines {
firstChar = " "
@@ -183,7 +176,7 @@ func parsePatch(patch string) ([]int, []int, []*PatchLine, error) {
}
patchLines[index] = &PatchLine{Kind: lineKind, Content: line}
}
return hunkStarts, stageableLines, patchLines, nil
return hunkStarts, stageableLines, patchLines
}
// Render returns the coloured string of the diff with any selected lines highlighted
@@ -201,6 +194,19 @@ func (p *PatchParser) Render(firstLineIndex int, lastLineIndex int, incLineIndic
return result
}
// PlainRenderLines returns the non-coloured string of diff part from firstLineIndex to
// lastLineIndex
func (p *PatchParser) PlainRenderLines(firstLineIndex, lastLineIndex int) string {
linesToCopy := p.PatchLines[firstLineIndex : lastLineIndex+1]
renderedLines := make([]string, len(linesToCopy))
for index, line := range linesToCopy {
renderedLines[index] = line.Content
}
return strings.Join(renderedLines, "\n")
}
// GetNextStageableLineIndex takes a line index and returns the line index of the next stageable line
// note this will actually include the current index if it is stageable
func (p *PatchParser) GetNextStageableLineIndex(currentIndex int) int {

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