Compare commits

...
334 Commits
Author SHA1 Message Date
README-bot 367b0d3318 Updated README.md 2022-07-18 23:36:44 +00:00
Jesse Duffield 97d8c591b6 Merge pull request #2039 from Ryooooooga/master 2022-07-19 09:36:23 +10:00
Ryooooooga 04772154b7 ci: change not to run Generate Sponsors action on the fork repository 2022-07-18 21:51:32 +09:00
Jesse Duffield c087dca60a Merge pull request #2027 from jesseduffield/gozes-jesse
Attempt at fixing CI
2022-07-05 19:37:09 +10:00
Jesse Duffield 5bd3eedd71 update docs 2022-07-05 19:33:44 +10:00
Jesse Duffield a99255c18b try having at least one commit created 2022-07-05 19:33:44 +10:00
Jesse Duffield 6d133fd611 better handling of gitignore files 2022-07-05 19:33:44 +10:00
Jesse Duffield bd9e85a2d2 init repo before setting username 2022-07-05 19:33:44 +10:00
Juan Sanchez Montalvo 11d766053e Allow adding a file to the .git/info/exclude file 2022-07-05 19:33:44 +10:00
Jesse Duffield 86038b3fae Merge pull request #2017 from m-mead/feat/m-mead/1858/add-confirm-prompt
feat: add confirm prompt for custom keybindings
2022-07-05 08:26:07 +10:00
Jesse Duffield 39ba397cf7 Update pkg/config/user_config.go 2022-07-04 11:36:18 -07:00
Michael Mead 9d304098bb feat: add confirm prompt for custom keybindings
- Supports configuring a custom confirmation prompt via `config.yml` for
  custom keybindings. A new `CustomCommandPrompt.Body` field is
  used to store the immutable body text of the confirmation popup.
- Adds a sample 'confirm' prompt to the example `config.yml`.
- Updates the `Prompts` section of the documentation to include
  'confirm' prompt type and also describe which fields pertain to it
  (i.e. `initialValue`).

Closes: https://github.com/jesseduffield/lazygit/issues/1858

Signed-off-by: Michael Mead <mmead.developer@gmail.com>
2022-07-04 11:36:13 -07:00
Jesse Duffield 582b1991a4 Merge pull request #1997 from shinhs0506/wsl-support 2022-07-04 18:13:55 +10:00
Jesse Duffield 6e6e8ce8ca Update CONTRIBUTING.md 2022-07-04 18:09:24 +10:00
John Shin 1eb47deae4 apply gofumpt 2022-07-03 02:00:40 -07:00
README-bot 7fbb073b18 Updated README.md 2022-07-03 08:39:40 +00:00
Jesse Duffield bc989dd846 Merge pull request #2022 from Ryooooooga/fix-config-env
docs(Config.md): fix docs on specifying config file
2022-07-03 18:39:19 +10:00
Ryooooooga 60049f8f4f docs(Config.md): fix docs on specifying config file 2022-07-02 19:53:54 +09:00
Jesse Duffield f565290737 Merge pull request #2002 from Ryooooooga/update-config-docs 2022-07-01 17:22:19 +10:00
Jesse Duffield a6eb2c7e9d Merge pull request #2006 from jesseduffield/better-killing 2022-07-01 17:16:29 +10:00
README-bot 3ab1b39ac7 Updated README.md 2022-07-01 07:15:58 +00:00
Jesse Duffield 36c8f592cf Merge pull request #2020 from mark2185/feature/default-branch 2022-07-01 17:15:39 +10:00
Luka Markušić f1efa02640 Ask for initial branch name 2022-06-30 13:53:58 +02:00
Jesse Duffield 41071c3703 Update README.md 2022-06-19 17:41:04 +10:00
README-bot 9618338b4f Updated README.md 2022-06-19 07:34:48 +00:00
Jesse Duffield 6d2bc6dca3 Update README.md 2022-06-19 17:34:32 +10:00
Jesse Duffield c9d891a913 better process killing 2022-06-18 13:39:22 +10:00
Ryooooooga 92b0e0edd0 docs(Config.md): add missing keybinding.branches.renameBranch 2022-06-16 23:29:48 +09:00
Jesse Duffield a5821f5ec8 Merge pull request #1960 from fsmiamoto/fix-interactive-rebase 2022-06-13 12:44:07 +10:00
Francisco Miamoto d8dfed79b3 fix: use todo parser to properly read rebase todo file 2022-06-12 20:01:32 -03:00
Francisco Miamoto 0b08a0b298 build: add github.com/fsmiamoto/git-todo-parser 2022-06-12 20:01:32 -03:00
John Shin 4d54b3801f apply formatting 2022-06-11 23:23:22 -07:00
John Shin 3067c2c321 support open file and link on WSL 2022-06-11 23:18:29 -07:00
Jesse Duffield 2bccbee32b Merge pull request #1992 from jesseduffield/fix-ci 2022-06-11 13:23:59 +10:00
Jesse Duffield 2fc77796b1 fix ci 2022-06-11 13:20:49 +10:00
Jesse Duffield 2864366e95 Merge pull request #1991 from jesseduffield/parallel-tests 2022-06-11 13:17:36 +10:00
Jesse Duffield 3d61893589 add more cache paths 2022-06-11 13:11:56 +10:00
Jesse Duffield ef36ac955d remove unused file 2022-06-11 13:11:51 +10:00
Jesse Duffield 02c5559704 run integration tests in parallel and properly cache windows build 2022-06-11 13:06:29 +10:00
Jesse Duffield 32c0b39dbd Merge pull request #1950 from HiromasaNojima/add_subcommand_into_specific_layout_directly 2022-06-11 11:51:14 +10:00
Jesse Duffield baa02ac154 Merge pull request #1969 from mark2185/fix-windows-create-new-git-repo 2022-06-11 11:50:53 +10:00
Jesse Duffield d0748c6c28 verbose tests 2022-06-11 11:23:56 +10:00
Jesse Duffield b1e4968d0b allow opening lazygit to a specific panel 2022-06-11 11:23:56 +10:00
Luka Markušić a955dbcfd7 Validate recent repo before blindly opening it 2022-06-11 09:42:51 +10:00
Luka Markušić 658a6b239b Fix parsing of 'y/n' when starting in non-repo 2022-06-11 09:42:51 +10:00
Jesse Duffield 36aa01c3ac Merge pull request #1970 from mark2185/fix-helpful-unmarshall-error 2022-06-11 09:19:30 +10:00
Jesse Duffield de3114edc3 Merge pull request #1972 from lei4519/feature/display-whole-graph-by-default 2022-06-09 20:17:25 +10:00
Jesse Duffield cdec70cc06 Merge pull request #1975 from mark2185/fix-unmatching-remote-branch-name 2022-06-09 20:12:56 +10:00
Jesse Duffield 24e3afbb56 Merge pull request #1983 from David-Else/master 2022-06-09 20:10:47 +10:00
Jesse Duffield f15fafd7cb Merge pull request #1929 from jesseduffield/set-author 2022-06-09 19:28:54 +10:00
Jesse Duffield 9591cc381a support setting the author of a commit
update copy
2022-06-09 19:12:20 +10:00
README-bot 901ab3ac1b Updated README.md 2022-06-08 06:18:32 +00:00
Jesse Duffield 9a8625b35c Merge pull request #1986 from Shin-JaeHeon/feature/korean
Add Korean translation
2022-06-08 16:18:14 +10:00
Shin-JaeHeon b6b1f5dc37 improve korean translation 2022-06-07 23:46:32 +09:00
Shin-JaeHeon d2a873cb40 improve korean translation 2022-06-07 23:36:24 +09:00
Shin-JaeHeon d533427173 Korean translation 2022-06-07 23:31:56 +09:00
David d669d9dab7 Update Fedora/RHEL installation information 2022-06-06 10:49:06 +01:00
Luka Markušić 55a941d5dc Fetch the correct remote branch 2022-06-01 20:35:09 +02:00
README-bot f9a3188b56 Updated README.md 2022-06-01 09:14:26 +00:00
Jesse Duffield f6ce220807 Update Custom_Command_Keybindings.md 2022-06-01 19:14:08 +10:00
Luka MarkušićandRyoga abf203e012 Update pkg/config/app_config.go
Print out error to narrow down the search

Co-authored-by: Ryoga <eial5q265e5+github@gmail.com>
2022-05-31 13:28:24 +02:00
Lay 684ea284af fix: lint error 2022-05-30 23:19:48 +08:00
Jesse Duffield b6b3be9ac7 Update docs/Config.md 2022-05-30 17:34:30 +10:00
Lay 666180cfd0 Add config param that displays the whole git graph by default 2022-05-30 13:52:39 +08:00
Luka Markušić abeb03b090 Add helpful message on unmarshall error 2022-05-28 17:29:30 +02:00
Jesse Duffield 8fd9dea641 Merge pull request #1936 from Ryooooooga/feature/tab-i18n 2022-05-18 22:24:45 +10:00
Jesse Duffield 499d51ecf6 Merge pull request #1931 from mark2185/fix-non-existant-recent-repo 2022-05-18 22:22:28 +10:00
Jesse Duffield e1e16a34da Merge pull request #1948 from Ryooooooga/feature/time-format 2022-05-18 22:20:51 +10:00
README-bot 15ff8a143e Updated README.md 2022-05-18 12:19:55 +00:00
Jesse Duffield 5d8c2eeccc Merge pull request #1945 from Ryooooooga/feature/fix-lint 2022-05-18 22:19:36 +10:00
Ryooooooga bfefef92a6 chore(i18n): move InitialViewTabContextMap to gui package 2022-05-18 21:09:48 +09:00
Ryooooooga 5275161a88 chore(i18n): localize panel titles 2022-05-18 20:55:42 +09:00
Ryooooooga e57931f56d test: fix timezone for time format tests 2022-05-18 20:55:29 +09:00
Ryooooooga 1f1d871837 feat: add ability to customize time format 2022-05-18 20:55:27 +09:00
Ryooooooga 9693afd671 fix: fix lint error 2022-05-18 20:34:35 +09:00
Jesse Duffield e28d1334e9 better debug setup 2022-05-17 22:14:24 +10:00
Jesse Duffield 6e29830f63 ignore some codespaces stuff 2022-05-17 11:55:31 +00:00
Jesse Duffield 95e816ae91 remove commented out line 2022-05-17 21:52:14 +10:00
Jesse Duffield e89d817d78 update launch.json 2022-05-17 11:45:47 +00:00
Jesse Duffield 2fe286f395 add launch.json 2022-05-17 21:38:37 +10:00
Luka Markušić 1de5ca3511 Don't panic when there are no valid git repos 2022-05-17 07:28:52 +02:00
Jesse Duffield f31dcd3091 Merge pull request #1930 from mark2185/feature/discard-staged-only 2022-05-15 20:33:25 +10:00
Luka Markušić 241d182da7 Make tooltip i18n-able 2022-05-15 12:16:20 +02:00
Jesse Duffield 8e7f6822fc Merge pull request #1944 from Ryooooooga/feature/fix-ambiguous-refname 2022-05-15 19:45:30 +10:00
Jesse Duffield 73491fed25 Merge pull request #1927 from HiromasaNojima/option_always_show_unstaged_staged_panels 2022-05-15 19:24:56 +10:00
Jesse Duffield d72ffdc4a7 refactor 2022-05-15 19:10:04 +10:00
Jesse Duffield 7b0bb68741 Merge pull request #1911 from Ryooooooga/feature/fix-log-show-signature 2022-05-15 18:50:36 +10:00
Ryooooooga 61970a4439 fix: fix ambiguous refname 2022-05-13 21:05:51 +09:00
README-bot 5529088e43 Updated README.md 2022-05-11 13:15:16 +00:00
Jesse Duffield 5f2102d1c1 rearrange 2022-05-11 23:13:06 +10:00
Jesse Duffield 96147bc11a add sponsors thingo 2022-05-11 23:11:24 +10:00
Ryooooooga 490a964432 chore(loaders): add -c log.showSignature=false flag to the rebasing commit loader 2022-05-10 19:47:10 +09:00
Ryooooooga f789e21377 perf: improve loading speed of commits and reflog when log.showSignature=true 2022-05-10 19:47:07 +09:00
Jesse Duffield 8c04118bb1 Update README.md 2022-05-09 22:53:42 +10:00
Jesse Duffield d622fefbeb Update README.md 2022-05-09 10:08:56 +10:00
Luka Markušić ca191159f5 Discard staged changes only 2022-05-08 14:24:28 +02:00
HiromasaNojima 1ef585969f add option to always show unstaged/staged panels 2022-05-08 17:24:55 +09:00
Jesse Duffield f7c44f2407 Merge pull request #1926 from jesseduffield/prep-for-author-setting 2022-05-08 14:30:58 +10:00
Jesse Duffield e67fef776b add author email to commits 2022-05-08 14:26:18 +10:00
Jesse Duffield 6f8063217d rename displayString to label for menu items 2022-05-08 14:26:18 +10:00
Jesse Duffield 125e948d82 Merge pull request #1893 from JensPfeifle/reset_author 2022-05-08 13:39:22 +10:00
Jesse Duffield 25b213b992 fix cheatsheet 2022-05-08 13:31:13 +10:00
Jesse Duffield 20db9fc939 update copy 2022-05-08 13:29:57 +10:00
Jesse Duffield 8ef0032ee2 update snapshot to include author name and email in git log 2022-05-08 13:29:57 +10:00
Jens Pfeifle ec4b5ca134 Update cheatsheets. 2022-05-08 13:29:57 +10:00
Jens Pfeifle fbe23b3754 fixup! Add command to reset the commit author from the commits panel. 2022-05-08 13:29:57 +10:00
Jens Pfeifle 1e08b90f67 Add integration test for reset author command. 2022-05-08 13:29:57 +10:00
Jens Pfeifle 7ac487545c fixup! Add command to reset the commit author from the commits panel. 2022-05-08 13:29:56 +10:00
Jens Pfeifle 7c573a5bea Add command to reset the commit author from the commits panel. 2022-05-08 13:29:56 +10:00
Jesse Duffield 8247089e53 Merge pull request #1925 from jesseduffield/details-view 2022-05-08 13:24:51 +10:00
Jesse Duffield 22d98249fe better popup resizing logic 2022-05-08 13:24:36 +10:00
Jesse Duffield f257740ea7 add tooltip view for showing menu item descriptions 2022-05-08 13:24:36 +10:00
Jesse Duffield 517e9445df refactor view definitions 2022-05-08 11:41:13 +10:00
Jesse Duffield 549409a6f3 Merge pull request #1923 from Ryooooooga/feature/fix-author-name 2022-05-07 23:41:16 +10:00
Jesse Duffield b91fb81230 Merge pull request #1920 from Ryooooooga/feature/fix-prompt-height 2022-05-07 23:40:26 +10:00
Dawid Dziurla 119002d98c README: delete Ubuntu installation 2022-05-07 12:02:37 +02:00
Dawid Dziurla 96f8201626 workflows: bump brew formula on Ubuntu 2022-05-07 11:59:40 +02:00
Ryooooooga 5717e72366 fix: fix copying author name to clipboard 2022-05-07 18:08:05 +09:00
Ryooooooga 540d2e379a fix: fix collision of Donate and Ask Question 2022-05-07 18:02:09 +09:00
Ryooooooga 04babdfb82 fix: fix the appStatus width calculation 2022-05-07 17:37:53 +09:00
Ryooooooga 265e6d8360 fix: fix the prompt height calculation 2022-05-07 16:36:20 +09:00
Jesse Duffield 5eefe5b5b1 Merge pull request #1919 from jesseduffield/more-documentation 2022-05-07 16:12:18 +10:00
Jesse Duffield 3bf0c9ef44 more documentation 2022-05-07 16:02:04 +10:00
Jesse Duffield cd5b041b0f clearer separation of concerns when bootstrapping application 2022-05-07 16:02:04 +10:00
Jesse Duffield cf80978f15 Merge pull request #1918 from RhydianJenkins/name-bug-fix 2022-05-07 11:02:24 +10:00
Jesse Duffield 9956a5bfb2 Merge pull request #1912 from Ryooooooga/feature/edit-hunk 2022-05-07 11:01:26 +10:00
Rhydian Jenkins 7d495573ab Bug fix - unused name param 2022-05-06 23:00:44 +01:00
Ryooooooga b07e0ea032 fix: fix context of edit hunk 2022-05-06 21:58:40 +09:00
Ryooooooga d458e78d95 feat: add ability to edit hunk 2022-05-06 21:53:00 +09:00
Jesse Duffield 0940e0182b Merge pull request #1870 from mark2185/feature/stash-unstaged 2022-05-06 20:17:33 +10:00
Jesse Duffield f7fae0b82e Merge pull request #1869 from mark2185/feature/unset-upstream 2022-05-06 20:14:13 +10:00
Luka Markušić 8b8a405b7c Update stashing tests to apply instead of pop 2022-05-06 12:03:18 +02:00
Jesse Duffield 6253258d4b Merge pull request #1913 from Ryooooooga/feature/japanese 2022-05-06 08:40:43 +10:00
Jesse Duffield 593eb19ca4 Merge pull request #1910 from Ryooooooga/feature/named-author-colors 2022-05-06 08:39:26 +10:00
Jesse Duffield 8c6260ed8d Merge pull request #1914 from Ryooooooga/feature/fix-crash-empty-panel 2022-05-06 08:38:06 +10:00
Ryooooooga 2eb866fc62 fix: fix a crash when pressing enter in empty commits, reflog, or stash panel 2022-05-05 21:41:44 +09:00
Ryooooooga cda359fbc9 feat(i18n): japanese translation 2022-05-05 18:42:27 +09:00
Ryooooooga 494368a241 feat: accept named colors for gui.authorColors 2022-05-04 19:03:00 +09:00
Jesse Duffield f143d04d87 Merge pull request #1904 from jesseduffield/yank-author 2022-05-01 15:11:00 +10:00
Jesse Duffield 4dd09ee0d5 allow copying commit author to clipboard 2022-05-01 14:14:29 +10:00
Jesse Duffield d85f4792af Merge pull request #1894 from Ryooooooga/feature/icons 2022-05-01 12:21:32 +10:00
Ryooooooga 5524f007f3 docs(Config.md): add docs about nerd fonts 2022-04-30 23:13:49 +09:00
Jesse Duffield babb9d8656 Merge pull request #1891 from Ryooooooga/feature/improve-default-edit-command-template 2022-04-30 13:53:24 +10:00
Ryooooooga db3568e4f2 chore(gui): remove todo 2022-04-29 17:53:43 +09:00
Ryooooooga 86af186683 feat(file_icons.go): add Cargo.toml icons 2022-04-24 18:23:14 +09:00
Ryooooooga e5730cb80b fix: improve default editCommandTemplate 2022-04-23 17:39:12 +09:00
Ryooooooga f972d6ae68 feat(gui): show remote icons 2022-04-23 12:25:42 +09:00
Ryooooooga 11d0e7e17d feat(gui): show branch icons 2022-04-23 12:25:41 +09:00
Ryooooooga cb13fe7f46 feat(gui): show commit icons 2022-04-23 12:25:41 +09:00
Ryooooooga b07aeda5a6 feat(gui): show file icons 2022-04-23 12:25:40 +09:00
Jesse Duffield 09bc6f2aef Merge pull request #1885 from briandipalma/patch-1 2022-04-21 08:34:33 +10:00
Brian Di Palma a5d4eccfa6 Update Custom_Command_Keybindings.md 2022-04-19 12:17:06 +01:00
Luka Markušić eb038d1950 Update stashPop and stashDrop setups 2022-04-18 10:29:36 +02:00
Luka Markušić 196d56d014 Add integration tests 2022-04-18 10:22:25 +02:00
Jesse Duffield 8b103b16bd add highlighting docs 2022-04-18 11:08:54 +10:00
Jesse Duffield dfb293c985 better upstream changes presentation 2022-04-18 11:03:28 +10:00
Jesse Duffield 9b947b74a2 allow hiding bottom line 2022-04-18 09:58:36 +10:00
Luka Markušić f5f6409c27 Remove stash_Copy test 2022-04-17 12:21:43 +02:00
Luka Markušić bd9daf80b7 Add integration tests 2022-04-17 11:08:36 +02:00
Jesse Duffield 3477cbc81f better weight distribution in window arrangement 2022-04-17 12:48:04 +10:00
Jesse Duffield 2fa6d8037c always show list counts 2022-04-16 17:29:17 +10:00
Jesse Duffield d3b6acf096 restore highlighting of popup messages 2022-04-16 17:29:17 +10:00
Jesse Duffield e68093fe99 add scrollbars 2022-04-16 17:29:17 +10:00
Jesse Duffield b838b74801 do not highlight line if there are no items to display 2022-04-16 15:19:32 +10:00
Jesse Duffield 00afa30ebf better appearance for reverse attribute 2022-04-16 15:19:32 +10:00
Jesse Duffield 6a153acc8f clearer highlighting of current line 2022-04-16 15:19:32 +10:00
Ryooooooga b3e18bd258 fix(loaders/file.go): fix not to trim renamed file names 2022-04-15 08:58:49 +10:00
Jesse Duffield 90c9c46ffc update integration test notes 2022-04-15 08:54:45 +10:00
Mukhlis Akbarrudin 21336d3aa2 refactor: explicitly add alternative keybinding 2022-04-15 08:23:27 +10:00
Mukhlis Akbarrudin af5f4af6c0 docs: add alternative keybinding to scroll up/down main panel 2022-04-15 08:23:27 +10:00
Luka Markušić 1ae2dc9941 The four horsemen of stashing 2022-04-14 21:45:55 +02:00
Luka Markušić e114b5b5e8 Refresh BRANCHES and COMMITS after (un)staging upstream 2022-04-13 15:54:32 +02:00
Luka Markušić c4b958e3fd There's gotta be a better way for initial content 2022-04-13 15:21:01 +02:00
Luka Markušić f83308c8df Add option to (un)set upstream for a local branch 2022-04-11 14:04:06 +02:00
TheBlob42 d0e099d2fc doc: add missing provider 2022-04-11 17:17:40 +10:00
TheBlob42 bcc0466498 feat: pull request support for bitbucket server 2022-04-11 17:17:40 +10:00
Luka Markušić 6f7038c827 Add option to stash only unstaged files 2022-04-10 09:35:59 +02:00
Luka Markušić 58ed23a47a Make worktrees work 2022-04-09 09:53:03 +10:00
Jesse Duffield 336f2772e8 bump gocui 2022-04-08 12:15:23 +10:00
Jesse Duffield 8433367dac Update CONTRIBUTING.md 2022-04-07 08:41:02 +10:00
casswedson b7928042f0 chore: typo hunting ft. codespell 2022-04-06 08:52:41 +10:00
Ryooooooga 3b5a019e1a feat(merge_panel): Add open/edit files in merge conflict panel 2022-04-06 08:27:03 +10:00
Ryooooooga 53257db99d fix: fix diff of renamed files 2022-04-06 08:26:13 +10:00
Ryooooooga 954d1a8147 fix commit description 2022-04-02 17:04:42 +11:00
Ryooooooga 09d24ebd1d fix test 2022-04-02 17:04:42 +11:00
Ryooooooga 4835fc00b8 introduce Ref interface 2022-04-02 17:04:42 +11:00
Ryooooooga 30be50b641 add Commit.ParentRefName() 2022-04-02 17:04:42 +11:00
Ryooooooga 99ecc1cfdf fix loaders 2022-04-02 17:04:42 +11:00
Ryooooooga 86c259623c feat: fix permission problem of temp dirs 2022-04-02 08:48:38 +11:00
Ryooooooga 2fbb52fa2c chore: remove dead code 2022-04-02 08:48:38 +11:00
Moritz Haase e35ab3c5fe pkg/gui: Use 'alert' popups instead of 'confirm' popups where appropriate
Invocations of 'IPopupHandler::Confirm()' that use neither 'HandleConfirm' nor
'HandleClose' can be replaced by 'Alert()'.
2022-03-30 20:13:43 +11:00
Moritz Haase 8fb2acc224 pkg/gui: Rename IPopupHandler::Ask() to Confirm()
Follow the JavaScript naming scheme for user interaction (alert, prompt,
confirm) as discussed in #1832.
2022-03-30 20:13:43 +11:00
Moritz Haase f2fb6453a1 pkg/gui: Show notification popup when update was successful
Show a proper notification popup once an update has been installed successfully
so the user knows we're done (so far a popup is only shown if an error occurred).
The popup also reminds him to restart lazygit for the changes to take effect.
2022-03-28 09:13:34 +11:00
Moritz Haase b7079634ee i18n: Make user-facing strings in the updater translatable
Convert a number of static (english) user-facing string in the updater code to
translatable ones.
2022-03-28 09:13:34 +11:00
Moritz Haase 9bccc20888 pkg/gui: Add support for 'notification' popups
Add a new 'Notification()' method to 'IPopupHandler' that makes it easier to
show a modal info message to the user. This is simply a convenience wrapper
around 'Ask()', so the popup can be closed using both 'Enter' and 'ESC'.
2022-03-28 09:13:34 +11:00
Adrian ac406f57ff [fix] Fixed Dockerfile 2022-03-27 22:03:16 +11:00
Jesse Duffield 5a0ac6fe67 Update README.md 2022-03-27 19:36:08 +11:00
Jesse Duffield b3636a537b reduce glitchiness of patch building mode 2022-03-27 19:18:07 +11:00
Jesse Duffield 48a244a923 update cheatsheets 2022-03-27 18:50:49 +11:00
Jesse Duffield 7c3d14ee19 allow amending and reverting commits when filtering by file 2022-03-27 18:50:29 +11:00
Jesse Duffield 897c4402a4 better colour 2022-03-27 18:16:16 +11:00
Jesse Duffield 860d1e0145 fix copy for merge confirmation 2022-03-27 18:16:16 +11:00
Jesse Duffield e011acbab2 better logging for stash flow 2022-03-27 18:16:16 +11:00
Jesse Duffield 3e5d4b2c74 add menu keybindings for various things 2022-03-27 18:16:16 +11:00
Jesse Duffield e94312b664 use colour 2022-03-27 18:16:16 +11:00
Jesse Duffield 9c226eed37 allow menu to store keybindings for quick menu navigation 2022-03-27 18:16:16 +11:00
Jesse Duffield e43ce23642 require hitting escape to exit menu panel 2022-03-27 18:16:16 +11:00
Jesse Duffield 0dfb7c08b7 remove controllers struct 2022-03-27 18:16:16 +11:00
Jesse Duffield f0a4dcfdc3 refactor menu context 2022-03-27 18:16:16 +11:00
Jesse Duffield 98e7ec0905 add type alias for Key 2022-03-27 18:16:16 +11:00
Jesse Duffield 7128d822cb show stdout in error message if stderr is blank 2022-03-27 14:58:20 +11:00
Jesse Duffield 20ec6d98ad refactor integration tests 2022-03-27 14:58:20 +11:00
Jesse Duffield 2b3d457aa4 honour push.default matching config value 2022-03-27 14:58:20 +11:00
Jesse Duffield ae10a5ea88 add git fetch prune integration test 2022-03-27 10:28:46 +11:00
Moritz Haase 4abd80e2c4 pkg/gui: Fix crash if auto-fetch interval is non-positive
Check whether the auto-fetch interval configured is actually positive before
starting the background fetcher. If it is not, an error is logged. Also improve
the config option documentation a bit to make it easier to understand how to
disable auto-fetch.
2022-03-27 10:14:33 +11:00
Moritz Haase 240483953f config: Add option 'git.autoRefresh' to en-/disable auto-refresh
Adds a new 'autoRefresh' option to the 'git' config section that allows user to
disable auto-refresh (defaults to on). If auto-refresh is enabled, the
refreshInterval is now checked before starting the timer to prevent crashes when
it is non-positive.

Fixes #1417
2022-03-27 10:14:33 +11:00
Jesse Duffield 51baa8c17d update cheatsheet 2022-03-26 18:00:46 +11:00
Jesse Duffield 102c33433b remove dead code 2022-03-26 18:00:46 +11:00
Jesse Duffield fe87114074 don't hide transient views upon losing focus 2022-03-26 18:00:46 +11:00
Jesse Duffield ad7703df65 show namesake for child views 2022-03-26 18:00:46 +11:00
Jesse Duffield 13b90ac37f support viewing commits of reflog entry and show better view title 2022-03-26 18:00:46 +11:00
Jesse Duffield e039429885 better wording again 2022-03-26 17:22:42 +11:00
Jesse Duffield e0b05f4464 fix cherry picking bug 2022-03-26 17:22:42 +11:00
Jesse Duffield 077b6eb8a3 refactor to make code clearer 2022-03-26 17:22:42 +11:00
Jesse Duffield 45dab51214 add basic commits controller for handling actions that apply to all commit contexts 2022-03-26 17:22:42 +11:00
Luka Markušić 540edb0bf4 Add copy commit attributes option 2022-03-26 17:22:42 +11:00
Crystal-RainSlide bbaa651943 Update chinese.go 2022-03-26 13:38:22 +11:00
Jesse Duffield f9979879a1 no more naked returns 2022-03-25 23:27:28 +11:00
Jesse Duffield cf74c2cf96 reorder 2022-03-24 20:14:41 +11:00
Jesse Duffield 43d3f2bcb6 refactor todo file generation 2022-03-24 20:14:41 +11:00
Jesse Duffield 99e55725fb simplify 2022-03-24 20:14:41 +11:00
Jesse Duffield 340a145bc8 refactor cheatsheet generator 2022-03-24 20:14:41 +11:00
Jesse Duffield cb26c7a1f2 more things 2022-03-24 20:14:41 +11:00
Jesse Duffield e392b9f86a no more filterThenMap 2022-03-24 20:14:41 +11:00
Jesse Duffield 67a76523fb rename 2022-03-24 20:14:41 +11:00
Jesse Duffield 94a53484a1 would you believe that I'm adding even more generics 2022-03-24 20:14:41 +11:00
Jesse Duffield 1b75ed3740 many more generics 2022-03-24 20:14:41 +11:00
Jesse Duffield bf4f06ab4e more generics 2022-03-24 20:14:41 +11:00
Jesse Duffield eda8f4a5d4 lots more generics 2022-03-24 20:14:41 +11:00
Jesse Duffield c7a629c440 make more use of generics 2022-03-24 20:14:41 +11:00
Jesse Duffield dde30fa104 add gone branches status 2022-03-24 17:50:25 +11:00
Jesse Duffield 13a9bbb984 skip flakey bisect test 2022-03-24 09:45:27 +11:00
Jesse Duffield f113ff21bf add confirmation before performing undo or redo action 2022-03-24 09:45:27 +11:00
Jesse Duffield 12ecd665c8 safe reword 2022-03-23 23:43:21 +11:00
Jesse Duffield cc5d13c833 allow adding whole diff to patch
this was causing a panic

add integration test for toggling all commit files
2022-03-23 23:36:58 +11:00
Jesse Duffield 5ded030a88 diff colour for reflog commits 2022-03-23 18:53:29 +11:00
Moritz Haase 8fb47fb7d6 pkg/commands: Don't duplicate line breaks when retrieving commit message
When using the "copy commit message to clipboard" action, the message will end
up in the clipboard with duplicate line breaks. The same issue also affects the
"Reword Commit" command. GetCommitMessage(), the function used to retrieve the
commit message first splits the output returned by git into separate lines -
without removing the line breaks. After removing the first line (which contains
the commit SHA), it joins the lines of the message itself back together - adding
a second set of line breaks along the way. Stop this from happening.

Fixes #1808.
2022-03-23 08:19:17 +11:00
Moritz Haase ac687a5a2a docs: Add section about code formatting to contributors guide
Explain that gofumpt is used instead of gofmt and how to configure VSCode to use it.
2022-03-23 08:18:06 +11:00
Jesse Duffield a34bdf1a04 update linters 2022-03-19 12:12:57 +11:00
Jesse Duffield d93fef4c61 use generics to DRY up context code 2022-03-19 12:12:57 +11:00
Moritz Haase 4b56d428ff pkg/updates: Fix resource availability check in Updater
When trying to download an update, a 'Could not find any binary at ...' error
message is shown erroneously. This happens since when checking the availability,
a response code of 403 ('Forbidden') instead of 200 ('OK') is expected. Since
'http.Head()' handles redirects automatically, there is no need to also accept
3xx status codes.

Fixes #1450.
2022-03-18 22:19:45 +11:00
Jesse Duffield 4fde97b066 update go to v1.18 2022-03-18 21:36:28 +11:00
Jesse Duffield 205c7d60aa update cheatsheets 2022-03-17 19:13:40 +11:00
Jesse Duffield 729da3549a go mod vendor 2022-03-17 19:13:40 +11:00
Jesse Duffield 7bdd7088e7 prevent early exit from setup script 2022-03-17 19:13:40 +11:00
Jesse Duffield 31ab43d0c5 add host helper 2022-03-17 19:13:40 +11:00
Jesse Duffield fb3752c11f clean up keybindings menu 2022-03-17 19:13:40 +11:00
Jesse Duffield 36c149836a softcode keybinding 2022-03-17 19:13:40 +11:00
Jesse Duffield ea503633aa move keybindings 2022-03-17 19:13:40 +11:00
Jesse Duffield 59d4df2a44 fix click handling 2022-03-17 19:13:40 +11:00
Jesse Duffield cf00949b85 fix integration tests 2022-03-17 19:13:40 +11:00
Jesse Duffield c7b03bd3c2 rename handlers 2022-03-17 19:13:40 +11:00
Jesse Duffield 1ad4518d35 update cheatsheet 2022-03-17 19:13:40 +11:00
Jesse Duffield 8fd6338527 move workspace reset menu into controller 2022-03-17 19:13:40 +11:00
Jesse Duffield ee1337b931 add remote branches controller 2022-03-17 19:13:40 +11:00
Jesse Duffield d543e767d4 update cheatsheets 2022-03-17 19:13:40 +11:00
Jesse Duffield d59c0e2725 remove dead code 2022-03-17 19:13:40 +11:00
Jesse Duffield 675510ba53 fix integration test 2022-03-17 19:13:40 +11:00
Jesse Duffield a3885e8ea3 abbrev all commits to length 40 for consistency 2022-03-17 19:13:40 +11:00
Jesse Duffield 4805db7d97 use correct context 2022-03-17 19:13:40 +11:00
Jesse Duffield 3e26f39dee remove dead code 2022-03-17 19:13:40 +11:00
Jesse Duffield ef7c4c9ca9 refactor custom commands
more custom command refactoring
2022-03-17 19:13:40 +11:00
Jesse Duffield 952a4f3f23 prevent interrupting confirmation panel 2022-03-17 19:13:40 +11:00
Jesse Duffield 46e9946854 refactor credential handling 2022-03-17 19:13:40 +11:00
Jesse Duffield d0805616e4 move function 2022-03-17 19:13:40 +11:00
Jesse Duffield 1a7fe2835c integration test for multiline commit message 2022-03-17 19:13:40 +11:00
Jesse Duffield bff5351ab3 better naming 2022-03-17 19:13:40 +11:00
Jesse Duffield d991d74b06 add commit message controller 2022-03-17 19:13:40 +11:00
Jesse Duffield 120078f011 use PopContext 2022-03-17 19:13:40 +11:00
Jesse Duffield ecaff7fc6c add commit files controller 2022-03-17 19:13:40 +11:00
Jesse Duffield 85f2319897 refactor custom commands panel 2022-03-17 19:13:40 +11:00
Jesse Duffield c685a413c9 stash controller 2022-03-17 19:13:40 +11:00
Jesse Duffield a643957f89 include stash in commitish controller 2022-03-17 19:13:40 +11:00
Jesse Duffield e842d1bc9e move git flow 2022-03-17 19:13:40 +11:00
Jesse Duffield 8a555dd62e refactor 2022-03-17 19:13:40 +11:00
Jesse Duffield bef26b9634 add common commit controller 2022-03-17 19:13:40 +11:00
Jesse Duffield 574c5ca0de add subcommits controller 2022-03-17 19:13:40 +11:00
Jesse Duffield 1253100431 cleanup 2022-03-17 19:13:40 +11:00
Jesse Duffield eab00de273 reflog controller 2022-03-17 19:13:40 +11:00
Jesse Duffield 371b8d638b more consistent naming 2022-03-17 19:13:40 +11:00
Jesse Duffield 55af07a1bb fix CI 2022-03-17 19:13:40 +11:00
Jesse Duffield 33a223e981 remove dead code 2022-03-17 19:13:40 +11:00
Jesse Duffield 943a8e83da ensure we retain state when returning to submodule parent 2022-03-17 19:13:40 +11:00
Jesse Duffield 94d66b267d defend against view not yet having a context defined against it 2022-03-17 19:13:40 +11:00
Jesse Duffield 41527270ed appease linter 2022-03-17 19:13:40 +11:00
Jesse Duffield 3188526ecb fix cheatsheet crash 2022-03-17 19:13:40 +11:00
Jesse Duffield 722410aded refactor controllers 2022-03-17 19:13:40 +11:00
Jesse Duffield b93b8cc00a controller for viewing sub commits 2022-03-17 19:13:40 +11:00
Jesse Duffield cd31a762b9 rename OSCommand field to os 2022-03-17 19:13:40 +11:00
Jesse Duffield d82f175e79 refactor contexts 2022-03-17 19:13:40 +11:00
Jesse Duffield 145c69d9ae working again 2022-03-17 19:13:40 +11:00
Jesse Duffield 482bdc4f1e more refactoring 2022-03-17 19:13:40 +11:00
Jesse Duffield 8e3484d8e9 add global controller 2022-03-17 19:13:40 +11:00
Jesse Duffield 226985bf76 refactor keybindings 2022-03-17 19:13:40 +11:00
Jesse Duffield 2db4636815 no more indirection 2022-03-17 19:13:40 +11:00
Jesse Duffield 2a1e3faa0c resetting controllers on new repo 2022-03-17 19:13:40 +11:00
Jesse Duffield eb056576cf fix integration test 2022-03-17 19:13:40 +11:00
Jesse Duffield c703cd8f88 fix suggestions panel 2022-03-17 19:13:40 +11:00
Jesse Duffield 0a8cff6ab6 some more refactoring 2022-03-17 19:13:40 +11:00
Jesse Duffield e2f5fe1016 pretty sure we can rely on our views existing before our contexts do 2022-03-17 19:13:40 +11:00
Jesse Duffield 182c999ee0 fix linting 2022-03-17 19:13:40 +11:00
Jesse Duffield b5515da00b move commit files context into new structure 2022-03-17 19:13:40 +11:00
Jesse Duffield c084abb378 move more view model logic into the files view model 2022-03-17 19:13:40 +11:00
Jesse Duffield 8ea7b7a62e migrate files context to new structure 2022-03-17 19:13:40 +11:00
Jesse Duffield 09dc160da9 cleaning up 2022-03-17 19:13:40 +11:00
Jesse Duffield e187293456 moving more into controllers package 2022-03-17 19:13:40 +11:00
Jesse Duffield b04038d08f no need to invoke sync controller here 2022-03-17 19:13:40 +11:00
Jesse Duffield 55b393c929 always fast forward 2022-03-17 19:13:40 +11:00
Jesse Duffield f0c81ea6dc remove redundant popup checks 2022-03-17 19:13:40 +11:00
Jesse Duffield f97de692e3 move helpers into their own struct 2022-03-17 19:13:40 +11:00
Jesse Duffield 2692637fbe standardise naming 2022-03-17 19:13:40 +11:00
Jesse Duffield 77d0732fa8 add actions abstraction 2022-03-17 19:13:40 +11:00
Jesse Duffield 51547e3822 move all refresh code into the one file 2022-03-17 19:13:40 +11:00
Jesse Duffield e363606fb6 move context keys into context package 2022-03-17 19:13:40 +11:00
Jesse Duffield 138be04e65 refactor contexts code 2022-03-17 19:13:40 +11:00
Jesse Duffield 1a74ed3214 avoid deadlock 2022-03-17 19:13:40 +11:00
Jesse Duffield cb0d3a480a use type switch instead of type key 2022-03-17 19:13:40 +11:00
Jesse Duffield a2318d75b5 fix some things 2022-03-17 19:13:40 +11:00
Jesse Duffield 1dd7307fde start moving commit panel handlers into controller
more

and more

move rebase commit refreshing into existing abstraction

and more

and more

WIP

and more

handling clicks

properly fix merge conflicts

update cheatsheet

lots more preparation to start moving things into controllers

WIP

better typing

expand on remotes controller

moving more code into controllers
2022-03-17 19:13:40 +11:00
Jesse Duffield a90b6efded start refactoring gui 2022-03-17 19:13:40 +11:00
Jesse Duffield fa8571e1f4 rename field 2022-03-17 19:01:28 +11:00
David Roman d8d0d4686d Only read env once when recording dirs 2022-03-17 19:01:28 +11:00
David Roman b8fc829f86 Record current directory on switch 2022-03-17 19:01:28 +11:00
Jesse Duffield 950bb5090d Update README.md 2022-03-17 18:08:26 +11:00
4986 changed files with 30833 additions and 15029 deletions
+1 -1
View File
@@ -25,4 +25,4 @@ jobs:
uses: "pascalgn/automerge-action@135f0bdb927d9807b5446f7ca9ecc2c51de03c4a"
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
MERGE_METHOD: rebase
MERGE_METHOD: rebase
+2 -2
View File
@@ -16,13 +16,13 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.16.x
go-version: 1.18.x
- name: Run goreleaser
uses: goreleaser/goreleaser-action@v1
env:
GITHUB_TOKEN: ${{secrets.GITHUB_API_TOKEN}}
homebrew:
runs-on: macos-latest
runs-on: ubuntu-latest
steps:
- name: Bump Homebrew formula
uses: dawidd6/action-homebrew-bump-formula@v3
+69 -10
View File
@@ -1,5 +1,8 @@
name: Continuous Integration
env:
GO_VERSION: 1.18
on:
push:
branches:
@@ -7,13 +10,18 @@ on:
pull_request:
jobs:
ci:
unit-tests:
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- windows-latest
include:
- os: ubuntu-latest
cache_path: ~/.cache/go-build
- os: windows-latest
cache_path: ~\AppData\Local\go-build
name: ci - ${{matrix.os}}
runs-on: ${{matrix.os}}
env:
@@ -24,17 +32,49 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.16.x
go-version: 1.18.x
- name: Cache build
uses: actions/cache@v3
with:
path: |
${{matrix.cache_path}}
~/go/pkg/mod
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-test
restore-keys: |
${{runner.os}}-go-
- name: Test code
# we're passing -short so that we skip the integration tests, which will be run in parallel below
run: |
go test ./... -short
integration-tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
parallelism: [5]
index: [0,1,2,3,4]
name: "Integration Tests (${{ matrix.index }}/${{ matrix.parallelism }})"
env:
GOFLAGS: -mod=vendor
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.18.x
- name: Cache build
uses: actions/cache@v1
with:
path: ~/.cache/go-build
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-test
restore-keys: |
${{runner.os}}-go-
- name: Test code
run: |
bash ./test.sh
PARALLEL_TOTAL=${{ matrix.parallelism }} PARALLEL_INDEX=${{ matrix.index }} go test pkg/gui/gui_test.go
build:
runs-on: ubuntu-latest
env:
@@ -46,11 +86,13 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.16.x
go-version: 1.18.x
- name: Cache build
uses: actions/cache@v1
with:
path: ~/.cache/go-build
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-build
restore-keys: |
${{runner.os}}-go-
@@ -74,11 +116,13 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.16.x
go-version: 1.18.x
- name: Cache build
uses: actions/cache@v1
with:
path: ~/.cache/go-build
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-build
restore-keys: |
${{runner.os}}-go-
@@ -87,11 +131,26 @@ jobs:
go run scripts/cheatsheet/main.go check
lint:
runs-on: ubuntu-latest
env:
GOFLAGS: -mod=vendor
steps:
- name: Checkout
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.18.x
- name: Cache build
uses: actions/cache@v1
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-test
restore-keys: |
${{runner.os}}-go-
- name: Lint
uses: golangci/golangci-lint-action@v2
uses: golangci/golangci-lint-action@v3.1.0
with:
version: latest
- name: Format code
+28
View File
@@ -0,0 +1,28 @@
# see https://github.com/JamesIves/github-sponsors-readme-action
name: Generate Sponsors README
on:
push:
branches:
- master
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v2
- name: Generate Sponsors 💖
uses: JamesIves/github-sponsors-readme-action@v1.0.8
with:
token: ${{ secrets.SPONSORS_TOKEN }}
file: 'README.md'
if: ${{ github.repository == 'jesseduffield/lazygit' }}
- name: Commit and push if changed
run: |-
git diff
git config --global user.email "actions@users.noreply.github.com"
git config --global user.name "README-bot"
git add README.md
git commit -m "Updated README.md" || exit 0
git push
+6 -1
View File
@@ -23,16 +23,21 @@ lazygit.exe
# Exceptions
!.gitignore
!.goreleaser.yml
!.golangci.yml
!.circleci/
!.github/
!.vscode/
# these are for our integration tests
!.git_keep
!.gitmodules_keep
test/git_server/data
test/integration/*/actual/
test/integration/*/actual_remote/
test/integration/*/used_config/
# these sample hooks waste too much space
test/integration/*/expected/**/hooks/
test/integration/*/expected_remote/**/hooks/
oryxBuildBinary
__debug_bin
+29
View File
@@ -0,0 +1,29 @@
linters:
disable:
- structcheck # gives false positives
enable:
- gofumpt
- thelper
- goimports
- tparallel
- wastedassign
- exportloopref
- unparam
- prealloc
- unconvert
- exhaustive
- makezero
- nakedret
# - goconst # TODO: enable and fix issues
fast: false
linters-settings:
exhaustive:
default-signifies-exhaustive: true
nakedret:
# the gods will judge me but I just don't like naked returns at all
max-func-lines: 0
run:
go: 1.18
+1
View File
@@ -0,0 +1 @@
disableStartupPopups: true
+36
View File
@@ -0,0 +1,36 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Lazygit",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "main.go",
"args": ["--debug", "--use-config-file=.vscode/debugger_config.yml"],
"console": "integratedTerminal",
"presentation": {
"hidden": true
}
},
{
"name": "Tail Lazygit logs",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "main.go",
"args": ["--logs", "--use-config-file=.vscode/debugger_config.yml"],
"console": "integratedTerminal",
"presentation": {
"hidden": true
}
}
],
"compounds": [
{
"name": "Run with logs",
"configurations": ["Tail Lazygit logs", "Debug Lazygit"],
"stopAll": true
}
]
}
+42 -1
View File
@@ -6,6 +6,10 @@ 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.
## PR walkthrough
[This video](https://www.youtube.com/watch?v=kNavnhzZHtk) walks through the process of adding a small feature to lazygit. If you have no idea where to start, watching that video is a good first step.
## All code changes happen through Pull Requests
Pull requests are the best way to propose changes to the codebase. We actively
@@ -50,6 +54,21 @@ var _ MyInterface = &MyStruct{}
This makes the intent clearer and means that if we fail to satisfy the interface we'll get an error in the file that needs fixing.
### Code Formatting
To check code formatting [gofumpt](https://pkg.go.dev/mvdan.cc/gofumpt#section-readme) (which is a bit stricter than [gofmt](https://pkg.go.dev/cmd/gofmt)) is used.
VSCode will format the code correctly if you tell the Go extension to use `gofumpt` via your [`settings.json`](https://code.visualstudio.com/docs/getstarted/settings#_settingsjson)
by setting [`formatting.gofumpt`](https://github.com/golang/tools/blob/master/gopls/doc/settings.md#gofumpt-bool) to `true`:
```jsonc
// .vscode/settings.json
{
"gopls": {
"formatting.gofumpt": true
}
}
```
## Internationalisation
Boy that's a hard word to spell. Anyway, lazygit is translated into several languages within the pkg/i18n package. If you need to render text to the user, you should add a new field to the TranslationSet struct in `pkg/i18n/english.go` and add the actual content within the `EnglishTranslationSet()` method in the same file. Although it is appreciated if you translate the text into other languages, it's not expected of you (google translate will likely do a bad job anyway!).
@@ -58,12 +77,34 @@ Boy that's a hard word to spell. Anyway, lazygit is translated into several lang
The easiest way to debug lazygit is to have two terminal tabs open at once: one for running lazygit (via `go run main.go -debug` in the project root) and one for viewing lazygit's logs (which can be done via `go run main.go --logs` or just `lazygit --logs`).
From most places in the codebase you have access to a logger e.g. `gui.Log.Warn("blah")`
From most places in the codebase you have access to a logger e.g. `gui.Log.Warn("blah")`.
If you find that the existing logs are too noisy, you can set the log level with e.g. `LOG_LEVEL=warn go run main.go -debug` and then only use `Warn` logs yourself.
If you need to log from code in the vendor directory (e.g. the `gocui` package), you won't have access to the logger, but you can easily add logging support by adding the following:
```go
func newLogger() *logrus.Entry {
// REPLACE THE BELOW PATH WITH YOUR ACTUAL LOG PATH (YOU'LL SEE THIS PRINTED WHEN YOU RUN `lazygit --logs`
logPath := "/Users/jesseduffield/Library/Application Support/jesseduffield/lazygit/development.log"
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
panic("unable to log to file")
}
logger := logrus.New()
logger.SetLevel(logrus.WarnLevel)
logger.SetOutput(file)
return logger.WithFields(logrus.Fields{})
}
var Log = newLogger()
...
Log.Warn("blah")
```
If you keep having to do some setup steps to reproduce an issue, read the Testing section below to see how to create an integration test by recording a lazygit session. It's pretty easy!
### VSCode debugger
If you want to trigger a debug session from VSCode, you can use the following snippet. Note that the `console` key is, at the time of writing, still an experimental feature.
```jsonc
+8 -6
View File
@@ -2,16 +2,18 @@
# docker build -t lazygit .
# docker run -it lazygit:latest /bin/sh
FROM golang:1.14-alpine3.11
FROM golang:1.18 as build
WORKDIR /go/src/github.com/jesseduffield/lazygit/
COPY ./ .
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build
FROM alpine:3.11
RUN apk add -U git xdg-utils
FROM alpine:3.15
RUN apk add --no-cache -U git xdg-utils
WORKDIR /go/src/github.com/jesseduffield/lazygit/
COPY --from=0 /go/src/github.com/jesseduffield/lazygit /go/src/github.com/jesseduffield/lazygit
COPY --from=0 /go/src/github.com/jesseduffield/lazygit/lazygit /bin/
COPY --from=build /go/src/github.com/jesseduffield/lazygit ./
COPY --from=build /go/src/github.com/jesseduffield/lazygit/lazygit /bin/
RUN echo "alias gg=lazygit" >> ~/.profile
ENTRYPOINT [ "lazygit" ]
+35 -26
View File
File diff suppressed because one or more lines are too long
+48 -38
View File
@@ -22,24 +22,25 @@ 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'
language: 'auto' # one of 'auto' | 'en' | 'zh' | 'pl' | 'nl' | 'ja' | 'ko'
timeFormat: '02 Jan 06 15:04 MST' # https://pkg.go.dev/time#Time.Format
theme:
lightTheme: false # For terminals with a light background
activeBorderColor:
- white
- green
- bold
inactiveBorderColor:
- green
- white
optionsTextColor:
- blue
selectedLineBgColor:
- default
- blue # set to `default` to have no background colour
selectedRangeBgColor:
- blue
cherryPickedCommitBgColor:
- blue
cherryPickedCommitFgColor:
- cyan
cherryPickedCommitFgColor:
- blue
unstagedChangesColor:
- red
commitLength:
@@ -50,8 +51,11 @@ gui:
showFileTree: true # for rendering changes files in a tree format
showListFooter: true # for seeing the '5 of 20' message in list panels
showRandomTip: true
showBottomLine: true # for hiding the bottom information line (unless it has important information to tell you)
showCommandLog: true
showIcons: false
commandLogSize: 8
splitDiff: 'auto' # one of 'auto' | 'always'
git:
paging:
colorArg: always
@@ -71,8 +75,11 @@ git:
# one of always, never, when-maximised
# this determines whether the git graph is rendered in the commits panel
showGraph: 'when-maximised'
# displays the whole git graph by default in the commits panel (equivalent to passing the `--all` argument to `git log`)
showWholeGraph: false
skipHookPrefix: WIP
autoFetch: true
autoRefresh: 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
@@ -81,11 +88,11 @@ git:
diffContextSize: 3 # how many lines of context are shown around a change in diffs
os:
editCommand: '' # see 'Configuring File Editing' section
editCommandTemplate: '{{editor}} {{filename}}'
editCommandTemplate: ''
openCommand: ''
refresher:
refreshInterval: 10 # file/submodule refresh interval in seconds
fetchInterval: 60 # re-fetch interval in seconds
refreshInterval: 10 # File/submodule refresh interval in seconds. Auto-refresh can be disabled via option 'git.autoRefresh'.
fetchInterval: 60 # Re-fetch interval in seconds. Auto-fetch can be disabled via option 'git.autoFetch'.
update:
method: prompt # can be: prompt | background | never
days: 14 # how often an update is checked for
@@ -181,6 +188,7 @@ keybinding:
checkoutBranchByName: 'c'
forceCheckoutBranch: 'F'
rebaseBranch: 'r'
renameBranch: 'R'
mergeIntoCurrentBranch: 'M'
viewGitFlowOptions: 'i'
fastForward: 'f' # fast-forward this branch from its upstream
@@ -271,7 +279,7 @@ 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}}'
editCommandTemplate: '{{editor}} +{{line}} -- {{filename}}'
```
or
@@ -279,23 +287,23 @@ or
```yaml
os:
editCommand: 'code'
editCommandTemplate: '{{editor}} --goto {{filename}}:{{line}}'
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 default config directory, use `CONFIG_DIR="$HOME/.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
lazygit --use-config-file="$HOME/.base_lg_conf,$HOME/.light_theme_lg_conf"
or
LG_CONFIG_FILE="~/.base_lg_conf,~/.light_theme_lg_conf" lazygit
LG_CONFIG_FILE="$HOME/.base_lg_conf,$HOME/.light_theme_lg_conf" lazygit
```
### Recommended Config Values
@@ -348,9 +356,20 @@ gui:
- default
```
## Struggling to see selected line
## Highlighting the selected line
If you struggle to see the selected line I recommend using the reverse attribute on selected lines like so:
If you don't like the default behaviour of highlighting the selected line with a blue background, you can use the `selectedLineBgColor` and `selectedRangeBgColor` keys to customise the behaviour. If you just want to embolden the selected line (this was the original default), you can do the following:
```yaml
gui:
theme:
selectedLineBgColor:
- default
selectedRangeBgColor:
- default
```
You can also use the reverse attribute like so:
```yaml
gui:
@@ -361,25 +380,6 @@ gui:
- reverse
```
The following has also worked for a couple of people:
```yaml
gui:
theme:
activeBorderColor:
- white
- bold
inactiveBorderColor:
- white
selectedLineBgColor:
- reverse
- blue
```
Alternatively you may have bold fonts disabled in your terminal, in which case enabling bold fonts should solve the problem.
If you're still having trouble please raise an issue.
## Custom Author Color
Lazygit will assign a random color for every commit author in the commits pane by default.
@@ -389,7 +389,8 @@ You can customize the color in case you're not happy with the randomly assigned
```yaml
gui:
authorColors:
'John Smith': '#ff0000' # use red for John Smith
'John Smith': 'red' # use red for John Smith
'Alan Smithee': '#00ff00' # use green for Alan Smithee
```
You can use wildcard to set a unified color in case your are lazy to customize the color for every author or you just want a single color for all/other authors:
@@ -398,7 +399,7 @@ You can use wildcard to set a unified color in case your are lazy to customize t
gui:
authorColors:
# use red for John Smith
'John Smith': '#ff0000'
'John Smith': 'red'
# use blue for other authors
'*': '#0000ff'
```
@@ -417,6 +418,15 @@ gui:
![border example](../../assets/colored-border-example.png)
## Display Nerd Fonts Icons
If you are using [Nerd Fonts](https://www.nerdfonts.com), you can display icons.
```yaml
gui:
showIcons: true
```
## Keybindings
For all possible keybinding options, check [Custom_Keybindings.md](https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md)
@@ -467,7 +477,7 @@ services:
Where:
- `gitDomain` stands for the domain used by git itself (i.e. the one present on clone URLs), e.g. `git.work.com`
- `provider` is one of `github`, `bitbucket` or `gitlab`
- `provider` is one of `github`, `bitbucket`, `bitbucketServer`, `azuredevops` or `gitlab`
- `webDomain` is the URL where your git service exposes a web interface and APIs, e.g. `gitservice.work.com`
## Predefined commit message prefix
+20 -23
View File
@@ -49,6 +49,13 @@ customCommands:
filter: '.*{{index .PromptResponses 0}}/(?P<branch>.*)'
valueFormat: '{{ .branch }}'
labelFormat: '{{ .branch | green }}'
- key: '<f1>'
command: 'git reset --soft {{.CheckedOutBranch.UpstreamRemote}}'
context: 'files'
prompts:
- type: 'confirm'
title: "Confirm:"
body: "Are you sure you want to reset HEAD to {{.CheckedOutBranch.UpstreamRemote}}?"
```
Looking at the command assigned to the 'n' key, here's what the result looks like:
@@ -94,28 +101,18 @@ 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 |
| 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 | |
| _field_ | _description_ | _required_ |
| ------------ | -----------------------------------------------------------------------------------------------| ---------- |
| type | one of 'input', 'menu', or 'confirm' | 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 |
| body | (only applicable to 'confirm' prompts) the immutable body text 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 groups which are going to be kept from the command's output | yes |
| valueFormat | (only applicable to 'menuFromCommand' prompts) how to format matched groups from 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 | yes |
| labelFormat | (only applicable to 'menuFromCommand' prompts) how to format matched groups from 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 | no |
The permitted option fields are:
| _field_ | _description_ | _required_ |
@@ -154,7 +151,7 @@ SelectedCommitFile
CheckedOutBranch
```
To see what fields are available on e.g. the `SelectedFile`, see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/commands/models/file.go) (all the modelling lives in the same directory). Note that the custom commands feature does not guarantee backwards compatibility (until we hit lazygit version 1.0 of course) which means a field you're accessing on an object may no longer be available from one release to the next. Typically however, all you'll need is `{{.SelectedFile.Name}}`, `{{.SelectedLocalCommit.Sha}}` and `{{.SelectedBranch.Name}}`. In the future we will likely introduce a tighter interface that exposes a limited set of fields for each model.
To see what fields are available on e.g. the `SelectedFile`, see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/commands/models/file.go) (all the modelling lives in the same directory). Note that the custom commands feature does not guarantee backwards compatibility (until we hit lazygit version 1.0 of course) which means a field you're accessing on an object may no longer be available from one release to the next. Typically however, all you'll need is `{{.SelectedFile.Name}}`, `{{.SelectedLocalCommit.Sha}}` and `{{.SelectedLocalBranch.Name}}`. In the future we will likely introduce a tighter interface that exposes a limited set of fields for each model.
### Keybinding collisions
+7 -4
View File
@@ -31,6 +31,11 @@ git add .
git commit -am "myfile1"
```
Be sure to:
- ensure that by the end of the test you've got at least one commit in the repo, as we've had issues in the past when that wasn't the case.
- set the git user email and name as above so that your own user details aren't included in the snapshot.
## Running tests
### From a TUI
@@ -96,16 +101,14 @@ MODE=record go test ./pkg/gui -run /<test name>
The resulting directory will look like:
```
actual/ (the resulting repo after running the test, ignored by git)
expected/ (the 'snapshot' repo)
actual/ (the resulting repo(s) after running the test, ignored by git)
expected/ (the 'snapshot' repo(s))
config/ (need not be present)
test.json
setup.sh
recording.json
```
Feel free to create a hierarchy of directories in the `test/integration` directory to group tests by feature.
## Sandboxing
The integration tests serve a secondary purpose of providing a setup for easy sandboxing. If you want to run a test in sandbox mode (meaning the session won't be recorded and we won't create/update snapshots), go:
+137 -151
View File
@@ -5,26 +5,26 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## 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>ctrl+r</kbd>: switch to a recent repo
<kbd>pgup</kbd>: scroll up main panel (fn+up/shift+k)
<kbd>pgdown</kbd>: scroll down main panel (fn+down/shift+j)
<kbd>m</kbd>: view merge/rebase options
<kbd>ctrl+p</kbd>: view custom patch options
<kbd>P</kbd>: push
<kbd>p</kbd>: pull
<kbd>R</kbd>: refresh
<kbd>x</kbd>: open menu
<kbd>z</kbd>: undo (via reflog) (experimental)
<kbd>ctrl+z</kbd>: redo (via reflog) (experimental)
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode
<kbd>:</kbd>: execute custom command
<kbd>ctrl+s</kbd>: view filter-by-path options
<kbd>W</kbd>: open diff menu
<kbd>ctrl+e</kbd>: open diff menu
<kbd>@</kbd>: open command log menu
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
<kbd>:</kbd>: execute custom command
<kbd>z</kbd>: undo (via reflog) (experimental)
<kbd>ctrl+z</kbd>: redo (via reflog) (experimental)
<kbd>P</kbd>: push
<kbd>p</kbd>: pull
</pre>
## List Panel Navigation
@@ -35,79 +35,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd><</kbd>: scroll to top
<kbd>></kbd>: scroll to bottom
<kbd>/</kbd>: start search
<kbd>H</kbd>: scroll left
<kbd>L</kbd>: scroll right
<kbd>]</kbd>: next tab
<kbd>[</kbd>: previous tab
</pre>
## Branches Panel (Branches Tab)
<pre>
<kbd>space</kbd>: checkout
<kbd>o</kbd>: create pull request
<kbd>O</kbd>: create pull request options
<kbd>ctrl+y</kbd>: copy pull request URL to clipboard
<kbd>c</kbd>: checkout by name
<kbd>F</kbd>: force checkout
<kbd>n</kbd>: new branch
<kbd>d</kbd>: delete branch
<kbd>r</kbd>: rebase checked-out branch onto this branch
<kbd>M</kbd>: merge into currently checked out branch
<kbd>i</kbd>: show git-flow options
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>g</kbd>: view reset options
<kbd>R</kbd>: rename branch
<kbd>ctrl+o</kbd>: copy branch name to clipboard
<kbd>enter</kbd>: view commits
</pre>
## Branches Panel (Remote Branches (in Remotes tab))
<pre>
<kbd>esc</kbd>: Return to remotes list
<kbd>g</kbd>: view reset options
<kbd>enter</kbd>: view commits
<kbd>space</kbd>: checkout
<kbd>n</kbd>: new branch
<kbd>M</kbd>: merge into currently checked out branch
<kbd>d</kbd>: delete branch
<kbd>r</kbd>: rebase checked-out branch onto this branch
<kbd>u</kbd>: set as upstream of checked-out branch
</pre>
## Branches Panel (Remotes Tab)
<pre>
<kbd>f</kbd>: fetch remote
<kbd>n</kbd>: add new remote
<kbd>d</kbd>: remove remote
<kbd>e</kbd>: edit remote
</pre>
## Branches Panel (Sub-commits)
<pre>
<kbd>enter</kbd>: view commit's files
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: view reset options
<kbd>n</kbd>: new branch
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
</pre>
## Branches Panel (Tags Tab)
<pre>
<kbd>space</kbd>: checkout
<kbd>d</kbd>: delete tag
<kbd>P</kbd>: push tag
<kbd>n</kbd>: create tag
<kbd>g</kbd>: view reset options
<kbd>enter</kbd>: view commits
</pre>
## Commit Files Panel
## Commit Files
<pre>
<kbd>ctrl+o</kbd>: copy the committed file name to the clipboard
@@ -116,110 +50,96 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>o</kbd>: open file
<kbd>e</kbd>: edit file
<kbd>space</kbd>: toggle file included in patch
<kbd>a</kbd>: toggle all files included in patch
<kbd>enter</kbd>: enter file to add selected lines to the patch (or toggle directory collapsed)
<kbd>`</kbd>: toggle file tree view
</pre>
## Commits Panel (Commits)
## Commits
<pre>
<kbd>ctrl+l</kbd>: open log menu
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>b</kbd>: view bisect options
<kbd>s</kbd>: squash down
<kbd>f</kbd>: fixup commit
<kbd>r</kbd>: reword commit
<kbd>R</kbd>: reword commit with editor
<kbd>g</kbd>: reset to this commit
<kbd>f</kbd>: fixup commit
<kbd>d</kbd>: delete commit
<kbd>e</kbd>: edit commit
<kbd>p</kbd>: pick commit (when mid-rebase)
<kbd>F</kbd>: create fixup commit for this commit
<kbd>S</kbd>: squash all 'fixup!' commits above selected commit (autosquash)
<kbd>d</kbd>: delete commit
<kbd>ctrl+j</kbd>: move commit down one
<kbd>ctrl+k</kbd>: move commit up one
<kbd>e</kbd>: edit commit
<kbd>A</kbd>: amend commit with staged changes
<kbd>p</kbd>: pick commit (when mid-rebase)
<kbd>t</kbd>: revert commit
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>v</kbd>: paste commits (cherry-pick)
<kbd>enter</kbd>: view commit's files
<kbd>space</kbd>: checkout commit
<kbd>n</kbd>: create new branch off of commit
<kbd>A</kbd>: amend commit with staged changes
<kbd>a</kbd>: reset commit author
<kbd>t</kbd>: revert commit
<kbd>T</kbd>: tag commit
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>ctrl+y</kbd>: copy commit message to clipboard
<kbd>o</kbd>: open commit in browser
<kbd>b</kbd>: view bisect options
</pre>
## Commits Panel (Reflog Tab)
<pre>
<kbd>enter</kbd>: view commit's files
<kbd>ctrl+l</kbd>: open log menu
<kbd>space</kbd>: checkout commit
<kbd>y</kbd>: copy commit attribute
<kbd>o</kbd>: open commit in browser
<kbd>n</kbd>: create new branch off of commit
<kbd>g</kbd>: view reset options
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
<kbd>enter</kbd>: view selected item's files
</pre>
## Extras Panel
<pre>
<kbd>@</kbd>: open command log menu
</pre>
## Files Panel
<pre>
<kbd>ctrl+b</kbd>: Filter commit files
</pre>
## Files Panel (Files)
## Files
<pre>
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
<kbd>d</kbd>: view 'discard changes' options
<kbd>space</kbd>: toggle staged
<kbd>ctrl+b</kbd>: Filter files (staged/unstaged)
<kbd>c</kbd>: commit changes
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>A</kbd>: amend last commit
<kbd>C</kbd>: commit changes using git editor
<kbd>space</kbd>: toggle staged
<kbd>d</kbd>: view 'discard changes' options
<kbd>e</kbd>: edit file
<kbd>o</kbd>: open file
<kbd>i</kbd>: add to .gitignore
<kbd>i</kbd>: Ignore or Exclude file
<kbd>r</kbd>: refresh files
<kbd>s</kbd>: stash changes
<kbd>s</kbd>: stash all changes
<kbd>S</kbd>: view stash options
<kbd>a</kbd>: stage/unstage all
<kbd>D</kbd>: view reset options
<kbd>enter</kbd>: stage individual hunks/lines for file, or collapse/expand for directory
<kbd>f</kbd>: fetch
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
<kbd>g</kbd>: view upstream reset options
<kbd>D</kbd>: view 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
<kbd>f</kbd>: fetch
</pre>
## Files Panel (Submodules)
## Local Branches
<pre>
<kbd>ctrl+o</kbd>: copy submodule name to clipboard
<kbd>enter</kbd>: enter submodule
<kbd>d</kbd>: remove submodule
<kbd>u</kbd>: update submodule
<kbd>n</kbd>: add new submodule
<kbd>e</kbd>: update submodule URL
<kbd>i</kbd>: initialize submodule
<kbd>b</kbd>: view bulk submodule options
<kbd>ctrl+o</kbd>: copy branch name to clipboard
<kbd>i</kbd>: show git-flow options
<kbd>space</kbd>: checkout
<kbd>n</kbd>: new branch
<kbd>o</kbd>: create pull request
<kbd>O</kbd>: create pull request options
<kbd>ctrl+y</kbd>: copy pull request URL to clipboard
<kbd>c</kbd>: checkout by name
<kbd>F</kbd>: force checkout
<kbd>d</kbd>: delete branch
<kbd>r</kbd>: rebase checked-out branch onto this branch
<kbd>M</kbd>: merge into currently checked out branch
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>g</kbd>: view reset options
<kbd>R</kbd>: rename branch
<kbd>u</kbd>: set/unset upstream
<kbd>enter</kbd>: view commits
</pre>
## Main Panel (Merging)
<pre>
<kbd>H</kbd>: scroll left
<kbd>L</kbd>: scroll right
<kbd>esc</kbd>: return to files panel
<kbd>M</kbd>: open external merge tool (git mergetool)
<kbd>space</kbd>: pick hunk
@@ -228,14 +148,16 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>►</kbd>: select next conflict
<kbd>▲</kbd>: select previous hunk
<kbd>▼</kbd>: select next hunk
<kbd>e</kbd>: edit file
<kbd>o</kbd>: open file
<kbd>z</kbd>: undo
</pre>
## Main Panel (Normal)
<pre>
<kbd>Ő</kbd>: scroll down (fn+up)
<kbd>ő</kbd>: scroll up (fn+down)
<kbd>mouse wheel down</kbd>: scroll down (fn+up)
<kbd>mouse wheel up</kbd>: scroll up (fn+down)
</pre>
## Main Panel (Patch Building)
@@ -252,8 +174,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
<kbd>H</kbd>: scroll left
<kbd>L</kbd>: scroll right
</pre>
## Main Panel (Staging)
@@ -270,34 +190,61 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>►</kbd>: select next hunk
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
<kbd>e</kbd>: edit file
<kbd>o</kbd>: open file
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
<kbd>H</kbd>: scroll left
<kbd>L</kbd>: scroll right
<kbd>c</kbd>: commit changes
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>C</kbd>: commit changes using git editor
<kbd>E</kbd>: edit hunk
</pre>
## Menu Panel
## Reflog
<pre>
<kbd>esc</kbd>: close menu
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
<kbd>space</kbd>: checkout commit
<kbd>y</kbd>: copy commit attribute
<kbd>o</kbd>: open commit in browser
<kbd>n</kbd>: create new branch off of commit
<kbd>g</kbd>: view reset options
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>enter</kbd>: view commits
</pre>
## Stash Panel
## Remote Branches
<pre>
<kbd>space</kbd>: checkout
<kbd>n</kbd>: new branch
<kbd>M</kbd>: merge into currently checked out branch
<kbd>r</kbd>: rebase checked-out branch onto this branch
<kbd>d</kbd>: delete branch
<kbd>u</kbd>: set as upstream of checked-out branch
<kbd>esc</kbd>: Return to remotes list
<kbd>g</kbd>: view reset options
<kbd>enter</kbd>: view commits
</pre>
## Remotes
<pre>
<kbd>f</kbd>: fetch remote
<kbd>n</kbd>: add new remote
<kbd>d</kbd>: remove remote
<kbd>e</kbd>: edit remote
</pre>
## Stash
<pre>
<kbd>enter</kbd>: view stash entry's files
<kbd>space</kbd>: apply
<kbd>g</kbd>: pop
<kbd>d</kbd>: drop
<kbd>n</kbd>: new branch
<kbd>enter</kbd>: view selected item's files
</pre>
## Status Panel
## Status
<pre>
<kbd>e</kbd>: edit config file
@@ -306,3 +253,42 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>enter</kbd>: switch to a recent repo
<kbd>a</kbd>: show all branch logs
</pre>
## Sub-commits
<pre>
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
<kbd>space</kbd>: checkout commit
<kbd>y</kbd>: copy commit attribute
<kbd>o</kbd>: open commit in browser
<kbd>n</kbd>: create new branch off of commit
<kbd>g</kbd>: view reset options
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>enter</kbd>: view selected item's files
</pre>
## Submodules
<pre>
<kbd>ctrl+o</kbd>: copy submodule name to clipboard
<kbd>enter</kbd>: enter submodule
<kbd>d</kbd>: remove submodule
<kbd>u</kbd>: update submodule
<kbd>n</kbd>: add new submodule
<kbd>e</kbd>: update submodule URL
<kbd>i</kbd>: initialize submodule
<kbd>b</kbd>: view bulk submodule options
</pre>
## Tags
<pre>
<kbd>space</kbd>: checkout
<kbd>d</kbd>: delete tag
<kbd>P</kbd>: push tag
<kbd>n</kbd>: create tag
<kbd>g</kbd>: view reset options
<kbd>enter</kbd>: view commits
</pre>
+294
View File
@@ -0,0 +1,294 @@
_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/cheatsheet/main.go generate` from the project root._
# Lazygit キーバインド
## グローバルキーバインド
<pre>
<kbd>ctrl+r</kbd>: 最近使用したリポジトリに切り替え
<kbd>pgup</kbd>: メインパネルを上にスクロール (fn+up/shift+k)
<kbd>pgdown</kbd>: メインパネルを下にスクロール (fn+down/shift+j)
<kbd>m</kbd>: view merge/rebase options
<kbd>ctrl+p</kbd>: view custom patch options
<kbd>R</kbd>: リフレッシュ
<kbd>x</kbd>: メニューを開く
<kbd>+</kbd>: 次のスクリーンモード (normal/half/fullscreen)
<kbd>_</kbd>: 前のスクリーンモード
<kbd>ctrl+s</kbd>: view filter-by-path options
<kbd>W</kbd>: 差分メニューを開く
<kbd>ctrl+e</kbd>: 差分メニューを開く
<kbd>@</kbd>: コマンドログメニューを開く
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
<kbd>:</kbd>: カスタムコマンドを実行
<kbd>z</kbd>: アンドゥ (via reflog) (experimental)
<kbd>ctrl+z</kbd>: リドゥ (via reflog) (experimental)
<kbd>P</kbd>: push
<kbd>p</kbd>: pull
</pre>
## 一覧パネルの操作
<pre>
<kbd>.</kbd>: 次のページ
<kbd>,</kbd>: 前のページ
<kbd><</kbd>: 最上部までスクロール
<kbd>></kbd>: 最下部までスクロール
<kbd>/</kbd>: 検索を開始
<kbd>H</kbd>: 左スクロール
<kbd>L</kbd>: 右スクロール
<kbd>]</kbd>: 次のタブ
<kbd>[</kbd>: 前のタブ
</pre>
## Stash
<pre>
<kbd>space</kbd>: 適用
<kbd>g</kbd>: pop
<kbd>d</kbd>: drop
<kbd>n</kbd>: 新しいブランチを作成
<kbd>enter</kbd>: view selected item's files
</pre>
## Sub-commits
<pre>
<kbd>ctrl+o</kbd>: コミットのSHAをクリップボードにコピー
<kbd>space</kbd>: コミットをチェックアウト
<kbd>y</kbd>: コミットの情報をコピー
<kbd>o</kbd>: ブラウザでコミットを開く
<kbd>n</kbd>: コミットにブランチを作成
<kbd>g</kbd>: view reset options
<kbd>c</kbd>: コミットをコピー (cherry-pick)
<kbd>C</kbd>: コミットを範囲コピー (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>enter</kbd>: view selected item's files
</pre>
## コミット
<pre>
<kbd>ctrl+o</kbd>: コミットのSHAをクリップボードにコピー
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>b</kbd>: view bisect options
<kbd>s</kbd>: squash down
<kbd>f</kbd>: fixup commit
<kbd>r</kbd>: コミットメッセージを変更
<kbd>R</kbd>: エディタでコミットメッセージを編集
<kbd>d</kbd>: コミットを削除
<kbd>e</kbd>: コミットを編集
<kbd>p</kbd>: pick commit (when mid-rebase)
<kbd>F</kbd>: このコミットに対するfixupコミットを作成
<kbd>S</kbd>: squash all 'fixup!' commits above selected commit (autosquash)
<kbd>ctrl+j</kbd>: コミットを1つ下に移動
<kbd>ctrl+k</kbd>: コミットを1つ上に移動
<kbd>v</kbd>: コミットを貼り付け (cherry-pick)
<kbd>A</kbd>: ステージされた変更でamendコミット
<kbd>a</kbd>: reset commit author
<kbd>t</kbd>: コミットをrevert
<kbd>T</kbd>: タグを作成
<kbd>ctrl+l</kbd>: ログメニューを開く
<kbd>space</kbd>: コミットをチェックアウト
<kbd>y</kbd>: コミットの情報をコピー
<kbd>o</kbd>: ブラウザでコミットを開く
<kbd>n</kbd>: コミットにブランチを作成
<kbd>g</kbd>: view reset options
<kbd>c</kbd>: コミットをコピー (cherry-pick)
<kbd>C</kbd>: コミットを範囲コピー (cherry-pick)
<kbd>enter</kbd>: view selected item's files
</pre>
## コミットファイル
<pre>
<kbd>ctrl+o</kbd>: コミットされたファイル名をクリップボードにコピー
<kbd>c</kbd>: checkout file
<kbd>d</kbd>: discard this commit's changes to this file
<kbd>o</kbd>: ファイルを開く
<kbd>e</kbd>: ファイルを編集
<kbd>space</kbd>: toggle file included in patch
<kbd>a</kbd>: toggle all files included in patch
<kbd>enter</kbd>: enter file to add selected lines to the patch (or toggle directory collapsed)
<kbd>`</kbd>: ファイルツリーの表示を切り替え
</pre>
## サブモジュール
<pre>
<kbd>ctrl+o</kbd>: サブモジュール名をクリップボードにコピー
<kbd>enter</kbd>: サブモジュールを開く
<kbd>d</kbd>: サブモジュールを削除
<kbd>u</kbd>: サブモジュールを更新
<kbd>n</kbd>: サブモジュールを新規追加
<kbd>e</kbd>: サブモジュールのURLを更新
<kbd>i</kbd>: サブモジュールを初期化
<kbd>b</kbd>: view bulk submodule options
</pre>
## ステータス
<pre>
<kbd>e</kbd>: 設定ファイルを編集
<kbd>o</kbd>: 設定ファイルを開く
<kbd>u</kbd>: 更新を確認
<kbd>enter</kbd>: 最近使用したリポジトリに切り替え
<kbd>a</kbd>: すべてのブランチログを表示
</pre>
## タグ
<pre>
<kbd>space</kbd>: チェックアウト
<kbd>d</kbd>: タグを削除
<kbd>P</kbd>: タグをpush
<kbd>n</kbd>: タグを作成
<kbd>g</kbd>: view reset options
<kbd>enter</kbd>: コミットを閲覧
</pre>
## ファイル
<pre>
<kbd>ctrl+o</kbd>: ファイル名をクリップボードにコピー
<kbd>ctrl+w</kbd>: 空白文字の差分の表示有無を切り替え
<kbd>d</kbd>: view 'discard changes' options
<kbd>space</kbd>: ステージ/アンステージ
<kbd>ctrl+b</kbd>: ファイルをフィルタ (ステージ/アンステージ)
<kbd>c</kbd>: 変更をコミット
<kbd>w</kbd>: pre-commitフックを実行せずに変更をコミット
<kbd>A</kbd>: 最新のコミットにamend
<kbd>C</kbd>: gitエディタを使用して変更をコミット
<kbd>e</kbd>: ファイルを編集
<kbd>o</kbd>: ファイルを開く
<kbd>i</kbd>: ファイルをignore
<kbd>r</kbd>: ファイルをリフレッシュ
<kbd>s</kbd>: 変更をstash
<kbd>S</kbd>: view stash options
<kbd>a</kbd>: すべての変更をステージ/アンステージ
<kbd>enter</kbd>: stage individual hunks/lines for file, or collapse/expand for directory
<kbd>g</kbd>: view upstream reset options
<kbd>D</kbd>: view reset options
<kbd>`</kbd>: ファイルツリーの表示を切り替え
<kbd>M</kbd>: git mergetoolを開く
<kbd>f</kbd>: fetch
</pre>
## ブランチ
<pre>
<kbd>ctrl+o</kbd>: ブランチ名をクリップボードにコピー
<kbd>i</kbd>: show git-flow options
<kbd>space</kbd>: チェックアウト
<kbd>n</kbd>: 新しいブランチを作成
<kbd>o</kbd>: Pull Requestを作成
<kbd>O</kbd>: create pull request options
<kbd>ctrl+y</kbd>: Pull RequestのURLをクリップボードにコピー
<kbd>c</kbd>: checkout by name
<kbd>F</kbd>: force checkout
<kbd>d</kbd>: ブランチを削除
<kbd>r</kbd>: rebase checked-out branch onto this branch
<kbd>M</kbd>: 現在のブランチにマージ
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>g</kbd>: view reset options
<kbd>R</kbd>: ブランチ名を変更
<kbd>u</kbd>: set/unset upstream
<kbd>enter</kbd>: コミットを閲覧
</pre>
## メインパネル (Merging)
<pre>
<kbd>esc</kbd>: ファイル一覧に戻る
<kbd>M</kbd>: git mergetoolを開く
<kbd>space</kbd>: pick hunk
<kbd>b</kbd>: pick all hunks
<kbd>◄</kbd>: 前のコンフリクトを選択
<kbd>►</kbd>: 次のコンフリクトを選択
<kbd>▲</kbd>: 前のhunkを選択
<kbd>▼</kbd>: 次のhunkを選択
<kbd>e</kbd>: ファイルを編集
<kbd>o</kbd>: ファイルを開く
<kbd>z</kbd>: アンドゥ
</pre>
## メインパネル (Normal)
<pre>
<kbd>mouse wheel down</kbd>: 下にスクロール (fn+up)
<kbd>mouse wheel up</kbd>: 上にスクロール (fn+down)
</pre>
## メインパネル (Patch Building)
<pre>
<kbd>esc</kbd>: line-by-lineモードを終了
<kbd>o</kbd>: ファイルを開く
<kbd>▲</kbd>: 前の行を選択
<kbd>▼</kbd>: 次の行を選択
<kbd>◄</kbd>: 前のhunkを選択
<kbd>►</kbd>: 次のhunkを選択
<kbd>ctrl+o</kbd>: 選択されたテキストをクリップボードにコピー
<kbd>space</kbd>: 行をパッチに追加/削除
<kbd>v</kbd>: 範囲選択を切り替え
<kbd>V</kbd>: 範囲選択を切り替え
<kbd>a</kbd>: hunk選択を切り替え
</pre>
## メインパネル (Staging)
<pre>
<kbd>esc</kbd>: ファイル一覧に戻る
<kbd>space</kbd>: 選択行をステージ/アンステージ
<kbd>d</kbd>: 変更を削除 (git reset)
<kbd>tab</kbd>: パネルを切り替え
<kbd>o</kbd>: ファイルを開く
<kbd>▲</kbd>: 前の行を選択
<kbd>▼</kbd>: 次の行を選択
<kbd>◄</kbd>: 前のhunkを選択
<kbd>►</kbd>: 次のhunkを選択
<kbd>ctrl+o</kbd>: 選択されたテキストをクリップボードにコピー
<kbd>e</kbd>: ファイルを編集
<kbd>v</kbd>: 範囲選択を切り替え
<kbd>V</kbd>: 範囲選択を切り替え
<kbd>a</kbd>: hunk選択を切り替え
<kbd>E</kbd>: edit hunk
</pre>
## リモート
<pre>
<kbd>f</kbd>: リモートをfetch
<kbd>n</kbd>: リモートを新規追加
<kbd>d</kbd>: リモートを削除
<kbd>e</kbd>: リモートを編集
</pre>
## リモートブランチ
<pre>
<kbd>space</kbd>: チェックアウト
<kbd>n</kbd>: 新しいブランチを作成
<kbd>M</kbd>: 現在のブランチにマージ
<kbd>r</kbd>: rebase checked-out branch onto this branch
<kbd>d</kbd>: ブランチを削除
<kbd>u</kbd>: set as upstream of checked-out branch
<kbd>esc</kbd>: リモート一覧に戻る
<kbd>g</kbd>: view reset options
<kbd>enter</kbd>: コミットを閲覧
</pre>
## 参照ログ
<pre>
<kbd>ctrl+o</kbd>: コミットのSHAをクリップボードにコピー
<kbd>space</kbd>: コミットをチェックアウト
<kbd>y</kbd>: コミットの情報をコピー
<kbd>o</kbd>: ブラウザでコミットを開く
<kbd>n</kbd>: コミットにブランチを作成
<kbd>g</kbd>: view reset options
<kbd>c</kbd>: コミットをコピー (cherry-pick)
<kbd>C</kbd>: コミットを範囲コピー (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>enter</kbd>: コミットを閲覧
</pre>
+294
View File
@@ -0,0 +1,294 @@
_This file is auto-generated. To update, make the changes in the pkg/i18n directory and then run `go run scripts/cheatsheet/main.go generate` from the project root._
# Lazygit 키 바인딩
## 글로벌 키 바인딩
<pre>
<kbd>ctrl+r</kbd>: 최근에 사용한 저장소로 전환
<kbd>pgup</kbd>: 메인 패널을 위로 스크롤 (fn+up/shift+k)
<kbd>pgdown</kbd>: 메인 패널을 아래로로 스크롤 (fn+down/shift+j)
<kbd>m</kbd>: view merge/rebase options
<kbd>ctrl+p</kbd>: 커스텀 Patch 옵션 보기
<kbd>R</kbd>: 새로고침
<kbd>x</kbd>: 매뉴 열기
<kbd>+</kbd>: 다음 스크린 모드 (normal/half/fullscreen)
<kbd>_</kbd>: 이전 스크린 모드
<kbd>ctrl+s</kbd>: view filter-by-path options
<kbd>W</kbd>: Diff 메뉴 열기
<kbd>ctrl+e</kbd>: Diff 메뉴 열기
<kbd>@</kbd>: 명령어 로그 메뉴 열기
<kbd>}</kbd>: diff 보기의 변경 사항 주위에 표시되는 컨텍스트의 크기를 늘리기
<kbd>{</kbd>: diff 보기의 변경 사항 주위에 표시되는 컨텍스트 크기 줄이기
<kbd>:</kbd>: execute custom command
<kbd>z</kbd>: 되돌리기 (reflog) (실험적)
<kbd>ctrl+z</kbd>: 다시 실행 (reflog) (실험적)
<kbd>P</kbd>: 푸시
<kbd>p</kbd>: 업데이트
</pre>
## List Panel Navigation
<pre>
<kbd>.</kbd>: 다음 페이지
<kbd>,</kbd>: 이전 페이지
<kbd><</kbd>: 맨 위로 스크롤
<kbd>></kbd>: 맨 아래로 스크롤
<kbd>/</kbd>: 검색 시작
<kbd>H</kbd>: 우 스크롤
<kbd>L</kbd>: 좌 스크롤
<kbd>]</kbd>: 이전 탭
<kbd>[</kbd>: 다음 탭
</pre>
## Reflog
<pre>
<kbd>ctrl+o</kbd>: 커밋 SHA를 클립보드에 복사
<kbd>space</kbd>: 커밋을 체크아웃
<kbd>y</kbd>: 커밋 attribute 복사
<kbd>o</kbd>: 브라우저에서 커밋 열기
<kbd>n</kbd>: 커밋에서 새 브랜치를 만듭니다.
<kbd>g</kbd>: view reset options
<kbd>c</kbd>: 커밋을 복사 (cherry-pick)
<kbd>C</kbd>: 커밋을 범위로 복사 (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>enter</kbd>: 커밋 보기
</pre>
## Stash
<pre>
<kbd>space</kbd>: 적용
<kbd>g</kbd>: pop
<kbd>d</kbd>: drop
<kbd>n</kbd>: 새 브랜치 생성
<kbd>enter</kbd>: view selected item's files
</pre>
## Sub-commits
<pre>
<kbd>ctrl+o</kbd>: 커밋 SHA를 클립보드에 복사
<kbd>space</kbd>: 커밋을 체크아웃
<kbd>y</kbd>: 커밋 attribute 복사
<kbd>o</kbd>: 브라우저에서 커밋 열기
<kbd>n</kbd>: 커밋에서 새 브랜치를 만듭니다.
<kbd>g</kbd>: view reset options
<kbd>c</kbd>: 커밋을 복사 (cherry-pick)
<kbd>C</kbd>: 커밋을 범위로 복사 (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>enter</kbd>: view selected item's files
</pre>
## 메인 패널 (Merging)
<pre>
<kbd>esc</kbd>: 파일 목록으로 돌아가기
<kbd>M</kbd>: git mergetool를 열기
<kbd>space</kbd>: pick hunk
<kbd>b</kbd>: pick all hunks
<kbd>◄</kbd>: 이전 충돌을 선택
<kbd>►</kbd>: 다음 충돌을 선택
<kbd>▲</kbd>: 이전 hunk를 선택
<kbd>▼</kbd>: 다음 hunk를 선택
<kbd>e</kbd>: 파일 편집
<kbd>o</kbd>: 파일 닫기
<kbd>z</kbd>: 되돌리기
</pre>
## 메인 패널 (Normal)
<pre>
<kbd>mouse wheel down</kbd>: 아래로 스크롤 (fn+up)
<kbd>mouse wheel up</kbd>: 위로 스크롤 (fn+down)
</pre>
## 메인 패널 (Patch Building)
<pre>
<kbd>esc</kbd>: line-by-line 모드 종료
<kbd>o</kbd>: 파일 닫기
<kbd>▲</kbd>: 이전 줄 선택
<kbd>▼</kbd>: 다음 줄 선택
<kbd>◄</kbd>: 이전 hunk를 선택
<kbd>►</kbd>: 다음 hunk를 선택
<kbd>ctrl+o</kbd>: 선택한 텍스트를 클립보드에 복사
<kbd>space</kbd>: line(s)을 패치에 추가/삭제
<kbd>v</kbd>: 드래그 선택 전환
<kbd>V</kbd>: 드래그 선택 전환
<kbd>a</kbd>: toggle select hunk
</pre>
## 메인 패널 (Staging)
<pre>
<kbd>esc</kbd>: 파일 목록으로 돌아가기
<kbd>space</kbd>: 선택한 행을 staged / unstaged
<kbd>d</kbd>: 변경을 삭제 (git reset)
<kbd>tab</kbd>: 패널 전환
<kbd>o</kbd>: 파일 닫기
<kbd>▲</kbd>: 이전 줄 선택
<kbd>▼</kbd>: 다음 줄 선택
<kbd>◄</kbd>: 이전 hunk를 선택
<kbd>►</kbd>: 다음 hunk를 선택
<kbd>ctrl+o</kbd>: 선택한 텍스트를 클립보드에 복사
<kbd>e</kbd>: 파일 편집
<kbd>v</kbd>: 드래그 선택 전환
<kbd>V</kbd>: 드래그 선택 전환
<kbd>a</kbd>: toggle select hunk
<kbd>E</kbd>: edit hunk
</pre>
## 브랜치
<pre>
<kbd>ctrl+o</kbd>: 브랜치명을 클립보드에 복사
<kbd>i</kbd>: git-flow 옵션 보기
<kbd>space</kbd>: 체크아웃
<kbd>n</kbd>: 새 브랜치 생성
<kbd>o</kbd>: 풀 리퀘스트 생성
<kbd>O</kbd>: 풀 리퀘스트 생성 옵션
<kbd>ctrl+y</kbd>: 풀 리퀘스트 URL을 클립보드에 복사
<kbd>c</kbd>: 이름으로 체크아웃
<kbd>F</kbd>: 강제 체크아웃
<kbd>d</kbd>: 브랜치 삭제
<kbd>r</kbd>: 체크아웃된 브랜치를 이 브랜치에 리베이스
<kbd>M</kbd>: 현재 브랜치에 병합
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>g</kbd>: view reset options
<kbd>R</kbd>: 브랜치 이름 변경
<kbd>u</kbd>: set/unset upstream
<kbd>enter</kbd>: 커밋 보기
</pre>
## 상태
<pre>
<kbd>e</kbd>: 설정 파일 수정
<kbd>o</kbd>: 설정 파일 열기
<kbd>u</kbd>: 업데이트 확인
<kbd>enter</kbd>: 최근에 사용한 저장소로 전환
<kbd>a</kbd>: 모든 브랜치 로그 표시
</pre>
## 서브모듈
<pre>
<kbd>ctrl+o</kbd>: 서브모듈 이름을 클립보드에 복사
<kbd>enter</kbd>: 서브모듈 열기
<kbd>d</kbd>: 서브모듈 삭제
<kbd>u</kbd>: 서브모듈 업데이트
<kbd>n</kbd>: 새로운 서브모듈 추가
<kbd>e</kbd>: 서브모듈의 URL을 수정
<kbd>i</kbd>: 서브모듈 초기화
<kbd>b</kbd>: view bulk submodule options
</pre>
## 원격
<pre>
<kbd>f</kbd>: 원격을 업데이트
<kbd>n</kbd>: 새로운 Remote 추가
<kbd>d</kbd>: Remote를 삭제
<kbd>e</kbd>: Remote를 수정
</pre>
## 원격 브랜치
<pre>
<kbd>space</kbd>: 체크아웃
<kbd>n</kbd>: 새 브랜치 생성
<kbd>M</kbd>: 현재 브랜치에 병합
<kbd>r</kbd>: 체크아웃된 브랜치를 이 브랜치에 리베이스
<kbd>d</kbd>: 브랜치 삭제
<kbd>u</kbd>: set as upstream of checked-out branch
<kbd>esc</kbd>: 원격목록으로 돌아가기
<kbd>g</kbd>: view reset options
<kbd>enter</kbd>: 커밋 보기
</pre>
## 커밋
<pre>
<kbd>ctrl+o</kbd>: 커밋 SHA를 클립보드에 복사
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>b</kbd>: bisect 옵션 보기
<kbd>s</kbd>: squash down
<kbd>f</kbd>: fixup commit
<kbd>r</kbd>: 커밋메시지 변경
<kbd>R</kbd>: 에디터에서 커밋메시지 수정
<kbd>d</kbd>: 커밋 삭제
<kbd>e</kbd>: 커밋을 편집
<kbd>p</kbd>: pick commit (when mid-rebase)
<kbd>F</kbd>: create fixup commit for this commit
<kbd>S</kbd>: squash all 'fixup!' commits above selected commit (autosquash)
<kbd>ctrl+j</kbd>: 커밋을 1개 아래로 이동
<kbd>ctrl+k</kbd>: 커밋을 1개 위로 이동
<kbd>v</kbd>: 커밋을 붙여넣기 (cherry-pick)
<kbd>A</kbd>: amend commit with staged changes
<kbd>a</kbd>: reset commit author
<kbd>t</kbd>: 커밋 되돌리기
<kbd>T</kbd>: tag commit
<kbd>ctrl+l</kbd>: 로그 메뉴 열기
<kbd>space</kbd>: 커밋을 체크아웃
<kbd>y</kbd>: 커밋 attribute 복사
<kbd>o</kbd>: 브라우저에서 커밋 열기
<kbd>n</kbd>: 커밋에서 새 브랜치를 만듭니다.
<kbd>g</kbd>: view reset options
<kbd>c</kbd>: 커밋을 복사 (cherry-pick)
<kbd>C</kbd>: 커밋을 범위로 복사 (cherry-pick)
<kbd>enter</kbd>: view selected item's files
</pre>
## 커밋 파일
<pre>
<kbd>ctrl+o</kbd>: 커밋한 파일명을 클립보드에 복사
<kbd>c</kbd>: checkout file
<kbd>d</kbd>: discard this commit's changes to this file
<kbd>o</kbd>: 파일 닫기
<kbd>e</kbd>: 파일 편집
<kbd>space</kbd>: toggle file included in patch
<kbd>a</kbd>: toggle all files included in patch
<kbd>enter</kbd>: enter file to add selected lines to the patch (or toggle directory collapsed)
<kbd>`</kbd>: 파일 트리뷰로 전환
</pre>
## 태그
<pre>
<kbd>space</kbd>: 체크아웃
<kbd>d</kbd>: 태그 삭제
<kbd>P</kbd>: 태그를 push
<kbd>n</kbd>: 태그를 생성
<kbd>g</kbd>: view reset options
<kbd>enter</kbd>: 커밋 보기
</pre>
## 파일
<pre>
<kbd>ctrl+o</kbd>: 파일명을 클립보드에 복사
<kbd>ctrl+w</kbd>: 공백문자를 Diff 뷰에서 표시 여부 전환
<kbd>d</kbd>: view 'discard changes' options
<kbd>space</kbd>: Staged 전환
<kbd>ctrl+b</kbd>: 파일을 필터하기 (Staged/unstaged)
<kbd>c</kbd>: 커밋 변경내용
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>A</kbd>: 마지맛 커밋 수정
<kbd>C</kbd>: Git 편집기를 사용하여 변경 내용을 커밋합니다.
<kbd>e</kbd>: 파일 편집
<kbd>o</kbd>: 파일 닫기
<kbd>i</kbd>: Ignore file
<kbd>r</kbd>: 파일 새로고침
<kbd>s</kbd>: 변경사항을 Stash
<kbd>S</kbd>: Stash 옵션 보기
<kbd>a</kbd>: 모든 변경을 Staged/unstaged으로 전환
<kbd>enter</kbd>: stage individual hunks/lines for file, or collapse/expand for directory
<kbd>g</kbd>: view upstream reset options
<kbd>D</kbd>: view reset options
<kbd>`</kbd>: 파일 트리뷰로 전환
<kbd>M</kbd>: git mergetool를 열기
<kbd>f</kbd>: fetch
</pre>
+149 -163
View File
@@ -5,26 +5,26 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## Globale Sneltoetsen
<pre>
<kbd>ctrl+r</kbd>: wissel naar een recente repo (<c-r>)
<kbd>pgup</kbd>: scroll naar beneden vanaf hoofdpaneel (fn+up)
<kbd>pgdown</kbd>: scroll naar beneden vanaf hoofdpaneel (fn+down)
<kbd>ctrl+r</kbd>: wissel naar een recente repo
<kbd>pgup</kbd>: scroll naar beneden vanaf hoofdpaneel (fn+up/shift+k)
<kbd>pgdown</kbd>: scroll naar beneden vanaf hoofdpaneel (fn+down/shift+j)
<kbd>m</kbd>: bekijk merge/rebase opties
<kbd>ctrl+p</kbd>: bekijk aangepaste patch opties
<kbd>P</kbd>: push
<kbd>p</kbd>: pull
<kbd>R</kbd>: verversen
<kbd>x</kbd>: open menu
<kbd>z</kbd>: ongedaan maken (via reflog) (experimenteel)
<kbd>ctrl+z</kbd>: redo (via reflog) (experimenteel)
<kbd>+</kbd>: volgende scherm modus (normaal/half/groot)
<kbd>_</kbd>: vorige scherm modus
<kbd>:</kbd>: voer 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
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
<kbd>:</kbd>: voer aangepaste commando uit
<kbd>z</kbd>: ongedaan maken (via reflog) (experimenteel)
<kbd>ctrl+z</kbd>: redo (via reflog) (experimenteel)
<kbd>P</kbd>: push
<kbd>p</kbd>: pull
</pre>
## Lijstpaneel Navigatie
@@ -35,79 +35,62 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd><</kbd>: scroll naar boven
<kbd>></kbd>: scroll naar beneden
<kbd>/</kbd>: start met zoeken
<kbd>H</kbd>: scroll left
<kbd>L</kbd>: scroll right
<kbd>]</kbd>: volgende tabblad
<kbd>[</kbd>: vorige tabblad
</pre>
## Branches Paneel (Branches Tabblad)
## Bestanden
<pre>
<kbd>ctrl+o</kbd>: kopieer de bestandsnaam naar het klembord
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
<kbd>d</kbd>: bekijk 'veranderingen ongedaan maken' opties
<kbd>space</kbd>: toggle staged
<kbd>ctrl+b</kbd>: Filter files (staged/unstaged)
<kbd>c</kbd>: commit veranderingen
<kbd>w</kbd>: commit veranderingen zonder pre-commit hook
<kbd>A</kbd>: wijzig laatste commit
<kbd>C</kbd>: commit veranderingen met de git editor
<kbd>e</kbd>: verander bestand
<kbd>o</kbd>: open bestand
<kbd>i</kbd>: Ignore or Exclude file
<kbd>r</kbd>: refresh bestanden
<kbd>s</kbd>: stash-bestanden
<kbd>S</kbd>: bekijk stash opties
<kbd>a</kbd>: toggle staged alle
<kbd>enter</kbd>: stage individuele hunks/lijnen
<kbd>g</kbd>: bekijk upstream reset opties
<kbd>D</kbd>: bekijk reset opties
<kbd>`</kbd>: toggle bestandsboom weergave
<kbd>M</kbd>: open external merge tool (git mergetool)
<kbd>f</kbd>: fetch
</pre>
## Branches
<pre>
<kbd>ctrl+o</kbd>: kopieer branch name naar klembord
<kbd>i</kbd>: laat git-flow opties zien
<kbd>space</kbd>: uitchecken
<kbd>n</kbd>: nieuwe branch
<kbd>o</kbd>: maak een pull-request
<kbd>O</kbd>: bekijk opties voor pull-aanvraag
<kbd>ctrl+y</kbd>: kopieer de URL van het pull-verzoek naar het klembord
<kbd>c</kbd>: uitchecken bij naam
<kbd>F</kbd>: forceer checkout
<kbd>n</kbd>: nieuwe branch
<kbd>d</kbd>: verwijder branch
<kbd>r</kbd>: rebase branch
<kbd>M</kbd>: merge in met huidige checked out branch
<kbd>i</kbd>: laat git-flow opties zien
<kbd>f</kbd>: fast-forward deze branch vanaf zijn upstream
<kbd>g</kbd>: bekijk reset opties
<kbd>R</kbd>: hernoem branch
<kbd>ctrl+o</kbd>: kopieer branch name naar klembord
<kbd>u</kbd>: set/unset upstream
<kbd>enter</kbd>: bekijk commits
</pre>
## Branches Paneel (Remote Branches (in Remotes tabblad))
<pre>
<kbd>esc</kbd>: ga terug naar remotes lijst
<kbd>g</kbd>: bekijk reset opties
<kbd>enter</kbd>: bekijk commits
<kbd>space</kbd>: uitchecken
<kbd>n</kbd>: nieuwe branch
<kbd>M</kbd>: merge in met huidige checked out branch
<kbd>d</kbd>: verwijder branch
<kbd>r</kbd>: rebase branch
<kbd>u</kbd>: stel in als upstream van uitgecheckte branch
</pre>
## Branches Paneel (Remotes Tabblad)
<pre>
<kbd>f</kbd>: fetch remote
<kbd>n</kbd>: voeg een nieuwe remote toe
<kbd>d</kbd>: verwijder remote
<kbd>e</kbd>: wijzig remote
</pre>
## Branches Paneel (Sub-commits)
<pre>
<kbd>enter</kbd>: bekijk gecommite bestanden
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: bekijk reset opties
<kbd>n</kbd>: nieuwe branch
<kbd>c</kbd>: kopieer commit (cherry-pick)
<kbd>C</kbd>: kopieer commit reeks (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
</pre>
## Branches Paneel (Tags Tabblad)
<pre>
<kbd>space</kbd>: uitchecken
<kbd>d</kbd>: verwijder tag
<kbd>P</kbd>: push tag
<kbd>n</kbd>: creëer tag
<kbd>g</kbd>: bekijk reset opties
<kbd>enter</kbd>: bekijk commits
</pre>
## Commit bestanden Paneel
## Commit bestanden
<pre>
<kbd>ctrl+o</kbd>: kopieer de vastgelegde bestandsnaam naar het klembord
@@ -116,110 +99,47 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>o</kbd>: open bestand
<kbd>e</kbd>: verander bestand
<kbd>space</kbd>: toggle bestand inbegrepen in patch
<kbd>a</kbd>: toggle all files included in patch
<kbd>enter</kbd>: enter bestand om geselecteerde regels toe te voegen aan de patch
<kbd>`</kbd>: toggle bestandsboom weergave
</pre>
## Commits Paneel (Commits)
## Commits
<pre>
<kbd>ctrl+l</kbd>: open log menu
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
<kbd>b</kbd>: view bisect options
<kbd>s</kbd>: squash beneden
<kbd>f</kbd>: Fixup commit
<kbd>r</kbd>: hernoem commit
<kbd>R</kbd>: hernoem commit met editor
<kbd>g</kbd>: reset naar deze commit
<kbd>f</kbd>: Fixup commit
<kbd>d</kbd>: verwijder commit
<kbd>e</kbd>: wijzig commit
<kbd>p</kbd>: kies commit (wanneer midden in rebase)
<kbd>F</kbd>: creëer fixup commit voor deze commit
<kbd>S</kbd>: squash bovenstaande commits
<kbd>d</kbd>: verwijder commit
<kbd>ctrl+j</kbd>: verplaats commit 1 naar beneden
<kbd>ctrl+k</kbd>: verplaats commit 1 naar boven
<kbd>e</kbd>: wijzig commit
<kbd>A</kbd>: wijzig commit met staged veranderingen
<kbd>p</kbd>: kies commit (wanneer midden in rebase)
<kbd>t</kbd>: commit ongedaan maken
<kbd>c</kbd>: kopieer commit (cherry-pick)
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
<kbd>C</kbd>: kopieer commit reeks (cherry-pick)
<kbd>v</kbd>: plak commits (cherry-pick)
<kbd>enter</kbd>: bekijk gecommite bestanden
<kbd>space</kbd>: checkout commit
<kbd>n</kbd>: creëer nieuwe branch van commit
<kbd>A</kbd>: wijzig commit met staged veranderingen
<kbd>a</kbd>: reset commit author
<kbd>t</kbd>: commit ongedaan maken
<kbd>T</kbd>: tag commit
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
<kbd>ctrl+y</kbd>: kopieer commit bericht naar klembord
<kbd>o</kbd>: open commit in browser
<kbd>b</kbd>: view bisect options
</pre>
## Commits Paneel (Reflog Tabblad)
<pre>
<kbd>enter</kbd>: bekijk gecommite bestanden
<kbd>ctrl+l</kbd>: open log menu
<kbd>space</kbd>: checkout commit
<kbd>y</kbd>: copy commit attribute
<kbd>o</kbd>: open commit in browser
<kbd>n</kbd>: creëer nieuwe branch van commit
<kbd>g</kbd>: bekijk reset opties
<kbd>c</kbd>: kopieer commit (cherry-pick)
<kbd>C</kbd>: kopieer commit reeks (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
<kbd>enter</kbd>: bekijk gecommite bestanden
</pre>
## Extras Paneel
## Mergen
<pre>
<kbd>@</kbd>: open command log menu
</pre>
## Bestanden Paneel
<pre>
<kbd>ctrl+b</kbd>: Commit dossiers filteren
</pre>
## Bestanden Paneel (Bestanden)
<pre>
<kbd>c</kbd>: commit veranderingen
<kbd>w</kbd>: commit veranderingen zonder pre-commit hook
<kbd>A</kbd>: wijzig laatste commit
<kbd>C</kbd>: commit veranderingen met de git editor
<kbd>space</kbd>: toggle staged
<kbd>d</kbd>: bekijk 'veranderingen ongedaan maken' opties
<kbd>e</kbd>: verander bestand
<kbd>o</kbd>: open bestand
<kbd>i</kbd>: voeg toe aan .gitignore
<kbd>r</kbd>: refresh bestanden
<kbd>s</kbd>: stash-bestanden
<kbd>S</kbd>: bekijk stash opties
<kbd>a</kbd>: toggle staged alle
<kbd>D</kbd>: bekijk reset opties
<kbd>enter</kbd>: stage individuele hunks/lijnen
<kbd>f</kbd>: fetch
<kbd>ctrl+o</kbd>: kopieer de bestandsnaam naar het klembord
<kbd>g</kbd>: bekijk upstream reset opties
<kbd>`</kbd>: toggle bestandsboom weergave
<kbd>M</kbd>: open external merge tool (git mergetool)
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
</pre>
## Bestanden Paneel (Submodules)
<pre>
<kbd>ctrl+o</kbd>: kopieer submodule naam naar klembord
<kbd>enter</kbd>: enter submodule
<kbd>d</kbd>: remove submodule
<kbd>u</kbd>: update submodule
<kbd>n</kbd>: voeg nieuwe submodule toe
<kbd>e</kbd>: update submodule URL
<kbd>i</kbd>: initialiseer submodule
<kbd>b</kbd>: bekijk bulk submodule opties
</pre>
## Hoofd Paneel (Mergen)
<pre>
<kbd>H</kbd>: scroll left
<kbd>L</kbd>: scroll right
<kbd>esc</kbd>: ga terug naar het bestanden paneel
<kbd>M</kbd>: open external merge tool (git mergetool)
<kbd>space</kbd>: kies hunk
@@ -228,17 +148,19 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>►</kbd>: selecteer volgende conflict
<kbd>▲</kbd>: selecteer bovenste hunk
<kbd>▼</kbd>: selecteer onderste hunk
<kbd>e</kbd>: verander bestand
<kbd>o</kbd>: open bestand
<kbd>z</kbd>: ongedaan maken
</pre>
## Hoofd Paneel (Normaal)
## Normaal
<pre>
<kbd>Ő</kbd>: scroll omlaag (fn+up)
<kbd>ő</kbd>: scroll omhoog (fn+down)
<kbd>mouse wheel down</kbd>: scroll omlaag (fn+up)
<kbd>mouse wheel up</kbd>: scroll omhoog (fn+down)
</pre>
## Hoofd Paneel (Patch Bouwen)
## Patch Bouwen
<pre>
<kbd>esc</kbd>: sluit lijn-bij-lijn modus
@@ -252,11 +174,47 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>v</kbd>: toggle drag selecteer
<kbd>V</kbd>: toggle drag selecteer
<kbd>a</kbd>: toggle selecteer hunk
<kbd>H</kbd>: scroll left
<kbd>L</kbd>: scroll right
</pre>
## Hoofd Paneel (Staging)
## Reflog
<pre>
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
<kbd>space</kbd>: checkout commit
<kbd>y</kbd>: copy commit attribute
<kbd>o</kbd>: open commit in browser
<kbd>n</kbd>: creëer nieuwe branch van commit
<kbd>g</kbd>: bekijk reset opties
<kbd>c</kbd>: kopieer commit (cherry-pick)
<kbd>C</kbd>: kopieer commit reeks (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
<kbd>enter</kbd>: bekijk commits
</pre>
## Remote Branches
<pre>
<kbd>space</kbd>: uitchecken
<kbd>n</kbd>: nieuwe branch
<kbd>M</kbd>: merge in met huidige checked out branch
<kbd>r</kbd>: rebase branch
<kbd>d</kbd>: verwijder branch
<kbd>u</kbd>: stel in als upstream van uitgecheckte branch
<kbd>esc</kbd>: ga terug naar remotes lijst
<kbd>g</kbd>: bekijk reset opties
<kbd>enter</kbd>: bekijk commits
</pre>
## Remotes
<pre>
<kbd>f</kbd>: fetch remote
<kbd>n</kbd>: voeg een nieuwe remote toe
<kbd>d</kbd>: verwijder remote
<kbd>e</kbd>: wijzig remote
</pre>
## Staging
<pre>
<kbd>esc</kbd>: ga terug naar het bestanden paneel
@@ -270,34 +228,23 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>►</kbd>: selecteer de volgende hunk
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
<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>H</kbd>: scroll left
<kbd>L</kbd>: scroll right
<kbd>c</kbd>: commit veranderingen
<kbd>w</kbd>: commit veranderingen zonder pre-commit hook
<kbd>C</kbd>: commit veranderingen met de git editor
<kbd>E</kbd>: edit hunk
</pre>
## Menu Paneel
## Stash
<pre>
<kbd>esc</kbd>: sluit menu
</pre>
## Stash Paneel
<pre>
<kbd>enter</kbd>: bekijk bestanden van stash entry
<kbd>space</kbd>: toepassen
<kbd>g</kbd>: pop
<kbd>d</kbd>: laten vallen
<kbd>n</kbd>: nieuwe branch
<kbd>enter</kbd>: bekijk gecommite bestanden
</pre>
## Status Paneel
## Status
<pre>
<kbd>e</kbd>: verander config bestand
@@ -306,3 +253,42 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>enter</kbd>: wissel naar een recente repo
<kbd>a</kbd>: alle logs van de branch laten zien
</pre>
## Sub-commits
<pre>
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
<kbd>space</kbd>: checkout commit
<kbd>y</kbd>: copy commit attribute
<kbd>o</kbd>: open commit in browser
<kbd>n</kbd>: creëer nieuwe branch van commit
<kbd>g</kbd>: bekijk reset opties
<kbd>c</kbd>: kopieer commit (cherry-pick)
<kbd>C</kbd>: kopieer commit reeks (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
<kbd>enter</kbd>: bekijk gecommite bestanden
</pre>
## Submodules
<pre>
<kbd>ctrl+o</kbd>: kopieer submodule naam naar klembord
<kbd>enter</kbd>: enter submodule
<kbd>d</kbd>: remove submodule
<kbd>u</kbd>: update submodule
<kbd>n</kbd>: voeg nieuwe submodule toe
<kbd>e</kbd>: update submodule URL
<kbd>i</kbd>: initialiseer submodule
<kbd>b</kbd>: bekijk bulk submodule opties
</pre>
## Tags
<pre>
<kbd>space</kbd>: uitchecken
<kbd>d</kbd>: verwijder tag
<kbd>P</kbd>: push tag
<kbd>n</kbd>: creëer tag
<kbd>g</kbd>: bekijk reset opties
<kbd>enter</kbd>: bekijk commits
</pre>
+191 -205
View File
@@ -5,26 +5,26 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## 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>ctrl+r</kbd>: switch to a recent repo
<kbd>pgup</kbd>: scroll up main panel (fn+up/shift+k)
<kbd>pgdown</kbd>: scroll down main panel (fn+down/shift+j)
<kbd>m</kbd>: widok scalenia/opcje zmiany bazy
<kbd>ctrl+p</kbd>: view custom patch options
<kbd>P</kbd>: push
<kbd>p</kbd>: pull
<kbd>R</kbd>: odśwież
<kbd>x</kbd>: open menu
<kbd>z</kbd>: undo (via reflog) (experimental)
<kbd>ctrl+z</kbd>: redo (via reflog) (experimental)
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode
<kbd>:</kbd>: wykonaj własną komendę
<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
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
<kbd>:</kbd>: wykonaj własną komendę
<kbd>z</kbd>: undo (via reflog) (experimental)
<kbd>ctrl+z</kbd>: redo (via reflog) (experimental)
<kbd>P</kbd>: push
<kbd>p</kbd>: pull
</pre>
## List Panel Navigation
@@ -35,210 +35,68 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd><</kbd>: scroll to top
<kbd>></kbd>: scroll to bottom
<kbd>/</kbd>: start search
<kbd>H</kbd>: scroll left
<kbd>L</kbd>: scroll right
<kbd>]</kbd>: next tab
<kbd>[</kbd>: previous tab
</pre>
## Gałęzie Panel (Branches Tab)
## Commity
<pre>
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>b</kbd>: view bisect options
<kbd>s</kbd>: ściśnij
<kbd>f</kbd>: napraw commit
<kbd>r</kbd>: zmień nazwę commita
<kbd>R</kbd>: zmień nazwę commita w edytorze
<kbd>d</kbd>: usuń commit
<kbd>e</kbd>: edytuj commit
<kbd>p</kbd>: wybierz commit (podczas zmiany bazy)
<kbd>F</kbd>: utwórz commit naprawczy dla tego commita
<kbd>S</kbd>: spłaszcz wszystkie commity naprawcze powyżej zaznaczonych commitów (autosquash)
<kbd>ctrl+j</kbd>: przenieś commit 1 w dół
<kbd>ctrl+k</kbd>: przenieś commit 1 w górę
<kbd>v</kbd>: wklej commity (przebieranie)
<kbd>A</kbd>: popraw commit zmianami z poczekalni
<kbd>a</kbd>: reset commit author
<kbd>t</kbd>: odwróć commit
<kbd>T</kbd>: tag commit
<kbd>ctrl+l</kbd>: open log menu
<kbd>space</kbd>: checkout commit
<kbd>y</kbd>: copy commit attribute
<kbd>o</kbd>: open commit in browser
<kbd>n</kbd>: create new branch off of commit
<kbd>g</kbd>: wyświetl opcje resetu
<kbd>c</kbd>: kopiuj commit (przebieranie)
<kbd>C</kbd>: kopiuj zakres commitów (przebieranie)
<kbd>enter</kbd>: przeglądaj pliki commita
</pre>
## Local Branches
<pre>
<kbd>ctrl+o</kbd>: copy branch name to clipboard
<kbd>i</kbd>: show git-flow options
<kbd>space</kbd>: przełącz
<kbd>n</kbd>: nowa gałąź
<kbd>o</kbd>: utwórz żądanie pobrania
<kbd>O</kbd>: utwórz opcje żądania ściągnięcia
<kbd>ctrl+y</kbd>: skopiuj adres URL żądania pobrania do schowka
<kbd>c</kbd>: przełącz używając nazwy
<kbd>F</kbd>: wymuś przełączenie
<kbd>n</kbd>: nowa gałąź
<kbd>d</kbd>: usuń gałąź
<kbd>r</kbd>: zmiana bazy gałęzi
<kbd>M</kbd>: scal do obecnej gałęzi
<kbd>i</kbd>: show git-flow options
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>g</kbd>: wyświetl opcje resetu
<kbd>R</kbd>: rename branch
<kbd>ctrl+o</kbd>: copy branch name to clipboard
<kbd>u</kbd>: set/unset upstream
<kbd>enter</kbd>: view commits
</pre>
## Gałęzie Panel (Remote Branches (in Remotes tab))
<pre>
<kbd>esc</kbd>: wróć do listy repozytoriów zdalnych
<kbd>g</kbd>: wyświetl opcje resetu
<kbd>enter</kbd>: view commits
<kbd>space</kbd>: przełącz
<kbd>n</kbd>: nowa gałąź
<kbd>M</kbd>: scal do obecnej gałęzi
<kbd>d</kbd>: usuń gałąź
<kbd>r</kbd>: zmiana bazy gałęzi
<kbd>u</kbd>: set as upstream of checked-out branch
</pre>
## Gałęzie Panel (Remotes Tab)
<pre>
<kbd>f</kbd>: fetch remote
<kbd>n</kbd>: add new remote
<kbd>d</kbd>: remove remote
<kbd>e</kbd>: edit remote
</pre>
## Gałęzie Panel (Sub-commits)
<pre>
<kbd>enter</kbd>: przeglądaj pliki commita
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: wyświetl opcje resetu
<kbd>n</kbd>: nowa gałąź
<kbd>c</kbd>: kopiuj commit (przebieranie)
<kbd>C</kbd>: kopiuj zakres commitów (przebieranie)
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
</pre>
## Gałęzie Panel (Tags Tab)
<pre>
<kbd>space</kbd>: przełącz
<kbd>d</kbd>: delete tag
<kbd>P</kbd>: push tag
<kbd>n</kbd>: create tag
<kbd>g</kbd>: wyświetl opcje resetu
<kbd>enter</kbd>: view commits
</pre>
## Pliki commita Panel
<pre>
<kbd>ctrl+o</kbd>: copy the committed file name to the clipboard
<kbd>c</kbd>: plik wybierania
<kbd>d</kbd>: porzuć zmiany commita dla tego pliku
<kbd>o</kbd>: otwórz plik
<kbd>e</kbd>: edytuj plik
<kbd>space</kbd>: toggle file included in patch
<kbd>enter</kbd>: enter file to add selected lines to the patch (or toggle directory collapsed)
<kbd>`</kbd>: toggle file tree view
</pre>
## Commity Panel (Commity)
<pre>
<kbd>ctrl+l</kbd>: open log menu
<kbd>s</kbd>: ściśnij
<kbd>r</kbd>: zmień nazwę commita
<kbd>R</kbd>: zmień nazwę commita w edytorze
<kbd>g</kbd>: zresetuj do tego commita
<kbd>f</kbd>: napraw commit
<kbd>F</kbd>: utwórz commit naprawczy dla tego commita
<kbd>S</kbd>: spłaszcz wszystkie commity naprawcze powyżej zaznaczonych commitów (autosquash)
<kbd>d</kbd>: usuń commit
<kbd>ctrl+j</kbd>: przenieś commit 1 w dół
<kbd>ctrl+k</kbd>: przenieś commit 1 w górę
<kbd>e</kbd>: edytuj commit
<kbd>A</kbd>: popraw commit zmianami z poczekalni
<kbd>p</kbd>: wybierz commit (podczas zmiany bazy)
<kbd>t</kbd>: odwróć commit
<kbd>c</kbd>: kopiuj commit (przebieranie)
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
<kbd>C</kbd>: kopiuj zakres commitów (przebieranie)
<kbd>v</kbd>: wklej commity (przebieranie)
<kbd>enter</kbd>: przeglądaj pliki commita
<kbd>space</kbd>: checkout commit
<kbd>n</kbd>: create new branch off of commit
<kbd>T</kbd>: tag commit
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>ctrl+y</kbd>: copy commit message to clipboard
<kbd>o</kbd>: open commit in browser
<kbd>b</kbd>: view bisect options
</pre>
## Commity Panel (Reflog Tab)
<pre>
<kbd>enter</kbd>: przeglądaj pliki commita
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: wyświetl opcje resetu
<kbd>c</kbd>: kopiuj commit (przebieranie)
<kbd>C</kbd>: kopiuj zakres commitów (przebieranie)
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
</pre>
## Extras Panel
<pre>
<kbd>@</kbd>: open command log menu
</pre>
## Pliki Panel
<pre>
<kbd>ctrl+b</kbd>: Filtrowanie commitów
</pre>
## Pliki Panel (Pliki)
<pre>
<kbd>c</kbd>: Zatwierdź zmiany
<kbd>w</kbd>: zatwierdź zmiany bez skryptu pre-commit
<kbd>A</kbd>: Zmień ostatni commit
<kbd>C</kbd>: Zatwierdź zmiany używając edytora
<kbd>space</kbd>: przełącz stan poczekalni
<kbd>d</kbd>: pokaż opcje porzucania zmian
<kbd>e</kbd>: edytuj plik
<kbd>o</kbd>: otwórz plik
<kbd>i</kbd>: dodaj do .gitignore
<kbd>r</kbd>: odśwież pliki
<kbd>s</kbd>: przechowaj zmiany
<kbd>S</kbd>: wyświetl opcje schowka
<kbd>a</kbd>: przełącz stan poczekalni wszystkich
<kbd>D</kbd>: wyświetl opcje resetu
<kbd>enter</kbd>: zatwierdź pojedyncze linie
<kbd>f</kbd>: pobierz
<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)
<pre>
<kbd>ctrl+o</kbd>: copy submodule name to clipboard
<kbd>enter</kbd>: enter submodule
<kbd>d</kbd>: remove submodule
<kbd>u</kbd>: update submodule
<kbd>n</kbd>: add new submodule
<kbd>e</kbd>: update submodule URL
<kbd>i</kbd>: initialize submodule
<kbd>b</kbd>: view bulk submodule options
</pre>
## Główne Panel (Scalanie)
<pre>
<kbd>H</kbd>: scroll left
<kbd>L</kbd>: scroll right
<kbd>esc</kbd>: wróć do panelu plików
<kbd>M</kbd>: open external merge tool (git mergetool)
<kbd>space</kbd>: wybierz kawałek
<kbd>b</kbd>: wybierz wszystkie kawałki
<kbd>◄</kbd>: poprzedni konflikt
<kbd>►</kbd>: następny konflikt
<kbd>▲</kbd>: wybierz poprzedni kawałek
<kbd>▼</kbd>: wybierz następny kawałek
<kbd>z</kbd>: cofnij
</pre>
## Główne Panel (Zwykłe)
<pre>
<kbd>Ő</kbd>: przewiń w dół (fn+up)
<kbd>ő</kbd>: przewiń w górę (fn+down)
</pre>
## Główne Panel (Patch Building)
## Main Panel (Patch Building)
<pre>
<kbd>esc</kbd>: wyście z trybu "linia po linii"
@@ -252,11 +110,50 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
<kbd>H</kbd>: scroll left
<kbd>L</kbd>: scroll right
</pre>
## Główne Panel (Poczekalnia)
## Pliki
<pre>
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
<kbd>d</kbd>: pokaż opcje porzucania zmian
<kbd>space</kbd>: przełącz stan poczekalni
<kbd>ctrl+b</kbd>: Filter files (staged/unstaged)
<kbd>c</kbd>: Zatwierdź zmiany
<kbd>w</kbd>: zatwierdź zmiany bez skryptu pre-commit
<kbd>A</kbd>: Zmień ostatni commit
<kbd>C</kbd>: Zatwierdź zmiany używając edytora
<kbd>e</kbd>: edytuj plik
<kbd>o</kbd>: otwórz plik
<kbd>i</kbd>: Ignore or Exclude file
<kbd>r</kbd>: odśwież pliki
<kbd>s</kbd>: przechowaj zmiany
<kbd>S</kbd>: wyświetl opcje schowka
<kbd>a</kbd>: przełącz stan poczekalni wszystkich
<kbd>enter</kbd>: zatwierdź pojedyncze linie
<kbd>g</kbd>: view upstream reset options
<kbd>D</kbd>: wyświetl opcje resetu
<kbd>`</kbd>: toggle file tree view
<kbd>M</kbd>: open external merge tool (git mergetool)
<kbd>f</kbd>: pobierz
</pre>
## Pliki commita
<pre>
<kbd>ctrl+o</kbd>: copy the committed file name to the clipboard
<kbd>c</kbd>: plik wybierania
<kbd>d</kbd>: porzuć zmiany commita dla tego pliku
<kbd>o</kbd>: otwórz plik
<kbd>e</kbd>: edytuj plik
<kbd>space</kbd>: toggle file included in patch
<kbd>a</kbd>: toggle all files included in patch
<kbd>enter</kbd>: enter file to add selected lines to the patch (or toggle directory collapsed)
<kbd>`</kbd>: toggle file tree view
</pre>
## Poczekalnia
<pre>
<kbd>esc</kbd>: wróć do panelu plików
@@ -270,34 +167,77 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>►</kbd>: następny kawałek
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
<kbd>e</kbd>: edytuj plik
<kbd>o</kbd>: otwórz plik
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
<kbd>H</kbd>: scroll left
<kbd>L</kbd>: scroll right
<kbd>c</kbd>: Zatwierdź zmiany
<kbd>w</kbd>: zatwierdź zmiany bez skryptu pre-commit
<kbd>C</kbd>: Zatwierdź zmiany używając edytora
<kbd>E</kbd>: edit hunk
</pre>
## Menu Panel
## Reflog
<pre>
<kbd>esc</kbd>: close menu
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
<kbd>space</kbd>: checkout commit
<kbd>y</kbd>: copy commit attribute
<kbd>o</kbd>: open commit in browser
<kbd>n</kbd>: create new branch off of commit
<kbd>g</kbd>: wyświetl opcje resetu
<kbd>c</kbd>: kopiuj commit (przebieranie)
<kbd>C</kbd>: kopiuj zakres commitów (przebieranie)
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>enter</kbd>: view commits
</pre>
## Schowek Panel
## Remote Branches
<pre>
<kbd>space</kbd>: przełącz
<kbd>n</kbd>: nowa gałąź
<kbd>M</kbd>: scal do obecnej gałęzi
<kbd>r</kbd>: zmiana bazy gałęzi
<kbd>d</kbd>: usuń gałąź
<kbd>u</kbd>: set as upstream of checked-out branch
<kbd>esc</kbd>: wróć do listy repozytoriów zdalnych
<kbd>g</kbd>: wyświetl opcje resetu
<kbd>enter</kbd>: view commits
</pre>
## Remotes
<pre>
<kbd>f</kbd>: fetch remote
<kbd>n</kbd>: add new remote
<kbd>d</kbd>: remove remote
<kbd>e</kbd>: edit remote
</pre>
## Scalanie
<pre>
<kbd>esc</kbd>: wróć do panelu plików
<kbd>M</kbd>: open external merge tool (git mergetool)
<kbd>space</kbd>: wybierz kawałek
<kbd>b</kbd>: wybierz wszystkie kawałki
<kbd>◄</kbd>: poprzedni konflikt
<kbd>►</kbd>: następny konflikt
<kbd>▲</kbd>: wybierz poprzedni kawałek
<kbd>▼</kbd>: wybierz następny kawałek
<kbd>e</kbd>: edytuj plik
<kbd>o</kbd>: otwórz plik
<kbd>z</kbd>: cofnij
</pre>
## Schowek
<pre>
<kbd>enter</kbd>: view stash entry's files
<kbd>space</kbd>: zastosuj
<kbd>g</kbd>: wyciągnij
<kbd>d</kbd>: porzuć
<kbd>n</kbd>: nowa gałąź
<kbd>enter</kbd>: przeglądaj pliki commita
</pre>
## Status Panel
## Status
<pre>
<kbd>e</kbd>: edytuj konfigurację
@@ -306,3 +246,49 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>enter</kbd>: switch to a recent repo
<kbd>a</kbd>: pokaż wszystkie logi gałęzi
</pre>
## Sub-commits
<pre>
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
<kbd>space</kbd>: checkout commit
<kbd>y</kbd>: copy commit attribute
<kbd>o</kbd>: open commit in browser
<kbd>n</kbd>: create new branch off of commit
<kbd>g</kbd>: wyświetl opcje resetu
<kbd>c</kbd>: kopiuj commit (przebieranie)
<kbd>C</kbd>: kopiuj zakres commitów (przebieranie)
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>enter</kbd>: przeglądaj pliki commita
</pre>
## Submodules
<pre>
<kbd>ctrl+o</kbd>: copy submodule name to clipboard
<kbd>enter</kbd>: enter submodule
<kbd>d</kbd>: remove submodule
<kbd>u</kbd>: update submodule
<kbd>n</kbd>: add new submodule
<kbd>e</kbd>: update submodule URL
<kbd>i</kbd>: initialize submodule
<kbd>b</kbd>: view bulk submodule options
</pre>
## Tags
<pre>
<kbd>space</kbd>: przełącz
<kbd>d</kbd>: delete tag
<kbd>P</kbd>: push tag
<kbd>n</kbd>: create tag
<kbd>g</kbd>: wyświetl opcje resetu
<kbd>enter</kbd>: view commits
</pre>
## Zwykłe
<pre>
<kbd>mouse wheel down</kbd>: przewiń w dół (fn+up)
<kbd>mouse wheel up</kbd>: przewiń w górę (fn+down)
</pre>
+172 -186
View File
@@ -5,26 +5,26 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
## 全局键绑定
<pre>
<kbd>ctrl+r</kbd>: 切换到最近的仓库 (<c-r>)
<kbd>pgup</kbd>: 向上滚动主面板 (fn+up)
<kbd>pgdown</kbd>: 向下滚动主面板 (fn+down)
<kbd>ctrl+r</kbd>: 切换到最近的仓库
<kbd>pgup</kbd>: 向上滚动主面板 (fn+up/shift+k)
<kbd>pgdown</kbd>: 向下滚动主面板 (fn+down/shift+j)
<kbd>m</kbd>: 查看 合并/变基 选项
<kbd>ctrl+p</kbd>: 查看自定义补丁选项
<kbd>P</kbd>: 推送
<kbd>p</kbd>: 拉取
<kbd>R</kbd>: 刷新
<kbd>x</kbd>: 打开菜单
<kbd>z</kbd>: (通过 reflog)撤销「实验功能」
<kbd>ctrl+z</kbd>: (通过 reflog)重做「实验功能」
<kbd>+</kbd>: 下一屏模式(正常/半屏/全屏)
<kbd>_</kbd>: 上一屏模式
<kbd>:</kbd>: 执行自定义命令
<kbd>ctrl+s</kbd>: 查看按路径过滤选项
<kbd>W</kbd>: 打开 diff 菜单
<kbd>ctrl+e</kbd>: 打开 diff 菜单
<kbd>@</kbd>: 打开命令日志菜单
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
<kbd>}</kbd>: 扩大差异视图中显示的上下文范围
<kbd>{</kbd>: 缩小差异视图中显示的上下文范围
<kbd>:</kbd>: 执行自定义命令
<kbd>z</kbd>: (通过 reflog)撤销「实验功能」
<kbd>ctrl+z</kbd>: (通过 reflog)重做「实验功能」
<kbd>P</kbd>: 推送
<kbd>p</kbd>: 拉取
</pre>
## 列表面板导航
@@ -35,174 +35,65 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd><</kbd>: 滚动到顶部
<kbd>></kbd>: 滚动到底部
<kbd>/</kbd>: 开始搜索
<kbd>H</kbd>: 向左滚动
<kbd>L</kbd>: 向右滚动
<kbd>]</kbd>: 下一个标签
<kbd>[</kbd>: 上一个标签
</pre>
## 分支 面板 (分支标签)
## Reflog 页面
<pre>
<kbd>ctrl+o</kbd>: 将提交的 SHA 复制到剪贴板
<kbd>space</kbd>: 检出提交
<kbd>y</kbd>: copy commit attribute
<kbd>o</kbd>: 在浏览器中打开提交
<kbd>n</kbd>: 从提交创建新分支
<kbd>g</kbd>: 查看重置选项
<kbd>c</kbd>: 复制提交(拣选)
<kbd>C</kbd>: 复制提交范围(拣选)
<kbd>ctrl+r</kbd>: 重置已拣选(复制)的提交
<kbd>enter</kbd>: 查看提交
</pre>
## 分支页面
<pre>
<kbd>ctrl+o</kbd>: 将分支名称复制到剪贴板
<kbd>i</kbd>: 显示 git-flow 选项
<kbd>space</kbd>: 检出
<kbd>n</kbd>: 新分支
<kbd>o</kbd>: 创建抓取请求
<kbd>O</kbd>: 创建抓取请求选项
<kbd>ctrl+y</kbd>: 将抓取请求 URL 复制到剪贴板
<kbd>c</kbd>: 按名称检出
<kbd>F</kbd>: 强制检出
<kbd>n</kbd>: 新分支
<kbd>d</kbd>: 删除分支
<kbd>r</kbd>: 将已检出的分支变基到该分支
<kbd>M</kbd>: 合并到当前检出的分支
<kbd>i</kbd>: 显示 git-flow 选项
<kbd>f</kbd>: 从上游快进此分支
<kbd>g</kbd>: 查看重置选项
<kbd>R</kbd>: 重命名分支
<kbd>ctrl+o</kbd>: 将分支名称复制到剪贴板
<kbd>u</kbd>: set/unset upstream
<kbd>enter</kbd>: 查看提交
</pre>
## 分支 面板 (远程分支(在远程页面中))
## 子提交
<pre>
<kbd>esc</kbd>: 返回远程仓库列表
<kbd>g</kbd>: 查看重置选项
<kbd>enter</kbd>: 查看提交
<kbd>space</kbd>: 检出
<kbd>n</kbd>: 新分支
<kbd>M</kbd>: 合并到当前检出的分支
<kbd>d</kbd>: 删除分支
<kbd>r</kbd>: 将已检出的分支变基到该分支
<kbd>u</kbd>: 设置为检出分支的上游
</pre>
## 分支 面板 (远程页面)
<pre>
<kbd>f</kbd>: 抓取远程仓库
<kbd>n</kbd>: 添加新的远程仓库
<kbd>d</kbd>: 删除远程
<kbd>e</kbd>: 编辑远程仓库
</pre>
## 分支 面板 (子提交)
<pre>
<kbd>enter</kbd>: 查看提交的文件
<kbd>space</kbd>: 检出提交
<kbd>g</kbd>: 查看重置选项
<kbd>n</kbd>: 新分支
<kbd>c</kbd>: 复制提交(拣选)
<kbd>C</kbd>: 复制提交范围(拣选)
<kbd>ctrl+r</kbd>: 重置已拣选(复制)的提交
<kbd>ctrl+o</kbd>: 将提交的 SHA 复制到剪贴板
</pre>
## 分支 面板 (标签页面)
<pre>
<kbd>space</kbd>: 检出
<kbd>d</kbd>: 删除标签
<kbd>P</kbd>: 推送标签
<kbd>n</kbd>: 创建标签
<kbd>g</kbd>: 查看重置选项
<kbd>enter</kbd>: 查看提交
</pre>
## 提交文件 面板
<pre>
<kbd>ctrl+o</kbd>: 将提交的文件名复制到剪贴板
<kbd>c</kbd>: 检出文件
<kbd>d</kbd>: 放弃对此文件的提交更改
<kbd>o</kbd>: 打开文件
<kbd>e</kbd>: 编辑文件
<kbd>space</kbd>: 补丁中包含的切换文件
<kbd>enter</kbd>: 输入文件以将所选行添加到补丁中(或切换目录折叠)
<kbd>`</kbd>: 切换文件树视图
</pre>
## 提交 面板 (提交)
<pre>
<kbd>ctrl+l</kbd>: open log menu
<kbd>s</kbd>: 向下压缩
<kbd>r</kbd>: 改写提交
<kbd>R</kbd>: 使用编辑器重命名提交
<kbd>g</kbd>: 重置为此提交
<kbd>f</kbd>: 修正提交(fixup)
<kbd>F</kbd>: 为此提交创建修正
<kbd>S</kbd>: 压缩在所选提交之上的所有“fixup!”提交(自动压缩)
<kbd>d</kbd>: 删除提交
<kbd>ctrl+j</kbd>: 下移提交
<kbd>ctrl+k</kbd>: 上移提交
<kbd>e</kbd>: 编辑提交
<kbd>A</kbd>: 用已暂存的更改来修补提交
<kbd>p</kbd>: 选择提交(变基过程中)
<kbd>t</kbd>: 还原提交
<kbd>c</kbd>: 复制提交(拣选)
<kbd>ctrl+o</kbd>: 将提交的 SHA 复制到剪贴板
<kbd>C</kbd>: 复制提交范围(拣选)
<kbd>v</kbd>: 粘贴提交(拣选)
<kbd>enter</kbd>: 查看提交的文件
<kbd>space</kbd>: 检出提交
<kbd>y</kbd>: copy commit attribute
<kbd>o</kbd>: 在浏览器中打开提交
<kbd>n</kbd>: 从提交创建新分支
<kbd>T</kbd>: 标签提交
<kbd>ctrl+r</kbd>: 重置已拣选(复制)的提交
<kbd>ctrl+y</kbd>: 将提交消息复制到剪贴板
<kbd>o</kbd>: open commit in browser
<kbd>b</kbd>: view bisect options
</pre>
## 提交 面板 (Reflog)
<pre>
<kbd>enter</kbd>: 查看提交的文件
<kbd>space</kbd>: 检出提交
<kbd>g</kbd>: 查看重置选项
<kbd>c</kbd>: 复制提交(拣选)
<kbd>C</kbd>: 复制提交范围(拣选)
<kbd>ctrl+r</kbd>: 重置已拣选(复制)的提交
<kbd>ctrl+o</kbd>: 将提交的 SHA 复制到剪贴板
<kbd>enter</kbd>: 查看提交的文件
</pre>
## Extras 面板
<pre>
<kbd>@</kbd>: 打开命令日志菜单
</pre>
## 文件 面板
<pre>
<kbd>ctrl+b</kbd>: 过滤提交文件
</pre>
## 文件 面板 (文件)
<pre>
<kbd>c</kbd>: 提交更改
<kbd>w</kbd>: 提交更改而无需预先提交钩子
<kbd>A</kbd>: 修补最后一次提交
<kbd>C</kbd>: 提交更改(使用编辑器编辑提交信息)
<kbd>space</kbd>: 切换暂存状态
<kbd>d</kbd>: 查看'放弃更改‘选项
<kbd>e</kbd>: 编辑文件
<kbd>o</kbd>: 打开文件
<kbd>i</kbd>: 添加到 .gitignore
<kbd>r</kbd>: 刷新文件
<kbd>s</kbd>: 将所有更改加入贮藏
<kbd>S</kbd>: 查看隐藏选项
<kbd>a</kbd>: 切换所有文件的暂存状态
<kbd>D</kbd>: 查看重置选项
<kbd>enter</kbd>: 暂存单个 块/行 用于文件, 或 折叠/展开 目录
<kbd>f</kbd>: 抓取
<kbd>ctrl+o</kbd>: 将文件名复制到剪贴板
<kbd>g</kbd>: 查看上游重置选项
<kbd>`</kbd>: 切换文件树视图
<kbd>M</kbd>: 打开合并工具
<kbd>ctrl+w</kbd>: 切换是否在差异视图中显示空白更改
</pre>
## 文件 面板 (子模块)
## 子模块
<pre>
<kbd>ctrl+o</kbd>: 将子模块名称复制到剪贴板
@@ -215,30 +106,81 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>b</kbd>: 查看批量子模块选项
</pre>
## 主要 面板 (合并中)
## 提交
<pre>
<kbd>H</kbd>: scroll left
<kbd>L</kbd>: scroll right
<kbd>esc</kbd>: 返回文件面板
<kbd>M</kbd>: 打开合并工具
<kbd>space</kbd>: 选中区块
<kbd>b</kbd>: 选中所有区块
<kbd>◄</kbd>: 选择上一个冲突
<kbd>►</kbd>: 选择下一个冲突
<kbd>▲</kbd>: 选择顶部块
<kbd>▼</kbd>: 选择底部块
<kbd>z</kbd>: 撤销
<kbd>ctrl+o</kbd>: 将提交的 SHA 复制到剪贴板
<kbd>ctrl+r</kbd>: 重置已拣选(复制)的提交
<kbd>b</kbd>: 查看二分查找选项
<kbd>s</kbd>: 向下压缩
<kbd>f</kbd>: 修正提交(fixup)
<kbd>r</kbd>: 改写提交
<kbd>R</kbd>: 使用编辑器重命名提交
<kbd>d</kbd>: 删除提交
<kbd>e</kbd>: 编辑提交
<kbd>p</kbd>: 选择提交(变基过程中)
<kbd>F</kbd>: 为此提交创建修正
<kbd>S</kbd>: 压缩在所选提交之上的所有“fixup!”提交(自动压缩)
<kbd>ctrl+j</kbd>: 下移提交
<kbd>ctrl+k</kbd>: 上移提交
<kbd>v</kbd>: 粘贴提交(拣选)
<kbd>A</kbd>: 用已暂存的更改来修补提交
<kbd>a</kbd>: reset commit author
<kbd>t</kbd>: 还原提交
<kbd>T</kbd>: 标签提交
<kbd>ctrl+l</kbd>: 打开日志菜单
<kbd>space</kbd>: 检出提交
<kbd>y</kbd>: copy commit attribute
<kbd>o</kbd>: 在浏览器中打开提交
<kbd>n</kbd>: 从提交创建新分支
<kbd>g</kbd>: 查看重置选项
<kbd>c</kbd>: 复制提交(拣选)
<kbd>C</kbd>: 复制提交范围(拣选)
<kbd>enter</kbd>: 查看提交的文件
</pre>
## 主要 面板 (正常)
## 提交文件
<pre>
<kbd>Ő</kbd>: 向下滚动 (fn+up)
<kbd>ő</kbd>: 向上滚动 (fn+down)
<kbd>ctrl+o</kbd>: 将提交的文件名复制到剪贴板
<kbd>c</kbd>: 检出文件
<kbd>d</kbd>: 放弃对此文件的提交更改
<kbd>o</kbd>: 打开文件
<kbd>e</kbd>: 编辑文件
<kbd>space</kbd>: 补丁中包含的切换文件
<kbd>a</kbd>: toggle all files included in patch
<kbd>enter</kbd>: 输入文件以将所选行添加到补丁中(或切换目录折叠)
<kbd>`</kbd>: 切换文件树视图
</pre>
## 主要 面板 (构建补丁中)
## 文件
<pre>
<kbd>ctrl+o</kbd>: 将文件名复制到剪贴板
<kbd>ctrl+w</kbd>: 切换是否在差异视图中显示空白字符差异
<kbd>d</kbd>: 查看'放弃更改'选项
<kbd>space</kbd>: 切换暂存状态
<kbd>ctrl+b</kbd>: Filter files (staged/unstaged)
<kbd>c</kbd>: 提交更改
<kbd>w</kbd>: 提交更改而无需预先提交钩子
<kbd>A</kbd>: 修补最后一次提交
<kbd>C</kbd>: 提交更改(使用编辑器编辑提交信息)
<kbd>e</kbd>: 编辑文件
<kbd>o</kbd>: 打开文件
<kbd>i</kbd>: 忽略文件
<kbd>r</kbd>: 刷新文件
<kbd>s</kbd>: 将所有更改加入贮藏
<kbd>S</kbd>: 查看贮藏选项
<kbd>a</kbd>: 切换所有文件的暂存状态
<kbd>enter</kbd>: 暂存单个 块/行 用于文件, 或 折叠/展开 目录
<kbd>g</kbd>: 查看上游重置选项
<kbd>D</kbd>: 查看重置选项
<kbd>`</kbd>: 切换文件树视图
<kbd>M</kbd>: 打开外部合并工具 (git mergetool)
<kbd>f</kbd>: 抓取
</pre>
## 构建补丁中
<pre>
<kbd>esc</kbd>: 退出逐行模式
@@ -247,16 +189,41 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>▼</kbd>: 选择下一行
<kbd>◄</kbd>: 选择上一个区块
<kbd>►</kbd>: 选择下一个区块
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
<kbd>ctrl+o</kbd>: 将选中文本复制到剪贴板
<kbd>space</kbd>: 添加/移除 行到补丁
<kbd>v</kbd>: 切换拖动选择
<kbd>V</kbd>: 切换拖动选择
<kbd>a</kbd>: 切换选择区块
<kbd>H</kbd>: scroll left
<kbd>L</kbd>: scroll right
</pre>
## 主要 面板 (正在暂存)
## 标签页面
<pre>
<kbd>space</kbd>: 检出
<kbd>d</kbd>: 删除标签
<kbd>P</kbd>: 推送标签
<kbd>n</kbd>: 创建标签
<kbd>g</kbd>: 查看重置选项
<kbd>enter</kbd>: 查看提交
</pre>
## 正在合并
<pre>
<kbd>esc</kbd>: 返回文件面板
<kbd>M</kbd>: 打开外部合并工具 (git mergetool)
<kbd>space</kbd>: 选中区块
<kbd>b</kbd>: 选中所有区块
<kbd>◄</kbd>: 选择上一个冲突
<kbd>►</kbd>: 选择下一个冲突
<kbd>▲</kbd>: 选择顶部块
<kbd>▼</kbd>: 选择底部块
<kbd>e</kbd>: 编辑文件
<kbd>o</kbd>: 打开文件
<kbd>z</kbd>: 撤销
</pre>
## 正在暂存
<pre>
<kbd>esc</kbd>: 返回文件面板
@@ -268,36 +235,22 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>▼</kbd>: 选择下一行
<kbd>◄</kbd>: 选择上一个区块
<kbd>►</kbd>: 选择下一个区块
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
<kbd>ctrl+o</kbd>: 将选中文本复制到剪贴板
<kbd>e</kbd>: 编辑文件
<kbd>o</kbd>: 打开文件
<kbd>v</kbd>: 切换拖动选择
<kbd>V</kbd>: 切换拖动选择
<kbd>a</kbd>: 切换选择区块
<kbd>H</kbd>: scroll left
<kbd>L</kbd>: scroll right
<kbd>c</kbd>: 提交更改
<kbd>w</kbd>: 提交更改而无需预先提交钩子
<kbd>C</kbd>: 提交更改(使用编辑器编辑提交信息)
<kbd>E</kbd>: edit hunk
</pre>
## 菜单 面板
## 正常
<pre>
<kbd>esc</kbd>: 关闭菜单
<kbd>mouse wheel down</kbd>: 向下滚动 (fn+up)
<kbd>mouse wheel up</kbd>: 向上滚动 (fn+down)
</pre>
## 贮藏 面板
<pre>
<kbd>enter</kbd>: 查看贮藏条目中的文件
<kbd>space</kbd>: 应用
<kbd>g</kbd>: 应用并删除
<kbd>d</kbd>: 删除
<kbd>n</kbd>: 新分支
</pre>
## 状态 面板
## 状态
<pre>
<kbd>e</kbd>: 编辑配置文件
@@ -306,3 +259,36 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
<kbd>enter</kbd>: 切换到最近的仓库
<kbd>a</kbd>: 显示所有分支的日志
</pre>
## 贮藏
<pre>
<kbd>space</kbd>: 应用
<kbd>g</kbd>: 应用并删除
<kbd>d</kbd>: 删除
<kbd>n</kbd>: 新分支
<kbd>enter</kbd>: 查看提交的文件
</pre>
## 远程分支
<pre>
<kbd>space</kbd>: 检出
<kbd>n</kbd>: 新分支
<kbd>M</kbd>: 合并到当前检出的分支
<kbd>r</kbd>: 将已检出的分支变基到该分支
<kbd>d</kbd>: 删除分支
<kbd>u</kbd>: 设置为检出分支的上游
<kbd>esc</kbd>: 返回远程仓库列表
<kbd>g</kbd>: 查看重置选项
<kbd>enter</kbd>: 查看提交
</pre>
## 远程页面
<pre>
<kbd>f</kbd>: 抓取远程仓库
<kbd>n</kbd>: 添加新的远程仓库
<kbd>d</kbd>: 删除远程
<kbd>e</kbd>: 编辑远程仓库
</pre>
+41 -19
View File
@@ -1,6 +1,6 @@
module github.com/jesseduffield/lazygit
go 1.14
go 1.18
require (
github.com/OpenPeeDeeP/xdg v1.0.0
@@ -9,42 +9,64 @@ 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 // indirect
github.com/fsmiamoto/git-todo-parser v0.0.2
github.com/fsnotify/fsnotify v1.4.7
github.com/gdamore/tcell/v2 v2.4.1-0.20220313203054-2a1a1b586447 // indirect
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 // indirect
github.com/golang/protobuf v1.3.2 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/go-errors/errors v1.4.2
github.com/gookit/color v1.4.2
github.com/imdario/mergo v0.3.11
github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4
github.com/jesseduffield/gocui v0.3.1-0.20220108045521-1945d7b9ed8b
github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
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
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mattn/go-runewidth v0.0.13
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/pmezard/go-difflib v1.0.0
github.com/sahilm/fuzzy v0.1.0
github.com/samber/lo v1.10.1
github.com/sanity-io/litter v1.5.2
github.com/sirupsen/logrus v1.4.2
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
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-20211216021012-1d35b9e2eb4e // 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
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emirpasic/gods v1.12.0 // indirect
github.com/fatih/color v1.9.0 // indirect
github.com/gdamore/encoding v1.0.0 // indirect
github.com/gdamore/tcell/v2 v2.5.1 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.0.0 // indirect
github.com/go-logfmt/logfmt v0.5.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
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/sergi/go-diff v1.1.0 // indirect
github.com/xanzy/ssh-agent v0.2.1 // indirect
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 // indirect
golang.org/x/exp v0.0.0-20220318154914-8dddf5d87bd8 // indirect
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c // indirect
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect
golang.org/x/text v0.3.7 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)
+24 -22
View File
@@ -27,23 +27,21 @@ github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3
github.com/fatih/color v1.7.1-0.20180516100307-2d684516a886/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/fsmiamoto/git-todo-parser v0.0.2 h1:l6Y+9q7jbM+yK/w6kASpHO7ejL9ARCErm3tCEqOT278=
github.com/fsmiamoto/git-todo-parser v0.0.2/go.mod h1:B+AgTbNE2BARvJqzXygThzqxLIaEWvwr2sxKYYb0Fas=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.4.0 h1:W6dxJEmaxYvhICFoTY3WrLLEXsQ11SaFnKGVEXW57KM=
github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
github.com/gdamore/tcell/v2 v2.4.1-0.20210926162909-66f061b1fc9b h1:eoaSI4eEwM5eTx/HvmRSwmicxuMhL73AyoEfM1oCJLc=
github.com/gdamore/tcell/v2 v2.4.1-0.20210926162909-66f061b1fc9b/go.mod h1:ZPwXnysybtQqdqKcWMWXux9aGdtMHe+kr+cwEZEe+A4=
github.com/gdamore/tcell/v2 v2.4.1-0.20220313203054-2a1a1b586447 h1:4idf9699cuWAc7ZIB+2RzuDWU30oRkB0X/FZTUlWOVY=
github.com/gdamore/tcell/v2 v2.4.1-0.20220313203054-2a1a1b586447/go.mod h1:I8YJFI9gzgl4dHi9UlRDZosCW+jYkDA37AXmXvL51w4=
github.com/gdamore/tcell/v2 v2.5.1 h1:zc3LPdpK184lBW7syF2a5C6MV827KmErk9jGVnmsl/I=
github.com/gdamore/tcell/v2 v2.5.1/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo=
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.4.1 h1:IvVlgbzSsaUNudsw5dcXSzF3EWyXTi5XrAdngnuhRyg=
github.com/go-errors/errors v1.4.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/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=
@@ -55,11 +53,7 @@ github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih
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=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -74,10 +68,14 @@ github.com/integrii/flaggy v1.4.0 h1:A1x7SYx4jqu5NSrY14z8Z+0UyX2S5ygfJJrfolWR3zM
github.com/integrii/flaggy v1.4.0/go.mod h1:tnTxHeTJbah0gQ6/K0RW0J7fMUBk9MCF5blhm43LNpI=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 h1:EQP2Tv8TIcC6Y4RI+1ZbJDOHfGJ570tPeYVCqo7/tws=
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk=
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.20220108045521-1945d7b9ed8b h1:AUK5nDiPiaahBtGIsf8rITgZ9SC+uddvnNKs0/mrYA8=
github.com/jesseduffield/gocui v0.3.1-0.20220108045521-1945d7b9ed8b/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6 h1:Fmay0Lz21taUpXiIbFkjjIIcn0E5GKwp5UFRuXaOiGQ=
github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0=
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo=
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=
@@ -137,6 +135,8 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/samber/lo v1.10.1 h1:0D3h7i0U3hRAbaCeQ82DLe67n0A7Bbl0/cEoWqFGp+U=
github.com/samber/lo v1.10.1/go.mod h1:2I7tgIv8Q1SG2xEIkRq0F2i2zgxVpnyPOP0d3Gj2r+A=
github.com/sanity-io/litter v1.5.2 h1:AnC8s9BMORWH5a4atZ4D6FPVvKGzHcnc5/IVTa87myw=
github.com/sanity-io/litter v1.5.2/go.mod h1:5Z71SvaYy5kcGtyglXOC9rrUi3c1E8CamFWjQsazTh0=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
@@ -154,6 +154,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
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/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
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=
@@ -165,6 +166,8 @@ golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPh
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/exp v0.0.0-20220318154914-8dddf5d87bd8 h1:s/+U+w0teGzcoH2mdIlFQ6KfVKGaYpgyGdUefZrn9TU=
golang.org/x/exp v0.0.0-20220318154914-8dddf5d87bd8/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
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/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -183,21 +186,19 @@ golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211113001501-0c823b97ae02/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 h1:EH1Deb8WZJ0xc0WK//leUHXcX9aLE5SymusoTmMZye8=
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/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/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -215,5 +216,6 @@ 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=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+50 -22
View File
@@ -7,13 +7,15 @@ import (
"os"
"path/filepath"
"runtime"
"strings"
"github.com/go-errors/errors"
"github.com/integrii/flaggy"
"github.com/jesseduffield/lazygit/pkg/app"
"github.com/jesseduffield/lazygit/pkg/app/daemon"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/constants"
"github.com/jesseduffield/lazygit/pkg/env"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/logs"
yaml "github.com/jesseduffield/yaml"
)
@@ -33,9 +35,8 @@ func main() {
filterPath := ""
flaggy.String(&filterPath, "f", "filter", "Path to filter on in `git log -- <path>`. When in filter mode, the commits, reflog, and stash are filtered based on the given path, and some operations are restricted")
dump := ""
flaggy.AddPositionalValue(&dump, "gitargs", 1, false, "Todo file")
flaggy.DefaultParser.PositionalFlags[0].Hidden = true
gitArg := ""
flaggy.AddPositionalValue(&gitArg, "git-arg", 1, false, "Panel to focus upon opening lazygit. Accepted values (based on git terminology): status, branch, log, stash. Ignored if --filter arg is passed.")
versionFlag := false
flaggy.Bool(&versionFlag, "v", "version", "Print the current version")
@@ -62,7 +63,7 @@ func main() {
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.String(&customConfig, "ucf", "use-config-file", "Comma separated list to custom config file(s)")
flaggy.Parse()
@@ -117,7 +118,7 @@ func main() {
}
if logFlag {
app.TailLogs()
logs.TailLogs()
os.Exit(0)
}
@@ -127,25 +128,52 @@ func main() {
}
}
appConfig, err := config.NewAppConfig("lazygit", version, commit, date, buildSource, debuggingFlag)
tempDir, err := os.MkdirTemp("", "lazygit-*")
if err != nil {
log.Fatal(err.Error())
}
defer os.RemoveAll(tempDir)
appConfig, err := config.NewAppConfig("lazygit", version, commit, date, buildSource, debuggingFlag, tempDir)
if err != nil {
log.Fatal(err.Error())
}
app, err := app.NewApp(appConfig, filterPath)
if err == nil {
err = app.Run()
}
common, err := app.NewCommon(appConfig)
if err != nil {
if errorMessage, known := app.KnownError(err); known {
log.Fatal(errorMessage)
}
newErr := errors.Wrap(err, 0)
stackTrace := newErr.ErrorStack()
app.Log.Error(stackTrace)
log.Fatal(fmt.Sprintf("%s: %s\n\n%s", app.Tr.ErrorOccurred, constants.Links.Issues, stackTrace))
log.Fatal(err)
}
if daemon.InDaemonMode() {
daemon.Handle(common)
return
}
parsedGitArg := parseGitArg(gitArg)
app.Run(appConfig, common, types.NewStartArgs(filterPath, parsedGitArg))
}
func parseGitArg(gitArg string) types.GitArg {
typedArg := types.GitArg(gitArg)
// using switch so that linter catches when a new git arg value is defined but not handled here
switch typedArg {
case types.GitArgNone, types.GitArgStatus, types.GitArgBranch, types.GitArgLog, types.GitArgStash:
return typedArg
}
permittedValues := []string{
string(types.GitArgStatus),
string(types.GitArgBranch),
string(types.GitArgLog),
string(types.GitArgStash),
}
log.Fatalf("Invalid git arg value: '%s'. Must be one of the following values: %s. e.g. 'lazygit status'. See 'lazygit --help'.",
gitArg,
strings.Join(permittedValues, ", "),
)
panic("unreachable")
}
+71 -184
View File
@@ -2,10 +2,8 @@ package app
import (
"bufio"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
@@ -13,117 +11,87 @@ import (
"strconv"
"strings"
"github.com/aybabtme/humanlog"
"github.com/go-errors/errors"
"github.com/jesseduffield/generics/slices"
"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/common"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/constants"
"github.com/jesseduffield/lazygit/pkg/env"
"github.com/jesseduffield/lazygit/pkg/gui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/updates"
"github.com/sirupsen/logrus"
)
// App struct
// App is the struct that's instantiated from within main.go and it manages
// bootstrapping and running the application.
type App struct {
*common.Common
closers []io.Closer
Config config.AppConfigurer
OSCommand *oscommands.OSCommand
Gui *gui.Gui
Updater *updates.Updater // may only need this on the Gui
ClientContext string
closers []io.Closer
Config config.AppConfigurer
OSCommand *oscommands.OSCommand
Gui *gui.Gui
Updater *updates.Updater // may only need this on the Gui
}
type errorMapping struct {
originalError string
newError string
}
func Run(config config.AppConfigurer, common *common.Common, startArgs types.StartArgs) {
app, err := NewApp(config, common)
func newProductionLogger() *logrus.Logger {
log := logrus.New()
log.Out = ioutil.Discard
log.SetLevel(logrus.ErrorLevel)
return log
}
if err == nil {
err = app.Run(startArgs)
}
func getLogLevel() logrus.Level {
strLevel := os.Getenv("LOG_LEVEL")
level, err := logrus.ParseLevel(strLevel)
if err != nil {
return logrus.DebugLevel
if errorMessage, known := knownError(common.Tr, err); known {
log.Fatal(errorMessage)
}
newErr := errors.Wrap(err, 0)
stackTrace := newErr.ErrorStack()
app.Log.Error(stackTrace)
log.Fatalf("%s: %s\n\n%s", common.Tr.ErrorOccurred, constants.Links.Issues, stackTrace)
}
return level
}
func newDevelopmentLogger() *logrus.Logger {
logger := logrus.New()
logger.SetLevel(getLogLevel())
logPath, err := config.LogPath()
if err != nil {
log.Fatal(err)
}
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Fatalf("Unable to log to log file: %v", err)
}
logger.SetOutput(file)
return logger
}
func newLogger(config config.AppConfigurer) *logrus.Entry {
var log *logrus.Logger
if config.GetDebug() || os.Getenv("DEBUG") == "TRUE" {
log = newDevelopmentLogger()
} else {
log = newProductionLogger()
}
// highly recommended: tail -f development.log | humanlog
// https://github.com/aybabtme/humanlog
log.Formatter = &logrus.JSONFormatter{}
return log.WithFields(logrus.Fields{
"debug": config.GetDebug(),
"version": config.GetVersion(),
"commit": config.GetCommit(),
"buildDate": config.GetBuildDate(),
})
}
// NewApp bootstrap a new application
func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
func NewCommon(config config.AppConfigurer) (*common.Common, error) {
userConfig := config.GetUserConfig()
app := &App{
closers: []io.Closer{},
Config: config,
}
var err error
log := newLogger(config)
tr, err := i18n.NewTranslationSetFromConfig(log, userConfig.Gui.Language)
if err != nil {
return app, err
return nil, err
}
app.Common = &common.Common{
return &common.Common{
Log: log,
Tr: tr,
UserConfig: userConfig,
Debug: config.GetDebug(),
}, nil
}
// NewApp bootstrap a new application
func NewApp(config config.AppConfigurer, common *common.Common) (*App, error) {
app := &App{
closers: []io.Closer{},
Config: config,
Common: common,
}
// if we are being called in 'demon' mode, we can just return here
app.ClientContext = os.Getenv("LAZYGIT_CLIENT_COMMAND")
if app.ClientContext != "" {
return app, nil
app.OSCommand = oscommands.NewOSCommand(common, config, oscommands.GetPlatform(), oscommands.NewNullGuiIO(app.Log))
var err error
app.Updater, err = updates.NewUpdater(common, config, app.OSCommand)
if err != nil {
return app, err
}
app.OSCommand = oscommands.NewOSCommand(app.Common, oscommands.GetPlatform(), oscommands.NewNullGuiIO(log))
app.Updater, err = updates.NewUpdater(app.Common, config, app.OSCommand)
dirName, err := os.Getwd()
if err != nil {
return app, err
}
@@ -135,7 +103,7 @@ func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
gitConfig := git_config.NewStdCachedGitConfig(app.Log)
app.Gui, err = gui.NewGui(app.Common, config, gitConfig, app.Updater, filterPath, showRecentRepos)
app.Gui, err = gui.NewGui(common, config, gitConfig, app.Updater, showRecentRepos, dirName)
if err != nil {
return app, err
}
@@ -178,6 +146,11 @@ func isGitVersionValid(versionStr string) bool {
return true
}
func isDirectoryAGitRepository(dir string) (bool, error) {
info, err := os.Stat(filepath.Join(dir, ".git"))
return info != nil && info.IsDir(), err
}
func (app *App) setupRepo() (bool, error) {
if err := app.validateGitVersion(); err != nil {
return false, err
@@ -194,19 +167,26 @@ func (app *App) setupRepo() (bool, error) {
if err != nil {
return false, err
}
info, _ := os.Stat(filepath.Join(cwd, ".git"))
if info != nil && info.IsDir() {
return false, err // Current directory appears to be a git repository.
if isRepo, err := isDirectoryAGitRepository(cwd); isRepo {
return false, err
}
shouldInitRepo := true
notARepository := app.UserConfig.NotARepository
initialBranch := ""
if notARepository == "prompt" {
// Offer to initialize a new repository in current directory.
fmt.Print(app.Tr.CreateRepo)
response, _ := bufio.NewReader(os.Stdin).ReadString('\n')
if strings.Trim(response, " \n") != "y" {
if strings.Trim(response, " \r\n") != "y" {
shouldInitRepo = false
} else {
// Ask for the initial branch name
fmt.Print(app.Tr.InitialBranch)
response, _ := bufio.NewReader(os.Stdin).ReadString('\n')
if trimmedResponse := strings.Trim(response, " \r\n"); len(trimmedResponse) > 0 {
initialBranch += "--initial-branch=" + trimmedResponse
}
}
} else if notARepository == "skip" {
shouldInitRepo = false
@@ -214,21 +194,18 @@ func (app *App) setupRepo() (bool, error) {
if !shouldInitRepo {
// check if we have a recent repo we can open
recentRepos := app.Config.GetAppState().RecentRepos
if len(recentRepos) > 0 {
var err error
// try opening each repo in turn, in case any have been deleted
for _, repoDir := range recentRepos {
if err = os.Chdir(repoDir); err == nil {
for _, repoDir := range app.Config.GetAppState().RecentRepos {
if isRepo, _ := isDirectoryAGitRepository(repoDir); isRepo {
if err := os.Chdir(repoDir); err == nil {
return true, nil
}
}
return false, err
}
fmt.Println(app.Tr.NoRecentRepositories)
os.Exit(1)
}
if err := app.OSCommand.Cmd.New("git init").Run(); err != nil {
if err := app.OSCommand.Cmd.New("git init " + initialBranch).Run(); err != nil {
return false, err
}
}
@@ -236,104 +213,14 @@ func (app *App) setupRepo() (bool, error) {
return false, nil
}
func (app *App) Run() error {
if app.ClientContext == "INTERACTIVE_REBASE" {
return app.Rebase()
}
if app.ClientContext == "EXIT_IMMEDIATELY" {
os.Exit(0)
}
err := app.Gui.RunAndHandleError()
func (app *App) Run(startArgs types.StartArgs) error {
err := app.Gui.RunAndHandleError(startArgs)
return err
}
func gitDir() string {
dir := env.GetGitDirEnv()
if dir == "" {
return ".git"
}
return dir
}
// Rebase contains logic for when we've been run in demon mode, meaning we've
// given lazygit as a command for git to call e.g. to edit a file
func (app *App) Rebase() error {
app.Log.Info("Lazygit invoked as interactive rebase demon")
app.Log.Info("args: ", os.Args)
if strings.HasSuffix(os.Args[1], "git-rebase-todo") {
if err := ioutil.WriteFile(os.Args[1], []byte(os.Getenv("LAZYGIT_REBASE_TODO")), 0644); err != nil {
return err
}
} else if strings.HasSuffix(os.Args[1], filepath.Join(gitDir(), "COMMIT_EDITMSG")) { // TODO: test
// if we are rebasing and squashing, we'll see a COMMIT_EDITMSG
// but in this case we don't need to edit it, so we'll just return
} else {
app.Log.Info("Lazygit demon did not match on any use cases")
}
return nil
}
// Close closes any resources
func (app *App) Close() error {
for _, closer := range app.closers {
err := closer.Close()
if err != nil {
return err
}
}
return nil
}
// KnownError takes an error and tells us whether it's an error that we know about where we can print a nicely formatted version of it rather than panicking with a stack trace
func (app *App) KnownError(err error) (string, bool) {
errorMessage := err.Error()
knownErrorMessages := []string{app.Tr.MinGitVersionError}
for _, message := range knownErrorMessages {
if errorMessage == message {
return message, true
}
}
mappings := []errorMapping{
{
originalError: "fatal: not a git repository",
newError: app.Tr.NotARepository,
},
}
for _, mapping := range mappings {
if strings.Contains(errorMessage, mapping.originalError) {
return mapping.newError, true
}
}
return "", false
}
func TailLogs() {
logFilePath, err := config.LogPath()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Tailing log file %s\n\n", logFilePath)
opts := humanlog.DefaultOptions
opts.Truncates = false
_, err = os.Stat(logFilePath)
if err != nil {
if os.IsNotExist(err) {
log.Fatal("Log file does not exist. Run `lazygit --debug` first to create the log file")
}
log.Fatal(err)
}
TailLogsForPlatform(logFilePath, opts)
return slices.TryForEach(app.closers, func(closer io.Closer) error {
return closer.Close()
})
}
+107
View File
@@ -0,0 +1,107 @@
package daemon
import (
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/env"
)
// Sometimes lazygit will be invoked in daemon mode from a parent lazygit process.
// We do this when git lets us supply a program to run within a git command.
// For example, if we want to ensure that a git command doesn't hang due to
// waiting for an editor to save a commit message, we can tell git to invoke lazygit
// as the editor via 'GIT_EDITOR=lazygit', and use the env var
// 'LAZYGIT_DAEMON_KIND=EXIT_IMMEDIATELY' to specify that we want to run lazygit
// as a daemon which simply exits immediately. Any additional arguments we want
// to pass to a daemon can be done via other env vars.
type DaemonKind string
const (
InteractiveRebase DaemonKind = "INTERACTIVE_REBASE"
ExitImmediately DaemonKind = "EXIT_IMMEDIATELY"
)
const (
DaemonKindEnvKey string = "LAZYGIT_DAEMON_KIND"
RebaseTODOEnvKey string = "LAZYGIT_REBASE_TODO"
)
type Daemon interface {
Run() error
}
func Handle(common *common.Common) {
d := getDaemon(common)
if d == nil {
return
}
if err := d.Run(); err != nil {
log.Fatal(err)
}
os.Exit(0)
}
func InDaemonMode() bool {
return getDaemonKind() != ""
}
func getDaemon(common *common.Common) Daemon {
switch getDaemonKind() {
case InteractiveRebase:
return &rebaseDaemon{c: common}
case ExitImmediately:
return &exitImmediatelyDaemon{c: common}
}
return nil
}
func getDaemonKind() DaemonKind {
return DaemonKind(os.Getenv(DaemonKindEnvKey))
}
type rebaseDaemon struct {
c *common.Common
}
func (self *rebaseDaemon) Run() error {
self.c.Log.Info("Lazygit invoked as interactive rebase demon")
self.c.Log.Info("args: ", os.Args)
if strings.HasSuffix(os.Args[1], "git-rebase-todo") {
if err := ioutil.WriteFile(os.Args[1], []byte(os.Getenv(RebaseTODOEnvKey)), 0o644); err != nil {
return err
}
} else if strings.HasSuffix(os.Args[1], filepath.Join(gitDir(), "COMMIT_EDITMSG")) { // TODO: test
// if we are rebasing and squashing, we'll see a COMMIT_EDITMSG
// but in this case we don't need to edit it, so we'll just return
} else {
self.c.Log.Info("Lazygit demon did not match on any use cases")
}
return nil
}
func gitDir() string {
dir := env.GetGitDirEnv()
if dir == "" {
return ".git"
}
return dir
}
type exitImmediatelyDaemon struct {
c *common.Common
}
func (self *exitImmediatelyDaemon) Run() error {
return nil
}
+39
View File
@@ -0,0 +1,39 @@
package app
import (
"strings"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/i18n"
)
type errorMapping struct {
originalError string
newError string
}
// knownError takes an error and tells us whether it's an error that we know about where we can print a nicely formatted version of it rather than panicking with a stack trace
func knownError(tr *i18n.TranslationSet, err error) (string, bool) {
errorMessage := err.Error()
knownErrorMessages := []string{tr.MinGitVersionError}
if slices.Contains(knownErrorMessages, errorMessage) {
return errorMessage, true
}
mappings := []errorMapping{
{
originalError: "fatal: not a git repository",
newError: tr.NotARepository,
},
}
if mapping, ok := slices.Find(mappings, func(mapping errorMapping) bool {
return strings.Contains(errorMessage, mapping.originalError)
}); ok {
return mapping.newError, true
}
return "", false
}
+46 -16
View File
@@ -1,31 +1,61 @@
//go:build !windows
// +build !windows
package app
import (
"io/ioutil"
"log"
"os"
"github.com/aybabtme/humanlog"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/sirupsen/logrus"
)
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)
func newLogger(config config.AppConfigurer) *logrus.Entry {
var log *logrus.Logger
if config.GetDebug() || os.Getenv("DEBUG") == "TRUE" {
log = newDevelopmentLogger()
} else {
log = newProductionLogger()
}
if err := humanlog.Scanner(stdout, os.Stdout, opts); err != nil {
log.Fatal(err)
}
// highly recommended: tail -f development.log | humanlog
// https://github.com/aybabtme/humanlog
log.Formatter = &logrus.JSONFormatter{}
if err := cmd.Wait(); err != nil {
return log.WithFields(logrus.Fields{
"debug": config.GetDebug(),
"version": config.GetVersion(),
"commit": config.GetCommit(),
"buildDate": config.GetBuildDate(),
})
}
func newProductionLogger() *logrus.Logger {
log := logrus.New()
log.Out = ioutil.Discard
log.SetLevel(logrus.ErrorLevel)
return log
}
func newDevelopmentLogger() *logrus.Logger {
logger := logrus.New()
logger.SetLevel(getLogLevel())
logPath, err := config.LogPath()
if err != nil {
log.Fatal(err)
}
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
if err != nil {
log.Fatalf("Unable to log to log file: %v", err)
}
logger.SetOutput(file)
return logger
}
os.Exit(0)
func getLogLevel() logrus.Level {
strLevel := os.Getenv("LOG_LEVEL")
level, err := logrus.ParseLevel(strLevel)
if err != nil {
return logrus.DebugLevel
}
return level
}
+7 -8
View File
@@ -17,11 +17,11 @@ func Check() {
tmpDir := filepath.Join(os.TempDir(), "lazygit_cheatsheet")
err := os.RemoveAll(tmpDir)
if err != nil {
log.Fatalf("Error occured while checking if cheatsheets are up to date: %v", err)
log.Fatalf("Error occurred while checking if cheatsheets are up to date: %v", err)
}
err = os.Mkdir(tmpDir, 0700)
err = os.Mkdir(tmpDir, 0o700)
if err != nil {
log.Fatalf("Error occured while checking if cheatsheets are up to date: %v", err)
log.Fatalf("Error occurred while checking if cheatsheets are up to date: %v", err)
}
generateAtDir(tmpDir)
@@ -45,9 +45,9 @@ func Check() {
Context: 1,
})
if err != nil {
log.Fatalf("Error occured while checking if cheatsheets are up to date: %v", err)
log.Fatalf("Error occurred while checking if cheatsheets are up to date: %v", err)
}
fmt.Printf("\nCheatsheets are out of date. Please run `%s` at the project root and commit the changes\n", CommandToRun())
fmt.Printf("\nCheatsheets are out of date. Please run `%s` at the project root and commit the changes. If you run the script and no keybindings files are updated as a result, try rebasing onto master and trying again.\n", CommandToRun())
os.Exit(1)
}
@@ -62,7 +62,7 @@ func obtainContent(dir string) string {
if re.MatchString(path) {
bytes, err := ioutil.ReadFile(path)
if err != nil {
log.Fatalf("Error occured while checking if cheatsheets are up to date: %v", err)
log.Fatalf("Error occurred while checking if cheatsheets are up to date: %v", err)
}
content += fmt.Sprintf("\n%s\n\n", filepath.Base(path))
content += string(bytes)
@@ -70,9 +70,8 @@ func obtainContent(dir string) string {
return nil
})
if err != nil {
log.Fatalf("Error occured while checking if cheatsheets are up to date: %v", err)
log.Fatalf("Error occurred while checking if cheatsheets are up to date: %v", err)
}
return content
+84 -131
View File
@@ -12,18 +12,33 @@ import (
"fmt"
"log"
"os"
"sort"
"github.com/jesseduffield/generics/maps"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/app"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui"
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/integration"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
type bindingSection struct {
title string
bindings []*gui.Binding
bindings []*types.Binding
}
type header struct {
// priority decides the order of the headers in the cheatsheet (lower means higher)
priority int
title string
}
type headerWithBindings struct {
header header
bindings []*types.Binding
}
func CommandToRun() string {
@@ -40,14 +55,19 @@ func generateAtDir(cheatsheetDir string) {
for lang := range translationSetsByLang {
mConfig.GetUserConfig().Gui.Language = lang
mApp, _ := app.NewApp(mConfig, "")
common, err := app.NewCommon(mConfig)
if err != nil {
log.Fatal(err)
}
mApp, _ := app.NewApp(mConfig, common)
path := cheatsheetDir + "/Keybindings_" + lang + ".md"
file, err := os.Create(path)
if err != nil {
panic(err)
}
bindingSections := getBindingSections(mApp)
bindings := mApp.Gui.GetCheatsheetKeybindings()
bindingSections := getBindingSections(bindings, mApp.Tr)
content := formatSections(mApp.Tr, bindingSections)
content = fmt.Sprintf("_This file is auto-generated. To update, make the changes in the "+
"pkg/i18n directory and then run `%s` from the project root._\n\n%s", CommandToRun(), content)
@@ -66,9 +86,7 @@ func writeString(file *os.File, str string) {
}
}
func localisedTitle(mApp *app.App, str string) string {
tr := mApp.Tr
func localisedTitle(tr *i18n.TranslationSet, str string) string {
contextTitleMap := map[string]string{
"global": tr.GlobalTitle,
"navigation": tr.NavigationTitle,
@@ -86,7 +104,6 @@ func localisedTitle(mApp *app.App, str string) string {
"commitMessage": tr.CommitMessageTitle,
"commits": tr.CommitsTitle,
"confirmation": tr.ConfirmationTitle,
"credentials": tr.CredentialsTitle,
"information": tr.InformationTitle,
"main": tr.MainTitle,
"patchBuilding": tr.PatchBuildingTitle,
@@ -109,142 +126,62 @@ func localisedTitle(mApp *app.App, str string) string {
return title
}
func formatTitle(title string) string {
return fmt.Sprintf("\n## %s\n\n", title)
}
func formatBinding(binding *gui.Binding) string {
if binding.Alternative != "" {
return fmt.Sprintf(" <kbd>%s</kbd>: %s (%s)\n", gui.GetKeyDisplay(binding.Key), binding.Description, binding.Alternative)
}
return fmt.Sprintf(" <kbd>%s</kbd>: %s\n", gui.GetKeyDisplay(binding.Key), binding.Description)
}
func getBindingSections(mApp *app.App) []*bindingSection {
bindingSections := []*bindingSection{}
bindings := mApp.Gui.GetInitialKeybindings()
type contextAndViewType struct {
subtitle string
title string
}
contextAndViewBindingMap := map[contextAndViewType][]*gui.Binding{}
outer:
for _, binding := range bindings {
if binding.Tag == "navigation" {
key := contextAndViewType{subtitle: "", title: "navigation"}
existing := contextAndViewBindingMap[key]
if existing == nil {
contextAndViewBindingMap[key] = []*gui.Binding{binding}
} else {
for _, navBinding := range contextAndViewBindingMap[key] {
if navBinding.Description == binding.Description {
continue outer
}
}
contextAndViewBindingMap[key] = append(contextAndViewBindingMap[key], binding)
}
continue outer
}
contexts := []string{}
if len(binding.Contexts) == 0 {
contexts = append(contexts, "")
} else {
contexts = append(contexts, binding.Contexts...)
}
for _, context := range contexts {
key := contextAndViewType{subtitle: context, title: binding.ViewName}
existing := contextAndViewBindingMap[key]
if existing == nil {
contextAndViewBindingMap[key] = []*gui.Binding{binding}
} else {
contextAndViewBindingMap[key] = append(contextAndViewBindingMap[key], binding)
}
}
}
type groupedBindingsType struct {
contextAndView contextAndViewType
bindings []*gui.Binding
}
groupedBindings := make([]groupedBindingsType, len(contextAndViewBindingMap))
for contextAndView, contextBindings := range contextAndViewBindingMap {
groupedBindings = append(groupedBindings, groupedBindingsType{contextAndView: contextAndView, bindings: contextBindings})
}
sort.Slice(groupedBindings, func(i, j int) bool {
first := groupedBindings[i].contextAndView
second := groupedBindings[j].contextAndView
if first.title == "" {
return true
}
if second.title == "" {
return false
}
if first.title == "navigation" {
return true
}
if second.title == "navigation" {
return false
}
return first.title < second.title || (first.title == second.title && first.subtitle < second.subtitle)
func getBindingSections(bindings []*types.Binding, tr *i18n.TranslationSet) []*bindingSection {
bindingsToDisplay := slices.Filter(bindings, func(binding *types.Binding) bool {
return binding.Description != "" || binding.Alternative != ""
})
for _, group := range groupedBindings {
contextAndView := group.contextAndView
contextBindings := group.bindings
mApp.Log.Info("viewname: " + contextAndView.title + ", context: " + contextAndView.subtitle)
viewName := contextAndView.title
if viewName == "" {
viewName = "global"
}
translatedView := localisedTitle(mApp, viewName)
var title string
if contextAndView.subtitle == "" {
addendum := " " + mApp.Tr.Panel
if viewName == "global" || viewName == "navigation" {
addendum = ""
bindingsByHeader := utils.MuiltiGroupBy(bindingsToDisplay, func(binding *types.Binding) []header {
return getHeaders(binding, tr)
})
bindingGroups := maps.MapToSlice(
bindingsByHeader,
func(header header, hBindings []*types.Binding) headerWithBindings {
uniqBindings := lo.UniqBy(hBindings, func(binding *types.Binding) string {
return binding.Description + keybindings.GetKeyDisplay(binding.Key)
})
return headerWithBindings{
header: header,
bindings: uniqBindings,
}
title = fmt.Sprintf("%s%s", translatedView, addendum)
} else {
translatedContextName := localisedTitle(mApp, contextAndView.subtitle)
title = fmt.Sprintf("%s %s (%s)", translatedView, mApp.Tr.Panel, translatedContextName)
}
},
)
for _, binding := range contextBindings {
bindingSections = addBinding(title, bindingSections, binding)
slices.SortFunc(bindingGroups, func(a, b headerWithBindings) bool {
if a.header.priority != b.header.priority {
return a.header.priority > b.header.priority
}
}
return a.header.title < b.header.title
})
return bindingSections
return slices.Map(bindingGroups, func(hb headerWithBindings) *bindingSection {
return &bindingSection{
title: hb.header.title,
bindings: hb.bindings,
}
})
}
func addBinding(title string, bindingSections []*bindingSection, binding *gui.Binding) []*bindingSection {
if binding.Description == "" && binding.Alternative == "" {
return bindingSections
// a binding may belong to multiple headers if it is applicable to multiple contexts,
// for example the copy-to-clipboard binding.
func getHeaders(binding *types.Binding, tr *i18n.TranslationSet) []header {
if binding.Tag == "navigation" {
return []header{{priority: 2, title: localisedTitle(tr, "navigation")}}
}
for _, section := range bindingSections {
if title == section.title {
section.bindings = append(section.bindings, binding)
return bindingSections
}
if binding.ViewName == "" {
return []header{{priority: 3, title: localisedTitle(tr, "global")}}
}
section := &bindingSection{
title: title,
bindings: []*gui.Binding{binding},
if len(binding.Contexts) == 0 {
return []header{}
}
return append(bindingSections, section)
return slices.Map(binding.Contexts, func(context string) header {
return header{priority: 1, title: localisedTitle(tr, context)}
})
}
func formatSections(tr *i18n.TranslationSet, bindingSections []*bindingSection) string {
@@ -261,3 +198,19 @@ func formatSections(tr *i18n.TranslationSet, bindingSections []*bindingSection)
return content
}
func formatTitle(title string) string {
return fmt.Sprintf("\n## %s\n\n", title)
}
func formatBinding(binding *types.Binding) string {
if binding.Alternative != "" {
return fmt.Sprintf(
" <kbd>%s</kbd>: %s (%s)\n",
keybindings.GetKeyDisplay(binding.Key),
binding.Description,
binding.Alternative,
)
}
return fmt.Sprintf(" <kbd>%s</kbd>: %s\n", keybindings.GetKeyDisplay(binding.Key), binding.Description)
}
+267
View File
@@ -0,0 +1,267 @@
package cheatsheet
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/stretchr/testify/assert"
)
func TestGetBindingSections(t *testing.T) {
tr := i18n.EnglishTranslationSet()
tests := []struct {
testName string
bindings []*types.Binding
expected []*bindingSection
}{
{
testName: "no bindings",
bindings: []*types.Binding{},
expected: []*bindingSection{},
},
{
testName: "one binding",
bindings: []*types.Binding{
{
ViewName: "files",
Contexts: []string{"files"},
Description: "stage file",
},
},
expected: []*bindingSection{
{
title: "Files",
bindings: []*types.Binding{
{
ViewName: "files",
Contexts: []string{"files"},
Description: "stage file",
},
},
},
},
},
{
testName: "global binding",
bindings: []*types.Binding{
{
ViewName: "",
Description: "quit",
},
},
expected: []*bindingSection{
{
title: "Global Keybindings",
bindings: []*types.Binding{
{
ViewName: "",
Description: "quit",
},
},
},
},
},
{
testName: "grouped bindings",
bindings: []*types.Binding{
{
ViewName: "files",
Description: "stage file",
Contexts: []string{"files"},
},
{
ViewName: "files",
Description: "unstage file",
Contexts: []string{"files"},
},
{
ViewName: "files",
Description: "drop submodule",
Contexts: []string{"submodules"},
},
},
expected: []*bindingSection{
{
title: "Files",
bindings: []*types.Binding{
{
ViewName: "files",
Description: "stage file",
Contexts: []string{"files"},
},
{
ViewName: "files",
Description: "unstage file",
Contexts: []string{"files"},
},
},
},
{
title: "Submodules",
bindings: []*types.Binding{
{
ViewName: "files",
Description: "drop submodule",
Contexts: []string{"submodules"},
},
},
},
},
},
{
testName: "with navigation bindings",
bindings: []*types.Binding{
{
ViewName: "files",
Contexts: []string{"files"},
Description: "stage file",
},
{
ViewName: "files",
Contexts: []string{"files"},
Description: "unstage file",
},
{
ViewName: "files",
Contexts: []string{"files"},
Description: "scroll",
Tag: "navigation",
},
{
ViewName: "commits",
Contexts: []string{"commits"},
Description: "revert commit",
},
},
expected: []*bindingSection{
{
title: "List Panel Navigation",
bindings: []*types.Binding{
{
ViewName: "files",
Contexts: []string{"files"},
Description: "scroll",
Tag: "navigation",
},
},
},
{
title: "Commits",
bindings: []*types.Binding{
{
ViewName: "commits",
Contexts: []string{"commits"},
Description: "revert commit",
},
},
},
{
title: "Files",
bindings: []*types.Binding{
{
ViewName: "files",
Contexts: []string{"files"},
Description: "stage file",
},
{
ViewName: "files",
Contexts: []string{"files"},
Description: "unstage file",
},
},
},
},
},
{
testName: "with duplicate navigation bindings",
bindings: []*types.Binding{
{
ViewName: "files",
Contexts: []string{"files"},
Description: "stage file",
},
{
ViewName: "files",
Contexts: []string{"files"},
Description: "unstage file",
},
{
ViewName: "files",
Contexts: []string{"files"},
Description: "scroll",
Tag: "navigation",
},
{
ViewName: "commits",
Contexts: []string{"commits"},
Description: "revert commit",
},
{
ViewName: "commits",
Contexts: []string{"commits"},
Description: "scroll",
Tag: "navigation",
},
{
ViewName: "commits",
Contexts: []string{"commits"},
Description: "page up",
Tag: "navigation",
},
},
expected: []*bindingSection{
{
title: "List Panel Navigation",
bindings: []*types.Binding{
{
ViewName: "files",
Contexts: []string{"files"},
Description: "scroll",
Tag: "navigation",
},
{
ViewName: "commits",
Contexts: []string{"commits"},
Description: "page up",
Tag: "navigation",
},
},
},
{
title: "Commits",
bindings: []*types.Binding{
{
ViewName: "commits",
Contexts: []string{"commits"},
Description: "revert commit",
},
},
},
{
title: "Files",
bindings: []*types.Binding{
{
ViewName: "files",
Contexts: []string{"files"},
Description: "stage file",
},
{
ViewName: "files",
Contexts: []string{"files"},
Description: "unstage file",
},
},
},
},
},
}
for _, test := range tests {
t.Run(test.testName, func(t *testing.T) {
actual := getBindingSections(test.bindings, &tr)
assert.EqualValues(t, test.expected, actual)
})
}
}
+9 -6
View File
@@ -5,6 +5,7 @@ import (
"os"
"path/filepath"
"strings"
"sync"
"github.com/go-errors/errors"
@@ -56,12 +57,13 @@ func NewGitCommand(
cmn *common.Common,
osCommand *oscommands.OSCommand,
gitConfig git_config.IGitConfig,
syncMutex *sync.Mutex,
) (*GitCommand, error) {
if err := navigateToRepoRootDirectory(os.Stat, os.Chdir); err != nil {
return nil, err
}
repo, err := setupRepository(gogit.PlainOpen, cmn.Tr.GitconfigParseErr)
repo, err := setupRepository(gogit.PlainOpenWithOptions, gogit.PlainOpenOptions{DetectDotGit: false, EnableDotGitCommonDir: true}, cmn.Tr.GitconfigParseErr)
if err != nil {
return nil, err
}
@@ -77,6 +79,7 @@ func NewGitCommand(
gitConfig,
dotGitDir,
repo,
syncMutex,
), nil
}
@@ -86,6 +89,7 @@ func NewGitCommandAux(
gitConfig git_config.IGitConfig,
dotGitDir string,
repo *gogit.Repository,
syncMutex *sync.Mutex,
) *GitCommand {
cmd := NewGitCmdObjBuilder(cmn.Log, osCommand.Cmd)
@@ -95,7 +99,7 @@ func NewGitCommandAux(
// on the one struct.
// common ones are: cmn, osCommand, dotGitDir, configCommands
configCommands := git_commands.NewConfigCommands(cmn, gitConfig, repo)
gitCommon := git_commands.NewGitCommon(cmn, cmd, osCommand, dotGitDir, repo, configCommands)
gitCommon := git_commands.NewGitCommon(cmn, cmd, osCommand, dotGitDir, repo, configCommands, syncMutex)
statusCommands := git_commands.NewStatusCommands(gitCommon)
fileLoader := loaders.NewFileLoader(cmn, cmd, configCommands)
@@ -203,7 +207,7 @@ func resolvePath(path string) (string, error) {
return filepath.EvalSymlinks(path)
}
func setupRepository(openGitRepository func(string) (*gogit.Repository, error), gitConfigParseErrorStr string) (*gogit.Repository, error) {
func setupRepository(openGitRepository func(string, *gogit.PlainOpenOptions) (*gogit.Repository, error), options gogit.PlainOpenOptions, gitConfigParseErrorStr string) (*gogit.Repository, error) {
unresolvedPath := env.GetGitDirEnv()
if unresolvedPath == "" {
var err error
@@ -218,8 +222,7 @@ func setupRepository(openGitRepository func(string) (*gogit.Repository, error),
return nil, err
}
repository, err := openGitRepository(path)
repository, err := openGitRepository(path, &options)
if err != nil {
if strings.Contains(err.Error(), `unquoted '\' must be followed by new line`) {
return nil, errors.New(gitConfigParseErrorStr)
@@ -251,7 +254,7 @@ func findDotGitDir(stat func(string) (os.FileInfo, error), readFile func(filenam
}
fileContent := string(fileBytes)
if !strings.HasPrefix(fileContent, "gitdir: ") {
return "", errors.New(".git is a file which suggests we are in a submodule but the file's contents do not contain a gitdir pointing to the actual .git directory")
return "", errors.New(".git is a file which suggests we are in a submodule or a worktree but the file's contents do not contain a gitdir pointing to the actual .git directory")
}
return strings.TrimSpace(strings.TrimPrefix(fileContent, "gitdir: ")), nil
}
+6 -8
View File
@@ -1,6 +1,10 @@
package git_commands
import "github.com/sirupsen/logrus"
import (
"github.com/jesseduffield/generics/maps"
"github.com/jesseduffield/generics/slices"
"github.com/sirupsen/logrus"
)
// although the typical terms in a git bisect are 'bad' and 'good', they're more
// generally known as 'new' and 'old'. Semi-recently git allowed the user to define
@@ -93,11 +97,5 @@ func (self *BisectInfo) Bisecting() bool {
return false
}
for _, status := range self.statusMap {
if status == BisectStatusOld {
return true
}
}
return false
return slices.Contains(maps.Values(self.statusMap), BisectStatusOld)
}
+5 -1
View File
@@ -109,6 +109,10 @@ func (self *BranchCommands) SetUpstream(remoteName string, remoteBranchName stri
return self.cmd.New(fmt.Sprintf("git branch --set-upstream-to=%s/%s %s", self.cmd.Quote(remoteName), self.cmd.Quote(remoteBranchName), self.cmd.Quote(branchName))).Run()
}
func (self *BranchCommands) UnsetUpstream(branchName string) error {
return self.cmd.New(fmt.Sprintf("git branch --unset-upstream %s", self.cmd.Quote(branchName))).Run()
}
func (self *BranchCommands) GetCurrentBranchUpstreamDifferenceCount() (string, string) {
return self.GetCommitDifferences("HEAD", "HEAD@{u}")
}
@@ -142,7 +146,7 @@ func (self *BranchCommands) Rename(oldName string, newName string) error {
}
func (self *BranchCommands) GetRawBranches() (string, error) {
return self.cmd.New(`git for-each-ref --sort=-committerdate --format="%(HEAD)|%(refname:short)|%(upstream:short)|%(upstream:track)" refs/heads`).DontLog().RunWithOutput()
return self.cmd.New(`git for-each-ref --sort=-committerdate --format="%(HEAD)%00%(refname:short)%00%(upstream:short)%00%(upstream:track)" refs/heads`).DontLog().RunWithOutput()
}
type MergeOpts struct {
+40 -1
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
)
@@ -22,6 +23,17 @@ func (self *CommitCommands) RewordLastCommit(message string) error {
return self.cmd.New("git commit --allow-empty --amend --only -m " + self.cmd.Quote(message)).Run()
}
// ResetAuthor resets the author of the topmost commit
func (self *CommitCommands) ResetAuthor() error {
return self.cmd.New("git commit --allow-empty --only --no-edit --amend --reset-author").Run()
}
// Sets the commit's author to the supplied value. Value is expected to be of the form 'Name <Email>'
func (self *CommitCommands) SetAuthor(value string) error {
commandStr := fmt.Sprintf("git commit --allow-empty --only --no-edit --amend --author=%s", self.cmd.Quote(value))
return self.cmd.New(commandStr).Run()
}
// ResetToCommit reset to commit
func (self *CommitCommands) ResetToCommit(sha string, strength string, envVars []string) error {
return self.cmd.New(fmt.Sprintf("git reset --%s %s", strength, sha)).
@@ -70,10 +82,37 @@ func (self *CommitCommands) GetHeadCommitMessage() (string, error) {
func (self *CommitCommands) GetCommitMessage(commitSha string) (string, error) {
cmdStr := "git rev-list --format=%B --max-count=1 " + commitSha
messageWithHeader, err := self.cmd.New(cmdStr).DontLog().RunWithOutput()
message := strings.Join(strings.SplitAfter(messageWithHeader, "\n")[1:], "\n")
message := strings.Join(strings.SplitAfter(messageWithHeader, "\n")[1:], "")
return strings.TrimSpace(message), err
}
func (self *CommitCommands) GetCommitDiff(commitSha string) (string, error) {
cmdStr := "git show --no-color " + commitSha
diff, err := self.cmd.New(cmdStr).DontLog().RunWithOutput()
return diff, err
}
type Author struct {
Name string
Email string
}
func (self *CommitCommands) GetCommitAuthor(commitSha string) (Author, error) {
cmdStr := "git show --no-patch --pretty=format:'%an%x00%ae' " + commitSha
output, err := self.cmd.New(cmdStr).DontLog().RunWithOutput()
if err != nil {
return Author{}, err
}
split := strings.SplitN(strings.TrimSpace(output), "\x00", 2)
if len(split) < 2 {
return Author{}, errors.New("unexpected git output")
}
author := Author{Name: split[0], Email: split[1]}
return author, err
}
func (self *CommitCommands) GetCommitMessageFirstLine(sha string) (string, error) {
return self.GetCommitMessagesFirstLine([]string{sha})
}
+46
View File
@@ -161,3 +161,49 @@ func TestCommitShowCmdObj(t *testing.T) {
})
}
}
func TestGetCommitMsg(t *testing.T) {
type scenario struct {
testName string
input string
expectedOutput string
}
scenarios := []scenario{
{
"empty",
` commit deadbeef`,
``,
},
{
"no line breaks (single line)",
`commit deadbeef
use generics to DRY up context code`,
`use generics to DRY up context code`,
},
{
"with line breaks",
`commit deadbeef
Merge pull request #1750 from mark2185/fix-issue-template
'git-rev parse' should be 'git rev-parse'`,
`Merge pull request #1750 from mark2185/fix-issue-template
'git-rev parse' should be 'git rev-parse'`,
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildCommitCommands(commonDeps{
runner: oscommands.NewFakeRunner(t).Expect("git rev-list --format=%B --max-count=1 deadbeef", s.input, nil),
})
output, err := instance.GetCommitMessage("deadbeef")
assert.NoError(t, err)
assert.Equal(t, s.expectedOutput, output)
})
}
}
+6
View File
@@ -1,6 +1,8 @@
package git_commands
import (
"sync"
gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
@@ -13,6 +15,8 @@ type GitCommon struct {
dotGitDir string
repo *gogit.Repository
config *ConfigCommands
// mutex for doing things like push/pull/fetch
syncMutex *sync.Mutex
}
func NewGitCommon(
@@ -22,6 +26,7 @@ func NewGitCommon(
dotGitDir string,
repo *gogit.Repository,
config *ConfigCommands,
syncMutex *sync.Mutex,
) *GitCommon {
return &GitCommon{
Common: cmn,
@@ -30,5 +35,6 @@ func NewGitCommon(
dotGitDir: dotGitDir,
repo: repo,
config: config,
syncMutex: syncMutex,
}
}
+7 -6
View File
@@ -5,7 +5,6 @@ import (
"strconv"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -59,14 +58,16 @@ func (self *FileCommands) GetEditCmdStr(filename string, lineNumber int) (string
}
editCmdTemplate := self.UserConfig.OS.EditCommandTemplate
if editCmdTemplate == config.DefaultEditCommandTemplate {
if len(editCmdTemplate) == 0 {
switch editor {
case "emacs", "nano", "vi", "vim":
editCmdTemplate = "{{editor}} +{{line}} {{filename}}"
case "emacs", "nano", "vi", "vim", "nvim":
editCmdTemplate = "{{editor}} +{{line}} -- {{filename}}"
case "subl":
editCmdTemplate = "{{editor}} {{filename}}:{{line}}"
editCmdTemplate = "{{editor}} -- {{filename}}:{{line}}"
case "code":
editCmdTemplate = "{{editor}} -r --goto {{filename}}:{{line}}"
editCmdTemplate = "{{editor}} -r --goto -- {{filename}}:{{line}}"
default:
editCmdTemplate = "{{editor}} -- {{filename}}"
}
}
return utils.ResolvePlaceholderString(editCmdTemplate, templateValues), nil
+20 -6
View File
@@ -47,7 +47,7 @@ func TestEditFileCmdStr(t *testing.T) {
gitConfigMockResponses: nil,
test: func(cmdStr string, err error) {
assert.NoError(t, err)
assert.Equal(t, `nano +1 "test"`, cmdStr)
assert.Equal(t, `nano "test"`, cmdStr)
},
},
{
@@ -61,7 +61,7 @@ func TestEditFileCmdStr(t *testing.T) {
gitConfigMockResponses: map[string]string{"core.editor": "nano"},
test: func(cmdStr string, err error) {
assert.NoError(t, err)
assert.Equal(t, `nano +1 "test"`, cmdStr)
assert.Equal(t, `nano "test"`, cmdStr)
},
},
{
@@ -79,7 +79,7 @@ func TestEditFileCmdStr(t *testing.T) {
gitConfigMockResponses: nil,
test: func(cmdStr string, err error) {
assert.NoError(t, err)
assert.Equal(t, `nano +1 "test"`, cmdStr)
assert.Equal(t, `nano "test"`, cmdStr)
},
},
{
@@ -97,7 +97,7 @@ func TestEditFileCmdStr(t *testing.T) {
gitConfigMockResponses: nil,
test: func(cmdStr string, err error) {
assert.NoError(t, err)
assert.Equal(t, `emacs +1 "test"`, cmdStr)
assert.Equal(t, `emacs "test"`, cmdStr)
},
},
{
@@ -112,7 +112,7 @@ func TestEditFileCmdStr(t *testing.T) {
gitConfigMockResponses: nil,
test: func(cmdStr string, err error) {
assert.NoError(t, err)
assert.Equal(t, `vi +1 "test"`, cmdStr)
assert.Equal(t, `vi "test"`, cmdStr)
},
},
{
@@ -127,7 +127,7 @@ func TestEditFileCmdStr(t *testing.T) {
gitConfigMockResponses: nil,
test: func(cmdStr string, err error) {
assert.NoError(t, err)
assert.Equal(t, `vi +1 "file/with space"`, cmdStr)
assert.Equal(t, `vi "file/with space"`, cmdStr)
},
},
{
@@ -144,6 +144,20 @@ func TestEditFileCmdStr(t *testing.T) {
assert.Equal(t, `vim +1 "open file/at line"`, cmdStr)
},
},
{
filename: "default edit command template",
configEditCommand: "vim",
configEditCommandTemplate: "",
runner: oscommands.NewFakeRunner(t),
getenv: func(env string) string {
return ""
},
gitConfigMockResponses: nil,
test: func(cmdStr string, err error) {
assert.NoError(t, err)
assert.Equal(t, `vim +1 -- "default edit command template"`, cmdStr)
},
},
}
for _, s := range scenarios {
+9 -9
View File
@@ -105,16 +105,16 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
}
baseIndex := sourceCommitIdx + 1
todo := ""
for i, commit := range commits[0:baseIndex] {
a := "pick"
if i == sourceCommitIdx || i == destinationCommitIdx {
a = "edit"
}
todo = a + " " + commit.Sha + " " + commit.Name + "\n" + todo
}
err := self.rebase.PrepareInteractiveRebaseCommand(commits[baseIndex].Sha, todo, true).Run()
todoLines := self.rebase.BuildTodoLines(commits[0:baseIndex], func(commit *models.Commit, i int) string {
if i == sourceCommitIdx || i == destinationCommitIdx {
return "edit"
} else {
return "pick"
}
})
err := self.rebase.PrepareInteractiveRebaseCommand(commits[baseIndex].Sha, todoLines, true).Run()
if err != nil {
return err
}
+90 -32
View File
@@ -7,6 +7,8 @@ import (
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/app/daemon"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
)
@@ -52,7 +54,7 @@ func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int, me
}
func (self *RebaseCommands) RewordCommitInEditor(commits []*models.Commit, index int) (oscommands.ICmdObj, error) {
todo, sha, err := self.GenerateGenericRebaseTodo(commits, index, "reword")
todo, sha, err := self.BuildSingleActionTodo(commits, index, "reword")
if err != nil {
return nil, err
}
@@ -60,6 +62,38 @@ func (self *RebaseCommands) RewordCommitInEditor(commits []*models.Commit, index
return self.PrepareInteractiveRebaseCommand(sha, todo, false), nil
}
func (self *RebaseCommands) ResetCommitAuthor(commits []*models.Commit, index int) error {
return self.GenericAmend(commits, index, func() error {
return self.commit.ResetAuthor()
})
}
func (self *RebaseCommands) SetCommitAuthor(commits []*models.Commit, index int, value string) error {
return self.GenericAmend(commits, index, func() error {
return self.commit.SetAuthor(value)
})
}
func (self *RebaseCommands) GenericAmend(commits []*models.Commit, index int, f func() error) error {
if index == 0 {
// we've selected the top commit so no rebase is required
return f()
}
err := self.BeginInteractiveRebaseForCommit(commits, index)
if err != nil {
return err
}
// now the selected commit should be our head so we'll amend it
err = f()
if err != nil {
return err
}
return self.ContinueRebase()
}
func (self *RebaseCommands) MoveCommitDown(commits []*models.Commit, index int) error {
// we must ensure that we have at least two commits after the selected one
if len(commits) <= index+2 {
@@ -67,17 +101,15 @@ func (self *RebaseCommands) MoveCommitDown(commits []*models.Commit, index int)
return errors.New(self.Tr.NoRoom)
}
todo := ""
orderedCommits := append(commits[0:index], commits[index+1], commits[index])
for _, commit := range orderedCommits {
todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo
}
return self.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todo, true).Run()
todoLines := self.BuildTodoLinesSingleAction(orderedCommits, "pick")
return self.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todoLines, true).Run()
}
func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, index int, action string) error {
todo, sha, err := self.GenerateGenericRebaseTodo(commits, index, action)
todo, sha, err := self.BuildSingleActionTodo(commits, index, action)
if err != nil {
return err
}
@@ -88,7 +120,8 @@ func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, index in
// PrepareInteractiveRebaseCommand returns the cmd for an interactive rebase
// we tell git to run lazygit to edit the todo list, and we pass the client
// lazygit a todo string to write to the todo file
func (self *RebaseCommands) PrepareInteractiveRebaseCommand(baseSha string, todo string, overrideEditor bool) oscommands.ICmdObj {
func (self *RebaseCommands) PrepareInteractiveRebaseCommand(baseSha string, todoLines []TodoLine, overrideEditor bool) oscommands.ICmdObj {
todo := self.buildTodo(todoLines)
ex := oscommands.GetLazygitPath()
debug := "FALSE"
@@ -109,8 +142,8 @@ func (self *RebaseCommands) PrepareInteractiveRebaseCommand(baseSha string, todo
}
cmdObj.AddEnvVars(
"LAZYGIT_CLIENT_COMMAND=INTERACTIVE_REBASE",
"LAZYGIT_REBASE_TODO="+todo,
daemon.DaemonKindEnvKey+"="+string(daemon.InteractiveRebase),
daemon.RebaseTODOEnvKey+"="+todo,
"DEBUG="+debug,
"LANG=en_US.UTF-8", // Force using EN as language
"LC_ALL=en_US.UTF-8", // Force using EN as language
@@ -124,38 +157,37 @@ func (self *RebaseCommands) PrepareInteractiveRebaseCommand(baseSha string, todo
return cmdObj
}
func (self *RebaseCommands) GenerateGenericRebaseTodo(commits []*models.Commit, actionIndex int, action string) (string, string, error) {
// produces TodoLines where every commit is picked (or dropped for merge commits) except for the commit at the given index, which
// will have the given action applied to it.
func (self *RebaseCommands) BuildSingleActionTodo(commits []*models.Commit, actionIndex int, action string) ([]TodoLine, string, error) {
baseIndex := actionIndex + 1
if len(commits) <= baseIndex {
return "", "", errors.New(self.Tr.CannotRebaseOntoFirstCommit)
return nil, "", errors.New(self.Tr.CannotRebaseOntoFirstCommit)
}
if action == "squash" || action == "fixup" {
baseIndex++
if len(commits) <= baseIndex {
return "", "", errors.New(self.Tr.CannotSquashOntoSecondCommit)
return nil, "", errors.New(self.Tr.CannotSquashOntoSecondCommit)
}
}
todo := ""
for i, commit := range commits[0:baseIndex] {
var commitAction string
todoLines := self.BuildTodoLines(commits[0:baseIndex], func(commit *models.Commit, i int) string {
if i == actionIndex {
commitAction = action
return action
} 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.
commitAction = "drop"
return "drop"
} else {
commitAction = "pick"
return "pick"
}
todo = commitAction + " " + commit.Sha + " " + commit.Name + "\n" + todo
}
})
return todo, commits[baseIndex].Sha, nil
return todoLines, commits[baseIndex].Sha, nil
}
// AmendTo amends the given commit with whatever files are staged
@@ -185,7 +217,7 @@ func (self *RebaseCommands) EditRebaseTodo(index int, action string) error {
content[contentIndex] = action + " " + strings.Join(splitLine[1:], " ")
result := strings.Join(content, "\n")
return ioutil.WriteFile(fileName, []byte(result), 0644)
return ioutil.WriteFile(fileName, []byte(result), 0o644)
}
func (self *RebaseCommands) getTodoCommitCount(content []string) int {
@@ -215,7 +247,7 @@ func (self *RebaseCommands) MoveTodoDown(index int) error {
rearrangedContent = append(rearrangedContent, content[contentIndex+1:]...)
result := strings.Join(rearrangedContent, "\n")
return ioutil.WriteFile(fileName, []byte(result), 0644)
return ioutil.WriteFile(fileName, []byte(result), 0o644)
}
// SquashAllAboveFixupCommits squashes all fixup! commits above the given one
@@ -244,7 +276,7 @@ func (self *RebaseCommands) BeginInteractiveRebaseForCommit(commits []*models.Co
return errors.New(self.Tr.DisabledForGPG)
}
todo, sha, err := self.GenerateGenericRebaseTodo(commits, commitIndex, "edit")
todo, sha, err := self.BuildSingleActionTodo(commits, commitIndex, "edit")
if err != nil {
return err
}
@@ -254,7 +286,7 @@ func (self *RebaseCommands) BeginInteractiveRebaseForCommit(commits []*models.Co
// RebaseBranch interactive rebases onto a branch
func (self *RebaseCommands) RebaseBranch(branchName string) error {
return self.PrepareInteractiveRebaseCommand(branchName, "", false).Run()
return self.PrepareInteractiveRebaseCommand(branchName, nil, false).Run()
}
func (self *RebaseCommands) GenericMergeOrRebaseActionCmdObj(commandType string, command string) oscommands.ICmdObj {
@@ -298,7 +330,7 @@ func (self *RebaseCommands) runSkipEditorCommand(cmdObj oscommands.ICmdObj) erro
lazyGitPath := oscommands.GetLazygitPath()
return cmdObj.
AddEnvVars(
"LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY",
daemon.DaemonKindEnvKey+"="+string(daemon.ExitImmediately),
"GIT_EDITOR="+lazyGitPath,
"EDITOR="+lazyGitPath,
"VISUAL="+lazyGitPath,
@@ -336,10 +368,36 @@ func (self *RebaseCommands) DiscardOldFileChanges(commits []*models.Commit, comm
// CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD
func (self *RebaseCommands) CherryPickCommits(commits []*models.Commit) error {
todo := ""
for _, commit := range commits {
todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo
}
todoLines := self.BuildTodoLinesSingleAction(commits, "pick")
return self.PrepareInteractiveRebaseCommand("HEAD", todo, false).Run()
return self.PrepareInteractiveRebaseCommand("HEAD", todoLines, false).Run()
}
func (self *RebaseCommands) buildTodo(todoLines []TodoLine) string {
lines := slices.Map(todoLines, func(todoLine TodoLine) string {
return todoLine.ToString()
})
return strings.Join(slices.Reverse(lines), "")
}
func (self *RebaseCommands) BuildTodoLines(commits []*models.Commit, f func(*models.Commit, int) string) []TodoLine {
return slices.MapWithIndex(commits, func(commit *models.Commit, i int) TodoLine {
return TodoLine{Action: f(commit, i), Commit: commit}
})
}
func (self *RebaseCommands) BuildTodoLinesSingleAction(commits []*models.Commit, action string) []TodoLine {
return self.BuildTodoLines(commits, func(commit *models.Commit, i int) string {
return action
})
}
type TodoLine struct {
Action string
Commit *models.Commit
}
func (self *TodoLine) ToString() string {
return self.Action + " " + self.Commit.Sha + " " + self.Commit.Name + "\n"
}
+4 -3
View File
@@ -5,10 +5,11 @@ import (
"testing"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/app/daemon"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
)
@@ -61,10 +62,10 @@ func TestRebaseSkipEditorCommand(t *testing.T) {
`^VISUAL=.*$`,
`^EDITOR=.*$`,
`^GIT_EDITOR=.*$`,
"^LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY$",
"^" + daemon.DaemonKindEnvKey + "=" + string(daemon.ExitImmediately) + "$",
} {
regexStr := regexStr
foundMatch := utils.IncludesStringFunc(envVars, func(envVar string) bool {
foundMatch := lo.ContainsBy(envVars, func(envVar string) bool {
return regexp.MustCompile(regexStr).MatchString(envVar)
})
if !foundMatch {
+1 -1
View File
@@ -40,7 +40,7 @@ func (self *RemoteCommands) UpdateRemoteUrl(remoteName string, updatedUrl string
func (self *RemoteCommands) DeleteRemoteBranch(remoteName string, branchName string) error {
command := fmt.Sprintf("git push %s --delete %s", self.cmd.Quote(remoteName), self.cmd.Quote(branchName))
return self.cmd.New(command).PromptOnCredentialRequest().Run()
return self.cmd.New(command).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
}
// CheckRemoteBranchExists Returns remote branch
+21 -1
View File
@@ -25,6 +25,10 @@ func NewStashCommands(
}
}
func (self *StashCommands) DropNewest() error {
return self.cmd.New("git stash drop").Run()
}
func (self *StashCommands) Drop(index int) error {
return self.cmd.New(fmt.Sprintf("git stash drop stash@{%d}", index)).Run()
}
@@ -38,7 +42,6 @@ func (self *StashCommands) Apply(index int) error {
}
// Save save stash
// TODO: before calling this, check if there is anything to save
func (self *StashCommands) Save(message string) error {
return self.cmd.New("git stash save " + self.cmd.Quote(message)).Run()
}
@@ -49,6 +52,23 @@ func (self *StashCommands) ShowStashEntryCmdObj(index int) oscommands.ICmdObj {
return self.cmd.New(cmdStr).DontLog()
}
func (self *StashCommands) StashAndKeepIndex(message string) error {
return self.cmd.New(fmt.Sprintf("git stash save %s --keep-index", self.cmd.Quote(message))).Run()
}
func (self *StashCommands) StashUnstagedChanges(message string) error {
if err := self.cmd.New("git commit --no-verify -m \"[lazygit] stashing unstaged changes\"").Run(); err != nil {
return err
}
if err := self.Save(message); err != nil {
return err
}
if err := self.cmd.New("git reset --soft HEAD^").Run(); err != nil {
return err
}
return nil
}
// SaveStagedChanges stashes only the currently staged changes. This takes a few steps
// shoutouts to Joe on https://stackoverflow.com/questions/14759748/stashing-only-staged-changes-in-git-is-it-possible
func (self *StashCommands) SaveStagedChanges(message string) error {
+5 -5
View File
@@ -47,7 +47,7 @@ func (self *SyncCommands) PushCmdObj(opts PushOpts) (oscommands.ICmdObj, error)
cmdStr += " " + self.cmd.Quote(opts.UpstreamBranch)
}
cmdObj := self.cmd.New(cmdStr).PromptOnCredentialRequest()
cmdObj := self.cmd.New(cmdStr).PromptOnCredentialRequest().WithMutex(self.syncMutex)
return cmdObj, nil
}
@@ -83,7 +83,7 @@ func (self *SyncCommands) Fetch(opts FetchOptions) error {
} else {
cmdObj.PromptOnCredentialRequest()
}
return cmdObj.Run()
return cmdObj.WithMutex(self.syncMutex).Run()
}
type PullOptions struct {
@@ -108,15 +108,15 @@ func (self *SyncCommands) Pull(opts PullOptions) error {
// setting GIT_SEQUENCE_EDITOR to ':' as a way of skipping it, in case the user
// has 'pull.rebase = interactive' configured.
return self.cmd.New(cmdStr).AddEnvVars("GIT_SEQUENCE_EDITOR=:").PromptOnCredentialRequest().Run()
return self.cmd.New(cmdStr).AddEnvVars("GIT_SEQUENCE_EDITOR=:").PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
}
func (self *SyncCommands) FastForward(branchName string, remoteName string, remoteBranchName string) error {
cmdStr := fmt.Sprintf("git fetch %s %s:%s", self.cmd.Quote(remoteName), self.cmd.Quote(remoteBranchName), self.cmd.Quote(branchName))
return self.cmd.New(cmdStr).PromptOnCredentialRequest().Run()
return self.cmd.New(cmdStr).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
}
func (self *SyncCommands) FetchRemote(remoteName string) error {
cmdStr := fmt.Sprintf("git fetch %s", self.cmd.Quote(remoteName))
return self.cmd.New(cmdStr).PromptOnCredentialRequest().Run()
return self.cmd.New(cmdStr).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
}
+1 -1
View File
@@ -27,5 +27,5 @@ func (self *TagCommands) Delete(tagName string) error {
}
func (self *TagCommands) Push(remoteName string, tagName string) error {
return self.cmd.New(fmt.Sprintf("git push %s %s", self.cmd.Quote(remoteName), self.cmd.Quote(tagName))).PromptOnCredentialRequest().Run()
return self.cmd.New(fmt.Sprintf("git push %s %s", self.cmd.Quote(remoteName), self.cmd.Quote(tagName))).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
}
+29 -8
View File
@@ -8,6 +8,7 @@ import (
"time"
"github.com/go-errors/errors"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
@@ -46,10 +47,9 @@ func (self *WorkingTreeCommands) StageFile(path string) error {
}
func (self *WorkingTreeCommands) StageFiles(paths []string) error {
quotedPaths := make([]string, len(paths))
for i, path := range paths {
quotedPaths[i] = self.cmd.Quote(path)
}
quotedPaths := slices.Map(paths, func(path string) string {
return self.cmd.Quote(path)
})
return self.cmd.New(fmt.Sprintf("git add -- %s", strings.Join(quotedPaths, " "))).Run()
}
@@ -218,6 +218,11 @@ func (self *WorkingTreeCommands) Ignore(filename string) error {
return self.os.AppendLineToFile(".gitignore", filename)
}
// Exclude adds a file to the .git/info/exclude for the repo
func (self *WorkingTreeCommands) Exclude(filename string) error {
return self.os.AppendLineToFile(".git/info/exclude", filename)
}
// WorktreeFileDiff returns the diff of a file
func (self *WorkingTreeCommands) WorktreeFileDiff(file *models.File, plain bool, cached bool, ignoreWhitespace bool) string {
// for now we assume an error means the file was deleted
@@ -230,6 +235,7 @@ func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain
trackedArg := "--"
colorArg := self.UserConfig.Git.Paging.ColorArg
quotedPath := self.cmd.Quote(node.GetPath())
quotedPrevPath := ""
ignoreWhitespaceArg := ""
contextSize := self.UserConfig.Git.DiffContextSize
if cached {
@@ -244,19 +250,25 @@ func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain
if ignoreWhitespace {
ignoreWhitespaceArg = " --ignore-all-space"
}
if prevPath := node.GetPreviousPath(); prevPath != "" {
quotedPrevPath = " " + self.cmd.Quote(prevPath)
}
cmdStr := fmt.Sprintf("git diff --submodule --no-ext-diff --unified=%d --color=%s%s%s %s %s", contextSize, colorArg, ignoreWhitespaceArg, cachedArg, trackedArg, quotedPath)
cmdStr := fmt.Sprintf("git diff --submodule --no-ext-diff --unified=%d --color=%s%s%s %s %s%s", contextSize, colorArg, ignoreWhitespaceArg, cachedArg, trackedArg, quotedPath, quotedPrevPath)
return self.cmd.New(cmdStr).DontLog()
}
func (self *WorkingTreeCommands) ApplyPatch(patch string, flags ...string) error {
filepath := filepath.Join(oscommands.GetTempDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch")
self.Log.Infof("saving temporary patch to %s", filepath)
if err := self.os.CreateFileWithContent(filepath, patch); err != nil {
filepath, err := self.SaveTemporaryPatch(patch)
if err != nil {
return err
}
return self.ApplyPatchFile(filepath, flags...)
}
func (self *WorkingTreeCommands) ApplyPatchFile(filepath string, flags ...string) error {
flagStr := ""
for _, flag := range flags {
flagStr += " --" + flag
@@ -265,6 +277,15 @@ func (self *WorkingTreeCommands) ApplyPatch(patch string, flags ...string) error
return self.cmd.New(fmt.Sprintf("git apply%s %s", flagStr, self.cmd.Quote(filepath))).Run()
}
func (self *WorkingTreeCommands) SaveTemporaryPatch(patch string) (string, error) {
filepath := filepath.Join(self.os.GetTempDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch")
self.Log.Infof("saving temporary patch to %s", filepath)
if err := self.os.CreateFileWithContent(filepath, patch); err != nil {
return "", err
}
return filepath, nil
}
// ShowFileDiff get the diff of specified from and to. Typically this will be used for a single commit so it'll be 123abc^..123abc
// but when we're in diff mode it could be any 'from' to any 'to'. The reverse flag is also here thanks to diff mode.
func (self *WorkingTreeCommands) ShowFileDiff(from string, to string, reverse bool, fileName string, plain bool) (string, error) {
+17 -7
View File
@@ -3,6 +3,7 @@ package commands
import (
"fmt"
"os"
"sync"
"testing"
"time"
@@ -76,7 +77,7 @@ func TestNavigateToRepoRootDirectory(t *testing.T) {
},
},
{
"An error occurred when getting path informations",
"An error occurred when getting path information",
func(string) (os.FileInfo, error) {
return nil, fmt.Errorf("An error occurred")
},
@@ -115,18 +116,20 @@ func TestNavigateToRepoRootDirectory(t *testing.T) {
func TestSetupRepository(t *testing.T) {
type scenario struct {
testName string
openGitRepository func(string) (*gogit.Repository, error)
openGitRepository func(string, *gogit.PlainOpenOptions) (*gogit.Repository, error)
errorStr string
options gogit.PlainOpenOptions
test func(*gogit.Repository, error)
}
scenarios := []scenario{
{
"A gitconfig parsing error occurred",
func(string) (*gogit.Repository, error) {
func(string, *gogit.PlainOpenOptions) (*gogit.Repository, error) {
return nil, fmt.Errorf(`unquoted '\' must be followed by new line`)
},
"error translated",
gogit.PlainOpenOptions{},
func(r *gogit.Repository, err error) {
assert.Error(t, err)
assert.EqualError(t, err, "error translated")
@@ -134,10 +137,11 @@ func TestSetupRepository(t *testing.T) {
},
{
"A gogit error occurred",
func(string) (*gogit.Repository, error) {
func(string, *gogit.PlainOpenOptions) (*gogit.Repository, error) {
return nil, fmt.Errorf("Error from inside gogit")
},
"",
gogit.PlainOpenOptions{},
func(r *gogit.Repository, err error) {
assert.Error(t, err)
assert.EqualError(t, err, "Error from inside gogit")
@@ -145,13 +149,14 @@ func TestSetupRepository(t *testing.T) {
},
{
"Setup done properly",
func(string) (*gogit.Repository, error) {
func(string, *gogit.PlainOpenOptions) (*gogit.Repository, error) {
assert.NoError(t, os.RemoveAll("/tmp/lazygit-test"))
r, err := gogit.PlainInit("/tmp/lazygit-test", false)
assert.NoError(t, err)
return r, nil
},
"",
gogit.PlainOpenOptions{},
func(r *gogit.Repository, err error) {
assert.NoError(t, err)
assert.NotNil(t, r)
@@ -162,7 +167,7 @@ func TestSetupRepository(t *testing.T) {
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
s.test(setupRepository(s.openGitRepository, s.errorStr))
s.test(setupRepository(s.openGitRepository, s.options, s.errorStr))
})
}
}
@@ -211,7 +216,12 @@ func TestNewGitCommand(t *testing.T) {
s := s
t.Run(s.testName, func(t *testing.T) {
s.setup()
s.test(NewGitCommand(utils.NewDummyCommon(), oscommands.NewDummyOSCommand(), git_config.NewFakeGitConfig(nil)))
s.test(
NewGitCommand(utils.NewDummyCommon(),
oscommands.NewDummyOSCommand(),
git_config.NewFakeGitConfig(nil),
&sync.Mutex{},
))
})
}
}
@@ -49,11 +49,24 @@ var azdoServiceDef = ServiceDefinition{
repoURLTemplate: "https://{{.webDomain}}/{{.org}}/{{.project}}/_git/{{.repo}}",
}
var bitbucketServerServiceDef = ServiceDefinition{
provider: "bitbucketServer",
pullRequestURLIntoDefaultBranch: "/pull-requests?create&sourceBranch={{.From}}",
pullRequestURLIntoTargetBranch: "/pull-requests?create&targetBranch={{.To}}&sourceBranch={{.From}}",
commitURL: "/commits/{{.CommitSha}}",
regexStrings: []string{
`^ssh://git@.*/(?P<project>.*)/(?P<repo>.*?)(?:\.git)?$`,
`^https://.*/scm/(?P<project>.*)/(?P<repo>.*?)(?:\.git)?$`,
},
repoURLTemplate: "https://{{.webDomain}}/projects/{{.project}}/repos/{{.repo}}",
}
var serviceDefinitions = []ServiceDefinition{
githubServiceDef,
bitbucketServiceDef,
gitLabServiceDef,
azdoServiceDef,
bitbucketServerServiceDef,
}
var defaultServiceDomains = []ServiceDomain{
@@ -9,6 +9,8 @@ import (
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
"github.com/jesseduffield/generics/slices"
)
// This package is for handling logic specific to a git hosting service like github, gitlab, bitbucket, etc.
@@ -94,8 +96,7 @@ func (self *HostingServiceMgr) getCandidateServiceDomains() []ServiceDomain {
serviceDefinitionByProvider[serviceDefinition.provider] = serviceDefinition
}
var serviceDomains = make([]ServiceDomain, len(defaultServiceDomains))
copy(serviceDomains, defaultServiceDomains)
serviceDomains := slices.Clone(defaultServiceDomains)
if len(self.configServiceDomains) > 0 {
for gitDomain, typeAndDomain := range self.configServiceDomains {
@@ -110,10 +111,10 @@ func (self *HostingServiceMgr) getCandidateServiceDomains() []ServiceDomain {
serviceDefinition, ok := serviceDefinitionByProvider[provider]
if !ok {
providerNames := []string{}
for _, serviceDefinition := range serviceDefinitions {
providerNames = append(providerNames, serviceDefinition.provider)
}
providerNames := slices.Map(serviceDefinitions, func(serviceDefinition ServiceDefinition) string {
return serviceDefinition.provider
})
self.log.Errorf("Unknown git service type: '%s'. Expected one of %s", provider, strings.Join(providerNames, ", "))
continue
}
@@ -125,7 +125,7 @@ func TestGetPullRequestURL(t *testing.T) {
},
},
{
testName: "Opens a link to new pull request on Azure DevOps (SSH) with specifc target",
testName: "Opens a link to new pull request on Azure DevOps (SSH) with specific target",
from: "feature/new",
to: "dev",
remoteUrl: "git@ssh.dev.azure.com:v3/myorg/myproject/myrepo",
@@ -144,7 +144,7 @@ func TestGetPullRequestURL(t *testing.T) {
},
},
{
testName: "Opens a link to new pull request on Azure DevOps (HTTP) with specifc target",
testName: "Opens a link to new pull request on Azure DevOps (HTTP) with specific target",
from: "feature/new",
to: "dev",
remoteUrl: "https://myorg@dev.azure.com/myorg/myproject/_git/myrepo",
@@ -153,6 +153,60 @@ func TestGetPullRequestURL(t *testing.T) {
assert.Equal(t, "https://dev.azure.com/myorg/myproject/_git/myrepo/pullrequestcreate?sourceRef=feature%2Fnew&targetRef=dev", url)
},
},
{
testName: "Opens a link to new pull request on Bitbucket Server (SSH)",
from: "feature/new",
remoteUrl: "ssh://git@mycompany.bitbucket.com/myproject/myrepo.git",
configServiceDomains: map[string]string{
// valid configuration for a bitbucket server URL
"mycompany.bitbucket.com": "bitbucketServer:mycompany.bitbucket.com",
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://mycompany.bitbucket.com/projects/myproject/repos/myrepo/pull-requests?create&sourceBranch=feature%2Fnew", url)
},
},
{
testName: "Opens a link to new pull request on Bitbucket Server (SSH) with specific target",
from: "feature/new",
to: "dev",
remoteUrl: "ssh://git@mycompany.bitbucket.com/myproject/myrepo.git",
configServiceDomains: map[string]string{
// valid configuration for a bitbucket server URL
"mycompany.bitbucket.com": "bitbucketServer:mycompany.bitbucket.com",
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://mycompany.bitbucket.com/projects/myproject/repos/myrepo/pull-requests?create&targetBranch=dev&sourceBranch=feature%2Fnew", url)
},
},
{
testName: "Opens a link to new pull request on Bitbucket Server (HTTP)",
from: "feature/new",
remoteUrl: "https://mycompany.bitbucket.com/scm/myproject/myrepo.git",
configServiceDomains: map[string]string{
// valid configuration for a bitbucket server URL
"mycompany.bitbucket.com": "bitbucketServer:mycompany.bitbucket.com",
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://mycompany.bitbucket.com/projects/myproject/repos/myrepo/pull-requests?create&sourceBranch=feature%2Fnew", url)
},
},
{
testName: "Opens a link to new pull request on Bitbucket Server (HTTP) with specific target",
from: "feature/new",
to: "dev",
remoteUrl: "https://mycompany.bitbucket.com/scm/myproject/myrepo.git",
configServiceDomains: map[string]string{
// valid configuration for a bitbucket server URL
"mycompany.bitbucket.com": "bitbucketServer:mycompany.bitbucket.com",
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://mycompany.bitbucket.com/projects/myproject/repos/myrepo/pull-requests?create&targetBranch=dev&sourceBranch=feature%2Fnew", url)
},
},
{
testName: "Throws an error if git service is unsupported",
from: "feature/divide-operation",
@@ -199,7 +253,7 @@ func TestGetPullRequestURL(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature%2Fprofile-page&t=1", url)
},
expectedLoggedErrors: []string{"Unknown git service type: 'noservice'. Expected one of github, bitbucket, gitlab, azuredevops"},
expectedLoggedErrors: []string{"Unknown git service type: 'noservice'. Expected one of github, bitbucket, gitlab, azuredevops, bitbucketServer"},
},
{
testName: "Escapes reserved URL characters in from branch name",
+52 -40
View File
@@ -4,6 +4,8 @@ import (
"regexp"
"strings"
"github.com/jesseduffield/generics/set"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/go-git/v5/config"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/common"
@@ -64,21 +66,20 @@ outer:
if strings.EqualFold(reflogBranch.Name, branch.Name) {
branch.Recency = reflogBranch.Recency
branchesWithRecency = append(branchesWithRecency, branch)
branches = append(branches[0:j], branches[j+1:]...)
branches = slices.Remove(branches, j)
continue outer
}
}
}
branches = append(branchesWithRecency, branches...)
branches = slices.Prepend(branches, branchesWithRecency...)
foundHead := false
for i, branch := range branches {
if branch.Head {
foundHead = true
branch.Recency = " *"
branches = append(branches[0:i], branches[i+1:]...)
branches = append([]*models.Branch{branch}, branches...)
branches = slices.Move(branches, i, 0)
break
}
}
@@ -87,7 +88,7 @@ outer:
if err != nil {
return nil, err
}
branches = append([]*models.Branch{{Name: currentBranchName, DisplayName: currentBranchDisplayName, Head: true, Recency: " *"}}, branches...)
branches = slices.Prepend(branches, &models.Branch{Name: currentBranchName, DisplayName: currentBranchDisplayName, Head: true, Recency: " *"})
}
configBranches, err := self.config.Branches()
@@ -114,38 +115,47 @@ func (self *BranchLoader) obtainBranches() []*models.Branch {
trimmedOutput := strings.TrimSpace(output)
outputLines := strings.Split(trimmedOutput, "\n")
branches := make([]*models.Branch, 0, len(outputLines))
for _, line := range outputLines {
return slices.FilterMap(outputLines, func(line string) (*models.Branch, bool) {
if line == "" {
continue
return nil, false
}
split := strings.Split(line, SEPARATION_CHAR)
split := strings.Split(line, "\x00")
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
return nil, false
}
name := strings.TrimPrefix(split[1], "heads/")
branch := &models.Branch{
Name: name,
Pullables: "?",
Pushables: "?",
Head: split[0] == "*",
}
return obtainBranch(split), true
})
}
upstreamName := split[2]
if upstreamName == "" {
// if we're here then it means we do not have a local version of the remote.
// The branch might still be tracking a remote though, we just don't know
// how many commits ahead/behind it is
branches = append(branches, branch)
continue
}
// Obtain branch information from parsed line output of getRawBranches()
// split contains the '|' separated tokens in the line of output
func obtainBranch(split []string) *models.Branch {
name := strings.TrimPrefix(split[1], "heads/")
branch := &models.Branch{
Name: name,
Pullables: "?",
Pushables: "?",
Head: split[0] == "*",
}
track := split[3]
upstreamName := split[2]
if upstreamName == "" {
// if we're here then it means we do not have a local version of the remote.
// The branch might still be tracking a remote though, we just don't know
// how many commits ahead/behind it is
return branch
}
track := split[3]
if track == "[gone]" {
branch.UpstreamGone = true
} else {
re := regexp.MustCompile(`ahead (\d+)`)
match := re.FindStringSubmatch(track)
if len(match) > 1 {
@@ -161,30 +171,32 @@ func (self *BranchLoader) obtainBranches() []*models.Branch {
} else {
branch.Pullables = "0"
}
branches = append(branches, branch)
}
return branches
return branch
}
// TODO: only look at the new reflog commits, and otherwise store the recencies in
// int form against the branch to recalculate the time ago
func (self *BranchLoader) obtainReflogBranches(reflogCommits []*models.Commit) []*models.Branch {
foundBranchesMap := map[string]bool{}
foundBranches := set.New[string]()
re := regexp.MustCompile(`checkout: moving from ([\S]+) to ([\S]+)`)
reflogBranches := make([]*models.Branch, 0, len(reflogCommits))
for _, commit := range reflogCommits {
if match := re.FindStringSubmatch(commit.Name); len(match) == 3 {
recency := utils.UnixToTimeAgo(commit.UnixTimestamp)
for _, branchName := range match[1:] {
if !foundBranchesMap[branchName] {
foundBranchesMap[branchName] = true
reflogBranches = append(reflogBranches, &models.Branch{
Recency: recency,
Name: branchName,
})
}
match := re.FindStringSubmatch(commit.Name)
if len(match) != 3 {
continue
}
recency := utils.UnixToTimeAgo(commit.UnixTimestamp)
for _, branchName := range match[1:] {
if !foundBranches.Includes(branchName) {
foundBranches.Add(branchName)
reflogBranches = append(reflogBranches, &models.Branch{
Recency: recency,
Name: branchName,
})
}
}
}
+52
View File
@@ -0,0 +1,52 @@
package loaders
// "*|feat/detect-purge|origin/feat/detect-purge|[ahead 1]"
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/stretchr/testify/assert"
)
func TestObtainBanch(t *testing.T) {
type scenario struct {
testName string
input []string
expectedBranch *models.Branch
}
scenarios := []scenario{
{
testName: "TrimHeads",
input: []string{"", "heads/a_branch", "", ""},
expectedBranch: &models.Branch{Name: "a_branch", Pushables: "?", Pullables: "?", Head: false},
},
{
testName: "NoUpstream",
input: []string{"", "a_branch", "", ""},
expectedBranch: &models.Branch{Name: "a_branch", Pushables: "?", Pullables: "?", Head: false},
},
{
testName: "IsHead",
input: []string{"*", "a_branch", "", ""},
expectedBranch: &models.Branch{Name: "a_branch", Pushables: "?", Pullables: "?", Head: true},
},
{
testName: "IsBehindAndAhead",
input: []string{"", "a_branch", "a_remote/a_branch", "[behind 2, ahead 3]"},
expectedBranch: &models.Branch{Name: "a_branch", Pushables: "3", Pullables: "2", Head: false},
},
{
testName: "RemoteBranchIsGone",
input: []string{"", "a_branch", "a_remote/a_branch", "[gone]"},
expectedBranch: &models.Branch{Name: "a_branch", UpstreamGone: true, Pushables: "?", Pullables: "?", Head: false},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
branch := obtainBranch(s.input)
assert.EqualValues(t, s.expectedBranch, branch)
})
}
}
+15 -16
View File
@@ -4,9 +4,11 @@ import (
"fmt"
"strings"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/samber/lo"
)
type CommitFileLoader struct {
@@ -33,25 +35,22 @@ func (self *CommitFileLoader) GetFilesInDiff(from string, to string, reverse boo
return nil, err
}
return self.getCommitFilesFromFilenames(filenames), nil
return getCommitFilesFromFilenames(filenames), nil
}
// filenames string is something like "file1\nfile2\nfile3"
func (self *CommitFileLoader) getCommitFilesFromFilenames(filenames string) []*models.CommitFile {
commitFiles := make([]*models.CommitFile, 0)
// filenames string is something like "MM\x00file1\x00MU\x00file2\x00AA\x00file3\x00"
// so we need to split it by the null character and then map each status-name pair to a commit file
func getCommitFilesFromFilenames(filenames string) []*models.CommitFile {
lines := strings.Split(strings.TrimRight(filenames, "\x00"), "\x00")
n := len(lines)
for i := 0; i < n-1; i += 2 {
// typical result looks like 'A my_file' meaning my_file was added
changeStatus := lines[i]
name := lines[i+1]
commitFiles = append(commitFiles, &models.CommitFile{
Name: name,
ChangeStatus: changeStatus,
})
if len(lines) == 1 {
return []*models.CommitFile{}
}
return commitFiles
// typical result looks like 'A my_file' meaning my_file was added
return slices.Map(lo.Chunk(lines, 2), func(chunk []string) *models.CommitFile {
return &models.CommitFile{
ChangeStatus: chunk[0],
Name: chunk[1],
}
})
}
+71
View File
@@ -0,0 +1,71 @@
package loaders
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/stretchr/testify/assert"
)
func TestGetCommitFilesFromFilenames(t *testing.T) {
tests := []struct {
testName string
input string
output []*models.CommitFile
}{
{
testName: "no files",
input: "",
output: []*models.CommitFile{},
},
{
testName: "one file",
input: "MM\x00Myfile\x00",
output: []*models.CommitFile{
{
Name: "Myfile",
ChangeStatus: "MM",
},
},
},
{
testName: "two files",
input: "MM\x00Myfile\x00M \x00MyOtherFile\x00",
output: []*models.CommitFile{
{
Name: "Myfile",
ChangeStatus: "MM",
},
{
Name: "MyOtherFile",
ChangeStatus: "M ",
},
},
},
{
testName: "three files",
input: "MM\x00Myfile\x00M \x00MyOtherFile\x00 M\x00YetAnother\x00",
output: []*models.CommitFile{
{
Name: "Myfile",
ChangeStatus: "MM",
},
{
Name: "MyOtherFile",
ChangeStatus: "M ",
},
{
Name: "YetAnother",
ChangeStatus: " M",
},
},
},
}
for _, test := range tests {
t.Run(test.testName, func(t *testing.T) {
result := getCommitFilesFromFilenames(test.input)
assert.Equal(t, test.output, result)
})
}
}
+57 -52
View File
@@ -1,6 +1,7 @@
package loaders
import (
"bytes"
"fmt"
"io/ioutil"
"os"
@@ -9,6 +10,8 @@ import (
"strconv"
"strings"
"github.com/fsmiamoto/git-todo-parser/todo"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
@@ -22,8 +25,6 @@ import (
// be processed as part of a rebase (these won't appear in git log but we
// grab them from the rebase-related files in the .git directory to show them
const SEPARATION_CHAR = "|"
// CommitLoader returns a list of Commit objects for the current repo
type CommitLoader struct {
*common.Common
@@ -90,14 +91,12 @@ func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit,
}
err = self.getLogCmd(opts).RunAndProcessLines(func(line string) (bool, error) {
if canExtractCommit(line) {
commit := self.extractCommitFromLine(line)
if commit.Sha == firstPushedCommit {
passedFirstPushedCommit = true
}
commit.Status = map[bool]string{true: "unpushed", false: "pushed"}[!passedFirstPushedCommit]
commits = append(commits, commit)
commit := self.extractCommitFromLine(line)
if commit.Sha == firstPushedCommit {
passedFirstPushedCommit = true
}
commit.Status = map[bool]string{true: "unpushed", false: "pushed"}[!passedFirstPushedCommit]
commits = append(commits, commit)
return false, nil
})
if err != nil {
@@ -158,15 +157,16 @@ func (self *CommitLoader) MergeRebasingCommits(commits []*models.Commit) ([]*mod
// example input:
// 8ad01fe32fcc20f07bc6693f87aa4977c327f1e1|10 hours ago|Jesse Duffield| (HEAD -> master, tag: v0.15.2)|refresh commits when adding a tag
func (self *CommitLoader) extractCommitFromLine(line string) *models.Commit {
split := strings.Split(line, SEPARATION_CHAR)
split := strings.SplitN(line, "\x00", 7)
sha := split[0]
unixTimestamp := split[1]
author := split[2]
extraInfo := strings.TrimSpace(split[3])
parentHashes := split[4]
authorName := split[2]
authorEmail := split[3]
extraInfo := strings.TrimSpace(split[4])
parentHashes := split[5]
message := split[6]
message := strings.Join(split[5:], SEPARATION_CHAR)
tags := []string{}
if extraInfo != "" {
@@ -179,14 +179,20 @@ func (self *CommitLoader) extractCommitFromLine(line string) *models.Commit {
unitTimestampInt, _ := strconv.Atoi(unixTimestamp)
parents := []string{}
if len(parentHashes) > 0 {
parents = strings.Split(parentHashes, " ")
}
return &models.Commit{
Sha: sha,
Name: message,
Tags: tags,
ExtraInfo: extraInfo,
UnixTimestamp: int64(unitTimestampInt),
Author: author,
Parents: strings.Split(parentHashes, " "),
AuthorName: authorName,
AuthorEmail: authorEmail,
Parents: parents,
}
}
@@ -200,16 +206,15 @@ func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode
return nil, nil
}
commitShas := make([]string, len(commits))
for i, commit := range commits {
commitShas[i] = commit.Sha
}
commitShas := slices.Map(commits, func(commit *models.Commit) string {
return commit.Sha
})
// note that we're not filtering these as we do non-rebasing commits just because
// I suspect that will cause some damage
cmdObj := self.cmd.New(
fmt.Sprintf(
"git show %s --no-patch --oneline %s --abbrev=%d",
"git -c log.showSignature=false show %s --no-patch --oneline %s --abbrev=%d",
strings.Join(commitShas, " "),
prettyFormat,
20,
@@ -219,14 +224,12 @@ func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode
hydratedCommits := make([]*models.Commit, 0, len(commits))
i := 0
err = cmdObj.RunAndProcessLines(func(line string) (bool, error) {
if canExtractCommit(line) {
commit := self.extractCommitFromLine(line)
matchingCommit := commits[i]
commit.Action = matchingCommit.Action
commit.Status = matchingCommit.Status
hydratedCommits = append(hydratedCommits, commit)
i++
}
commit := self.extractCommitFromLine(line)
matchingCommit := commits[i]
commit.Action = matchingCommit.Action
commit.Status = matchingCommit.Status
hydratedCommits = append(hydratedCommits, commit)
i++
return false, nil
})
if err != nil {
@@ -306,21 +309,24 @@ func (self *CommitLoader) getInteractiveRebasingCommits() ([]*models.Commit, err
}
commits := []*models.Commit{}
lines := strings.Split(string(bytesContent), "\n")
for _, line := range lines {
if line == "" || line == "noop" {
return commits, nil
}
if strings.HasPrefix(line, "#") {
todos, err := todo.Parse(bytes.NewBuffer(bytesContent))
if err != nil {
self.Log.Error(fmt.Sprintf("error occurred while parsing git-rebase-todo file: %s", err.Error()))
return nil, nil
}
for _, t := range todos {
if t.Commit == "" {
// Command does not have a commit associated, skip
continue
}
splitLine := strings.Split(line, " ")
commits = append([]*models.Commit{{
Sha: splitLine[1],
Name: strings.Join(splitLine[2:], " "),
commits = slices.Prepend(commits, &models.Commit{
Sha: t.Commit,
Name: t.Msg,
Status: "rebasing",
Action: splitLine[0],
}}, commits...)
Action: t.Command.String(),
})
}
return commits, nil
@@ -429,27 +435,26 @@ func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj {
return self.cmd.New(
fmt.Sprintf(
"git log %s %s %s --oneline %s%s --abbrev=%d%s",
"git -c log.showSignature=false log %s %s %s --oneline %s%s --abbrev=%d%s",
self.cmd.Quote(opts.RefName),
orderFlag,
allFlag,
prettyFormat,
limitFlag,
20,
40,
filterFlag,
),
).DontLog()
}
var prettyFormat = fmt.Sprintf(
"--pretty=format:\"%%H%s%%at%s%%aN%s%%d%s%%p%s%%s\"",
SEPARATION_CHAR,
SEPARATION_CHAR,
SEPARATION_CHAR,
SEPARATION_CHAR,
SEPARATION_CHAR,
"--pretty=format:\"%%H%s%%at%s%%aN%s%%ae%s%%d%s%%p%s%%s\"",
NULL_CODE,
NULL_CODE,
NULL_CODE,
NULL_CODE,
NULL_CODE,
NULL_CODE,
)
func canExtractCommit(line string) bool {
return line != "" && strings.Split(line, " ")[0] != "gpg:"
}
const NULL_CODE = "%x00"
+27 -36
View File
@@ -2,6 +2,7 @@ package loaders
import (
"path/filepath"
"strings"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
@@ -11,32 +12,14 @@ import (
"github.com/stretchr/testify/assert"
)
func NewDummyCommitLoader() *CommitLoader {
cmn := utils.NewDummyCommon()
return &CommitLoader{
Common: cmn,
cmd: nil,
getCurrentBranchName: func() (string, string, error) { return "master", "master", nil },
getRebaseMode: func() (enums.RebaseMode, error) { return enums.REBASE_MODE_NONE, nil },
dotGitDir: ".git",
readFile: func(filename string) ([]byte, error) {
return []byte(""), nil
},
walkFiles: func(root string, fn filepath.WalkFunc) error {
return nil
},
}
}
const commitsOutput = `0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield| (HEAD -> better-tests)|b21997d6b4cbdf84b149|better typing for rebase mode
b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164|1640824515|Jesse Duffield| (origin/better-tests)|e94e8fc5b6fab4cb755f|fix logging
e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c|1640823749|Jesse Duffield||d8084cd558925eb7c9c3|refactor
d8084cd558925eb7c9c38afeed5725c21653ab90|1640821426|Jesse Duffield||65f910ebd85283b5cce9|WIP
65f910ebd85283b5cce9bf67d03d3f1a9ea3813a|1640821275|Jesse Duffield||26c07b1ab33860a1a759|WIP
26c07b1ab33860a1a7591a0638f9925ccf497ffa|1640750752|Jesse Duffield||3d4470a6c072208722e5|WIP
3d4470a6c072208722e5ae9a54bcb9634959a1c5|1640748818|Jesse Duffield||053a66a7be3da43aacdc|WIP
053a66a7be3da43aacdc7aa78e1fe757b82c4dd2|1640739815|Jesse Duffield||985fe482e806b172aea4|refactoring the config struct`
var commitsOutput = strings.Replace(`0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com| (HEAD -> better-tests)|b21997d6b4cbdf84b149|better typing for rebase mode
b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164|1640824515|Jesse Duffield|jessedduffield@gmail.com| (origin/better-tests)|e94e8fc5b6fab4cb755f|fix logging
e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c|1640823749|Jesse Duffield|jessedduffield@gmail.com||d8084cd558925eb7c9c3|refactor
d8084cd558925eb7c9c38afeed5725c21653ab90|1640821426|Jesse Duffield|jessedduffield@gmail.com||65f910ebd85283b5cce9|WIP
65f910ebd85283b5cce9bf67d03d3f1a9ea3813a|1640821275|Jesse Duffield|jessedduffield@gmail.com||26c07b1ab33860a1a759|WIP
26c07b1ab33860a1a7591a0638f9925ccf497ffa|1640750752|Jesse Duffield|jessedduffield@gmail.com||3d4470a6c072208722e5|WIP
3d4470a6c072208722e5ae9a54bcb9634959a1c5|1640748818|Jesse Duffield|jessedduffield@gmail.com||053a66a7be3da43aacdc|WIP
053a66a7be3da43aacdc7aa78e1fe757b82c4dd2|1640739815|Jesse Duffield|jessedduffield@gmail.com||985fe482e806b172aea4|refactoring the config struct`, "|", "\x00", -1)
func TestGetCommits(t *testing.T) {
type scenario struct {
@@ -57,7 +40,7 @@ func TestGetCommits(t *testing.T) {
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
runner: oscommands.NewFakeRunner(t).
Expect(`git merge-base "HEAD" "HEAD"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
Expect(`git log "HEAD" --topo-order --oneline --pretty=format:"%H|%at|%aN|%d|%p|%s" --abbrev=20`, "", nil),
Expect(`git -c log.showSignature=false log "HEAD" --topo-order --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40`, "", nil),
expectedCommits: []*models.Commit{},
expectedError: nil,
@@ -71,7 +54,7 @@ func TestGetCommits(t *testing.T) {
// here it's seeing which commits are yet to be pushed
Expect(`git merge-base "HEAD" "HEAD"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
// here it's actually getting all the commits in a formatted form, one per line
Expect(`git log "HEAD" --topo-order --oneline --pretty=format:"%H|%at|%aN|%d|%p|%s" --abbrev=20`, commitsOutput, nil).
Expect(`git -c log.showSignature=false log "HEAD" --topo-order --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40`, commitsOutput, nil).
// here it's seeing where our branch diverged from the master branch so that we can mark that commit and parent commits as 'merged'
Expect(`git merge-base "HEAD" "master"`, "26c07b1ab33860a1a7591a0638f9925ccf497ffa", nil),
@@ -83,7 +66,8 @@ func TestGetCommits(t *testing.T) {
Action: "",
Tags: []string{},
ExtraInfo: "(HEAD -> better-tests)",
Author: "Jesse Duffield",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640826609,
Parents: []string{
"b21997d6b4cbdf84b149",
@@ -96,7 +80,8 @@ func TestGetCommits(t *testing.T) {
Action: "",
Tags: []string{},
ExtraInfo: "(origin/better-tests)",
Author: "Jesse Duffield",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640824515,
Parents: []string{
"e94e8fc5b6fab4cb755f",
@@ -109,7 +94,8 @@ func TestGetCommits(t *testing.T) {
Action: "",
Tags: []string{},
ExtraInfo: "",
Author: "Jesse Duffield",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640823749,
Parents: []string{
"d8084cd558925eb7c9c3",
@@ -122,7 +108,8 @@ func TestGetCommits(t *testing.T) {
Action: "",
Tags: []string{},
ExtraInfo: "",
Author: "Jesse Duffield",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640821426,
Parents: []string{
"65f910ebd85283b5cce9",
@@ -135,7 +122,8 @@ func TestGetCommits(t *testing.T) {
Action: "",
Tags: []string{},
ExtraInfo: "",
Author: "Jesse Duffield",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640821275,
Parents: []string{
"26c07b1ab33860a1a759",
@@ -148,7 +136,8 @@ func TestGetCommits(t *testing.T) {
Action: "",
Tags: []string{},
ExtraInfo: "",
Author: "Jesse Duffield",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640750752,
Parents: []string{
"3d4470a6c072208722e5",
@@ -161,7 +150,8 @@ func TestGetCommits(t *testing.T) {
Action: "",
Tags: []string{},
ExtraInfo: "",
Author: "Jesse Duffield",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640748818,
Parents: []string{
"053a66a7be3da43aacdc",
@@ -174,7 +164,8 @@ func TestGetCommits(t *testing.T) {
Action: "",
Tags: []string{},
ExtraInfo: "",
Author: "Jesse Duffield",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640739815,
Parents: []string{
"985fe482e806b172aea4",
+6 -6
View File
@@ -7,7 +7,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
type FileLoaderConfig interface {
@@ -57,10 +57,10 @@ func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File
change := status.Change
stagedChange := change[0:1]
unstagedChange := change[1:2]
untracked := utils.IncludesString([]string{"??", "A ", "AM"}, change)
hasNoStagedChanges := utils.IncludesString([]string{" ", "U", "?"}, stagedChange)
hasInlineMergeConflicts := utils.IncludesString([]string{"UU", "AA"}, change)
hasMergeConflicts := hasInlineMergeConflicts || utils.IncludesString([]string{"DD", "AU", "UA", "UD", "DU"}, change)
untracked := lo.Contains([]string{"??", "A ", "AM"}, change)
hasNoStagedChanges := lo.Contains([]string{" ", "U", "?"}, stagedChange)
hasInlineMergeConflicts := lo.Contains([]string{"UU", "AA"}, change)
hasMergeConflicts := hasInlineMergeConflicts || lo.Contains([]string{"DD", "AU", "UA", "UD", "DU"}, change)
file := &models.File{
Name: status.Name,
@@ -125,7 +125,7 @@ func (c *FileLoader) GitStatus(opts GitStatusOptions) ([]FileStatus, error) {
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.PreviousName = splitLines[i+1]
status.StatusString = fmt.Sprintf("%s %s -> %s", status.Change, status.PreviousName, status.Name)
i++
}
+11 -4
View File
@@ -32,25 +32,32 @@ func (self *ReflogCommitLoader) GetReflogCommits(lastReflogCommit *models.Commit
filterPathArg = fmt.Sprintf(" --follow -- %s", self.cmd.Quote(filterPath))
}
cmdObj := self.cmd.New(fmt.Sprintf(`git log -g --abbrev=20 --format="%%h %%ct %%gs"%s`, filterPathArg)).DontLog()
cmdObj := self.cmd.New(fmt.Sprintf(`git -c log.showSignature=false log -g --abbrev=40 --format="%s"%s`, "%h%x00%ct%x00%gs%x00%p", filterPathArg)).DontLog()
onlyObtainedNewReflogCommits := false
err := cmdObj.RunAndProcessLines(func(line string) (bool, error) {
fields := strings.SplitN(line, " ", 3)
if len(fields) <= 2 {
fields := strings.SplitN(line, "\x00", 4)
if len(fields) <= 3 {
return false, nil
}
unixTimestamp, _ := strconv.Atoi(fields[1])
parentHashes := fields[3]
parents := []string{}
if len(parentHashes) > 0 {
parents = strings.Split(parentHashes, " ")
}
commit := &models.Commit{
Sha: fields[0],
Name: fields[2],
UnixTimestamp: int64(unixTimestamp),
Status: "reflog",
Parents: parents,
}
// note that the unix timestamp here is the timestamp of the COMMIT, not the reflog entry itself,
// so two consequetive reflog entries may have both the same SHA and therefore same timestamp.
// so two consecutive reflog entries may have both the same SHA and therefore same timestamp.
// We use the reflog message to disambiguate, and fingers crossed that we never see the same of those
// twice in a row. Reason being that it would mean we'd be erroneously exiting early.
if lastReflogCommit != nil && commit.Sha == lastReflogCommit.Sha && commit.UnixTimestamp == lastReflogCommit.UnixTimestamp && commit.Name == lastReflogCommit.Name {
+21 -11
View File
@@ -2,6 +2,7 @@ package loaders
import (
"errors"
"strings"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
@@ -11,12 +12,12 @@ import (
"github.com/stretchr/testify/assert"
)
const reflogOutput = `c3c4b66b64c97ffeecde 1643150483 checkout: moving from A to B
c3c4b66b64c97ffeecde 1643150483 checkout: moving from B to A
c3c4b66b64c97ffeecde 1643150483 checkout: moving from A to B
c3c4b66b64c97ffeecde 1643150483 checkout: moving from master to A
f4ddf2f0d4be4ccc7efa 1643149435 checkout: moving from A to master
`
var reflogOutput = strings.Replace(`c3c4b66b64c97ffeecde|1643150483|checkout: moving from A to B|51baa8c1
c3c4b66b64c97ffeecde|1643150483|checkout: moving from B to A|51baa8c1
c3c4b66b64c97ffeecde|1643150483|checkout: moving from A to B|51baa8c1
c3c4b66b64c97ffeecde|1643150483|checkout: moving from master to A|51baa8c1
f4ddf2f0d4be4ccc7efa|1643149435|checkout: moving from A to master|51baa8c1
`, "|", "\x00", -1)
func TestGetReflogCommits(t *testing.T) {
type scenario struct {
@@ -33,7 +34,7 @@ func TestGetReflogCommits(t *testing.T) {
{
testName: "no reflog entries",
runner: oscommands.NewFakeRunner(t).
Expect(`git log -g --abbrev=20 --format="%h %ct %gs"`, "", nil),
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p"`, "", nil),
lastReflogCommit: nil,
expectedCommits: []*models.Commit{},
@@ -43,7 +44,7 @@ func TestGetReflogCommits(t *testing.T) {
{
testName: "some reflog entries",
runner: oscommands.NewFakeRunner(t).
Expect(`git log -g --abbrev=20 --format="%h %ct %gs"`, reflogOutput, nil),
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p"`, reflogOutput, nil),
lastReflogCommit: nil,
expectedCommits: []*models.Commit{
@@ -52,30 +53,35 @@ func TestGetReflogCommits(t *testing.T) {
Name: "checkout: moving from A to B",
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from B to A",
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from master to A",
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
{
Sha: "f4ddf2f0d4be4ccc7efa",
Name: "checkout: moving from A to master",
Status: "reflog",
UnixTimestamp: 1643149435,
Parents: []string{"51baa8c1"},
},
},
expectedOnlyObtainedNew: false,
@@ -84,13 +90,14 @@ func TestGetReflogCommits(t *testing.T) {
{
testName: "some reflog entries where last commit is given",
runner: oscommands.NewFakeRunner(t).
Expect(`git log -g --abbrev=20 --format="%h %ct %gs"`, reflogOutput, nil),
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p"`, reflogOutput, nil),
lastReflogCommit: &models.Commit{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from B to A",
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
expectedCommits: []*models.Commit{
{
@@ -98,6 +105,7 @@ func TestGetReflogCommits(t *testing.T) {
Name: "checkout: moving from A to B",
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
},
expectedOnlyObtainedNew: true,
@@ -106,13 +114,14 @@ func TestGetReflogCommits(t *testing.T) {
{
testName: "when passing filterPath",
runner: oscommands.NewFakeRunner(t).
Expect(`git log -g --abbrev=20 --format="%h %ct %gs" --follow -- "path"`, reflogOutput, nil),
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p" --follow -- "path"`, reflogOutput, nil),
lastReflogCommit: &models.Commit{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from B to A",
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
filterPath: "path",
expectedCommits: []*models.Commit{
@@ -121,6 +130,7 @@ func TestGetReflogCommits(t *testing.T) {
Name: "checkout: moving from A to B",
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
},
expectedOnlyObtainedNew: true,
@@ -129,7 +139,7 @@ func TestGetReflogCommits(t *testing.T) {
{
testName: "when command returns error",
runner: oscommands.NewFakeRunner(t).
Expect(`git log -g --abbrev=20 --format="%h %ct %gs"`, "", errors.New("haha")),
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p"`, "", errors.New("haha")),
lastReflogCommit: nil,
filterPath: "",
+11 -13
View File
@@ -3,9 +3,9 @@ package loaders
import (
"fmt"
"regexp"
"sort"
"strings"
"github.com/jesseduffield/generics/slices"
gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
@@ -42,37 +42,35 @@ func (self *RemoteLoader) GetRemotes() ([]*models.Remote, error) {
}
// first step is to get our remotes from go-git
remotes := make([]*models.Remote, len(goGitRemotes))
for i, goGitRemote := range goGitRemotes {
remotes := slices.Map(goGitRemotes, func(goGitRemote *gogit.Remote) *models.Remote {
remoteName := goGitRemote.Config().Name
re := regexp.MustCompile(fmt.Sprintf(`(?m)^\s*%s\/([\S]+)`, remoteName))
matches := re.FindAllStringSubmatch(remoteBranchesStr, -1)
branches := make([]*models.RemoteBranch, len(matches))
for j, match := range matches {
branches[j] = &models.RemoteBranch{
branches := slices.Map(matches, func(match []string) *models.RemoteBranch {
return &models.RemoteBranch{
Name: match[1],
RemoteName: remoteName,
}
}
})
remotes[i] = &models.Remote{
return &models.Remote{
Name: goGitRemote.Config().Name,
Urls: goGitRemote.Config().URLs,
Branches: branches,
}
}
})
// now lets sort our remotes by name alphabetically
sort.Slice(remotes, func(i, j int) bool {
slices.SortFunc(remotes, func(a, b *models.Remote) bool {
// we want origin at the top because we'll be most likely to want it
if remotes[i].Name == "origin" {
if a.Name == "origin" {
return true
}
if remotes[j].Name == "origin" {
if b.Name == "origin" {
return false
}
return strings.ToLower(remotes[i].Name) < strings.ToLower(remotes[j].Name)
return strings.ToLower(a.Name) < strings.ToLower(b.Name)
})
return remotes, nil
+4 -5
View File
@@ -5,6 +5,7 @@ import (
"strconv"
"strings"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
@@ -65,11 +66,9 @@ outer:
func (self *StashLoader) getUnfilteredStashEntries() []*models.StashEntry {
rawString, _ := self.cmd.New("git stash list --pretty='%gs'").DontLog().RunWithOutput()
stashEntries := []*models.StashEntry{}
for i, line := range utils.SplitLines(rawString) {
stashEntries = append(stashEntries, self.stashEntryFromLine(line, i))
}
return stashEntries
return slices.MapWithIndex(utils.SplitLines(rawString), func(line string, index int) *models.StashEntry {
return self.stashEntryFromLine(line, index)
})
}
func (c *StashLoader) stashEntryFromLine(line string, index int) *models.StashEntry {
+6 -14
View File
@@ -1,8 +1,7 @@
package loaders
import (
"strings"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
@@ -27,25 +26,18 @@ func NewTagLoader(
func (self *TagLoader) GetTags() ([]*models.Tag, error) {
// get remote branches, sorted by creation date (descending)
// see: https://git-scm.com/docs/git-tag#Documentation/git-tag.txt---sortltkeygt
remoteBranchesStr, err := self.cmd.New(`git tag --list --sort=-creatordate`).DontLog().RunWithOutput()
tagsOutput, err := self.cmd.New(`git tag --list --sort=-creatordate`).DontLog().RunWithOutput()
if err != nil {
return nil, err
}
content := utils.TrimTrailingNewline(remoteBranchesStr)
if content == "" {
return nil, nil
}
split := utils.SplitLines(tagsOutput)
split := strings.Split(content, "\n")
// first step is to get our remotes from go-git
tags := make([]*models.Tag, len(split))
for i, tagName := range split {
tags[i] = &models.Tag{
tags := slices.Map(split, func(tagName string) *models.Tag {
return &models.Tag{
Name: tagName,
}
}
})
return tags, nil
}
+68
View File
@@ -0,0 +1,68 @@
package loaders
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
)
const tagsOutput = `v0.34
v0.33
v0.32.2
v0.32.1
v0.32
testtag
`
func TestGetTags(t *testing.T) {
type scenario struct {
testName string
runner *oscommands.FakeCmdObjRunner
expectedTags []*models.Tag
expectedError error
}
scenarios := []scenario{
{
testName: "should return no tags if there are none",
runner: oscommands.NewFakeRunner(t).
Expect(`git tag --list --sort=-creatordate`, "", nil),
expectedTags: []*models.Tag{},
expectedError: nil,
},
{
testName: "should return tags if present",
runner: oscommands.NewFakeRunner(t).
Expect(`git tag --list --sort=-creatordate`, tagsOutput, nil),
expectedTags: []*models.Tag{
{Name: "v0.34"},
{Name: "v0.33"},
{Name: "v0.32.2"},
{Name: "v0.32.1"},
{Name: "v0.32"},
{Name: "testtag"},
},
expectedError: nil,
},
}
for _, scenario := range scenarios {
scenario := scenario
t.Run(scenario.testName, func(t *testing.T) {
loader := &TagLoader{
Common: utils.NewDummyCommon(),
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
}
tags, err := loader.GetTags()
assert.Equal(t, scenario.expectedTags, tags)
assert.Equal(t, scenario.expectedError, err)
scenario.runner.CheckForMissingCalls()
})
}
}
+18 -5
View File
@@ -5,11 +5,12 @@ package models
type Branch struct {
Name string
// the displayname is something like '(HEAD detached at 123asdf)', whereas in that case the name would be '123asdf'
DisplayName string
Recency string
Pushables string
Pullables string
Head bool
DisplayName string
Recency string
Pushables string
Pullables string
UpstreamGone bool
Head bool
// if we have a named remote locally this will be the name of that remote e.g.
// 'origin' or 'tiwood'. If we don't have the remote locally it'll look like
// 'git@github.com:tiwood/lazygit.git'
@@ -17,10 +18,18 @@ type Branch struct {
UpstreamBranch string
}
func (b *Branch) FullRefName() string {
return "refs/heads/" + b.Name
}
func (b *Branch) RefName() string {
return b.Name
}
func (b *Branch) ParentRefName() string {
return b.RefName() + "^"
}
func (b *Branch) ID() string {
return b.RefName()
}
@@ -39,6 +48,10 @@ func (b *Branch) RemoteBranchStoredLocally() bool {
return b.IsTrackingRemote() && b.Pushables != "?" && b.Pullables != "?"
}
func (b *Branch) RemoteBranchNotStoredLocally() bool {
return b.IsTrackingRemote() && b.Pushables == "?" && b.Pullables == "?"
}
func (b *Branch) MatchesUpstream() bool {
return b.RemoteBranchStoredLocally() && b.Pushables == "0" && b.Pullables == "0"
}
+20 -1
View File
@@ -6,6 +6,9 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
// Special commit hash for empty tree object
const EmptyTreeCommitHash = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
// Commit : A git commit
type Commit struct {
Sha string
@@ -14,7 +17,8 @@ type Commit struct {
Action string // one of "", "pick", "edit", "squash", "reword", "drop", "fixup"
Tags []string
ExtraInfo string // something like 'HEAD -> master, tag: v0.15.2'
Author string
AuthorName string // something like 'Jesse Duffield'
AuthorEmail string // something like 'jessedduffield@gmail.com'
UnixTimestamp int64
// SHAs of parent commits (will be multiple if it's a merge commit)
@@ -25,10 +29,25 @@ func (c *Commit) ShortSha() string {
return utils.ShortSha(c.Sha)
}
func (c *Commit) FullRefName() string {
return c.Sha
}
func (c *Commit) RefName() string {
return c.Sha
}
func (c *Commit) ParentRefName() string {
if c.IsFirstCommit() {
return EmptyTreeCommitHash
}
return c.RefName() + "^"
}
func (c *Commit) IsFirstCommit() bool {
return len(c.Parents) == 0
}
func (c *Commit) ID() string {
return c.RefName()
}
+5
View File
@@ -27,6 +27,7 @@ type IFile interface {
GetHasStagedChanges() bool
GetIsTracked() bool
GetPath() string
GetPreviousPath() string
}
func (f *File) IsRename() bool {
@@ -85,3 +86,7 @@ func (f *File) GetPath() string {
// TODO: remove concept of name; just use path
return f.Name
}
func (f *File) GetPreviousPath() string {
return f.PreviousName
}
+8
View File
@@ -10,10 +10,18 @@ func (r *RemoteBranch) FullName() string {
return r.RemoteName + "/" + r.Name
}
func (r *RemoteBranch) FullRefName() string {
return "refs/remotes/" + r.FullName()
}
func (r *RemoteBranch) RefName() string {
return r.FullName()
}
func (r *RemoteBranch) ParentRefName() string {
return r.RefName() + "^"
}
func (r *RemoteBranch) ID() string {
return r.RefName()
}
+8
View File
@@ -8,10 +8,18 @@ type StashEntry struct {
Name string
}
func (s *StashEntry) FullRefName() string {
return s.RefName()
}
func (s *StashEntry) RefName() string {
return fmt.Sprintf("stash@{%d}", s.Index)
}
func (s *StashEntry) ParentRefName() string {
return s.RefName() + "^"
}
func (s *StashEntry) ID() string {
return s.RefName()
}
+8
View File
@@ -5,10 +5,18 @@ type Tag struct {
Name string
}
func (t *Tag) FullRefName() string {
return "refs/tags/" + t.RefName()
}
func (t *Tag) RefName() string {
return t.Name
}
func (t *Tag) ParentRefName() string {
return t.RefName() + "^"
}
func (t *Tag) ID() string {
return t.RefName()
}
+17
View File
@@ -2,6 +2,7 @@ package oscommands
import (
"os/exec"
"sync"
)
// A command object is a general way to represent a command to be run on the
@@ -50,6 +51,9 @@ type ICmdObj interface {
PromptOnCredentialRequest() ICmdObj
FailOnCredentialRequest() ICmdObj
WithMutex(mutex *sync.Mutex) ICmdObj
Mutex() *sync.Mutex
GetCredentialStrategy() CredentialStrategy
}
@@ -70,6 +74,9 @@ type CmdObj struct {
// if set to true, it means we might be asked to enter a username/password by this command.
credentialStrategy CredentialStrategy
// can be set so that we don't run certain commands simultaneously
mutex *sync.Mutex
}
type CredentialStrategy int
@@ -132,6 +139,16 @@ func (self *CmdObj) IgnoreEmptyError() ICmdObj {
return self
}
func (self *CmdObj) Mutex() *sync.Mutex {
return self.mutex
}
func (self *CmdObj) WithMutex(mutex *sync.Mutex) ICmdObj {
self.mutex = mutex
return self
}
func (self *CmdObj) ShouldIgnoreEmptyError() bool {
return self.ignoreEmptyError
}
+39 -10
View File
@@ -34,6 +34,11 @@ type cmdObjRunner struct {
var _ ICmdObjRunner = &cmdObjRunner{}
func (self *cmdObjRunner) Run(cmdObj ICmdObj) error {
if cmdObj.Mutex() != nil {
cmdObj.Mutex().Lock()
defer cmdObj.Mutex().Unlock()
}
if cmdObj.GetCredentialStrategy() != NONE {
return self.runWithCredentialHandling(cmdObj)
}
@@ -42,17 +47,14 @@ func (self *cmdObjRunner) Run(cmdObj ICmdObj) error {
return self.runAndStream(cmdObj)
}
_, err := self.RunWithOutput(cmdObj)
_, err := self.RunWithOutputAux(cmdObj)
return err
}
func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
if cmdObj.ShouldStreamOutput() {
err := self.runAndStream(cmdObj)
// for now we're not capturing output, just because it would take a little more
// effort and there's currently no use case for it. Some commands call RunWithOutput
// but ignore the output, hence why we've got this check here.
return "", err
if cmdObj.Mutex() != nil {
cmdObj.Mutex().Lock()
defer cmdObj.Mutex().Unlock()
}
if cmdObj.GetCredentialStrategy() != NONE {
@@ -63,6 +65,18 @@ func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
return "", err
}
if cmdObj.ShouldStreamOutput() {
err := self.runAndStream(cmdObj)
// for now we're not capturing output, just because it would take a little more
// effort and there's currently no use case for it. Some commands call RunWithOutput
// but ignore the output, hence why we've got this check here.
return "", err
}
return self.RunWithOutputAux(cmdObj)
}
func (self *cmdObjRunner) RunWithOutputAux(cmdObj ICmdObj) (string, error) {
self.log.WithField("command", cmdObj.ToString()).Debug("RunCommand")
if cmdObj.ShouldLog() {
@@ -77,6 +91,11 @@ func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
}
func (self *cmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error {
if cmdObj.Mutex() != nil {
cmdObj.Mutex().Lock()
defer cmdObj.Mutex().Unlock()
}
if cmdObj.GetCredentialStrategy() != NONE {
return errors.New("cannot call RunAndProcessLines with credential strategy. If you're seeing this then a contributor to Lazygit has accidentally called this method! Please raise an issue")
}
@@ -104,7 +123,7 @@ func (self *cmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line st
return err
}
if stop {
_ = cmd.Process.Kill()
_ = Kill(cmd)
break
}
}
@@ -188,6 +207,9 @@ func (self *cmdObjRunner) runAndStreamAux(
cmdObj ICmdObj,
onRun func(*cmdHandler, io.Writer),
) error {
// if we're streaming this we don't want any fancy terminal stuff
cmdObj.AddEnvVars("TERM=dumb")
cmdWriter := self.guiIO.newCmdWriterFn()
if cmdObj.ShouldLog() {
@@ -204,6 +226,9 @@ func (self *cmdObjRunner) runAndStreamAux(
return err
}
var stdout bytes.Buffer
handler.stdoutPipe = io.TeeReader(handler.stdoutPipe, &stdout)
defer func() {
if closeErr := handler.close(); closeErr != nil {
self.log.Error(closeErr)
@@ -215,10 +240,14 @@ func (self *cmdObjRunner) runAndStreamAux(
err = cmd.Wait()
if err != nil {
errStr := stderr.String()
if cmdObj.ShouldIgnoreEmptyError() && errStr == "" {
if errStr != "" {
return errors.New(errStr)
}
if cmdObj.ShouldIgnoreEmptyError() {
return nil
}
return errors.New(stderr.String())
return errors.New(stdout.String())
}
return nil
@@ -20,6 +20,7 @@ func (b *Buffer) Read(p []byte) (n int, err error) {
defer b.m.Unlock()
return b.b.Read(p)
}
func (b *Buffer) Write(p []byte) (n int, err error) {
b.m.Lock()
defer b.m.Unlock()
+2 -2
View File
@@ -72,7 +72,7 @@ func CopyFile(src, dst string) (err error) {
return
}
return
return //nolint: nakedret
}
// CopyDir recursively copies a directory tree, attempting to preserve permissions.
@@ -133,5 +133,5 @@ func CopyDir(src string, dst string) (err error) {
}
}
return
return //nolint: nakedret
}
+3 -2
View File
@@ -2,12 +2,13 @@ package oscommands
import (
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// NewDummyOSCommand creates a new dummy OSCommand for testing
func NewDummyOSCommand() *OSCommand {
osCmd := NewOSCommand(utils.NewDummyCommon(), dummyPlatform, NewNullGuiIO(utils.NewDummyLog()))
osCmd := NewOSCommand(utils.NewDummyCommon(), config.NewDummyAppConfig(), dummyPlatform, NewNullGuiIO(utils.NewDummyLog()))
return osCmd
}
@@ -56,7 +57,7 @@ var dummyPlatform = &Platform{
}
func NewDummyOSCommandWithRunner(runner *FakeCmdObjRunner) *OSCommand {
osCommand := NewOSCommand(utils.NewDummyCommon(), dummyPlatform, NewNullGuiIO(utils.NewDummyLog()))
osCommand := NewOSCommand(utils.NewDummyCommon(), config.NewDummyAppConfig(), dummyPlatform, NewNullGuiIO(utils.NewDummyLog()))
osCommand.Cmd = NewDummyCmdObjBuilder(runner)
return osCommand
@@ -21,7 +21,7 @@ type FakeCmdObjRunner struct {
var _ ICmdObjRunner = &FakeCmdObjRunner{}
func NewFakeRunner(t *testing.T) *FakeCmdObjRunner {
func NewFakeRunner(t *testing.T) *FakeCmdObjRunner { //nolint:thelper
return &FakeCmdObjRunner{t: t}
}
+28 -40
View File
@@ -12,7 +12,10 @@ import (
"github.com/go-errors/errors"
"github.com/atotto/clipboard"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/kill"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -26,6 +29,8 @@ type OSCommand struct {
removeFileFn func(string) error
Cmd *CmdObjBuilder
tempDir string
}
// Platform stores the os state
@@ -38,13 +43,14 @@ type Platform struct {
}
// NewOSCommand os command runner
func NewOSCommand(common *common.Common, platform *Platform, guiIO *guiIO) *OSCommand {
func NewOSCommand(common *common.Common, config config.AppConfigurer, platform *Platform, guiIO *guiIO) *OSCommand {
c := &OSCommand{
Common: common,
Platform: platform,
getenvFn: os.Getenv,
removeFileFn: os.RemoveAll,
guiIO: guiIO,
tempDir: config.GetTempDir(),
}
runner := &cmdObjRunner{log: common.Log, guiIO: guiIO}
@@ -72,9 +78,14 @@ func FileType(path string) string {
}
func (c *OSCommand) OpenFile(filename string) error {
return c.OpenFileAtLine(filename, 1)
}
func (c *OSCommand) OpenFileAtLine(filename string, lineNumber int) error {
commandTemplate := c.UserConfig.OS.OpenCommand
templateValues := map[string]string{
"filename": c.Quote(filename),
"line": fmt.Sprintf("%d", lineNumber),
}
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
return c.Cmd.NewShell(command).Run()
@@ -98,7 +109,7 @@ func (c *OSCommand) Quote(message string) string {
// AppendLineToFile adds a new line in file
func (c *OSCommand) AppendLineToFile(filename, line string) error {
c.LogCommand(fmt.Sprintf("Appending '%s' to file '%s'", line, filename), false)
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o600)
if err != nil {
return utils.WrapError(err)
}
@@ -111,27 +122,6 @@ func (c *OSCommand) AppendLineToFile(filename, line string) error {
return nil
}
// CreateTempFile writes a string to a new temp file and returns the file's name
func (c *OSCommand) CreateTempFile(filename, content string) (string, error) {
tmpfile, err := ioutil.TempFile("", filename)
if err != nil {
c.Log.Error(err)
return "", utils.WrapError(err)
}
c.LogCommand(fmt.Sprintf("Creating temp file '%s'", tmpfile.Name()), false)
if _, err := tmpfile.WriteString(content); err != nil {
c.Log.Error(err)
return "", utils.WrapError(err)
}
if err := tmpfile.Close(); err != nil {
c.Log.Error(err)
return "", utils.WrapError(err)
}
return tmpfile.Name(), nil
}
// CreateFileWithContent creates a file with the given content
func (c *OSCommand) CreateFileWithContent(path string, content string) error {
c.LogCommand(fmt.Sprintf("Creating file '%s'", path), false)
@@ -140,7 +130,7 @@ func (c *OSCommand) CreateFileWithContent(path string, content string) error {
return err
}
if err := ioutil.WriteFile(path, []byte(content), 0644); err != nil {
if err := ioutil.WriteFile(path, []byte(content), 0o644); err != nil {
c.Log.Error(err)
return utils.WrapError(err)
}
@@ -168,15 +158,11 @@ func (c *OSCommand) FileExists(path string) (bool, error) {
// PipeCommands runs a heap of commands and pipes their inputs/outputs together like A | B | C
func (c *OSCommand) PipeCommands(commandStrings ...string) error {
cmds := make([]*exec.Cmd, len(commandStrings))
logCmdStr := ""
for i, str := range commandStrings {
if i > 0 {
logCmdStr += " | "
}
logCmdStr += str
cmds[i] = c.Cmd.New(str).GetCmd()
}
cmds := slices.Map(commandStrings, func(cmdString string) *exec.Cmd {
return c.Cmd.New(cmdString).GetCmd()
})
logCmdStr := strings.Join(commandStrings, " | ")
c.LogCommand(logCmdStr, true)
for i := 0; i < len(cmds)-1; i++ {
@@ -230,12 +216,14 @@ func (c *OSCommand) PipeCommands(commandStrings ...string) error {
return nil
}
// Kill kills a process. If the process has Setpgid == true, then we have anticipated that it might spawn its own child processes, so we've given it a process group ID (PGID) equal to its process id (PID) and given its child processes will inherit the PGID, we can kill that group, rather than killing the process itself.
func Kill(cmd *exec.Cmd) error {
if cmd.Process == nil {
// somebody got to it before we were able to, poor bastard
return nil
}
return cmd.Process.Kill()
return kill.Kill(cmd)
}
// PrepareForChildren sets Setpgid to true on the cmd, so that when we run it as a subprocess, we can kill its group rather than the process itself. This is because some commands, like `docker-compose logs` spawn multiple children processes, and killing the parent process isn't sufficient for killing those child processes. We set the group id here, and then in subprocess.go we check if the group id is set and if so, we kill the whole group rather than just the one process.
func PrepareForChildren(cmd *exec.Cmd) {
kill.PrepareForChildren(cmd)
}
func (c *OSCommand) CopyToClipboard(str string) error {
@@ -255,8 +243,8 @@ func (c *OSCommand) Getenv(key string) string {
return c.getenvFn(key)
}
func GetTempDir() string {
return filepath.Join(os.TempDir(), "lazygit")
func (c *OSCommand) GetTempDir() string {
return c.tempDir
}
// GetLazygitPath returns the path of the currently executed file
+1 -62
View File
@@ -1,41 +1,12 @@
package oscommands
import (
"io/ioutil"
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestOSCommandRunWithOutput(t *testing.T) {
type scenario struct {
command string
test func(string, error)
}
scenarios := []scenario{
{
"echo -n '123'",
func(output string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "123", output)
},
},
{
"rmdir unexisting-folder",
func(output string, err error) {
assert.Regexp(t, "rmdir.*unexisting-folder.*", err.Error())
},
},
}
for _, s := range scenarios {
c := NewDummyOSCommand()
s.test(c.Cmd.New(s.command).RunWithOutput())
}
}
func TestOSCommandRun(t *testing.T) {
type scenario struct {
command string
@@ -141,7 +112,7 @@ func TestOSCommandFileType(t *testing.T) {
{
"testDirectory",
func() {
if err := os.Mkdir("testDirectory", 0644); err != nil {
if err := os.Mkdir("testDirectory", 0o644); err != nil {
panic(err)
}
},
@@ -164,35 +135,3 @@ func TestOSCommandFileType(t *testing.T) {
_ = os.RemoveAll(s.path)
}
}
func TestOSCommandCreateTempFile(t *testing.T) {
type scenario struct {
testName string
filename string
content string
test func(string, error)
}
scenarios := []scenario{
{
"valid case",
"filename",
"content",
func(path string, err error) {
assert.NoError(t, err)
content, err := ioutil.ReadFile(path)
assert.NoError(t, err)
assert.Equal(t, "content", string(content))
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
s.test(NewDummyOSCommand().CreateTempFile(s.filename, s.content))
})
}
}
@@ -10,6 +10,34 @@ import (
"github.com/stretchr/testify/assert"
)
func TestOSCommandRunWithOutput(t *testing.T) {
type scenario struct {
command string
test func(string, error)
}
scenarios := []scenario{
{
"echo -n '123'",
func(output string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "123", output)
},
},
{
"rmdir unexisting-folder",
func(output string, err error) {
assert.Regexp(t, "rmdir.*unexisting-folder.*", err.Error())
},
},
}
for _, s := range scenarios {
c := NewDummyOSCommand()
s.test(c.Cmd.New(s.command).RunWithOutput())
}
}
func TestOSCommandOpenFileDarwin(t *testing.T) {
type scenario struct {
filename string
+2 -1
View File
@@ -5,6 +5,7 @@ import (
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
type PatchHunk struct {
@@ -54,7 +55,7 @@ func (hunk *PatchHunk) updatedLines(lineIndices []int, reverse bool) []string {
if line == "" {
break
}
isLineSelected := utils.IncludesInt(lineIndices, lineIdx)
isLineSelected := lo.Contains(lineIndices, lineIdx)
firstChar, content := line[:1], line[1:]
transformedFirstChar := transformedFirstChar(firstChar, reverse, isLineSelected)
+18 -19
View File
@@ -4,7 +4,9 @@ import (
"sort"
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/jesseduffield/generics/maps"
"github.com/jesseduffield/generics/slices"
"github.com/samber/lo"
"github.com/sirupsen/logrus"
)
@@ -26,8 +28,10 @@ type fileInfo struct {
diff string
}
type applyPatchFunc func(patch string, flags ...string) error
type loadFileDiffFunc func(from string, to string, reverse bool, filename string, plain bool) (string, error)
type (
applyPatchFunc func(patch string, flags ...string) error
loadFileDiffFunc func(from string, to string, reverse bool, filename string, plain bool) (string, error)
)
// PatchManager manages the building of a patch for a commit to be applied to another commit (or the working tree, or removed from the current commit). We also support building patches from things like stashes, for which there is less flexibility
type PatchManager struct {
@@ -70,8 +74,9 @@ func (p *PatchManager) Start(from, to string, reverse bool, canRebase bool) {
func (p *PatchManager) addFileWhole(info *fileInfo) {
info.mode = WHOLE
lineCount := len(strings.Split(info.diff, "\n"))
info.includedLineIndices = make([]int, lineCount)
// add every line index
// TODO: add tests and then use lo.Range to simplify
info.includedLineIndices = make([]int, lineCount)
for i := 0; i < lineCount; i++ {
info.includedLineIndices[i] = i
}
@@ -138,7 +143,7 @@ func (p *PatchManager) AddFileLineRange(filename string, firstLineIdx, lastLineI
return err
}
info.mode = PART
info.includedLineIndices = utils.UnionInt(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx))
info.includedLineIndices = lo.Union(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx))
return nil
}
@@ -149,7 +154,7 @@ func (p *PatchManager) RemoveFileLineRange(filename string, firstLineIdx, lastLi
return err
}
info.mode = PART
info.includedLineIndices = utils.DifferenceInt(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx))
info.includedLineIndices, _ = lo.Difference(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx))
if len(info.includedLineIndices) == 0 {
p.removeFile(info)
}
@@ -190,21 +195,15 @@ func (p *PatchManager) RenderPatchForFile(filename string, plain bool, reverse b
func (p *PatchManager) renderEachFilePatch(plain bool) []string {
// sort files by name then iterate through and render each patch
filenames := make([]string, len(p.fileInfoMap))
index := 0
for filename := range p.fileInfoMap {
filenames[index] = filename
index++
}
filenames := maps.Keys(p.fileInfoMap)
sort.Strings(filenames)
output := []string{}
for _, filename := range filenames {
patch := p.RenderPatchForFile(filename, plain, false, true)
if patch != "" {
output = append(output, patch)
}
}
patches := slices.Map(filenames, func(filename string) string {
return p.RenderPatchForFile(filename, plain, false, true)
})
output := slices.Filter(patches, func(patch string) bool {
return patch != ""
})
return output
}
+4 -2
View File
@@ -8,8 +8,10 @@ import (
"github.com/sirupsen/logrus"
)
var hunkHeaderRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)[^\+]+\+(\d+)[^@]+@@(.*)$`)
var patchHeaderRegexp = regexp.MustCompile(`(?ms)(^diff.*?)^@@`)
var (
hunkHeaderRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)[^\+]+\+(\d+)[^@]+@@(.*)$`)
patchHeaderRegexp = regexp.MustCompile(`(?ms)(^diff.*?)^@@`)
)
func GetHeaderFromDiff(diff string) string {
match := patchHeaderRegexp.FindStringSubmatch(diff)
+18 -13
View File
@@ -4,9 +4,10 @@ import (
"regexp"
"strings"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
"github.com/sirupsen/logrus"
)
@@ -183,16 +184,21 @@ func parsePatch(patch string) ([]int, []int, []*PatchLine) {
// Render returns the coloured string of the diff with any selected lines highlighted
func (p *PatchParser) Render(firstLineIndex int, lastLineIndex int, incLineIndices []int) string {
renderedLines := make([]string, len(p.PatchLines))
for index, patchLine := range p.PatchLines {
selected := index >= firstLineIndex && index <= lastLineIndex
included := utils.IncludesInt(incLineIndices, index)
renderedLines[index] = patchLine.render(selected, included)
}
result := strings.Join(renderedLines, "\n")
if strings.TrimSpace(utils.Decolorise(result)) == "" {
contentToDisplay := slices.Some(p.PatchLines, func(line *PatchLine) bool {
return line.Content != ""
})
if !contentToDisplay {
return ""
}
renderedLines := slices.MapWithIndex(p.PatchLines, func(patchLine *PatchLine, index int) string {
selected := index >= firstLineIndex && index <= lastLineIndex
included := lo.Contains(incLineIndices, index)
return patchLine.render(selected, included)
})
result := strings.Join(renderedLines, "\n")
return result
}
@@ -201,10 +207,9 @@ func (p *PatchParser) Render(firstLineIndex int, lastLineIndex int, incLineIndic
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
}
renderedLines := slices.Map(linesToCopy, func(line *PatchLine) string {
return line.Content
})
return strings.Join(renderedLines, "\n")
}
+20 -8
View File
@@ -1,6 +1,7 @@
package config
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
@@ -43,13 +44,22 @@ type AppConfigurer interface {
GetUserConfigPaths() []string
GetUserConfigDir() string
ReloadUserConfig() error
GetTempDir() string
GetAppState() *AppState
SaveAppState() error
}
// NewAppConfig makes a new app config
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag bool) (*AppConfig, error) {
func NewAppConfig(
name string,
version,
commit,
date string,
buildSource string,
debuggingFlag bool,
tempDir string,
) (*AppConfig, error) {
configDir, err := findOrCreateConfigDir()
if err != nil && !os.IsPermission(err) {
return nil, err
@@ -74,15 +84,13 @@ 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",
Name: name,
Version: version,
Commit: commit,
BuildDate: date,
@@ -123,7 +131,7 @@ func configDirForVendor(vendor string) string {
func findOrCreateConfigDir() (string, error) {
folder := ConfigDir()
return folder, os.MkdirAll(folder, 0755)
return folder, os.MkdirAll(folder, 0o755)
}
func loadUserConfigWithDefaults(configFiles []string) (*UserConfig, error) {
@@ -160,7 +168,7 @@ func loadUserConfig(configFiles []string, base *UserConfig) (*UserConfig, error)
}
if err := yaml.Unmarshal(content, base); err != nil {
return nil, err
return nil, fmt.Errorf("The config at `%s` couldn't be parsed, please inspect it before opening up an issue.\n%w", path, err)
}
}
@@ -221,6 +229,10 @@ func (c *AppConfig) ReloadUserConfig() error {
return nil
}
func (c *AppConfig) GetTempDir() string {
return c.TempDir
}
func configFilePath(filename string) (string, error) {
folder, err := findOrCreateConfigDir()
if err != nil {
@@ -232,7 +244,7 @@ func configFilePath(filename string) (string, error) {
var ConfigFilename = "config.yml"
// ConfigFilename returns the filename of the deafult config file
// ConfigFilename returns the filename of the default config file
func (c *AppConfig) ConfigFilename() string {
return filepath.Join(c.UserConfigDir, ConfigFilename)
}
@@ -249,7 +261,7 @@ func (c *AppConfig) SaveAppState() error {
return err
}
err = ioutil.WriteFile(filepath, marshalledAppState, 0644)
err = ioutil.WriteFile(filepath, marshalledAppState, 0o644)
if err != nil && os.IsPermission(err) {
// apparently when people have read-only permissions they prefer us to fail silently
return nil
+2 -4
View File
@@ -3,14 +3,12 @@
package config
const DefaultEditCommandTemplate = `{{editor}} {{filename}}`
// GetPlatformDefaultConfig gets the defaults for the platform
func GetPlatformDefaultConfig() OSConfig {
return OSConfig{
EditCommand: ``,
EditCommandTemplate: DefaultEditCommandTemplate,
OpenCommand: "open {{filename}}",
EditCommandTemplate: "",
OpenCommand: "open -- {{filename}}",
OpenLinkCommand: "open {{link}}",
}
}
+19 -2
View File
@@ -1,12 +1,29 @@
package config
const DefaultEditCommandTemplate = `{{editor}} {{filename}}`
import (
"io/ioutil"
"strings"
)
func isWSL() bool {
data, err := ioutil.ReadFile("/proc/sys/kernel/osrelease")
return err == nil && strings.Contains(string(data), "microsoft")
}
// GetPlatformDefaultConfig gets the defaults for the platform
func GetPlatformDefaultConfig() OSConfig {
if isWSL() {
return OSConfig{
EditCommand: ``,
EditCommandTemplate: "",
OpenCommand: `powershell.exe start explorer.exe {{filename}} >/dev/null`,
OpenLinkCommand: `powershell.exe start {{link}} >/dev/null`,
}
}
return OSConfig{
EditCommand: ``,
EditCommandTemplate: DefaultEditCommandTemplate,
EditCommandTemplate: "",
OpenCommand: `xdg-open {{filename}} >/dev/null`,
OpenLinkCommand: `xdg-open {{link}} >/dev/null`,
}
+1 -3
View File
@@ -1,12 +1,10 @@
package config
const DefaultEditCommandTemplate = `{{editor}} {{filename}}`
// GetPlatformDefaultConfig gets the defaults for the platform
func GetPlatformDefaultConfig() OSConfig {
return OSConfig{
EditCommand: ``,
EditCommandTemplate: DefaultEditCommandTemplate,
EditCommandTemplate: "",
OpenCommand: `start "" {{filename}}`,
OpenLinkCommand: `start "" {{link}}`,
}
+81 -57
View File
@@ -1,5 +1,9 @@
package config
import (
"time"
)
type UserConfig struct {
Gui GuiConfig `yaml:"gui"`
Git GitConfig `yaml:"git"`
@@ -36,6 +40,7 @@ type GuiConfig struct {
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
MainPanelSplitMode string `yaml:"mainPanelSplitMode"`
Language string `yaml:"language"`
TimeFormat string `yaml:"timeFormat"`
Theme ThemeConfig `yaml:"theme"`
CommitLength CommitLengthConfig `yaml:"commitLength"`
SkipNoStagedFilesWarning bool `yaml:"skipNoStagedFilesWarning"`
@@ -43,7 +48,10 @@ type GuiConfig struct {
ShowFileTree bool `yaml:"showFileTree"`
ShowRandomTip bool `yaml:"showRandomTip"`
ShowCommandLog bool `yaml:"showCommandLog"`
ShowBottomLine bool `yaml:"showBottomLine"`
ShowIcons bool `yaml:"showIcons"`
CommandLogSize int `yaml:"commandLogSize"`
SplitDiff string `yaml:"splitDiff"`
}
type ThemeConfig struct {
@@ -68,12 +76,13 @@ type GitConfig struct {
Merging MergingConfig `yaml:"merging"`
SkipHookPrefix string `yaml:"skipHookPrefix"`
AutoFetch bool `yaml:"autoFetch"`
AutoRefresh bool `yaml:"autoRefresh"`
BranchLogCmd string `yaml:"branchLogCmd"`
AllBranchesLogCmd string `yaml:"allBranchesLogCmd"`
OverrideGpg bool `yaml:"overrideGpg"`
DisableForcePushing bool `yaml:"disableForcePushing"`
CommitPrefixes map[string]CommitPrefixConfig `yaml:"commitPrefixes"`
// this shoudl really be under 'gui', not 'git'
// this should really be under 'gui', not 'git'
ParseEmoji bool `yaml:"parseEmoji"`
Log LogConfig `yaml:"log"`
DiffContextSize int `yaml:"diffContextSize"`
@@ -95,8 +104,9 @@ type MergingConfig struct {
}
type LogConfig struct {
Order string `yaml:"order"` // one of date-order, author-date-order, topo-order
ShowGraph string `yaml:"showGraph"` // one of always, never, when-maximised
Order string `yaml:"order"` // one of date-order, author-date-order, topo-order
ShowGraph string `yaml:"showGraph"` // one of always, never, when-maximised
ShowWholeGraph bool `yaml:"showWholeGraph"`
}
type CommitPrefixConfig struct {
@@ -200,7 +210,7 @@ type KeybindingFilesConfig struct {
CommitChangesWithoutHook string `yaml:"commitChangesWithoutHook"`
AmendLastCommit string `yaml:"amendLastCommit"`
CommitChangesWithEditor string `yaml:"commitChangesWithEditor"`
IgnoreFile string `yaml:"ignoreFile"`
IgnoreOrExcludeFile string `yaml:"IgnoreOrExcludeFile"`
RefreshFiles string `yaml:"refreshFiles"`
StashAllChanges string `yaml:"stashAllChanges"`
ViewStashOptions string `yaml:"viewStashOptions"`
@@ -229,28 +239,29 @@ type KeybindingBranchesConfig struct {
}
type KeybindingCommitsConfig struct {
SquashDown string `yaml:"squashDown"`
RenameCommit string `yaml:"renameCommit"`
RenameCommitWithEditor string `yaml:"renameCommitWithEditor"`
ViewResetOptions string `yaml:"viewResetOptions"`
MarkCommitAsFixup string `yaml:"markCommitAsFixup"`
CreateFixupCommit string `yaml:"createFixupCommit"`
SquashAboveCommits string `yaml:"squashAboveCommits"`
MoveDownCommit string `yaml:"moveDownCommit"`
MoveUpCommit string `yaml:"moveUpCommit"`
AmendToCommit string `yaml:"amendToCommit"`
PickCommit string `yaml:"pickCommit"`
RevertCommit string `yaml:"revertCommit"`
CherryPickCopy string `yaml:"cherryPickCopy"`
CherryPickCopyRange string `yaml:"cherryPickCopyRange"`
PasteCommits string `yaml:"pasteCommits"`
TagCommit string `yaml:"tagCommit"`
CheckoutCommit string `yaml:"checkoutCommit"`
ResetCherryPick string `yaml:"resetCherryPick"`
CopyCommitMessageToClipboard string `yaml:"copyCommitMessageToClipboard"`
OpenLogMenu string `yaml:"openLogMenu"`
OpenInBrowser string `yaml:"openInBrowser"`
ViewBisectOptions string `yaml:"viewBisectOptions"`
SquashDown string `yaml:"squashDown"`
RenameCommit string `yaml:"renameCommit"`
RenameCommitWithEditor string `yaml:"renameCommitWithEditor"`
ViewResetOptions string `yaml:"viewResetOptions"`
MarkCommitAsFixup string `yaml:"markCommitAsFixup"`
CreateFixupCommit string `yaml:"createFixupCommit"`
SquashAboveCommits string `yaml:"squashAboveCommits"`
MoveDownCommit string `yaml:"moveDownCommit"`
MoveUpCommit string `yaml:"moveUpCommit"`
AmendToCommit string `yaml:"amendToCommit"`
ResetCommitAuthor string `yaml:"resetCommitAuthor"`
PickCommit string `yaml:"pickCommit"`
RevertCommit string `yaml:"revertCommit"`
CherryPickCopy string `yaml:"cherryPickCopy"`
CherryPickCopyRange string `yaml:"cherryPickCopyRange"`
PasteCommits string `yaml:"pasteCommits"`
TagCommit string `yaml:"tagCommit"`
CheckoutCommit string `yaml:"checkoutCommit"`
ResetCherryPick string `yaml:"resetCherryPick"`
CopyCommitAttributeToClipboard string `yaml:"copyCommitAttributeToClipboard"`
OpenLogMenu string `yaml:"openLogMenu"`
OpenInBrowser string `yaml:"openInBrowser"`
ViewBisectOptions string `yaml:"viewBisectOptions"`
}
type KeybindingStashConfig struct {
@@ -266,6 +277,7 @@ type KeybindingMainConfig struct {
ToggleDragSelectAlt string `yaml:"toggleDragSelect-alt"`
ToggleSelectHunk string `yaml:"toggleSelectHunk"`
PickBothHunks string `yaml:"pickBothHunks"`
EditSelectHunk string `yaml:"editSelectHunk"`
}
type KeybindingSubmodulesConfig struct {
@@ -301,12 +313,15 @@ type CustomCommand struct {
}
type CustomCommandPrompt struct {
Type string `yaml:"type"` // one of 'input' and 'menu'
Type string `yaml:"type"` // one of 'input', 'menu', or 'confirm'
Title string `yaml:"title"`
// this only apply to prompts
// this only apply to input prompts
InitialValue string `yaml:"initialValue"`
// this only applies to confirm prompts
Body string `yaml:"body"`
// this only applies to menus
Options []CustomCommandMenuOption
@@ -335,30 +350,35 @@ func GetDefaultConfig() *UserConfig {
ExpandFocusedSidePanel: false,
MainPanelSplitMode: "flexible",
Language: "auto",
TimeFormat: time.RFC822,
Theme: ThemeConfig{
LightTheme: false,
ActiveBorderColor: []string{"green", "bold"},
InactiveBorderColor: []string{"white"},
OptionsTextColor: []string{"blue"},
SelectedLineBgColor: []string{"default"},
SelectedLineBgColor: []string{"blue"},
SelectedRangeBgColor: []string{"blue"},
CherryPickedCommitBgColor: []string{"blue"},
CherryPickedCommitFgColor: []string{"cyan"},
CherryPickedCommitBgColor: []string{"cyan"},
CherryPickedCommitFgColor: []string{"blue"},
UnstagedChangesColor: []string{"red"},
},
CommitLength: CommitLengthConfig{Show: true},
SkipNoStagedFilesWarning: false,
ShowListFooter: true,
ShowCommandLog: true,
ShowBottomLine: true,
ShowFileTree: true,
ShowRandomTip: true,
ShowIcons: false,
CommandLogSize: 8,
SplitDiff: "auto",
},
Git: GitConfig{
Paging: PagingConfig{
ColorArg: "always",
Pager: "",
UseConfig: false},
UseConfig: false,
},
Commit: CommitConfig{
SignOff: false,
},
@@ -367,11 +387,13 @@ func GetDefaultConfig() *UserConfig {
Args: "",
},
Log: LogConfig{
Order: "topo-order",
ShowGraph: "when-maximised",
Order: "topo-order",
ShowGraph: "when-maximised",
ShowWholeGraph: false,
},
SkipHookPrefix: "WIP",
AutoFetch: true,
AutoRefresh: 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,
@@ -468,7 +490,7 @@ func GetDefaultConfig() *UserConfig {
CommitChangesWithoutHook: "w",
AmendLastCommit: "A",
CommitChangesWithEditor: "C",
IgnoreFile: "i",
IgnoreOrExcludeFile: "i",
RefreshFiles: "r",
StashAllChanges: "s",
ViewStashOptions: "S",
@@ -495,28 +517,29 @@ func GetDefaultConfig() *UserConfig {
FetchRemote: "f",
},
Commits: KeybindingCommitsConfig{
SquashDown: "s",
RenameCommit: "r",
RenameCommitWithEditor: "R",
ViewResetOptions: "g",
MarkCommitAsFixup: "f",
CreateFixupCommit: "F",
SquashAboveCommits: "S",
MoveDownCommit: "<c-j>",
MoveUpCommit: "<c-k>",
AmendToCommit: "A",
PickCommit: "p",
RevertCommit: "t",
CherryPickCopy: "c",
CherryPickCopyRange: "C",
PasteCommits: "v",
TagCommit: "T",
CheckoutCommit: "<space>",
ResetCherryPick: "<c-R>",
CopyCommitMessageToClipboard: "<c-y>",
OpenLogMenu: "<c-l>",
OpenInBrowser: "o",
ViewBisectOptions: "b",
SquashDown: "s",
RenameCommit: "r",
RenameCommitWithEditor: "R",
ViewResetOptions: "g",
MarkCommitAsFixup: "f",
CreateFixupCommit: "F",
SquashAboveCommits: "S",
MoveDownCommit: "<c-j>",
MoveUpCommit: "<c-k>",
AmendToCommit: "A",
ResetCommitAuthor: "a",
PickCommit: "p",
RevertCommit: "t",
CherryPickCopy: "c",
CherryPickCopyRange: "C",
PasteCommits: "v",
TagCommit: "T",
CheckoutCommit: "<space>",
ResetCherryPick: "<c-R>",
CopyCommitAttributeToClipboard: "y",
OpenLogMenu: "<c-l>",
OpenInBrowser: "o",
ViewBisectOptions: "b",
},
Stash: KeybindingStashConfig{
PopStash: "g",
@@ -529,6 +552,7 @@ func GetDefaultConfig() *UserConfig {
ToggleDragSelectAlt: "V",
ToggleSelectHunk: "a",
PickBothHunks: "b",
EditSelectHunk: "E",
},
Submodules: KeybindingSubmodulesConfig{
Init: "i",
+5 -1
View File
@@ -1,6 +1,10 @@
package env
import "os"
import (
"os"
)
// This package encapsulates accessing/mutating the ENV of the program.
func GetGitDirEnv() string {
return os.Getenv("GIT_DIR")
+16 -17
View File
@@ -4,32 +4,31 @@ import (
"sync"
"time"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// statusManager's job is to handle rendering of loading states and toast notifications
// that you see at the bottom left of the screen.
type statusManager struct {
statuses []appStatus
nextId int
mutex sync.Mutex
}
type appStatus struct {
message string
statusType string
id int
}
type statusManager struct {
statuses []appStatus
nextId int
mutex sync.Mutex
}
func (m *statusManager) removeStatus(id int) {
m.mutex.Lock()
defer m.mutex.Unlock()
newStatuses := []appStatus{}
for _, status := range m.statuses {
if status.id != id {
newStatuses = append(newStatuses, status)
}
}
m.statuses = newStatuses
m.statuses = slices.Filter(m.statuses, func(status appStatus) bool {
return status.id != id
})
}
func (m *statusManager) addWaitingStatus(message string) int {
@@ -83,7 +82,7 @@ func (m *statusManager) getStatusString() string {
return topStatus.message
}
func (gui *Gui) raiseToast(message string) {
func (gui *Gui) toast(message string) {
gui.statusManager.addToastStatus(message)
gui.renderAppStatus()
@@ -106,8 +105,8 @@ func (gui *Gui) renderAppStatus() {
})
}
// WithWaitingStatus wraps a function and shows a waiting status while the function is still executing
func (gui *Gui) WithWaitingStatus(message string, f func() error) error {
// withWaitingStatus wraps a function and shows a waiting status while the function is still executing
func (gui *Gui) withWaitingStatus(message string, f func() error) error {
go utils.Safe(func() {
id := gui.statusManager.addWaitingStatus(message)
@@ -119,7 +118,7 @@ func (gui *Gui) WithWaitingStatus(message string, f func() error) error {
if err := f(); err != nil {
gui.OnUIThread(func() error {
return gui.surfaceError(err)
return gui.c.Error(err)
})
}
})
+97 -67
View File
@@ -2,11 +2,95 @@ package gui
import (
"github.com/jesseduffield/lazygit/pkg/gui/boxlayout"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/mattn/go-runewidth"
)
// In this file we use the boxlayout package, along with knowledge about the app's state,
// to arrange the windows (i.e. panels) on the screen.
const INFO_SECTION_PADDING = " "
func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions {
width, height := gui.g.Size()
sideSectionWeight, mainSectionWeight := gui.getMidSectionWeights()
sidePanelsDirection := boxlayout.COLUMN
portraitMode := width <= 84 && height > 45
if portraitMode {
sidePanelsDirection = boxlayout.ROW
}
mainPanelsDirection := boxlayout.ROW
if gui.splitMainPanelSideBySide() {
mainPanelsDirection = boxlayout.COLUMN
}
extrasWindowSize := gui.getExtrasWindowSize(height)
showInfoSection := gui.c.UserConfig.Gui.ShowBottomLine || (gui.State.Searching.isSearching || gui.isAnyModeActive())
infoSectionSize := 0
if showInfoSection {
infoSectionSize = 1
}
root := &boxlayout.Box{
Direction: boxlayout.ROW,
Children: []*boxlayout.Box{
{
Direction: sidePanelsDirection,
Weight: 1,
Children: []*boxlayout.Box{
{
Direction: boxlayout.ROW,
Weight: sideSectionWeight,
ConditionalChildren: gui.sidePanelChildren,
},
{
Direction: boxlayout.ROW,
Weight: mainSectionWeight,
Children: []*boxlayout.Box{
{
Direction: mainPanelsDirection,
Children: gui.mainSectionChildren(),
Weight: 1,
},
{
Window: "extras",
Size: extrasWindowSize,
},
},
},
},
},
{
Direction: boxlayout.COLUMN,
Size: infoSectionSize,
Children: gui.infoSectionChildren(informationStr, appStatus),
},
},
}
layerOneWindows := boxlayout.ArrangeWindows(root, 0, 0, width, height)
limitWindows := boxlayout.ArrangeWindows(&boxlayout.Box{Window: "limit"}, 0, 0, width, height)
return MergeMaps(layerOneWindows, limitWindows)
}
func MergeMaps[K comparable, V any](maps ...map[K]V) map[K]V {
result := map[K]V{}
for _, currMap := range maps {
for key, value := range currMap {
result[key] = value
}
}
return result
}
func (gui *Gui) mainSectionChildren() []*boxlayout.Box {
currentWindow := gui.currentWindow()
@@ -44,7 +128,7 @@ func (gui *Gui) getMidSectionWeights() (int, int) {
currentWindow := gui.currentWindow()
// we originally specified this as a ratio i.e. .20 would correspond to a weight of 1 against 4
sidePanelWidthRatio := gui.UserConfig.Gui.SidePanelWidth
sidePanelWidthRatio := gui.c.UserConfig.Gui.SidePanelWidth
// we could make this better by creating ratios like 2:3 rather than always 1:something
mainSectionWeight := int(1/sidePanelWidthRatio) - 1
sideSectionWeight := 1
@@ -73,7 +157,7 @@ func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*
return []*boxlayout.Box{
{
Window: "searchPrefix",
Size: len(SEARCH_PREFIX),
Size: runewidth.StringWidth(SEARCH_PREFIX),
},
{
Window: "search",
@@ -88,7 +172,7 @@ func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*
result = append(result,
&boxlayout.Box{
Window: "appStatus",
Size: len(appStatus) + len(INFO_SECTION_PADDING),
Size: runewidth.StringWidth(appStatus) + runewidth.StringWidth(INFO_SECTION_PADDING),
},
)
}
@@ -102,7 +186,7 @@ func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*
{
Window: "information",
// unlike appStatus, informationStr has various colors so we need to decolorise before taking the length
Size: len(INFO_SECTION_PADDING) + len(utils.Decolorise(informationStr)),
Size: runewidth.StringWidth(INFO_SECTION_PADDING) + runewidth.StringWidth(utils.Decolorise(informationStr)),
},
}...,
)
@@ -115,7 +199,7 @@ func (gui *Gui) splitMainPanelSideBySide() bool {
return false
}
mainPanelSplitMode := gui.UserConfig.Gui.MainPanelSplitMode
mainPanelSplitMode := gui.c.UserConfig.Gui.MainPanelSplitMode
width, height := gui.g.Size()
switch mainPanelSplitMode {
@@ -138,76 +222,18 @@ func (gui *Gui) getExtrasWindowSize(screenHeight int) int {
}
var baseSize int
if gui.currentStaticContext().GetKey() == COMMAND_LOG_CONTEXT_KEY {
if gui.currentStaticContext().GetKey() == context.COMMAND_LOG_CONTEXT_KEY {
baseSize = 1000 // my way of saying 'fill the available space'
} else if screenHeight < 40 {
baseSize = 1
} else {
baseSize = gui.UserConfig.Gui.CommandLogSize
baseSize = gui.c.UserConfig.Gui.CommandLogSize
}
frameSize := 2
return baseSize + frameSize
}
func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions {
width, height := gui.g.Size()
sideSectionWeight, mainSectionWeight := gui.getMidSectionWeights()
sidePanelsDirection := boxlayout.COLUMN
portraitMode := width <= 84 && height > 45
if portraitMode {
sidePanelsDirection = boxlayout.ROW
}
mainPanelsDirection := boxlayout.ROW
if gui.splitMainPanelSideBySide() {
mainPanelsDirection = boxlayout.COLUMN
}
extrasWindowSize := gui.getExtrasWindowSize(height)
root := &boxlayout.Box{
Direction: boxlayout.ROW,
Children: []*boxlayout.Box{
{
Direction: sidePanelsDirection,
Weight: 1,
Children: []*boxlayout.Box{
{
Direction: boxlayout.ROW,
Weight: sideSectionWeight,
ConditionalChildren: gui.sidePanelChildren,
},
{
Direction: boxlayout.ROW,
Weight: mainSectionWeight,
Children: []*boxlayout.Box{
{
Direction: mainPanelsDirection,
Children: gui.mainSectionChildren(),
Weight: 1,
},
{
Window: "extras",
Size: extrasWindowSize,
},
},
},
},
},
{
Direction: boxlayout.COLUMN,
Size: 1,
Children: gui.infoSectionChildren(informationStr, appStatus),
},
},
}
return boxlayout.ArrangeWindows(root, 0, 0, width, height)
}
// 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 accordion mode is NOT in effect. If it is in effect
@@ -259,7 +285,7 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
fullHeightBox("stash"),
}
} else if height >= 28 {
accordionMode := gui.UserConfig.Gui.ExpandFocusedSidePanel
accordionMode := gui.c.UserConfig.Gui.ExpandFocusedSidePanel
accordionBox := func(defaultBox *boxlayout.Box) *boxlayout.Box {
if accordionMode && defaultBox.Window == currentWindow {
return &boxlayout.Box{
@@ -311,6 +337,10 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
}
}
func (gui *Gui) getCyclableWindows() []string {
return []string{"status", "files", "branches", "commits", "stash"}
}
func (gui *Gui) currentSideWindowName() string {
// there is always one and only one cyclable context in the context stack. We'll look from top to bottom
gui.State.ContextManager.RLock()
@@ -320,7 +350,7 @@ func (gui *Gui) currentSideWindowName() string {
reversedIdx := len(gui.State.ContextManager.ContextStack) - 1 - idx
context := gui.State.ContextManager.ContextStack[reversedIdx]
if context.GetKind() == SIDE_CONTEXT {
if context.GetKind() == types.SIDE_CONTEXT {
return context.GetWindowName()
}
}
-99
View File
@@ -1,99 +0,0 @@
package gui
type BasicContext struct {
OnFocus func(opts ...OnFocusOpts) error
OnFocusLost func() error
OnRender func() error
// this is for pushing some content to the main view
OnRenderToMain func(opts ...OnFocusOpts) error
Kind ContextKind
Key ContextKey
ViewName string
WindowName string
OnGetOptionsMap func() map[string]string
ParentContext Context
// we can't know on the calling end whether a Context is actually a nil value without reflection, so we're storing this flag here to tell us. There has got to be a better way around this
hasParent bool
}
func (self *BasicContext) GetOptionsMap() map[string]string {
if self.OnGetOptionsMap != nil {
return self.OnGetOptionsMap()
}
return nil
}
func (self *BasicContext) SetParentContext(context Context) {
self.ParentContext = context
self.hasParent = true
}
func (self *BasicContext) GetParentContext() (Context, bool) {
return self.ParentContext, self.hasParent
}
func (self *BasicContext) SetWindowName(windowName string) {
self.WindowName = windowName
}
func (self *BasicContext) GetWindowName() string {
windowName := self.WindowName
if windowName != "" {
return windowName
}
// TODO: actually set this for everything so we don't default to the view name
return self.ViewName
}
func (self *BasicContext) HandleRender() error {
if self.OnRender != nil {
return self.OnRender()
}
return nil
}
func (self *BasicContext) GetViewName() string {
return self.ViewName
}
func (self *BasicContext) HandleFocus(opts ...OnFocusOpts) error {
if self.OnFocus != nil {
if err := self.OnFocus(opts...); err != nil {
return err
}
}
if self.OnRenderToMain != nil {
if err := self.OnRenderToMain(opts...); err != nil {
return err
}
}
return nil
}
func (self *BasicContext) HandleFocusLost() error {
if self.OnFocusLost != nil {
return self.OnFocusLost()
}
return nil
}
func (self *BasicContext) HandleRenderToMain() error {
if self.OnRenderToMain != nil {
return self.OnRenderToMain()
}
return nil
}
func (self *BasicContext) GetKind() ContextKind {
return self.Kind
}
func (self *BasicContext) GetKey() ContextKey {
return self.Key
}
-219
View File
@@ -1,219 +0,0 @@
package gui
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
)
func (gui *Gui) handleOpenBisectMenu() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
// no shame in getting this directly rather than using the cached value
// given how cheap it is to obtain
info := gui.Git.Bisect.GetInfo()
commit := gui.getSelectedLocalCommit()
if info.Started() {
return gui.openMidBisectMenu(info, commit)
} else {
return gui.openStartBisectMenu(info, commit)
}
}
func (gui *Gui) openMidBisectMenu(info *git_commands.BisectInfo, commit *models.Commit) error {
// if there is not yet a 'current' bisect commit, or if we have
// selected the current commit, we need to jump to the next 'current' commit
// after we perform a bisect action. The reason we don't unconditionally jump
// is that sometimes the user will want to go and mark a few commits as skipped
// in a row and they wouldn't want to be jumped back to the current bisect
// commit each time.
// Originally we were allowing the user to, from the bisect menu, select whether
// they were talking about the selected commit or the current bisect commit,
// and that was a bit confusing (and required extra keypresses).
selectCurrentAfter := info.GetCurrentSha() == "" || info.GetCurrentSha() == commit.Sha
// we need to wait to reselect if our bisect commits aren't ancestors of our 'start'
// ref, because we'll be reloading our commits in that case.
waitToReselect := selectCurrentAfter && !gui.Git.Bisect.ReachableFromStart(info)
menuItems := []*menuItem{
{
displayString: fmt.Sprintf(gui.Tr.Bisect.Mark, commit.ShortSha(), info.NewTerm()),
onPress: func() error {
gui.logAction(gui.Tr.Actions.BisectMark)
if err := gui.Git.Bisect.Mark(commit.Sha, info.NewTerm()); err != nil {
return gui.surfaceError(err)
}
return gui.afterMark(selectCurrentAfter, waitToReselect)
},
},
{
displayString: fmt.Sprintf(gui.Tr.Bisect.Mark, commit.ShortSha(), info.OldTerm()),
onPress: func() error {
gui.logAction(gui.Tr.Actions.BisectMark)
if err := gui.Git.Bisect.Mark(commit.Sha, info.OldTerm()); err != nil {
return gui.surfaceError(err)
}
return gui.afterMark(selectCurrentAfter, waitToReselect)
},
},
{
displayString: fmt.Sprintf(gui.Tr.Bisect.Skip, commit.ShortSha()),
onPress: func() error {
gui.logAction(gui.Tr.Actions.BisectSkip)
if err := gui.Git.Bisect.Skip(commit.Sha); err != nil {
return gui.surfaceError(err)
}
return gui.afterMark(selectCurrentAfter, waitToReselect)
},
},
{
displayString: gui.Tr.Bisect.ResetOption,
onPress: func() error {
return gui.resetBisect()
},
},
}
return gui.createMenu(
gui.Tr.Bisect.BisectMenuTitle,
menuItems,
createMenuOptions{showCancel: true},
)
}
func (gui *Gui) openStartBisectMenu(info *git_commands.BisectInfo, commit *models.Commit) error {
return gui.createMenu(
gui.Tr.Bisect.BisectMenuTitle,
[]*menuItem{
{
displayString: fmt.Sprintf(gui.Tr.Bisect.MarkStart, commit.ShortSha(), info.NewTerm()),
onPress: func() error {
gui.logAction(gui.Tr.Actions.StartBisect)
if err := gui.Git.Bisect.Start(); err != nil {
return gui.surfaceError(err)
}
if err := gui.Git.Bisect.Mark(commit.Sha, info.NewTerm()); err != nil {
return gui.surfaceError(err)
}
return gui.postBisectCommandRefresh()
},
},
{
displayString: fmt.Sprintf(gui.Tr.Bisect.MarkStart, commit.ShortSha(), info.OldTerm()),
onPress: func() error {
gui.logAction(gui.Tr.Actions.StartBisect)
if err := gui.Git.Bisect.Start(); err != nil {
return gui.surfaceError(err)
}
if err := gui.Git.Bisect.Mark(commit.Sha, info.OldTerm()); err != nil {
return gui.surfaceError(err)
}
return gui.postBisectCommandRefresh()
},
},
},
createMenuOptions{showCancel: true},
)
}
func (gui *Gui) resetBisect() error {
return gui.ask(askOpts{
title: gui.Tr.Bisect.ResetTitle,
prompt: gui.Tr.Bisect.ResetPrompt,
handleConfirm: func() error {
gui.logAction(gui.Tr.Actions.ResetBisect)
if err := gui.Git.Bisect.Reset(); err != nil {
return gui.surfaceError(err)
}
return gui.postBisectCommandRefresh()
},
})
}
func (gui *Gui) showBisectCompleteMessage(candidateShas []string) error {
prompt := gui.Tr.Bisect.CompletePrompt
if len(candidateShas) > 1 {
prompt = gui.Tr.Bisect.CompletePromptIndeterminate
}
formattedCommits, err := gui.Git.Commit.GetCommitsOneline(candidateShas)
if err != nil {
return gui.surfaceError(err)
}
return gui.ask(askOpts{
title: gui.Tr.Bisect.CompleteTitle,
prompt: fmt.Sprintf(prompt, strings.TrimSpace(formattedCommits)),
handleConfirm: func() error {
gui.logAction(gui.Tr.Actions.ResetBisect)
if err := gui.Git.Bisect.Reset(); err != nil {
return gui.surfaceError(err)
}
return gui.postBisectCommandRefresh()
},
})
}
func (gui *Gui) afterMark(selectCurrent bool, waitToReselect bool) error {
done, candidateShas, err := gui.Git.Bisect.IsDone()
if err != nil {
return gui.surfaceError(err)
}
if err := gui.afterBisectMarkRefresh(selectCurrent, waitToReselect); err != nil {
return gui.surfaceError(err)
}
if done {
return gui.showBisectCompleteMessage(candidateShas)
}
return nil
}
func (gui *Gui) postBisectCommandRefresh() error {
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{}})
}
func (gui *Gui) afterBisectMarkRefresh(selectCurrent bool, waitToReselect bool) error {
selectFn := func() {
if selectCurrent {
gui.selectCurrentBisectCommit()
}
}
if waitToReselect {
return gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []RefreshableView{}, then: selectFn})
} else {
selectFn()
return gui.postBisectCommandRefresh()
}
}
func (gui *Gui) selectCurrentBisectCommit() {
info := gui.Git.Bisect.GetInfo()
if info.GetCurrentSha() != "" {
// find index of commit with that sha, move cursor to that.
for i, commit := range gui.State.Commits {
if commit.Sha == info.GetCurrentSha() {
gui.State.Contexts.BranchCommits.GetPanelState().SetSelectedLineIdx(i)
_ = gui.State.Contexts.BranchCommits.HandleFocus()
break
}
}
}
}
+97 -37
View File
@@ -1,6 +1,10 @@
package boxlayout
import "math"
import (
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
type Dimensions struct {
X0 int
@@ -69,45 +73,12 @@ func ArrangeWindows(root *Box, x0, y0, width, height int) map[string]Dimensions
availableSize = height
}
// work out size taken up by children
reservedSize := 0
totalWeight := 0
for _, child := range children {
// assuming either size or weight are non-zero
reservedSize += child.Size
totalWeight += child.Weight
}
remainingSize := availableSize - reservedSize
if remainingSize < 0 {
remainingSize = 0
}
unitSize := 0
extraSize := 0
if totalWeight > 0 {
unitSize = remainingSize / totalWeight
extraSize = remainingSize % totalWeight
}
sizes := calcSizes(children, availableSize)
result := map[string]Dimensions{}
offset := 0
for _, child := range children {
var boxSize int
if child.isStatic() {
boxSize = child.Size
// assuming that only one static child can have a size greater than the
// available space. In that case we just crop the size to what's available
if boxSize > availableSize {
boxSize = availableSize
}
} else {
// TODO: consider more evenly distributing the remainder
boxSize = unitSize * child.Weight
boxExtraSize := int(math.Min(float64(extraSize), float64(child.Weight)))
boxSize += boxExtraSize
extraSize -= boxExtraSize
}
for i, child := range children {
boxSize := sizes[i]
var resultForChild map[string]Dimensions
if direction == COLUMN {
@@ -123,6 +94,95 @@ func ArrangeWindows(root *Box, x0, y0, width, height int) map[string]Dimensions
return result
}
func calcSizes(boxes []*Box, availableSpace int) []int {
normalizedWeights := normalizeWeights(slices.Map(boxes, func(box *Box) int { return box.Weight }))
totalWeight := 0
reservedSpace := 0
for i, box := range boxes {
if box.isStatic() {
reservedSpace += box.Size
} else {
totalWeight += normalizedWeights[i]
}
}
dynamicSpace := utils.Max(0, availableSpace-reservedSpace)
unitSize := 0
extraSpace := 0
if totalWeight > 0 {
unitSize = dynamicSpace / totalWeight
extraSpace = dynamicSpace % totalWeight
}
result := make([]int, len(boxes))
for i, box := range boxes {
if box.isStatic() {
// assuming that only one static child can have a size greater than the
// available space. In that case we just crop the size to what's available
result[i] = utils.Min(availableSpace, box.Size)
} else {
result[i] = unitSize * normalizedWeights[i]
}
}
// distribute the remainder across dynamic boxes.
for extraSpace > 0 {
for i, weight := range normalizedWeights {
if weight > 0 {
result[i]++
extraSpace--
normalizedWeights[i]--
if extraSpace == 0 {
break
}
}
}
}
return result
}
// removes common multiple from weights e.g. if we get 2, 4, 4 we return 1, 2, 2.
func normalizeWeights(weights []int) []int {
if len(weights) == 0 {
return []int{}
}
// to spare us some computation we'll exit early if any of our weights is 1
if slices.Some(weights, func(weight int) bool { return weight == 1 }) {
return weights
}
// map weights to factorSlices and find the lowest common factor
positiveWeights := slices.Filter(weights, func(weight int) bool { return weight > 0 })
factorSlices := slices.Map(positiveWeights, func(weight int) []int { return calcFactors(weight) })
commonFactors := factorSlices[0]
for _, factors := range factorSlices {
commonFactors = lo.Intersect(commonFactors, factors)
}
if len(commonFactors) == 0 {
return weights
}
newWeights := slices.Map(weights, func(weight int) int { return weight / commonFactors[0] })
return normalizeWeights(newWeights)
}
func calcFactors(n int) []int {
factors := []int{}
for i := 2; i <= n; i++ {
if n%i == 0 {
factors = append(factors, i)
}
}
return factors
}
func (b *Box) isStatic() bool {
return b.Size > 0
}
+232 -63
View File
@@ -19,24 +19,24 @@ func TestArrangeWindows(t *testing.T) {
scenarios := []scenario{
{
"Empty box",
&Box{},
0,
0,
10,
10,
func(result map[string]Dimensions) {
testName: "Empty box",
root: &Box{},
x0: 0,
y0: 0,
width: 10,
height: 10,
test: func(result map[string]Dimensions) {
assert.EqualValues(t, result, map[string]Dimensions{})
},
},
{
"Box with static and dynamic panel",
&Box{Children: []*Box{{Size: 1, Window: "static"}, {Weight: 1, Window: "dynamic"}}},
0,
0,
10,
10,
func(result map[string]Dimensions) {
testName: "Box with static and dynamic panel",
root: &Box{Children: []*Box{{Size: 1, Window: "static"}, {Weight: 1, Window: "dynamic"}}},
x0: 0,
y0: 0,
width: 10,
height: 10,
test: func(result map[string]Dimensions) {
assert.EqualValues(
t,
result,
@@ -48,13 +48,13 @@ func TestArrangeWindows(t *testing.T) {
},
},
{
"Box with static and two dynamic panels",
&Box{Children: []*Box{{Size: 1, Window: "static"}, {Weight: 1, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}}},
0,
0,
10,
10,
func(result map[string]Dimensions) {
testName: "Box with static and two dynamic panels",
root: &Box{Children: []*Box{{Size: 1, Window: "static"}, {Weight: 1, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}}},
x0: 0,
y0: 0,
width: 10,
height: 10,
test: func(result map[string]Dimensions) {
assert.EqualValues(
t,
result,
@@ -67,13 +67,13 @@ func TestArrangeWindows(t *testing.T) {
},
},
{
"Box with COLUMN direction",
&Box{Direction: COLUMN, Children: []*Box{{Size: 1, Window: "static"}, {Weight: 1, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}}},
0,
0,
10,
10,
func(result map[string]Dimensions) {
testName: "Box with COLUMN direction",
root: &Box{Direction: COLUMN, Children: []*Box{{Size: 1, Window: "static"}, {Weight: 1, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}}},
x0: 0,
y0: 0,
width: 10,
height: 10,
test: func(result map[string]Dimensions) {
assert.EqualValues(
t,
result,
@@ -86,19 +86,19 @@ func TestArrangeWindows(t *testing.T) {
},
},
{
"Box with COLUMN direction only on wide boxes with narrow box",
&Box{ConditionalDirection: func(width int, height int) Direction {
testName: "Box with COLUMN direction only on wide boxes with narrow box",
root: &Box{ConditionalDirection: func(width int, height int) Direction {
if width > 4 {
return COLUMN
} else {
return ROW
}
}, Children: []*Box{{Weight: 1, Window: "dynamic1"}, {Weight: 1, Window: "dynamic2"}}},
0,
0,
4,
4,
func(result map[string]Dimensions) {
x0: 0,
y0: 0,
width: 4,
height: 4,
test: func(result map[string]Dimensions) {
assert.EqualValues(
t,
result,
@@ -110,19 +110,20 @@ func TestArrangeWindows(t *testing.T) {
},
},
{
"Box with COLUMN direction only on wide boxes with wide box",
&Box{ConditionalDirection: func(width int, height int) Direction {
testName: "Box with COLUMN direction only on wide boxes with wide box",
root: &Box{ConditionalDirection: func(width int, height int) Direction {
if width > 4 {
return COLUMN
} else {
return ROW
}
}, Children: []*Box{{Weight: 1, Window: "dynamic1"}, {Weight: 1, Window: "dynamic2"}}},
0,
0,
5,
5,
func(result map[string]Dimensions) {
// 5 / 2 = 2 remainder 1. That remainder goes to the first box.
x0: 0,
y0: 0,
width: 5,
height: 5,
test: func(result map[string]Dimensions) {
assert.EqualValues(
t,
result,
@@ -134,19 +135,19 @@ func TestArrangeWindows(t *testing.T) {
},
},
{
"Box with conditional children where box is wide",
&Box{ConditionalChildren: func(width int, height int) []*Box {
testName: "Box with conditional children where box is wide",
root: &Box{ConditionalChildren: func(width int, height int) []*Box {
if width > 4 {
return []*Box{{Window: "wide", Weight: 1}}
} else {
return []*Box{{Window: "narrow", Weight: 1}}
}
}},
0,
0,
5,
5,
func(result map[string]Dimensions) {
x0: 0,
y0: 0,
width: 5,
height: 5,
test: func(result map[string]Dimensions) {
assert.EqualValues(
t,
result,
@@ -157,19 +158,19 @@ func TestArrangeWindows(t *testing.T) {
},
},
{
"Box with conditional children where box is narrow",
&Box{ConditionalChildren: func(width int, height int) []*Box {
testName: "Box with conditional children where box is narrow",
root: &Box{ConditionalChildren: func(width int, height int) []*Box {
if width > 4 {
return []*Box{{Window: "wide", Weight: 1}}
} else {
return []*Box{{Window: "narrow", Weight: 1}}
}
}},
0,
0,
4,
4,
func(result map[string]Dimensions) {
x0: 0,
y0: 0,
width: 4,
height: 4,
test: func(result map[string]Dimensions) {
assert.EqualValues(
t,
result,
@@ -180,13 +181,13 @@ func TestArrangeWindows(t *testing.T) {
},
},
{
"Box with static child with size too large",
&Box{Direction: COLUMN, Children: []*Box{{Size: 11, Window: "static"}, {Weight: 1, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}}},
0,
0,
10,
10,
func(result map[string]Dimensions) {
testName: "Box with static child with size too large",
root: &Box{Direction: COLUMN, Children: []*Box{{Size: 11, Window: "static"}, {Weight: 1, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}}},
x0: 0,
y0: 0,
width: 10,
height: 10,
test: func(result map[string]Dimensions) {
assert.EqualValues(
t,
result,
@@ -200,6 +201,118 @@ func TestArrangeWindows(t *testing.T) {
)
},
},
{
// 10 total space minus 2 from the status box leaves us with 8.
// Total weight is 3, 8 / 3 = 2 with 2 remainder.
// We want to end up with 2, 3, 5 (one unit from remainder to each dynamic box)
testName: "Distributing remainder across weighted boxes",
root: &Box{Direction: COLUMN, Children: []*Box{{Size: 2, Window: "static"}, {Weight: 1, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}}},
x0: 0,
y0: 0,
width: 10,
height: 10,
test: func(result map[string]Dimensions) {
assert.EqualValues(
t,
result,
map[string]Dimensions{
"static": {X0: 0, X1: 1, Y0: 0, Y1: 9}, // 2
"dynamic1": {X0: 2, X1: 4, Y0: 0, Y1: 9}, // 3
"dynamic2": {X0: 5, X1: 9, Y0: 0, Y1: 9}, // 5
},
)
},
},
{
// 9 total space.
// total weight is 5, 9 / 5 = 1 with 4 remainder
// we want to give 2 of that remainder to the first, 1 to the second, and 1 to the last.
// Reason being that we just give units to each box evenly and consider weight in subsequent passes.
testName: "Distributing remainder across weighted boxes 2",
root: &Box{Direction: COLUMN, Children: []*Box{{Weight: 2, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}, {Weight: 1, Window: "dynamic3"}}},
x0: 0,
y0: 0,
width: 9,
height: 10,
test: func(result map[string]Dimensions) {
assert.EqualValues(
t,
result,
map[string]Dimensions{
"dynamic1": {X0: 0, X1: 3, Y0: 0, Y1: 9}, // 4
"dynamic2": {X0: 4, X1: 6, Y0: 0, Y1: 9}, // 3
"dynamic3": {X0: 7, X1: 8, Y0: 0, Y1: 9}, // 2
},
)
},
},
{
// 9 total space.
// total weight is 5, 9 / 5 = 1 with 4 remainder
// we want to give 2 of that remainder to the first, 1 to the second, and 1 to the last.
// Reason being that we just give units to each box evenly and consider weight in subsequent passes.
testName: "Distributing remainder across weighted boxes with unnormalized weights",
root: &Box{Direction: COLUMN, Children: []*Box{{Weight: 4, Window: "dynamic1"}, {Weight: 4, Window: "dynamic2"}, {Weight: 2, Window: "dynamic3"}}},
x0: 0,
y0: 0,
width: 9,
height: 10,
test: func(result map[string]Dimensions) {
assert.EqualValues(
t,
result,
map[string]Dimensions{
"dynamic1": {X0: 0, X1: 3, Y0: 0, Y1: 9}, // 4
"dynamic2": {X0: 4, X1: 6, Y0: 0, Y1: 9}, // 3
"dynamic3": {X0: 7, X1: 8, Y0: 0, Y1: 9}, // 2
},
)
},
},
{
testName: "Another distribution test",
root: &Box{Direction: COLUMN, Children: []*Box{
{Weight: 3, Window: "dynamic1"},
{Weight: 1, Window: "dynamic2"},
{Weight: 1, Window: "dynamic3"},
}},
x0: 0,
y0: 0,
width: 9,
height: 10,
test: func(result map[string]Dimensions) {
assert.EqualValues(
t,
result,
map[string]Dimensions{
"dynamic1": {X0: 0, X1: 4, Y0: 0, Y1: 9}, // 5
"dynamic2": {X0: 5, X1: 6, Y0: 0, Y1: 9}, // 2
"dynamic3": {X0: 7, X1: 8, Y0: 0, Y1: 9}, // 2
},
)
},
},
{
testName: "Box with zero weight",
root: &Box{Direction: COLUMN, Children: []*Box{
{Weight: 1, Window: "dynamic1"},
{Weight: 0, Window: "dynamic2"},
}},
x0: 0,
y0: 0,
width: 10,
height: 10,
test: func(result map[string]Dimensions) {
assert.EqualValues(
t,
result,
map[string]Dimensions{
"dynamic1": {X0: 0, X1: 9, Y0: 0, Y1: 9},
"dynamic2": {X0: 10, X1: 9, Y0: 0, Y1: 9}, // when X0 > X1, we will hide the window
},
)
},
},
}
for _, s := range scenarios {
@@ -209,3 +322,59 @@ func TestArrangeWindows(t *testing.T) {
})
}
}
func TestNormalizeWeights(t *testing.T) {
scenarios := []struct {
testName string
input []int
expected []int
}{
{
testName: "empty",
input: []int{},
expected: []int{},
},
{
testName: "one item of value 1",
input: []int{1},
expected: []int{1},
},
{
testName: "one item of value greater than 1",
input: []int{2},
expected: []int{1},
},
{
testName: "slice contains 1",
input: []int{2, 1},
expected: []int{2, 1},
},
{
testName: "slice contains 2 and 2",
input: []int{2, 2},
expected: []int{1, 1},
},
{
testName: "no common multiple",
input: []int{2, 3},
expected: []int{2, 3},
},
{
testName: "complex case",
input: []int{10, 10, 20},
expected: []int{1, 1, 2},
},
{
testName: "when a zero weight is included it is ignored",
input: []int{10, 10, 20, 0},
expected: []int{1, 1, 2, 0},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
assert.EqualValues(t, s.expected, normalizeWeights(s.input))
})
}
}
+4 -532
View File
@@ -1,548 +1,20 @@
package gui
import (
"errors"
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// list panel functions
func (gui *Gui) getSelectedBranch() *models.Branch {
if len(gui.State.Branches) == 0 {
return nil
}
selectedLine := gui.State.Panels.Branches.SelectedLineIdx
if selectedLine == -1 {
return nil
}
return gui.State.Branches[selectedLine]
}
func (gui *Gui) branchesRenderToMain() error {
var task updateTask
branch := gui.getSelectedBranch()
branch := gui.State.Contexts.Branches.GetSelected()
if branch == nil {
task = NewRenderStringTask(gui.Tr.NoBranchesThisRepo)
task = NewRenderStringTask(gui.c.Tr.NoBranchesThisRepo)
} else {
cmdObj := gui.Git.Branch.GetGraphCmdObj(branch.Name)
cmdObj := gui.git.Branch.GetGraphCmdObj(branch.FullRefName())
task = NewRunPtyTask(cmdObj.GetCmd())
}
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: "Log",
title: gui.c.Tr.LogTitle,
task: task,
},
})
}
// gui.refreshStatus is called at the end of this because that's when we can
// be sure there is a state.Branches array to pick the current branch from
func (gui *Gui) refreshBranches() {
reflogCommits := gui.State.FilteredReflogCommits
if gui.State.Modes.Filtering.Active() {
// in filter mode we filter our reflog commits to just those containing the path
// however we need all the reflog entries to populate the recencies of our branches
// which allows us to order them correctly. So if we're filtering we'll just
// manually load all the reflog commits here
var err error
reflogCommits, _, err = gui.Git.Loaders.ReflogCommits.GetReflogCommits(nil, "")
if err != nil {
gui.Log.Error(err)
}
}
branches, err := gui.Git.Loaders.Branches.Load(reflogCommits)
if err != nil {
_ = gui.surfaceError(err)
}
gui.State.Branches = branches
if err := gui.postRefreshUpdate(gui.State.Contexts.Branches); err != nil {
gui.Log.Error(err)
}
gui.refreshStatus()
}
// specific functions
func (gui *Gui) handleBranchPress() error {
if gui.State.Panels.Branches.SelectedLineIdx == -1 {
return nil
}
if gui.State.Panels.Branches.SelectedLineIdx == 0 {
return gui.createErrorPanel(gui.Tr.AlreadyCheckedOutBranch)
}
branch := gui.getSelectedBranch()
gui.logAction(gui.Tr.Actions.CheckoutBranch)
return gui.handleCheckoutRef(branch.Name, handleCheckoutRefOptions{})
}
func (gui *Gui) handleCreatePullRequestPress() error {
branch := gui.getSelectedBranch()
return gui.createPullRequest(branch.Name, "")
}
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 {
hostingServiceMgr := gui.getHostingServiceMgr()
branch := gui.getSelectedBranch()
branchExistsOnRemote := gui.Git.Remote.CheckRemoteBranchExists(branch.Name)
if !branchExistsOnRemote {
return gui.surfaceError(errors.New(gui.Tr.NoBranchOnRemote))
}
url, err := hostingServiceMgr.GetPullRequestURL(branch.Name, "")
if err != nil {
return gui.surfaceError(err)
}
gui.logAction(gui.Tr.Actions.CopyPullRequestURL)
if err := gui.OSCommand.CopyToClipboard(url); err != nil {
return gui.surfaceError(err)
}
gui.raiseToast(gui.Tr.PullRequestURLCopiedToClipboard)
return nil
}
func (gui *Gui) handleGitFetch() error {
if err := gui.createLoaderPanel(gui.Tr.FetchWait); err != nil {
return err
}
go utils.Safe(func() {
err := gui.fetch()
gui.handleCredentialsPopup(err)
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC})
})
return nil
}
func (gui *Gui) handleForceCheckout() error {
branch := gui.getSelectedBranch()
message := gui.Tr.SureForceCheckout
title := gui.Tr.ForceCheckoutBranch
return gui.ask(askOpts{
title: title,
prompt: message,
handleConfirm: func() error {
gui.logAction(gui.Tr.Actions.ForceCheckoutBranch)
if err := gui.Git.Branch.Checkout(branch.Name, git_commands.CheckoutOptions{Force: true}); err != nil {
_ = gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
},
})
}
type handleCheckoutRefOptions struct {
WaitingStatus string
EnvVars []string
onRefNotFound func(ref string) error
}
func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions) error {
waitingStatus := options.WaitingStatus
if waitingStatus == "" {
waitingStatus = gui.Tr.CheckingOutStatus
}
cmdOptions := git_commands.CheckoutOptions{Force: false, EnvVars: options.EnvVars}
onSuccess := func() {
gui.State.Panels.Branches.SelectedLineIdx = 0
gui.State.Panels.Commits.SelectedLineIdx = 0
// loading a heap of commits is slow so we limit them whenever doing a reset
gui.State.Panels.Commits.LimitCommits = true
}
return gui.WithWaitingStatus(waitingStatus, func() error {
if err := gui.Git.Branch.Checkout(ref, cmdOptions); err != nil {
// note, this will only work for english-language git commands. If we force git to use english, and the error isn't this one, then the user will receive an english command they may not understand. I'm not sure what the best solution to this is. Running the command once in english and a second time in the native language is one option
if options.onRefNotFound != nil && strings.Contains(err.Error(), "did not match any file(s) known to git") {
return options.onRefNotFound(ref)
}
if strings.Contains(err.Error(), "Please commit your changes or stash them before you switch branch") {
// offer to autostash changes
return gui.ask(askOpts{
title: gui.Tr.AutoStashTitle,
prompt: gui.Tr.AutoStashPrompt,
handleConfirm: func() error {
if err := gui.Git.Stash.Save(gui.Tr.StashPrefix + ref); err != nil {
return gui.surfaceError(err)
}
if err := gui.Git.Branch.Checkout(ref, cmdOptions); err != nil {
return gui.surfaceError(err)
}
onSuccess()
if err := gui.Git.Stash.Pop(0); err != nil {
if err := gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI}); err != nil {
return err
}
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI})
},
})
}
if err := gui.surfaceError(err); err != nil {
return err
}
}
onSuccess()
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI})
})
}
func (gui *Gui) handleCheckoutByName() error {
return gui.prompt(promptOpts{
title: gui.Tr.BranchName + ":",
findSuggestionsFunc: gui.getRefsSuggestionsFunc(),
handleConfirm: func(response string) error {
gui.logAction("Checkout branch")
return gui.handleCheckoutRef(response, handleCheckoutRefOptions{
onRefNotFound: func(ref string) error {
return gui.ask(askOpts{
title: gui.Tr.BranchNotFoundTitle,
prompt: fmt.Sprintf("%s %s%s", gui.Tr.BranchNotFoundPrompt, ref, "?"),
handleConfirm: func() error {
return gui.createNewBranchWithName(ref)
},
})
},
})
}},
)
}
func (gui *Gui) getCheckedOutBranch() *models.Branch {
if len(gui.State.Branches) == 0 {
return nil
}
return gui.State.Branches[0]
}
func (gui *Gui) createNewBranchWithName(newBranchName string) error {
branch := gui.getSelectedBranch()
if branch == nil {
return nil
}
if err := gui.Git.Branch.New(newBranchName, branch.Name); err != nil {
return gui.surfaceError(err)
}
gui.State.Panels.Branches.SelectedLineIdx = 0
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}
func (gui *Gui) handleDeleteBranch() error {
return gui.deleteBranch(false)
}
func (gui *Gui) deleteBranch(force bool) error {
selectedBranch := gui.getSelectedBranch()
if selectedBranch == nil {
return nil
}
checkedOutBranch := gui.getCheckedOutBranch()
if checkedOutBranch.Name == selectedBranch.Name {
return gui.createErrorPanel(gui.Tr.CantDeleteCheckOutBranch)
}
return gui.deleteNamedBranch(selectedBranch, force)
}
func (gui *Gui) deleteNamedBranch(selectedBranch *models.Branch, force bool) error {
title := gui.Tr.DeleteBranch
var templateStr string
if force {
templateStr = gui.Tr.ForceDeleteBranchMessage
} else {
templateStr = gui.Tr.DeleteBranchMessage
}
message := utils.ResolvePlaceholderString(
templateStr,
map[string]string{
"selectedBranchName": selectedBranch.Name,
},
)
return gui.ask(askOpts{
title: title,
prompt: message,
handleConfirm: func() error {
gui.logAction(gui.Tr.Actions.DeleteBranch)
if err := gui.Git.Branch.Delete(selectedBranch.Name, force); err != nil {
errMessage := err.Error()
if !force && strings.Contains(errMessage, "git branch -D ") {
return gui.deleteNamedBranch(selectedBranch, true)
}
return gui.createErrorPanel(errMessage)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{BRANCHES}})
},
})
}
func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
if gui.Git.Branch.IsHeadDetached() {
return gui.createErrorPanel("Cannot merge branch in detached head state. You might have checked out a commit directly or a remote branch, in which case you should checkout the local branch you want to be on")
}
checkedOutBranchName := gui.getCheckedOutBranch().Name
if checkedOutBranchName == branchName {
return gui.createErrorPanel(gui.Tr.CantMergeBranchIntoItself)
}
prompt := utils.ResolvePlaceholderString(
gui.Tr.ConfirmMerge,
map[string]string{
"checkedOutBranch": checkedOutBranchName,
"selectedBranch": branchName,
},
)
return gui.ask(askOpts{
title: gui.Tr.MergingTitle,
prompt: prompt,
handleConfirm: func() error {
gui.logAction(gui.Tr.Actions.Merge)
err := gui.Git.Branch.Merge(branchName, git_commands.MergeOpts{})
return gui.handleGenericMergeCommandResult(err)
},
})
}
func (gui *Gui) handleMerge() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
selectedBranchName := gui.getSelectedBranch().Name
return gui.mergeBranchIntoCheckedOutBranch(selectedBranchName)
}
func (gui *Gui) handleRebaseOntoLocalBranch() error {
selectedBranchName := gui.getSelectedBranch().Name
return gui.handleRebaseOntoBranch(selectedBranchName)
}
func (gui *Gui) handleRebaseOntoBranch(selectedBranchName string) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
checkedOutBranch := gui.getCheckedOutBranch().Name
if selectedBranchName == checkedOutBranch {
return gui.createErrorPanel(gui.Tr.CantRebaseOntoSelf)
}
prompt := utils.ResolvePlaceholderString(
gui.Tr.ConfirmRebase,
map[string]string{
"checkedOutBranch": checkedOutBranch,
"selectedBranch": selectedBranchName,
},
)
return gui.ask(askOpts{
title: gui.Tr.RebasingTitle,
prompt: prompt,
handleConfirm: func() error {
gui.logAction(gui.Tr.Actions.RebaseBranch)
err := gui.Git.Rebase.RebaseBranch(selectedBranchName)
return gui.handleGenericMergeCommandResult(err)
},
})
}
func (gui *Gui) handleFastForward() error {
branch := gui.getSelectedBranch()
if branch == nil || !branch.IsRealBranch() {
return nil
}
if !branch.IsTrackingRemote() {
return gui.createErrorPanel(gui.Tr.FwdNoUpstream)
}
if !branch.RemoteBranchStoredLocally() {
return gui.createErrorPanel(gui.Tr.FwdNoLocalUpstream)
}
if branch.HasCommitsToPush() {
return gui.createErrorPanel(gui.Tr.FwdCommitsToPush)
}
action := gui.Tr.Actions.FastForwardBranch
message := utils.ResolvePlaceholderString(
gui.Tr.Fetching,
map[string]string{
"from": fmt.Sprintf("%s/%s", branch.UpstreamRemote, branch.UpstreamBranch),
"to": branch.Name,
},
)
go utils.Safe(func() {
_ = gui.createLoaderPanel(message)
if gui.State.Panels.Branches.SelectedLineIdx == 0 {
_ = gui.pullWithLock(PullFilesOptions{action: action, FastForwardOnly: true})
} else {
gui.logAction(action)
err := gui.Git.Sync.FastForward(branch.Name, branch.UpstreamRemote, branch.UpstreamBranch)
gui.handleCredentialsPopup(err)
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{BRANCHES}})
}
})
return nil
}
func (gui *Gui) handleCreateResetToBranchMenu() error {
branch := gui.getSelectedBranch()
if branch == nil {
return nil
}
return gui.createResetMenu(branch.Name)
}
func (gui *Gui) handleRenameBranch() error {
branch := gui.getSelectedBranch()
if branch == nil || !branch.IsRealBranch() {
return nil
}
promptForNewName := func() error {
return gui.prompt(promptOpts{
title: gui.Tr.NewBranchNamePrompt + " " + branch.Name + ":",
initialContent: branch.Name,
handleConfirm: func(newBranchName string) error {
gui.logAction(gui.Tr.Actions.RenameBranch)
if err := gui.Git.Branch.Rename(branch.Name, newBranchName); err != nil {
return gui.surfaceError(err)
}
// need to find where the branch is now so that we can re-select it. That means we need to refetch the branches synchronously and then find our branch
gui.refreshBranches()
// now that we've got our stuff again we need to find that branch and reselect it.
for i, newBranch := range gui.State.Branches {
if newBranch.Name == newBranchName {
gui.State.Panels.Branches.SetSelectedLineIdx(i)
if err := gui.State.Contexts.Branches.HandleRender(); err != nil {
return err
}
}
}
return nil
},
})
}
// 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.
if !branch.IsTrackingRemote() {
return promptForNewName()
}
return gui.ask(askOpts{
title: gui.Tr.LcRenameBranch,
prompt: gui.Tr.RenameBranchWarning,
handleConfirm: promptForNewName,
})
}
func (gui *Gui) currentBranch() *models.Branch {
if len(gui.State.Branches) == 0 {
return nil
}
return gui.State.Branches[0]
}
func (gui *Gui) handleNewBranchOffCurrentItem() error {
context := gui.currentSideListContext()
item, ok := context.GetSelectedItem()
if !ok {
return nil
}
message := utils.ResolvePlaceholderString(
gui.Tr.NewBranchNameBranchOff,
map[string]string{
"branchName": item.Description(),
},
)
prefilledName := ""
if context.GetKey() == REMOTE_BRANCHES_CONTEXT_KEY {
// will set to the remote's branch name without the remote name
prefilledName = strings.SplitAfterN(item.ID(), "/", 2)[1]
}
return gui.prompt(promptOpts{
title: message,
initialContent: prefilledName,
handleConfirm: func(response string) error {
gui.logAction(gui.Tr.Actions.CreateBranch)
if err := gui.Git.Branch.New(sanitizedBranchName(response), item.ID()); err != nil {
return err
}
// if we're currently in the branch commits context then the selected commit
// is about to go to the top of the list
if context.GetKey() == BRANCH_COMMITS_CONTEXT_KEY {
context.GetPanelState().SetSelectedLineIdx(0)
}
if context.GetKey() != gui.State.Contexts.Branches.GetKey() {
if err := gui.pushContext(gui.State.Contexts.Branches); err != nil {
return err
}
}
gui.State.Panels.Branches.SelectedLineIdx = 0
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
},
})
}
// sanitizedBranchName will remove all spaces in favor of a dash "-" to meet
// git's branch naming requirement.
func sanitizedBranchName(input string) string {
return strings.Replace(input, " ", "-", -1)
}
-195
View File
@@ -1,195 +0,0 @@
package gui
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 := ContextKey(gui.State.Modes.CherryPicking.ContextKey)
if oldContextKey != context.GetKey() {
// need to reset the cherry picking mode
gui.State.Modes.CherryPicking.ContextKey = string(context.GetKey())
gui.State.Modes.CherryPicking.CherryPickedCommits = make([]*models.Commit, 0)
return gui.rerenderContextViewIfPresent(oldContextKey)
}
return nil
}
func (gui *Gui) handleCopyCommit() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
// get currently selected commit, add the sha to state.
context := gui.currentSideListContext()
if context == nil {
return nil
}
if err := gui.resetCherryPickingIfNecessary(context); err != nil {
return err
}
item, ok := context.GetSelectedItem()
if !ok {
return nil
}
commit, ok := item.(*models.Commit)
if !ok {
return nil
}
// we will un-copy it if it's already copied
for index, cherryPickedCommit := range gui.State.Modes.CherryPicking.CherryPickedCommits {
if commit.Sha == cherryPickedCommit.Sha {
gui.State.Modes.CherryPicking.CherryPickedCommits = append(gui.State.Modes.CherryPicking.CherryPickedCommits[0:index], gui.State.Modes.CherryPicking.CherryPickedCommits[index+1:]...)
return context.HandleRender()
}
}
gui.addCommitToCherryPickedCommits(context.GetPanelState().GetSelectedLineIdx())
return context.HandleRender()
}
func (gui *Gui) cherryPickedCommitShaMap() map[string]bool {
commitShaMap := map[string]bool{}
for _, commit := range gui.State.Modes.CherryPicking.CherryPickedCommits {
commitShaMap[commit.Sha] = true
}
return commitShaMap
}
func (gui *Gui) commitsListForContext() []*models.Commit {
context := gui.currentSideListContext()
if context == nil {
return nil
}
// using a switch statement, but we should use polymorphism
switch context.GetKey() {
case BRANCH_COMMITS_CONTEXT_KEY:
return gui.State.Commits
case REFLOG_COMMITS_CONTEXT_KEY:
return gui.State.FilteredReflogCommits
case SUB_COMMITS_CONTEXT_KEY:
return gui.State.SubCommits
default:
gui.Log.Errorf("no commit list for context %s", context.GetKey())
return nil
}
}
func (gui *Gui) addCommitToCherryPickedCommits(index int) {
commitShaMap := gui.cherryPickedCommitShaMap()
commitsList := gui.commitsListForContext()
commitShaMap[commitsList[index].Sha] = true
newCommits := []*models.Commit{}
for _, commit := range commitsList {
if commitShaMap[commit.Sha] {
// duplicating just the things we need to put in the rebase TODO list
newCommits = append(newCommits, &models.Commit{Name: commit.Name, Sha: commit.Sha})
}
}
gui.State.Modes.CherryPicking.CherryPickedCommits = newCommits
}
func (gui *Gui) handleCopyCommitRange() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
// get currently selected commit, add the sha to state.
context := gui.currentSideListContext()
if context == nil {
return nil
}
if err := gui.resetCherryPickingIfNecessary(context); err != nil {
return err
}
commitShaMap := gui.cherryPickedCommitShaMap()
commitsList := gui.commitsListForContext()
selectedLineIdx := context.GetPanelState().GetSelectedLineIdx()
if selectedLineIdx > len(commitsList)-1 {
return nil
}
// find the last commit that is copied that's above our position
// if there are none, startIndex = 0
startIndex := 0
for index, commit := range commitsList[0:selectedLineIdx] {
if commitShaMap[commit.Sha] {
startIndex = index
}
}
for index := startIndex; index <= selectedLineIdx; index++ {
gui.addCommitToCherryPickedCommits(index)
}
return context.HandleRender()
}
// HandlePasteCommits begins a cherry-pick rebase with the commits the user has copied
func (gui *Gui) HandlePasteCommits() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
return gui.ask(askOpts{
title: gui.Tr.CherryPick,
prompt: gui.Tr.SureCherryPick,
handleConfirm: func() error {
return gui.WithWaitingStatus(gui.Tr.CherryPickingStatus, func() error {
gui.logAction(gui.Tr.Actions.CherryPick)
err := gui.Git.Rebase.CherryPickCommits(gui.State.Modes.CherryPicking.CherryPickedCommits)
return gui.handleGenericMergeCommandResult(err)
})
},
})
}
func (gui *Gui) exitCherryPickingMode() error {
contextKey := ContextKey(gui.State.Modes.CherryPicking.ContextKey)
gui.State.Modes.CherryPicking.ContextKey = ""
gui.State.Modes.CherryPicking.CherryPickedCommits = nil
if contextKey == "" {
gui.Log.Warn("context key blank when trying to exit cherry picking mode")
return nil
}
return gui.rerenderContextViewIfPresent(contextKey)
}
func (gui *Gui) rerenderContextViewIfPresent(contextKey ContextKey) error {
if contextKey == "" {
return nil
}
context := gui.mustContextForContextKey(contextKey)
viewName := context.GetViewName()
view, err := gui.g.View(viewName)
if err != nil {
gui.Log.Error(err)
return nil
}
if ContextKey(view.Context) == contextKey {
if err := context.HandleRender(); err != nil {
return err
}
}
return nil
}
+7 -7
View File
@@ -22,7 +22,7 @@ import (
// So we call logAction to log the 'Stage File' part and then we call logCommand to log the command itself.
// We pass logCommand to our OSCommand struct so that it can handle logging commands
// for us.
func (gui *Gui) logAction(action string) {
func (gui *Gui) LogAction(action string) {
if gui.Views.Extras == nil {
return
}
@@ -32,7 +32,7 @@ func (gui *Gui) logAction(action string) {
fmt.Fprint(gui.Views.Extras, "\n"+style.FgYellow.Sprint(action))
}
func (gui *Gui) logCommand(cmdStr string, commandLine bool) {
func (gui *Gui) LogCommand(cmdStr string, commandLine bool) {
if gui.Views.Extras == nil {
return
}
@@ -52,23 +52,23 @@ func (gui *Gui) logCommand(cmdStr string, commandLine bool) {
func (gui *Gui) printCommandLogHeader() {
introStr := fmt.Sprintf(
gui.Tr.CommandLogHeader,
gui.getKeyDisplay(gui.UserConfig.Keybinding.Universal.ExtrasMenu),
gui.c.Tr.CommandLogHeader,
gui.getKeyDisplay(gui.c.UserConfig.Keybinding.Universal.ExtrasMenu),
)
fmt.Fprintln(gui.Views.Extras, style.FgCyan.Sprint(introStr))
if gui.UserConfig.Gui.ShowRandomTip {
if gui.c.UserConfig.Gui.ShowRandomTip {
fmt.Fprintf(
gui.Views.Extras,
"%s: %s",
style.FgYellow.Sprint(gui.Tr.RandomTip),
style.FgYellow.Sprint(gui.c.Tr.RandomTip),
style.FgGreen.Sprint(gui.getRandomTip()),
)
}
}
func (gui *Gui) getRandomTip() string {
config := gui.UserConfig.Keybinding
config := gui.c.UserConfig.Keybinding
formattedKey := func(key string) string {
return gui.getKeyDisplay(key)
+37 -257
View File
@@ -1,298 +1,78 @@
package gui
import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/controllers"
)
func (gui *Gui) getSelectedCommitFileNode() *filetree.CommitFileNode {
selectedLine := gui.State.Panels.CommitFiles.SelectedLineIdx
if selectedLine == -1 || selectedLine > gui.State.CommitFileTreeViewModel.GetItemsLength()-1 {
return nil
}
return gui.State.CommitFileTreeViewModel.GetItemAtIndex(selectedLine)
}
func (gui *Gui) getSelectedCommitFile() *models.CommitFile {
node := gui.getSelectedCommitFileNode()
if node == nil {
return nil
}
return node.File
}
func (gui *Gui) getSelectedCommitFilePath() string {
node := gui.getSelectedCommitFileNode()
if node == nil {
return ""
}
return node.GetPath()
}
// TODO: do we need this?
func (gui *Gui) onCommitFileFocus() error {
gui.escapeLineByLinePanel()
return nil
}
func (gui *Gui) commitFilesRenderToMain() error {
node := gui.getSelectedCommitFileNode()
node := gui.State.Contexts.CommitFiles.GetSelected()
if node == nil {
return nil
}
to := gui.State.CommitFileTreeViewModel.GetParent()
from, reverse := gui.getFromAndReverseArgsForDiff(to)
ref := gui.State.Contexts.CommitFiles.GetRef()
to := ref.RefName()
from, reverse := gui.State.Modes.Diffing.GetFromAndReverseArgsForDiff(ref.ParentRefName())
cmdObj := gui.Git.WorkingTree.ShowFileDiffCmdObj(from, to, reverse, node.GetPath(), false)
cmdObj := gui.git.WorkingTree.ShowFileDiffCmdObj(from, to, reverse, node.GetPath(), false)
task := NewRunPtyTask(cmdObj.GetCmd())
mainContext := gui.State.Contexts.Normal
if node.File != nil {
mainContext = gui.State.Contexts.PatchBuilding
}
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: "Patch",
task: task,
title: "Patch",
task: task,
context: mainContext,
},
secondary: gui.secondaryPatchPanelUpdateOpts(),
})
}
func (gui *Gui) handleCheckoutCommitFile() error {
node := gui.getSelectedCommitFileNode()
if node == nil {
return nil
}
func (gui *Gui) SwitchToCommitFilesContext(opts controllers.SwitchToCommitFilesContextOpts) error {
gui.State.Contexts.CommitFiles.SetSelectedLineIdx(0)
gui.State.Contexts.CommitFiles.SetRef(opts.Ref)
gui.State.Contexts.CommitFiles.SetTitleRef(opts.Ref.Description())
gui.State.Contexts.CommitFiles.SetCanRebase(opts.CanRebase)
gui.State.Contexts.CommitFiles.SetParentContext(opts.Context)
gui.State.Contexts.CommitFiles.SetWindowName(opts.Context.GetWindowName())
gui.logAction(gui.Tr.Actions.CheckoutFile)
if err := gui.Git.WorkingTree.CheckoutFile(gui.State.CommitFileTreeViewModel.GetParent(), node.GetPath()); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}
func (gui *Gui) handleDiscardOldFileChange() error {
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
if err := gui.refreshCommitFilesContext(); err != nil {
return err
}
fileName := gui.getSelectedCommitFileName()
return gui.ask(askOpts{
title: gui.Tr.DiscardFileChangesTitle,
prompt: gui.Tr.DiscardFileChangesPrompt,
handleConfirm: func() error {
return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error {
gui.logAction(gui.Tr.Actions.DiscardOldFileChange)
if err := gui.Git.Rebase.DiscardOldFileChanges(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, fileName); err != nil {
if err := gui.handleGenericMergeCommandResult(err); err != nil {
return err
}
}
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI})
})
},
})
return gui.c.PushContext(gui.State.Contexts.CommitFiles)
}
func (gui *Gui) refreshCommitFilesView() error {
currentSideContext := gui.currentSideContext()
if currentSideContext.GetKey() == COMMIT_FILES_CONTEXT_KEY || currentSideContext.GetKey() == BRANCH_COMMITS_CONTEXT_KEY {
if err := gui.handleRefreshPatchBuildingPanel(-1); err != nil {
return err
}
}
func (gui *Gui) refreshCommitFilesContext() error {
ref := gui.State.Contexts.CommitFiles.GetRef()
to := ref.RefName()
from, reverse := gui.State.Modes.Diffing.GetFromAndReverseArgsForDiff(ref.ParentRefName())
to := gui.State.Panels.CommitFiles.refName
from, reverse := gui.getFromAndReverseArgsForDiff(to)
files, err := gui.Git.Loaders.CommitFiles.GetFilesInDiff(from, to, reverse)
files, err := gui.git.Loaders.CommitFiles.GetFilesInDiff(from, to, reverse)
if err != nil {
return gui.surfaceError(err)
return gui.c.Error(err)
}
gui.State.CommitFileTreeViewModel.SetParent(to)
gui.State.CommitFileTreeViewModel.SetFiles(files)
gui.State.Model.CommitFiles = files
gui.State.Contexts.CommitFiles.CommitFileTreeViewModel.SetTree()
return gui.postRefreshUpdate(gui.State.Contexts.CommitFiles)
return gui.c.PostRefreshUpdate(gui.State.Contexts.CommitFiles)
}
func (gui *Gui) handleOpenOldCommitFile() error {
node := gui.getSelectedCommitFileNode()
func (gui *Gui) getSelectedCommitFileName() string {
node := gui.State.Contexts.CommitFiles.GetSelected()
if node == nil {
return nil
return ""
}
return gui.openFile(node.GetPath())
}
func (gui *Gui) handleEditCommitFile() error {
node := gui.getSelectedCommitFileNode()
if node == nil {
return nil
}
if node.File == nil {
return gui.createErrorPanel(gui.Tr.ErrCannotEditDirectory)
}
return gui.editFile(node.GetPath())
}
func (gui *Gui) handleToggleFileForPatch() error {
node := gui.getSelectedCommitFileNode()
if node == nil {
return nil
}
toggleTheFile := func() error {
if !gui.Git.Patch.PatchManager.Active() {
if err := gui.startPatchManager(); err != nil {
return err
}
}
// if there is any file that hasn't been fully added we'll fully add everything,
// otherwise we'll remove everything
adding := node.AnyFile(func(file *models.CommitFile) bool {
return gui.Git.Patch.PatchManager.GetFileStatus(file.Name, gui.State.CommitFileTreeViewModel.GetParent()) != patch.WHOLE
})
err := node.ForEachFile(func(file *models.CommitFile) error {
if adding {
return gui.Git.Patch.PatchManager.AddFileWhole(file.Name)
} else {
return gui.Git.Patch.PatchManager.RemoveFile(file.Name)
}
})
if err != nil {
return gui.surfaceError(err)
}
if gui.Git.Patch.PatchManager.IsEmpty() {
gui.Git.Patch.PatchManager.Reset()
}
return gui.postRefreshUpdate(gui.State.Contexts.CommitFiles)
}
if gui.Git.Patch.PatchManager.Active() && gui.Git.Patch.PatchManager.To != gui.State.CommitFileTreeViewModel.GetParent() {
return gui.ask(askOpts{
title: gui.Tr.DiscardPatch,
prompt: gui.Tr.DiscardPatchConfirm,
handleConfirm: func() error {
gui.Git.Patch.PatchManager.Reset()
return toggleTheFile()
},
})
}
return toggleTheFile()
}
func (gui *Gui) startPatchManager() error {
canRebase := gui.State.Panels.CommitFiles.canRebase
to := gui.State.Panels.CommitFiles.refName
from, reverse := gui.getFromAndReverseArgsForDiff(to)
gui.Git.Patch.PatchManager.Start(from, to, reverse, canRebase)
return nil
}
func (gui *Gui) handleEnterCommitFile() error {
return gui.enterCommitFile(OnFocusOpts{ClickedViewName: "", ClickedViewLineIdx: -1})
}
func (gui *Gui) enterCommitFile(opts OnFocusOpts) error {
node := gui.getSelectedCommitFileNode()
if node == nil {
return nil
}
if node.File == nil {
return gui.handleToggleCommitFileDirCollapsed()
}
enterTheFile := func() error {
if !gui.Git.Patch.PatchManager.Active() {
if err := gui.startPatchManager(); err != nil {
return err
}
}
return gui.pushContext(gui.State.Contexts.PatchBuilding, opts)
}
if gui.Git.Patch.PatchManager.Active() && gui.Git.Patch.PatchManager.To != gui.State.CommitFileTreeViewModel.GetParent() {
return gui.ask(askOpts{
title: gui.Tr.DiscardPatch,
prompt: gui.Tr.DiscardPatchConfirm,
handleConfirm: func() error {
gui.Git.Patch.PatchManager.Reset()
return enterTheFile()
},
})
}
return enterTheFile()
}
func (gui *Gui) handleToggleCommitFileDirCollapsed() error {
node := gui.getSelectedCommitFileNode()
if node == nil {
return nil
}
gui.State.CommitFileTreeViewModel.ToggleCollapsed(node.GetPath())
if err := gui.postRefreshUpdate(gui.State.Contexts.CommitFiles); err != nil {
gui.Log.Error(err)
}
return nil
}
func (gui *Gui) switchToCommitFilesContext(refName string, canRebase bool, context Context, windowName string) error {
// sometimes the commitFiles view is already shown in another window, so we need to ensure that window
// no longer considers the commitFiles view as its main view.
gui.resetWindowForView(gui.Views.CommitFiles)
gui.State.Panels.CommitFiles.SelectedLineIdx = 0
gui.State.Panels.CommitFiles.refName = refName
gui.State.Panels.CommitFiles.canRebase = canRebase
gui.State.Contexts.CommitFiles.SetParentContext(context)
gui.State.Contexts.CommitFiles.SetWindowName(windowName)
if err := gui.refreshCommitFilesView(); err != nil {
return err
}
return gui.pushContext(gui.State.Contexts.CommitFiles)
}
// NOTE: this is very similar to handleToggleFileTreeView, could be DRY'd with generics
func (gui *Gui) handleToggleCommitFileTreeView() error {
path := gui.getSelectedCommitFilePath()
gui.State.CommitFileTreeViewModel.ToggleShowTree()
// find that same node in the new format and move the cursor to it
if path != "" {
gui.State.CommitFileTreeViewModel.ExpandToPath(path)
index, found := gui.State.CommitFileTreeViewModel.GetIndexForPath(path)
if found {
gui.State.Contexts.CommitFiles.GetPanelState().SetSelectedLineIdx(index)
}
}
if err := gui.State.Contexts.CommitFiles.HandleRender(); err != nil {
return err
}
if err := gui.State.Contexts.CommitFiles.HandleFocus(); err != nil {
return err
}
return nil
return node.Path
}
+12 -33
View File
@@ -8,50 +8,29 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) handleCommitConfirm() error {
message := strings.TrimSpace(gui.Views.CommitMessage.TextArea.GetContent())
gui.State.failedCommitMessage = message
if message == "" {
return gui.createErrorPanel(gui.Tr.CommitWithoutMessageErr)
}
cmdObj := gui.Git.Commit.CommitCmdObj(message)
gui.logAction(gui.Tr.Actions.Commit)
_ = gui.returnFromContext()
return gui.withGpgHandling(cmdObj, gui.Tr.CommittingStatus, func() error {
gui.Views.CommitMessage.ClearTextArea()
gui.State.failedCommitMessage = ""
return nil
})
}
func (gui *Gui) handleCommitClose() error {
return gui.returnFromContext()
}
func (gui *Gui) handleCommitMessageFocused() error {
message := utils.ResolvePlaceholderString(
gui.Tr.CommitMessageConfirm,
gui.c.Tr.CommitMessageConfirm,
map[string]string{
"keyBindClose": gui.getKeyDisplay(gui.UserConfig.Keybinding.Universal.Return),
"keyBindConfirm": gui.getKeyDisplay(gui.UserConfig.Keybinding.Universal.Confirm),
"keyBindNewLine": gui.getKeyDisplay(gui.UserConfig.Keybinding.Universal.AppendNewline),
"keyBindClose": gui.getKeyDisplay(gui.c.UserConfig.Keybinding.Universal.Return),
"keyBindConfirm": gui.getKeyDisplay(gui.c.UserConfig.Keybinding.Universal.Confirm),
"keyBindNewLine": gui.getKeyDisplay(gui.c.UserConfig.Keybinding.Universal.AppendNewline),
},
)
gui.RenderCommitLength()
return gui.renderString(gui.Views.Options, message)
}
func (gui *Gui) getBufferLength(view *gocui.View) string {
return " " + strconv.Itoa(strings.Count(view.TextArea.GetContent(), "")-1) + " "
}
// RenderCommitLength is a function.
func (gui *Gui) RenderCommitLength() {
if !gui.UserConfig.Gui.CommitLength.Show {
if !gui.c.UserConfig.Gui.CommitLength.Show {
return
}
gui.Views.CommitMessage.Subtitle = gui.getBufferLength(gui.Views.CommitMessage)
gui.Views.CommitMessage.Subtitle = getBufferLength(gui.Views.CommitMessage)
}
func getBufferLength(view *gocui.View) string {
return " " + strconv.Itoa(strings.Count(view.TextArea.GetContent(), "")-1) + " "
}
+11 -787
View File
@@ -1,10 +1,6 @@
package gui
import (
"fmt"
"sync"
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -15,21 +11,16 @@ const COMMIT_THRESHOLD = 200
// list panel functions
func (gui *Gui) getSelectedLocalCommit() *models.Commit {
selectedLine := gui.State.Panels.Commits.SelectedLineIdx
if selectedLine == -1 || selectedLine > len(gui.State.Commits)-1 {
return nil
}
return gui.State.Commits[selectedLine]
return gui.State.Contexts.LocalCommits.GetSelected()
}
func (gui *Gui) onCommitFocus() error {
state := gui.State.Panels.Commits
if state.SelectedLineIdx > COMMIT_THRESHOLD && state.LimitCommits {
state.LimitCommits = false
context := gui.State.Contexts.LocalCommits
if context.GetSelectedLineIdx() > COMMIT_THRESHOLD && context.GetLimitCommits() {
context.SetLimitCommits(false)
go utils.Safe(func() {
if err := gui.refreshCommitsWithLimit(); err != nil {
_ = gui.surfaceError(err)
_ = gui.c.Error(err)
}
})
}
@@ -41,11 +32,11 @@ func (gui *Gui) onCommitFocus() error {
func (gui *Gui) branchCommitsRenderToMain() error {
var task updateTask
commit := gui.getSelectedLocalCommit()
commit := gui.State.Contexts.LocalCommits.GetSelected()
if commit == nil {
task = NewRenderStringTask(gui.Tr.NoCommitsThisBranch)
task = NewRenderStringTask(gui.c.Tr.NoCommitsThisBranch)
} else {
cmdObj := gui.Git.Commit.ShowCmdObj(commit.Sha, gui.State.Modes.Filtering.GetPath())
cmdObj := gui.git.Commit.ShowCmdObj(commit.Sha, gui.State.Modes.Filtering.GetPath())
task = NewRunPtyTask(cmdObj.GetCmd())
}
@@ -58,785 +49,18 @@ func (gui *Gui) branchCommitsRenderToMain() error {
})
}
// during startup, the bottleneck is fetching the reflog entries. We need these
// on startup to sort the branches by recency. So we have two phases: INITIAL, and COMPLETE.
// In the initial phase we don't get any reflog commits, but we asynchronously get them
// and refresh the branches after that
func (gui *Gui) refreshReflogCommitsConsideringStartup() {
switch gui.State.StartupStage {
case INITIAL:
go utils.Safe(func() {
_ = gui.refreshReflogCommits()
gui.refreshBranches()
gui.State.StartupStage = COMPLETE
})
case COMPLETE:
_ = gui.refreshReflogCommits()
}
}
// whenever we change commits, we should update branches because the upstream/downstream
// counts can change. Whenever we change branches we should probably also change commits
// e.g. in the case of switching branches.
func (gui *Gui) refreshCommits() {
wg := sync.WaitGroup{}
wg.Add(2)
go utils.Safe(func() {
gui.refreshReflogCommitsConsideringStartup()
gui.refreshBranches()
wg.Done()
})
go utils.Safe(func() {
_ = gui.refreshCommitsWithLimit()
context, ok := gui.State.Contexts.CommitFiles.GetParentContext()
if ok && context.GetKey() == BRANCH_COMMITS_CONTEXT_KEY {
// This makes sense when we've e.g. just amended a commit, meaning we get a new commit SHA at the same position.
// However if we've just added a brand new commit, it pushes the list down by one and so we would end up
// showing the contents of a different commit than the one we initially entered.
// Ideally we would know when to refresh the commit files context and when not to,
// or perhaps we could just pop that context off the stack whenever cycling windows.
// For now the awkwardness remains.
commit := gui.getSelectedLocalCommit()
if commit != nil {
gui.State.Panels.CommitFiles.refName = commit.RefName()
_ = gui.refreshCommitFilesView()
}
}
wg.Done()
})
wg.Wait()
}
func (gui *Gui) refreshCommitsWithLimit() error {
gui.Mutexes.BranchCommitsMutex.Lock()
defer gui.Mutexes.BranchCommitsMutex.Unlock()
commits, err := gui.Git.Loaders.Commits.GetCommits(
loaders.GetCommitsOptions{
Limit: gui.State.Panels.Commits.LimitCommits,
FilterPath: gui.State.Modes.Filtering.GetPath(),
IncludeRebaseCommits: true,
RefName: gui.refForLog(),
All: gui.State.ShowWholeGitGraph,
},
)
if err != nil {
return err
}
gui.State.Commits = commits
return gui.postRefreshUpdate(gui.State.Contexts.BranchCommits)
}
func (gui *Gui) refForLog() string {
bisectInfo := gui.Git.Bisect.GetInfo()
gui.State.BisectInfo = bisectInfo
bisectInfo := gui.git.Bisect.GetInfo()
gui.State.Model.BisectInfo = bisectInfo
if !bisectInfo.Started() {
return "HEAD"
}
// need to see if our bisect's current commit is reachable from our 'new' ref.
if bisectInfo.Bisecting() && !gui.Git.Bisect.ReachableFromStart(bisectInfo) {
if bisectInfo.Bisecting() && !gui.git.Bisect.ReachableFromStart(bisectInfo) {
return bisectInfo.GetNewSha()
}
return bisectInfo.GetStartSha()
}
func (gui *Gui) refreshRebaseCommits() error {
gui.Mutexes.BranchCommitsMutex.Lock()
defer gui.Mutexes.BranchCommitsMutex.Unlock()
updatedCommits, err := gui.Git.Loaders.Commits.MergeRebasingCommits(gui.State.Commits)
if err != nil {
return err
}
gui.State.Commits = updatedCommits
return gui.postRefreshUpdate(gui.State.Contexts.BranchCommits)
}
// specific functions
func (gui *Gui) handleCommitSquashDown() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
if len(gui.State.Commits) <= 1 {
return gui.createErrorPanel(gui.Tr.YouNoCommitsToSquash)
}
applied, err := gui.handleMidRebaseCommand("squash")
if err != nil {
return err
}
if applied {
return nil
}
return gui.ask(askOpts{
title: gui.Tr.Squash,
prompt: gui.Tr.SureSquashThisCommit,
handleConfirm: func() error {
return gui.WithWaitingStatus(gui.Tr.SquashingStatus, func() error {
gui.logAction(gui.Tr.Actions.SquashCommitDown)
err := gui.Git.Rebase.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "squash")
return gui.handleGenericMergeCommandResult(err)
})
},
})
}
func (gui *Gui) handleCommitFixup() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
if len(gui.State.Commits) <= 1 {
return gui.createErrorPanel(gui.Tr.YouNoCommitsToSquash)
}
applied, err := gui.handleMidRebaseCommand("fixup")
if err != nil {
return err
}
if applied {
return nil
}
return gui.ask(askOpts{
title: gui.Tr.Fixup,
prompt: gui.Tr.SureFixupThisCommit,
handleConfirm: func() error {
return gui.WithWaitingStatus(gui.Tr.FixingStatus, func() error {
gui.logAction(gui.Tr.Actions.FixupCommit)
err := gui.Git.Rebase.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "fixup")
return gui.handleGenericMergeCommandResult(err)
})
},
})
}
func (gui *Gui) handleRewordCommit() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
applied, err := gui.handleMidRebaseCommand("reword")
if err != nil {
return err
}
if applied {
return nil
}
commit := gui.getSelectedLocalCommit()
if commit == nil {
return nil
}
message, err := gui.Git.Commit.GetCommitMessage(commit.Sha)
if err != nil {
return gui.surfaceError(err)
}
// TODO: use the commit message panel here
return gui.prompt(promptOpts{
title: gui.Tr.LcRewordCommit,
initialContent: message,
handleConfirm: func(response string) error {
gui.logAction(gui.Tr.Actions.RewordCommit)
if err := gui.Git.Rebase.RewordCommit(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, response); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
},
})
}
func (gui *Gui) handleRewordCommitEditor() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
applied, err := gui.handleMidRebaseCommand("reword")
if err != nil {
return err
}
if applied {
return nil
}
gui.logAction(gui.Tr.Actions.RewordCommit)
subProcess, err := gui.Git.Rebase.RewordCommitInEditor(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx)
if err != nil {
return gui.surfaceError(err)
}
if subProcess != nil {
return gui.runSubprocessWithSuspenseAndRefresh(subProcess)
}
return nil
}
// handleMidRebaseCommand sees if the selected commit is in fact a rebasing
// commit meaning you are trying to edit the todo file rather than actually
// begin a rebase. It then updates the todo file with that action
func (gui *Gui) handleMidRebaseCommand(action string) (bool, error) {
selectedCommit := gui.State.Commits[gui.State.Panels.Commits.SelectedLineIdx]
if selectedCommit.Status != "rebasing" {
return false, nil
}
// for now we do not support setting 'reword' because it requires an editor
// and that means we either unconditionally wait around for the subprocess to ask for
// our input or we set a lazygit client as the EDITOR env variable and have it
// request us to edit the commit message when prompted.
if action == "reword" {
return true, gui.createErrorPanel(gui.Tr.LcRewordNotSupported)
}
gui.logAction("Update rebase TODO")
gui.logCommand(
fmt.Sprintf("Updating rebase action of commit %s to '%s'", selectedCommit.ShortSha(), action),
false,
)
if err := gui.Git.Rebase.EditRebaseTodo(gui.State.Panels.Commits.SelectedLineIdx, action); err != nil {
return false, gui.surfaceError(err)
}
return true, gui.refreshRebaseCommits()
}
func (gui *Gui) handleCommitDelete() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
applied, err := gui.handleMidRebaseCommand("drop")
if err != nil {
return err
}
if applied {
return nil
}
return gui.ask(askOpts{
title: gui.Tr.DeleteCommitTitle,
prompt: gui.Tr.DeleteCommitPrompt,
handleConfirm: func() error {
return gui.WithWaitingStatus(gui.Tr.DeletingStatus, func() error {
gui.logAction(gui.Tr.Actions.DropCommit)
err := gui.Git.Rebase.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "drop")
return gui.handleGenericMergeCommandResult(err)
})
},
})
}
func (gui *Gui) handleCommitMoveDown() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
index := gui.State.Panels.Commits.SelectedLineIdx
selectedCommit := gui.State.Commits[index]
if selectedCommit.Status == "rebasing" {
if gui.State.Commits[index+1].Status != "rebasing" {
return nil
}
// logging directly here because MoveTodoDown doesn't have enough information
// to provide a useful log
gui.logAction(gui.Tr.Actions.MoveCommitDown)
gui.logCommand(fmt.Sprintf("Moving commit %s down", selectedCommit.ShortSha()), false)
if err := gui.Git.Rebase.MoveTodoDown(index); err != nil {
return gui.surfaceError(err)
}
gui.State.Panels.Commits.SelectedLineIdx++
return gui.refreshRebaseCommits()
}
return gui.WithWaitingStatus(gui.Tr.MovingStatus, func() error {
gui.logAction(gui.Tr.Actions.MoveCommitDown)
err := gui.Git.Rebase.MoveCommitDown(gui.State.Commits, index)
if err == nil {
gui.State.Panels.Commits.SelectedLineIdx++
}
return gui.handleGenericMergeCommandResult(err)
})
}
func (gui *Gui) handleCommitMoveUp() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
index := gui.State.Panels.Commits.SelectedLineIdx
if index == 0 {
return nil
}
selectedCommit := gui.State.Commits[index]
if selectedCommit.Status == "rebasing" {
// logging directly here because MoveTodoDown doesn't have enough information
// to provide a useful log
gui.logAction(gui.Tr.Actions.MoveCommitUp)
gui.logCommand(
fmt.Sprintf("Moving commit %s up", selectedCommit.ShortSha()),
false,
)
if err := gui.Git.Rebase.MoveTodoDown(index - 1); err != nil {
return gui.surfaceError(err)
}
gui.State.Panels.Commits.SelectedLineIdx--
return gui.refreshRebaseCommits()
}
return gui.WithWaitingStatus(gui.Tr.MovingStatus, func() error {
gui.logAction(gui.Tr.Actions.MoveCommitUp)
err := gui.Git.Rebase.MoveCommitDown(gui.State.Commits, index-1)
if err == nil {
gui.State.Panels.Commits.SelectedLineIdx--
}
return gui.handleGenericMergeCommandResult(err)
})
}
func (gui *Gui) handleCommitEdit() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
applied, err := gui.handleMidRebaseCommand("edit")
if err != nil {
return err
}
if applied {
return nil
}
return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error {
gui.logAction(gui.Tr.Actions.EditCommit)
err = gui.Git.Rebase.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "edit")
return gui.handleGenericMergeCommandResult(err)
})
}
func (gui *Gui) handleCommitAmendTo() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
return gui.ask(askOpts{
title: gui.Tr.AmendCommitTitle,
prompt: gui.Tr.AmendCommitPrompt,
handleConfirm: func() error {
return gui.WithWaitingStatus(gui.Tr.AmendingStatus, func() error {
gui.logAction(gui.Tr.Actions.AmendCommit)
err := gui.Git.Rebase.AmendTo(gui.State.Commits[gui.State.Panels.Commits.SelectedLineIdx].Sha)
return gui.handleGenericMergeCommandResult(err)
})
},
})
}
func (gui *Gui) handleCommitPick() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
applied, err := gui.handleMidRebaseCommand("pick")
if err != nil {
return err
}
if applied {
return nil
}
// at this point we aren't actually rebasing so we will interpret this as an
// attempt to pull. We might revoke this later after enabling configurable keybindings
return gui.handlePullFiles()
}
func (gui *Gui) handleCommitRevert() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
commit := gui.getSelectedLocalCommit()
if commit.IsMerge() {
return gui.createRevertMergeCommitMenu(commit)
} else {
return gui.ask(askOpts{
title: gui.Tr.Actions.RevertCommit,
prompt: utils.ResolvePlaceholderString(
gui.Tr.ConfirmRevertCommit,
map[string]string{
"selectedCommit": commit.ShortSha(),
}),
handleConfirm: func() error {
gui.logAction(gui.Tr.Actions.RevertCommit)
if err := gui.Git.Commit.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.Git.Commit.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
gui.logAction(gui.Tr.Actions.RevertCommit)
if err := gui.Git.Commit.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}})
}
func (gui *Gui) handleViewCommitFiles() error {
commit := gui.getSelectedLocalCommit()
if commit == nil {
return nil
}
return gui.switchToCommitFilesContext(commit.Sha, true, gui.State.Contexts.BranchCommits, "commits")
}
func (gui *Gui) handleCreateFixupCommit() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
commit := gui.getSelectedLocalCommit()
if commit == nil {
return nil
}
prompt := utils.ResolvePlaceholderString(
gui.Tr.SureCreateFixupCommit,
map[string]string{
"commit": commit.Sha,
},
)
return gui.ask(askOpts{
title: gui.Tr.CreateFixupCommit,
prompt: prompt,
handleConfirm: func() error {
gui.logAction(gui.Tr.Actions.CreateFixupCommit)
if err := gui.Git.Commit.CreateFixupCommit(commit.Sha); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
},
})
}
func (gui *Gui) handleSquashAllAboveFixupCommits() error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
commit := gui.getSelectedLocalCommit()
if commit == nil {
return nil
}
prompt := utils.ResolvePlaceholderString(
gui.Tr.SureSquashAboveCommits,
map[string]string{
"commit": commit.Sha,
},
)
return gui.ask(askOpts{
title: gui.Tr.SquashAboveCommits,
prompt: prompt,
handleConfirm: func() error {
return gui.WithWaitingStatus(gui.Tr.SquashingStatus, func() error {
gui.logAction(gui.Tr.Actions.SquashAllAboveFixupCommits)
err := gui.Git.Rebase.SquashAllAboveFixupCommits(commit.Sha)
return gui.handleGenericMergeCommandResult(err)
})
},
})
}
func (gui *Gui) handleTagCommit() error {
commit := gui.getSelectedLocalCommit()
if commit == nil {
return nil
}
return gui.createTagMenu(commit.Sha)
}
func (gui *Gui) createTagMenu(commitSha string) error {
items := []*menuItem{
{
displayString: gui.Tr.LcLightweightTag,
onPress: func() error {
return gui.handleCreateLightweightTag(commitSha)
},
},
{
displayString: gui.Tr.LcAnnotatedTag,
onPress: func() error {
return gui.handleCreateAnnotatedTag(commitSha)
},
},
}
return gui.createMenu(gui.Tr.TagMenuTitle, items, createMenuOptions{showCancel: true})
}
func (gui *Gui) afterTagCreate() error {
gui.State.Panels.Tags.SelectedLineIdx = 0 // Set to the top
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{COMMITS, TAGS}})
}
func (gui *Gui) handleCreateAnnotatedTag(commitSha string) error {
return gui.prompt(promptOpts{
title: gui.Tr.TagNameTitle,
handleConfirm: func(tagName string) error {
return gui.prompt(promptOpts{
title: gui.Tr.TagMessageTitle,
handleConfirm: func(msg string) error {
gui.logAction(gui.Tr.Actions.CreateAnnotatedTag)
if err := gui.Git.Tag.CreateAnnotated(tagName, commitSha, msg); err != nil {
return gui.surfaceError(err)
}
return gui.afterTagCreate()
},
})
},
})
}
func (gui *Gui) handleCreateLightweightTag(commitSha string) error {
return gui.prompt(promptOpts{
title: gui.Tr.TagNameTitle,
handleConfirm: func(tagName string) error {
gui.logAction(gui.Tr.Actions.CreateLightweightTag)
if err := gui.Git.Tag.CreateLightweight(tagName, commitSha); err != nil {
return gui.surfaceError(err)
}
return gui.afterTagCreate()
},
})
}
func (gui *Gui) handleCheckoutCommit() error {
commit := gui.getSelectedLocalCommit()
if commit == nil {
return nil
}
return gui.ask(askOpts{
title: gui.Tr.LcCheckoutCommit,
prompt: gui.Tr.SureCheckoutThisCommit,
handleConfirm: func() error {
gui.logAction(gui.Tr.Actions.CheckoutCommit)
return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{})
},
})
}
func (gui *Gui) handleCreateCommitResetMenu() error {
commit := gui.getSelectedLocalCommit()
if commit == nil {
return gui.createErrorPanel(gui.Tr.NoCommitsThisBranch)
}
return gui.createResetMenu(commit.Sha)
}
func (gui *Gui) handleOpenSearchForCommitsPanel(string) error {
// we usually lazyload these commits but now that we're searching we need to load them now
if gui.State.Panels.Commits.LimitCommits {
gui.State.Panels.Commits.LimitCommits = false
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{COMMITS}}); err != nil {
return err
}
}
return gui.handleOpenSearch("commits")
}
func (gui *Gui) handleGotoBottomForCommitsPanel() error {
// we usually lazyload these commits but now that we're searching we need to load them now
if gui.State.Panels.Commits.LimitCommits {
gui.State.Panels.Commits.LimitCommits = false
if err := gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []RefreshableView{COMMITS}}); err != nil {
return err
}
}
for _, context := range gui.getListContexts() {
if context.GetViewName() == "commits" {
return context.handleGotoBottom()
}
}
return nil
}
func (gui *Gui) handleCopySelectedCommitMessageToClipboard() error {
commit := gui.getSelectedLocalCommit()
if commit == nil {
return nil
}
message, err := gui.Git.Commit.GetCommitMessage(commit.Sha)
if err != nil {
return gui.surfaceError(err)
}
gui.logAction(gui.Tr.Actions.CopyCommitMessageToClipboard)
if err := gui.OSCommand.CopyToClipboard(message); err != nil {
return gui.surfaceError(err)
}
gui.raiseToast(gui.Tr.CommitMessageCopiedToClipboard)
return nil
}
func (gui *Gui) handleOpenLogMenu() error {
return gui.createMenu(gui.Tr.LogMenuTitle, []*menuItem{
{
displayString: gui.Tr.ToggleShowGitGraphAll,
onPress: func() error {
gui.State.ShowWholeGitGraph = !gui.State.ShowWholeGitGraph
if gui.State.ShowWholeGitGraph {
gui.State.Panels.Commits.LimitCommits = false
}
return gui.WithWaitingStatus(gui.Tr.LcLoadingCommits, func() error {
return gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []RefreshableView{COMMITS}})
})
},
},
{
displayString: gui.Tr.ShowGitGraph,
opensMenu: true,
onPress: func() error {
onSelect := func(value string) {
gui.UserConfig.Git.Log.ShowGraph = value
gui.render()
}
return gui.createMenu(gui.Tr.LogMenuTitle, []*menuItem{
{
displayString: "always",
onPress: func() error {
onSelect("always")
return nil
},
},
{
displayString: "never",
onPress: func() error {
onSelect("never")
return nil
},
},
{
displayString: "when maximised",
onPress: func() error {
onSelect("when-maximised")
return nil
},
},
}, createMenuOptions{showCancel: true})
},
},
{
displayString: gui.Tr.SortCommits,
opensMenu: true,
onPress: func() error {
onSelect := func(value string) error {
gui.UserConfig.Git.Log.Order = value
return gui.WithWaitingStatus(gui.Tr.LcLoadingCommits, func() error {
return gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []RefreshableView{COMMITS}})
})
}
return gui.createMenu(gui.Tr.LogMenuTitle, []*menuItem{
{
displayString: "topological (topo-order)",
onPress: func() error {
return onSelect("topo-order")
},
},
{
displayString: "date-order",
onPress: func() error {
return onSelect("date-order")
},
},
{
displayString: "author-date-order",
onPress: func() error {
return onSelect("author-date-order")
},
},
}, createMenuOptions{showCancel: true})
},
},
}, createMenuOptions{showCancel: true})
}
func (gui *Gui) handleOpenCommitInBrowser() error {
commit := gui.getSelectedLocalCommit()
if commit == nil {
return nil
}
hostingServiceMgr := gui.getHostingServiceMgr()
url, err := hostingServiceMgr.GetCommitURL(commit.Sha)
if err != nil {
return gui.surfaceError(err)
}
gui.logAction(gui.Tr.Actions.OpenCommitInBrowser)
if err := gui.OSCommand.OpenLink(url); err != nil {
return gui.surfaceError(err)
}
return nil
}

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