Compare commits

..

248 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
1383 changed files with 51048 additions and 25903 deletions

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"

View File

@@ -29,26 +29,3 @@ jobs:
with:
token: ${{secrets.GITHUB_API_TOKEN}}
formula: lazygit
ppa:
runs-on: ubuntu-20.04
steps:
- name: Checkout PPA repo
uses: actions/checkout@v2
with:
repository: dawidd6/lazygit-debian
token: ${{secrets.GITHUB_API_TOKEN}}
fetch-depth: 0
- name: Setup git
uses: dawidd6/action-git-user-config@v1
- name: Update PPA repo
run: |
version="$(echo "$GITHUB_REF" | sed 's@refs/tags/v@@')"
sudo apt update
sudo apt install -y git-buildpackage
git fetch --tags https://github.com/$GITHUB_REPOSITORY
gbp import-ref -u "$version"
gbp dch -D xenial -N "$version"-1
git add debian/changelog
git commit -m "d/changelog: dch $version"
gbp tag
git push --tags origin master

View File

@@ -8,7 +8,14 @@ on:
jobs:
ci:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- windows-latest
name: ci - ${{matrix.os}}
runs-on: ${{matrix.os}}
env:
GOFLAGS: -mod=vendor
steps:
@@ -22,19 +29,37 @@ jobs:
uses: actions/cache@v1
with:
path: ~/.cache/go-build
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-test
restore-keys: |
${{runner.os}}-go-
- 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
- name: Test code
run: |
./test.sh
- name: Build binaries
uses: goreleaser/goreleaser-action@v1
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:
args: --skip-publish --snapshot
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

View File

@@ -12,3 +12,9 @@ jobs:
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

4
.gitignore vendored
View File

@@ -27,8 +27,10 @@ lazygit
test/git_server/data
test/integration/*/actual/
test/integration/*/actual_remote/
test/integration/*/used_config/
# these sample hooks waste too space space
# these sample hooks waste too much space
test/integration/*/expected/.git_keep/hooks/
test/integration/*/expected_remote/hooks/
!.git_keep/
lazygit.exe

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

@@ -21,7 +21,6 @@ If you're a mere mortal like me and you're tired of hearing how powerful git is
- [Binary releases](#binary-releases)
- [Homebrew](#homebrew)
- [MacPorts](#macports)
- [Ubuntu](#ubuntu)
- [Void Linux](#void-linux)
- [Scoop (Windows)](#scoop-windows)
- [Arch Linux](#arch-linux)
@@ -53,7 +52,7 @@ Github Sponsors is matching all donations dollar-for-dollar for 12 months so if
### Binary Releases
For Windows, Mac OS(10.10+) or Linux, you can download a binary release [here](../../releases).
For Windows, Mac OS(10.12+) or Linux, you can download a binary release [here](../../releases).
### Homebrew
@@ -82,6 +81,8 @@ sudo port install lazygit
### Ubuntu
**Deprecated**: will no longer receive updates.
Packages for Ubuntu are available via [Launchpad PPA](https://launchpad.net/~lazygit-team).
```sh

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

@@ -22,6 +22,7 @@ gui:
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:
@@ -35,12 +36,17 @@ gui:
- 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
@@ -53,14 +59,17 @@ git:
manualCommit: false
# extra args passed to `git merge`, e.g. --no-ff
args: ''
pull:
mode: 'merge' # one of 'merge' | 'rebase' | 'ff-only'
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
@@ -92,12 +101,14 @@ keybinding:
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'
@@ -127,7 +138,9 @@ keybinding:
diffingMenu-alt: '<c-e>' # deprecated
copyToClipboard: '<c-o>'
submitEditorText: '<enter>'
appendNewline: '<tab>'
appendNewline: '<a-enter>'
extrasMenu: '@'
toggleWhitespaceInDiffView: '<c-w>'
status:
checkForUpdate: 'u'
recentRepos: '<enter>'
@@ -146,6 +159,7 @@ keybinding:
toggleTreeView: '`'
branches:
createPullRequest: 'o'
viewPullRequestOptions: 'O'
checkoutBranchByName: 'c'
forceCheckoutBranch: 'F'
rebaseBranch: 'r'
@@ -196,14 +210,14 @@ keybinding:
```yaml
os:
openCommand: 'cmd /c "start "" {{filename}}"'
openCommand: 'start "" {{filename}}'
```
### Linux
```yaml
os:
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'
openCommand: 'xdg-open {{filename}} >/dev/null'
```
### OSX
@@ -213,6 +227,57 @@ os:
openCommand: 'open {{filename}}'
```
### 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
@@ -227,7 +292,8 @@ os:
For color attributes you can choose an array of attributes (with max one color attribute)
The available attributes are:
- default
**Colors**
- black
- red
- green
@@ -236,7 +302,12 @@ The available attributes are:
- magenta
- cyan
- white
- '#ff00ff'
**Modifiers**
- bold
- default
- reverse # useful for high-contrast
- underline

View File

@@ -35,6 +35,20 @@ customCommands:
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:
@@ -79,12 +93,28 @@ The permitted contexts are:
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 |
| _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_ |

View File

@@ -3,6 +3,7 @@
## 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
@@ -16,9 +17,10 @@
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode
<kbd>:</kbd>: execute custom command
<kbd>|</kbd>: view filter-by-path options
<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>
## List Panel Navigation
@@ -38,6 +40,7 @@
<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
@@ -122,7 +125,7 @@
<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>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
@@ -154,6 +157,12 @@
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
</pre>
## Extras Panel
<pre>
<kbd>@</kbd>: open command log menu
</pre>
## Files Panel (Files)
<pre>
@@ -176,6 +185,8 @@
<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>
## Files Panel (Submodules)
@@ -195,12 +206,13 @@
<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 both hunks
<kbd>b</kbd>: pick all hunks
<kbd>◄</kbd>: select previous conflict
<kbd>►</kbd>: select next conflict
<kbd>▲</kbd>: select top hunk
<kbd>▼</kbd>: select bottom hunk
<kbd>▲</kbd>: select previous hunk
<kbd>▼</kbd>: select next hunk
<kbd>z</kbd>: undo
</pre>

View File

@@ -1,10 +1,11 @@
# Lazygit Sneltoetsen
## Globaale Sneltoetsen
## Globale Sneltoetsen
<pre>
<kbd>pgup</kbd>: scroll naar beneden vanaf hooft paneel (fn+up)
<kbd>pgdown</kbd>: scroll naar beneden vabaf hooft paneel (fn+down)
<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
@@ -13,31 +14,33 @@
<kbd>x</kbd>: open menu
<kbd>z</kbd>: ongedaan maken (via reflog) (experimenteel)
<kbd>ctrl+z</kbd>: redo (via reflog) (experimenteel)
<kbd>+</kbd>: volgende schermmode (normaal/half/groot )
<kbd>_</kbd>: vorige schermmode
<kbd>:</kbd>: voor aangepast commando uit
<kbd>|</kbd>: bekijk scoping opties
<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>
## List Panel Navigation
## Lijstpaneel Navigatie
<pre>
<kbd>.</kbd>: volgende pagina
<kbd>,</kbd>: vorige pagina
<kbd><</kbd>: scroll naar boven
<kbd>></kbd>: scroll naar beneden
<kbd>/</kbd>: start met zoekken
<kbd>]</kbd>: volgende tab
<kbd>[</kbd>: vorige tab
<kbd>/</kbd>: start met zoeken
<kbd>]</kbd>: volgende tabblad
<kbd>[</kbd>: vorige tabblad
</pre>
## Branches Paneel (Branches Tab)
## Branches Paneel (Branches Tabblad)
<pre>
<kbd>space</kbd>: uitchecken
<kbd>o</kbd>: maak een pull-aanvraag
<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
@@ -49,16 +52,16 @@
<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>: copieer branch name naar clipboard
<kbd>enter</kbd>: view commits
<kbd>ctrl+o</kbd>: kopieer branch name naar klembord
<kbd>enter</kbd>: bekijk commits
</pre>
## Branches Paneel (Remote Branches (in Remotes tab))
## 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>: view commits
<kbd>enter</kbd>: bekijk commits
<kbd>space</kbd>: uitchecken
<kbd>n</kbd>: nieuwe branch
<kbd>M</kbd>: merge in met huidige checked out branch
@@ -67,7 +70,7 @@
<kbd>u</kbd>: stel in als upstream van uitgecheckte branch
</pre>
## Branches Paneel (Remotes Tab)
## Branches Paneel (Remotes Tabblad)
<pre>
<kbd>f</kbd>: fetch remote
@@ -83,13 +86,13 @@
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: bekijk reset opties
<kbd>n</kbd>: nieuwe branch
<kbd>c</kbd>: kopiëer commit (cherry-pick)
<kbd>C</kbd>: kopiëer commit reeks (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (gecopieerde) commits selectie
<kbd>ctrl+o</kbd>: copieer commit SHA naar clipboard
<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 Tab)
## Branches Paneel (Tags Tabblad)
<pre>
<kbd>space</kbd>: uitchecken
@@ -97,7 +100,7 @@
<kbd>P</kbd>: push tag
<kbd>n</kbd>: creëer tag
<kbd>g</kbd>: bekijk reset opties
<kbd>enter</kbd>: view commits
<kbd>enter</kbd>: bekijk commits
</pre>
## Commit bestanden Paneel
@@ -109,8 +112,8 @@
<kbd>o</kbd>: open bestand
<kbd>e</kbd>: verander bestand
<kbd>space</kbd>: toggle bestand inbegrepen in patch
<kbd>enter</kbd>: enter bestand to add selecteered lines to the patch
<kbd>`</kbd>: toggle file tree view
<kbd>enter</kbd>: enter bestand om geselecteerde regels toe te voegen aan de patch
<kbd>`</kbd>: toggle bestandsboom weergave
</pre>
## Commits Paneel (Commits)
@@ -130,28 +133,34 @@
<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>: kopiëer commit (cherry-pick)
<kbd>ctrl+o</kbd>: copieer commit SHA naar clipboard
<kbd>C</kbd>: kopiëer commit reeks (cherry-pick)
<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>: create new branch off of commit
<kbd>n</kbd>: creëer nieuwe branch van commit
<kbd>T</kbd>: tag commit
<kbd>ctrl+r</kbd>: reset cherry-picked (gecopieerde) commits selectie
<kbd>ctrl+y</kbd>: copieer commit bericht naar clipboard
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
<kbd>ctrl+y</kbd>: kopieer commit bericht naar klembord
</pre>
## Commits Paneel (Reflog Tab)
## 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>: kopiëer commit (cherry-pick)
<kbd>C</kbd>: kopiëer commit reeks (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (gecopieerde) commits selectie
<kbd>ctrl+o</kbd>: copieer commit SHA naar clipboard
<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)
@@ -175,26 +184,29 @@
<kbd>f</kbd>: fetch
<kbd>ctrl+o</kbd>: kopieer de bestandsnaam naar het klembord
<kbd>g</kbd>: bekijk upstream reset opties
<kbd>`</kbd>: toggle file tree view
<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>
## Bestanden Paneel (Submodules)
<pre>
<kbd>ctrl+o</kbd>: copy submodule name to clipboard
<kbd>ctrl+o</kbd>: kopieer submodule naam naar klembord
<kbd>enter</kbd>: enter submodule
<kbd>d</kbd>: view reset and remove submodule options
<kbd>d</kbd>: bekijk reset en verwijder submodule opties
<kbd>u</kbd>: update submodule
<kbd>n</kbd>: add new submodule
<kbd>n</kbd>: voeg nieuwe submodule toe
<kbd>e</kbd>: update submodule URL
<kbd>i</kbd>: initialize submodule
<kbd>b</kbd>: view bulk submodule options
<kbd>i</kbd>: initialiseer submodule
<kbd>b</kbd>: bekijk bulk submodule opties
</pre>
## Hooft Paneel (Merggen)
## Hoofd Paneel (Mergen)
<pre>
<kbd>esc</kbd>: ga terug naar het bestanden paneel
<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
@@ -204,29 +216,29 @@
<kbd>z</kbd>: ongedaan maken
</pre>
## Hooft Paneel (Normaal)
## Hoofd Paneel (Normaal)
<pre>
<kbd>Ő</kbd>: scroll omlaag (fn+up)
<kbd>ő</kbd>: scroll omhoog (fn+down)
</pre>
## Hooft Paneel (Patch Bouwen)
## Hoofd Paneel (Patch Bouwen)
<pre>
<kbd>esc</kbd>: sluit lijn-bij-lijn mode
<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
<kbd>v</kbd>: toggle drag selecteer
<kbd>V</kbd>: toggle drag selecteer
<kbd>a</kbd>: toggle selecteer hunk
</pre>
## Hooft Paneel (Staging)
## Hoofd Paneel (Staging)
<pre>
<kbd>esc</kbd>: ga terug naar het bestanden paneel
@@ -240,9 +252,9 @@
<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>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
@@ -257,7 +269,7 @@
## Stash Paneel
<pre>
<kbd>enter</kbd>: view stash entry's files
<kbd>enter</kbd>: bekijk bestanden van stash entry
<kbd>space</kbd>: toepassen
<kbd>g</kbd>: pop
<kbd>d</kbd>: laten vallen
@@ -271,5 +283,5 @@
<kbd>o</kbd>: open config bestand
<kbd>u</kbd>: check voor updates
<kbd>enter</kbd>: wissel naar een recente repo
<kbd>a</kbd>: alle takken van het houtblok laten zien
<kbd>a</kbd>: alle logs van de branch laten zien
</pre>

View File

@@ -3,6 +3,7 @@
## 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
@@ -16,9 +17,10 @@
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode
<kbd>:</kbd>: execute custom command
<kbd>|</kbd>: view filter-by-path options
<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>
## List Panel Navigation
@@ -38,6 +40,7 @@
<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
@@ -122,7 +125,7 @@
<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>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
@@ -154,6 +157,12 @@
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
</pre>
## Extras Panel
<pre>
<kbd>@</kbd>: open command log menu
</pre>
## Pliki Panel (Pliki)
<pre>
@@ -176,6 +185,8 @@
<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>
## Pliki Panel (Submodules)
@@ -195,12 +206,13 @@
<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 both hunks
<kbd>b</kbd>: pick all hunks
<kbd>◄</kbd>: select previous conflict
<kbd>►</kbd>: select next conflict
<kbd>▲</kbd>: select top hunk
<kbd>▼</kbd>: select bottom hunk
<kbd>▲</kbd>: select previous hunk
<kbd>▼</kbd>: select next hunk
<kbd>z</kbd>: cofnij
</pre>

29
go.mod
View File

@@ -9,39 +9,42 @@ require (
github.com/cli/safeexec v1.0.0
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
github.com/creack/pty v1.1.11
github.com/fatih/color v1.9.0
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/gdamore/tcell/v2 v2.2.0 // indirect
github.com/go-errors/errors v1.1.1
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/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.20210417110745-37f79434200d
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe // indirect
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/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.12
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mgutz/str v1.2.0
github.com/onsi/ginkgo v1.10.3 // indirect
github.com/onsi/gomega v1.7.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
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/spkg/bom v0.0.0-20160624110644-59b7046e48ad
github.com/stretchr/testify v1.4.0
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-20210415045647-66c3f260301c // indirect
golang.org/x/term v0.0.0-20210406210042-72f3dc4e9b72 // indirect
golang.org/x/text v0.3.6 // 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
)
replace github.com/go-git/go-git/v5 => github.com/jesseduffield/go-git/v5 v5.1.1

148
go.sum
View File

@@ -23,7 +23,6 @@ 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/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.1-0.20180516100307-2d684516a886 h1:NAFoy+QgUpERgK3y1xiVh5HcOvSeZHpXTTo5qnvnuK4=
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=
@@ -33,27 +32,24 @@ github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
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.0.0 h1:GRWG8aLfWAlekj9Q6W29bVvkHENc6hp79XOqG4AWDOs=
github.com/gdamore/tcell/v2 v2.0.0/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
github.com/gdamore/tcell/v2 v2.1.0 h1:UnSmozHgBkQi2PGsFr+rpdXuAPRRucMegpQp3Z3kDro=
github.com/gdamore/tcell/v2 v2.1.0/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
github.com/gdamore/tcell/v2 v2.2.0 h1:vSyEgKwraXPSOkvCk7IwOSyX+Pv3V2cV9CikJMXg4U4=
github.com/gdamore/tcell/v2 v2.2.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
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.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.1.1 h1:ljK/pL5ltg3qoN+OtN6yCv9HWSfMwxSx90GJCZQxYNg=
github.com/go-errors/errors v1.1.1/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 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
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/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -62,66 +58,35 @@ github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
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/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/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
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-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.20161105104656-d666c9f652af h1:9ZI/QyVOerYYeqMt4svycU2Lz0WvxNHCpHHbsFsi/oA=
github.com/jesseduffield/gocui v0.3.1-0.20161105104656-d666c9f652af/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20201010224802-8a6768078fd7 h1:K3MGrjmpPtIhfXmKh/zsIF0CdmNKOkjpIwcUfAa/J2A=
github.com/jesseduffield/gocui v0.3.1-0.20201010224802-8a6768078fd7/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20201224041937-f5a9733d1860 h1:1xfQM6T5A4jqcVvUnYaKR6bGrOhDLWQsp79JFNJpzcQ=
github.com/jesseduffield/gocui v0.3.1-0.20201224041937-f5a9733d1860/go.mod h1:9LmtJcK+Kwiuc2huslzS37uFJPdHka2Cs/cQ06JZdbk=
github.com/jesseduffield/gocui v0.3.1-0.20210208224444-2eecee85583d h1:Jto9W9w8CFwZiAYXa7LsHDEOb5cKCA1f5LOL1A3jva4=
github.com/jesseduffield/gocui v0.3.1-0.20210208224444-2eecee85583d/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20210329125022-96b1d3106429 h1:Ih3UVczKRabZnQ7RisGi5uItC2QJxdqgef7AClJ2G9A=
github.com/jesseduffield/gocui v0.3.1-0.20210329125022-96b1d3106429/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20210329125502-e830abf4b73a h1:RVYf2MA/RJbodE+S0e2z++JmB9A7hD1lUsI0euv1fmA=
github.com/jesseduffield/gocui v0.3.1-0.20210329125502-e830abf4b73a/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20210329130738-e026850021e3 h1:UDiArPlzkg+8mmNjhUOamQoyiTSzQUGIpOsu5hCRJVI=
github.com/jesseduffield/gocui v0.3.1-0.20210329130738-e026850021e3/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20210329131148-bcc4dcd991ff h1:fTt3EzLtpsc7OA7A6Vd6JVnlxvcAy7cY9lmN9yzDwSs=
github.com/jesseduffield/gocui v0.3.1-0.20210329131148-bcc4dcd991ff/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
github.com/jesseduffield/gocui v0.3.1-0.20210402033412-1238f910f001 h1:1WH+lTSK5YMr8emISHPA+VqYDDcLei6djuSxBCLIaiI=
github.com/jesseduffield/gocui v0.3.1-0.20210402033412-1238f910f001/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
github.com/jesseduffield/gocui v0.3.1-0.20210402040718-77a1b9631715 h1:nELTdFJiZk3vv7j8nWoHvl7H2IqTr26EHKl6LaorRA8=
github.com/jesseduffield/gocui v0.3.1-0.20210402040718-77a1b9631715/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
github.com/jesseduffield/gocui v0.3.1-0.20210402113210-6fd7ef27ce76 h1:miXVlortFNTlOX+KiKW3cVxOR6+Uhl4pnQRei2X26Y4=
github.com/jesseduffield/gocui v0.3.1-0.20210402113210-6fd7ef27ce76/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
github.com/jesseduffield/gocui v0.3.1-0.20210403045716-a3be78c4ccf6 h1:nENhj0TKu+11RrPm9Ls5YtzkpbNHM0faXr9UECDhODQ=
github.com/jesseduffield/gocui v0.3.1-0.20210403045716-a3be78c4ccf6/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
github.com/jesseduffield/gocui v0.3.1-0.20210405041826-439abd8b6e07 h1:BymGR28auSeuW0QELl0JomK0iFLPS/WRjFlc1iGZiOQ=
github.com/jesseduffield/gocui v0.3.1-0.20210405041826-439abd8b6e07/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
github.com/jesseduffield/gocui v0.3.1-0.20210405093708-e79dab8f7772 h1:dg9krj10Udac4IcvlVCOAPktQkfggkgtqRmbDKk7Pzw=
github.com/jesseduffield/gocui v0.3.1-0.20210405093708-e79dab8f7772/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
github.com/jesseduffield/gocui v0.3.1-0.20210406065811-95ef6e13779b h1:3+4+muhhikpls5FePXSRNFgcdoPx8dTdqaCy3AqLz98=
github.com/jesseduffield/gocui v0.3.1-0.20210406065811-95ef6e13779b/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
github.com/jesseduffield/gocui v0.3.1-0.20210406065942-1b0c68414064 h1:Oe+QJuUIOd2TU+A3BW5sT1eXqceoBcOOfyoHlGf7F8Y=
github.com/jesseduffield/gocui v0.3.1-0.20210406065942-1b0c68414064/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
github.com/jesseduffield/gocui v0.3.1-0.20210409121040-210802112d8a h1:ocrSuZxQIgWWt27b+rjiyIIPz6fzfFeoL5Q4cpa2cAo=
github.com/jesseduffield/gocui v0.3.1-0.20210409121040-210802112d8a/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
github.com/jesseduffield/gocui v0.3.1-0.20210410011117-a2bb4baca390 h1:Es72JiUjt01TtvqCugdvOR91baB3DhuWF1DNuxA0frA=
github.com/jesseduffield/gocui v0.3.1-0.20210410011117-a2bb4baca390/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
github.com/jesseduffield/gocui v0.3.1-0.20210412111008-6ef019af3724 h1:U70Do3/OSw5n/oLJGPWsQHnos2p0yq8yAeD2muioJhQ=
github.com/jesseduffield/gocui v0.3.1-0.20210412111008-6ef019af3724/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
github.com/jesseduffield/gocui v0.3.1-0.20210412113212-ee65bd542c08 h1:d003y2GByfR3PqN/JvxNuqyo8vx4m0epwY2hW7sNU80=
github.com/jesseduffield/gocui v0.3.1-0.20210412113212-ee65bd542c08/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
github.com/jesseduffield/gocui v0.3.1-0.20210412130453-de7bb5079f9f h1:JPpHlvSrKNxro+K9rM3nEHCdZ16qD0hnEedHPF07OtA=
github.com/jesseduffield/gocui v0.3.1-0.20210412130453-de7bb5079f9f/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
github.com/jesseduffield/gocui v0.3.1-0.20210417105214-bdf37de5c917 h1:H4THGOdAJf61wByuq8EHF/NAgtqrTxpSIPsrCXU9HAY=
github.com/jesseduffield/gocui v0.3.1-0.20210417105214-bdf37de5c917/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
github.com/jesseduffield/gocui v0.3.1-0.20210417110745-37f79434200d h1:2BPcc19W0j576hvhxtKma4jcD/+qAYvw1ln2HcIEZGU=
github.com/jesseduffield/gocui v0.3.1-0.20210417110745-37f79434200d/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe h1:qsVhCf2RFyyKIUe/+gJblbCpXMUki9rZrHuEctg6M/E=
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4=
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=
@@ -134,37 +99,30 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3
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/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
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/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
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 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
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.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
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.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow=
github.com/mattn/go-runewidth v0.0.12/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.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
@@ -176,11 +134,13 @@ 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/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/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
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=
@@ -196,21 +156,24 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
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/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
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/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-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
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=
@@ -222,47 +185,32 @@ golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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-20190626150813-e07cf5db2756/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 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
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-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210217105451-b926d437f341 h1:2/QtM1mL37YmcsT8HaDNHDgTqqFVw+zr8UzMiBVLzYU=
golang.org/x/sys v0.0.0-20210217105451-b926d437f341/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54 h1:rF3Ohx8DRyl8h2zw9qojyLHLhrJpEMgyPOImREEryf0=
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210402192133-700132347e07 h1:4k6HsQjxj6hVMsI2Vf0yKlzt5lXxZsMW1q0zaq2k8zY=
golang.org/x/sys v0.0.0-20210402192133-700132347e07/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210415045647-66c3f260301c h1:6L+uOeS3OQt/f4eFHXZcTxeZrGCuz+CLElgEBjbcTA4=
golang.org/x/sys v0.0.0-20210415045647-66c3f260301c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
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-20210317153231-de623e64d2a6 h1:EC6+IGYTjPpRfv9a2b/6Puw0W+hLtAhkV1tPsXhutqs=
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210406210042-72f3dc4e9b72 h1:VqE9gduFZ4dbR7XoL77lHFp0/DyDUBKSXK7CMFkVcV0=
golang.org/x/term v0.0.0-20210406210042-72f3dc4e9b72/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.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/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/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=
@@ -271,3 +219,5 @@ 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.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=

View File

@@ -61,6 +61,9 @@ func main() {
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 != "" {
@@ -72,6 +75,10 @@ func main() {
gitDir = filepath.Join(repoPath, ".git")
}
if customConfig != "" {
os.Setenv("LG_CONFIG_FILE", customConfig)
}
if useConfigDir != "" {
os.Setenv("CONFIG_DIR", useConfigDir)
}

View File

@@ -15,12 +15,12 @@ import (
"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/secureexec"
"github.com/jesseduffield/lazygit/pkg/updates"
"github.com/sirupsen/logrus"
)
@@ -104,7 +104,10 @@ func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
}
var err error
app.Log = newLogger(config)
app.Tr = i18n.NewTranslationSet(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")
@@ -124,7 +127,7 @@ func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
return app, err
}
app.GitCommand, err = commands.NewGitCommand(app.Log, app.OSCommand, app.Tr, app.Config)
app.GitCommand, err = commands.NewGitCommand(app.Log, app.OSCommand, app.Tr, app.Config, git_config.NewStdCachedGitConfig(app.Log))
if err != nil {
return app, err
}
@@ -318,6 +321,9 @@ func TailLogs() {
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) {
@@ -326,22 +332,5 @@ func TailLogs() {
log.Fatal(err)
}
cmd := secureexec.Command("tail", "-f", logFilePath)
stdout, _ := cmd.StdoutPipe()
if err := cmd.Start(); err != nil {
log.Fatal(err)
}
opts := humanlog.DefaultOptions
opts.Truncates = false
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)
TailLogsForPlatform(logFilePath, opts)
}

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

@@ -11,7 +11,7 @@ import (
// NewBranch create new branch
func (c *GitCommand) NewBranch(name string, base string) error {
return c.RunCommand("git checkout -b %s %s", name, base)
return c.RunCommand("git checkout -b %s %s", c.OSCommand.Quote(name), c.OSCommand.Quote(base))
}
// CurrentBranchName get the current branch name and displayname.
@@ -47,7 +47,7 @@ func (c *GitCommand) DeleteBranch(branch string, force bool) error {
command = "git branch -D"
}
return c.OSCommand.RunCommand("%s %s", command, branch)
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
@@ -61,7 +61,7 @@ func (c *GitCommand) Checkout(branch string, options CheckoutOptions) error {
if options.Force {
forceArg = " --force"
}
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git checkout%s %s", forceArg, branch), oscommands.RunCommandOptions{EnvVars: options.EnvVars})
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
@@ -73,24 +73,24 @@ func (c *GitCommand) GetBranchGraph(branchName string) (string, error) {
}
func (c *GitCommand) GetUpstreamForBranch(branchName string) (string, error) {
output, err := c.RunCommandWithOutput("git rev-parse --abbrev-ref --symbolic-full-name %s@{u}", branchName)
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": branchName,
"branchName": c.OSCommand.Quote(branchName),
}
return utils.ResolvePlaceholderString(branchLogCmdTemplate, templateValues)
}
func (c *GitCommand) SetUpstreamBranch(upstream string) error {
return c.RunCommand("git branch -u %s", upstream)
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", remoteName, remoteBranchName, branchName)
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) {
@@ -124,7 +124,7 @@ type MergeOpts struct {
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, branchName)
command := fmt.Sprintf("git merge --no-edit %s %s", mergeArgs, c.OSCommand.Quote(branchName))
if opts.FastForwardOnly {
command = fmt.Sprintf("%s --ff-only", command)
}
@@ -144,18 +144,18 @@ func (c *GitCommand) IsHeadDetached() bool {
// ResetHardHead runs `git reset --hard`
func (c *GitCommand) ResetHard(ref string) error {
return c.RunCommand("git reset --hard " + ref)
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 " + ref)
return c.RunCommand("git reset --soft " + c.OSCommand.Quote(ref))
}
func (c *GitCommand) ResetMixed(ref string) error {
return c.RunCommand("git reset --mixed " + ref)
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", oldName, newName)
return c.RunCommand("git branch --move %s %s", c.OSCommand.Quote(oldName), c.OSCommand.Quote(newName))
}

View File

@@ -47,6 +47,10 @@ func (c *GitCommand) GetCommitMessage(commitSha string) (string, error) {
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())
@@ -69,6 +73,10 @@ 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 := ""

View File

@@ -38,6 +38,8 @@ func TestGitCommandResetToCommit(t *testing.T) {
// TestGitCommandCommitStr is a function.
func TestGitCommandCommitStr(t *testing.T) {
gitCmd := NewDummyGitCommand()
type scenario struct {
testName string
message string
@@ -50,25 +52,24 @@ func TestGitCommandCommitStr(t *testing.T) {
testName: "Commit",
message: "test",
flags: "",
expected: "git commit -m \"test\"",
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 \"test\"",
expected: "git commit --no-verify -m " + gitCmd.OSCommand.Quote("test"),
},
{
testName: "Commit with multiline message",
message: "line1\nline2",
flags: "",
expected: "git commit -m \"line1\" -m \"line2\"",
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) {
gitCmd := NewDummyGitCommand()
cmdStr := gitCmd.CommitCmdStr(s.message, s.flags)
assert.Equal(t, s.expected, cmdStr)
})

View File

@@ -15,12 +15,8 @@ func (c *GitCommand) ConfiguredPager() string {
if os.Getenv("PAGER") != "" {
return os.Getenv("PAGER")
}
output, err := c.RunCommandWithOutput("git config --get-all core.pager")
if err != nil {
return ""
}
trimmedOutput := strings.TrimSpace(output)
return strings.Split(trimmedOutput, "\n")[0]
output := c.GitConfig.Get("core.pager")
return strings.Split(output, "\n")[0]
}
func (c *GitCommand) GetPager(width int) string {
@@ -42,11 +38,6 @@ func (c *GitCommand) colorArg() string {
return c.Config.GetUserConfig().Git.Paging.ColorArg
}
func (c *GitCommand) GetConfigValue(key string) string {
output, _ := c.getGitConfigValue(key)
return output
}
// 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 {
@@ -55,8 +46,5 @@ func (c *GitCommand) UsingGpg() bool {
return false
}
gpgsign := c.GetConfigValue("commit.gpgsign")
value := strings.ToLower(gpgsign)
return value == "true" || value == "1" || value == "yes" || value == "on"
return c.GitConfig.GetBool("commit.gpgsign")
}

View File

@@ -1,70 +0,0 @@
package commands
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestGitCommandUsingGpg is a function.
func TestGitCommandUsingGpg(t *testing.T) {
type scenario struct {
testName string
getGitConfigValue func(string) (string, error)
test func(bool)
}
scenarios := []scenario{
{
"Option global and local config commit.gpgsign is not set",
func(string) (string, error) { return "", nil },
func(gpgEnabled bool) {
assert.False(t, gpgEnabled)
},
},
{
"Option commit.gpgsign is true",
func(string) (string, error) {
return "True", nil
},
func(gpgEnabled bool) {
assert.True(t, gpgEnabled)
},
},
{
"Option commit.gpgsign is on",
func(string) (string, error) {
return "ON", nil
},
func(gpgEnabled bool) {
assert.True(t, gpgEnabled)
},
},
{
"Option commit.gpgsign is yes",
func(string) (string, error) {
return "YeS", nil
},
func(gpgEnabled bool) {
assert.True(t, gpgEnabled)
},
},
{
"Option commit.gpgsign is 1",
func(string) (string, error) {
return "1", nil
},
func(gpgEnabled bool) {
assert.True(t, gpgEnabled)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.getGitConfigValue = s.getGitConfigValue
s.test(gitCmd.UsingGpg())
})
}
}

View File

@@ -1,6 +1,7 @@
package commands
import (
"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"
@@ -14,11 +15,12 @@ func NewDummyGitCommand() *GitCommand {
// NewDummyGitCommandWithOSCommand creates a new dummy GitCommand for testing
func NewDummyGitCommandWithOSCommand(osCommand *oscommands.OSCommand) *GitCommand {
newAppConfig := config.NewDummyAppConfig()
return &GitCommand{
Log: utils.NewDummyLog(),
OSCommand: osCommand,
Tr: i18n.NewTranslationSet(utils.NewDummyLog()),
Config: config.NewDummyAppConfig(),
getGitConfigValue: func(string) (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

@@ -2,8 +2,10 @@ package commands
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"time"
"github.com/go-errors/errors"
@@ -14,7 +16,11 @@ import (
// CatFile obtains the content of a file
func (c *GitCommand) CatFile(fileName string) (string, error) {
return c.OSCommand.CatFile(fileName)
buf, err := ioutil.ReadFile(fileName)
if err != nil {
return "", nil
}
return string(buf), nil
}
func (c *GitCommand) OpenMergeToolCmd() string {
@@ -117,14 +123,14 @@ func (c *GitCommand) DiscardAllFileChanges(file *models.File) error {
if err := c.RunCommand("git checkout --ours -- %s", quotedFileName); err != nil {
return err
}
if err := c.RunCommand("git add %s", quotedFileName); err != nil {
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)
return c.RunCommand("git rm -- %s", quotedFileName)
}
// if the file isn't tracked, we assume you want to delete it
@@ -189,17 +195,18 @@ func (c *GitCommand) Ignore(filename string) error {
}
// WorktreeFileDiff returns the diff of a file
func (c *GitCommand) WorktreeFileDiff(file *models.File, plain bool, cached bool) string {
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))
s, _ := c.OSCommand.RunCommandWithOutput(c.WorktreeFileDiffCmdStr(file, plain, cached, ignoreWhitespace))
return s
}
func (c *GitCommand) WorktreeFileDiffCmdStr(node models.IFile, plain bool, cached bool) string {
func (c *GitCommand) WorktreeFileDiffCmdStr(node models.IFile, plain bool, cached bool, ignoreWhitespace bool) string {
cachedArg := ""
trackedArg := "--"
colorArg := c.colorArg()
path := c.OSCommand.Quote(node.GetPath())
quotedPath := c.OSCommand.Quote(node.GetPath())
ignoreWhitespaceArg := ""
if cached {
cachedArg = "--cached"
}
@@ -209,12 +216,15 @@ func (c *GitCommand) WorktreeFileDiffCmdStr(node models.IFile, plain bool, cache
if plain {
colorArg = "never"
}
if ignoreWhitespace {
ignoreWhitespaceArg = "--ignore-all-space"
}
return fmt.Sprintf("git diff --submodule --no-ext-diff --color=%s %s %s %s", colorArg, cachedArg, trackedArg, path)
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.GetUserConfigDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch")
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
@@ -246,12 +256,12 @@ func (c *GitCommand) ShowFileDiffCmdStr(from string, to string, reverse bool, fi
reverseFlag = " -R "
}
return fmt.Sprintf("git diff --submodule --no-ext-diff --no-renames --color=%s %s %s %s -- %s", colorArg, from, to, reverseFlag, fileName)
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, fileName)
return c.RunCommand("git checkout %s -- %s", commitSha, c.OSCommand.Quote(fileName))
}
// DiscardOldFileChanges discards changes to a file from an old commit
@@ -261,7 +271,7 @@ func (c *GitCommand) DiscardOldFileChanges(commits []*models.Commit, commitIndex
}
// 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", fileName); err != nil {
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
}
@@ -289,7 +299,7 @@ func (c *GitCommand) DiscardAnyUnstagedFileChanges() error {
// 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", name)
return c.RunCommand("git rm -r --cached -- %s", c.OSCommand.Quote(name))
}
// RemoveUntrackedFiles runs `git clean -fd`
@@ -317,8 +327,12 @@ func (c *GitCommand) ResetAndClean() error {
return c.RemoveUntrackedFiles()
}
func (c *GitCommand) EditFileCmdStr(filename string) (string, error) {
editor := c.GetConfigValue("core.editor")
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")
@@ -335,8 +349,15 @@ func (c *GitCommand) EditFileCmdStr(filename string) (string, error) {
}
}
if editor == "" {
return "", errors.New("No editor defined in $GIT_EDITOR, $VISUAL, $EDITOR, or git config")
return "", errors.New("No editor defined in config file, $GIT_EDITOR, $VISUAL, $EDITOR, or git config")
}
return fmt.Sprintf("%s %s", editor, c.OSCommand.Quote(filename)), nil
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
}

View File

@@ -4,37 +4,15 @@ import (
"fmt"
"io/ioutil"
"os/exec"
"runtime"
"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"
)
// TestGitCommandCatFile tests emitting a file using commands, where commands vary by OS.
func TestGitCommandCatFile(t *testing.T) {
var osCmd string
switch os := runtime.GOOS; os {
case "windows":
osCmd = "type"
default:
osCmd = "cat"
}
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, osCmd, cmd)
assert.EqualValues(t, []string{"test.txt"}, args)
return secureexec.Command("echo", "-n", "test")
}
o, err := gitCmd.CatFile("test.txt")
assert.NoError(t, err)
assert.Equal(t, "test", o)
}
// TestGitCommandStageFile is a function.
func TestGitCommandStageFile(t *testing.T) {
gitCmd := NewDummyGitCommand()
@@ -333,11 +311,12 @@ func TestGitCommandDiscardAllFileChanges(t *testing.T) {
// 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
testName string
command func(string, ...string) *exec.Cmd
file *models.File
plain bool
cached bool
ignoreWhitespace bool
}
scenarios := []scenario{
@@ -356,6 +335,7 @@ func TestGitCommandDiff(t *testing.T) {
},
false,
false,
false,
},
{
"cached",
@@ -372,6 +352,7 @@ func TestGitCommandDiff(t *testing.T) {
},
false,
true,
false,
},
{
"plain",
@@ -388,6 +369,7 @@ func TestGitCommandDiff(t *testing.T) {
},
true,
false,
false,
},
{
"File not tracked and file has no staged changes",
@@ -404,6 +386,24 @@ func TestGitCommandDiff(t *testing.T) {
},
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,
},
}
@@ -411,7 +411,7 @@ func TestGitCommandDiff(t *testing.T) {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
gitCmd.WorktreeFileDiff(s.file, s.plain, s.cached)
gitCmd.WorktreeFileDiff(s.file, s.plain, s.cached, s.ignoreWhitespace)
})
}
}
@@ -433,7 +433,7 @@ func TestGitCommandCheckoutFile(t *testing.T) {
"test999.txt",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: "git checkout 11af912 test999.txt",
Expect: "git checkout 11af912 -- test999.txt",
Replace: "echo",
},
}),
@@ -447,7 +447,7 @@ func TestGitCommandCheckoutFile(t *testing.T) {
"test999.txt",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: "git checkout 11af912 test999.txt",
Expect: "git checkout 11af912 -- test999.txt",
Replace: "test",
},
}),
@@ -524,24 +524,21 @@ func TestGitCommandApplyPatch(t *testing.T) {
}
}
// TestGitCommandDiscardOldFileChanges is a function.
func TestGitCommandDiscardOldFileChanges(t *testing.T) {
type scenario struct {
testName string
getGitConfigValue func(string) (string, error)
commits []*models.Commit
commitIndex int
fileName string
command func(string, ...string) *exec.Cmd
test func(error)
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",
func(string) (string, error) {
return "", nil
},
nil,
[]*models.Commit{},
0,
"test999.txt",
@@ -552,9 +549,7 @@ func TestGitCommandDiscardOldFileChanges(t *testing.T) {
},
{
"returns error when using gpg",
func(string) (string, error) {
return "true", nil
},
map[string]string{"commit.gpgsign": "true"},
[]*models.Commit{{Name: "commit", Sha: "123456"}},
0,
"test999.txt",
@@ -565,9 +560,7 @@ func TestGitCommandDiscardOldFileChanges(t *testing.T) {
},
{
"checks out file if it already existed",
func(string) (string, error) {
return "", nil
},
nil,
[]*models.Commit{
{Name: "commit", Sha: "123456"},
{Name: "commit2", Sha: "abcdef"},
@@ -584,7 +577,7 @@ func TestGitCommandDiscardOldFileChanges(t *testing.T) {
Replace: "echo",
},
{
Expect: "git checkout HEAD^ test999.txt",
Expect: "git checkout HEAD^ -- test999.txt",
Replace: "echo",
},
{
@@ -609,7 +602,7 @@ func TestGitCommandDiscardOldFileChanges(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd.OSCommand.Command = s.command
gitCmd.getGitConfigValue = s.getGitConfigValue
gitCmd.GitConfig = git_config.NewFakeGitConfig(s.gitConfigMockResponses)
s.test(gitCmd.DiscardOldFileChanges(s.commits, s.commitIndex, s.fileName))
})
}
@@ -718,32 +711,55 @@ func TestGitCommandRemoveUntrackedFiles(t *testing.T) {
// TestEditFileCmdStr is a function.
func TestEditFileCmdStr(t *testing.T) {
gitCmd := NewDummyGitCommand()
type scenario struct {
filename string
command func(string, ...string) *exec.Cmd
getenv func(string) string
getGitConfigValue func(string) (string, error)
test func(string, error)
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 ""
},
func(cf string) (string, error) {
return "", nil
},
nil,
func(cmdStr string, err error) {
assert.EqualError(t, err, "No editor defined in $GIT_EDITOR, $VISUAL, $EDITOR, or git config")
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")
@@ -751,16 +767,16 @@ func TestEditFileCmdStr(t *testing.T) {
func(env string) string {
return ""
},
func(cf string) (string, error) {
return "nano", nil
},
map[string]string{"core.editor": "nano"},
func(cmdStr string, err error) {
assert.NoError(t, err)
assert.Equal(t, "nano \"test\"", cmdStr)
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")
@@ -772,15 +788,15 @@ func TestEditFileCmdStr(t *testing.T) {
return ""
},
func(cf string) (string, error) {
return "", nil
},
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")
@@ -792,16 +808,16 @@ func TestEditFileCmdStr(t *testing.T) {
return ""
},
func(cf string) (string, error) {
return "", nil
},
nil,
func(cmdStr string, err error) {
assert.NoError(t, err)
assert.Equal(t, "emacs \"test\"", cmdStr)
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")
@@ -809,16 +825,16 @@ func TestEditFileCmdStr(t *testing.T) {
func(env string) string {
return ""
},
func(cf string) (string, error) {
return "", nil
},
nil,
func(cmdStr string, err error) {
assert.NoError(t, err)
assert.Equal(t, "vi \"test\"", cmdStr)
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")
@@ -826,21 +842,37 @@ func TestEditFileCmdStr(t *testing.T) {
func(env string) string {
return ""
},
func(cf string) (string, error) {
return "", nil
},
nil,
func(cmdStr string, err error) {
assert.NoError(t, err)
assert.Equal(t, "vi \"file/with space\"", cmdStr)
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 := NewDummyGitCommand()
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.getGitConfigValue = s.getGitConfigValue
s.test(gitCmd.EditFileCmdStr(s.filename))
gitCmd.GitConfig = git_config.NewFakeGitConfig(s.gitConfigMockResponses)
s.test(gitCmd.EditFileCmdStr(s.filename, 1))
}
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/go-errors/errors"
gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/config"
@@ -32,32 +33,32 @@ type GitCommand struct {
Repo *gogit.Repository
Tr *i18n.TranslationSet
Config config.AppConfigurer
getGitConfigValue func(string) (string, error)
DotGitDir string
onSuccessfulContinue func() error
PatchManager *patch.PatchManager
GitConfig git_config.IGitConfig
// Push to current determines whether the user has configured to push to the remote branch of the same name as the current or not
PushToCurrent bool
}
// NewGitCommand it runs git commands
func NewGitCommand(log *logrus.Entry, osCommand *oscommands.OSCommand, tr *i18n.TranslationSet, config config.AppConfigurer) (*GitCommand, error) {
func NewGitCommand(
log *logrus.Entry,
osCommand *oscommands.OSCommand,
tr *i18n.TranslationSet,
config config.AppConfigurer,
gitConfig git_config.IGitConfig,
) (*GitCommand, error) {
var repo *gogit.Repository
// see what our default push behaviour is
output, err := osCommand.RunCommandWithOutput("git config --get push.default")
pushToCurrent := false
if err != nil {
log.Errorf("error reading git config: %v", err)
} else {
pushToCurrent = strings.TrimSpace(output) == "current"
}
pushToCurrent := gitConfig.Get("push.default") == "current"
if err := navigateToRepoRootDirectory(os.Stat, os.Chdir); err != nil {
return nil, err
}
var err error
if repo, err = setupRepository(gogit.PlainOpen, tr.GitconfigParseErr); err != nil {
return nil, err
}
@@ -68,14 +69,14 @@ func NewGitCommand(log *logrus.Entry, osCommand *oscommands.OSCommand, tr *i18n.
}
gitCommand := &GitCommand{
Log: log,
OSCommand: osCommand,
Tr: tr,
Repo: repo,
Config: config,
getGitConfigValue: getGitConfigValue,
DotGitDir: dotGitDir,
PushToCurrent: pushToCurrent,
Log: log,
OSCommand: osCommand,
Tr: tr,
Repo: repo,
Config: config,
DotGitDir: dotGitDir,
PushToCurrent: pushToCurrent,
GitConfig: gitConfig,
}
gitCommand.PatchManager = patch.NewPatchManager(log, gitCommand.ApplyPatch, gitCommand.ShowFileDiff)
@@ -246,3 +247,7 @@ func (c *GitCommand) RunCommandWithOutput(formatString string, formatArgs ...int
return output, err
}
}
func (c *GitCommand) NewCmdObjFromStr(cmdStr string) oscommands.ICmdObj {
return c.OSCommand.NewCmdObjFromStr(cmdStr).AddEnvVars("GIT_OPTIONAL_LOCKS=0")
}

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

@@ -1,4 +1,4 @@
package commands
package git_config
import (
"bytes"

View File

@@ -8,6 +8,7 @@ import (
"github.com/go-errors/errors"
gogit "github.com/jesseduffield/go-git/v5"
"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"
@@ -209,7 +210,8 @@ func TestNewGitCommand(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
s.setup()
s.test(NewGitCommand(utils.NewDummyLog(), oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog()), config.NewDummyAppConfig()))
newAppConfig := config.NewDummyAppConfig()
s.test(NewGitCommand(utils.NewDummyLog(), oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog(), newAppConfig.GetUserConfig().Gui.Language), newAppConfig, git_config.NewFakeGitConfig(nil)))
})
}
}

View File

@@ -52,6 +52,12 @@ func (b *BranchListBuilder) obtainBranches() []*models.Branch {
}
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{

View File

@@ -10,9 +10,9 @@ import (
"strconv"
"strings"
"github.com/fatih/color"
"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"
)
@@ -72,10 +72,6 @@ func (c *CommitListBuilder) extractCommitFromLine(line string) *models.Commit {
unitTimestampInt, _ := strconv.Atoi(unixTimestamp)
// Any commit with multiple parents is a merge commit.
// If there's a space then it means there must be more than one parent hash
isMerge := strings.Contains(parentHashes, " ")
return &models.Commit{
Sha: sha,
Name: message,
@@ -83,7 +79,7 @@ func (c *CommitListBuilder) extractCommitFromLine(line string) *models.Commit {
ExtraInfo: extraInfo,
UnixTimestamp: int64(unitTimestampInt),
Author: author,
IsMerge: isMerge,
Parents: strings.Split(parentHashes, " "),
}
}
@@ -169,8 +165,7 @@ func (c *CommitListBuilder) GetCommits(opts GetCommitsOptions) ([]*models.Commit
if rebaseMode != "" {
currentCommit := commits[len(rebasingCommits)]
blue := color.New(color.FgYellow)
youAreHere := blue.Sprintf("<-- %s ---", c.Tr.YouAreHere)
youAreHere := style.FgYellow.Sprintf("<-- %s ---", c.Tr.YouAreHere)
currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name)
}
@@ -327,7 +322,7 @@ func (c *CommitListBuilder) getMergeBase(refName string) (string, error) {
}
// 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", refName, baseBranch)
output, _ := c.OSCommand.RunCommandWithOutput("git merge-base %s %s", c.OSCommand.Quote(refName), c.OSCommand.Quote(baseBranch))
return ignoringWarnings(output), nil
}
@@ -344,7 +339,7 @@ func ignoringWarnings(commandOutput string) string {
// 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}", refName, refName)
output, err := c.OSCommand.RunCommandWithOutput("git merge-base %s %s@{u}", c.OSCommand.Quote(refName), c.OSCommand.Quote(refName))
if err != nil {
return "", err
}
@@ -366,8 +361,8 @@ func (c *CommitListBuilder) getLogCmd(opts GetCommitsOptions) *exec.Cmd {
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 --date=unix %s",
opts.RefName,
"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,

View File

@@ -19,7 +19,7 @@ func NewDummyCommitListBuilder() *CommitListBuilder {
Log: utils.NewDummyLog(),
GitCommand: NewDummyGitCommandWithOSCommand(osCommand),
OSCommand: osCommand,
Tr: i18n.NewTranslationSet(utils.NewDummyLog()),
Tr: i18n.NewTranslationSet(utils.NewDummyLog(), "auto"),
}
}

View File

@@ -8,8 +8,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
const RENAME_SEPARATOR = " -> "
// GetStatusFiles git status files
type GetStatusFileOptions struct {
NoRenames bool
@@ -17,44 +15,36 @@ type GetStatusFileOptions struct {
func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
// check if config wants us ignoring untracked files
untrackedFilesSetting := c.GetConfigValue("status.showUntrackedFiles")
untrackedFilesSetting := c.GitConfig.Get("status.showUntrackedFiles")
if untrackedFilesSetting == "" {
untrackedFilesSetting = "all"
}
untrackedFilesArg := fmt.Sprintf("--untracked-files=%s", untrackedFilesSetting)
statusOutput, err := c.GitStatus(GitStatusOptions{NoRenames: opts.NoRenames, UntrackedFilesArg: untrackedFilesArg})
statuses, err := c.GitStatus(GitStatusOptions{NoRenames: opts.NoRenames, UntrackedFilesArg: untrackedFilesArg})
if err != nil {
c.Log.Error(err)
}
statusStrings := utils.SplitLines(statusOutput)
files := []*models.File{}
for _, statusString := range statusStrings {
if strings.HasPrefix(statusString, "warning") {
c.Log.Warningf("warning when calling git status: %s", statusString)
for _, status := range statuses {
if strings.HasPrefix(status.StatusString, "warning") {
c.Log.Warningf("warning when calling git status: %s", status.StatusString)
continue
}
change := statusString[0:2]
change := status.Change
stagedChange := change[0:1]
unstagedChange := statusString[1:2]
name := statusString[3:]
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)
previousName := ""
if strings.Contains(name, RENAME_SEPARATOR) {
split := strings.Split(name, RENAME_SEPARATOR)
name = split[1]
previousName = split[0]
}
file := &models.File{
Name: name,
PreviousName: previousName,
DisplayString: statusString,
Name: status.Name,
PreviousName: status.PreviousName,
DisplayString: status.StatusString,
HasStagedChanges: !hasNoStagedChanges,
HasUnstagedChanges: unstagedChange != " ",
Tracked: !untracked,
@@ -62,7 +52,7 @@ func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
Added: unstagedChange == "A" || untracked,
HasMergeConflicts: hasMergeConflicts,
HasInlineMergeConflicts: hasInlineMergeConflicts,
Type: c.OSCommand.FileType(name),
Type: c.OSCommand.FileType(status.Name),
ShortStatus: change,
}
files = append(files, file)
@@ -71,13 +61,20 @@ func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
return files
}
// GitStatus returns the plaintext short status of the repo
// GitStatus returns the file status of the repo
type GitStatusOptions struct {
NoRenames bool
UntrackedFilesArg string
}
func (c *GitCommand) GitStatus(opts GitStatusOptions) (string, error) {
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"
@@ -85,20 +82,35 @@ func (c *GitCommand) GitStatus(opts GitStatusOptions) (string, error) {
statusLines, err := c.RunCommandWithOutput("git status %s --porcelain -z %s", opts.UntrackedFilesArg, noRenamesFlag)
if err != nil {
return "", err
return []FileStatus{}, err
}
splitLines := strings.Split(statusLines, "\x00")
// if a line starts with 'R' then the next line is the original file.
for i := 0; i < len(splitLines)-1; i++ {
response := []FileStatus{}
for i := 0; i < len(splitLines); i++ {
original := splitLines[i]
if strings.HasPrefix(original, "R ") {
next := splitLines[i+1]
updated := "R " + next + RENAME_SEPARATOR + strings.TrimPrefix(original, "R ")
splitLines[i] = updated
splitLines = append(splitLines[0:i+1], splitLines[i+2:]...)
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 strings.Join(splitLines, "\n"), nil
return response, nil
}

View File

@@ -31,8 +31,8 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
"Several files found",
func(cmd string, args ...string) *exec.Cmd {
return secureexec.Command(
"echo",
"MM file1.txt\nA file3.txt\nAM file2.txt\n?? file4.txt\nUU file5.txt",
"printf",
`MM file1.txt\0A file3.txt\0AM file2.txt\0?? file4.txt\0UU file5.txt`,
)
},
func(files []*models.File) {
@@ -106,6 +106,111 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
},
}
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)
},
},

View File

@@ -2,8 +2,8 @@ package commands
import (
"fmt"
"regexp"
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
@@ -13,26 +13,25 @@ import (
// 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)
re := regexp.MustCompile(`(\w+).*HEAD@\{([^\}]+)\}: (.*)`)
filterPathArg := ""
if filterPath != "" {
filterPathArg = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(filterPath))
}
cmd := c.OSCommand.ExecutableFromString(fmt.Sprintf("git reflog --abbrev=20 --date=unix %s", filterPathArg))
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) {
match := re.FindStringSubmatch(line)
if len(match) <= 1 {
fields := strings.SplitN(line, " ", 3)
if len(fields) <= 2 {
return false, nil
}
unixTimestamp, _ := strconv.Atoi(match[2])
unixTimestamp, _ := strconv.Atoi(fields[1])
commit := &models.Commit{
Sha: match[1],
Name: match[3],
Sha: fields[0],
Name: fields[2],
UnixTimestamp: int64(unixTimestamp),
Status: "reflog",
}

View File

@@ -1,28 +1,16 @@
package commands
import (
"regexp"
"sort"
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/utils"
)
const semverRegex = `v?((\d+\.?)+)([^\d]?.*)`
func convertToInt(s string) int {
i, err := strconv.Atoi(s)
if err != nil {
return 0
}
return i
}
func (c *GitCommand) GetTags() ([]*models.Tag, error) {
// get remote branches
remoteBranchesStr, err := c.OSCommand.RunCommandWithOutput(`git tag --list`)
// 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
}
@@ -37,52 +25,10 @@ func (c *GitCommand) GetTags() ([]*models.Tag, error) {
// 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,
}
}
// now lets sort our tags by name numerically
re := regexp.MustCompile(semverRegex)
// the reason this is complicated is because we're both sorting alphabetically
// and when we're dealing with semver strings
sort.Slice(tags, func(i, j int) bool {
a := tags[i].Name
b := tags[j].Name
matchA := re.FindStringSubmatch(a)
matchB := re.FindStringSubmatch(b)
if len(matchA) > 0 && len(matchB) > 0 {
numbersA := strings.Split(matchA[1], ".")
numbersB := strings.Split(matchB[1], ".")
k := 0
for {
if len(numbersA) == k && len(numbersB) == k {
break
}
if len(numbersA) == k {
return true
}
if len(numbersB) == k {
return false
}
if convertToInt(numbersA[k]) < convertToInt(numbersB[k]) {
return true
}
if convertToInt(numbersA[k]) > convertToInt(numbersB[k]) {
return false
}
k++
}
return strings.ToLower(matchA[3]) < strings.ToLower(matchB[3])
}
return strings.ToLower(a) < strings.ToLower(b)
})
return tags, nil
}

View File

@@ -24,3 +24,26 @@ func (b *Branch) ID() string {
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

@@ -13,8 +13,8 @@ type Commit struct {
Author string
UnixTimestamp int64
// IsMerge tells us whether we're dealing with a merge commit i.e. a commit with two parents
IsMerge bool
// SHAs of parent commits (will be multiple if it's a merge commit)
Parents []string
}
func (c *Commit) ShortSha() string {
@@ -35,3 +35,7 @@ func (c *Commit) ID() string {
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

@@ -29,8 +29,6 @@ type IFile interface {
GetPath() string
}
const RENAME_SEPARATOR = " -> "
func (f *File) IsRename() bool {
return f.PreviousName != ""
}
@@ -63,7 +61,7 @@ func (f *File) IsSubmodule(configs []*SubmoduleConfig) bool {
func (f *File) SubmoduleConfig(configs []*SubmoduleConfig) *SubmoduleConfig {
for _, config := range configs {
if f.Name == config.Name {
if f.Name == config.Path {
return config
}
}

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

@@ -1,3 +1,4 @@
//go:build !windows
// +build !windows
package oscommands
@@ -18,11 +19,10 @@ import (
// 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 {
c.Log.WithField("command", command).Info("RunCommand")
c.LogCommand(command, true)
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

View File

@@ -1,9 +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, command string, output func(string) string) error {
return c.RunCommand(command)
func RunCommandWithOutputLiveWrapper(c *OSCommand, cmdObj ICmdObj, output func(string) string) error {
return c.RunCommand(cmdObj.ToString())
}

View File

@@ -24,10 +24,8 @@ import (
// Platform stores the os state
type Platform struct {
OS string
CatCmd []string
Shell string
ShellArg string
EscapedQuote string
OpenCommand string
OpenLinkCommand string
}
@@ -203,7 +201,14 @@ 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 = commandStr
quotedCommand = strings.NewReplacer(
"^", "^^",
"&", "^&",
"|", "^|",
"<", "^<",
">", "^>",
"%", "^%",
).Replace(commandStr)
} else {
quotedCommand = c.Quote(commandStr)
}
@@ -213,28 +218,16 @@ func (c *OSCommand) ShellCommandFromString(commandStr string) *exec.Cmd {
}
// RunCommandWithOutputLive runs RunCommandWithOutputLiveWrapper
func (c *OSCommand) RunCommandWithOutputLive(command string, output func(string) string) error {
return RunCommandWithOutputLiveWrapper(c, command, output)
}
func (c *OSCommand) CatFile(filename string) (string, error) {
arr := append(c.Platform.CatCmd, filename)
cmdStr := strings.Join(arr, " ")
c.Log.WithField("command", cmdStr).Info("Cat")
cmd := c.Command(arr[0], arr[1:]...)
output, err := sanitisedCommandOutput(cmd.CombinedOutput())
if err != nil {
c.Log.WithField("command", cmdStr).Error(output)
}
return output, err
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(command string, promptUserForCredential func(string) string) error {
func (c *OSCommand) DetectUnamePass(cmdObj ICmdObj, promptUserForCredential func(string) string) error {
ttyText := ""
errMessage := c.RunCommandWithOutputLive(command, func(word string) string {
errMessage := c.RunCommandWithOutputLive(cmdObj, func(word string) string {
ttyText = ttyText + " " + word
prompts := map[string]string{
@@ -265,7 +258,7 @@ func (c *OSCommand) RunCommand(formatString string, formatArgs ...interface{}) e
// 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.Command(c.Platform.Shell, c.Platform.ShellArg, command)
cmd := c.ShellCommandFromString(command)
c.LogExecCmd(cmd)
_, err := sanitisedCommandOutput(cmd.CombinedOutput())
@@ -304,9 +297,8 @@ func (c *OSCommand) OpenFile(filename string) error {
templateValues := map[string]string{
"filename": c.Quote(filename),
}
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
err := c.RunCommand(command)
err := c.RunShellCommand(command)
return err
}
@@ -319,7 +311,7 @@ func (c *OSCommand) OpenLink(link string) error {
}
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
err := c.RunCommand(command)
err := c.RunShellCommand(command)
return err
}
@@ -341,17 +333,23 @@ func (c *OSCommand) PrepareShellSubProcess(command string) *exec.Cmd {
// Quote wraps a message in platform-specific quotation marks
func (c *OSCommand) Quote(message string) string {
var quote string
if c.Platform.OS == "windows" {
message = strings.Replace(message, `"`, `"'"'"`, -1)
message = strings.Replace(message, `\"`, `\\"`, -1)
quote = `\"`
message = strings.NewReplacer(
`"`, `"'"'"`,
`\"`, `\\"`,
).Replace(message)
} else {
message = strings.Replace(message, `\`, `\\`, -1)
message = strings.Replace(message, `"`, `\"`, -1)
message = strings.Replace(message, "`", "\\`", -1)
message = strings.Replace(message, "$", "\\$", -1)
quote = `"`
message = strings.NewReplacer(
`\`, `\\`,
`"`, `\"`,
`$`, `\$`,
"`", "\\`",
).Replace(message)
}
escapedQuote := c.Platform.EscapedQuote
return escapedQuote + message + escapedQuote
return quote + message + quote
}
// AppendLineToFile adds a new line in file
@@ -554,7 +552,9 @@ func RunLineOutputCmd(cmd *exec.Cmd, onLine func(line string) (bool, error)) err
}
func (c *OSCommand) CopyToClipboard(str string) error {
c.LogCommand(fmt.Sprintf("Copying '%s' to clipboard", utils.TruncateWithEllipsis(str, 40)), false)
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)
}
@@ -563,3 +563,30 @@ func (c *OSCommand) RemoveFile(path string) error {
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

@@ -1,3 +1,4 @@
//go:build !windows
// +build !windows
package oscommands
@@ -9,10 +10,8 @@ import (
func getPlatform() *Platform {
return &Platform{
OS: runtime.GOOS,
CatCmd: []string{"cat"},
Shell: "bash",
ShellArg: "-c",
EscapedQuote: `"`,
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

@@ -3,10 +3,8 @@ package oscommands
import (
"io/ioutil"
"os"
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/stretchr/testify/assert"
)
@@ -59,57 +57,6 @@ func TestOSCommandRunCommand(t *testing.T) {
}
}
// 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 secureexec.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 secureexec.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 secureexec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
OSCmd := NewDummyOSCommand()
OSCmd.Command = s.command
OSCmd.Config.GetUserConfig().OS.OpenCommand = "open {{filename}}"
s.test(OSCmd.OpenFile(s.filename))
}
}
// TestOSCommandQuote is a function.
func TestOSCommandQuote(t *testing.T) {
osCommand := NewDummyOSCommand()
@@ -118,7 +65,7 @@ func TestOSCommandQuote(t *testing.T) {
actual := osCommand.Quote("hello `test`")
expected := osCommand.Platform.EscapedQuote + "hello \\`test\\`" + osCommand.Platform.EscapedQuote
expected := "\"hello \\`test\\`\""
assert.EqualValues(t, expected, actual)
}
@@ -131,7 +78,7 @@ func TestOSCommandQuoteSingleQuote(t *testing.T) {
actual := osCommand.Quote("hello 'test'")
expected := osCommand.Platform.EscapedQuote + "hello 'test'" + osCommand.Platform.EscapedQuote
expected := `"hello 'test'"`
assert.EqualValues(t, expected, actual)
}
@@ -144,7 +91,7 @@ func TestOSCommandQuoteDoubleQuote(t *testing.T) {
actual := osCommand.Quote(`hello "test"`)
expected := osCommand.Platform.EscapedQuote + `hello \"test\"` + osCommand.Platform.EscapedQuote
expected := `"hello \"test\""`
assert.EqualValues(t, expected, actual)
}
@@ -155,9 +102,9 @@ func TestOSCommandQuoteWindows(t *testing.T) {
osCommand.Platform.OS = "windows"
actual := osCommand.Quote(`hello "test"`)
actual := osCommand.Quote(`hello "test" 'test2'`)
expected := osCommand.Platform.EscapedQuote + `hello "'"'"test"'"'"` + osCommand.Platform.EscapedQuote
expected := `\"hello "'"'"test"'"'" 'test2'\"`
assert.EqualValues(t, expected, actual)
}

View File

@@ -2,10 +2,8 @@ package oscommands
func getPlatform() *Platform {
return &Platform{
OS: "windows",
CatCmd: []string{"cmd", "/c", "type"},
Shell: "cmd",
ShellArg: "/c",
EscapedQuote: `\"`,
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))
}
}

View File

@@ -138,7 +138,14 @@ func ModifiedPatchForLines(log *logrus.Entry, filename string, diffText string,
// I want to know, given a hunk, what line a given index is on
func (hunk *PatchHunk) LineNumberOfLine(idx int) int {
lines := hunk.bodyLines[0 : idx-hunk.FirstLineIdx-1]
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{"+", " "})

View File

@@ -4,7 +4,7 @@ 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"
@@ -95,45 +95,39 @@ 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, theme.SelectedRangeBgColor)
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) {
@@ -200,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 {

View File

@@ -5,14 +5,62 @@ import (
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/config"
)
// Service is a service that repository is on (Github, Bitbucket, ...)
type Service struct {
Name string
PullRequestURL string
Name string
pullRequestURLIntoDefaultBranch func(owner string, repository string, from string) string
pullRequestURLIntoTargetBranch func(owner string, repository string, from string, to string) string
}
// NewService builds a Service based on the host type
func NewService(typeName string, repositoryDomain string, siteDomain string) *Service {
var service *Service
switch typeName {
case "github":
service = &Service{
Name: repositoryDomain,
pullRequestURLIntoDefaultBranch: func(owner string, repository string, from string) string {
return fmt.Sprintf("https://%s/%s/%s/compare/%s?expand=1", siteDomain, owner, repository, from)
},
pullRequestURLIntoTargetBranch: func(owner string, repository string, from string, to string) string {
return fmt.Sprintf("https://%s/%s/%s/compare/%s...%s?expand=1", siteDomain, owner, repository, to, from)
},
}
case "bitbucket":
service = &Service{
Name: repositoryDomain,
pullRequestURLIntoDefaultBranch: func(owner string, repository string, from string) string {
return fmt.Sprintf("https://%s/%s/%s/pull-requests/new?source=%s&t=1", siteDomain, owner, repository, from)
},
pullRequestURLIntoTargetBranch: func(owner string, repository string, from string, to string) string {
return fmt.Sprintf("https://%s/%s/%s/pull-requests/new?source=%s&dest=%s&t=1", siteDomain, owner, repository, from, to)
},
}
case "gitlab":
service = &Service{
Name: repositoryDomain,
pullRequestURLIntoDefaultBranch: func(owner string, repository string, from string) string {
return fmt.Sprintf("https://%s/%s/%s/merge_requests/new?merge_request[source_branch]=%s", siteDomain, owner, repository, from)
},
pullRequestURLIntoTargetBranch: func(owner string, repository string, from string, to string) string {
return fmt.Sprintf("https://%s/%s/%s/merge_requests/new?merge_request[source_branch]=%s&merge_request[target_branch]=%s", siteDomain, owner, repository, from, to)
},
}
}
return service
}
func (s *Service) PullRequestURL(owner string, repository string, from string, to string) string {
if to == "" {
return s.pullRequestURLIntoDefaultBranch(owner, repository, from)
} else {
return s.pullRequestURLIntoTargetBranch(owner, repository, from, to)
}
}
// PullRequest opens a link in browser to create new pull request
@@ -28,31 +76,6 @@ type RepoInformation struct {
Repository string
}
// NewService builds a Service based on the host type
func NewService(typeName string, repositoryDomain string, siteDomain string) *Service {
var service *Service
switch typeName {
case "github":
service = &Service{
Name: repositoryDomain,
PullRequestURL: fmt.Sprintf("https://%s%s", siteDomain, "/%s/%s/compare/%s?expand=1"),
}
case "bitbucket":
service = &Service{
Name: repositoryDomain,
PullRequestURL: fmt.Sprintf("https://%s%s", siteDomain, "/%s/%s/pull-requests/new?source=%s&t=1"),
}
case "gitlab":
service = &Service{
Name: repositoryDomain,
PullRequestURL: fmt.Sprintf("https://%s%s", siteDomain, "/%s/%s/merge_requests/new?merge_request[source_branch]=%s"),
}
}
return service
}
func getServices(config config.AppConfigurer) []*Service {
services := []*Service{
NewService("github", "github.com", "github.com"),
@@ -90,8 +113,8 @@ func NewPullRequest(gitCommand *GitCommand) *PullRequest {
}
// Create opens link to new pull request in browser
func (pr *PullRequest) Create(branch *models.Branch) (string, error) {
pullRequestURL, err := pr.getPullRequestURL(branch)
func (pr *PullRequest) Create(from string, to string) (string, error) {
pullRequestURL, err := pr.getPullRequestURL(from, to)
if err != nil {
return "", err
}
@@ -100,8 +123,8 @@ func (pr *PullRequest) Create(branch *models.Branch) (string, error) {
}
// CopyURL copies the pull request URL to the clipboard
func (pr *PullRequest) CopyURL(branch *models.Branch) (string, error) {
pullRequestURL, err := pr.getPullRequestURL(branch)
func (pr *PullRequest) CopyURL(from string, to string) (string, error) {
pullRequestURL, err := pr.getPullRequestURL(from, to)
if err != nil {
return "", err
}
@@ -109,8 +132,8 @@ func (pr *PullRequest) CopyURL(branch *models.Branch) (string, error) {
return pullRequestURL, pr.GitCommand.OSCommand.CopyToClipboard(pullRequestURL)
}
func (pr *PullRequest) getPullRequestURL(branch *models.Branch) (string, error) {
branchExistsOnRemote := pr.GitCommand.CheckRemoteBranchExists(branch)
func (pr *PullRequest) getPullRequestURL(from string, to string) (string, error) {
branchExistsOnRemote := pr.GitCommand.CheckRemoteBranchExists(from)
if !branchExistsOnRemote {
return "", errors.New(pr.GitCommand.Tr.NoBranchOnRemote)
@@ -131,9 +154,8 @@ func (pr *PullRequest) getPullRequestURL(branch *models.Branch) (string, error)
}
repoInfo := getRepoInfoFromURL(repoURL)
pullRequestURL := fmt.Sprintf(
gitService.PullRequestURL, repoInfo.Owner, repoInfo.Repository, branch.Name,
)
pullRequestURL := gitService.PullRequestURL(repoInfo.Owner, repoInfo.Repository, from, to)
return pullRequestURL, nil
}

View File

@@ -0,0 +1,256 @@
//go:build !windows
// +build !windows
package commands
import (
"os/exec"
"strings"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/stretchr/testify/assert"
)
// TestCreatePullRequest is a function.
func TestCreatePullRequest(t *testing.T) {
type scenario struct {
testName string
from string
to string
remoteUrl string
command func(string, ...string) *exec.Cmd
test func(url string, err error)
}
scenarios := []scenario{
{
testName: "Opens a link to new pull request on bitbucket",
from: "feature/profile-page",
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
}
assert.Equal(t, cmd, "bash")
assert.Equal(t, args, []string{"-c", `open "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1"`})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1", url)
},
},
{
testName: "Opens a link to new pull request on bitbucket with http remote url",
from: "feature/events",
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
}
assert.Equal(t, cmd, "bash")
assert.Equal(t, args, []string{"-c", `open "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1"`})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1", url)
},
},
{
testName: "Opens a link to new pull request on github",
from: "feature/sum-operation",
remoteUrl: "git@github.com:peter/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@github.com:peter/calculator.git")
}
assert.Equal(t, cmd, "bash")
assert.Equal(t, args, []string{"-c", `open "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1"`})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1", url)
},
},
{
testName: "Opens a link to new pull request on bitbucket with specific target branch",
from: "feature/profile-page/avatar",
to: "feature/profile-page",
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
}
assert.Equal(t, cmd, "bash")
assert.Equal(t, args, []string{"-c", `open "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar&dest=feature/profile-page&t=1"`})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar&dest=feature/profile-page&t=1", url)
},
},
{
testName: "Opens a link to new pull request on bitbucket with http remote url with specified target branch",
from: "feature/remote-events",
to: "feature/events",
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
}
assert.Equal(t, cmd, "bash")
assert.Equal(t, args, []string{"-c", `open "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events&dest=feature/events&t=1"`})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events&dest=feature/events&t=1", url)
},
},
{
testName: "Opens a link to new pull request on github with specific target branch",
from: "feature/sum-operation",
to: "feature/operations",
remoteUrl: "git@github.com:peter/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@github.com:peter/calculator.git")
}
assert.Equal(t, cmd, "bash")
assert.Equal(t, args, []string{"-c", `open "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1"`})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1", url)
},
},
{
testName: "Opens a link to new pull request on gitlab",
from: "feature/ui",
remoteUrl: "git@gitlab.com:peter/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
}
assert.Equal(t, cmd, "bash")
assert.Equal(t, args, []string{"-c", `open "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"`})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
},
},
{
testName: "Opens a link to new pull request on gitlab in nested groups",
from: "feature/ui",
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
}
assert.Equal(t, cmd, "bash")
assert.Equal(t, args, []string{"-c", `open "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"`})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
},
},
{
testName: "Opens a link to new pull request on gitlab with specific target branch",
from: "feature/commit-ui",
to: "epic/ui",
remoteUrl: "git@gitlab.com:peter/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
}
assert.Equal(t, cmd, "bash")
assert.Equal(t, args, []string{"-c", `open "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui"`})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url)
},
},
{
testName: "Opens a link to new pull request on gitlab with specific target branch in nested groups",
from: "feature/commit-ui",
to: "epic/ui",
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
}
assert.Equal(t, cmd, "bash")
assert.Equal(t, args, []string{"-c", `open "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui"`})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url)
},
},
{
testName: "Throws an error if git service is unsupported",
from: "feature/divide-operation",
remoteUrl: "git@something.com:peter/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.Error(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCommand := NewDummyGitCommand()
gitCommand.OSCommand.Command = s.command
gitCommand.OSCommand.Platform.OS = "darwin"
gitCommand.OSCommand.Platform.Shell = "bash"
gitCommand.OSCommand.Platform.ShellArg = "-c"
gitCommand.OSCommand.Config.GetUserConfig().OS.OpenLinkCommand = "open {{link}}"
gitCommand.OSCommand.Config.GetUserConfig().Services = map[string]string{
// valid configuration for a custom service URL
"git.work.com": "gitlab:code.work.com",
// invalid configurations for a custom service URL
"invalid.work.com": "noservice:invalid.work.com",
"noservice.work.com": "noservice.work.com",
}
gitCommand.GitConfig = git_config.NewFakeGitConfig(map[string]string{"remote.origin.url": s.remoteUrl})
dummyPullRequest := NewPullRequest(gitCommand)
s.test(dummyPullRequest.Create(s.from, s.to))
})
}
}

View File

@@ -1,12 +1,8 @@
package commands
import (
"os/exec"
"strings"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/stretchr/testify/assert"
)
@@ -43,135 +39,3 @@ func TestGetRepoInfoFromURL(t *testing.T) {
})
}
}
// TestCreatePullRequest is a function.
func TestCreatePullRequest(t *testing.T) {
type scenario struct {
testName string
branch *models.Branch
remoteUrl string
command func(string, ...string) *exec.Cmd
test func(url string, err error)
}
scenarios := []scenario{
{
testName: "Opens a link to new pull request on bitbucket",
branch: &models.Branch{
Name: "feature/profile-page",
},
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
}
assert.Equal(t, cmd, "open")
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1"})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1", url)
},
},
{
testName: "Opens a link to new pull request on bitbucket with http remote url",
branch: &models.Branch{
Name: "feature/events",
},
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
}
assert.Equal(t, cmd, "open")
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1"})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1", url)
},
},
{
testName: "Opens a link to new pull request on github",
branch: &models.Branch{
Name: "feature/sum-operation",
},
remoteUrl: "git@github.com:peter/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@github.com:peter/calculator.git")
}
assert.Equal(t, cmd, "open")
assert.Equal(t, args, []string{"https://github.com/peter/calculator/compare/feature/sum-operation?expand=1"})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1", url)
},
},
{
testName: "Opens a link to new pull request on gitlab",
branch: &models.Branch{
Name: "feature/ui",
},
remoteUrl: "git@gitlab.com:peter/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
}
assert.Equal(t, cmd, "open")
assert.Equal(t, args, []string{"https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
},
},
{
testName: "Throws an error if git service is unsupported",
branch: &models.Branch{
Name: "feature/divide-operation",
},
remoteUrl: "git@something.com:peter/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.Error(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCommand := NewDummyGitCommand()
gitCommand.OSCommand.Command = s.command
gitCommand.OSCommand.Config.GetUserConfig().OS.OpenLinkCommand = "open {{link}}"
gitCommand.OSCommand.Config.GetUserConfig().Services = map[string]string{
// valid configuration for a custom service URL
"git.work.com": "gitlab:code.work.com",
// invalid configurations for a custom service URL
"invalid.work.com": "noservice:invalid.work.com",
"noservice.work.com": "noservice.work.com",
}
gitCommand.getGitConfigValue = func(path string) (string, error) {
assert.Equal(t, path, "remote.origin.url")
return s.remoteUrl, nil
}
dummyPullRequest := NewPullRequest(gitCommand)
s.test(dummyPullRequest.Create(s.branch))
})
}
}

View File

@@ -0,0 +1,256 @@
//go:build windows
// +build windows
package commands
import (
"os/exec"
"strings"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/stretchr/testify/assert"
)
// TestCreatePullRequestOnWindows is a function.
func TestCreatePullRequestOnWindows(t *testing.T) {
type scenario struct {
testName string
from string
to string
remoteUrl string
command func(string, ...string) *exec.Cmd
test func(url string, err error)
}
scenarios := []scenario{
{
testName: "Opens a link to new pull request on bitbucket",
from: "feature/profile-page",
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
}
assert.Equal(t, cmd, "cmd")
assert.Equal(t, args, []string{"/c", "start", "", "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page^&t=1"})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1", url)
},
},
{
testName: "Opens a link to new pull request on bitbucket with http remote url",
from: "feature/events",
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
}
assert.Equal(t, cmd, "cmd")
assert.Equal(t, args, []string{"/c", "start", "", "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events^&t=1"})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1", url)
},
},
{
testName: "Opens a link to new pull request on github",
from: "feature/sum-operation",
remoteUrl: "git@github.com:peter/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@github.com:peter/calculator.git")
}
assert.Equal(t, cmd, "cmd")
assert.Equal(t, args, []string{"/c", "start", "", "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1"})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1", url)
},
},
{
testName: "Opens a link to new pull request on bitbucket with specific target branch",
from: "feature/profile-page/avatar",
to: "feature/profile-page",
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
}
assert.Equal(t, cmd, "cmd")
assert.Equal(t, args, []string{"/c", "start", "", "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar^&dest=feature/profile-page^&t=1"})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar&dest=feature/profile-page&t=1", url)
},
},
{
testName: "Opens a link to new pull request on bitbucket with http remote url with specified target branch",
from: "feature/remote-events",
to: "feature/events",
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
}
assert.Equal(t, cmd, "cmd")
assert.Equal(t, args, []string{"/c", "start", "", "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events^&dest=feature/events^&t=1"})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events&dest=feature/events&t=1", url)
},
},
{
testName: "Opens a link to new pull request on github with specific target branch",
from: "feature/sum-operation",
to: "feature/operations",
remoteUrl: "git@github.com:peter/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@github.com:peter/calculator.git")
}
assert.Equal(t, cmd, "cmd")
assert.Equal(t, args, []string{"/c", "start", "", "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1"})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1", url)
},
},
{
testName: "Opens a link to new pull request on gitlab",
from: "feature/ui",
remoteUrl: "git@gitlab.com:peter/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
}
assert.Equal(t, cmd, "cmd")
assert.Equal(t, args, []string{"/c", "start", "", "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
},
},
{
testName: "Opens a link to new pull request on gitlab in nested groups",
from: "feature/ui",
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
}
assert.Equal(t, cmd, "cmd")
assert.Equal(t, args, []string{"/c", "start", "", "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
},
},
{
testName: "Opens a link to new pull request on gitlab with specific target branch",
from: "feature/commit-ui",
to: "epic/ui",
remoteUrl: "git@gitlab.com:peter/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
}
assert.Equal(t, cmd, "cmd")
assert.Equal(t, args, []string{"/c", "start", "", "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui^&merge_request[target_branch]=epic/ui"})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url)
},
},
{
testName: "Opens a link to new pull request on gitlab with specific target branch in nested groups",
from: "feature/commit-ui",
to: "epic/ui",
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
}
assert.Equal(t, cmd, "cmd")
assert.Equal(t, args, []string{"/c", "start", "", "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui^&merge_request[target_branch]=epic/ui"})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url)
},
},
{
testName: "Throws an error if git service is unsupported",
from: "feature/divide-operation",
remoteUrl: "git@something.com:peter/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.Error(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCommand := NewDummyGitCommand()
gitCommand.OSCommand.Command = s.command
gitCommand.OSCommand.Platform.OS = "windows"
gitCommand.OSCommand.Platform.Shell = "cmd"
gitCommand.OSCommand.Platform.ShellArg = "/c"
gitCommand.OSCommand.Config.GetUserConfig().OS.OpenLinkCommand = `start "" {{link}}`
gitCommand.OSCommand.Config.GetUserConfig().Services = map[string]string{
// valid configuration for a custom service URL
"git.work.com": "gitlab:code.work.com",
// invalid configurations for a custom service URL
"invalid.work.com": "noservice:invalid.work.com",
"noservice.work.com": "noservice.work.com",
}
gitCommand.GitConfig = git_config.NewFakeGitConfig(map[string]string{"remote.origin.url": s.remoteUrl})
dummyPullRequest := NewPullRequest(gitCommand)
s.test(dummyPullRequest.Create(s.from, s.to))
})
}
}

View File

@@ -119,7 +119,7 @@ func (c *GitCommand) GenerateGenericRebaseTodo(commits []*models.Commit, actionI
var commitAction string
if i == actionIndex {
commitAction = action
} else if commit.IsMerge {
} else if commit.IsMerge() {
// your typical interactive rebase will actually drop merge commits by default. Damn git CLI, you scary!
// doing this means we don't need to worry about rebasing over merges which always causes problems.
// you typically shouldn't be doing rebases that pass over merge commits anyway.

View File

@@ -2,36 +2,35 @@ package commands
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/commands/models"
)
func (c *GitCommand) AddRemote(name string, url string) error {
return c.RunCommand("git remote add %s %s", name, url)
return c.RunCommand("git remote add %s %s", c.OSCommand.Quote(name), c.OSCommand.Quote(url))
}
func (c *GitCommand) RemoveRemote(name string) error {
return c.RunCommand("git remote remove %s", name)
return c.RunCommand("git remote remove %s", c.OSCommand.Quote(name))
}
func (c *GitCommand) RenameRemote(oldRemoteName string, newRemoteName string) error {
return c.RunCommand("git remote rename %s %s", oldRemoteName, newRemoteName)
return c.RunCommand("git remote rename %s %s", c.OSCommand.Quote(oldRemoteName), c.OSCommand.Quote(newRemoteName))
}
func (c *GitCommand) UpdateRemoteUrl(remoteName string, updatedUrl string) error {
return c.RunCommand("git remote set-url %s %s", remoteName, updatedUrl)
return c.RunCommand("git remote set-url %s %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(updatedUrl))
}
func (c *GitCommand) DeleteRemoteBranch(remoteName string, branchName string, promptUserForCredential func(string) string) error {
command := fmt.Sprintf("git push %s --delete %s", remoteName, branchName)
return c.OSCommand.DetectUnamePass(command, promptUserForCredential)
command := fmt.Sprintf("git push %s --delete %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(branchName))
cmdObj := c.NewCmdObjFromStr(command)
return c.OSCommand.DetectUnamePass(cmdObj, promptUserForCredential)
}
// CheckRemoteBranchExists Returns remote branch
func (c *GitCommand) CheckRemoteBranchExists(branch *models.Branch) bool {
func (c *GitCommand) CheckRemoteBranchExists(branchName string) bool {
_, err := c.OSCommand.RunCommandWithOutput(
"git show-ref --verify -- refs/remotes/origin/%s",
branch.Name,
c.OSCommand.Quote(branchName),
)
return err == nil
@@ -39,5 +38,5 @@ func (c *GitCommand) CheckRemoteBranchExists(branch *models.Branch) bool {
// GetRemoteURL returns current repo remote url
func (c *GitCommand) GetRemoteURL() string {
return c.GetConfigValue("remote.origin.url")
return c.GitConfig.Get("remote.origin.url")
}

View File

@@ -69,11 +69,11 @@ func (c *GitCommand) SubmoduleStash(submodule *models.SubmoduleConfig) error {
return nil
}
return c.RunCommand("git -C %s stash --include-untracked", submodule.Path)
return c.RunCommand("git -C %s stash --include-untracked", c.OSCommand.Quote(submodule.Path))
}
func (c *GitCommand) SubmoduleReset(submodule *models.SubmoduleConfig) error {
return c.RunCommand("git submodule update --init --force %s", submodule.Path)
return c.RunCommand("git submodule update --init --force -- %s", c.OSCommand.Quote(submodule.Path))
}
func (c *GitCommand) SubmoduleUpdateAll() error {
@@ -84,13 +84,13 @@ func (c *GitCommand) SubmoduleUpdateAll() error {
func (c *GitCommand) SubmoduleDelete(submodule *models.SubmoduleConfig) error {
// based on https://gist.github.com/myusuf3/7f645819ded92bda6677
if err := c.RunCommand("git submodule deinit --force %s", submodule.Path); err != nil {
if err := c.RunCommand("git submodule deinit --force -- %s", c.OSCommand.Quote(submodule.Path)); err != nil {
if strings.Contains(err.Error(), "did not match any file(s) known to git") {
if err := c.RunCommand("git config --file .gitmodules --remove-section submodule.%s", submodule.Name); err != nil {
if err := c.RunCommand("git config --file .gitmodules --remove-section submodule.%s", c.OSCommand.Quote(submodule.Name)); err != nil {
return err
}
if err := c.RunCommand("git config --remove-section submodule.%s", submodule.Name); err != nil {
if err := c.RunCommand("git config --remove-section submodule.%s", c.OSCommand.Quote(submodule.Name)); err != nil {
return err
}
@@ -119,11 +119,11 @@ func (c *GitCommand) SubmoduleAdd(name string, path string, url string) error {
func (c *GitCommand) SubmoduleUpdateUrl(name string, path string, newUrl string) error {
// the set-url command is only for later git versions so we're doing it manually here
if err := c.RunCommand("git config --file .gitmodules submodule.%s.url %s", name, newUrl); err != nil {
if err := c.RunCommand("git config --file .gitmodules submodule.%s.url %s", c.OSCommand.Quote(name), c.OSCommand.Quote(newUrl)); err != nil {
return err
}
if err := c.RunCommand("git submodule sync %s", path); err != nil {
if err := c.RunCommand("git submodule sync -- %s", c.OSCommand.Quote(path)); err != nil {
return err
}
@@ -131,11 +131,11 @@ func (c *GitCommand) SubmoduleUpdateUrl(name string, path string, newUrl string)
}
func (c *GitCommand) SubmoduleInit(path string) error {
return c.RunCommand("git submodule init %s", path)
return c.RunCommand("git submodule init -- %s", c.OSCommand.Quote(path))
}
func (c *GitCommand) SubmoduleUpdate(path string) error {
return c.RunCommand("git submodule update --init %s", path)
return c.RunCommand("git submodule update --init -- %s", c.OSCommand.Quote(path))
}
func (c *GitCommand) SubmoduleBulkInitCmdStr() string {

View File

@@ -2,27 +2,43 @@ package commands
import (
"fmt"
"github.com/go-errors/errors"
)
// Push pushes to a branch
func (c *GitCommand) Push(branchName string, force bool, upstream string, args string, promptUserForCredential func(string) string) error {
followTagsFlag := "--follow-tags"
if c.GetConfigValue("push.followTags") == "false" {
followTagsFlag = ""
type PushOpts struct {
Force bool
UpstreamRemote string
UpstreamBranch string
SetUpstream bool
PromptUserForCredential func(string) string
}
func (c *GitCommand) Push(opts PushOpts) error {
cmdStr := "git push"
if opts.Force {
cmdStr += " --force-with-lease"
}
forceFlag := ""
if force {
forceFlag = "--force-with-lease"
if opts.SetUpstream {
cmdStr += " --set-upstream"
}
setUpstreamArg := ""
if upstream != "" {
setUpstreamArg = "--set-upstream " + upstream
if opts.UpstreamRemote != "" {
cmdStr += " " + c.OSCommand.Quote(opts.UpstreamRemote)
}
cmd := fmt.Sprintf("git push %s %s %s %s", followTagsFlag, forceFlag, setUpstreamArg, args)
return c.OSCommand.DetectUnamePass(cmd, promptUserForCredential)
if opts.UpstreamBranch != "" {
if opts.UpstreamRemote == "" {
return errors.New(c.Tr.MustSpecifyOriginError)
}
cmdStr += " " + c.OSCommand.Quote(opts.UpstreamBranch)
}
cmdObj := c.NewCmdObjFromStr(cmdStr)
return c.OSCommand.DetectUnamePass(cmdObj, opts.PromptUserForCredential)
}
type FetchOptions struct {
@@ -33,16 +49,17 @@ type FetchOptions struct {
// Fetch fetch git repo
func (c *GitCommand) Fetch(opts FetchOptions) error {
command := "git fetch"
cmdStr := "git fetch"
if opts.RemoteName != "" {
command = fmt.Sprintf("%s %s", command, opts.RemoteName)
cmdStr = fmt.Sprintf("%s %s", cmdStr, c.OSCommand.Quote(opts.RemoteName))
}
if opts.BranchName != "" {
command = fmt.Sprintf("%s %s", command, opts.BranchName)
cmdStr = fmt.Sprintf("%s %s", cmdStr, c.OSCommand.Quote(opts.BranchName))
}
return c.OSCommand.DetectUnamePass(command, func(question string) string {
cmdObj := c.NewCmdObjFromStr(cmdStr)
return c.OSCommand.DetectUnamePass(cmdObj, func(question string) string {
if opts.PromptUserForCredential != nil {
return opts.PromptUserForCredential(question)
}
@@ -50,12 +67,45 @@ func (c *GitCommand) Fetch(opts FetchOptions) error {
})
}
type PullOptions struct {
PromptUserForCredential func(string) string
RemoteName string
BranchName string
FastForwardOnly bool
}
func (c *GitCommand) Pull(opts PullOptions) error {
if opts.PromptUserForCredential == nil {
return errors.New("PromptUserForCredential is required")
}
cmdStr := "git pull --no-edit"
if opts.FastForwardOnly {
cmdStr += " --ff-only"
}
if opts.RemoteName != "" {
cmdStr = fmt.Sprintf("%s %s", cmdStr, c.OSCommand.Quote(opts.RemoteName))
}
if opts.BranchName != "" {
cmdStr = fmt.Sprintf("%s %s", cmdStr, c.OSCommand.Quote(opts.BranchName))
}
// setting GIT_SEQUENCE_EDITOR to ':' as a way of skipping it, in case the user
// has 'pull.rebase = interactive' configured.
cmdObj := c.NewCmdObjFromStr(cmdStr).AddEnvVars("GIT_SEQUENCE_EDITOR=:")
return c.OSCommand.DetectUnamePass(cmdObj, opts.PromptUserForCredential)
}
func (c *GitCommand) FastForward(branchName string, remoteName string, remoteBranchName string, promptUserForCredential func(string) string) error {
command := fmt.Sprintf("git fetch %s %s:%s", remoteName, remoteBranchName, branchName)
return c.OSCommand.DetectUnamePass(command, promptUserForCredential)
cmdStr := fmt.Sprintf("git fetch %s %s:%s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(remoteBranchName), c.OSCommand.Quote(branchName))
cmdObj := c.NewCmdObjFromStr(cmdStr)
return c.OSCommand.DetectUnamePass(cmdObj, promptUserForCredential)
}
func (c *GitCommand) FetchRemote(remoteName string, promptUserForCredential func(string) string) error {
command := fmt.Sprintf("git fetch %s", remoteName)
return c.OSCommand.DetectUnamePass(command, promptUserForCredential)
cmdStr := fmt.Sprintf("git fetch %s", c.OSCommand.Quote(remoteName))
cmdObj := c.NewCmdObjFromStr(cmdStr)
return c.OSCommand.DetectUnamePass(cmdObj, promptUserForCredential)
}

View File

@@ -11,87 +11,153 @@ import (
// TestGitCommandPush is a function.
func TestGitCommandPush(t *testing.T) {
type scenario struct {
testName string
getGitConfigValue func(string) (string, error)
command func(string, ...string) *exec.Cmd
forcePush bool
test func(error)
testName string
command func(string, ...string) *exec.Cmd
opts PushOpts
test func(error)
}
prompt := func(passOrUname string) string {
return "\n"
}
scenarios := []scenario{
{
"Push with force disabled, follow-tags on",
func(string) (string, error) {
return "", nil
},
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push", "--follow-tags"}, args)
return secureexec.Command("echo")
},
false,
func(err error) {
assert.NoError(t, err)
},
},
{
"Push with force enabled, follow-tags on",
func(string) (string, error) {
return "", nil
},
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push", "--follow-tags", "--force-with-lease"}, args)
return secureexec.Command("echo")
},
true,
func(err error) {
assert.NoError(t, err)
},
},
{
"Push with force disabled, follow-tags off",
func(string) (string, error) {
return "false", nil
},
"Push with force disabled",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push"}, args)
return secureexec.Command("echo")
},
false,
PushOpts{Force: false, PromptUserForCredential: prompt},
func(err error) {
assert.NoError(t, err)
},
},
{
"Push with an error occurring, follow-tags on",
func(string) (string, error) {
return "", nil
},
"Push with force enabled",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push", "--follow-tags"}, args)
assert.EqualValues(t, []string{"push", "--force-with-lease"}, args)
return secureexec.Command("echo")
},
PushOpts{Force: true, PromptUserForCredential: prompt},
func(err error) {
assert.NoError(t, err)
},
},
{
"Push with an error occurring",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push"}, args)
return secureexec.Command("test")
},
false,
PushOpts{Force: false, PromptUserForCredential: prompt},
func(err error) {
assert.Error(t, err)
},
},
{
"Push with force disabled, upstream supplied",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push", "origin", "master"}, args)
return secureexec.Command("echo")
},
PushOpts{
Force: false,
UpstreamRemote: "origin",
UpstreamBranch: "master",
PromptUserForCredential: prompt,
},
func(err error) {
assert.NoError(t, err)
},
},
{
"Push with force disabled, setting upstream",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push", "--set-upstream", "origin", "master"}, args)
return secureexec.Command("echo")
},
PushOpts{
Force: false,
UpstreamRemote: "origin",
UpstreamBranch: "master",
PromptUserForCredential: prompt,
SetUpstream: true,
},
func(err error) {
assert.NoError(t, err)
},
},
{
"Push with force enabled, setting upstream",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push", "--force-with-lease", "--set-upstream", "origin", "master"}, args)
return secureexec.Command("echo")
},
PushOpts{
Force: true,
UpstreamRemote: "origin",
UpstreamBranch: "master",
PromptUserForCredential: prompt,
SetUpstream: true,
},
func(err error) {
assert.NoError(t, err)
},
},
{
"Push with remote branch but no origin",
func(cmd string, args ...string) *exec.Cmd {
return nil
},
PushOpts{
Force: true,
UpstreamRemote: "",
UpstreamBranch: "master",
PromptUserForCredential: prompt,
SetUpstream: true,
},
func(err error) {
assert.Error(t, err)
assert.EqualValues(t, "Must specify a remote if specifying a branch", err.Error())
},
},
{
"Push with force disabled, upstream supplied",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push", "origin", "master"}, args)
return secureexec.Command("echo")
},
PushOpts{
Force: false,
UpstreamRemote: "origin",
UpstreamBranch: "master",
PromptUserForCredential: prompt,
},
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
gitCmd.getGitConfigValue = s.getGitConfigValue
err := gitCmd.Push("test", s.forcePush, "", "", func(passOrUname string) string {
return "\n"
})
err := gitCmd.Push(s.opts)
s.test(err)
})
}

View File

@@ -1,16 +1,19 @@
package commands
import "fmt"
import (
"fmt"
)
func (c *GitCommand) CreateLightweightTag(tagName string, commitSha string) error {
return c.RunCommand("git tag %s %s", tagName, commitSha)
return c.RunCommand("git tag -- %s %s", c.OSCommand.Quote(tagName), commitSha)
}
func (c *GitCommand) DeleteTag(tagName string) error {
return c.RunCommand("git tag -d %s", tagName)
return c.RunCommand("git tag -d %s", c.OSCommand.Quote(tagName))
}
func (c *GitCommand) PushTag(remoteName string, tagName string, promptUserForCredential func(string) string) error {
command := fmt.Sprintf("git push %s %s", remoteName, tagName)
return c.OSCommand.DetectUnamePass(command, promptUserForCredential)
cmdStr := fmt.Sprintf("git push %s %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(tagName))
cmdObj := c.NewCmdObjFromStr(cmdStr)
return c.OSCommand.DetectUnamePass(cmdObj, promptUserForCredential)
}

View File

@@ -12,17 +12,19 @@ import (
// AppConfig contains the base configuration fields required for lazygit.
type AppConfig struct {
Debug bool `long:"debug" env:"DEBUG" default:"false"`
Version string `long:"version" env:"VERSION" default:"unversioned"`
Commit string `long:"commit" env:"COMMIT"`
BuildDate string `long:"build-date" env:"BUILD_DATE"`
Name string `long:"name" env:"NAME" default:"lazygit"`
BuildSource string `long:"build-source" env:"BUILD_SOURCE" default:""`
UserConfig *UserConfig
UserConfigDir string
UserConfigPath string
AppState *AppState
IsNewRepo bool
Debug bool `long:"debug" env:"DEBUG" default:"false"`
Version string `long:"version" env:"VERSION" default:"unversioned"`
Commit string `long:"commit" env:"COMMIT"`
BuildDate string `long:"build-date" env:"BUILD_DATE"`
Name string `long:"name" env:"NAME" default:"lazygit"`
BuildSource string `long:"build-source" env:"BUILD_SOURCE" default:""`
UserConfig *UserConfig
UserConfigPaths []string
DeafultConfFiles bool
UserConfigDir string
TempDir string
AppState *AppState
IsNewRepo bool
}
// AppConfigurer interface allows individual app config structs to inherit Fields
@@ -35,23 +37,35 @@ type AppConfigurer interface {
GetName() string
GetBuildSource() string
GetUserConfig() *UserConfig
GetUserConfigPaths() []string
GetUserConfigDir() string
GetUserConfigPath() string
GetTempDir() string
GetAppState() *AppState
SaveAppState() error
SetIsNewRepo(bool)
GetIsNewRepo() bool
ReloadUserConfig() error
ShowCommandLogOnStartup() bool
}
// NewAppConfig makes a new app config
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag bool) (*AppConfig, error) {
configDir, err := findOrCreateConfigDir()
if err != nil {
if err != nil && !os.IsPermission(err) {
return nil, err
}
userConfig, err := loadUserConfigWithDefaults(configDir)
var userConfigPaths []string
customConfigFiles := os.Getenv("LG_CONFIG_FILE")
if customConfigFiles != "" {
// Load user defined config files
userConfigPaths = strings.Split(customConfigFiles, ",")
} else {
// Load default config files
userConfigPaths = []string{filepath.Join(configDir, ConfigFilename)}
}
userConfig, err := loadUserConfigWithDefaults(userConfigPaths)
if err != nil {
return nil, err
}
@@ -60,28 +74,35 @@ func NewAppConfig(name, version, commit, date string, buildSource string, debugg
debuggingFlag = true
}
tempDir := filepath.Join(os.TempDir(), "lazygit")
appState, err := loadAppState()
if err != nil {
return nil, err
}
appConfig := &AppConfig{
Name: "lazygit",
Version: version,
Commit: commit,
BuildDate: date,
Debug: debuggingFlag,
BuildSource: buildSource,
UserConfig: userConfig,
UserConfigDir: configDir,
UserConfigPath: filepath.Join(configDir, "config.yml"),
AppState: appState,
IsNewRepo: false,
Name: "lazygit",
Version: version,
Commit: commit,
BuildDate: date,
Debug: debuggingFlag,
BuildSource: buildSource,
UserConfig: userConfig,
UserConfigPaths: userConfigPaths,
UserConfigDir: configDir,
TempDir: tempDir,
AppState: appState,
IsNewRepo: false,
}
return appConfig, nil
}
func isCustomConfigFile(path string) bool {
return path != filepath.Join(ConfigDir(), ConfigFilename)
}
func ConfigDir() string {
legacyConfigDirectory := configDirForVendor("jesseduffield")
if _, err := os.Stat(legacyConfigDirectory); !os.IsNotExist(err) {
@@ -102,43 +123,45 @@ func configDirForVendor(vendor string) string {
func findOrCreateConfigDir() (string, error) {
folder := ConfigDir()
err := os.MkdirAll(folder, 0755)
if err != nil {
return "", err
}
return folder, nil
return folder, os.MkdirAll(folder, 0755)
}
func loadUserConfigWithDefaults(configDir string) (*UserConfig, error) {
return loadUserConfig(configDir, GetDefaultConfig())
func loadUserConfigWithDefaults(configFiles []string) (*UserConfig, error) {
return loadUserConfig(configFiles, GetDefaultConfig())
}
func loadUserConfig(configDir string, base *UserConfig) (*UserConfig, error) {
fileName := filepath.Join(configDir, "config.yml")
func loadUserConfig(configFiles []string, base *UserConfig) (*UserConfig, error) {
for _, path := range configFiles {
if _, err := os.Stat(path); err != nil {
if !os.IsNotExist(err) {
return nil, err
}
if _, err := os.Stat(fileName); err != nil {
if os.IsNotExist(err) {
file, err := os.Create(fileName)
// if use has supplied their own custom config file path(s), we assume
// the files have already been created, so we won't go and create them here.
if isCustomConfigFile(path) {
return nil, err
}
file, err := os.Create(path)
if err != nil {
if strings.Contains(err.Error(), "read-only file system") {
return base, nil
if os.IsPermission(err) {
// apparently when people have read-only permissions they prefer us to fail silently
continue
}
return nil, err
}
file.Close()
} else {
}
content, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
}
content, err := ioutil.ReadFile(fileName)
if err != nil {
return nil, err
}
if err := yaml.Unmarshal(content, base); err != nil {
return nil, err
if err := yaml.Unmarshal(content, base); err != nil {
return nil, err
}
}
return base, nil
@@ -190,22 +213,25 @@ func (c *AppConfig) GetUserConfig() *UserConfig {
return c.UserConfig
}
// GetUserConfig returns the user config
func (c *AppConfig) GetUserConfigPath() string {
return c.UserConfigPath
}
// GetAppState returns the app state
func (c *AppConfig) GetAppState() *AppState {
return c.AppState
}
func (c *AppConfig) GetUserConfigPaths() []string {
return c.UserConfigPaths
}
func (c *AppConfig) GetUserConfigDir() string {
return c.UserConfigDir
}
func (c *AppConfig) GetTempDir() string {
return c.TempDir
}
func (c *AppConfig) ReloadUserConfig() error {
userConfig, err := loadUserConfigWithDefaults(c.UserConfigDir)
userConfig, err := loadUserConfigWithDefaults(c.UserConfigPaths)
if err != nil {
return err
}
@@ -223,9 +249,11 @@ func configFilePath(filename string) (string, error) {
return filepath.Join(folder, filename), nil
}
// ConfigFilename returns the filename of the current config file
var ConfigFilename = "config.yml"
// ConfigFilename returns the filename of the deafult config file
func (c *AppConfig) ConfigFilename() string {
return filepath.Join(c.UserConfigDir, "config.yml")
return filepath.Join(c.UserConfigDir, ConfigFilename)
}
// SaveAppState marshalls the AppState struct and writes it to the disk
@@ -240,13 +268,34 @@ func (c *AppConfig) SaveAppState() error {
return err
}
return ioutil.WriteFile(filepath, marshalledAppState, 0644)
err = ioutil.WriteFile(filepath, marshalledAppState, 0644)
if err != nil && os.IsPermission(err) {
// apparently when people have read-only permissions they prefer us to fail silently
return nil
}
return err
}
// originally we could only hide the command log permanently via the config
// but now we do it via state. So we need to still support the config for the
// sake of backwards compatibility
func (c *AppConfig) ShowCommandLogOnStartup() bool {
if !c.UserConfig.Gui.ShowCommandLog {
return false
}
return !c.AppState.HideCommandLog
}
// loadAppState loads recorded AppState from file
func loadAppState() (*AppState, error) {
filepath, err := configFilePath("state.yml")
if err != nil {
if os.IsPermission(err) {
// apparently when people have read-only permissions they prefer us to fail silently
return getDefaultAppState(), nil
}
return nil, err
}
@@ -274,6 +323,10 @@ type AppState struct {
LastUpdateCheck int64
RecentRepos []string
StartupPopupVersion int
// these are for custom commands typed in directly, not for custom commands in the lazygit config
CustomCommandsHistory []string
HideCommandLog bool
}
func getDefaultAppState() *AppState {

View File

@@ -1,3 +1,4 @@
//go:build !windows && !linux
// +build !windows,!linux
package config
@@ -5,7 +6,9 @@ package config
// GetPlatformDefaultConfig gets the defaults for the platform
func GetPlatformDefaultConfig() OSConfig {
return OSConfig{
OpenCommand: "open {{filename}}",
OpenLinkCommand: "open {{link}}",
EditCommand: ``,
EditCommandTemplate: `{{editor}} {{filename}}`,
OpenCommand: "open {{filename}}",
OpenLinkCommand: "open {{link}}",
}
}

View File

@@ -3,7 +3,9 @@ package config
// GetPlatformDefaultConfig gets the defaults for the platform
func GetPlatformDefaultConfig() OSConfig {
return OSConfig{
OpenCommand: `sh -c "xdg-open {{filename}} >/dev/null"`,
OpenLinkCommand: `sh -c "xdg-open {{link}} >/dev/null"`,
EditCommand: ``,
EditCommandTemplate: `{{editor}} {{filename}}`,
OpenCommand: `xdg-open {{filename}} >/dev/null`,
OpenLinkCommand: `xdg-open {{link}} >/dev/null`,
}
}

View File

@@ -3,7 +3,9 @@ package config
// GetPlatformDefaultConfig gets the defaults for the platform
func GetPlatformDefaultConfig() OSConfig {
return OSConfig{
OpenCommand: `cmd /c "start "" {{filename}}"`,
OpenLinkCommand: `cmd /c "start "" {{link}}"`,
EditCommand: ``,
EditCommandTemplate: `{{editor}} {{filename}}`,
OpenCommand: `start "" {{filename}}`,
OpenLinkCommand: `start "" {{link}}`,
}
}

View File

@@ -14,6 +14,7 @@ func NewDummyAppConfig() *AppConfig {
Debug: false,
BuildSource: "",
UserConfig: GetDefaultConfig(),
AppState: &AppState{},
}
_ = yaml.Unmarshal([]byte{}, appConfig.AppState)
return appConfig

View File

@@ -32,9 +32,11 @@ type GuiConfig struct {
SidePanelWidth float64 `yaml:"sidePanelWidth"`
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
MainPanelSplitMode string `yaml:"mainPanelSplitMode"`
Language string `yaml:"language"`
Theme ThemeConfig `yaml:"theme"`
CommitLength CommitLengthConfig `yaml:"commitLength"`
SkipNoStagedFilesWarning bool `yaml:"skipNoStagedFilesWarning"`
ShowListFooter bool `yaml:"showListFooter"`
ShowFileTree bool `yaml:"showFileTree"`
ShowRandomTip bool `yaml:"showRandomTip"`
ShowCommandLog bool `yaml:"showCommandLog"`
@@ -42,12 +44,14 @@ type GuiConfig struct {
}
type ThemeConfig struct {
LightTheme bool `yaml:"lightTheme"`
ActiveBorderColor []string `yaml:"activeBorderColor"`
InactiveBorderColor []string `yaml:"inactiveBorderColor"`
OptionsTextColor []string `yaml:"optionsTextColor"`
SelectedLineBgColor []string `yaml:"selectedLineBgColor"`
SelectedRangeBgColor []string `yaml:"selectedRangeBgColor"`
LightTheme bool `yaml:"lightTheme"`
ActiveBorderColor []string `yaml:"activeBorderColor"`
InactiveBorderColor []string `yaml:"inactiveBorderColor"`
OptionsTextColor []string `yaml:"optionsTextColor"`
SelectedLineBgColor []string `yaml:"selectedLineBgColor"`
SelectedRangeBgColor []string `yaml:"selectedRangeBgColor"`
CherryPickedCommitBgColor []string `yaml:"cherryPickedCommitBgColor"`
CherryPickedCommitFgColor []string `yaml:"cherryPickedCommitFgColor"`
}
type CommitLengthConfig struct {
@@ -57,7 +61,6 @@ type CommitLengthConfig struct {
type GitConfig struct {
Paging PagingConfig `yaml:"paging"`
Merging MergingConfig `yaml:"merging"`
Pull PullConfig `yaml:"pull"`
SkipHookPrefix string `yaml:"skipHookPrefix"`
AutoFetch bool `yaml:"autoFetch"`
BranchLogCmd string `yaml:"branchLogCmd"`
@@ -65,6 +68,7 @@ type GitConfig struct {
OverrideGpg bool `yaml:"overrideGpg"`
DisableForcePushing bool `yaml:"disableForcePushing"`
CommitPrefixes map[string]CommitPrefixConfig `yaml:"commitPrefixes"`
ParseEmoji bool `yaml:"parseEmoji"`
}
type PagingConfig struct {
@@ -78,10 +82,6 @@ type MergingConfig struct {
Args string `yaml:"args"`
}
type PullConfig struct {
Mode string `yaml:"mode"`
}
type CommitPrefixConfig struct {
Pattern string `yaml:"pattern"`
Replace string `yaml:"replace"`
@@ -106,63 +106,66 @@ type KeybindingConfig struct {
// damn looks like we have some inconsistencies here with -alt and -alt1
type KeybindingUniversalConfig struct {
Quit string `yaml:"quit"`
QuitAlt1 string `yaml:"quit-alt1"`
Return string `yaml:"return"`
QuitWithoutChangingDirectory string `yaml:"quitWithoutChangingDirectory"`
TogglePanel string `yaml:"togglePanel"`
PrevItem string `yaml:"prevItem"`
NextItem string `yaml:"nextItem"`
PrevItemAlt string `yaml:"prevItem-alt"`
NextItemAlt string `yaml:"nextItem-alt"`
PrevPage string `yaml:"prevPage"`
NextPage string `yaml:"nextPage"`
GotoTop string `yaml:"gotoTop"`
GotoBottom string `yaml:"gotoBottom"`
PrevBlock string `yaml:"prevBlock"`
NextBlock string `yaml:"nextBlock"`
PrevBlockAlt string `yaml:"prevBlock-alt"`
NextBlockAlt string `yaml:"nextBlock-alt"`
NextBlockAlt2 string `yaml:"nextBlock-alt2"`
PrevBlockAlt2 string `yaml:"prevBlock-alt2"`
NextMatch string `yaml:"nextMatch"`
PrevMatch string `yaml:"prevMatch"`
StartSearch string `yaml:"startSearch"`
OptionMenu string `yaml:"optionMenu"`
OptionMenuAlt1 string `yaml:"optionMenu-alt1"`
Select string `yaml:"select"`
GoInto string `yaml:"goInto"`
Confirm string `yaml:"confirm"`
ConfirmAlt1 string `yaml:"confirm-alt1"`
Remove string `yaml:"remove"`
New string `yaml:"new"`
Edit string `yaml:"edit"`
OpenFile string `yaml:"openFile"`
ScrollUpMain string `yaml:"scrollUpMain"`
ScrollDownMain string `yaml:"scrollDownMain"`
ScrollUpMainAlt1 string `yaml:"scrollUpMain-alt1"`
ScrollDownMainAlt1 string `yaml:"scrollDownMain-alt1"`
ScrollUpMainAlt2 string `yaml:"scrollUpMain-alt2"`
ScrollDownMainAlt2 string `yaml:"scrollDownMain-alt2"`
ExecuteCustomCommand string `yaml:"executeCustomCommand"`
CreateRebaseOptionsMenu string `yaml:"createRebaseOptionsMenu"`
PushFiles string `yaml:"pushFiles"`
PullFiles string `yaml:"pullFiles"`
Refresh string `yaml:"refresh"`
CreatePatchOptionsMenu string `yaml:"createPatchOptionsMenu"`
NextTab string `yaml:"nextTab"`
PrevTab string `yaml:"prevTab"`
NextScreenMode string `yaml:"nextScreenMode"`
PrevScreenMode string `yaml:"prevScreenMode"`
Undo string `yaml:"undo"`
Redo string `yaml:"redo"`
FilteringMenu string `yaml:"filteringMenu"`
DiffingMenu string `yaml:"diffingMenu"`
DiffingMenuAlt string `yaml:"diffingMenu-alt"`
CopyToClipboard string `yaml:"copyToClipboard"`
SubmitEditorText string `yaml:"submitEditorText"`
AppendNewline string `yaml:"appendNewline"`
ExtrasMenu string `yaml:"extrasMenu"`
Quit string `yaml:"quit"`
QuitAlt1 string `yaml:"quit-alt1"`
Return string `yaml:"return"`
QuitWithoutChangingDirectory string `yaml:"quitWithoutChangingDirectory"`
TogglePanel string `yaml:"togglePanel"`
PrevItem string `yaml:"prevItem"`
NextItem string `yaml:"nextItem"`
PrevItemAlt string `yaml:"prevItem-alt"`
NextItemAlt string `yaml:"nextItem-alt"`
PrevPage string `yaml:"prevPage"`
NextPage string `yaml:"nextPage"`
GotoTop string `yaml:"gotoTop"`
GotoBottom string `yaml:"gotoBottom"`
PrevBlock string `yaml:"prevBlock"`
NextBlock string `yaml:"nextBlock"`
PrevBlockAlt string `yaml:"prevBlock-alt"`
NextBlockAlt string `yaml:"nextBlock-alt"`
NextBlockAlt2 string `yaml:"nextBlock-alt2"`
PrevBlockAlt2 string `yaml:"prevBlock-alt2"`
JumpToBlock []string `yaml:"jumpToBlock"`
NextMatch string `yaml:"nextMatch"`
PrevMatch string `yaml:"prevMatch"`
StartSearch string `yaml:"startSearch"`
OptionMenu string `yaml:"optionMenu"`
OptionMenuAlt1 string `yaml:"optionMenu-alt1"`
Select string `yaml:"select"`
GoInto string `yaml:"goInto"`
Confirm string `yaml:"confirm"`
ConfirmAlt1 string `yaml:"confirm-alt1"`
Remove string `yaml:"remove"`
New string `yaml:"new"`
Edit string `yaml:"edit"`
OpenFile string `yaml:"openFile"`
ScrollUpMain string `yaml:"scrollUpMain"`
ScrollDownMain string `yaml:"scrollDownMain"`
ScrollUpMainAlt1 string `yaml:"scrollUpMain-alt1"`
ScrollDownMainAlt1 string `yaml:"scrollDownMain-alt1"`
ScrollUpMainAlt2 string `yaml:"scrollUpMain-alt2"`
ScrollDownMainAlt2 string `yaml:"scrollDownMain-alt2"`
ExecuteCustomCommand string `yaml:"executeCustomCommand"`
CreateRebaseOptionsMenu string `yaml:"createRebaseOptionsMenu"`
PushFiles string `yaml:"pushFiles"`
PullFiles string `yaml:"pullFiles"`
Refresh string `yaml:"refresh"`
CreatePatchOptionsMenu string `yaml:"createPatchOptionsMenu"`
NextTab string `yaml:"nextTab"`
PrevTab string `yaml:"prevTab"`
NextScreenMode string `yaml:"nextScreenMode"`
PrevScreenMode string `yaml:"prevScreenMode"`
Undo string `yaml:"undo"`
Redo string `yaml:"redo"`
FilteringMenu string `yaml:"filteringMenu"`
DiffingMenu string `yaml:"diffingMenu"`
DiffingMenuAlt string `yaml:"diffingMenu-alt"`
CopyToClipboard string `yaml:"copyToClipboard"`
OpenRecentRepos string `yaml:"openRecentRepos"`
SubmitEditorText string `yaml:"submitEditorText"`
AppendNewline string `yaml:"appendNewline"`
ExtrasMenu string `yaml:"extrasMenu"`
ToggleWhitespaceInDiffView string `yaml:"toggleWhitespaceInDiffView"`
}
type KeybindingStatusConfig struct {
@@ -185,10 +188,12 @@ type KeybindingFilesConfig struct {
Fetch string `yaml:"fetch"`
ToggleTreeView string `yaml:"toggleTreeView"`
OpenMergeTool string `yaml:"openMergeTool"`
OpenStatusFilter string `yaml:"openStatusFilter"`
}
type KeybindingBranchesConfig struct {
CreatePullRequest string `yaml:"createPullRequest"`
ViewPullRequestOptions string `yaml:"viewPullRequestOptions"`
CopyPullRequestURL string `yaml:"copyPullRequestURL"`
CheckoutBranchByName string `yaml:"checkoutBranchByName"`
ForceCheckoutBranch string `yaml:"forceCheckoutBranch"`
@@ -247,6 +252,12 @@ type KeybindingSubmodulesConfig struct {
// OSConfig contains config on the level of the os
type OSConfig struct {
// EditCommand is the command for editing a file
EditCommand string `yaml:"editCommand,omitempty"`
// EditCommandTemplate is the command template for editing a file
EditCommandTemplate string `yaml:"editCommandTemplate,omitempty"`
// OpenCommand is the command for opening a file
OpenCommand string `yaml:"openCommand,omitempty"`
@@ -273,6 +284,12 @@ type CustomCommandPrompt struct {
// this only applies to menus
Options []CustomCommandMenuOption
// this only applies to menuFromCommand
Command string `yaml:"command"`
Filter string `yaml:"filter"`
ValueFormat string `yaml:"valueFormat"`
LabelFormat string `yaml:"labelFormat"`
}
type CustomCommandMenuOption struct {
@@ -292,18 +309,22 @@ func GetDefaultConfig() *UserConfig {
SidePanelWidth: 0.3333,
ExpandFocusedSidePanel: false,
MainPanelSplitMode: "flexible",
Language: "auto",
Theme: ThemeConfig{
LightTheme: false,
ActiveBorderColor: []string{"green", "bold"},
InactiveBorderColor: []string{"white"},
OptionsTextColor: []string{"blue"},
SelectedLineBgColor: []string{"default"},
SelectedRangeBgColor: []string{"blue"},
LightTheme: false,
ActiveBorderColor: []string{"green", "bold"},
InactiveBorderColor: []string{"white"},
OptionsTextColor: []string{"blue"},
SelectedLineBgColor: []string{"default"},
SelectedRangeBgColor: []string{"blue"},
CherryPickedCommitBgColor: []string{"blue"},
CherryPickedCommitFgColor: []string{"cyan"},
},
CommitLength: CommitLengthConfig{Show: true},
SkipNoStagedFilesWarning: false,
ShowListFooter: true,
ShowCommandLog: true,
ShowFileTree: false,
ShowFileTree: true,
ShowRandomTip: true,
CommandLogSize: 8,
},
@@ -316,15 +337,13 @@ func GetDefaultConfig() *UserConfig {
ManualCommit: false,
Args: "",
},
Pull: PullConfig{
Mode: "merge",
},
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",
DisableForcePushing: false,
CommitPrefixes: map[string]CommitPrefixConfig(nil),
ParseEmoji: false,
},
Refresher: RefresherConfig{
RefreshInterval: 10,
@@ -359,6 +378,7 @@ func GetDefaultConfig() *UserConfig {
NextBlockAlt: "l",
PrevBlockAlt2: "<backtab>",
NextBlockAlt2: "<tab>",
JumpToBlock: []string{"1", "2", "3", "4", "5"},
NextMatch: "n",
PrevMatch: "N",
StartSearch: "/",
@@ -372,6 +392,7 @@ func GetDefaultConfig() *UserConfig {
New: "n",
Edit: "e",
OpenFile: "o",
OpenRecentRepos: "<c-r>",
ScrollUpMain: "<pgup>",
ScrollDownMain: "<pgdown>",
ScrollUpMainAlt1: "K",
@@ -397,6 +418,7 @@ func GetDefaultConfig() *UserConfig {
SubmitEditorText: "<enter>",
AppendNewline: "<a-enter>",
ExtrasMenu: "@",
ToggleWhitespaceInDiffView: "<c-w>",
},
Status: KeybindingStatusConfig{
CheckForUpdate: "u",
@@ -417,10 +439,12 @@ func GetDefaultConfig() *UserConfig {
Fetch: "f",
ToggleTreeView: "`",
OpenMergeTool: "M",
OpenStatusFilter: "<c-b>",
},
Branches: KeybindingBranchesConfig{
CopyPullRequestURL: "<c-y>",
CreatePullRequest: "o",
ViewPullRequestOptions: "O",
CheckoutBranchByName: "c",
ForceCheckoutBranch: "F",
RebaseBranch: "r",

View File

@@ -210,7 +210,7 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
// The stash window by default only contains one line so that it's not hogging
// too much space, but if you access it it should take up some space. This is
// the default behaviour when accordian mode is NOT in effect. If it is in effect
// the default behaviour when accordion mode is NOT in effect. If it is in effect
// then when it's accessed it will have weight 2, not 1.
func (gui *Gui) getDefaultStashWindowBox() *boxlayout.Box {
gui.State.ContextManager.RLock()
@@ -259,9 +259,9 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
fullHeightBox("stash"),
}
} else if height >= 28 {
accordianMode := gui.Config.GetUserConfig().Gui.ExpandFocusedSidePanel
accordianBox := func(defaultBox *boxlayout.Box) *boxlayout.Box {
if accordianMode && defaultBox.Window == currentWindow {
accordionMode := gui.Config.GetUserConfig().Gui.ExpandFocusedSidePanel
accordionBox := func(defaultBox *boxlayout.Box) *boxlayout.Box {
if accordionMode && defaultBox.Window == currentWindow {
return &boxlayout.Box{
Window: defaultBox.Window,
Weight: 2,
@@ -276,10 +276,10 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
Window: "status",
Size: 3,
},
accordianBox(&boxlayout.Box{Window: "files", Weight: 1}),
accordianBox(&boxlayout.Box{Window: "branches", Weight: 1}),
accordianBox(&boxlayout.Box{Window: "commits", Weight: 1}),
accordianBox(gui.getDefaultStashWindowBox()),
accordionBox(&boxlayout.Box{Window: "files", Weight: 1}),
accordionBox(&boxlayout.Box{Window: "branches", Weight: 1}),
accordionBox(&boxlayout.Box{Window: "commits", Weight: 1}),
accordionBox(gui.getDefaultStashWindowBox()),
}
} else {
squashedHeight := 1

View File

@@ -7,8 +7,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -91,23 +89,25 @@ func (gui *Gui) handleBranchPress() error {
}
func (gui *Gui) handleCreatePullRequestPress() error {
pullRequest := commands.NewPullRequest(gui.GitCommand)
branch := gui.getSelectedBranch()
url, err := pullRequest.Create(branch)
if err != nil {
return gui.surfaceError(err)
}
gui.OnRunCommand(oscommands.NewCmdLogEntry(fmt.Sprintf("Creating pull request at URL: %s", url), "Create pull request", false))
return gui.createPullRequest(branch.Name, "")
}
return nil
func (gui *Gui) handleCreatePullRequestMenu() error {
selectedBranch := gui.getSelectedBranch()
if selectedBranch == nil {
return nil
}
checkedOutBranch := gui.getCheckedOutBranch()
return gui.createPullRequestMenu(selectedBranch, checkedOutBranch)
}
func (gui *Gui) handleCopyPullRequestURLPress() error {
pullRequest := commands.NewPullRequest(gui.GitCommand)
branch := gui.getSelectedBranch()
url, err := pullRequest.CopyURL(branch)
url, err := pullRequest.CopyURL(branch.Name, "")
if err != nil {
return gui.surfaceError(err)
}
@@ -218,7 +218,7 @@ func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions)
func (gui *Gui) handleCheckoutByName() error {
return gui.prompt(promptOpts{
title: gui.Tr.BranchName + ":",
findSuggestionsFunc: gui.findBranchNameSuggestions,
findSuggestionsFunc: gui.getRefsSuggestionsFunc(),
handleConfirm: func(response string) error {
return gui.handleCheckoutRef(response, handleCheckoutRefOptions{
span: "Checkout branch",
@@ -296,7 +296,7 @@ func (gui *Gui) deleteNamedBranch(selectedBranch *models.Branch, force bool) err
handleConfirm: func() error {
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.DeleteBranch).DeleteBranch(selectedBranch.Name, force); err != nil {
errMessage := err.Error()
if !force && strings.Contains(errMessage, "is not fully merged") {
if !force && strings.Contains(errMessage, "git branch -D ") {
return gui.deleteNamedBranch(selectedBranch, true)
}
return gui.createErrorPanel(errMessage)
@@ -379,16 +379,14 @@ func (gui *Gui) handleRebaseOntoBranch(selectedBranchName string) error {
func (gui *Gui) handleFastForward() error {
branch := gui.getSelectedBranch()
if branch == nil {
if branch == nil || !branch.IsRealBranch() {
return nil
}
if branch.Pushables == "" {
return nil
}
if branch.Pushables == "?" {
if !branch.IsTrackingRemote() {
return gui.createErrorPanel(gui.Tr.FwdNoUpstream)
}
if branch.Pushables != "0" {
if branch.HasCommitsToPush() {
return gui.createErrorPanel(gui.Tr.FwdCommitsToPush)
}
@@ -414,7 +412,7 @@ func (gui *Gui) handleFastForward() error {
_ = gui.createLoaderPanel(message)
if gui.State.Panels.Branches.SelectedLineIdx == 0 {
_ = gui.pullWithMode("ff-only", PullFilesOptions{span: span})
_ = gui.pullWithLock(PullFilesOptions{span: span, FastForwardOnly: true})
} else {
err := gui.GitCommand.WithSpan(span).FastForward(branch.Name, remoteName, remoteBranchName, gui.promptUserForCredential)
gui.handleCredentialsPopup(err)
@@ -435,7 +433,7 @@ func (gui *Gui) handleCreateResetToBranchMenu() error {
func (gui *Gui) handleRenameBranch() error {
branch := gui.getSelectedBranch()
if branch == nil {
if branch == nil || !branch.IsRealBranch() {
return nil
}
@@ -469,8 +467,7 @@ func (gui *Gui) handleRenameBranch() error {
// I could do an explicit check here for whether the branch is tracking a remote branch
// but if we've selected it we'll already know that via Pullables and Pullables.
// Bit of a hack but I'm lazy.
notTrackingRemote := branch.Pullables == "?"
if notTrackingRemote {
if !branch.IsTrackingRemote() {
return promptForNewName()
}
@@ -505,8 +502,8 @@ func (gui *Gui) handleNewBranchOffCurrentItem() error {
prefilledName := ""
if context.GetKey() == REMOTE_BRANCHES_CONTEXT_KEY {
// will set to the remote's existing name
prefilledName = item.ID()
// will set to the remote's branch name without the remote name
prefilledName = strings.SplitAfterN(item.ID(), "/", 2)[1]
}
return gui.prompt(promptOpts{
@@ -536,32 +533,6 @@ func (gui *Gui) handleNewBranchOffCurrentItem() error {
})
}
func (gui *Gui) getBranchNames() []string {
result := make([]string, len(gui.State.Branches))
for i, branch := range gui.State.Branches {
result[i] = branch.Name
}
return result
}
func (gui *Gui) findBranchNameSuggestions(input string) []*types.Suggestion {
branchNames := gui.getBranchNames()
matchingBranchNames := utils.FuzzySearch(sanitizedBranchName(input), branchNames)
suggestions := make([]*types.Suggestion, len(matchingBranchNames))
for i, branchName := range matchingBranchNames {
suggestions[i] = &types.Suggestion{
Value: branchName,
Label: utils.ColoredString(branchName, presentation.GetBranchColor(branchName)),
}
}
return suggestions
}
// sanitizedBranchName will remove all spaces in favor of a dash "-" to meet
// git's branch naming requirement.
func sanitizedBranchName(input string) string {

View File

@@ -5,11 +5,11 @@ import "github.com/jesseduffield/lazygit/pkg/commands/models"
// you can only copy from one context at a time, because the order and position of commits matter
func (gui *Gui) resetCherryPickingIfNecessary(context Context) error {
oldContextKey := gui.State.Modes.CherryPicking.ContextKey
oldContextKey := ContextKey(gui.State.Modes.CherryPicking.ContextKey)
if oldContextKey != context.GetKey() {
// need to reset the cherry picking mode
gui.State.Modes.CherryPicking.ContextKey = context.GetKey()
gui.State.Modes.CherryPicking.ContextKey = string(context.GetKey())
gui.State.Modes.CherryPicking.CherryPickedCommits = make([]*models.Commit, 0)
return gui.rerenderContextViewIfPresent(oldContextKey)
@@ -156,7 +156,7 @@ func (gui *Gui) HandlePasteCommits() error {
}
func (gui *Gui) exitCherryPickingMode() error {
contextKey := gui.State.Modes.CherryPicking.ContextKey
contextKey := ContextKey(gui.State.Modes.CherryPicking.ContextKey)
gui.State.Modes.CherryPicking.ContextKey = ""
gui.State.Modes.CherryPicking.CherryPickedCommits = nil

View File

@@ -6,11 +6,10 @@ import (
"strings"
"time"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/constants"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) GetOnRunCommand() func(entry oscommands.CmdLogEntry) {
@@ -25,17 +24,17 @@ func (gui *Gui) GetOnRunCommand() func(entry oscommands.CmdLogEntry) {
gui.Views.Extras.Autoscroll = true
if entry.GetSpan() != currentSpan {
fmt.Fprint(gui.Views.Extras, "\n"+utils.ColoredString(entry.GetSpan(), color.FgYellow))
fmt.Fprint(gui.Views.Extras, "\n"+style.FgYellow.Sprint(entry.GetSpan()))
currentSpan = entry.GetSpan()
}
clrAttr := theme.DefaultTextColor
textStyle := theme.DefaultTextColor
if !entry.GetCommandLine() {
clrAttr = color.FgMagenta
textStyle = style.FgMagenta
}
gui.CmdLog = append(gui.CmdLog, entry.GetCmdStr())
indentedCmdStr := " " + strings.Replace(entry.GetCmdStr(), "\n", "\n ", -1)
fmt.Fprint(gui.Views.Extras, "\n"+utils.ColoredString(indentedCmdStr, clrAttr))
fmt.Fprint(gui.Views.Extras, "\n"+textStyle.Sprint(indentedCmdStr))
}
}
@@ -44,14 +43,14 @@ func (gui *Gui) printCommandLogHeader() {
gui.Tr.CommandLogHeader,
gui.getKeyDisplay(gui.Config.GetUserConfig().Keybinding.Universal.ExtrasMenu),
)
fmt.Fprintln(gui.Views.Extras, utils.ColoredString(introStr, color.FgCyan))
fmt.Fprintln(gui.Views.Extras, style.FgCyan.Sprint(introStr))
if gui.Config.GetUserConfig().Gui.ShowRandomTip {
fmt.Fprintf(
gui.Views.Extras,
"%s: %s",
utils.ColoredString(gui.Tr.RandomTip, color.FgYellow),
utils.ColoredString(gui.getRandomTip(), color.FgGreen),
style.FgYellow.Sprint(gui.Tr.RandomTip),
style.FgGreen.Sprint(gui.getRandomTip()),
)
}
}
@@ -102,7 +101,7 @@ func (gui *Gui) getRandomTip() string {
formattedKey(config.Universal.GoInto),
),
fmt.Sprintf(
"You can diff two commits by pressing '%s' one one commit and then navigating to the other. You can then press '%s' to view the files of the diff",
"You can diff two commits by pressing '%s' on one commit and then navigating to the other. You can then press '%s' to view the files of the diff",
formattedKey(config.Universal.DiffingMenu),
formattedKey(config.Universal.GoInto),
),
@@ -174,7 +173,7 @@ func (gui *Gui) getRandomTip() string {
constants.Links.Docs.CustomCommands,
),
fmt.Sprintf(
"If you ever find a bug, do not hesistate to raise an issue on the repo:\n%s",
"If you ever find a bug, do not hesitate to raise an issue on the repo:\n%s",
constants.Links.Issues,
),
}

View File

@@ -10,7 +10,7 @@ import (
)
func (gui *Gui) handleCommitConfirm() error {
message := gui.trimmedContent(gui.Views.CommitMessage)
message := strings.TrimSpace(gui.Views.CommitMessage.TextArea.GetContent())
if message == "" {
return gui.createErrorPanel(gui.Tr.CommitWithoutMessageErr)
}
@@ -23,8 +23,8 @@ func (gui *Gui) handleCommitConfirm() error {
cmdStr := gui.GitCommand.CommitCmdStr(message, flags)
gui.OnRunCommand(oscommands.NewCmdLogEntry(cmdStr, gui.Tr.Spans.Commit, true))
return gui.withGpgHandling(cmdStr, gui.Tr.CommittingStatus, func() error {
gui.Views.CommitMessage.ClearTextArea()
_ = gui.returnFromContext()
gui.clearEditorView(gui.Views.CommitMessage)
return nil
})
}
@@ -48,7 +48,7 @@ func (gui *Gui) handleCommitMessageFocused() error {
}
func (gui *Gui) getBufferLength(view *gocui.View) string {
return " " + strconv.Itoa(strings.Count(view.Buffer(), "")-1) + " "
return " " + strconv.Itoa(strings.Count(view.TextArea.GetContent(), "")-1) + " "
}
// RenderCommitLength is a function.

View File

@@ -461,9 +461,43 @@ func (gui *Gui) handleCommitRevert() error {
return err
}
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.RevertCommit).Revert(gui.State.Commits[gui.State.Panels.Commits.SelectedLineIdx].Sha); err != nil {
return gui.surfaceError(err)
commit := gui.getSelectedLocalCommit()
if commit.IsMerge() {
return gui.createRevertMergeCommitMenu(commit)
} else {
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.RevertCommit).Revert(commit.Sha); err != nil {
return gui.surfaceError(err)
}
return gui.afterRevertCommit()
}
}
func (gui *Gui) createRevertMergeCommitMenu(commit *models.Commit) error {
menuItems := make([]*menuItem, len(commit.Parents))
for i, parentSha := range commit.Parents {
i := i
message, err := gui.GitCommand.GetCommitMessageFirstLine(parentSha)
if err != nil {
return gui.surfaceError(err)
}
menuItems[i] = &menuItem{
displayString: fmt.Sprintf("%s: %s", utils.SafeTruncate(parentSha, 8), message),
onPress: func() error {
parentNumber := i + 1
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.RevertCommit).RevertMerge(commit.Sha, parentNumber); err != nil {
return gui.surfaceError(err)
}
return gui.afterRevertCommit()
},
}
}
return gui.createMenu(gui.Tr.SelectParentCommitForMerge, menuItems, createMenuOptions{showCancel: true})
}
func (gui *Gui) afterRevertCommit() error {
gui.State.Panels.Commits.SelectedLineIdx++
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI, scope: []RefreshableView{COMMITS, BRANCHES}})
}

View File

@@ -1,16 +1,13 @@
// lots of this has been directly ported from one of the example files, will brush up later
// Copyright 2014 The gocui Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gui
import (
"fmt"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
@@ -37,7 +34,6 @@ type askOpts struct {
handleConfirm func() error
handleClose func() error
handlersManageFocus bool
findSuggestionsFunc func(string) []*types.Suggestion
}
type promptOpts struct {
@@ -54,7 +50,6 @@ func (gui *Gui) ask(opts askOpts) error {
handleConfirm: opts.handleConfirm,
handleClose: opts.handleClose,
handlersManageFocus: opts.handlersManageFocus,
findSuggestionsFunc: opts.findSuggestionsFunc,
})
}
@@ -107,13 +102,6 @@ func (gui *Gui) wrappedPromptConfirmationFunction(handlersManageFocus bool, func
}
}
func (gui *Gui) clearConfirmationViewKeyBindings() {
keybindingConfig := gui.Config.GetUserConfig().Keybinding
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.Confirm), gocui.ModNone)
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone)
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.Return), gocui.ModNone)
}
func (gui *Gui) closeConfirmationPrompt(handlersManageFocus bool) error {
// we've already closed it so we can just return
if !gui.Views.Confirmation.Visible {
@@ -169,7 +157,13 @@ func (gui *Gui) getConfirmationPanelDimensions(wrap bool, prompt string) (int, i
height/2 + panelHeight/2
}
func (gui *Gui) prepareConfirmationPanel(title, prompt string, hasLoader bool, findSuggestionsFunc func(string) []*types.Suggestion) error {
func (gui *Gui) prepareConfirmationPanel(
title,
prompt string,
hasLoader bool,
findSuggestionsFunc func(string) []*types.Suggestion,
editable bool,
) error {
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(true, prompt)
// calling SetView on an existing view returns the same view, so I'm not bothering
// to reassign to gui.Views.Confirmation
@@ -182,20 +176,22 @@ func (gui *Gui) prepareConfirmationPanel(title, prompt string, hasLoader bool, f
gui.g.StartTicking()
}
gui.Views.Confirmation.Title = title
gui.Views.Confirmation.Wrap = true
// for now we do not support wrapping in our editor
gui.Views.Confirmation.Wrap = !editable
gui.Views.Confirmation.FgColor = theme.GocuiDefaultTextColor
gui.findSuggestions = findSuggestionsFunc
if findSuggestionsFunc != nil {
suggestionsViewHeight := 11
suggestionsView, err := gui.g.SetView("suggestions", x0, y1, x1, y1+suggestionsViewHeight, 0)
suggestionsView, err := gui.g.SetView("suggestions", x0, y1+1, x1, y1+suggestionsViewHeight, 0)
if err != nil {
return err
}
suggestionsView.Wrap = true
suggestionsView.Wrap = false
suggestionsView.FgColor = theme.GocuiDefaultTextColor
gui.setSuggestions([]*types.Suggestion{})
gui.setSuggestions(findSuggestionsFunc(""))
suggestionsView.Visible = true
suggestionsView.Title = fmt.Sprintf(gui.Tr.SuggestionsTitle, gui.Config.GetUserConfig().Keybinding.Universal.TogglePanel)
}
gui.g.Update(func(g *gocui.Gui) error {
@@ -209,19 +205,27 @@ func (gui *Gui) createPopupPanel(opts createPopupPanelOpts) error {
// remove any previous keybindings
gui.clearConfirmationViewKeyBindings()
err := gui.prepareConfirmationPanel(opts.title, opts.prompt, opts.hasLoader, opts.findSuggestionsFunc)
err := gui.prepareConfirmationPanel(
opts.title,
opts.prompt,
opts.hasLoader,
opts.findSuggestionsFunc,
opts.editable,
)
if err != nil {
return err
}
gui.Views.Confirmation.Editable = opts.editable
gui.Views.Confirmation.Editor = gocui.EditorFunc(gui.defaultEditor)
confirmationView := gui.Views.Confirmation
confirmationView.Editable = opts.editable
confirmationView.Editor = gocui.EditorFunc(gui.defaultEditor)
if opts.editable {
if err := gui.Views.Confirmation.SetEditorContent(opts.prompt); err != nil {
return err
}
textArea := confirmationView.TextArea
textArea.Clear()
textArea.TypeString(opts.prompt)
confirmationView.RenderTextArea()
} else {
if err := gui.renderStringSync(gui.Views.Confirmation, opts.prompt); err != nil {
if err := gui.renderStringSync(confirmationView, opts.prompt); err != nil {
return err
}
}
@@ -243,7 +247,7 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
gui.renderString(gui.Views.Options, actions)
var onConfirm func() error
if opts.handleConfirmPrompt != nil {
onConfirm = gui.wrappedPromptConfirmationFunction(opts.handlersManageFocus, opts.handleConfirmPrompt, func() string { return gui.Views.Confirmation.Buffer() })
onConfirm = gui.wrappedPromptConfirmationFunction(opts.handlersManageFocus, opts.handleConfirmPrompt, func() string { return gui.Views.Confirmation.TextArea.GetContent() })
} else {
onConfirm = gui.wrappedConfirmationFunction(opts.handlersManageFocus, opts.handleConfirm)
}
@@ -255,7 +259,11 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
}
keybindingConfig := gui.Config.GetUserConfig().Keybinding
onSuggestionConfirm := gui.wrappedPromptConfirmationFunction(opts.handlersManageFocus, opts.handleConfirmPrompt, func() string { return gui.getSelectedSuggestionValue() })
onSuggestionConfirm := gui.wrappedPromptConfirmationFunction(
opts.handlersManageFocus,
opts.handleConfirmPrompt,
gui.getSelectedSuggestionValue,
)
confirmationKeybindings := []confirmationKeybinding{
{
@@ -276,7 +284,12 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
{
viewName: "confirmation",
key: gui.getKey(keybindingConfig.Universal.TogglePanel),
handler: func() error { return gui.replaceContext(gui.State.Contexts.Suggestions) },
handler: func() error {
if len(gui.State.Suggestions) > 0 {
return gui.replaceContext(gui.State.Contexts.Suggestions)
}
return nil
},
},
{
viewName: "suggestions",
@@ -309,6 +322,16 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
return nil
}
func (gui *Gui) clearConfirmationViewKeyBindings() {
keybindingConfig := gui.Config.GetUserConfig().Keybinding
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.Confirm), gocui.ModNone)
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone)
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.Return), gocui.ModNone)
_ = gui.g.DeleteKeybinding("suggestions", gui.getKey(keybindingConfig.Universal.Confirm), gocui.ModNone)
_ = gui.g.DeleteKeybinding("suggestions", gui.getKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone)
_ = gui.g.DeleteKeybinding("suggestions", gui.getKey(keybindingConfig.Universal.Return), gocui.ModNone)
}
func (gui *Gui) wrappedHandler(f func() error) func(g *gocui.Gui, v *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
return f()
@@ -316,8 +339,7 @@ func (gui *Gui) wrappedHandler(f func() error) func(g *gocui.Gui, v *gocui.View)
}
func (gui *Gui) createErrorPanel(message string) error {
colorFunction := color.New(color.FgRed).SprintFunc()
coloredMessage := colorFunction(strings.TrimSpace(message))
coloredMessage := style.FgRed.Sprint(strings.TrimSpace(message))
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil {
return err
}

View File

@@ -128,33 +128,37 @@ func (gui *Gui) pushContextWithView(viewName string) error {
func (gui *Gui) returnFromContext() error {
gui.g.Update(func(*gocui.Gui) error {
gui.State.ContextManager.Lock()
if len(gui.State.ContextManager.ContextStack) == 1 {
// cannot escape from bottommost context
gui.State.ContextManager.Unlock()
return nil
}
n := len(gui.State.ContextManager.ContextStack) - 1
currentContext := gui.State.ContextManager.ContextStack[n]
newContext := gui.State.ContextManager.ContextStack[n-1]
gui.State.ContextManager.ContextStack = gui.State.ContextManager.ContextStack[:n]
gui.State.ContextManager.Unlock()
if err := gui.deactivateContext(currentContext); err != nil {
return err
}
return gui.activateContext(newContext)
return gui.returnFromContextSync()
})
return nil
}
func (gui *Gui) returnFromContextSync() error {
gui.State.ContextManager.Lock()
if len(gui.State.ContextManager.ContextStack) == 1 {
// cannot escape from bottommost context
gui.State.ContextManager.Unlock()
return nil
}
n := len(gui.State.ContextManager.ContextStack) - 1
currentContext := gui.State.ContextManager.ContextStack[n]
newContext := gui.State.ContextManager.ContextStack[n-1]
gui.State.ContextManager.ContextStack = gui.State.ContextManager.ContextStack[:n]
gui.State.ContextManager.Unlock()
if err := gui.deactivateContext(currentContext); err != nil {
return err
}
return gui.activateContext(newContext)
}
func (gui *Gui) deactivateContext(c Context) error {
view, _ := gui.g.View(c.GetViewName())

View File

@@ -41,9 +41,9 @@ func (gui *Gui) promptUserForCredential(passOrUname string) string {
func (gui *Gui) handleSubmitCredential() error {
credentialsView := gui.Views.Credentials
message := gui.trimmedContent(credentialsView)
message := strings.TrimSpace(credentialsView.TextArea.GetContent())
gui.credentials <- message
gui.clearEditorView(credentialsView)
credentialsView.ClearTextArea()
if err := gui.returnFromContext(); err != nil {
return err
}

View File

@@ -1,13 +1,18 @@
package gui
import (
"bytes"
"errors"
"log"
"regexp"
"strconv"
"strings"
"text/template"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -28,6 +33,11 @@ type CustomCommandObjects struct {
PromptResponses []string
}
type commandMenuEntry struct {
label string
value string
}
func (gui *Gui) resolveTemplate(templateStr string, promptResponses []string) (string, error) {
objects := CustomCommandObjects{
SelectedFile: gui.getSelectedFile(),
@@ -49,6 +59,180 @@ func (gui *Gui) resolveTemplate(templateStr string, promptResponses []string) (s
return utils.ResolveTemplate(templateStr, objects)
}
func (gui *Gui) inputPrompt(prompt config.CustomCommandPrompt, promptResponses []string, responseIdx int, wrappedF func() error) error {
title, err := gui.resolveTemplate(prompt.Title, promptResponses)
if err != nil {
return gui.surfaceError(err)
}
initialValue, err := gui.resolveTemplate(prompt.InitialValue, promptResponses)
if err != nil {
return gui.surfaceError(err)
}
return gui.prompt(promptOpts{
title: title,
initialContent: initialValue,
handleConfirm: func(str string) error {
promptResponses[responseIdx] = str
return wrappedF()
},
})
}
func (gui *Gui) menuPrompt(prompt config.CustomCommandPrompt, promptResponses []string, responseIdx int, wrappedF func() error) error {
// need to make a menu here some how
menuItems := make([]*menuItem, len(prompt.Options))
for i, option := range prompt.Options {
option := option
nameTemplate := option.Name
if nameTemplate == "" {
// this allows you to only pass values rather than bother with names/descriptions
nameTemplate = option.Value
}
name, err := gui.resolveTemplate(nameTemplate, promptResponses)
if err != nil {
return gui.surfaceError(err)
}
description, err := gui.resolveTemplate(option.Description, promptResponses)
if err != nil {
return gui.surfaceError(err)
}
value, err := gui.resolveTemplate(option.Value, promptResponses)
if err != nil {
return gui.surfaceError(err)
}
menuItems[i] = &menuItem{
displayStrings: []string{name, style.FgYellow.Sprint(description)},
onPress: func() error {
promptResponses[responseIdx] = value
return wrappedF()
},
}
}
title, err := gui.resolveTemplate(prompt.Title, promptResponses)
if err != nil {
return gui.surfaceError(err)
}
return gui.createMenu(title, menuItems, createMenuOptions{showCancel: true})
}
func (gui *Gui) GenerateMenuCandidates(commandOutput, filter, valueFormat, labelFormat string) ([]commandMenuEntry, error) {
reg, err := regexp.Compile(filter)
if err != nil {
return nil, gui.surfaceError(errors.New("unable to parse filter regex, error: " + err.Error()))
}
buff := bytes.NewBuffer(nil)
valueTemp, err := template.New("format").Parse(valueFormat)
if err != nil {
return nil, gui.surfaceError(errors.New("unable to parse value format, error: " + err.Error()))
}
colorFuncMap := style.TemplateFuncMapAddColors(template.FuncMap{})
descTemp, err := template.New("format").Funcs(colorFuncMap).Parse(labelFormat)
if err != nil {
return nil, gui.surfaceError(errors.New("unable to parse label format, error: " + err.Error()))
}
candidates := []commandMenuEntry{}
for _, str := range strings.Split(string(commandOutput), "\n") {
if str == "" {
continue
}
tmplData := map[string]string{}
out := reg.FindAllStringSubmatch(str, -1)
if len(out) > 0 {
for groupIdx, group := range reg.SubexpNames() {
// Record matched group with group ids
matchName := "group_" + strconv.Itoa(groupIdx)
tmplData[matchName] = out[0][groupIdx]
// Record last named group non-empty matches as group matches
if group != "" {
tmplData[group] = out[0][groupIdx]
}
}
}
err = valueTemp.Execute(buff, tmplData)
if err != nil {
return candidates, gui.surfaceError(err)
}
entry := commandMenuEntry{
value: strings.TrimSpace(buff.String()),
}
if labelFormat != "" {
buff.Reset()
err = descTemp.Execute(buff, tmplData)
if err != nil {
return candidates, gui.surfaceError(err)
}
entry.label = strings.TrimSpace(buff.String())
} else {
entry.label = entry.value
}
candidates = append(candidates, entry)
buff.Reset()
}
return candidates, err
}
func (gui *Gui) menuPromptFromCommand(prompt config.CustomCommandPrompt, promptResponses []string, responseIdx int, wrappedF func() error) error {
// Collect cmd to run from config
cmdStr, err := gui.resolveTemplate(prompt.Command, promptResponses)
if err != nil {
return gui.surfaceError(err)
}
// Collect Filter regexp
filter, err := gui.resolveTemplate(prompt.Filter, promptResponses)
if err != nil {
return gui.surfaceError(err)
}
// Run and save output
message, err := gui.GitCommand.RunCommandWithOutput(cmdStr)
if err != nil {
return gui.surfaceError(err)
}
// Need to make a menu out of what the cmd has displayed
candidates, err := gui.GenerateMenuCandidates(message, filter, prompt.ValueFormat, prompt.LabelFormat)
if err != nil {
return gui.surfaceError(err)
}
menuItems := make([]*menuItem, len(candidates))
for i := range candidates {
menuItems[i] = &menuItem{
displayStrings: []string{candidates[i].label},
onPress: func() error {
promptResponses[responseIdx] = candidates[i].value
return wrappedF()
},
}
}
title, err := gui.resolveTemplate(prompt.Title, promptResponses)
if err != nil {
return gui.surfaceError(err)
}
return gui.createMenu(title, menuItems, createMenuOptions{showCancel: true})
}
func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand) func() error {
return func() error {
promptResponses := make([]string, len(customCommand.Prompts))
@@ -89,72 +273,18 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
switch prompt.Type {
case "input":
f = func() error {
title, err := gui.resolveTemplate(prompt.Title, promptResponses)
if err != nil {
return gui.surfaceError(err)
}
initialValue, err := gui.resolveTemplate(prompt.InitialValue, promptResponses)
if err != nil {
return gui.surfaceError(err)
}
return gui.prompt(promptOpts{
title: title,
initialContent: initialValue,
handleConfirm: func(str string) error {
promptResponses[idx] = str
return wrappedF()
},
})
return gui.inputPrompt(prompt, promptResponses, idx, wrappedF)
}
case "menu":
f = func() error {
// need to make a menu here some how
menuItems := make([]*menuItem, len(prompt.Options))
for i, option := range prompt.Options {
option := option
nameTemplate := option.Name
if nameTemplate == "" {
// this allows you to only pass values rather than bother with names/descriptions
nameTemplate = option.Value
}
name, err := gui.resolveTemplate(nameTemplate, promptResponses)
if err != nil {
return gui.surfaceError(err)
}
description, err := gui.resolveTemplate(option.Description, promptResponses)
if err != nil {
return gui.surfaceError(err)
}
value, err := gui.resolveTemplate(option.Value, promptResponses)
if err != nil {
return gui.surfaceError(err)
}
menuItems[i] = &menuItem{
displayStrings: []string{name, utils.ColoredString(description, color.FgYellow)},
onPress: func() error {
promptResponses[idx] = value
return wrappedF()
},
}
}
title, err := gui.resolveTemplate(prompt.Title, promptResponses)
if err != nil {
return gui.surfaceError(err)
}
return gui.createMenu(title, menuItems, createMenuOptions{showCancel: true})
return gui.menuPrompt(prompt, promptResponses, idx, wrappedF)
}
case "menuFromCommand":
f = func() error {
return gui.menuPromptFromCommand(prompt, promptResponses, idx, wrappedF)
}
default:
return gui.createErrorPanel("custom command prompt must have a type of 'input' or 'menu'")
return gui.createErrorPanel("custom command prompt must have a type of 'input', 'menu' or 'menuFromCommand'")
}
}

View File

@@ -0,0 +1,63 @@
package gui
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGuiGenerateMenuCandidates(t *testing.T) {
type scenario struct {
testName string
cmdOut string
filter string
valueFormat string
labelFormat string
test func([]commandMenuEntry, error)
}
scenarios := []scenario{
{
"Extract remote branch name",
"upstream/pr-1",
"(?P<remote>[a-z_]+)/(?P<branch>.*)",
"{{ .branch }}",
"Remote: {{ .remote }}",
func(actualEntry []commandMenuEntry, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "pr-1", actualEntry[0].value)
assert.EqualValues(t, "Remote: upstream", actualEntry[0].label)
},
},
{
"Multiple named groups with empty labelFormat",
"upstream/pr-1",
"(?P<remote>[a-z]*)/(?P<branch>.*)",
"{{ .branch }}|{{ .remote }}",
"",
func(actualEntry []commandMenuEntry, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "pr-1|upstream", actualEntry[0].value)
assert.EqualValues(t, "pr-1|upstream", actualEntry[0].label)
},
},
{
"Multiple named groups with group ids",
"upstream/pr-1",
"(?P<remote>[a-z]*)/(?P<branch>.*)",
"{{ .group_2 }}|{{ .group_1 }}",
"Remote: {{ .group_1 }}",
func(actualEntry []commandMenuEntry, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "pr-1|upstream", actualEntry[0].value)
assert.EqualValues(t, "Remote: upstream", actualEntry[0].label)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
s.test(NewDummyGui().GenerateMenuCandidates(s.cmdOut, s.filter, s.valueFormat, s.labelFormat))
})
}
}

View File

@@ -3,10 +3,12 @@ package gui
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/gui/modes/diffing"
)
func (gui *Gui) exitDiffMode() error {
gui.State.Modes.Diffing = Diffing{}
gui.State.Modes.Diffing = diffing.New()
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}
@@ -123,7 +125,8 @@ func (gui *Gui) handleCreateDiffingMenuPanel() error {
displayString: gui.Tr.LcEnterRefToDiff,
onPress: func() error {
return gui.prompt(promptOpts{
title: gui.Tr.LcEnteRefName,
title: gui.Tr.LcEnteRefName,
findSuggestionsFunc: gui.getRefsSuggestionsFunc(),
handleConfirm: func(response string) error {
gui.State.Modes.Diffing.Ref = strings.TrimSpace(response)
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
@@ -145,7 +148,7 @@ func (gui *Gui) handleCreateDiffingMenuPanel() error {
{
displayString: gui.Tr.LcExitDiffMode,
onPress: func() error {
gui.State.Modes.Diffing = Diffing{}
gui.State.Modes.Diffing = diffing.New()
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
},
},

23
pkg/gui/dummies.go Normal file
View File

@@ -0,0 +1,23 @@
package gui
import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/updates"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// NewDummyGui creates a new dummy GUI for testing
func NewDummyUpdater() *updates.Updater {
newAppConfig := config.NewDummyAppConfig()
DummyUpdater, _ := updates.NewUpdater(utils.NewDummyLog(), newAppConfig, oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog(), newAppConfig.GetUserConfig().Gui.Language))
return DummyUpdater
}
func NewDummyGui() *Gui {
newAppConfig := config.NewDummyAppConfig()
DummyGui, _ := NewGui(utils.NewDummyLog(), commands.NewDummyGitCommand(), oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog(), newAppConfig.GetUserConfig().Gui.Language), newAppConfig, NewDummyUpdater(), "", false)
return DummyGui
}

View File

@@ -6,90 +6,81 @@ import (
"github.com/jesseduffield/gocui"
)
// we've just copy+pasted the editor from gocui to here so that we can also re-
// render the commit message length on each keypress
func (gui *Gui) commitMessageEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
func (gui *Gui) handleEditorKeypress(textArea *gocui.TextArea, key gocui.Key, ch rune, mod gocui.Modifier, allowMultiline bool) bool {
newlineKey, ok := gui.getKey(gui.Config.GetUserConfig().Keybinding.Universal.AppendNewline).(gocui.Key)
if !ok {
newlineKey = gocui.KeyAltEnter
}
matched := true
switch {
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
v.EditDelete(true)
textArea.BackSpaceChar()
case key == gocui.KeyCtrlD || key == gocui.KeyDelete:
v.EditDelete(false)
textArea.DeleteChar()
case key == gocui.KeyArrowDown:
v.MoveCursor(0, 1, false)
textArea.MoveCursorDown()
case key == gocui.KeyArrowUp:
v.MoveCursor(0, -1, false)
textArea.MoveCursorUp()
case key == gocui.KeyArrowLeft:
v.MoveCursor(-1, 0, false)
textArea.MoveCursorLeft()
case key == gocui.KeyArrowRight:
v.MoveCursor(1, 0, false)
textArea.MoveCursorRight()
case key == newlineKey:
v.EditNewLine()
if allowMultiline {
textArea.TypeRune('\n')
} else {
return false
}
case key == gocui.KeySpace:
v.EditWrite(' ')
textArea.TypeRune(' ')
case key == gocui.KeyInsert:
v.Overwrite = !v.Overwrite
textArea.ToggleOverwrite()
case key == gocui.KeyCtrlU:
v.EditDeleteToStartOfLine()
case key == gocui.KeyCtrlA:
v.EditGotoToStartOfLine()
case key == gocui.KeyCtrlE:
v.EditGotoToEndOfLine()
textArea.DeleteToStartOfLine()
case key == gocui.KeyCtrlA || key == gocui.KeyHome:
textArea.GoToStartOfLine()
case key == gocui.KeyCtrlE || key == gocui.KeyEnd:
textArea.GoToEndOfLine()
// TODO: see if we need all three of these conditions: maybe the final one is sufficient
case ch != 0 && mod == 0 && unicode.IsPrint(ch):
v.EditWrite(ch)
textArea.TypeRune(ch)
default:
matched = false
return false
}
return true
}
// we've just copy+pasted the editor from gocui to here so that we can also re-
// render the commit message length on each keypress
func (gui *Gui) commitMessageEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
matched := gui.handleEditorKeypress(v.TextArea, key, ch, mod, true)
// This function is called again on refresh as part of the general resize popup call,
// but we need to call it here so that when we go to render the text area it's not
// considered out of bounds to add a newline, meaning we can avoid unnecessary scrolling.
err := gui.resizePopupPanel(v, v.TextArea.GetContent())
if err != nil {
gui.Log.Error(err)
}
v.RenderTextArea()
gui.RenderCommitLength()
return matched
}
func (gui *Gui) defaultEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
matched := true
switch {
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
v.EditDelete(true)
case key == gocui.KeyCtrlD || key == gocui.KeyDelete:
v.EditDelete(false)
case key == gocui.KeyArrowDown:
v.MoveCursor(0, 1, false)
case key == gocui.KeyArrowUp:
v.MoveCursor(0, -1, false)
case key == gocui.KeyArrowLeft:
v.MoveCursor(-1, 0, false)
case key == gocui.KeyArrowRight:
v.MoveCursor(1, 0, false)
case key == gocui.KeySpace:
v.EditWrite(' ')
case key == gocui.KeyInsert:
v.Overwrite = !v.Overwrite
case key == gocui.KeyCtrlU:
v.EditDeleteToStartOfLine()
case key == gocui.KeyCtrlA:
v.EditGotoToStartOfLine()
case key == gocui.KeyCtrlE:
v.EditGotoToEndOfLine()
matched := gui.handleEditorKeypress(v.TextArea, key, ch, mod, false)
// TODO: see if we need all three of these conditions: maybe the final one is sufficient
case ch != 0 && mod == 0 && unicode.IsPrint(ch):
v.EditWrite(ch)
default:
matched = false
}
v.RenderTextArea()
if gui.findSuggestions != nil {
input := v.Buffer()
suggestions := gui.findSuggestions(input)
gui.setSuggestions(suggestions)
input := v.TextArea.GetContent()
gui.suggestionsAsyncHandler.Do(func() func() {
suggestions := gui.findSuggestions(input)
return func() { gui.setSuggestions(suggestions) }
})
}
return matched

View File

@@ -11,15 +11,16 @@ func (gui *Gui) handleCreateExtrasMenuPanel() error {
return err
}
}
gui.ShowExtrasWindow = !gui.ShowExtrasWindow
show := !gui.ShowExtrasWindow
gui.ShowExtrasWindow = show
gui.Config.GetAppState().HideCommandLog = !show
_ = gui.Config.SaveAppState()
return nil
},
},
{
displayString: gui.Tr.FocusCommandLog,
onPress: func() error {
return gui.handleFocusCommandLog()
},
onPress: gui.handleFocusCommandLog,
},
}

View File

@@ -11,7 +11,7 @@ import (
)
// macs for some bizarre reason cap the number of watchable files to 256.
// there's no obvious platform agonstic way to check the situation of the user's
// there's no obvious platform agnostic way to check the situation of the user's
// computer so we're just arbitrarily capping at 200. This isn't so bad because
// file watching is only really an added bonus for faster refreshing.
const MAX_WATCHED_FILES = 50

View File

@@ -57,13 +57,6 @@ func (gui *Gui) selectFile(alreadySelected bool) error {
}
if !alreadySelected {
// TODO: pull into update task interface
if err := gui.resetOrigin(gui.Views.Main); err != nil {
return err
}
if err := gui.resetOrigin(gui.Views.Secondary); err != nil {
return err
}
gui.takeOverMergeConflictScrolling()
}
@@ -71,7 +64,7 @@ func (gui *Gui) selectFile(alreadySelected bool) error {
return gui.refreshMergePanelWithLock()
}
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(node, false, !node.GetHasUnstagedChanges() && node.GetHasStagedChanges())
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(node, false, !node.GetHasUnstagedChanges() && node.GetHasStagedChanges(), gui.State.IgnoreWhitespaceInDiffView)
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
refreshOpts := refreshMainOpts{main: &viewUpdateOpts{
@@ -81,7 +74,7 @@ func (gui *Gui) selectFile(alreadySelected bool) error {
if node.GetHasUnstagedChanges() {
if node.GetHasStagedChanges() {
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(node, false, true)
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(node, false, true, gui.State.IgnoreWhitespaceInDiffView)
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
refreshOpts.secondary = &viewUpdateOpts{
@@ -347,9 +340,10 @@ func (gui *Gui) handleWIPCommitPress() error {
return gui.createErrorPanel(gui.Tr.SkipHookPrefixNotConfigured)
}
if err := gui.Views.CommitMessage.SetEditorContent(skipHookPrefix); err != nil {
return err
}
textArea := gui.Views.CommitMessage.TextArea
textArea.Clear()
textArea.TypeString(skipHookPrefix)
gui.Views.CommitMessage.RenderTextArea()
return gui.handleCommitPress()
}
@@ -473,14 +467,49 @@ func (gui *Gui) handleCommitEditorPress() error {
)
}
func (gui *Gui) handleStatusFilterPressed() error {
menuItems := []*menuItem{
{
displayString: gui.Tr.FilterStagedFiles,
onPress: func() error {
return gui.setStatusFiltering(filetree.DisplayStaged)
},
},
{
displayString: gui.Tr.FilterUnstagedFiles,
onPress: func() error {
return gui.setStatusFiltering(filetree.DisplayUnstaged)
},
},
{
displayString: gui.Tr.ResetCommitFilterState,
onPress: func() error {
return gui.setStatusFiltering(filetree.DisplayAll)
},
},
}
return gui.createMenu(gui.Tr.FilteringMenuTitle, menuItems, createMenuOptions{showCancel: true})
}
func (gui *Gui) setStatusFiltering(filter filetree.FileManagerDisplayFilter) error {
state := gui.State
state.FileManager.SetDisplayFilter(filter)
return gui.handleRefreshFiles()
}
func (gui *Gui) editFile(filename string) error {
cmdStr, err := gui.GitCommand.EditFileCmdStr(filename)
return gui.editFileAtLine(filename, 1)
}
func (gui *Gui) editFileAtLine(filename string, lineNumber int) error {
cmdStr, err := gui.GitCommand.EditFileCmdStr(filename, lineNumber)
if err != nil {
return gui.surfaceError(err)
}
return gui.runSubprocessWithSuspenseAndRefresh(
gui.OSCommand.WithSpan(gui.Tr.Spans.EditFile).PrepareShellSubProcess(cmdStr),
gui.OSCommand.WithSpan(gui.Tr.Spans.EditFile).ShellCommandFromString(cmdStr),
)
}
@@ -617,7 +646,7 @@ func (gui *Gui) handlePullFiles() error {
}
// if we have no upstream branch we need to set that first
if currentBranch.Pullables == "?" {
if !currentBranch.IsTrackingRemote() {
// see if we have this branch in our config with an upstream
conf, err := gui.GitCommand.Repo.Config()
if err != nil {
@@ -630,8 +659,9 @@ func (gui *Gui) handlePullFiles() error {
}
return gui.prompt(promptOpts{
title: gui.Tr.EnterUpstream,
initialContent: "origin/" + currentBranch.Name,
title: gui.Tr.EnterUpstream,
initialContent: "origin/" + currentBranch.Name,
findSuggestionsFunc: gui.getRemoteBranchesSuggestionsFunc("/"),
handleConfirm: func(upstream string) error {
if err := gui.GitCommand.SetUpstreamBranch(upstream); err != nil {
errorMessage := err.Error()
@@ -649,9 +679,10 @@ func (gui *Gui) handlePullFiles() error {
}
type PullFilesOptions struct {
RemoteName string
BranchName string
span string
RemoteName string
BranchName string
FastForwardOnly bool
span string
}
func (gui *Gui) pullFiles(opts PullFilesOptions) error {
@@ -659,55 +690,53 @@ func (gui *Gui) pullFiles(opts PullFilesOptions) error {
return err
}
mode := gui.Config.GetUserConfig().Git.Pull.Mode
// TODO: this doesn't look like a good idea. Why the goroutine?
go utils.Safe(func() { _ = gui.pullWithMode(mode, opts) })
go utils.Safe(func() { _ = gui.pullWithLock(opts) })
return nil
}
func (gui *Gui) pullWithMode(mode string, opts PullFilesOptions) error {
func (gui *Gui) pullWithLock(opts PullFilesOptions) error {
gui.Mutexes.FetchMutex.Lock()
defer gui.Mutexes.FetchMutex.Unlock()
gitCommand := gui.GitCommand.WithSpan(opts.span)
err := gitCommand.Fetch(
commands.FetchOptions{
err := gitCommand.Pull(
commands.PullOptions{
PromptUserForCredential: gui.promptUserForCredential,
RemoteName: opts.RemoteName,
BranchName: opts.BranchName,
FastForwardOnly: opts.FastForwardOnly,
},
)
gui.handleCredentialsPopup(err)
if err != nil {
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}
switch mode {
case "rebase":
err := gitCommand.RebaseBranch("FETCH_HEAD")
return gui.handleGenericMergeCommandResult(err)
case "merge":
err := gitCommand.Merge("FETCH_HEAD", commands.MergeOpts{})
return gui.handleGenericMergeCommandResult(err)
case "ff-only":
err := gitCommand.Merge("FETCH_HEAD", commands.MergeOpts{FastForwardOnly: true})
return gui.handleGenericMergeCommandResult(err)
default:
return gui.createErrorPanel(fmt.Sprintf("git pull mode '%s' unrecognised", mode))
if err == nil {
_ = gui.closeConfirmationPrompt(false)
}
return gui.handleGenericMergeCommandResult(err)
}
func (gui *Gui) pushWithForceFlag(force bool, upstream string, args string) error {
type pushOpts struct {
force bool
upstreamRemote string
upstreamBranch string
setUpstream bool
}
func (gui *Gui) push(opts pushOpts) error {
if err := gui.createLoaderPanel(gui.Tr.PushWait); err != nil {
return err
}
go utils.Safe(func() {
branchName := gui.getCheckedOutBranch().Name
err := gui.GitCommand.WithSpan(gui.Tr.Spans.Push).Push(branchName, force, upstream, args, gui.promptUserForCredential)
if err != nil && !force && strings.Contains(err.Error(), "Updates were rejected") {
err := gui.GitCommand.WithSpan(gui.Tr.Spans.Push).Push(commands.PushOpts{
Force: opts.force,
UpstreamRemote: opts.upstreamRemote,
UpstreamBranch: opts.upstreamBranch,
SetUpstream: opts.setUpstream,
PromptUserForCredential: gui.promptUserForCredential,
})
if err != nil && !opts.force && strings.Contains(err.Error(), "Updates were rejected") {
forcePushDisabled := gui.Config.GetUserConfig().Git.DisableForcePushing
if forcePushDisabled {
_ = gui.createErrorPanel(gui.Tr.UpdatesRejectedAndForcePushDisabled)
@@ -717,7 +746,10 @@ func (gui *Gui) pushWithForceFlag(force bool, upstream string, args string) erro
title: gui.Tr.ForcePush,
prompt: gui.Tr.ForcePushPrompt,
handleConfirm: func() error {
return gui.pushWithForceFlag(true, upstream, args)
newOpts := opts
newOpts.force = true
return gui.push(newOpts)
},
})
return
@@ -740,33 +772,60 @@ func (gui *Gui) pushFiles() error {
return nil
}
if currentBranch.Pullables == "?" {
// see if we have this branch in our config with an upstream
conf, err := gui.GitCommand.Repo.Config()
if currentBranch.IsTrackingRemote() {
if currentBranch.HasCommitsToPull() {
return gui.requestToForcePush()
} else {
return gui.push(pushOpts{})
}
} else {
// see if we have an upstream for this branch in our config
upstreamRemote, upstreamBranch, err := gui.upstreamForBranchInConfig(currentBranch.Name)
if err != nil {
return gui.surfaceError(err)
}
for branchName, branch := range conf.Branches {
if branchName == currentBranch.Name {
return gui.pushWithForceFlag(false, "", fmt.Sprintf("%s %s", branch.Remote, branchName))
}
if upstreamBranch != "" {
return gui.push(
pushOpts{
force: false,
upstreamRemote: upstreamRemote,
upstreamBranch: upstreamBranch,
},
)
}
if gui.GitCommand.PushToCurrent {
return gui.pushWithForceFlag(false, "", "--set-upstream")
return gui.push(pushOpts{setUpstream: true})
} else {
return gui.prompt(promptOpts{
title: gui.Tr.EnterUpstream,
initialContent: "origin " + currentBranch.Name,
handleConfirm: func(response string) error {
return gui.pushWithForceFlag(false, response, "")
title: gui.Tr.EnterUpstream,
initialContent: "origin " + currentBranch.Name,
findSuggestionsFunc: gui.getRemoteBranchesSuggestionsFunc(" "),
handleConfirm: func(upstream string) error {
var upstreamBranch, upstreamRemote string
split := strings.Split(upstream, " ")
if len(split) == 2 {
upstreamRemote = split[0]
upstreamBranch = split[1]
} else {
upstreamRemote = upstream
upstreamBranch = ""
}
return gui.push(pushOpts{
force: false,
upstreamRemote: upstreamRemote,
upstreamBranch: upstreamBranch,
setUpstream: true,
})
},
})
}
} else if currentBranch.Pullables == "0" {
return gui.pushWithForceFlag(false, "", "")
}
}
func (gui *Gui) requestToForcePush() error {
forcePushDisabled := gui.Config.GetUserConfig().Git.DisableForcePushing
if forcePushDisabled {
return gui.createErrorPanel(gui.Tr.ForcePushDisabled)
@@ -776,11 +835,26 @@ func (gui *Gui) pushFiles() error {
title: gui.Tr.ForcePush,
prompt: gui.Tr.ForcePushPrompt,
handleConfirm: func() error {
return gui.pushWithForceFlag(true, "", "")
return gui.push(pushOpts{force: true})
},
})
}
func (gui *Gui) upstreamForBranchInConfig(branchName string) (string, string, error) {
conf, err := gui.GitCommand.Repo.Config()
if err != nil {
return "", "", err
}
for configBranchName, configBranch := range conf.Branches {
if configBranchName == branchName {
return configBranch.Remote, configBranchName, nil
}
}
return "", "", nil
}
func (gui *Gui) handleSwitchToMerge() error {
file := gui.getSelectedFile()
if file == nil {
@@ -812,8 +886,21 @@ func (gui *Gui) anyFilesWithMergeConflicts() bool {
func (gui *Gui) handleCustomCommand() error {
return gui.prompt(promptOpts{
title: gui.Tr.CustomCommand,
title: gui.Tr.CustomCommand,
findSuggestionsFunc: gui.getCustomCommandsHistorySuggestionsFunc(),
handleConfirm: func(command string) error {
gui.Config.GetAppState().CustomCommandsHistory = utils.Limit(
utils.Uniq(
append(gui.Config.GetAppState().CustomCommandsHistory, command),
),
1000,
)
err := gui.Config.SaveAppState()
if err != nil {
gui.Log.Error(err)
}
gui.OnRunCommand(oscommands.NewCmdLogEntry(command, gui.Tr.Spans.CustomCommand, true))
return gui.runSubprocessWithSuspenseAndRefresh(
gui.OSCommand.PrepareShellSubProcess(command),

View File

@@ -54,23 +54,33 @@ func TestBuildTreeFromFiles(t *testing.T) {
name: "paths that can be compressed",
files: []*models.File{
{
Name: "dir1/a",
Name: "dir1/dir3/a",
},
{
Name: "dir2/b",
Name: "dir2/dir4/b",
},
},
expected: &FileNode{
Path: "",
Children: []*FileNode{
{
File: &models.File{Name: "dir1/a"},
Path: "dir1/a",
Path: "dir1/dir3",
Children: []*FileNode{
{
File: &models.File{Name: "dir1/dir3/a"},
Path: "dir1/dir3/a",
},
},
CompressionLevel: 1,
},
{
File: &models.File{Name: "dir2/b"},
Path: "dir2/b",
Path: "dir2/dir4",
Children: []*FileNode{
{
File: &models.File{Name: "dir2/dir4/b"},
Path: "dir2/dir4/b",
},
},
CompressionLevel: 1,
},
},
@@ -201,12 +211,12 @@ func TestBuildFlatTreeFromFiles(t *testing.T) {
{
File: &models.File{Name: "dir1/a"},
Path: "dir1/a",
CompressionLevel: 1,
CompressionLevel: 0,
},
{
File: &models.File{Name: "dir2/b"},
Path: "dir2/b",
CompressionLevel: 1,
CompressionLevel: 0,
},
},
},
@@ -351,23 +361,33 @@ func TestBuildTreeFromCommitFiles(t *testing.T) {
name: "paths that can be compressed",
files: []*models.CommitFile{
{
Name: "dir1/a",
Name: "dir1/dir3/a",
},
{
Name: "dir2/b",
Name: "dir2/dir4/b",
},
},
expected: &CommitFileNode{
Path: "",
Children: []*CommitFileNode{
{
File: &models.CommitFile{Name: "dir1/a"},
Path: "dir1/a",
Path: "dir1/dir3",
Children: []*CommitFileNode{
{
File: &models.CommitFile{Name: "dir1/dir3/a"},
Path: "dir1/dir3/a",
},
},
CompressionLevel: 1,
},
{
File: &models.CommitFile{Name: "dir2/b"},
Path: "dir2/b",
Path: "dir2/dir4",
Children: []*CommitFileNode{
{
File: &models.CommitFile{Name: "dir2/dir4/b"},
Path: "dir2/dir4/b",
},
},
CompressionLevel: 1,
},
},
@@ -464,12 +484,12 @@ func TestBuildFlatTreeFromCommitFiles(t *testing.T) {
{
File: &models.CommitFile{Name: "dir1/a"},
Path: "dir1/a",
CompressionLevel: 1,
CompressionLevel: 0,
},
{
File: &models.CommitFile{Name: "dir2/b"},
Path: "dir2/b",
CompressionLevel: 1,
CompressionLevel: 0,
},
},
},

View File

@@ -8,11 +8,20 @@ import (
"github.com/sirupsen/logrus"
)
type FileManagerDisplayFilter int
const (
DisplayAll FileManagerDisplayFilter = iota
DisplayStaged
DisplayUnstaged
)
type FileManager struct {
files []*models.File
tree *FileNode
showTree bool
log *logrus.Entry
filter FileManagerDisplayFilter
collapsedPaths CollapsedPaths
sync.RWMutex
}
@@ -22,6 +31,7 @@ func NewFileManager(files []*models.File, log *logrus.Entry, showTree bool) *Fil
files: files,
log: log,
showTree: showTree,
filter: DisplayAll,
collapsedPaths: CollapsedPaths{},
RWMutex: sync.RWMutex{},
}
@@ -35,6 +45,35 @@ func (m *FileManager) ExpandToPath(path string) {
m.collapsedPaths.ExpandToPath(path)
}
func (m *FileManager) GetFilesForDisplay() []*models.File {
files := m.files
if m.filter == DisplayAll {
return files
}
result := make([]*models.File, 0)
if m.filter == DisplayStaged {
for _, file := range files {
if file.HasStagedChanges {
result = append(result, file)
}
}
} else {
for _, file := range files {
if !file.HasStagedChanges {
result = append(result, file)
}
}
}
return result
}
func (m *FileManager) SetDisplayFilter(filter FileManagerDisplayFilter) {
m.filter = filter
m.SetTree()
}
func (m *FileManager) ToggleShowTree() {
m.showTree = !m.showTree
m.SetTree()
@@ -73,10 +112,11 @@ func (m *FileManager) SetFiles(files []*models.File) {
}
func (m *FileManager) SetTree() {
filesForDisplay := m.GetFilesForDisplay()
if m.showTree {
m.tree = BuildTreeFromFiles(m.files)
m.tree = BuildTreeFromFiles(filesForDisplay)
} else {
m.tree = BuildFlatTreeFromFiles(m.files)
m.tree = BuildFlatTreeFromFiles(filesForDisplay)
}
}

View File

@@ -3,11 +3,15 @@ package filetree
import (
"testing"
"github.com/gookit/color"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/stretchr/testify/assert"
"github.com/xo/terminfo"
)
func TestRender(t *testing.T) {
color.ForceSetColorLevel(terminfo.ColorLevelNone)
scenarios := []struct {
name string
root *FileNode
@@ -89,3 +93,62 @@ func TestRender(t *testing.T) {
})
}
}
func TestFilterAction(t *testing.T) {
scenarios := []struct {
name string
filter FileManagerDisplayFilter
files []*models.File
expected []*models.File
}{
{
name: "filter files with unstaged changes",
filter: DisplayUnstaged,
files: []*models.File{
{Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true},
{Name: "dir2/file5", ShortStatus: "M ", HasStagedChanges: true},
{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
},
expected: []*models.File{
{Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true},
{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
},
},
{
name: "filter files with staged changes",
filter: DisplayStaged,
files: []*models.File{
{Name: "dir2/dir2/file4", ShortStatus: "M ", HasStagedChanges: true},
{Name: "dir2/file5", ShortStatus: "M ", HasStagedChanges: false},
{Name: "file1", ShortStatus: "M ", HasStagedChanges: true},
},
expected: []*models.File{
{Name: "dir2/dir2/file4", ShortStatus: "M ", HasStagedChanges: true},
{Name: "file1", ShortStatus: "M ", HasStagedChanges: true},
},
},
{
name: "filter all files",
filter: DisplayAll,
files: []*models.File{
{Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true},
{Name: "dir2/file5", ShortStatus: "M ", HasUnstagedChanges: true},
{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
},
expected: []*models.File{
{Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true},
{Name: "dir2/file5", ShortStatus: "M ", HasUnstagedChanges: true},
{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.name, func(t *testing.T) {
mngr := &FileManager{files: s.files, filter: s.filter}
result := mngr.GetFilesForDisplay()
assert.EqualValues(t, s.expected, result)
})
}
}

View File

@@ -1,8 +1,6 @@
package filetree
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/commands/models"
)
@@ -182,7 +180,7 @@ func (s *FileNode) NameAtDepth(depth int) string {
prevName = join(splitPrevName[depth:])
}
return fmt.Sprintf("%s%s%s", prevName, " → ", name)
return prevName + " → " + name
}
return name

View File

@@ -84,9 +84,13 @@ func TestCompress(t *testing.T) {
Path: "",
Children: []*FileNode{
{
Path: "dir1/file2",
File: &models.File{Name: "file2", ShortStatus: "M ", HasUnstagedChanges: true},
CompressionLevel: 1,
Path: "dir1",
Children: []*FileNode{
{
File: &models.File{Name: "file2", ShortStatus: "M ", HasUnstagedChanges: true},
Path: "dir1/file2",
},
},
},
{
Path: "dir2",
@@ -102,9 +106,14 @@ func TestCompress(t *testing.T) {
},
},
{
Path: "dir3/dir3-1/file5",
File: &models.File{Name: "file5", ShortStatus: "M ", HasUnstagedChanges: true},
CompressionLevel: 2,
Path: "dir3/dir3-1",
CompressionLevel: 1,
Children: []*FileNode{
{
File: &models.File{Name: "file5", ShortStatus: "M ", HasUnstagedChanges: true},
Path: "dir3/dir3-1/file5",
},
},
},
{
File: &models.File{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},

View File

@@ -170,7 +170,7 @@ func compressAux(node INode) INode {
children := node.GetChildren()
for i := range children {
grandchildren := children[i].GetChildren()
for len(grandchildren) == 1 {
for len(grandchildren) == 1 && !grandchildren[0].IsLeaf() {
grandchildren[0].SetCompressionLevel(children[i].GetCompressionLevel() + 1)
children[i] = grandchildren[0]
grandchildren = children[i].GetChildren()

View File

@@ -35,7 +35,8 @@ func (gui *Gui) handleCreateFilteringMenuPanel() error {
displayString: gui.Tr.LcFilterPathOption,
onPress: func() error {
return gui.prompt(promptOpts{
title: gui.Tr.LcEnterFileName,
findSuggestionsFunc: gui.getFilePathSuggestionsFunc(),
title: gui.Tr.EnterFileName,
handleConfirm: func(response string) error {
return gui.setFiltering(strings.TrimSpace(response))
},

190
pkg/gui/find_suggestions.go Normal file
View File

@@ -0,0 +1,190 @@
package gui
import (
"fmt"
"os"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/jesseduffield/minimal/gitignore"
"gopkg.in/ozeidan/fuzzy-patricia.v3/patricia"
)
// Thinking out loud: I'm typically a staunch advocate of organising code by feature rather than type,
// because colocating code that relates to the same feature means far less effort
// to get all the context you need to work on any particular feature. But the one
// major benefit of grouping by type is that it makes it makes it less likely that
// somebody will re-implement the same logic twice, because they can quickly see
// if a certain method has been used for some use case, given that as a starting point
// they know about the type. In that vein, I'm including all our functions for
// finding suggestions in this file, so that it's easy to see if a function already
// exists for fetching a particular model.
func (gui *Gui) getRemoteNames() []string {
result := make([]string, len(gui.State.Remotes))
for i, remote := range gui.State.Remotes {
result[i] = remote.Name
}
return result
}
func matchesToSuggestions(matches []string) []*types.Suggestion {
suggestions := make([]*types.Suggestion, len(matches))
for i, match := range matches {
suggestions[i] = &types.Suggestion{
Value: match,
Label: match,
}
}
return suggestions
}
func (gui *Gui) getRemoteSuggestionsFunc() func(string) []*types.Suggestion {
remoteNames := gui.getRemoteNames()
return fuzzySearchFunc(remoteNames)
}
func (gui *Gui) getBranchNames() []string {
result := make([]string, len(gui.State.Branches))
for i, branch := range gui.State.Branches {
result[i] = branch.Name
}
return result
}
func (gui *Gui) getBranchNameSuggestionsFunc() func(string) []*types.Suggestion {
branchNames := gui.getBranchNames()
return func(input string) []*types.Suggestion {
var matchingBranchNames []string
if input == "" {
matchingBranchNames = branchNames
} else {
matchingBranchNames = utils.FuzzySearch(input, branchNames)
}
suggestions := make([]*types.Suggestion, len(matchingBranchNames))
for i, branchName := range matchingBranchNames {
suggestions[i] = &types.Suggestion{
Value: branchName,
Label: presentation.GetBranchTextStyle(branchName).Sprint(branchName),
}
}
return suggestions
}
}
// here we asynchronously fetch the latest set of paths in the repo and store in
// gui.State.FilesTrie. On the main thread we'll be doing a fuzzy search via
// gui.State.FilesTrie. So if we've looked for a file previously, we'll start with
// the old trie and eventually it'll be swapped out for the new one.
// Notably, unlike other suggestion functions we're not showing all the options
// if nothing has been typed because there'll be too much to display efficiently
func (gui *Gui) getFilePathSuggestionsFunc() func(string) []*types.Suggestion {
_ = gui.WithWaitingStatus(gui.Tr.LcLoadingFileSuggestions, func() error {
trie := patricia.NewTrie()
// load every non-gitignored file in the repo
ignore, err := gitignore.FromGit()
if err != nil {
return err
}
err = ignore.Walk(".",
func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
trie.Insert(patricia.Prefix(path), path)
return nil
})
// cache the trie for future use
gui.State.FilesTrie = trie
// refresh the selections view
gui.suggestionsAsyncHandler.Do(func() func() {
// assuming here that the confirmation view is what we're typing into.
// This assumption may prove false over time
suggestions := gui.findSuggestions(gui.Views.Confirmation.TextArea.GetContent())
return func() { gui.setSuggestions(suggestions) }
})
return err
})
return func(input string) []*types.Suggestion {
matchingNames := []string{}
_ = gui.State.FilesTrie.VisitFuzzy(patricia.Prefix(input), true, func(prefix patricia.Prefix, item patricia.Item, skipped int) error {
matchingNames = append(matchingNames, item.(string))
return nil
})
// doing another fuzzy search for good measure
matchingNames = utils.FuzzySearch(input, matchingNames)
suggestions := make([]*types.Suggestion, len(matchingNames))
for i, name := range matchingNames {
suggestions[i] = &types.Suggestion{
Value: name,
Label: name,
}
}
return suggestions
}
}
func (gui *Gui) getRemoteBranchNames(separator string) []string {
result := []string{}
for _, remote := range gui.State.Remotes {
for _, branch := range remote.Branches {
result = append(result, fmt.Sprintf("%s%s%s", remote.Name, separator, branch.Name))
}
}
return result
}
func (gui *Gui) getRemoteBranchesSuggestionsFunc(separator string) func(string) []*types.Suggestion {
return fuzzySearchFunc(gui.getRemoteBranchNames(separator))
}
func (gui *Gui) getTagNames() []string {
result := make([]string, len(gui.State.Tags))
for i, tag := range gui.State.Tags {
result[i] = tag.Name
}
return result
}
func (gui *Gui) getRefsSuggestionsFunc() func(string) []*types.Suggestion {
remoteBranchNames := gui.getRemoteBranchNames("/")
localBranchNames := gui.getBranchNames()
tagNames := gui.getTagNames()
additionalRefNames := []string{"HEAD", "FETCH_HEAD", "MERGE_HEAD", "ORIG_HEAD"}
refNames := append(append(append(remoteBranchNames, localBranchNames...), tagNames...), additionalRefNames...)
return fuzzySearchFunc(refNames)
}
func (gui *Gui) getCustomCommandsHistorySuggestionsFunc() func(string) []*types.Suggestion {
// reversing so that we display the latest command first
history := utils.Reverse(gui.Config.GetAppState().CustomCommandsHistory)
return fuzzySearchFunc(history)
}
func fuzzySearchFunc(options []string) func(string) []*types.Suggestion {
return func(input string) []*types.Suggestion {
var matches []string
if input == "" {
matches = options
} else {
matches = utils.FuzzySearch(input, options)
}
return matchesToSuggestions(matches)
}
}

View File

@@ -5,14 +5,12 @@ import (
"io/ioutil"
"log"
"os"
"runtime"
"sync"
"os/exec"
"strings"
"time"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
@@ -21,15 +19,18 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/lbl"
"github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts"
"github.com/jesseduffield/lazygit/pkg/gui/modes/cherrypicking"
"github.com/jesseduffield/lazygit/pkg/gui/modes/diffing"
"github.com/jesseduffield/lazygit/pkg/gui/modes/filtering"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/tasks"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/updates"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/mattn/go-runewidth"
"github.com/sirupsen/logrus"
"gopkg.in/ozeidan/fuzzy-patricia.v3/patricia"
)
// screen sizing determines how much space your selected window takes up (window
@@ -49,10 +50,6 @@ const StartupPopupVersion = 5
// OverlappingEdges determines if panel edges overlap
var OverlappingEdges = false
func init() {
runewidth.DefaultCondition.EastAsianWidth = false
}
type ContextManager struct {
ContextStack []Context
sync.RWMutex
@@ -120,6 +117,8 @@ type Gui struct {
// the extras window contains things like the command log
ShowExtrasWindow bool
suggestionsAsyncHandler *tasks.AsyncHandler
}
type listPanelState struct {
@@ -269,31 +268,10 @@ const (
COMPLETE
)
// if ref is blank we're not diffing anything
type Diffing struct {
Ref string
Reverse bool
}
func (m *Diffing) Active() bool {
return m.Ref != ""
}
type CherryPicking struct {
CherryPickedCommits []*models.Commit
// we only allow cherry picking from one context at a time, so you can't copy a commit from the local commits context and then also copy a commit in the reflog context
ContextKey ContextKey
}
func (m *CherryPicking) Active() bool {
return len(m.CherryPickedCommits) > 0
}
type Modes struct {
Filtering filtering.Filtering
CherryPicking CherryPicking
Diffing Diffing
CherryPicking cherrypicking.CherryPicking
Diffing diffing.Diffing
}
type guiMutexes struct {
@@ -358,6 +336,12 @@ type guiState struct {
// do this whenever we switch back and forth between repos to get the views
// back in sync with the repo state
ViewsSetup bool
// flag as to whether or not the diff view should ignore whitespace
IgnoreWhitespaceInDiffView bool
// for displaying suggestions while typing in a file name
FilesTrie *patricia.Trie
}
// reuseState determines if we pull the repo state from our repo state map or
@@ -424,12 +408,9 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
},
Ptmx: nil,
Modes: Modes{
Filtering: filtering.NewFiltering(filterPath),
CherryPicking: CherryPicking{
CherryPickedCommits: make([]*models.Commit, 0),
ContextKey: "",
},
Diffing: Diffing{},
Filtering: filtering.New(filterPath),
CherryPicking: cherrypicking.New(),
Diffing: diffing.New(),
},
ViewContextMap: contexts.initialViewContextMap(),
ViewTabContextMap: contexts.initialViewTabContextMap(),
@@ -437,6 +418,7 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
// TODO: put contexts in the context manager
ContextManager: NewContextManager(initialContext),
Contexts: contexts,
FilesTrie: patricia.NewTrie(),
}
gui.RepoStateMap[Repo(currentDir)] = gui.State
@@ -446,19 +428,20 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
// NewGui builds a new gui handler
func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *oscommands.OSCommand, tr *i18n.TranslationSet, config config.AppConfigurer, updater *updates.Updater, filterPath string, showRecentRepos bool) (*Gui, error) {
gui := &Gui{
Log: log,
GitCommand: gitCommand,
OSCommand: oSCommand,
Config: config,
Tr: tr,
Updater: updater,
statusManager: &statusManager{},
viewBufferManagerMap: map[string]*tasks.ViewBufferManager{},
showRecentRepos: showRecentRepos,
RepoPathStack: []string{},
RepoStateMap: map[Repo]*guiState{},
CmdLog: []string{},
ShowExtrasWindow: config.GetUserConfig().Gui.ShowCommandLog,
Log: log,
GitCommand: gitCommand,
OSCommand: oSCommand,
Config: config,
Tr: tr,
Updater: updater,
statusManager: &statusManager{},
viewBufferManagerMap: map[string]*tasks.ViewBufferManager{},
showRecentRepos: showRecentRepos,
RepoPathStack: []string{},
RepoStateMap: map[Repo]*guiState{},
CmdLog: []string{},
ShowExtrasWindow: config.ShowCommandLogOnStartup(),
suggestionsAsyncHandler: tasks.NewAsyncHandler(),
}
gui.resetState(filterPath, false)
@@ -516,7 +499,7 @@ func (gui *Gui) Run() error {
g.NextSearchMatchKey = gui.getKey(userConfig.Keybinding.Universal.NextMatch)
g.PrevSearchMatchKey = gui.getKey(userConfig.Keybinding.Universal.PrevMatch)
g.ASCII = runtime.GOOS == "windows" && runewidth.IsEastAsian()
g.ShowListFooter = userConfig.Gui.ShowListFooter
if userConfig.Gui.MouseEvents {
g.Mouse = true
@@ -628,7 +611,7 @@ func (gui *Gui) runSubprocess(subprocess *exec.Cmd) error {
subprocess.Stderr = os.Stdout
subprocess.Stdin = os.Stdin
fmt.Fprintf(os.Stdout, "\n%s\n\n", utils.ColoredString("+ "+strings.Join(subprocess.Args, " "), color.FgBlue))
fmt.Fprintf(os.Stdout, "\n%s\n\n", style.FgBlue.Sprint("+ "+strings.Join(subprocess.Args, " ")))
if err := subprocess.Run(); err != nil {
// not handling the error explicitly because usually we're going to see it
@@ -640,7 +623,7 @@ func (gui *Gui) runSubprocess(subprocess *exec.Cmd) error {
subprocess.Stderr = ioutil.Discard
subprocess.Stdin = nil
fmt.Fprintf(os.Stdout, "\n%s", utils.ColoredString(gui.Tr.PressEnterToReturn, color.FgGreen))
fmt.Fprintf(os.Stdout, "\n%s", style.FgGreen.Sprint(gui.Tr.PressEnterToReturn))
fmt.Scanln() // wait for enter press
return nil

View File

@@ -1,3 +1,4 @@
//go:build !windows
// +build !windows
package gui
@@ -55,8 +56,8 @@ func Test(t *testing.T) {
updateSnapshots,
record,
speedEnv,
func(t *testing.T, expected string, actual string) {
assert.Equal(t, expected, actual, fmt.Sprintf("expected:\n%s\nactual:\n%s\n", expected, actual))
func(t *testing.T, expected string, actual string, prefix string) {
assert.Equal(t, expected, actual, fmt.Sprintf("Unexpected %s. Expected:\n%s\nActual:\n%s\n", prefix, expected, actual))
},
includeSkipped,
)

View File

@@ -3,8 +3,8 @@ package gui
import (
"fmt"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/constants"
"github.com/jesseduffield/lazygit/pkg/gui/style"
)
func (gui *Gui) informationStr() string {
@@ -15,8 +15,8 @@ func (gui *Gui) informationStr() string {
}
if gui.g.Mouse {
donate := color.New(color.FgMagenta, color.Underline).Sprint(gui.Tr.Donate)
askQuestion := color.New(color.FgYellow, color.Underline).Sprint(gui.Tr.AskQuestion)
donate := style.FgMagenta.SetUnderline().Sprint(gui.Tr.Donate)
askQuestion := style.FgYellow.SetUnderline().Sprint(gui.Tr.AskQuestion)
return fmt.Sprintf("%s %s %s", donate, askQuestion, gui.Config.GetVersion())
} else {
return gui.Config.GetVersion()

View File

@@ -231,6 +231,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Modifier: gocui.ModNone,
Handler: gui.handleTopLevelReturn,
},
{
ViewName: "",
Key: gui.getKey(config.Universal.OpenRecentRepos),
Handler: gui.handleCreateRecentReposMenu,
Alternative: "<c-r>",
Description: gui.Tr.SwitchRepo,
},
{
ViewName: "",
Key: gui.getKey(config.Universal.ScrollUpMain),
@@ -374,6 +381,12 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleShowAllBranchLogs,
Description: gui.Tr.LcAllBranchesLogGraph,
},
{
ViewName: "files",
Key: gui.getKey("<c-b>"),
Handler: gui.handleStatusFilterPressed,
Description: gui.Tr.LcCommitFileFilter,
},
{
ViewName: "files",
Contexts: []string{string(FILES_CONTEXT_KEY)},
@@ -538,6 +551,14 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleCreatePullRequestPress,
Description: gui.Tr.LcCreatePullRequest,
},
{
ViewName: "branches",
Contexts: []string{string(LOCAL_BRANCHES_CONTEXT_KEY)},
Key: gui.getKey(config.Branches.ViewPullRequestOptions),
Handler: gui.handleCreatePullRequestMenu,
Description: gui.Tr.LcCreatePullRequestOptions,
OpensMenu: true,
},
{
ViewName: "branches",
Contexts: []string{string(LOCAL_BRANCHES_CONTEXT_KEY)},
@@ -1282,11 +1303,19 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Modifier: gocui.ModNone,
Handler: gui.handleSelectNextHunk,
},
{
ViewName: "main",
Contexts: []string{string(MAIN_PATCH_BUILDING_CONTEXT_KEY), string(MAIN_STAGING_CONTEXT_KEY)},
Key: gui.getKey(config.Universal.CopyToClipboard),
Modifier: gocui.ModNone,
Handler: gui.copySelectedToClipboard,
Description: gui.Tr.LcCopySelectedTexToClipboard,
},
{
ViewName: "main",
Contexts: []string{string(MAIN_STAGING_CONTEXT_KEY)},
Key: gui.getKey(config.Universal.Edit),
Handler: gui.handleFileEdit,
Handler: gui.handleLineByLineEdit,
Description: gui.Tr.LcEditFile,
},
{
@@ -1443,8 +1472,8 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
ViewName: "main",
Contexts: []string{string(MAIN_MERGING_CONTEXT_KEY)},
Key: gui.getKey(config.Main.PickBothHunks),
Handler: gui.handlePickBothHunks,
Description: gui.Tr.PickBothHunks,
Handler: gui.handlePickAllHunks,
Description: gui.Tr.PickAllHunks,
},
{
ViewName: "main",
@@ -1464,29 +1493,29 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
ViewName: "main",
Contexts: []string{string(MAIN_MERGING_CONTEXT_KEY)},
Key: gui.getKey(config.Universal.PrevItem),
Handler: gui.handleSelectTop,
Description: gui.Tr.SelectTop,
Handler: gui.handleSelectPrevConflictHunk,
Description: gui.Tr.SelectPrevHunk,
},
{
ViewName: "main",
Contexts: []string{string(MAIN_MERGING_CONTEXT_KEY)},
Key: gui.getKey(config.Universal.NextItem),
Handler: gui.handleSelectBottom,
Description: gui.Tr.SelectBottom,
Handler: gui.handleSelectNextConflictHunk,
Description: gui.Tr.SelectNextHunk,
},
{
ViewName: "main",
Contexts: []string{string(MAIN_MERGING_CONTEXT_KEY)},
Key: gocui.MouseWheelUp,
Modifier: gocui.ModNone,
Handler: gui.handleSelectTop,
Handler: gui.handleSelectPrevConflictHunk,
},
{
ViewName: "main",
Contexts: []string{string(MAIN_MERGING_CONTEXT_KEY)},
Key: gocui.MouseWheelDown,
Modifier: gocui.ModNone,
Handler: gui.handleSelectBottom,
Handler: gui.handleSelectNextConflictHunk,
},
{
ViewName: "main",
@@ -1507,14 +1536,14 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Contexts: []string{string(MAIN_MERGING_CONTEXT_KEY)},
Key: gui.getKey(config.Universal.PrevItemAlt),
Modifier: gocui.ModNone,
Handler: gui.handleSelectTop,
Handler: gui.handleSelectPrevConflictHunk,
},
{
ViewName: "main",
Contexts: []string{string(MAIN_MERGING_CONTEXT_KEY)},
Key: gui.getKey(config.Universal.NextItemAlt),
Modifier: gocui.ModNone,
Handler: gui.handleSelectBottom,
Handler: gui.handleSelectNextConflictHunk,
},
{
ViewName: "main",
@@ -1712,6 +1741,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Description: gui.Tr.LcViewBulkSubmoduleOptions,
OpensMenu: true,
},
{
ViewName: "files",
Contexts: []string{string(FILES_CONTEXT_KEY)},
Key: gui.getKey(config.Universal.ToggleWhitespaceInDiffView),
Handler: gui.toggleWhitespaceInDiffView,
Description: gui.Tr.ToggleWhitespaceInDiffView,
},
{
ViewName: "extras",
Key: gocui.MouseWheelUp,
@@ -1782,8 +1818,18 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
}
// Appends keybindings to jump to a particular sideView using numbers
for i, window := range []string{"status", "files", "branches", "commits", "stash"} {
bindings = append(bindings, &Binding{ViewName: "", Key: rune(i+1) + '0', Modifier: gocui.ModNone, Handler: gui.goToSideWindow(window)})
windows := []string{"status", "files", "branches", "commits", "stash"}
if len(config.Universal.JumpToBlock) != len(windows) {
log.Fatal("Jump to block keybindings cannot be set. Exactly 5 keybindings must be supplied.")
} else {
for i, window := range windows {
bindings = append(bindings, &Binding{
ViewName: "",
Key: gui.getKey(config.Universal.JumpToBlock[i]),
Modifier: gocui.ModNone,
Handler: gui.goToSideWindow(window)})
}
}
for viewName := range gui.State.Contexts.initialViewTabContextMap() {

View File

@@ -111,6 +111,7 @@ func (gui *Gui) createAllViews() error {
gui.Views.Credentials.Editable = true
gui.Views.Suggestions.Visible = false
gui.Views.Suggestions.ContainsList = true
gui.Views.Menu.Visible = false

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