Compare commits

..

1342 Commits

Author SHA1 Message Date
Jesse Duffield
79a72fe7a6 refactor 2022-09-22 09:03:11 -07:00
Yuki Osaki
ac8e76bee0 Merge branch 'master' into gh-integration 2022-09-07 11:49:45 +09:00
Jesse Duffield
448ff80d7d Merge pull request #2123 from Ryooooooga/feature/edit-initial-scroll 2022-08-31 22:43:06 -07:00
Jesse Duffield
74f9b8a3b4 Merge pull request #2143 from Abirdcfly/master 2022-08-31 22:42:32 -07:00
README-bot
25c4aa532e Updated README.md 2022-09-01 05:42:26 +00:00
Jesse Duffield
e7aacafc2e Merge pull request #2132 from jtraub/fix-open-gitlab-commit 2022-08-31 22:42:05 -07:00
Abirdcfly
d78d694959 chore: remove duplicate word in comments
Signed-off-by: Abirdcfly <fp544037857@gmail.com>
2022-08-30 13:23:37 +08:00
Konstantin Mikhailov
a67a08eeac Fix open commit in browser for some Gitlab repos 2022-08-24 11:23:02 +10:00
Yuki Osaki
c06974b879 Merge branch 'master' into gh-integration 2022-08-22 11:50:50 +09:00
Yuki Osaki
b5578b3816 fix reload issue 2022-08-19 13:11:43 +09:00
Yuki Osaki
93bbea1221 fix layout 2022-08-19 12:18:35 +09:00
Jesse Duffield
f6d6b5dec0 Merge pull request #2126 from yofreee/master 2022-08-19 08:30:30 +10:00
Jesse Duffield
ce98279896 Merge pull request #2124 from Ryooooooga/feature/improve-integration-test-portability 2022-08-19 08:25:50 +10:00
README-bot
e0e4138396 Updated README.md 2022-08-18 22:25:16 +00:00
Jesse Duffield
014daf7bc0 Merge pull request #2067 from nullishamy/feat/detect-bare-repo 2022-08-19 08:24:53 +10:00
Yofre Ormaza
67d6b69115 docs(readme): Added lazygit install method on ubuntu 2022-08-18 14:43:38 -05:00
nullishamy
956372cf8a Run gofumpt 2022-08-18 18:26:34 +01:00
nullishamy
290e865584 Merge branch 'master' into feat/detect-bare-repo 2022-08-18 17:46:07 +01:00
Ryooooooga
6248091e9c test: improve integration test portability 2022-08-18 23:48:53 +09:00
Ryooooooga
3ada4dde12 fix: fix initial scroll position of edit box 2022-08-18 23:39:15 +09:00
Yuki Osaki
f277ea2350 fix conflict 2022-08-16 22:07:42 +09:00
Jesse Duffield
ef82f39431 better PR template 2022-08-16 08:08:39 +10:00
nullishamy
21a4522a51 Merge branch 'master' into feat/detect-bare-repo 2022-08-15 14:00:34 +01:00
nullishamy
154bd975a6 Apply refactoring suggestions 2022-08-15 13:59:34 +01:00
Jesse Duffield
fbe54512a8 formatting 2022-08-15 20:10:25 +10:00
Jesse Duffield
7fd0e55b7f add PR template 2022-08-15 20:09:17 +10:00
Jesse Duffield
13b04e9e8c Merge pull request #2116 from jesseduffield/test-go-mod-vendor 2022-08-15 20:05:06 +10:00
Jesse Duffield
8a1937787d fix gocui mismatch 2022-08-15 20:01:43 +10:00
Jesse Duffield
a94c703afb fail on vendor directory mismatch
try this

or this

more
2022-08-15 19:55:27 +10:00
Jesse Duffield
6d7a7afbbc update test readme 2022-08-15 19:24:36 +10:00
Yuki Osaki
4a31f3fd50 Merge branch 'master' into gh-integration 2022-08-14 20:48:42 +09:00
Jesse Duffield
6abcfd5cba missed a spot 2022-08-14 21:39:07 +10:00
Jesse Duffield
a95d3e26b3 Merge pull request #2114 from jesseduffield/more-test-examples 2022-08-14 21:37:09 +10:00
Jesse Duffield
af45692e24 fix CI 2022-08-14 21:34:37 +10:00
Jesse Duffield
fed2aaf37f migrate menuFromCommand integration test 2022-08-14 21:30:37 +10:00
Jesse Duffield
b2ae651686 add slow flag to integration tests 2022-08-14 20:49:20 +10:00
Jesse Duffield
e875d6b448 ensuring you can't accidentally forget to add a test to the tests list 2022-08-14 20:49:20 +10:00
Jesse Duffield
53979f7cec a more complex custom command test 2022-08-14 20:49:20 +10:00
Jesse Duffield
9c0d860980 basic custom command test 2022-08-14 20:49:20 +10:00
Jesse Duffield
4aea005f26 Merge pull request #2098 from Ryooooooga/feature/not-a-repository-quit 2022-08-14 17:37:07 +10:00
Jesse Duffield
39e9266089 Merge pull request #2110 from mark2185/fix-ignore-or-exclude-file-menu 2022-08-14 17:35:35 +10:00
Jesse Duffield
f999c90a7b Merge pull request #2113 from jesseduffield/better-test-structure 2022-08-14 17:31:53 +10:00
Jesse Duffield
502723421b build integration binaries on CI to ensure they compile 2022-08-14 17:20:52 +10:00
Jesse Duffield
5173d7f5e1 better CLI interface 2022-08-14 17:20:52 +10:00
Jesse Duffield
349a7d2453 even better structure 2022-08-14 11:24:07 +10:00
Jesse Duffield
f3837000dd bump gocui 2022-08-13 20:12:04 +10:00
Jesse Duffield
d1b093e703 no need for this 2022-08-13 19:30:51 +10:00
README-bot
c66a2c3465 Updated README.md 2022-08-13 04:18:48 +00:00
Jesse Duffield
ba7d639940 Merge pull request #2094 from jesseduffield/better-integration-tests 2022-08-13 14:18:27 +10:00
Jesse Duffield
cad84c9e74 ensure we don't try to run another test when lazygit is invoked as a daemon 2022-08-13 14:12:35 +10:00
Jesse Duffield
5e475355bf add tests for my tests 2022-08-13 13:56:50 +10:00
Jesse Duffield
304d74370e refactor to ensure code doesn't depend on integration code 2022-08-13 13:56:50 +10:00
Jesse Duffield
2bdefe2049 add assertion to prevent flakiness 2022-08-13 13:56:50 +10:00
Jesse Duffield
faed509bfd fix CI 2022-08-13 13:56:50 +10:00
Jesse Duffield
b8d9443999 rename helpers to components 2022-08-13 13:55:17 +10:00
Jesse Duffield
de84b6c4b9 remove buggy-ass action 2022-08-13 13:55:09 +10:00
Jesse Duffield
610eddfe05 fix CI 2022-08-13 13:55:09 +10:00
Jesse Duffield
1ef6f4c0e1 renaming 2022-08-13 13:55:08 +10:00
Jesse Duffield
ae798157d2 update comments 2022-08-13 13:55:08 +10:00
Jesse Duffield
a45b22e12f re-name Input and improve documentation 2022-08-13 13:55:08 +10:00
Jesse Duffield
ba96baee32 move code from main into app package to allow test to be injected 2022-08-13 13:55:08 +10:00
Jesse Duffield
d890238c7b move input and assert into integration tests package 2022-08-13 13:52:13 +10:00
Jesse Duffield
46ae55f91e introduce gui adapter 2022-08-13 13:51:56 +10:00
Luka Markušić
0ff5b74d80 IgnoreOrExclude should be a menu 2022-08-11 14:23:02 +02:00
Jesse Duffield
225c563c63 another integration test 2022-08-11 21:24:16 +10:00
Jesse Duffield
77881a9c7d add new integration test pattern 2022-08-11 21:24:15 +10:00
Jesse Duffield
c7f9d5801b Merge pull request #2108 from eetann/fix-document-link 2022-08-10 16:04:39 +10:00
eetann
77622e5638 fix: document link 2022-08-10 13:17:07 +09:00
nullishamy
3016469708 Merge branch 'master' into feat/detect-bare-repo 2022-08-08 13:51:14 +01:00
Ryooooooga
8b371ada73 feat(config): add notARepository: quit 2022-08-08 18:11:58 +09:00
Jesse Duffield
fc49068a6f better bug report template 2022-08-08 18:53:22 +10:00
Jesse Duffield
95297f72a3 even better 2022-08-08 18:50:48 +10:00
Jesse Duffield
121872ac27 new feature request template 2022-08-08 18:46:11 +10:00
nullishamy
d072b0c75e Merge branch 'master' into feat/detect-bare-repo 2022-08-07 16:20:59 +01:00
Jesse Duffield
70a46028e1 Merge pull request #2093 from jesseduffield/fix-hidden-suggestions 2022-08-07 19:30:43 +10:00
Jesse Duffield
e4e04cfa8f fix hidden suggestions 2022-08-07 19:20:02 +10:00
Jesse Duffield
2e7b935bfb Merge pull request #2089 from jesseduffield/render-to-main-refactor 2022-08-07 13:03:28 +10:00
Jesse Duffield
71a9389ca4 Merge pull request #2087 from xxdavid/bitbucket_custom_username 2022-08-07 13:01:04 +10:00
Jesse Duffield
b77f3160f1 Merge pull request #2004 from mark2185/fix-add-to-gitignore-newline 2022-08-07 12:49:39 +10:00
Jesse Duffield
92bde6dda4 Merge pull request #2086 from luzpaz/README 2022-08-07 12:48:04 +10:00
Jesse Duffield
d73a236d7c allow rendering to main panels from anywhere 2022-08-07 12:05:51 +10:00
Jesse Duffield
fcf20f3b93 Merge pull request #2088 from jesseduffield/merge-conflict-refactor 2022-08-07 11:26:46 +10:00
Jesse Duffield
9578329cd9 update cheatsheet 2022-08-07 11:20:25 +10:00
Jesse Duffield
4c5fa83566 ensure we remove temporary directory 2022-08-07 11:16:14 +10:00
Jesse Duffield
755ae0ef84 add deadlock mutex package
write to deadlock stderr after closing gocui

more deadlock checking
2022-08-07 11:16:14 +10:00
Jesse Duffield
7410acd1aa move merge conflicts code into controller 2022-08-07 11:16:03 +10:00
David Pavlík
4db22aec0e support custom SSH usernames on BitBucket, fixes #1890 2022-08-06 22:54:57 +02:00
nullishamy
a91d977f89 Merge branch 'master' into feat/detect-bare-repo 2022-08-06 14:08:59 +01:00
Jesse Duffield
445a625b56 rename merging context to mergeConflicts 2022-08-06 18:05:00 +10:00
README-bot
cd2c01d1cf Updated README.md 2022-08-06 03:56:13 +00:00
Jesse Duffield
17316ca061 Merge pull request #2023 from jesseduffield/one-context-per-view 2022-08-06 13:55:56 +10:00
Jesse Duffield
54fb73080a use ptmx map so that we can have multiple ptmx's stored for resizing 2022-08-06 13:49:11 +10:00
Jesse Duffield
524bf83a4a refactor to only have one context per view 2022-08-06 13:49:11 +10:00
luz paz
7fcd1d2a6e fix typo in README.md
'Maintenance' is spelled incorrectly.
2022-08-05 13:25:59 -04:00
Luka Markušić
fdf6a9cc2b Test appending to empty file 2022-08-04 13:52:04 +02:00
Luka Markušić
57f86b8f90 Rerun integration test 2022-08-03 14:08:52 +02:00
Luka Markušić
6160d85d4f Use tmpdir for tests 2022-08-03 14:06:12 +02:00
Luka Markušić
64224e7caa Update pkg/commands/oscommands/os.go
Co-authored-by: Ryoga <eial5q265e5+github@gmail.com>
2022-08-03 13:57:26 +02:00
Luka Markušić
d56bb0b8ef Fix the integration test 2022-08-03 07:55:51 +02:00
Luka Markušić
d238d8952b Add AppendLineToFile tests 2022-08-03 07:55:51 +02:00
Luka Markušić
86d5654d20 Preserve trailing newline setting when adding to gitignore 2022-08-03 07:55:51 +02:00
Jesse Duffield
6dfef08efc Merge pull request #2079 from jesseduffield/fix-menu-popup-focus-issue 2022-08-02 09:21:33 +10:00
Jesse Duffield
c1c6e2fac2 make exception for searching from menu 2022-08-02 09:16:01 +10:00
nullishamy
0b4f9f8c76 Refactor branching logic 2022-08-01 21:58:00 +01:00
nullishamy
a658cd4076 Factor out opening of recent repos 2022-08-01 20:05:35 +01:00
nullishamy
69718fb557 Factor out redundant statement 2022-08-01 19:39:39 +01:00
nullishamy
b9b2f58bc8 Format, bug fixes 2022-08-01 17:41:20 +01:00
nullishamy
bdb0b9ae6e Merge branch 'master' into feat/detect-bare-repo 2022-08-01 17:05:33 +01:00
nullishamy
2866827ca8 Apply suggestions from code review 2022-08-01 17:05:16 +01:00
Jesse Duffield
932b0b593e add integration test to ensure we don't run into issues with popup focus 2022-08-01 22:17:06 +10:00
Jesse Duffield
3ee2ad511e prevent crash when opening recent repos panel if no recent repos are present 2022-08-01 22:10:08 +10:00
Jesse Duffield
debc58b6c5 fix popup focus issue 2022-08-01 22:09:48 +10:00
Jesse Duffield
fab2e14b55 fix issue caused by opening a menu over a prompt 2022-08-01 21:38:57 +10:00
Jesse Duffield
81f80ce968 Merge pull request #2077 from jesseduffield/file-node-fix
handle nil properly with file nodes
2022-08-01 20:36:26 +10:00
Jesse Duffield
95426c5e46 handle nil properly with file nodes 2022-08-01 20:32:01 +10:00
Jesse Duffield
4ffc9a5395 Merge pull request #2076 from jesseduffield/build-info 2022-08-01 20:22:27 +10:00
Jesse Duffield
86ac309e08 add build info when building from source 2022-08-01 20:16:50 +10:00
nullishamy
9987e65c35 Merge branch 'master' into feat/detect-bare-repo 2022-08-01 03:14:49 +01:00
Jesse Duffield
69f4292fe3 Merge pull request #2075 from jesseduffield/generic-trees 2022-07-31 19:47:06 +10:00
Jesse Duffield
682be18507 refactor to use generics for file nodes
use less generic names
2022-07-31 19:43:14 +10:00
Jesse Duffield
2ca2acaca5 Merge pull request #2072 from jesseduffield/optimistic-file-rendering 2022-07-31 19:31:23 +10:00
Jesse Duffield
5f4c29d7b5 Merge pull request #2005 from mark2185/feature/recent-repos-path
Show active branch for recent repo
2022-07-31 19:30:30 +10:00
Luka Markušić
44de380c2b Add i18n for unknown branch 2022-07-31 08:57:57 +02:00
Luka Markušić
37bdbd9a21 Display short SHA when in detached HEAD state 2022-07-31 08:51:27 +02:00
Luka Markušić
767ef31661 Use filepath.Join instead of manual concat 2022-07-31 08:43:31 +02:00
README-bot
bb64fe4309 Updated README.md 2022-07-31 06:20:16 +00:00
Jesse Duffield
c81c046615 Merge pull request #2059 from sportshead/master 2022-07-31 16:19:59 +10:00
Jesse Duffield
a905165046 remove double negatives 2022-07-31 16:11:39 +10:00
Jesse Duffield
7077ea428f add optimistic rendering for staging and unstaging files 2022-07-31 14:34:57 +10:00
sportshead
f2880ecb46 Add empty output message and refreshing to showOutput 2022-07-31 12:04:40 +08:00
Luka Markušić
966733240c Refactor a bit, enable worktrees 2022-07-30 19:02:19 +02:00
Luka Markušić
25ddac0d8f Gotta go fast 2022-07-30 17:42:15 +02:00
Luka Markušić
7c09ce3871 Parallelize fetching current branch 2022-07-30 17:05:17 +02:00
Luka Markušić
e6e4513f45 Show active branch for recent repo
Split recent repo menu into three columns
2022-07-30 17:05:17 +02:00
Luka Markušić
9c6239df3d Worktrees should not be filtered out
But non-git directories should
2022-07-30 17:03:30 +02:00
Jesse Duffield
c26650258d Merge pull request #2064 from jesseduffield/fix-update-gocui
Update gocui
2022-07-30 21:08:47 +10:00
Jesse Duffield
c943cdd8d2 Merge pull request #2071 from jesseduffield/ellipsis-with-menu
Add better support for OpensMenu option when creating a menu
2022-07-30 20:40:54 +10:00
Jesse Duffield
ab5a8091f5 add better support for OpensMenu option when creating a menu 2022-07-30 20:27:51 +10:00
Jesse Duffield
95678b1b43 Merge pull request #2065 from jesseduffield/fix-rewording-merge-commit-with-editor
Fix rewording a merge commit with editor
2022-07-30 19:51:59 +10:00
Jesse Duffield
f947214ce0 Merge pull request #2070 from mark2185/feature/document-SelectedPath
Add SelectedPath to the list of placeholder values
2022-07-30 19:50:01 +10:00
Jesse Duffield
8c65f63e76 Merge pull request #2062 from jesseduffield/mark2185-patch-1
Update bug_report.md
2022-07-30 19:48:58 +10:00
Luka Markušić
085904d209 Add link to the installation instructions 2022-07-30 08:46:53 +02:00
Luka Markušić
cc377b5c49 Fix rewording a merge commit with editor 2022-07-30 08:43:42 +02:00
README-bot
1f97ddd9b1 Updated README.md 2022-07-30 06:38:57 +00:00
Jesse Duffield
5cef0f2921 Merge pull request #2069 from mark2185/fix-github-linter
Fix github linter errors
2022-07-30 16:38:41 +10:00
Luka Markušić
61e14c46b2 Add SelectedPath to the list of placeholder values 2022-07-30 08:25:40 +02:00
Luka Markušić
1f482e585e Fix github linter errors 2022-07-30 08:10:29 +02:00
nullishamy
41b54d742f Check for bare repositories 2022-07-29 23:55:34 +01:00
Luka Markušić
83dfc3b28b Update gocui 2022-07-29 06:59:52 +02:00
Luka Markušić
017d44ff17 Update bug_report.md
Add the `please try updating` tidbit to the bug report because that's one of the most common answers (and it cuts down debugging time)
2022-07-28 21:17:15 +02:00
sportshead
168fbe0a6c Add showOutput option to docs 2022-07-28 18:57:16 +08:00
sportshead
e1f41b653c Add showOutput option to custom commands (#1163) 2022-07-28 18:40:30 +08:00
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ć
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
Ram Bhosale
7be25a105d allow skipping confirmation prompt after opening subprocess 2022-03-17 17:52:31 +11:00
Jesse Duffield
28c9d85141 fix tests 2022-03-16 20:52:49 +11:00
tiwood
f0d0d45ba7 refactor: 💡 Use new approach introduced via #1637
fix: 🐛 The root URI for Azure DevOps repositories contains _git

refactor so that we don't have conditional logic based on service definition

no need for this commend anymore

add comment

Fixed RegEx for HTTP remote git URL

Added Tests

pretty sure we can do this safely
2022-03-16 20:52:49 +11:00
Moritz Haase
08ee3309cb docs: Let 'Tag' badge in README.md link to Github 'Releases' page
Clicking the badge that shows the current tag (i.e. release) will now direct you
to the 'Releases' page.
2022-03-16 20:44:49 +11:00
Moritz Haase
7544d853fc docs: Remove 'GolangCI' badge from README.md
The service has apparently closed down some time ago.

See: https://medium.com/golangci/golangci-com-is-closing-d1fc1bd30e0e
2022-03-16 20:44:49 +11:00
Francisco Miamoto
ca8180e1b7 Use editFileAtLine method for line by line panel 2022-03-16 20:43:53 +11:00
Jesse Duffield
f53b10072d open code in existing window 2022-03-16 19:55:58 +11:00
Jesse Duffield
11acac0091 more explicit 2022-03-16 19:55:58 +11:00
Luka Markušić
866f4b9f0e Support line offset for most common editors by default 2022-03-16 19:55:58 +11:00
Jesse Duffield
f56988039a ignore current user language when generating cheatsheets 2022-03-16 19:46:11 +11:00
Akash Soedamah
7e6b43d13b Just spotted some small typos in NL translation
I'm learning the LazyGit keybindings right now
2022-03-16 19:46:11 +11:00
Daniel Kiss
f5a5b7f966 Add unstagedChangesColor config option 2022-03-16 19:21:39 +11:00
Moritz Haase
91dab7fef9 docs: Update 'VS Code launch configuration' in contribution guide
To simplify debugging via VS Code, add '--debug' argument to example launch
configuration. Also, reword the hint regarding the 'console' key slightly. It
has apparently been available for a long time in stable releases and is also
listed in the documentation. It is however, still marked as an experimental
feature, even in the current release v1.65.2.

See: https://code.visualstudio.com/updates/v1_5#_launch-debug-target-in-integrated-terminal
See: https://code.visualstudio.com/Docs/editor/debugging#_launchjson-attributes
2022-03-15 19:42:52 +11:00
Moritz Haase
75e654634f docs: Update link to GoDoc in README.md to use HTTPS instead of HTTP 2022-03-15 19:42:23 +11:00
Moritz Haase
c9f1a40ffa docs: Minor formatting fixes for README.md
Running 'mdformat' on README.md adds and removes a few line breaks to improve
readability. Also, '$' doesn't need to be escaped.
2022-03-15 19:42:23 +11:00
Moritz Haase
eb182abf48 docs: Fix typos in README.md 2022-03-15 19:42:23 +11:00
MATSUDA Takashi
2c7c99522b chore: go mod vendor 2022-03-15 13:31:11 +11:00
MATSUDA Takashi
a9fa3b2af1 chore: go get github.com/gdamore/tcell/v2@2a1a1b58 2022-03-15 13:31:11 +11:00
Yuki Osaki
c3d2722cb8 fix conflicts 2022-02-25 11:50:59 +09:00
Yuki Osaki
ed4e7ac960 Merge branch 'master' into gh-integration 2022-02-25 09:01:03 +09:00
Jesse Duffield
f4011643dd whoops 2022-02-05 19:14:08 +11:00
Mark Kopenga
f8b8307a29 Merge pull request #1757 from bnoctis/i18n/polish-intro
Fix & polish Chinese intro message
2022-02-03 10:21:29 +01:00
Blair Noctis
471fe313d8 fix & polish Chinese intro message
- Fix first sentence of 3), #1756
- Use 你 instead of 您 to pose a closer feel, as fellow programmers
- Polish a little so it feels more natural
2022-02-02 18:59:15 +08:00
Matt Cles
9adf4a1908 Add shared function for loading map of custom colors 2022-02-01 18:55:45 +11:00
Matt Cles
4df7646654 Add configurable colors for branch prefixes
Branches can now be colored based on their prefix, if it matches
a user defined prefix in the config file. If no user defined
prefix matches, then it will fallback to the defaults: green for
'feature', yellow for 'bugfix', and red for 'hotfix'. All
remaining branches will be set to the default text color.
2022-02-01 18:55:45 +11:00
Mark Kopenga
c7c4a375a9 Merge pull request #1750 from mark2185/fix-issue-template
'git-rev parse' should be 'git rev-parse'
2022-01-31 19:24:20 +01:00
Luka Markušić
a2fd3541d5 'git-rev parse' should be 'git rev-parse' 2022-01-31 18:07:35 +01:00
Jing Mi
15ca38ba2e Update README.md
Use `go install` instead of `go get` to install as `$GOPATH/bin/lazygit`, since using `go get` to install binary is deprecated
2022-01-31 08:59:55 +11:00
Jesse Duffield
e0ae134ee4 generate snapshot for expected dir in separate tmp dir 2022-01-29 00:17:32 +11:00
Jesse Duffield
1d90e1b565 add submodule integration tests 2022-01-29 00:17:32 +11:00
Jesse Duffield
1b09674ce8 simplify submodule remove 2022-01-29 00:17:32 +11:00
Jesse Duffield
d13a648132 ensure stash panel refreshes 2022-01-28 20:07:30 +11:00
Jesse Duffield
bed185eb28 stop retrying due to index lock for now 2022-01-27 21:25:04 +11:00
Jesse Duffield
84a1992055 better locking of merge panel state 2022-01-27 21:25:04 +11:00
Jesse Duffield
7f85bf5563 Update CONTRIBUTING.md 2022-01-27 19:32:30 +11:00
Jesse Duffield
3e21143a0e add debugging section to contributor guide 2022-01-27 19:30:25 +11:00
Jesse Duffield
fa2e7ae1e7 show only merge conflict files when there are merge conflicts 2022-01-26 20:28:32 +11:00
Jesse Duffield
5a3f81d1f7 select current bisect commit even if bisect was started on another branch 2022-01-26 19:29:17 +11:00
Jesse Duffield
ebbdf829e7 fix panic on rebase 2022-01-26 17:20:58 +11:00
Jesse Duffield
5e6e1617aa add another bisect integration test 2022-01-26 16:52:20 +11:00
Jesse Duffield
5e9cfab283 better rendering of bisect markets in commits panel 2022-01-26 16:52:20 +11:00
Jesse Duffield
ca7cfc3232 only show commits from start ref if bad commit is reachable from there 2022-01-26 16:52:20 +11:00
Jesse Duffield
dc765c4166 add a file close that was missed 2022-01-26 14:50:47 +11:00
Jesse Duffield
c8cc18920f improve merge conflict flow 2022-01-26 14:50:47 +11:00
Jesse Duffield
ce3bcfe37c fix reflog failing to properly refresh 2022-01-26 10:58:33 +11:00
Jesse Duffield
f4ddf2f0d4 redo commit revert integration test 2022-01-26 09:23:55 +11:00
Jesse Duffield
54b1bc31cd allow running integration tests at original speed 2022-01-26 09:23:55 +11:00
glendsoza
eb57e3ead0 Fixed the issue with linting 2022-01-26 09:04:12 +11:00
glendsoza
0caa391c4d Changes as per review 2022-01-26 09:04:12 +11:00
glendsoza
0c6bdac2f7 Changes as per review 2022-01-26 09:04:12 +11:00
glendsoza
257e222f8d ISSUE 1706: Ask confirmation before reverting a commit 2022-01-26 09:04:12 +11:00
Mikael Elkiaer
874e230aef run go fmt 2022-01-25 23:23:55 +11:00
Mikael Elkiaer
4da5795ef1 fixed indentation by swapping spaces for tabs 2022-01-25 23:23:55 +11:00
Mikael Elkiaer
03c9acad26 add tests specific for URL escaping in PRs 2022-01-25 23:23:55 +11:00
Mikael Elkiaer
d53322675d update unit tests not expecting url escaping 2022-01-25 23:23:55 +11:00
Mikael Elkiaer
ae18ad5b66 add URL encoding in pull request branch names 2022-01-25 23:23:55 +11:00
MATSUDA Takashi
b70075eba6 go mod vendor 2022-01-25 22:54:09 +11:00
MATSUDA Takashi
e413c216ba go get github.com/gdamore/tcell/v2@66f061b1 2022-01-25 22:54:09 +11:00
Jesse Duffield
14b9a0b647 stop skipping stash warnings 2022-01-24 19:18:09 +11:00
Jesse Duffield
58bdcbf1dd always refresh after stash action 2022-01-24 19:18:09 +11:00
Jesse Duffield
88d685df53 better bisect script 2022-01-23 14:41:48 +11:00
Jesse Duffield
61ccc1efd2 exclude interactive rebase TODO commits from commit graph 2022-01-22 15:12:24 +11:00
Jesse Duffield
5b7dd9e43c properly resolve cyclic dependency 2022-01-22 10:48:51 +11:00
Jesse Duffield
4ab5e54139 add support for git bisect 2022-01-22 10:48:51 +11:00
Birger Skogeng Pedersen
ab84410b41 check returned error (if any) from UpdateWindowTitle 2022-01-21 23:13:39 +11:00
Birger Skogeng Pedersen
a78cbf4882 remove redundant title-setting shell command args 2022-01-21 23:13:39 +11:00
Birger Skogeng Pedersen
62a7d9bbcc invoke title-setting shell command appropriately 2022-01-21 23:13:39 +11:00
Birger Skogeng Pedersen
555d8bbc96 set repo name as window title when loading repo, fix #1691 2022-01-21 23:13:39 +11:00
bin101
ad23bd03a0 fix: custom service usage 2022-01-21 23:13:00 +11:00
Jesse Duffield
1f923bdc4b softer auto-generation message 2022-01-19 21:40:50 +11:00
Jesse Duffield
b5a8ecf786 update contributing docs 2022-01-18 22:06:17 +11:00
Jesse Duffield
3e80a9e886 refactor to group up more commonly used git command stuff 2022-01-18 22:01:09 +11:00
Jesse Duffield
9706416a41 the gods will judge me 2022-01-18 21:42:23 +11:00
Jesse Duffield
56f2ecb06c another integration test 2022-01-18 21:25:52 +11:00
Jesse Duffield
d7c79ba20b fix integration test which was actually asserting incorrect behaviour 2022-01-18 21:25:52 +11:00
Jesse Duffield
b6fb7f1365 fix integration test 2022-01-18 21:25:52 +11:00
Jesse Duffield
dbb8b17d83 add integration test for deleting a range of lines in the staging panel 2022-01-18 21:25:52 +11:00
Jesse Duffield
d019626342 do not show branch graph when in filtering mode 2022-01-17 22:00:53 +11:00
Jesse Duffield
595aca2a4b make integration test pass 2022-01-17 19:14:59 +11:00
Jesse Duffield
2691477aff allow sandbox mode with integration tests 2022-01-17 19:14:59 +11:00
Jesse Duffield
8ca71eeb36 add git bisect run script 2022-01-17 19:14:59 +11:00
Jesse Duffield
d3a3c8d87d add integration test for merge conflicts resolved externally 2022-01-17 19:14:59 +11:00
Jesse Duffield
ee622d044e add integration test for staging view 2022-01-17 19:14:59 +11:00
Jesse Duffield
99035959a1 fix merge scroll bug 2022-01-16 23:16:05 +11:00
Jesse Duffield
0092c9d08d fix bug with subprocess 2022-01-16 03:32:09 +00:00
Jesse Duffield
befa35645e fix bug which prevented quitting with confirm 2022-01-15 20:35:25 +11:00
Jesse Duffield
7a690f9078 appease CI 2022-01-15 15:34:01 +11:00
Jesse Duffield
dafac52a4c see if this fixes CI linting 2022-01-15 15:34:01 +11:00
Jesse Duffield
1c84f77319 always specify upstream when pushing/pulling 2022-01-15 15:34:01 +11:00
Jesse Duffield
8d8bdb948b avoid deadlock in merge panel 2022-01-15 14:15:41 +11:00
Jesse Duffield
cdcfeb396f stop refreshing the screen so much 2022-01-15 14:15:41 +11:00
Jesse Duffield
f5b9ad8c00 add complex custom command integration test 2022-01-15 10:14:19 +11:00
Jesse Duffield
8263d15b03 fix issue where custom command would not open a menu 2022-01-15 10:14:19 +11:00
TicClick
4744b39f03 work with absolute paths when invoked with --path 2022-01-09 14:35:57 +11:00
Jesse Duffield
2436ff197a fewer panics 2022-01-09 14:14:47 +11:00
Jesse Duffield
3b30b9bba2 add integration test for rewording old commit 2022-01-09 14:14:47 +11:00
Jesse Duffield
e5096e71ab add integration test for discarding old files 2022-01-09 14:14:47 +11:00
Jesse Duffield
ceb927fec0 fix formatting 2022-01-09 14:14:47 +11:00
Jesse Duffield
0dfd02c42d allow rewording old commits 2022-01-09 14:14:47 +11:00
Jesse Duffield
ee15202207 add newline after message because it looks like the message doesn't appear otherwise for some reason 2022-01-09 14:09:53 +11:00
Jesse Duffield
a936c0592f more refactoring 2022-01-09 14:09:53 +11:00
Jesse Duffield
06687c8a59 add integration test for entering credentials 2022-01-09 14:09:53 +11:00
Jesse Duffield
4d80c87736 use a string builder for credential checking 2022-01-09 14:09:53 +11:00
Jesse Duffield
267ecbe694 refactor code for handling credential requests 2022-01-09 14:09:53 +11:00
Jesse Duffield
ccf90466fa fix test 2022-01-09 14:09:53 +11:00
Jesse Duffield
16c9b5404d restore field 2022-01-09 14:09:53 +11:00
Jesse Duffield
18f48a43d5 add some more linters 2022-01-09 14:09:53 +11:00
Jesse Duffield
5d6d894286 fix test 2022-01-09 14:09:53 +11:00
Jesse Duffield
e4e521f58a pass repo to struct 2022-01-09 14:09:53 +11:00
Jesse Duffield
fdf79fdeee fix bug that caused credentials popup to be raised unexpectedly 2022-01-09 14:09:53 +11:00
Jesse Duffield
0dd1c12e2f fix format issue 2022-01-09 14:09:53 +11:00
Jesse Duffield
364c5db19c shorten name 2022-01-09 14:09:53 +11:00
Jesse Duffield
c9a0cc6b30 refactor 2022-01-09 14:09:53 +11:00
Jesse Duffield
3621854dc7 fix tests 2022-01-09 14:09:53 +11:00
Jesse Duffield
c6b57d9b57 WIP 2022-01-09 14:09:53 +11:00
Jesse Duffield
a7a61cdc83 rearrange 2022-01-09 14:09:53 +11:00
Jesse Duffield
ee8ff6512f trim down gitcommand struct some more 2022-01-09 14:09:53 +11:00
Jesse Duffield
e8229f0ee0 support general git config calls 2022-01-09 14:09:53 +11:00
Jesse Duffield
610e503296 refactor git flow 2022-01-09 14:09:53 +11:00
Jesse Duffield
e92076d2c2 start removing direct calls to cmd.New from gui 2022-01-09 14:09:53 +11:00
Jesse Duffield
d9089098c3 remove field 2022-01-09 14:09:53 +11:00
Jesse Duffield
3f44eac95b remove repo field 2022-01-09 14:09:53 +11:00
Jesse Duffield
946a35b59d remove OSCommand field 2022-01-09 14:09:53 +11:00
Jesse Duffield
007235df23 refactor 2022-01-09 14:09:53 +11:00
Jesse Duffield
f503ff1ecb start breaking up git struct 2022-01-09 14:09:53 +11:00
Jesse Duffield
4a1d23dc27 bump gocui 2022-01-08 16:05:11 +11:00
Pieter van Loon
7539929703 use bright colors for highlighting 2022-01-08 16:05:11 +11:00
Jesse Duffield
48a4565d1f remove log of error now that we're returning it normally 2022-01-07 11:01:42 +11:00
Jakob Kogler
673c4a1296 also test for empty prefill after successful commit 2022-01-07 11:01:42 +11:00
Jakob Kogler
ee7a6391a8 remove redundant commit 2022-01-07 11:01:42 +11:00
Jakob Kogler
68fc6059d3 rename variable to failedCommitMessage 2022-01-07 11:01:42 +11:00
Jakob Kogler
d517531c16 test remembering the commit message for a failing commit 2022-01-07 11:01:42 +11:00
Jakob Kogler
f981255a5b don't ignore error when commit with subprocess fails
If signing by GPG is enabled, the git commit command will be executed in
a subprocess, differently from when it is executed without GPG signing.
In case of an error, e.g. a failing pre-commit hook, the error needs to
be passed along, and not just ignored.
2022-01-07 11:01:42 +11:00
Jakob Kogler
beedc2553d remember the message if commit fails
In case a commit fails, e.g. because a pre-commit hook returns an error,
lazygit will now remember the commit message and will suggest it during
the next commit (e.g. after fixing the error of the pre-commit hook).
2022-01-07 11:01:42 +11:00
Jesse Duffield
0d3e5e6a1d simplify fetch 2022-01-07 10:52:51 +11:00
Jesse Duffield
93729ba61b fix some things 2022-01-07 10:52:51 +11:00
Jesse Duffield
91fe68576c refactor 2022-01-07 10:52:51 +11:00
Jesse Duffield
bbb5eee23a privatise some fields 2022-01-07 10:52:51 +11:00
Jesse Duffield
05fa483f48 simplify how we log commands 2022-01-07 10:52:51 +11:00
Jesse Duffield
e524e39842 move lint into ci yaml 2022-01-04 11:12:04 +11:00
Jesse Duffield
e8a1a4ffc0 add cheatsheet check script 2022-01-04 11:12:04 +11:00
Jesse Duffield
8e66d2761e make it clear that keybinding cheat sheets are auto-generated 2022-01-04 09:33:35 +11:00
Jesse Duffield
95b2e9540a update tests 2022-01-04 09:07:15 +11:00
Jesse Duffield
3911575041 appease golangci-lint 2022-01-04 09:07:15 +11:00
Jesse Duffield
efa743b52e small change 2022-01-04 09:07:15 +11:00
Jesse Duffield
6da6c1f2f2 small copy changes 2022-01-04 09:07:15 +11:00
Jesse Duffield
c82606a92a fix broken format call 2022-01-04 09:07:15 +11:00
Jesse Duffield
194ff1630c do dependency injection up front and in one place 2022-01-04 09:07:15 +11:00
Jesse Duffield
2cb8aff940 no more mocking command 2022-01-04 09:07:15 +11:00
Jesse Duffield
25195eacee WIP 2022-01-04 09:07:15 +11:00
Jesse Duffield
ad9b2df104 more test refactoring 2022-01-04 09:07:15 +11:00
Jesse Duffield
9c4a819683 refactor sync test 2022-01-04 09:07:15 +11:00
Jesse Duffield
38bc48312e refactor files_test.go 2022-01-04 09:07:15 +11:00
Jesse Duffield
547e0153ec stash and tags loaders 2022-01-04 09:07:15 +11:00
Jesse Duffield
44b6d26b10 move remotes loader into loaders package 2022-01-04 09:07:15 +11:00
Jesse Duffield
d69ce7a529 move reflog commit loader into loaders package 2022-01-04 09:07:15 +11:00
Jesse Duffield
9b2b0fc122 WIP 2022-01-04 09:07:15 +11:00
Jesse Duffield
96c2887fd0 WIP 2022-01-04 09:07:15 +11:00
Jesse Duffield
66e840bc3f more refactoring 2022-01-04 09:07:15 +11:00
Jesse Duffield
5b35724243 WIP 2022-01-04 09:07:15 +11:00
Jesse Duffield
b028f37ba8 updating specs 2022-01-04 09:07:15 +11:00
Jesse Duffield
1fc0d786ae better typing for rebase mode 2022-01-04 09:07:15 +11:00
Jesse Duffield
9d4ff6b465 fix logging 2022-01-04 09:07:15 +11:00
Jesse Duffield
95f4ceea34 refactor 2022-01-04 09:07:15 +11:00
Jesse Duffield
43a4fa970d WIP 2022-01-04 09:07:15 +11:00
Jesse Duffield
192a548c99 refactoring the config struct 2022-01-04 09:07:15 +11:00
Jesse Duffield
01ea5813a8 align Gui struct with GitCommand 2022-01-04 09:07:15 +11:00
Jesse Duffield
03b946cc8f no more config in git command struct 2022-01-04 09:07:15 +11:00
Jesse Duffield
18ab086126 introduce Common struct for passing around common stuff 2022-01-04 09:07:15 +11:00
Jesse Duffield
b4c078d565 WIP 2022-01-04 09:07:15 +11:00
Jesse Duffield
157dd309f7 fix integration test 2022-01-03 15:26:53 +11:00
Jesse Duffield
9ef65574db refactor to rename pull_request to hosting_service and apply SRP 2021-12-29 09:01:06 +11:00
Jesse Duffield
f89747451a allow opening a commit in the browser 2021-12-29 09:01:06 +11:00
Francisco Miamoto
8a76b5a4ee use custom handler only for branch commits context 2021-12-28 15:54:19 +11:00
Francisco Miamoto
1a7d0cd7ae add binding to go to bottom on lists
Related to #1584
2021-12-28 15:54:19 +11:00
Francisco Miamoto
2696a63a0a fix pushTag test by adding extra enter 2021-12-26 17:08:31 +11:00
Francisco Miamoto
8c8b925b3a set tag index directly
We can do this since they are already sorted by date created.
2021-12-26 17:08:31 +11:00
Francisco Miamoto
b8735cc609 fix tags integration tests 2021-12-26 17:08:31 +11:00
Francisco Miamoto
517dab7d05 add annotated tag integration test 2021-12-26 17:08:31 +11:00
Francisco Miamoto
e5f0301c66 update docs on integration tests
The instructions provided were not working as expected.
2021-12-26 17:08:31 +11:00
Francisco Miamoto
eff6c4283b change formatting for menu items 2021-12-26 17:08:31 +11:00
Francisco Miamoto
7888ff6cb9 set show cancel as true for tag menu 2021-12-26 17:08:31 +11:00
Francisco Miamoto
e7a005f44d update translations to match conventions 2021-12-26 17:08:31 +11:00
Francisco Miamoto
3e58797096 show tag menu for creation on tags tab 2021-12-26 17:08:31 +11:00
Francisco Miamoto
b1d6ccddfb support creating annotated tags 2021-12-26 17:08:31 +11:00
Jesse Duffield
4df003cc44 handle ssh protocol 2021-12-26 17:05:05 +11:00
Jesse Duffield
d9db5ccfbe refactor to use regex for matching git service URL 2021-12-26 16:48:23 +11:00
Jesse Duffield
7bc8b96aba add FindNamedMatches function in utils 2021-12-26 16:48:23 +11:00
Marius Bergmann
38743ec99f Suggest existing remote for non-tracking branch
Currently, when pushing or pulling a branch that has no tracking remote,
lazygit suggests the (hard-coded) remote named 'origin'. However, a
repository might not have a remote with this name, in which case the
suggestion makes no sense. This happens to me quite regularly when I
choose a more meaningful name than 'origin' for a remote.

This change keeps the current behavior by suggesting 'origin' when there
is either a remote with that name or no remote at all. However, when
'origin' does not exist, the name of the first remote is suggested.

Suggest existing remote for non-tracking branch

Currently, when pushing or pulling a branch that has no tracking remote,
lazygit suggests the (hard-coded) remote named 'origin'. However, a
repository might not have a remote with this name, in which case the
suggestion makes no sense. This happens to me quite regularly when I
choose a more meaningful name than 'origin' for a remote.

This change keeps the current behavior by suggesting 'origin' when there
is either a remote with that name or no remote at all. However, when
'origin' does not exist, the name of the first existing remote is
suggested.
2021-12-26 15:47:58 +11:00
justinsb
630de34bf2 Use "reword" for amending a commit message everywhere
We were inconsistent about "rename" vs "reword" for commits.  reword
is the term used in git itself (for example, in rebase).
2021-12-25 22:34:41 +11:00
Tsvetomir Bonev
fca2f90f57 chore: add funtoo linux instructions 2021-12-25 22:26:46 +11:00
Mark Sagi-Kazar
fdf0d4a2c3 implement signoff
Signed-off-by: Mark Sagi-Kazar <mark.sagikazar@gmail.com>
2021-12-25 12:01:55 +11:00
Mark Sagi-Kazar
b4ea565c99 add signoff config
Signed-off-by: Mark Sagi-Kazar <mark.sagikazar@gmail.com>
2021-12-25 12:01:55 +11:00
Cokile
76e6745526 fix typo 2021-12-25 11:54:27 +11:00
Cokile
3771f9c98b support config unified color for commit authors 2021-12-25 11:54:27 +11:00
Chris Burgin
4f627e5d27 wrap branch names in quotes 2021-12-25 11:50:06 +11:00
Erik Scrafford
089e3bf4fe change branch regex to only grab the first remote on each line of branch command 2021-12-23 19:30:47 +11:00
Mark Kopenga
d5c1cfda4b Merge pull request #1624 from sirodoht/fix-filetree-docs
Fix `showFileTree` default value in docs
2021-12-13 14:34:30 +01:00
Theodore Keloglou
3926d0f278 fix show filetree default value in docs 2021-12-13 13:28:14 +00:00
Jesse Duffield
18283ad41b add popup handler for easier testing 2021-12-06 22:37:28 +11:00
Jesse Duffield
1996eddd91 more efficient context diff size changing 2021-12-06 22:37:28 +11:00
DerTeta
de0e885c65 Refresh staging- or patch building panel when rendering their respective context 2021-12-06 22:37:28 +11:00
DerTeta
f7ffbbd72a Add a menu entry and keybinding to { for decreasing the context size 2021-12-06 22:37:28 +11:00
DerTeta
0fbde05928 Add a menu item and keybinding to } to increase the context size 2021-12-06 22:37:28 +11:00
DerTeta
ba844c18a5 Add the DecreaseContextInDiffView function 2021-12-06 22:37:28 +11:00
DerTeta
e1cf6912db Add the IncreaseContextInDiffView function 2021-12-06 22:37:28 +11:00
DerTeta
c99d373e13 Use DiffContextSize in ShowStashEntryCmdStr 2021-12-06 22:37:28 +11:00
DerTeta
ecfafb6fbe Use DiffContextSize in ShowCmdStr 2021-12-06 22:37:28 +11:00
DerTeta
14d9e776be Use DiffContextSize in ShowFileDiffStr 2021-12-06 22:37:28 +11:00
DerTeta
ca88620e8f Use DiffContextSize in WorkTreeFileDiffCmdStr 2021-12-06 22:37:28 +11:00
DerTeta
9feaf5d70f Add the DiffContextSize setting to GitConfig
It defaults to 3 lines, which is the default value for git.
2021-12-06 22:37:28 +11:00
DerTeta
3e3151f86a Fix: Don't access a view if it's nil
The way the `if` expression in `deactivateContext` was composed,
it was possible to have it to evaluate to `true` even though the
`view` variable was `nil`.

As far as I can tell, this seems to be only possible during tests.
Nonetheless, I think the expression looks more "correct" this way.
2021-12-06 22:37:28 +11:00
alextrastero
28cdcddb0a Update README.md
add a link to ubuntu installation instructions
2021-12-01 18:00:50 +11:00
Jesse Duffield
02bf6a5c17 fix delta again 2021-11-22 21:07:04 +11:00
Mark Kopenga
8abc953582 Merge pull request #1581 from black-desk/translate
fix chinese translate
2021-11-16 11:29:21 +01:00
black_desk
9c4f837d45 fix chinese translate 2021-11-16 17:06:46 +08:00
Jesse Duffield
2f45db8f7c fix scrolling in sub commits panel 2021-11-10 08:54:14 +11:00
Yuki Osaki
bf355bc0b5 fix based on the reviews 2021-11-10 00:19:39 +09:00
Yuki Osaki
27a12fe2ea Update pkg/commands/github.go
Co-authored-by: Jesse Duffield <jessedduffield@gmail.com>
2021-11-09 21:21:59 +09:00
Jesse Duffield
3fb478a30e add tests 2021-11-07 13:32:36 +11:00
Jesse Duffield
5d12a6bf99 restore some code that was erroneously removed 2021-11-07 13:32:36 +11:00
Yuki Osaki
37aa558d34 Merge branch 'master' into gh-integration 2021-11-06 12:49:36 +09:00
Jesse Duffield
1d40d03bb2 refactor 2021-11-05 07:58:21 +11:00
Jesse Duffield
9a9e3d506d more consistent rendering 2021-11-05 07:58:21 +11:00
Jesse Duffield
06ca71e955 fix bug 2021-11-05 07:58:21 +11:00
Jesse Duffield
308a3b51b3 some more throttling stuff 2021-11-05 07:58:21 +11:00
Jesse Duffield
ccd80a0e4b add menu options for log stuff 2021-11-05 07:58:21 +11:00
Jesse Duffield
37be9dbea1 support scrolling left and right 2021-11-05 07:58:21 +11:00
Jesse Duffield
f6ec7babf5 add some config 2021-11-05 07:58:21 +11:00
Jesse Duffield
802cfb1a04 render commit graph 2021-11-05 07:58:21 +11:00
Yuki Osaki
588bd95118 handle when repo is not set 2021-11-03 00:14:25 +09:00
Yuki Osaki
2b8c959cb0 add Gh version check 2021-11-02 22:04:22 +09:00
Yuki Osaki
4fead59416 Merge branch 'master' into gh-integration 2021-11-02 21:11:35 +09:00
Jesse Duffield
2fc1498517 some refactoring in anticipation of the graph feature 2021-11-01 10:03:49 +11:00
Jesse Duffield
7a464ae5b7 add graph algorithm 2021-11-01 10:03:49 +11:00
Jesse Duffield
927ee63106 support aborting a merge or rebase with esc 2021-11-01 09:18:30 +11:00
Yuki Osaki
2d1c29fa76 Add enableGhCommand flag 2021-11-01 00:00:39 +09:00
Yuki Osaki
66f3978ab5 use surfaceError 2021-10-31 23:48:52 +09:00
Yuki Osaki
c8de14aa9d generate github state struct 2021-10-31 23:39:01 +09:00
Yuki Osaki
d2e75a6359 remove BranchesWithGithubPullRequests check 2021-10-31 23:31:05 +09:00
Yuki Osaki
b36f6db521 refactor logic 2021-10-31 23:16:04 +09:00
Yuki Osaki
769a6cd069 add local 2021-10-31 23:10:05 +09:00
Yuki Osaki
1547b4155e remove refreshStatus 2021-10-31 22:59:32 +09:00
Yuki Osaki
153970fb08 split logic into functions 2021-10-31 22:55:11 +09:00
Yuki Osaki
aed7c29509 move pr color logic to own method 2021-10-31 22:40:39 +09:00
Yuki Osaki
4a1fa4889d Merge branch 'master' into gh-integration 2021-10-31 22:29:37 +09:00
Yuki Osaki
515e2d2fa9 refactor the code 2021-10-31 22:28:03 +09:00
Jesse Duffield
9989c96321 better formatting 2021-10-31 22:33:39 +11:00
Yuki Osaki
c1f3fd6b3f Update GithubMostRecentPRs method 2021-10-31 15:40:18 +09:00
Yuki Osaki
9d774c2450 Use c.GetRemotes() 2021-10-31 14:34:27 +09:00
Yuki Osaki
04edfc2c63 remove unnecessary check 2021-10-31 14:03:45 +09:00
Yuki Osaki
1bf9d2e30c Merge branch 'master' into gh-integration 2021-10-31 13:59:22 +09:00
Yuki Osaki
502c23dff7 update the logic 2021-10-31 13:58:07 +09:00
Jesse Duffield
f91892b8f1 fix truncation 2021-10-30 20:19:40 +11:00
Jesse Duffield
72bce201df support scrolling the list in the integrations app 2021-10-30 18:26:06 +11:00
Jesse Duffield
5df0fe0765 fix crash 2021-10-30 18:26:06 +11:00
Jesse Duffield
c47c539e12 support user-configurable author colours 2021-10-30 18:26:06 +11:00
Jesse Duffield
c96496c3a7 show author info in rebase commits 2021-10-30 18:26:06 +11:00
Jesse Duffield
7561703e8d move author name colouring code into its own file 2021-10-30 18:26:06 +11:00
Jesse Duffield
b04b457246 fix yet another issue with indentation 2021-10-30 18:26:06 +11:00
Jesse Duffield
6457800748 fix another issue with indentation 2021-10-30 18:26:06 +11:00
Jesse Duffield
7d9461877a fix issue with indentation 2021-10-30 18:26:06 +11:00
Jesse Duffield
e122f421e6 only use a single initial for double sized runes 2021-10-30 18:26:06 +11:00
Ryooooooga
6171690b00 Fix multibyte initial characters 2021-10-30 18:26:06 +11:00
Jesse Duffield
253504a094 associate random colours with authors 2021-10-30 18:26:06 +11:00
Jesse Duffield
f704707d29 stream output from certain git commands in command log panel 2021-10-30 18:26:06 +11:00
Yuki Osaki
61d08dc4af better error handle 2021-10-30 15:58:37 +09:00
Yuki Osaki
a71db3355a update it not to inject the PRs into our branches 2021-10-30 00:37:38 +09:00
Yuki Osaki
7fbf4fa7f7 say 'create / open' instead of 'create / show' 2021-10-29 21:09:31 +09:00
Yuki Osaki
3ed1611d94 Move it to guiState 2021-10-29 21:03:04 +09:00
Yuki Osaki
ada3b528d3 fix conflict 2021-10-29 20:29:41 +09:00
Yuki Osaki
3df068b1fc Merge branch 'master' into gh-integration 2021-10-29 19:19:43 +09:00
Jesse Duffield
01d82749b1 fix commit message prefix thingo 2021-10-25 22:40:15 +00:00
Jesse Duffield
3eb124c732 easier hiding of command log 2021-10-23 12:54:57 +11:00
Jesse Duffield
ef544e6ce9 add more suggestions 2021-10-23 12:29:52 +11:00
Jesse Duffield
629494144f show suggestions when typing in an origin 2021-10-23 12:29:52 +11:00
Jesse Duffield
b6a5e9d615 use cached git config 2021-10-23 10:26:47 +11:00
Jesse Duffield
5011cac7ea show filetree by default 2021-10-22 22:39:17 +11:00
Sam Burville
5df0475612 Add variable to simplify JumpToBlock keybinding
This removes the magic '5' and instead uses the number of windows.
2021-10-22 22:38:26 +11:00
Sam Burville
f6e316dfe5 Improve JumpToBlock keybinding functionality
Improve experience when yaml file has != 5 keybindings and change view
helper to use the length of the array instead of hardcoded value.
2021-10-22 22:38:26 +11:00
Sam Burville
91e8765d9c Add JumpToBlock keybinding
This should allow users to decide their own keybinding for jumping
between blocks/panels.
E.g. A user could choose 5-9 instead of 1-5.
2021-10-22 22:38:26 +11:00
Jesse Duffield
80a8e9b04d fix merge conflict scrolling 2021-10-22 22:16:52 +11:00
Jesse Duffield
2008c39516 add tests for dealing with remotes 2021-10-22 21:33:17 +11:00
Jesse Duffield
6388af70ac simplify pull logic 2021-10-22 21:33:17 +11:00
Jesse Duffield
5ee559b896 fix issue where upstream origin and branch were quoted together
fix issue where upstream origin and branch were quoted together
2021-10-20 09:29:17 +11:00
Jesse Duffield
ca7252ef8e suggest files when picking a path to filter on
async fetching of suggestions

remove limit

cache the trie for future use

more

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

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

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

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

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

WIP

WIP

WIP

WIP

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

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

View File

@@ -4,7 +4,6 @@ about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
@@ -12,6 +11,7 @@ A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
@@ -23,10 +23,15 @@ A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. Windows]
- Lazygit Version [e.g. v0.1.45]
- The last commit id if you built project from sources (run : ```git-rev parse HEAD```)
**Version info:**
_Run `lazygit --version` and paste the result here_
_Run `git --version` and paste the result here_
**Additional context**
Add any other context about the problem here.
**Note:** please try updating to the latest version or [manually building](https://github.com/jesseduffield/lazygit/#manual) the latest `master` to see if the issue still occurs.
<!--
If you want to try and debug this issue yourself, you can run `lazygit --debug` in one terminal panel and `lazygit --logs` in another to view the logs.
-->

View File

@@ -4,7 +4,6 @@ about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
@@ -18,3 +17,13 @@ A clear and concise description of any alternative solutions or features you've
**Additional context**
Add any other context or screenshots about the feature request here.
<!--
You may be able to add your desired feature with a custom command. Check out the examples here: https://github.com/jesseduffield/lazygit/wiki/Custom-Commands-Compendium
If a custom command does what you want but you still want to see the feature built-in to lazygit, feel free to paste the custom command into the issue to help us better understand the functionality you want.
We also encourage you to put up a PR yourself! Who cares if you've never written Go before, neither did any of the existing contributors before their first lazygit PR! Check out the PR tutorial here: https://www.youtube.com/watch?v=kNavnhzZHtk&ab_channel=JesseDuffield
Also check out the contributing guide here: https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md
-->

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

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

10
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,10 @@
- **PR Description**
- **Please check if the PR fulfills these requirements**
* [ ] Cheatsheets are up-to-date (run `go run scripts/cheatsheet/main.go generate`)
* [ ] Code has been formatted (see [here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [ ] Tests have been added/updated (see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md) for the integration test guide)
* [ ] Text is internationalised (see [here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [ ] Docs (specifically `docs/Config.md`) have been updated if necessary
* [ ] You've read through your own file changes for silly mistakes etc

View File

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

View File

@@ -6,7 +6,7 @@ on:
- 'v*'
jobs:
cd:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout code
@@ -16,30 +16,16 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.14.x
go-version: 1.18.x
- name: Run goreleaser
uses: goreleaser/goreleaser-action@v1
env:
GITHUB_TOKEN: ${{secrets.GITHUB_API_TOKEN}}
- name: Bump Homebrew
homebrew:
runs-on: ubuntu-latest
steps:
- name: Bump Homebrew formula
uses: dawidd6/action-homebrew-bump-formula@v3
with:
token: ${{secrets.GITHUB_API_TOKEN}}
formula: lazygit
- name: Checkout PPA repo
uses: actions/checkout@v2
with:
repository: dawidd6/lazygit-debian
token: ${{secrets.GITHUB_API_TOKEN}}
fetch-depth: 0
- name: Update PPA repo
run: |
version="$(echo "$GITHUB_REF" | sed 's@refs/tags/v@@')"
sudo apt install -y git-buildpackage
git fetch --tags https://github.com/$GITHUB_REPOSITORY
gbp import-ref -u "$version"
gbp dch -D xenial -N "$version"-1
git add debian/changelog
git commit -S -m "d/changelog: dch $version"
gbp tag
git push --tags origin master

View File

@@ -1,5 +1,8 @@
name: Continuous Integration
env:
GO_VERSION: 1.18
on:
push:
branches:
@@ -7,7 +10,160 @@ 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:
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@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-old:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
parallelism: [5]
index: [0,1,2,3,4]
name: "Integration Tests (Old pattern) (${{ 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
~/go/pkg/mod
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-test
restore-keys: |
${{runner.os}}-go-
- name: Test code
run: |
PARALLEL_TOTAL=${{ matrix.parallelism }} PARALLEL_INDEX=${{ matrix.index }} go test pkg/integration/deprecated/*.go
integration-tests:
runs-on: ubuntu-latest
name: "Integration Tests"
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
~/go/pkg/mod
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-test
restore-keys: |
${{runner.os}}-go-
- name: Test code
run: |
go test pkg/integration/clients/*.go
build:
runs-on: ubuntu-latest
env:
GOFLAGS: -mod=vendor
GOARCH: amd64
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.18.x
- name: Cache build
uses: actions/cache@v1
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-build
restore-keys: |
${{runner.os}}-go-
- name: Build linux binary
run: |
GOOS=linux go build
- name: Build windows binary
run: |
GOOS=windows go build
- name: Build darwin binary
run: |
GOOS=darwin go build
- name: Build integration test binary
run: |
GOOS=linux go build cmd/integration_test/main.go
- name: Build integration test injector
run: |
GOOS=linux go build pkg/integration/clients/injector/main.go
check-codebase:
runs-on: ubuntu-latest
env:
GOFLAGS: -mod=vendor
GOARCH: amd64
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.18.x
- name: Cache build
uses: actions/cache@v1
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-build
restore-keys: |
${{runner.os}}-go-
- name: Check Cheatsheet
run: |
go run scripts/cheatsheet/main.go check
- name: Check Vendor Directory
# ensure our vendor directory matches up with our go modules
run: |
go mod vendor && git diff --exit-code || (echo "Unexpected change to vendor directory. Run 'go mod vendor' locally and commit the changes" && exit 1)
lint:
runs-on: ubuntu-latest
env:
GOFLAGS: -mod=vendor
@@ -17,24 +173,20 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.14.x
go-version: 1.18.x
- name: Cache build
uses: actions/cache@v1
with:
path: ~/.cache/go-build
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-test
restore-keys: |
${{runner.os}}-go-
- name: Format code
run: |
if [ $(find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;|wc -l) -gt 0 ]; then
find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;
exit 1
fi
- name: Test code
run: |
./test.sh
- name: Build binaries
uses: goreleaser/goreleaser-action@v1
- name: Lint
uses: golangci/golangci-lint-action@v3.1.0
with:
args: --skip-publish --snapshot
version: latest
- name: errors
run: golangci-lint run
if: ${{ failure() }}

28
.github/workflows/sponsors.yml vendored Normal file
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

24
.gitignore vendored
View File

@@ -18,16 +18,34 @@ coverage.txt
# Binaries
lazygit
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
# we'll scrap these lines once we've fully moved over to the new integration test approach
test/integration/*/actual/
test/integration/*/used_config/
# these sample hooks waste too space space
test/integration/*/expected/.git_keep/hooks/
!.git_keep/
# these sample hooks waste too much space
test/integration/*/expected/**/hooks/
test/integration/*/expected_remote/**/hooks/
test/integration_new/**/actual/
test/integration_new/**/used_config/
# these sample hooks waste too much space
test/integration_new/**/expected/**/hooks/
test/integration_new/**/expected_remote/**/hooks/
oryxBuildBinary
__debug_bin

29
.golangci.yml Normal file
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
.vscode/debugger_config.yml vendored Normal file
View File

@@ -0,0 +1 @@
disableStartupPopups: true

36
.vscode/launch.json vendored Normal file
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
}
]
}

View File

@@ -1,74 +1,3 @@
# Contributor Covenant Code of Conduct
# Lazygit Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the [project leader](https://github.com/jesseduffield).
All complaints will be reviewed and investigated and will result in a response that
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
Be nice, or face the wrath of the maintainer.

View File

@@ -1,35 +1,156 @@
# Contributing
♥ We love pull requests from everyone !
When contributing to this repository, please first discuss the change you wish
to make via issue, email, or any other method with the owners of this repository
before making a change.
before making a change.
## 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
## So all code changes happen through Pull Requests
Pull requests are the best way to propose changes to the codebase. We actively
welcome your pull requests:
1. Fork the repo and create your branch from `master`.
2. If you've added code that should be tested, add tests.
3. If you've added code that need documentation, update the documentation.
4. Make sure your code follows the [effective go](https://golang.org/doc/effective_go.html) guidelines as much as possible.
5. Be sure to test your modifications.
6. Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
7. Issue that pull request!
4. Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
5. Issue that pull request!
If you've never written Go in your life, then join the club! Lazygit was the maintainer's first Go program, and most contributors have never used Go before. Go is widely considered an easy-to-learn language, so if you're looking for an open source project to gain dev experience, you've come to the right place.
## Code of conduct
Please note by participating in this project, you agree to abide by the [code of conduct].
[code of conduct]: https://github.com/jesseduffield/lazygit/blob/master/CODE-OF-CONDUCT.md
## Any contributions you make will be under the MIT Software License
In short, when you submit code changes, your submissions are understood to be
under the same [MIT License](http://choosealicense.com/licenses/mit/) that
covers the project. Feel free to contact the maintainers if that's a concern.
## Report bugs using Github's [issues](https://github.com/jesseduffield/lazygit/issues)
We use GitHub issues to track public bugs. Report a bug by [opening a new
issue](https://github.com/jesseduffield/lazygit/issues/new); it's that easy!
## Go
This project is written in Go. Go is an opinionated language with strict idioms, but some of those idioms are a little extreme. Some things we do differently:
1. There is no shame in using `self` as a receiver name in a struct method. In fact we encourage it
2. There is no shame in prefixing an interface with 'I' instead of suffixing with 'er' when there are several methods on the interface.
3. If a struct implements an interface, we make it explicit with something like:
```go
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
}
}
```
To run gofumpt from your terminal go:
```
go install mvdan.cc/gofumpt@latest && gofumpt -l -w .
```
## 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. Then you can access via `gui.Tr.YourNewText` (or `app.Tr.YourNewText`, etc). 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!).
## Debugging
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")`.
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
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "debug lazygit",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "main.go",
"args": ["--debug"],
"console": "externalTerminal" // <-- you need this to actually see the lazygit UI in a window while debugging
}
]
}
```
## Testing
Lazygit has two kinds of tests: unit tests and integration tests. Unit tests go in files that end in `_test.go`, and are written in Go. For integration tests, see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
## Updating Gocui
Sometimes you will need to make a change in the gocui fork (https://github.com/jesseduffield/gocui). Gocui is the package responsible for rendering windows and handling user input. Here's the typical process to follow:
1. Make the changes in gocui inside the vendor directory so it's easy to test against lazygit
2. Copy the changes over to the actual gocui repo (clone it if you haven't already, and use the `awesome` branch, not `master`)
3. Raise a PR on the gocui repo with your changes
4. After that PR is merged, make a PR in lazygit bumping the gocui version. You can bump the version by running the following at the lazygit repo root:
```sh
./scripts/bump_gocui.sh
```
5. Raise a PR in lazygit with those changes
## Improvements
If you can think of any way to improve these docs let us know.

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" ]

125
README.md

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,56 @@
package main
import (
"fmt"
"log"
"os"
"github.com/jesseduffield/lazygit/pkg/integration/clients"
)
var usage = `
Usage:
See https://github.com/jesseduffield/lazygit/tree/master/pkg/integration/README.md
CLI mode:
> go run cmd/integration_test/main.go cli [--slow] <test1> <test2> ...
If you pass no test names, it runs all tests
Accepted environment variables:
KEY_PRESS_DELAY (e.g. 200): the number of milliseconds to wait between keypresses
MODE:
* ask (default): if a snapshot test fails, asks if you want to update the snapshot
* check: if a snapshot test fails, exits with an error
* update: if a snapshot test fails, updates the snapshot
* sandbox: uses the test's setup step to run the test in a sandbox where you can do whatever you want
TUI mode:
> go run cmd/integration_test/main.go tui
This will open up a terminal UI where you can run tests
Help:
> go run cmd/integration_test/main.go help
`
func main() {
if len(os.Args) < 2 {
log.Fatal(usage)
}
switch os.Args[1] {
case "help":
fmt.Println(usage)
case "cli":
testNames := os.Args[2:]
slow := false
// get the next arg if it's --slow
if len(os.Args) > 2 && (os.Args[2] == "--slow" || os.Args[2] == "-slow") {
testNames = os.Args[3:]
slow = true
}
clients.RunCLI(testNames, slow)
case "tui":
clients.RunTUI()
default:
log.Fatal(usage)
}
}

View File

@@ -2,176 +2,235 @@
Default path for the config file:
* Linux: `~/.config/jesseduffield/lazygit/config.yml`
* MacOS: `~/Library/Application Support/jesseduffield/lazygit/config.yml`
* Windows: `%APPDATA%\jesseduffield\lazygit\config.yml`
- Linux: `~/.config/lazygit/config.yml`
- MacOS: `~/Library/Application Support/lazygit/config.yml`
- Windows: `%APPDATA%\lazygit\config.yml`
For old installations (slightly embarrassing: I didn't realise at the time that you didn't need to supply a vendor name to the path so I just used my name):
- Linux: `~/.config/jesseduffield/lazygit/config.yml`
- MacOS: `~/Library/Application Support/jesseduffield/lazygit/config.yml`
- Windows: `%APPDATA%\jesseduffield\lazygit\config.yml`
## Default
```yaml
gui:
# stuff relating to the UI
scrollHeight: 2 # how many lines you scroll by
scrollPastBottom: true # enable scrolling past the bottom
sidePanelWidth: 0.3333 # number from 0 to 1
expandFocusedSidePanel: false
mainPanelSplitMode: 'flexible' # one of 'horizontal' | 'flexible' | 'vertical'
theme:
lightTheme: false # For terminals with a light background
activeBorderColor:
- white
- bold
inactiveBorderColor:
- green
optionsTextColor:
- blue
selectedLineBgColor:
- default
selectedRangeBgColor:
- blue
commitLength:
show: true
mouseEvents: true
skipUnstageLineWarning: false
skipStashWarning: true
git:
paging:
colorArg: always
useConfig: false
merging:
# only applicable to unix users
manualCommit: false
# extra args passed to `git merge`, e.g. --no-ff
args: ""
pull:
mode: 'merge' # one of 'merge' | 'rebase' | 'ff-only'
skipHookPrefix: WIP
autoFetch: true
branchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --"
overrideGpg: false # prevents lazygit from spawning a separate process when using GPG
disableForcePushing: false
update:
method: prompt # can be: prompt | background | never
days: 14 # how often an update is checked for
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
confirmOnQuit: false
# determines whether hitting 'esc' will quit the application when there is nothing to cancel/close
quitOnTopLevelReturn: true
disableStartupPopups: false
keybinding:
universal:
quit: 'q'
quit-alt1: '<c-c>' # alternative/alias of quit
return: '<esc>' # return to previous menu, will quit if there's nowhere to return
quitWithoutChangingDirectory: 'Q'
togglePanel: '<tab>' # goto the next panel
prevItem: '<up>' # go one line up
nextItem: '<down>' # go one line down
prevItem-alt: 'k' # go one line up
nextItem-alt: 'j' # go one line down
prevPage: ',' # go to next page in list
nextPage: '.' # go to previous page in list
gotoTop: '<' # go to top of list
gotoBottom: '>' # go to bottom of list
prevBlock: '<left>' # goto the previous block / panel
nextBlock: '<right>' # goto the next block / panel
prevBlock-alt: 'h' # goto the previous block / panel
nextBlock-alt: 'l' # goto the next block / panel
nextMatch: 'n'
prevMatch: 'N'
optionMenu: 'x' # show help menu
optionMenu-alt1: '?' # show help menu
select: '<space>'
goInto: '<enter>'
confirm: '<enter>'
confirm-alt1: 'y'
remove: 'd'
new: 'n'
edit: 'e'
openFile: 'o'
scrollUpMain: '<pgup>' # main panel scrool up
scrollDownMain: '<pgdown>' # main panel scrool down
scrollUpMain-alt1: 'K' # main panel scrool up
scrollDownMain-alt1: 'J' # main panel scrool down
scrollUpMain-alt2: '<c-u>' # main panel scrool up
scrollDownMain-alt2: '<c-d>' # main panel scrool down
executeCustomCommand: ':'
createRebaseOptionsMenu: 'm'
pushFiles: 'P'
pullFiles: 'p'
refresh: 'R'
createPatchOptionsMenu: '<c-p>'
nextTab: ']'
prevTab: '['
nextScreenMode: '+'
prevScreenMode: '_'
undo: 'z'
redo: '<c-z>'
filteringMenu: '<c-s>'
diffingMenu: 'W'
diffingMenu-alt: '<c-e>' # deprecated
copyToClipboard: '<c-o>'
submitEditorText: '<enter>'
appendNewline: '<tab>'
status:
checkForUpdate: 'u'
recentRepos: '<enter>'
files:
commitChanges: 'c'
commitChangesWithoutHook: 'w' # commit changes without pre-commit hook
amendLastCommit: 'A'
commitChangesWithEditor: 'C'
ignoreFile: 'i'
refreshFiles: 'r'
stashAllChanges: 's'
viewStashOptions: 'S'
toggleStagedAll: 'a' # stage/unstage all
viewResetOptions: 'D'
fetch: 'f'
branches:
createPullRequest: 'o'
checkoutBranchByName: 'c'
forceCheckoutBranch: 'F'
rebaseBranch: 'r'
mergeIntoCurrentBranch: 'M'
viewGitFlowOptions: 'i'
fastForward: 'f' # fast-forward this branch from its upstream
pushTag: 'P'
setUpstream: 'u' # set as upstream of checked-out branch
fetchRemote: 'f'
commits:
squashDown: 's'
renameCommit: 'r'
renameCommitWithEditor: 'R'
viewResetOptions: 'g'
markCommitAsFixup: 'f'
createFixupCommit: 'F' # create fixup commit for this commit
squashAboveCommits: 'S'
moveDownCommit: '<c-j>' # move commit down one
moveUpCommit: '<c-k>' # move commit up one
amendToCommit: 'A'
pickCommit: 'p' # pick commit (when mid-rebase)
revertCommit: 't'
cherryPickCopy: 'c'
cherryPickCopyRange: 'C'
pasteCommits: 'v'
tagCommit: 'T'
checkoutCommit: '<space>'
resetCherryPick: '<c-R>'
copyCommitMessageToClipboard: '<c-y>'
stash:
popStash: 'g'
commitFiles:
checkoutCommitFile: 'c'
main:
toggleDragSelect: 'v'
toggleDragSelect-alt: 'V'
toggleSelectHunk: 'a'
pickBothHunks: 'b'
submodules:
init: 'i'
update: 'u'
bulkMenu: 'b'
gui:
# stuff relating to the UI
scrollHeight: 2 # how many lines you scroll by
scrollPastBottom: true # enable scrolling past the bottom
sidePanelWidth: 0.3333 # number from 0 to 1
expandFocusedSidePanel: false
mainPanelSplitMode: 'flexible' # one of 'horizontal' | 'flexible' | 'vertical'
language: 'auto' # one of 'auto' | 'en' | 'zh' | 'pl' | 'nl' | '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:
- green
- bold
inactiveBorderColor:
- white
optionsTextColor:
- blue
selectedLineBgColor:
- blue # set to `default` to have no background colour
selectedRangeBgColor:
- blue
cherryPickedCommitBgColor:
- cyan
cherryPickedCommitFgColor:
- blue
unstagedChangesColor:
- red
commitLength:
show: true
mouseEvents: true
skipUnstageLineWarning: false
skipStashWarning: false
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
useConfig: false
commit:
signOff: false
merging:
# only applicable to unix users
manualCommit: false
# extra args passed to `git merge`, e.g. --no-ff
args: ''
log:
# one of date-order, author-date-order, topo-order.
# topo-order makes it easier to read the git log graph, but commits may not
# appear chronologically. See https://git-scm.com/docs/git-log#_commit_ordering
order: 'topo-order'
# 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
disableForcePushing: false
parseEmoji: false
enableGhCommand: false
diffContextSize: 3 # how many lines of context are shown around a change in diffs
os:
editCommand: '' # see 'Configuring File Editing' section
editCommandTemplate: ''
openCommand: ''
refresher:
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
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
confirmOnQuit: false
# determines whether hitting 'esc' will quit the application when there is nothing to cancel/close
quitOnTopLevelReturn: false
disableStartupPopups: false
notARepository: 'prompt' # one of: 'prompt' | 'create' | 'skip' | 'quit'
promptToReturnFromSubprocess: true # display confirmation when subprocess terminates
keybinding:
universal:
quit: 'q'
quit-alt1: '<c-c>' # alternative/alias of quit
return: '<esc>' # return to previous menu, will quit if there's nowhere to return
quitWithoutChangingDirectory: 'Q'
togglePanel: '<tab>' # goto the next panel
prevItem: '<up>' # go one line up
nextItem: '<down>' # go one line down
prevItem-alt: 'k' # go one line up
nextItem-alt: 'j' # go one line down
prevPage: ',' # go to next page in list
nextPage: '.' # go to previous page in list
gotoTop: '<' # go to top of list
gotoBottom: '>' # go to bottom of list
scrollLeft: 'H' # scroll left within list view
scrollRight: 'L' # scroll right within list view
prevBlock: '<left>' # goto the previous block / panel
nextBlock: '<right>' # goto the next block / panel
prevBlock-alt: 'h' # goto the previous block / panel
nextBlock-alt: 'l' # goto the next block / panel
jumpToBlock: ['1', '2', '3', '4', '5'] # goto the Nth block / panel
nextMatch: 'n'
prevMatch: 'N'
optionMenu: 'x' # show help menu
optionMenu-alt1: '?' # show help menu
select: '<space>'
goInto: '<enter>'
openRecentRepos: '<c-r>'
confirm: '<enter>'
confirm-alt1: 'y'
remove: 'd'
new: 'n'
edit: 'e'
openFile: 'o'
scrollUpMain: '<pgup>' # main panel scroll up
scrollDownMain: '<pgdown>' # main panel scroll down
scrollUpMain-alt1: 'K' # main panel scroll up
scrollDownMain-alt1: 'J' # main panel scroll down
scrollUpMain-alt2: '<c-u>' # main panel scroll up
scrollDownMain-alt2: '<c-d>' # main panel scroll down
executeCustomCommand: ':'
createRebaseOptionsMenu: 'm'
pushFiles: 'P'
pullFiles: 'p'
refresh: 'R'
createPatchOptionsMenu: '<c-p>'
nextTab: ']'
prevTab: '['
nextScreenMode: '+'
prevScreenMode: '_'
undo: 'z'
redo: '<c-z>'
filteringMenu: '<c-s>'
diffingMenu: 'W'
diffingMenu-alt: '<c-e>' # deprecated
copyToClipboard: '<c-o>'
submitEditorText: '<enter>'
appendNewline: '<a-enter>'
extrasMenu: '@'
toggleWhitespaceInDiffView: '<c-w>'
increaseContextInDiffView: '}'
decreaseContextInDiffView: '{'
status:
checkForUpdate: 'u'
recentRepos: '<enter>'
files:
commitChanges: 'c'
commitChangesWithoutHook: 'w' # commit changes without pre-commit hook
amendLastCommit: 'A'
commitChangesWithEditor: 'C'
ignoreFile: 'i'
refreshFiles: 'r'
stashAllChanges: 's'
viewStashOptions: 'S'
toggleStagedAll: 'a' # stage/unstage all
viewResetOptions: 'D'
fetch: 'f'
toggleTreeView: '`'
branches:
createPullRequest: 'o'
viewPullRequestOptions: 'O'
checkoutBranchByName: 'c'
forceCheckoutBranch: 'F'
rebaseBranch: 'r'
renameBranch: 'R'
mergeIntoCurrentBranch: 'M'
viewGitFlowOptions: 'i'
fastForward: 'f' # fast-forward this branch from its upstream
pushTag: 'P'
setUpstream: 'u' # set as upstream of checked-out branch
fetchRemote: 'f'
commits:
squashDown: 's'
renameCommit: 'r'
renameCommitWithEditor: 'R'
viewResetOptions: 'g'
markCommitAsFixup: 'f'
createFixupCommit: 'F' # create fixup commit for this commit
squashAboveCommits: 'S'
moveDownCommit: '<c-j>' # move commit down one
moveUpCommit: '<c-k>' # move commit up one
amendToCommit: 'A'
pickCommit: 'p' # pick commit (when mid-rebase)
revertCommit: 't'
cherryPickCopy: 'c'
cherryPickCopyRange: 'C'
pasteCommits: 'v'
tagCommit: 'T'
checkoutCommit: '<space>'
resetCherryPick: '<c-R>'
copyCommitMessageToClipboard: '<c-y>'
openLogMenu: '<c-l>'
viewBisectOptions: 'b'
stash:
popStash: 'g'
commitFiles:
checkoutCommitFile: 'c'
main:
toggleDragSelect: 'v'
toggleDragSelect-alt: 'V'
toggleSelectHunk: 'a'
pickBothHunks: 'b'
submodules:
init: 'i'
update: 'u'
bulkMenu: 'b'
```
## Platform Defaults
@@ -179,22 +238,73 @@ Default path for the config file:
### Windows
```yaml
os:
openCommand: 'cmd /c "start "" {{filename}}"'
os:
openCommand: 'start "" {{filename}}'
```
### Linux
```yaml
os:
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'
os:
openCommand: 'xdg-open {{filename}} >/dev/null'
```
### OSX
```yaml
os:
openCommand: 'open {{filename}}'
os:
openCommand: 'open {{filename}}'
```
### Configuring File Editing
Lazygit will edit a file with the first set editor in the following:
1. config.yaml
```yaml
os:
editCommand: 'vim' # as an example
```
2. \$(git config core.editor)
3. \$GIT_EDITOR
4. \$VISUAL
5. \$EDITOR
6. \$(which vi)
Lazygit will log an error if none of these options are set.
You can specify the current line number when you're in the patch explorer.
```yaml
os:
editCommand: 'vim'
editCommandTemplate: '{{editor}} +{{line}} -- {{filename}}'
```
or
```yaml
os:
editCommand: 'code'
editCommandTemplate: '{{editor}} --goto -- {{filename}}:{{line}}'
```
`{{editor}}` in `editCommandTemplate` is replaced with the value of `editCommand`.
### Overriding default config file location
To override the default config directory, use `CONFIG_DIR="$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="$HOME/.base_lg_conf,$HOME/.light_theme_lg_conf"
or
LG_CONFIG_FILE="$HOME/.base_lg_conf,$HOME/.light_theme_lg_conf" lazygit
```
### Recommended Config Values
@@ -202,8 +312,8 @@ Default path for the config file:
for users of VSCode
```yaml
os:
openCommand: 'code -rg {{filename}}'
os:
openCommand: 'code -rg {{filename}}'
```
## Color Attributes
@@ -211,7 +321,8 @@ for users of VSCode
For color attributes you can choose an array of attributes (with max one color attribute)
The available attributes are:
- default
**Colors**
- black
- red
- green
@@ -220,7 +331,12 @@ The available attributes are:
- magenta
- cyan
- white
- '#ff00ff'
**Modifiers**
- bold
- default
- reverse # useful for high-contrast
- underline
@@ -229,35 +345,89 @@ The available attributes are:
If you have issues with a light terminal theme where you can't read / see the text add these settings
```yaml
gui:
theme:
lightTheme: true
activeBorderColor:
- black
- bold
inactiveBorderColor:
- black
selectedLineBgColor:
- default
gui:
theme:
lightTheme: true
activeBorderColor:
- black
- bold
inactiveBorderColor:
- black
selectedLineBgColor:
- default
```
## Struggling to see selected line
## Highlighting the selected line
If you struggle to see the selected line I recomment 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:
- reverse
selectedRangeBgColor:
- reverse
gui:
theme:
selectedLineBgColor:
- default
selectedRangeBgColor:
- default
```
You can also use the reverse attribute like so:
```yaml
gui:
theme:
selectedLineBgColor:
- reverse
selectedRangeBgColor:
- reverse
```
## Custom Author Color
Lazygit will assign a random color for every commit author in the commits pane by default.
You can customize the color in case you're not happy with the randomly assigned one:
```yaml
gui:
authorColors:
'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:
```yaml
gui:
authorColors:
# use red for John Smith
'John Smith': 'red'
# use blue for other authors
'*': '#0000ff'
```
## Custom Branch Color
You can customize the color of branches based on the branch prefix:
```yaml
gui:
branchColors:
'docs': '#11aaff' # use a light blue for branches beginning with 'docs/'
```
## Example Coloring
![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)
@@ -265,33 +435,33 @@ For all possible keybinding options, check [Custom_Keybindings.md](https://githu
### Example Keybindings For Colemak Users
```yaml
keybinding:
universal:
prevItem-alt: 'u'
nextItem-alt: 'e'
prevBlock-alt: 'n'
nextBlock-alt: 'i'
nextMatch: '='
prevMatch: '-'
new: 'k'
edit: 'o'
openFile: 'O'
scrollUpMain-alt1: 'U'
scrollDownMain-alt1: 'E'
scrollUpMain-alt2: '<c-u>'
scrollDownMain-alt2: '<c-e>'
undo: 'l'
redo: '<c-r>'
diffingMenu: 'M'
filteringMenu: '<c-f>'
files:
ignoreFile: 'I'
commits:
moveDownCommit: '<c-e>'
moveUpCommit: '<c-u>'
branches:
viewGitFlowOptions: 'I'
setUpstream: 'U'
keybinding:
universal:
prevItem-alt: 'u'
nextItem-alt: 'e'
prevBlock-alt: 'n'
nextBlock-alt: 'i'
nextMatch: '='
prevMatch: '-'
new: 'k'
edit: 'o'
openFile: 'O'
scrollUpMain-alt1: 'U'
scrollDownMain-alt1: 'E'
scrollUpMain-alt2: '<c-u>'
scrollDownMain-alt2: '<c-e>'
undo: 'l'
redo: '<c-r>'
diffingMenu: 'M'
filteringMenu: '<c-f>'
files:
ignoreFile: 'I'
commits:
moveDownCommit: '<c-e>'
moveUpCommit: '<c-u>'
branches:
viewGitFlowOptions: 'I'
setUpstream: 'U'
```
## Custom pull request URLs
@@ -302,29 +472,31 @@ the pull request. You can do so on your `config.yml` file using the following sy
```yaml
services:
"<gitDomain>": "<provider>:<webDomain>"
'<gitDomain>': '<provider>:<webDomain>'
```
Where:
- `gitDomain` stands for the domain used by git itself (i.e. the one present on clone URLs), e.g. `git.work.com`
- `provider` is one of `github`, `bitbucket` or `gitlab`
- `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
In situations where certain naming pattern is used for branches and commits, pattern can be used to populate
commit message with prefix that is parsed from the branch name.
Example:
* Branch name: feature/AB-123
* Commit message: [AB-123] Adding feature
- Branch name: feature/AB-123
- Commit message: [AB-123] Adding feature
```yaml
git:
commitPrefixes:
my_project: # This is repository folder name
pattern: "^\\w+\\/(\\w+-\\w+)"
replace: "[$1] "
git:
commitPrefixes:
my_project: # This is repository folder name
pattern: "^\\w+\\/(\\w+-\\w+).*"
replace: '[$1] '
```
## Custom git log command
@@ -339,3 +511,29 @@ git:
Result:
![](https://i.imgur.com/Nibq35B.png)
## Launching not in a repository behaviour
By default, when launching lazygit from a directory that is not a repository,
you will be prompted to choose if you would like to initialize a repo. You can
override this behaviour in the config with one of the following:
```yaml
# for default prompting behaviour
notARepository: 'prompt'
```
```yaml
# to skip and initialize a new repo
notARepository: 'create'
```
```yaml
# to skip without creating a new repo
notARepository: 'skip'
```
```yaml
# to exit immediately if run outside of the Git repository
notARepository: 'quit'
```

View File

@@ -2,7 +2,7 @@
You can add custom command keybindings in your config.yml (accessible by pressing 'o' on the status panel from within lazygit) like so:
```
```yml
customCommands:
- key: '<c-r>'
command: 'hub browse -- "commit/{{.SelectedLocalCommit.Sha}}"'
@@ -35,6 +35,27 @@ customCommands:
command: "git flow {{index .PromptResponses 0}} start {{index .PromptResponses 1}}"
context: 'localBranches'
loadingText: 'creating branch'
- key : 'r'
description: 'Checkout a remote branch as FETCH_HEAD'
command: "git fetch {{index .PromptResponses 0}} {{index .PromptResponses 1}} && git checkout FETCH_HEAD"
context: 'remotes'
prompts:
- type: 'input'
title: 'Remote:'
initialValue: "{{index .SelectedRemote.Name }}"
- type: 'menuFromCommand'
title: 'Remote branch:'
command: 'git branch -r --list {{index .PromptResponses 0}}/*'
filter: '.*{{index .PromptResponses 0}}/(?P<branch>.*)'
valueFormat: '{{ .branch }}'
labelFormat: '{{ .branch | green }}'
- 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:
@@ -55,6 +76,8 @@ For a given custom command, here are the allowed fields:
| prompts | a list of prompts that will request user input before running the final command | no |
| loadingText | text to display while waiting for command to finish | no |
| description | text to display in the keybindings menu that appears when you press 'x' | no |
| stream | whether you want to stream the command's output to the Command Log panel | no |
| showOutput | whether you want to show the command's output in a gui prompt | no |
### Contexts
@@ -79,12 +102,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 |
| _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_ |
@@ -95,7 +124,7 @@ The permitted option fields are:
If an option has no name the value will be displayed to the user in place of the name, so you're allowed to only include the value like so:
```
```yml
prompts:
- type: 'menu'
title: 'What kind of branch is it?'
@@ -107,13 +136,14 @@ If an option has no name the value will be displayed to the user in place of the
### Placeholder values
Your commands can contain placeholder strings using Go's [template syntax](https://jan.newmarch.name/go/template/chapter-template.html). The template syntax is pretty powerful, letting you do things like conditionals if you want, but for the most part you'll simply want to be accessing the fields on the following objects:
Your commands can contain placeholder strings using Go's [template syntax](https://jan.newmarch.name/golang/template/chapter-template.html). The template syntax is pretty powerful, letting you do things like conditionals if you want, but for the most part you'll simply want to be accessing the fields on the following objects:
```
SelectedLocalCommit
SelectedReflogCommit
SelectedSubCommit
SelectedFile
SelectedPath
SelectedLocalBranch
SelectedRemoteBranch
SelectedRemote
@@ -123,7 +153,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/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
@@ -131,7 +161,7 @@ If your custom keybinding collides with an inbuilt keybinding that is defined fo
### Debugging
If you want to verify that your command actually does what you expect, you can wrap it in an 'echo' call and set `subprocess: true` so that it doesn't actually execute the command but you can see how the placeholders were resolved. Alternatively you can run lazygit in debug mode with `lazygit --debug` and in another terminal window run `lazygit --logs` to see which commands are actually run
If you want to verify that your command actually does what you expect, you can wrap it in an 'echo' call and set `showOutput: true` so that it doesn't actually execute the command but you can see how the placeholders were resolved. Alternatively you can run lazygit in debug mode with `lazygit --debug` and in another terminal window run `lazygit --logs` to see which commands are actually run
### More Examples

View File

@@ -21,7 +21,7 @@ the `colorArg` key is for whether you want the `--color=always` arg in your `git
git:
paging:
colorArg: always
pager: delta --dark --paging=never --24-bit-color=never
pager: delta --dark --paging=never
```
![](https://i.imgur.com/QJpQkF3.png)

View File

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

7
docs/README.md Normal file
View File

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

View File

@@ -1,72 +1,228 @@
_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 Keybindings
## Global Keybindings
<pre>
<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>|</kbd>: view scoping options
<kbd>ctrl+s</kbd>: view filter-by-path options
<kbd>W</kbd>: open diff menu
<kbd>ctrl+e</kbd>: open diff menu
<kbd>@</kbd>: open command log menu
<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
<pre>
<kbd>]</kbd>: next tab
<kbd>[</kbd>: previous tab
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
<kbd>H</kbd>: scroll left
<kbd>L</kbd>: scroll right
<kbd>]</kbd>: next tab
<kbd>[</kbd>: previous tab
</pre>
## Branches Panel (Branches Tab)
## Commit Files
<pre>
<kbd>ctrl+o</kbd>: copy the committed file name to the clipboard
<kbd>c</kbd>: checkout file
<kbd>d</kbd>: discard this commit's changes to this file
<kbd>o</kbd>: open file
<kbd>e</kbd>: edit file
<kbd>space</kbd>: toggle file included in patch
<kbd>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
<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>: squash down
<kbd>f</kbd>: fixup commit
<kbd>r</kbd>: reword commit
<kbd>R</kbd>: reword commit with editor
<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>ctrl+j</kbd>: move commit down one
<kbd>ctrl+k</kbd>: move commit up one
<kbd>v</kbd>: paste commits (cherry-pick)
<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+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>enter</kbd>: view selected item's files
</pre>
## 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>e</kbd>: edit file
<kbd>o</kbd>: open file
<kbd>i</kbd>: ignore or exclude file
<kbd>r</kbd>: refresh files
<kbd>s</kbd>: stash all changes
<kbd>S</kbd>: view stash options
<kbd>a</kbd>: stage/unstage all
<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>: toggle file tree view
<kbd>M</kbd>: open external merge tool (git mergetool)
<kbd>f</kbd>: fetch
</pre>
## Local Branches
<pre>
<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>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>u</kbd>: set/unset upstream
<kbd>enter</kbd>: view commits
</pre>
## Branches Panel (Remote Branches (in Remotes tab))
## Main Panel (Merging)
<pre>
<kbd>esc</kbd>: Return to remotes list
<kbd>e</kbd>: edit file
<kbd>o</kbd>: open file
<kbd>◄</kbd>: select previous conflict
<kbd>►</kbd>: select next conflict
<kbd>▲</kbd>: select previous hunk
<kbd>▼</kbd>: select next hunk
<kbd>z</kbd>: undo
<kbd>M</kbd>: open external merge tool (git mergetool)
<kbd>space</kbd>: pick hunk
<kbd>b</kbd>: pick all hunks
<kbd>esc</kbd>: return to files panel
</pre>
## Main Panel (Normal)
<pre>
<kbd>mouse wheel ▼</kbd>: scroll down (fn+up)
<kbd>mouse wheel ▲</kbd>: scroll up (fn+down)
</pre>
## Main Panel (Patch Building)
<pre>
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
<kbd>o</kbd>: open file
<kbd>e</kbd>: edit file
<kbd>space</kbd>: add/remove line(s) to patch
<kbd>esc</kbd>: exit custom patch builder
</pre>
## Main Panel (Staging)
<pre>
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
<kbd>o</kbd>: open file
<kbd>e</kbd>: edit file
<kbd>esc</kbd>: return to files panel
<kbd>tab</kbd>: switch to other panel (staged/unstaged changes)
<kbd>space</kbd>: toggle line staged / unstaged
<kbd>d</kbd>: delete change (git reset)
<kbd>E</kbd>: edit hunk
</pre>
## Reflog
<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 commits
</pre>
## Remote Branches
<pre>
<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>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>
## Branches Panel (Remotes Tab)
## Remotes
<pre>
<kbd>f</kbd>: fetch remote
@@ -75,20 +231,55 @@
<kbd>e</kbd>: edit remote
</pre>
## Branches Panel (Sub-commits)
## Stash
<pre>
<kbd>enter</kbd>: view commit's files
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: view reset options
<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
<pre>
<kbd>e</kbd>: edit config file
<kbd>o</kbd>: open config file
<kbd>u</kbd>: check for update
<kbd>enter</kbd>: switch to a recent repo
<kbd>a</kbd>: show all branch logs
</pre>
## 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>ctrl+o</kbd>: copy commit SHA to clipboard
<kbd>enter</kbd>: view selected item's files
</pre>
## Branches Panel (Tags Tab)
## 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
@@ -98,173 +289,3 @@
<kbd>g</kbd>: view reset options
<kbd>enter</kbd>: view commits
</pre>
## Commit Files Panel
<pre>
<kbd>ctrl+o</kbd>: copy the committed file name to the clipboard
<kbd>c</kbd>: checkout file
<kbd>d</kbd>: discard this commit's changes to this file
<kbd>o</kbd>: open file
<kbd>e</kbd>: edit file
<kbd>space</kbd>: toggle file included in patch
<kbd>enter</kbd>: enter file to add selected lines to the patch
</pre>
## Commits Panel (Commits)
<pre>
<kbd>s</kbd>: squash down
<kbd>r</kbd>: reword commit
<kbd>R</kbd>: rename commit with editor
<kbd>g</kbd>: reset to this commit
<kbd>f</kbd>: fixup commit
<kbd>F</kbd>: create fixup commit for this commit
<kbd>S</kbd>: squash above commits
<kbd>d</kbd>: delete commit
<kbd>ctrl+j</kbd>: move commit down one
<kbd>ctrl+k</kbd>: move commit up one
<kbd>e</kbd>: edit commit
<kbd>A</kbd>: amend commit with staged changes
<kbd>p</kbd>: pick commit (when mid-rebase)
<kbd>t</kbd>: revert commit
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>v</kbd>: paste commits (cherry-pick)
<kbd>enter</kbd>: view commit's files
<kbd>space</kbd>: checkout commit
<kbd>n</kbd>: create new branch off of commit
<kbd>T</kbd>: tag commit
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
</pre>
## Commits Panel (Reflog Tab)
<pre>
<kbd>enter</kbd>: view commit's files
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: view reset options
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
</pre>
## Files Panel (Files)
<pre>
<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>r</kbd>: refresh files
<kbd>s</kbd>: stash 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
<kbd>f</kbd>: fetch
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
<kbd>g</kbd>: view upstream reset options
</pre>
## Files Panel (Submodules)
<pre>
<kbd>ctrl+o</kbd>: copy submodule name to clipboard
<kbd>enter</kbd>: enter submodule
<kbd>d</kbd>: view reset and remove submodule options
<kbd>u</kbd>: update submodule
<kbd>n</kbd>: add new submodule
<kbd>e</kbd>: update submodule URL
<kbd>i</kbd>: initialize submodule
<kbd>b</kbd>: view bulk submodule options
</pre>
## Main Panel (Merging)
<pre>
<kbd>esc</kbd>: return to files panel
<kbd>space</kbd>: pick hunk
<kbd>b</kbd>: pick both hunks
<kbd>◄</kbd>: select previous conflict
<kbd>►</kbd>: select next conflict
<kbd>▲</kbd>: select top hunk
<kbd>▼</kbd>: select bottom hunk
<kbd>z</kbd>: undo
</pre>
## Main Panel (Normal)
<pre>
<kbd> ̄</kbd>: scroll down (fn+up)
<kbd>¦</kbd>: scroll up (fn+down)
</pre>
## Main Panel (Patch Building)
<pre>
<kbd>esc</kbd>: exit line-by-line mode
<kbd>o</kbd>: open file
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>space</kbd>: add/remove line(s) to patch
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
</pre>
## Main Panel (Staging)
<pre>
<kbd>esc</kbd>: return to files panel
<kbd>space</kbd>: toggle line staged / unstaged
<kbd>d</kbd>: delete change (git reset)
<kbd>tab</kbd>: switch to other panel
<kbd>o</kbd>: open file
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>e</kbd>: edit file
<kbd>o</kbd>: open file
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
<kbd>c</kbd>: commit changes
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>C</kbd>: commit changes using git editor
</pre>
## Menu Panel
<pre>
<kbd>esc</kbd>: close menu
</pre>
## Stash Panel
<pre>
<kbd>enter</kbd>: view stash entry's files
<kbd>space</kbd>: apply
<kbd>g</kbd>: pop
<kbd>d</kbd>: drop
<kbd>n</kbd>: new branch
</pre>
## Status Panel
<pre>
<kbd>e</kbd>: edit config file
<kbd>o</kbd>: open config file
<kbd>u</kbd>: check for update
<kbd>enter</kbd>: switch to a recent repo
</pre>

View File

@@ -0,0 +1,291 @@
_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>e</kbd>: ファイルを編集
<kbd>o</kbd>: ファイルを開く
<kbd>◄</kbd>: 前のコンフリクトを選択
<kbd>►</kbd>: 次のコンフリクトを選択
<kbd>▲</kbd>: 前のhunkを選択
<kbd>▼</kbd>: 次のhunkを選択
<kbd>z</kbd>: アンドゥ
<kbd>M</kbd>: git mergetoolを開く
<kbd>space</kbd>: pick hunk
<kbd>b</kbd>: pick all hunks
<kbd>esc</kbd>: ファイル一覧に戻る
</pre>
## メインパネル (Normal)
<pre>
<kbd>mouse wheel ▼</kbd>: 下にスクロール (fn+up)
<kbd>mouse wheel ▲</kbd>: 上にスクロール (fn+down)
</pre>
## メインパネル (Patch Building)
<pre>
<kbd>◄</kbd>: 前のhunkを選択
<kbd>►</kbd>: 次のhunkを選択
<kbd>v</kbd>: 範囲選択を切り替え
<kbd>V</kbd>: 範囲選択を切り替え
<kbd>a</kbd>: hunk選択を切り替え
<kbd>ctrl+o</kbd>: 選択されたテキストをクリップボードにコピー
<kbd>o</kbd>: ファイルを開く
<kbd>e</kbd>: ファイルを編集
<kbd>space</kbd>: 行をパッチに追加/削除
<kbd>esc</kbd>: exit custom patch builder
</pre>
## メインパネル (Staging)
<pre>
<kbd>◄</kbd>: 前のhunkを選択
<kbd>►</kbd>: 次のhunkを選択
<kbd>v</kbd>: 範囲選択を切り替え
<kbd>V</kbd>: 範囲選択を切り替え
<kbd>a</kbd>: hunk選択を切り替え
<kbd>ctrl+o</kbd>: 選択されたテキストをクリップボードにコピー
<kbd>o</kbd>: ファイルを開く
<kbd>e</kbd>: ファイルを編集
<kbd>esc</kbd>: ファイル一覧に戻る
<kbd>tab</kbd>: パネルを切り替え
<kbd>space</kbd>: 選択行をステージ/アンステージ
<kbd>d</kbd>: 変更を削除 (git reset)
<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>

View File

@@ -0,0 +1,291 @@
_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>e</kbd>: 파일 편집
<kbd>o</kbd>: 파일 닫기
<kbd>◄</kbd>: 이전 충돌을 선택
<kbd>►</kbd>: 다음 충돌을 선택
<kbd>▲</kbd>: 이전 hunk를 선택
<kbd>▼</kbd>: 다음 hunk를 선택
<kbd>z</kbd>: 되돌리기
<kbd>M</kbd>: git mergetool를 열기
<kbd>space</kbd>: pick hunk
<kbd>b</kbd>: pick all hunks
<kbd>esc</kbd>: 파일 목록으로 돌아가기
</pre>
## 메인 패널 (Normal)
<pre>
<kbd>mouse wheel ▼</kbd>: 아래로 스크롤 (fn+up)
<kbd>mouse wheel ▲</kbd>: 위로 스크롤 (fn+down)
</pre>
## 메인 패널 (Patch Building)
<pre>
<kbd>◄</kbd>: 이전 hunk를 선택
<kbd>►</kbd>: 다음 hunk를 선택
<kbd>v</kbd>: 드래그 선택 전환
<kbd>V</kbd>: 드래그 선택 전환
<kbd>a</kbd>: toggle select hunk
<kbd>ctrl+o</kbd>: 선택한 텍스트를 클립보드에 복사
<kbd>o</kbd>: 파일 닫기
<kbd>e</kbd>: 파일 편집
<kbd>space</kbd>: line(s)을 패치에 추가/삭제
<kbd>esc</kbd>: exit custom patch builder
</pre>
## 메인 패널 (Staging)
<pre>
<kbd>◄</kbd>: 이전 hunk를 선택
<kbd>►</kbd>: 다음 hunk를 선택
<kbd>v</kbd>: 드래그 선택 전환
<kbd>V</kbd>: 드래그 선택 전환
<kbd>a</kbd>: toggle select hunk
<kbd>ctrl+o</kbd>: 선택한 텍스트를 클립보드에 복사
<kbd>o</kbd>: 파일 닫기
<kbd>e</kbd>: 파일 편집
<kbd>esc</kbd>: 파일 목록으로 돌아가기
<kbd>tab</kbd>: 패널 전환
<kbd>space</kbd>: 선택한 행을 staged / unstaged
<kbd>d</kbd>: 변경을 삭제 (git reset)
<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>

View File

@@ -1,105 +1,96 @@
_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 Sneltoetsen
## Globaale Sneltoetsen
## Globale Sneltoetsen
<pre>
<kbd>pgup</kbd>: scroll naar beneden vanaf hooft paneel (fn+up)
<kbd>pgdown</kbd>: scroll naar beneden vabaf hooft paneel (fn+down)
<kbd>ctrl+r</kbd>: wissel naar een recente repo
<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 schermmode (normaal/half/groot )
<kbd>_</kbd>: vorige schermmode
<kbd>:</kbd>: voor aangepast commando uit
<kbd>|</kbd>: bekijk scoping opties
<kbd>+</kbd>: volgende scherm modus (normaal/half/groot)
<kbd>_</kbd>: vorige scherm modus
<kbd>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>
## List Panel Navigation
## Lijstpaneel Navigatie
<pre>
<kbd>]</kbd>: volgende tab
<kbd>[</kbd>: vorige tab
<kbd>,</kbd>: vorige pagina
<kbd>.</kbd>: volgende pagina
<kbd><</kbd>: scroll naar boven
<kbd>/</kbd>: start met zoekken
<kbd>/</kbd>: start met zoeken
<kbd>></kbd>: scroll naar beneden
<kbd>H</kbd>: scroll left
<kbd>L</kbd>: scroll right
<kbd>]</kbd>: volgende tabblad
<kbd>[</kbd>: vorige tabblad
</pre>
## Branches Paneel (Branches Tab)
## 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>o</kbd>: maak een pull-aanvraag
<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>: copieer branch name naar clipboard
<kbd>enter</kbd>: view commits
<kbd>u</kbd>: set/unset upstream
<kbd>enter</kbd>: bekijk commits
</pre>
## Branches Paneel (Remote Branches (in Remotes tab))
<pre>
<kbd>esc</kbd>: Ga terug naar remotes lijst
<kbd>g</kbd>: bekijk reset opties
<kbd>enter</kbd>: view 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 Tab)
<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>: kopiëer commit (cherry-pick)
<kbd>C</kbd>: kopiëer commit reeks (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (gecopieerde) commits selectie
<kbd>ctrl+o</kbd>: copieer commit SHA naar clipboard
</pre>
## Branches Paneel (Tags Tab)
<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>: view commits
</pre>
## Commit bestanden Paneel
## Commit bestanden
<pre>
<kbd>ctrl+o</kbd>: kopieer de vastgelegde bestandsnaam naar het klembord
@@ -108,163 +99,193 @@
<kbd>o</kbd>: open bestand
<kbd>e</kbd>: verander bestand
<kbd>space</kbd>: toggle bestand inbegrepen in patch
<kbd>enter</kbd>: enter bestand to add selecteered lines to the patch
<kbd>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+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>: kopiëer commit (cherry-pick)
<kbd>ctrl+o</kbd>: copieer commit SHA naar clipboard
<kbd>C</kbd>: kopiëer commit reeks (cherry-pick)
<kbd>v</kbd>: plak commits (cherry-pick)
<kbd>enter</kbd>: bekijk gecommite bestanden
<kbd>space</kbd>: checkout commit
<kbd>n</kbd>: create new branch off of 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 (gecopieerde) commits selectie
</pre>
## Commits Paneel (Reflog Tab)
<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>: kopiëer commit (cherry-pick)
<kbd>C</kbd>: kopiëer commit reeks (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (gecopieerde) commits selectie
<kbd>ctrl+o</kbd>: copieer commit SHA naar clipboard
<kbd>c</kbd>: kopieer commit (cherry-pick)
<kbd>C</kbd>: kopieer commit reeks (cherry-pick)
<kbd>enter</kbd>: bekijk gecommite bestanden
</pre>
## Bestanden Paneel (Bestanden)
## Mergen
<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
</pre>
## Bestanden Paneel (Submodules)
<pre>
<kbd>ctrl+o</kbd>: copy submodule name to clipboard
<kbd>enter</kbd>: enter submodule
<kbd>d</kbd>: view reset and remove submodule options
<kbd>u</kbd>: update submodule
<kbd>n</kbd>: add new submodule
<kbd>e</kbd>: update submodule URL
<kbd>i</kbd>: initialize submodule
<kbd>b</kbd>: view bulk submodule options
</pre>
## Hooft Paneel (Merggen)
<pre>
<kbd>esc</kbd>: ga terug naar het bestanden paneel
<kbd>space</kbd>: kies hunk
<kbd>b</kbd>: kies bijde hunks
<kbd>◄</kbd>: selecteer voorgaand conflict
<kbd>►</kbd>: selecteer volgende conflict
<kbd>▲</kbd>: selecteer bovenste hunk
<kbd>▼</kbd>: selecteer onderste hunk
<kbd>z</kbd>: ongedaan maken
<kbd>M</kbd>: open external merge tool (git mergetool)
<kbd>space</kbd>: kies hunk
<kbd>b</kbd>: kies bijde hunks
<kbd>esc</kbd>: ga terug naar het bestanden paneel
</pre>
## Hooft Paneel (Normaal)
## Normaal
<pre>
<kbd></kbd>: scroll omlaag (fn+up)
<kbd></kbd>: scroll omhoog (fn+down)
<kbd>mouse wheel ▼</kbd>: scroll omlaag (fn+up)
<kbd>mouse wheel ▲</kbd>: scroll omhoog (fn+down)
</pre>
## Hooft Paneel (Patch Bouwen)
## Patch Bouwen
<pre>
<kbd>esc</kbd>: sluit lijn-bij-lijn mode
<kbd>o</kbd>: open bestand
<kbd>▲</kbd>: selecteer de vorige lijn
<kbd>▼</kbd>: selecteer de volgende lijn
<kbd>◄</kbd>: selecteer de vorige hunk
<kbd>►</kbd>: selecteer de volgende hunk
<kbd>v</kbd>: toggle drag selecteer
<kbd>V</kbd>: toggle drag selecteer
<kbd>a</kbd>: toggle selecteer hunk
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
<kbd>o</kbd>: open bestand
<kbd>e</kbd>: verander bestand
<kbd>space</kbd>: voeg toe/verwijder lijn(en) in patch
<kbd>v</kbd>: toggle drag selecteer
<kbd>V</kbd>: toggle drag selecteer
<kbd>a</kbd>: toggle selecteer hunk
<kbd>esc</kbd>: sluit lijn-bij-lijn modus
</pre>
## Hooft 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>◄</kbd>: selecteer de vorige hunk
<kbd>►</kbd>: selecteer de volgende hunk
<kbd>v</kbd>: toggle drag selecteer
<kbd>V</kbd>: toggle drag selecteer
<kbd>a</kbd>: toggle selecteer hunk
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
<kbd>o</kbd>: open bestand
<kbd>e</kbd>: verander bestand
<kbd>esc</kbd>: ga terug naar het bestanden paneel
<kbd>tab</kbd>: ga naar een ander paneel
<kbd>space</kbd>: toggle lijnen staged / unstaged
<kbd>d</kbd>: verwijdert change (git reset)
<kbd>tab</kbd>: ga naar een ander paneel
<kbd>o</kbd>: open bestand
<kbd>▲</kbd>: selecteer de vorige lijn
<kbd>▼</kbd>: selecteer de volgende lijn
<kbd>◄</kbd>: selecteer de vorige hunk
<kbd>►</kbd>: selecteer de volgende hunk
<kbd>e</kbd>: verander bestand
<kbd>o</kbd>: open bestand
<kbd>v</kbd>: toggle drag selecteer
<kbd>V</kbd>: toggle drag selecteer
<kbd>a</kbd>: toggle selecteer hunk
<kbd>c</kbd>: Commit veranderingen
<kbd>w</kbd>: commit veranderingen zonder pre-commit hook
<kbd>C</kbd>: commit veranderingen met de git editor
<kbd>E</kbd>: edit hunk
</pre>
## Menu Paneel
## Stash
<pre>
<kbd>esc</kbd>: sluit menu
</pre>
## Stash Paneel
<pre>
<kbd>enter</kbd>: view stash entry's files
<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
<kbd>o</kbd>: open config bestand
<kbd>u</kbd>: check voor updates
<kbd>enter</kbd>: wissel naar een recente repo
<kbd>a</kbd>: alle logs van de branch laten zien
</pre>
## 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>

View File

@@ -1,72 +1,205 @@
_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 Keybindings
## Globalne
<pre>
<kbd>pgup</kbd>: scroll up main panel (fn+up)
<kbd>pgdown</kbd>: scroll down main panel (fn+down)
<kbd>m</kbd>: view merge/rebase options
<kbd>ctrl+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>: execute custom command
<kbd>|</kbd>: view scoping options
<kbd>ctrl+s</kbd>: view filter-by-path options
<kbd>W</kbd>: open diff menu
<kbd>ctrl+e</kbd>: open diff menu
<kbd>@</kbd>: open command log menu
<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
<pre>
<kbd>]</kbd>: next tab
<kbd>[</kbd>: previous tab
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
<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>o</kbd>: utwórz żądanie wyciągnięcia
<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>: rebase branch
<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>: view reset options
<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))
## Main Panel (Patch Building)
<pre>
<kbd>esc</kbd>: return to remotes list
<kbd>g</kbd>: view reset options
<kbd></kbd>: poprzedni kawałek
<kbd></kbd>: następny kawałek
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
<kbd>o</kbd>: otwórz plik
<kbd>e</kbd>: edytuj plik
<kbd>space</kbd>: add/remove line(s) to patch
<kbd>esc</kbd>: wyście z trybu "linia po linii"
</pre>
## 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>◄</kbd>: poprzedni kawałek
<kbd>►</kbd>: następny kawałek
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
<kbd>o</kbd>: otwórz plik
<kbd>e</kbd>: edytuj plik
<kbd>esc</kbd>: wróć do panelu plików
<kbd>tab</kbd>: switch to other panel (staged/unstaged changes)
<kbd>space</kbd>: toggle line staged / unstaged
<kbd>d</kbd>: delete change (git reset)
<kbd>E</kbd>: edit hunk
</pre>
## Reflog
<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>: view commits
</pre>
## 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>r</kbd>: rebase branch
<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>
## Gałęzie Panel (Remotes Tab)
## Remotes
<pre>
<kbd>f</kbd>: fetch remote
@@ -75,111 +208,63 @@
<kbd>e</kbd>: edit remote
</pre>
## Gałęzie Panel (Sub-commits)
## Scalanie
<pre>
<kbd>enter</kbd>: view commit's files
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: view reset options
<kbd>e</kbd>: edytuj plik
<kbd>o</kbd>: otwórz plik
<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
<kbd>M</kbd>: open external merge tool (git mergetool)
<kbd>space</kbd>: wybierz kawałek
<kbd>b</kbd>: wybierz wszystkie kawałki
<kbd>esc</kbd>: wróć do panelu plików
</pre>
## Schowek
<pre>
<kbd>space</kbd>: zastosuj
<kbd>g</kbd>: wyciągnij
<kbd>d</kbd>: porzuć
<kbd>n</kbd>: nowa gałąź
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>enter</kbd>: przeglądaj pliki commita
</pre>
## Status
<pre>
<kbd>e</kbd>: edytuj konfigurację
<kbd>o</kbd>: otwórz konfigurację
<kbd>u</kbd>: sprawdź aktualizacje
<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
</pre>
## Gałęzie Panel (Tags Tab)
<pre>
<kbd>space</kbd>: przełącz
<kbd>d</kbd>: delete tag
<kbd>P</kbd>: push tag
<kbd>n</kbd>: create tag
<kbd>g</kbd>: view reset options
<kbd>enter</kbd>: view commits
</pre>
## Commit files Panel
<pre>
<kbd>ctrl+o</kbd>: copy the committed file name to the clipboard
<kbd>c</kbd>: checkout file
<kbd>d</kbd>: discard this commit's changes to this file
<kbd>o</kbd>: otwórz plik
<kbd>e</kbd>: edytuj plik
<kbd>space</kbd>: toggle file included in patch
<kbd>enter</kbd>: enter file to add selected lines to the patch
</pre>
## Commity Panel (Commity)
<pre>
<kbd>s</kbd>: ściśnij w dół
<kbd>r</kbd>: przemianuj commit
<kbd>R</kbd>: przemianuj commit w edytorze
<kbd>g</kbd>: zresetuj do tego commita
<kbd>f</kbd>: napraw commit
<kbd>F</kbd>: create fixup commit for this commit
<kbd>S</kbd>: squash above commits
<kbd>d</kbd>: delete commit
<kbd>ctrl+j</kbd>: move commit down one
<kbd>ctrl+k</kbd>: move commit up one
<kbd>e</kbd>: edit commit
<kbd>A</kbd>: amend commit with staged changes
<kbd>p</kbd>: pick commit (when mid-rebase)
<kbd>t</kbd>: revert commit
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>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>y</kbd>: copy commit attribute
<kbd>o</kbd>: open commit in browser
<kbd>n</kbd>: create new branch off of commit
<kbd>T</kbd>: tag 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>
## Commity Panel (Reflog Tab)
<pre>
<kbd>enter</kbd>: view commit's files
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: view reset options
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
</pre>
## Pliki Panel (Pliki)
<pre>
<kbd>c</kbd>: commituj zmiany
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>A</kbd>: zmień ostatnie zatwierdzenie
<kbd>C</kbd>: commituj zmiany używając edytora z gita
<kbd>space</kbd>: przełącz zatwierdzenie
<kbd>d</kbd>: view 'discard changes' options
<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 pliki
<kbd>S</kbd>: view stash options
<kbd>a</kbd>: przełącz wszystkie zatwierdzenia
<kbd>D</kbd>: view reset options
<kbd>enter</kbd>: zatwierdź pojedyncze linie
<kbd>f</kbd>: fetch
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
<kbd>g</kbd>: view upstream reset options
</pre>
## Pliki Panel (Submodules)
## Submodules
<pre>
<kbd>ctrl+o</kbd>: copy submodule name to clipboard
<kbd>enter</kbd>: enter submodule
<kbd>d</kbd>: view reset and remove submodule options
<kbd>d</kbd>: remove submodule
<kbd>u</kbd>: update submodule
<kbd>n</kbd>: add new submodule
<kbd>e</kbd>: update submodule URL
@@ -187,84 +272,20 @@
<kbd>b</kbd>: view bulk submodule options
</pre>
## Main Panel (Merging)
## Tags
<pre>
<kbd>esc</kbd>: wróć do panelu plików
<kbd>space</kbd>: pick hunk
<kbd>b</kbd>: pick both hunks
<kbd></kbd>: select previous conflict
<kbd></kbd>: select next conflict
<kbd></kbd>: select top hunk
<kbd>▼</kbd>: select bottom hunk
<kbd>z</kbd>: cofnij
<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>
## Main Panel (Normal)
## Zwykłe
<pre>
<kbd> ̄</kbd>: scroll down (fn+up)
<kbd>¦</kbd>: scroll up (fn+down)
</pre>
## Main Panel (Patch Building)
<pre>
<kbd>esc</kbd>: exit line-by-line mode
<kbd>o</kbd>: otwórz plik
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>space</kbd>: add/remove line(s) to patch
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
</pre>
## Main Panel (Zatwierdzanie)
<pre>
<kbd>esc</kbd>: wróć do panelu plików
<kbd>space</kbd>: toggle line staged / unstaged
<kbd>d</kbd>: delete change (git reset)
<kbd>tab</kbd>: switch to other panel
<kbd>o</kbd>: otwórz plik
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>e</kbd>: edytuj plik
<kbd>o</kbd>: otwórz plik
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
<kbd>c</kbd>: commituj zmiany
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>C</kbd>: commituj zmiany używając edytora z gita
</pre>
## Menu Panel
<pre>
<kbd>esc</kbd>: close menu
</pre>
## Schowek Panel
<pre>
<kbd>enter</kbd>: view stash entry's files
<kbd>space</kbd>: zastosuj
<kbd>g</kbd>: wyciągnij
<kbd>d</kbd>: porzuć
<kbd>n</kbd>: nowa gałąź
</pre>
## Status Panel
<pre>
<kbd>e</kbd>: edytuj plik konfiguracyjny
<kbd>o</kbd>: otwórz plik konfiguracyjny
<kbd>u</kbd>: sprawdź aktualizacje
<kbd>enter</kbd>: switch to a recent repo
<kbd>mouse wheel ▼</kbd>: przewiń w dół (fn+up)
<kbd>mouse wheel ▲</kbd>: przewiń w górę (fn+down)
</pre>

View File

@@ -0,0 +1,291 @@
_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>: 查看 合并/变基 选项
<kbd>ctrl+p</kbd>: 查看自定义补丁选项
<kbd>R</kbd>: 刷新
<kbd>x</kbd>: 打开菜单
<kbd>+</kbd>: 下一屏模式(正常/半屏/全屏)
<kbd>_</kbd>: 上一屏模式
<kbd>ctrl+s</kbd>: 查看按路径过滤选项
<kbd>W</kbd>: 打开 diff 菜单
<kbd>ctrl+e</kbd>: 打开 diff 菜单
<kbd>@</kbd>: 打开命令日志菜单
<kbd>}</kbd>: 扩大差异视图中显示的上下文范围
<kbd>{</kbd>: 缩小差异视图中显示的上下文范围
<kbd>:</kbd>: 执行自定义命令
<kbd>z</kbd>: (通过 reflog撤销「实验功能」
<kbd>ctrl+z</kbd>: (通过 reflog重做「实验功能」
<kbd>P</kbd>: 推送
<kbd>p</kbd>: 拉取
</pre>
## 列表面板导航
<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>: 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>d</kbd>: 删除分支
<kbd>r</kbd>: 将已检出的分支变基到该分支
<kbd>M</kbd>: 合并到当前检出的分支
<kbd>f</kbd>: 从上游快进此分支
<kbd>g</kbd>: 查看重置选项
<kbd>R</kbd>: 重命名分支
<kbd>u</kbd>: set/unset upstream
<kbd>enter</kbd>: 查看提交
</pre>
## 子提交
<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>enter</kbd>: 输入子模块
<kbd>d</kbd>: 删除子模块
<kbd>u</kbd>: 更新子模块
<kbd>n</kbd>: 添加新的子模块
<kbd>e</kbd>: 更新子模块 URL
<kbd>i</kbd>: 初始化子模块
<kbd>b</kbd>: 查看批量子模块选项
</pre>
## 提交
<pre>
<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>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>◄</kbd>: 选择上一个区块
<kbd>►</kbd>: 选择下一个区块
<kbd>v</kbd>: 切换拖动选择
<kbd>V</kbd>: 切换拖动选择
<kbd>a</kbd>: 切换选择区块
<kbd>ctrl+o</kbd>: 将选中文本复制到剪贴板
<kbd>o</kbd>: 打开文件
<kbd>e</kbd>: 编辑文件
<kbd>space</kbd>: 添加/移除 行到补丁
<kbd>esc</kbd>: 退出逐行模式
</pre>
## 标签页面
<pre>
<kbd>space</kbd>: 检出
<kbd>d</kbd>: 删除标签
<kbd>P</kbd>: 推送标签
<kbd>n</kbd>: 创建标签
<kbd>g</kbd>: 查看重置选项
<kbd>enter</kbd>: 查看提交
</pre>
## 正在合并
<pre>
<kbd>e</kbd>: 编辑文件
<kbd>o</kbd>: 打开文件
<kbd>◄</kbd>: 选择上一个冲突
<kbd>►</kbd>: 选择下一个冲突
<kbd>▲</kbd>: 选择顶部块
<kbd>▼</kbd>: 选择底部块
<kbd>z</kbd>: 撤销
<kbd>M</kbd>: 打开外部合并工具 (git mergetool)
<kbd>space</kbd>: 选中区块
<kbd>b</kbd>: 选中所有区块
<kbd>esc</kbd>: 返回文件面板
</pre>
## 正在暂存
<pre>
<kbd>◄</kbd>: 选择上一个区块
<kbd>►</kbd>: 选择下一个区块
<kbd>v</kbd>: 切换拖动选择
<kbd>V</kbd>: 切换拖动选择
<kbd>a</kbd>: 切换选择区块
<kbd>ctrl+o</kbd>: 将选中文本复制到剪贴板
<kbd>o</kbd>: 打开文件
<kbd>e</kbd>: 编辑文件
<kbd>esc</kbd>: 返回文件面板
<kbd>tab</kbd>: 切换到其他面板
<kbd>space</kbd>: 切换行暂存状态
<kbd>d</kbd>: 取消变更 (git reset)
<kbd>E</kbd>: edit hunk
</pre>
## 正常
<pre>
<kbd>mouse wheel ▼</kbd>: 向下滚动 (fn+up)
<kbd>mouse wheel ▲</kbd>: 向上滚动 (fn+down)
</pre>
## 状态
<pre>
<kbd>e</kbd>: 编辑配置文件
<kbd>o</kbd>: 打开配置文件
<kbd>u</kbd>: 检查更新
<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>

74
go.mod
View File

@@ -1,40 +1,74 @@
module github.com/jesseduffield/lazygit
go 1.14
go 1.18
require (
github.com/OpenPeeDeeP/xdg v1.0.0
github.com/atotto/clipboard v0.1.2
github.com/aybabtme/humanlog v0.4.1
github.com/cli/safeexec v1.0.0
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
github.com/creack/pty v1.1.11
github.com/fatih/color v1.9.0
github.com/fsmiamoto/git-todo-parser v0.0.2
github.com/fsnotify/fsnotify v1.4.7
github.com/go-errors/errors v1.1.1
github.com/go-logfmt/logfmt v0.5.0 // indirect
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
github.com/golang/protobuf v1.3.2 // indirect
github.com/google/go-cmp v0.3.1 // indirect
github.com/gdamore/tcell/v2 v2.5.2
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.20201010224802-8a6768078fd7
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe
github.com/jesseduffield/gocui v0.3.1-0.20220815095708-156fda5e0419
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/mattn/go-colorable v0.1.7 // indirect
github.com/mattn/go-runewidth v0.0.9
github.com/kyokomi/emoji/v2 v2.2.8
github.com/lucasb-eyer/go-colorful v1.2.0
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/sasha-s/go-deadlock v0.3.1
github.com/sirupsen/logrus v1.4.2
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
github.com/stretchr/testify v1.4.0
github.com/tcnksm/go-gitconfig v0.1.2
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-20201005172224-997123666555 // indirect
github.com/stretchr/testify v1.7.0
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
)
replace github.com/go-git/go-git/v5 => github.com/jesseduffield/go-git/v5 v5.1.1
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/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/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect
github.com/rivo/uniseg v0.3.4 // 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-20220811171246-fbc7d0a398ab // indirect
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 // indirect
golang.org/x/text v0.3.7 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)

121
go.sum
View File

@@ -11,49 +11,56 @@ github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn
github.com/aybabtme/humanlog v0.4.1 h1:D8d9um55rrthJsP8IGSHBcti9lTb/XknmDAX6Zy8tek=
github.com/aybabtme/humanlog v0.4.1/go.mod h1:B0bnQX4FTSU3oftPMTTPvENCy8LqixLDvYJA9TUCAGo=
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI=
github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/fatih/color v1.7.1-0.20180516100307-2d684516a886 h1:NAFoy+QgUpERgK3y1xiVh5HcOvSeZHpXTTo5qnvnuK4=
github.com/fatih/color v1.7.1-0.20180516100307-2d684516a886/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
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/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
github.com/gdamore/tcell/v2 v2.5.2 h1:tKzG29kO9p2V++3oBY2W9zUjYu7IK1MENFeY/BzJSVY=
github.com/gdamore/tcell/v2 v2.5.2/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.1.1 h1:ljK/pL5ltg3qoN+OtN6yCv9HWSfMwxSx90GJCZQxYNg=
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
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=
github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12 h1:PbKy9zOy4aAKrJ5pibIRpVO2BXnK1Tlcg+caKI7Ox5M=
github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/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/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
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.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/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=
github.com/gookit/color v1.4.2 h1:tXy44JFSFkKnELV6WaMo/lLfu/meqITX3iAV52do7lk=
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
@@ -61,12 +68,16 @@ 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.20201010224802-8a6768078fd7 h1:K3MGrjmpPtIhfXmKh/zsIF0CdmNKOkjpIwcUfAa/J2A=
github.com/jesseduffield/gocui v0.3.1-0.20201010224802-8a6768078fd7/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe h1:qsVhCf2RFyyKIUe/+gJblbCpXMUki9rZrHuEctg6M/E=
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4=
github.com/jesseduffield/gocui v0.3.1-0.20220815095708-156fda5e0419 h1:p3Ix7RUcy4X16Lk5jTSfTxecJT7ryqYHclfRbo/Svzs=
github.com/jesseduffield/gocui v0.3.1-0.20220815095708-156fda5e0419/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=
github.com/jesseduffield/yaml v2.1.0+incompatible/go.mod h1:w0xGhOSIJCGYYW+hnFPTutCy5aACpkcwbmORt5axGqk=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
@@ -79,26 +90,30 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/kyokomi/emoji/v2 v2.2.8 h1:jcofPxjHWEkJtkIbcLHvZhxKgCPl6C7MyjTrD4KDqUE=
github.com/kyokomi/emoji/v2 v2.2.8/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mgutz/str v1.2.0 h1:4IzWSdIz9qPQWLfKZ0rJcV0jcUDpxvP4JVZ4GXQyvSw=
github.com/mgutz/str v1.2.0/go.mod h1:w1v0ofgLaJdoD0HpQ3fycxKD1WtxpjSo151pK/31q6w=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
@@ -110,10 +125,25 @@ github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY=
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ=
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.3.4 h1:3Z3Eu6FGHZWSfNKJTOUiPatWwfc7DzJRU04jFUqJODw=
github.com/rivo/uniseg v0.3.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
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/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0=
github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
@@ -123,24 +153,28 @@ github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKk
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE=
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=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/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 h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c h1:dk0ukUIHmGHqASjP0iue2261isepFCC6XRCSd1nHgDw=
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c/go.mod h1:iQL9McJNjoIa5mjH6nYTCTZXUN6RP+XW3eib7Ya3XcI=
@@ -153,24 +187,32 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201005172224-997123666555 h1:fihtqzYxy4E31W1yUlyRGveTZT1JIP0bmKaDZ2ceKAw=
golang.org/x/sys v0.0.0-20201005172224-997123666555/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-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-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/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-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.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/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 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0 h1:KzcWKJ0nMAmGoBhYVMnkWc1rXjB42lKy5aIys4TdLOA=
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0/go.mod h1:XoytMOotjRRJVkIsQdxsPIioRLYFISEaY9a4tftOXAo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
@@ -179,3 +221,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/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=

131
main.go
View File

@@ -1,139 +1,24 @@
package main
import (
"bytes"
"fmt"
"log"
"os"
"path/filepath"
"runtime"
"github.com/go-errors/errors"
"github.com/integrii/flaggy"
"github.com/jesseduffield/lazygit/pkg/app"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/env"
yaml "github.com/jesseduffield/yaml"
)
// These values may be set by the build script via the LDFLAGS argument
var (
commit string
version = "unversioned"
date string
version string
buildSource = "unknown"
)
func main() {
flaggy.DefaultParser.ShowVersionWithVersionFlag = false
repoPath := ""
flaggy.String(&repoPath, "p", "path", "Path of git repo. (equivalent to --work-tree=<path> --git-dir=<path>/.git/)")
filterPath := ""
flaggy.String(&filterPath, "f", "filter", "Path to filter on in `git log -- <path>`. When in filter mode, the commits, reflog, and stash are filtered based on the given path, and some operations are restricted")
dump := ""
flaggy.AddPositionalValue(&dump, "gitargs", 1, false, "Todo file")
flaggy.DefaultParser.PositionalFlags[0].Hidden = true
versionFlag := false
flaggy.Bool(&versionFlag, "v", "version", "Print the current version")
debuggingFlag := false
flaggy.Bool(&debuggingFlag, "d", "debug", "Run in debug mode with logging (see --logs flag below). Use the LOG_LEVEL env var to set the log level (debug/info/warn/error)")
logFlag := false
flaggy.Bool(&logFlag, "l", "logs", "Tail lazygit logs (intended to be used when `lazygit --debug` is called in a separate terminal tab)")
configFlag := false
flaggy.Bool(&configFlag, "c", "config", "Print the default config")
configDirFlag := false
flaggy.Bool(&configDirFlag, "cd", "print-config-dir", "Print the config directory")
useConfigDir := ""
flaggy.String(&useConfigDir, "ucd", "use-config-dir", "override default config directory with provided directory")
workTree := ""
flaggy.String(&workTree, "w", "work-tree", "equivalent of the --work-tree git argument")
gitDir := ""
flaggy.String(&gitDir, "g", "git-dir", "equivalent of the --git-dir git argument")
flaggy.Parse()
if repoPath != "" {
if workTree != "" || gitDir != "" {
log.Fatal("--path option is incompatible with the --work-tree and --git-dir options")
}
workTree = repoPath
gitDir = filepath.Join(repoPath, ".git")
ldFlagsBuildInfo := &app.BuildInfo{
Commit: commit,
Date: date,
Version: version,
BuildSource: buildSource,
}
if useConfigDir != "" {
os.Setenv("CONFIG_DIR", useConfigDir)
}
if workTree != "" {
env.SetGitWorkTreeEnv(workTree)
}
if gitDir != "" {
env.SetGitDirEnv(gitDir)
}
if versionFlag {
fmt.Printf("commit=%s, build date=%s, build source=%s, version=%s, os=%s, arch=%s\n", commit, date, buildSource, version, runtime.GOOS, runtime.GOARCH)
os.Exit(0)
}
if configFlag {
var buf bytes.Buffer
encoder := yaml.NewEncoder(&buf)
err := encoder.Encode(config.GetDefaultConfig())
if err != nil {
log.Fatal(err.Error())
}
fmt.Printf("%s\n", buf.String())
os.Exit(0)
}
if configDirFlag {
fmt.Printf("%s\n", config.ConfigDir())
os.Exit(0)
}
if logFlag {
app.TailLogs()
os.Exit(0)
}
if workTree != "" {
if err := os.Chdir(workTree); err != nil {
log.Fatal(err.Error())
}
}
appConfig, err := config.NewAppConfig("lazygit", version, commit, date, buildSource, debuggingFlag)
if err != nil {
log.Fatal(err.Error())
}
app, err := app.NewApp(appConfig, filterPath)
if err == nil {
err = app.Run()
}
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\n\n%s", app.Tr.ErrorOccurred, stackTrace))
}
app.Start(ldFlagsBuildInfo, nil)
}

View File

@@ -2,118 +2,101 @@ package app
import (
"bufio"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/aybabtme/humanlog"
"github.com/go-errors/errors"
"github.com/jesseduffield/generics/slices"
appTypes "github.com/jesseduffield/lazygit/pkg/app/types"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/git_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/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 {
closers []io.Closer
Config config.AppConfigurer
Log *logrus.Entry
OSCommand *oscommands.OSCommand
GitCommand *commands.GitCommand
Gui *gui.Gui
Tr *i18n.TranslationSet
Updater *updates.Updater // may only need this on the Gui
ClientContext string
*common.Common
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 appTypes.StartArgs,
) {
app, err := NewApp(config, common)
func newProductionLogger(config config.AppConfigurer) *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(configurer config.AppConfigurer) *logrus.Logger {
logger := logrus.New()
logger.SetLevel(getLogLevel())
logPath, err := config.LogPath()
func NewCommon(config config.AppConfigurer) (*common.Common, error) {
userConfig := config.GetUserConfig()
var err error
log := newLogger(config)
tr, err := i18n.NewTranslationSetFromConfig(log, userConfig.Gui.Language)
if err != nil {
log.Fatal(err)
}
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
panic("unable to log to file") // TODO: don't panic (also, remove this call to the `panic` function)
}
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(config)
} else {
log = newProductionLogger(config)
return nil, err
}
// 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(),
})
return &common.Common{
Log: log,
Tr: tr,
UserConfig: userConfig,
Debug: config.GetDebug(),
}, nil
}
// NewApp bootstrap a new application
func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
func NewApp(config config.AppConfigurer, common *common.Common) (*App, error) {
app := &App{
closers: []io.Closer{},
Config: config,
Common: common,
}
app.OSCommand = oscommands.NewOSCommand(common, config, oscommands.GetPlatform(), oscommands.NewNullGuiIO(app.Log))
var err error
app.Log = newLogger(config)
app.Tr = i18n.NewTranslationSet(app.Log)
// 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.Updater, err = updates.NewUpdater(common, config, app.OSCommand)
if err != nil {
return app, err
}
app.OSCommand = oscommands.NewOSCommand(app.Log, config)
app.Updater, err = updates.NewUpdater(app.Log, config, app.OSCommand, app.Tr)
dirName, err := os.Getwd()
if err != nil {
return app, err
}
@@ -123,31 +106,69 @@ func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
return app, err
}
app.GitCommand, err = commands.NewGitCommand(app.Log, app.OSCommand, app.Tr, app.Config)
gitConfig := git_config.NewStdCachedGitConfig(app.Log)
app.Gui, err = gui.NewGui(common, config, gitConfig, app.Updater, showRecentRepos, dirName)
if err != nil {
return app, err
}
app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config, app.Updater, filterPath, showRecentRepos)
if err != nil {
return app, err
if app.Gui.Config.GetUserConfig().Git.EnableGhCommand {
if err := app.validateGhVersion(); err != nil {
return nil, err
}
}
return app, nil
}
func (app *App) validateGitVersion() error {
output, err := app.OSCommand.RunCommandWithOutput("git --version")
// if we get an error anywhere here we'll show the same status
minVersionError := errors.New(app.Tr.MinGitVersionError)
func (app *App) validateGhVersion() error {
output, err := app.OSCommand.Cmd.New("gh --version").RunWithOutput()
if err != nil {
return minVersionError
return fmt.Errorf(app.Tr.FailedToObtainGhVersionError, err.Error())
}
if isGitVersionValid(output) {
return nil
if !isGhVersionValid(output) {
return errors.New(app.Tr.MinGhVersionError)
}
return minVersionError
return nil
}
func isGhVersionValid(versionStr string) bool {
// output should be something like:
// gh version 2.0.0 (2021-08-23)
// https://github.com/cli/cli/releases/tag/v2.0.0
re := regexp.MustCompile(`[^\d]+([\d\.]+)`)
matches := re.FindStringSubmatch(versionStr)
if len(matches) == 0 {
return false
}
ghVersion := matches[1]
majorVersion, err := strconv.Atoi(ghVersion[0:1])
if err != nil {
return false
}
if majorVersion < 2 {
return false
}
return true
}
func (app *App) validateGitVersion() error {
output, err := app.OSCommand.Cmd.New("git --version").RunWithOutput()
if err != nil {
return fmt.Errorf(app.Tr.FailedToObtainGitVersionError, err.Error())
}
if !isGitVersionValid(output) {
return errors.New(app.Tr.MinGitVersionError)
}
return nil
}
func isGitVersionValid(versionStr string) bool {
@@ -171,165 +192,125 @@ func isGitVersionValid(versionStr string) bool {
return true
}
func isDirectoryAGitRepository(dir string) (bool, error) {
info, err := os.Stat(filepath.Join(dir, ".git"))
return info != nil, err
}
func openRecentRepo(app *App) bool {
for _, repoDir := range app.Config.GetAppState().RecentRepos {
if isRepo, _ := isDirectoryAGitRepository(repoDir); isRepo {
if err := os.Chdir(repoDir); err == nil {
return true
}
}
}
return false
}
func (app *App) setupRepo() (bool, error) {
if err := app.validateGitVersion(); err != nil {
return false, err
}
if env.GetGitDirEnv() != "" {
// we've been given the git dir directly. We'll verify this dir when initializing our GitCommand object
// we've been given the git dir directly. We'll verify this dir when initializing our Git object
return false, nil
}
// if we are not in a git repo, we ask if we want to `git init`
if err := app.OSCommand.RunCommand("git status"); err != nil {
if err := commands.VerifyInGitRepo(app.OSCommand); err != nil {
cwd, err := os.Getwd()
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.
}
// 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" {
// check if we have a recent repo we can open
recentRepos := app.Config.GetAppState().RecentRepos
if len(recentRepos) > 0 {
var err error
// try opening each repo in turn, in case any have been deleted
for _, repoDir := range recentRepos {
if err = os.Chdir(repoDir); err == nil {
return true, nil
}
}
return false, err
}
os.Exit(1)
}
if err := app.OSCommand.RunCommand("git init"); err != nil {
if isRepo, err := isDirectoryAGitRepository(cwd); isRepo {
return false, err
}
var shouldInitRepo bool
initialBranchArg := ""
switch app.UserConfig.NotARepository {
case "prompt":
// Offer to initialize a new repository in current directory.
fmt.Print(app.Tr.CreateRepo)
response, _ := bufio.NewReader(os.Stdin).ReadString('\n')
shouldInitRepo = (strings.Trim(response, " \r\n") == "y")
if shouldInitRepo {
// 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 {
initialBranchArg += "--initial-branch=" + app.OSCommand.Quote(trimmedResponse)
}
}
case "create":
shouldInitRepo = true
case "skip":
shouldInitRepo = false
case "quit":
fmt.Fprintln(os.Stderr, app.Tr.NotARepository)
os.Exit(1)
default:
fmt.Fprintln(os.Stderr, app.Tr.IncorrectNotARepository)
os.Exit(1)
}
if shouldInitRepo {
if err := app.OSCommand.Cmd.New("git init " + initialBranchArg).Run(); err != nil {
return false, err
}
return false, nil
}
// check if we have a recent repo we can open
for _, repoDir := range app.Config.GetAppState().RecentRepos {
if isRepo, _ := isDirectoryAGitRepository(repoDir); isRepo {
if err := os.Chdir(repoDir); err == nil {
return true, nil
}
}
}
fmt.Fprintln(os.Stderr, app.Tr.NoRecentRepositories)
os.Exit(1)
}
// Run this afterward so that the previous repo creation steps can run without this interfering
if isBare, err := git_commands.IsBareRepo(app.OSCommand); isBare {
if err != nil {
return false, err
}
fmt.Print(app.Tr.BareRepo)
response, _ := bufio.NewReader(os.Stdin).ReadString('\n')
if shouldOpenRecent := strings.Trim(response, " \r\n") == "y"; !shouldOpenRecent {
os.Exit(0)
}
if didOpenRepo := openRecentRepo(app); didOpenRepo {
return true, nil
}
fmt.Println(app.Tr.NoRecentRepositories)
os.Exit(1)
}
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.RunWithSubprocesses()
func (app *App) Run(startArgs appTypes.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)
_, 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)
}
cmd := exec.Command("tail", "-f", logFilePath)
stdout, _ := cmd.StdoutPipe()
if err := cmd.Start(); err != nil {
log.Fatal(err)
}
opts := humanlog.DefaultOptions
opts.Truncates = false
if err := humanlog.Scanner(stdout, os.Stdout, opts); err != nil {
log.Fatal(err)
}
if err := cmd.Wait(); err != nil {
log.Fatal(err)
}
os.Exit(0)
return slices.TryForEach(app.closers, func(closer io.Closer) error {
return closer.Close()
})
}

View File

@@ -36,9 +36,50 @@ func TestIsGitVersionValid(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.versionStr, func(t *testing.T) {
result := isGitVersionValid(s.versionStr)
assert.Equal(t, result, s.expectedResult)
})
}
}
func TestIsValidGhVersion(t *testing.T) {
type scenario struct {
versionStr string
expectedResult bool
}
scenarios := []scenario{
{
"",
false,
},
{
`gh version 1.0.0 (2020-08-23)
https://github.com/cli/cli/releases/tag/v1.0.0`,
false,
},
{
`gh version 2.0.0 (2021-08-23)
https://github.com/cli/cli/releases/tag/v2.0.0`,
true,
},
{
`gh version 1.1.0 (2021-10-14)
https://github.com/cli/cli/releases/tag/v1.1.0
A new release of gh is available: 1.1.0 → v2.2.0
To upgrade, run: brew update && brew upgrade gh
https://github.com/cli/cli/releases/tag/v2.2.0`,
false,
},
}
for _, s := range scenarios {
t.Run(s.versionStr, func(t *testing.T) {
result := isGhVersionValid(s.versionStr)
assert.Equal(t, result, s.expectedResult)
})
}
}

107
pkg/app/daemon/daemon.go Normal file
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
}

265
pkg/app/entry_point.go Normal file
View File

@@ -0,0 +1,265 @@
package app
import (
"bytes"
"fmt"
"log"
"os"
"path/filepath"
"runtime"
"runtime/debug"
"strings"
"github.com/integrii/flaggy"
"github.com/jesseduffield/lazygit/pkg/app/daemon"
appTypes "github.com/jesseduffield/lazygit/pkg/app/types"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/env"
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
"github.com/jesseduffield/lazygit/pkg/logs"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
"gopkg.in/yaml.v3"
)
type cliArgs struct {
RepoPath string
FilterPath string
GitArg string
PrintVersionInfo bool
Debug bool
TailLogs bool
PrintDefaultConfig bool
PrintConfigDir bool
UseConfigDir string
WorkTree string
GitDir string
CustomConfigFile string
}
type BuildInfo struct {
Commit string
Date string
Version string
BuildSource string
}
func Start(buildInfo *BuildInfo, integrationTest integrationTypes.IntegrationTest) {
cliArgs := parseCliArgsAndEnvVars()
mergeBuildInfo(buildInfo)
if cliArgs.RepoPath != "" {
if cliArgs.WorkTree != "" || cliArgs.GitDir != "" {
log.Fatal("--path option is incompatible with the --work-tree and --git-dir options")
}
absRepoPath, err := filepath.Abs(cliArgs.RepoPath)
if err != nil {
log.Fatal(err)
}
cliArgs.WorkTree = absRepoPath
cliArgs.GitDir = filepath.Join(absRepoPath, ".git")
}
if cliArgs.CustomConfigFile != "" {
os.Setenv("LG_CONFIG_FILE", cliArgs.CustomConfigFile)
}
if cliArgs.UseConfigDir != "" {
os.Setenv("CONFIG_DIR", cliArgs.UseConfigDir)
}
if cliArgs.WorkTree != "" {
env.SetGitWorkTreeEnv(cliArgs.WorkTree)
}
if cliArgs.GitDir != "" {
env.SetGitDirEnv(cliArgs.GitDir)
}
if cliArgs.PrintVersionInfo {
fmt.Printf("commit=%s, build date=%s, build source=%s, version=%s, os=%s, arch=%s\n", buildInfo.Commit, buildInfo.Date, buildInfo.BuildSource, buildInfo.Version, runtime.GOOS, runtime.GOARCH)
os.Exit(0)
}
if cliArgs.PrintDefaultConfig {
var buf bytes.Buffer
encoder := yaml.NewEncoder(&buf)
err := encoder.Encode(config.GetDefaultConfig())
if err != nil {
log.Fatal(err.Error())
}
fmt.Printf("%s\n", buf.String())
os.Exit(0)
}
if cliArgs.PrintConfigDir {
fmt.Printf("%s\n", config.ConfigDir())
os.Exit(0)
}
if cliArgs.TailLogs {
logs.TailLogs()
os.Exit(0)
}
if cliArgs.WorkTree != "" {
if err := os.Chdir(cliArgs.WorkTree); err != nil {
log.Fatal(err.Error())
}
}
tempDir, err := os.MkdirTemp("", "lazygit-*")
if err != nil {
log.Fatal(err.Error())
}
defer os.RemoveAll(tempDir)
appConfig, err := config.NewAppConfig("lazygit", buildInfo.Version, buildInfo.Commit, buildInfo.Date, buildInfo.BuildSource, cliArgs.Debug, tempDir)
if err != nil {
log.Fatal(err.Error())
}
if integrationTest != nil {
integrationTest.SetupConfig(appConfig)
}
common, err := NewCommon(appConfig)
if err != nil {
log.Fatal(err)
}
if daemon.InDaemonMode() {
daemon.Handle(common)
return
}
parsedGitArg := parseGitArg(cliArgs.GitArg)
Run(appConfig, common, appTypes.NewStartArgs(cliArgs.FilterPath, parsedGitArg, integrationTest))
}
func parseCliArgsAndEnvVars() *cliArgs {
flaggy.DefaultParser.ShowVersionWithVersionFlag = false
repoPath := ""
flaggy.String(&repoPath, "p", "path", "Path of git repo. (equivalent to --work-tree=<path> --git-dir=<path>/.git/)")
filterPath := ""
flaggy.String(&filterPath, "f", "filter", "Path to filter on in `git log -- <path>`. When in filter mode, the commits, reflog, and stash are filtered based on the given path, and some operations are restricted")
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.")
printVersionInfo := false
flaggy.Bool(&printVersionInfo, "v", "version", "Print the current version")
debug := false
flaggy.Bool(&debug, "d", "debug", "Run in debug mode with logging (see --logs flag below). Use the LOG_LEVEL env var to set the log level (debug/info/warn/error)")
tailLogs := false
flaggy.Bool(&tailLogs, "l", "logs", "Tail lazygit logs (intended to be used when `lazygit --debug` is called in a separate terminal tab)")
printDefaultConfig := false
flaggy.Bool(&printDefaultConfig, "c", "config", "Print the default config")
printConfigDir := false
flaggy.Bool(&printConfigDir, "cd", "print-config-dir", "Print the config directory")
useConfigDir := ""
flaggy.String(&useConfigDir, "ucd", "use-config-dir", "override default config directory with provided directory")
workTree := ""
flaggy.String(&workTree, "w", "work-tree", "equivalent of the --work-tree git argument")
gitDir := ""
flaggy.String(&gitDir, "g", "git-dir", "equivalent of the --git-dir git argument")
customConfigFile := ""
flaggy.String(&customConfigFile, "ucf", "use-config-file", "Comma separated list to custom config file(s)")
flaggy.Parse()
if os.Getenv("DEBUG") == "TRUE" {
debug = true
}
return &cliArgs{
RepoPath: repoPath,
FilterPath: filterPath,
GitArg: gitArg,
PrintVersionInfo: printVersionInfo,
Debug: debug,
TailLogs: tailLogs,
PrintDefaultConfig: printDefaultConfig,
PrintConfigDir: printConfigDir,
UseConfigDir: useConfigDir,
WorkTree: workTree,
GitDir: gitDir,
CustomConfigFile: customConfigFile,
}
}
func parseGitArg(gitArg string) appTypes.GitArg {
typedArg := appTypes.GitArg(gitArg)
// using switch so that linter catches when a new git arg value is defined but not handled here
switch typedArg {
case appTypes.GitArgNone, appTypes.GitArgStatus, appTypes.GitArgBranch, appTypes.GitArgLog, appTypes.GitArgStash:
return typedArg
}
permittedValues := []string{
string(appTypes.GitArgStatus),
string(appTypes.GitArgBranch),
string(appTypes.GitArgLog),
string(appTypes.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")
}
// the buildInfo struct we get passed in is based on what's baked into the lazygit
// binary via the LDFLAGS argument. Some lazygit distributions will make use of these
// arguments and some will not. Go recently started baking in build info
// into the binary by default e.g. the git commit hash. So in this function
// we merge the two together, giving priority to the stuff set by LDFLAGS.
// Note: this mutates the argument passed in
func mergeBuildInfo(buildInfo *BuildInfo) {
// if the version has already been set by build flags then we'll honour that.
// chances are it's something like v0.31.0 which is more informative than a
// commit hash.
if buildInfo.Version != "" {
return
}
buildInfo.Version = "unversioned"
goBuildInfo, ok := debug.ReadBuildInfo()
if !ok {
return
}
revision, ok := lo.Find(goBuildInfo.Settings, func(setting debug.BuildSetting) bool {
return setting.Key == "vcs.revision"
})
if ok {
buildInfo.Commit = revision.Value
// if lazygit was built from source we'll show the version as the
// abbreviated commit hash
buildInfo.Version = utils.ShortSha(revision.Value)
}
// if version hasn't been set we assume that neither has the date
time, ok := lo.Find(goBuildInfo.Settings, func(setting debug.BuildSetting) bool {
return setting.Key == "vcs.time"
})
if ok {
buildInfo.Date = time.Value
}
}

39
pkg/app/errors.go Normal file
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
}

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

@@ -0,0 +1,56 @@
package app
import (
"io/ioutil"
"log"
"os"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/sirupsen/logrus"
)
func newLogger(config config.AppConfigurer) *logrus.Entry {
var log *logrus.Logger
if config.GetDebug() {
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{})
}
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
}
func getLogLevel() logrus.Level {
strLevel := os.Getenv("LOG_LEVEL")
level, err := logrus.ParseLevel(strLevel)
if err != nil {
return logrus.DebugLevel
}
return level
}

33
pkg/app/types/types.go Normal file
View File

@@ -0,0 +1,33 @@
package app
import (
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
)
// StartArgs is the struct that represents some things we want to do on program start
type StartArgs struct {
// FilterPath determines which path we're going to filter on so that we only see commits from that file.
FilterPath string
// GitArg determines what context we open in
GitArg GitArg
// integration test (only relevant when invoking lazygit in the context of an integration test)
IntegrationTest integrationTypes.IntegrationTest
}
type GitArg string
const (
GitArgNone GitArg = ""
GitArgStatus GitArg = "status"
GitArgBranch GitArg = "branch"
GitArgLog GitArg = "log"
GitArgStash GitArg = "stash"
)
func NewStartArgs(filterPath string, gitArg GitArg, test integrationTypes.IntegrationTest) StartArgs {
return StartArgs{
FilterPath: filterPath,
GitArg: gitArg,
IntegrationTest: test,
}
}

78
pkg/cheatsheet/check.go Normal file
View File

@@ -0,0 +1,78 @@
package cheatsheet
import (
"fmt"
"io/fs"
"io/ioutil"
"log"
"os"
"path/filepath"
"regexp"
"github.com/pmezard/go-difflib/difflib"
)
func Check() {
dir := GetDir()
tmpDir := filepath.Join(os.TempDir(), "lazygit_cheatsheet")
err := os.RemoveAll(tmpDir)
if err != nil {
log.Fatalf("Error occurred while checking if cheatsheets are up to date: %v", err)
}
err = os.Mkdir(tmpDir, 0o700)
if err != nil {
log.Fatalf("Error occurred while checking if cheatsheets are up to date: %v", err)
}
generateAtDir(tmpDir)
defer os.RemoveAll(tmpDir)
actualContent := obtainContent(dir)
expectedContent := obtainContent(tmpDir)
if expectedContent == "" {
log.Fatal("empty expected content")
}
if actualContent != expectedContent {
err := difflib.WriteUnifiedDiff(os.Stdout, difflib.UnifiedDiff{
A: difflib.SplitLines(expectedContent),
B: difflib.SplitLines(actualContent),
FromFile: "Expected",
FromDate: "",
ToFile: "Actual",
ToDate: "",
Context: 1,
})
if err != nil {
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. 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)
}
fmt.Println("\nCheatsheets are up to date")
}
func obtainContent(dir string) string {
re := regexp.MustCompile(`Keybindings_\w+\.md$`)
content := ""
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if re.MatchString(path) {
bytes, err := ioutil.ReadFile(path)
if err != nil {
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)
}
return nil
})
if err != nil {
log.Fatalf("Error occurred while checking if cheatsheets are up to date: %v", err)
}
return content
}

211
pkg/cheatsheet/generate.go Normal file
View File

@@ -0,0 +1,211 @@
// This "script" generates a file called Keybindings_{{.LANG}}.md
// in current working directory.
//
// The content of this generated file is a keybindings cheatsheet.
//
// To generate cheatsheet in english run:
// go run scripts/generate_cheatsheet.go
package cheatsheet
import (
"fmt"
"log"
"os"
"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/keybindings"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
type bindingSection struct {
title string
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 {
return "go run scripts/cheatsheet/main.go generate"
}
func GetDir() string {
return utils.GetLazygitRootDirectory() + "/docs/keybindings"
}
func generateAtDir(cheatsheetDir string) {
translationSetsByLang := i18n.GetTranslationSets()
mConfig := config.NewDummyAppConfig()
for lang := range translationSetsByLang {
mConfig.GetUserConfig().Gui.Language = lang
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)
}
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)
writeString(file, content)
}
}
func Generate() {
generateAtDir(GetDir())
}
func writeString(file *os.File, str string) {
_, err := file.WriteString(str)
if err != nil {
log.Fatal(err)
}
}
func localisedTitle(tr *i18n.TranslationSet, str string) string {
contextTitleMap := map[string]string{
"global": tr.GlobalTitle,
"navigation": tr.NavigationTitle,
"branches": tr.BranchesTitle,
"localBranches": tr.LocalBranchesTitle,
"files": tr.FilesTitle,
"status": tr.StatusTitle,
"submodules": tr.SubmodulesTitle,
"subCommits": tr.SubCommitsTitle,
"remoteBranches": tr.RemoteBranchesTitle,
"remotes": tr.RemotesTitle,
"reflogCommits": tr.ReflogCommitsTitle,
"tags": tr.TagsTitle,
"commitFiles": tr.CommitFilesTitle,
"commitMessage": tr.CommitMessageTitle,
"commits": tr.CommitsTitle,
"confirmation": tr.ConfirmationTitle,
"information": tr.InformationTitle,
"main": tr.NormalTitle,
"patchBuilding": tr.PatchBuildingTitle,
"mergeConflicts": tr.MergingTitle,
"staging": tr.StagingTitle,
"menu": tr.MenuTitle,
"search": tr.SearchTitle,
"secondary": tr.SecondaryTitle,
"stash": tr.StashTitle,
"suggestions": tr.SuggestionsCheatsheetTitle,
"extras": tr.ExtrasTitle,
}
title, ok := contextTitleMap[str]
if !ok {
panic(fmt.Sprintf("title not found for %s", str))
}
return title
}
func getBindingSections(bindings []*types.Binding, tr *i18n.TranslationSet) []*bindingSection {
excludedViews := []string{"stagingSecondary", "patchBuildingSecondary"}
bindingsToDisplay := slices.Filter(bindings, func(binding *types.Binding) bool {
if lo.Contains(excludedViews, binding.ViewName) {
return false
}
return (binding.Description != "" || binding.Alternative != "")
})
bindingsByHeader := lo.GroupBy(bindingsToDisplay, func(binding *types.Binding) header {
return getHeader(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.LabelFromKey(binding.Key)
})
return headerWithBindings{
header: header,
bindings: uniqBindings,
}
},
)
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 slices.Map(bindingGroups, func(hb headerWithBindings) *bindingSection {
return &bindingSection{
title: hb.header.title,
bindings: hb.bindings,
}
})
}
func getHeader(binding *types.Binding, tr *i18n.TranslationSet) header {
if binding.Tag == "navigation" {
return header{priority: 2, title: localisedTitle(tr, "navigation")}
}
if binding.ViewName == "" {
return header{priority: 3, title: localisedTitle(tr, "global")}
}
return header{priority: 1, title: localisedTitle(tr, binding.ViewName)}
}
func formatSections(tr *i18n.TranslationSet, bindingSections []*bindingSection) string {
content := fmt.Sprintf("# Lazygit %s\n", tr.Keybindings)
for _, section := range bindingSections {
content += formatTitle(section.title)
content += "<pre>\n"
for _, binding := range section.bindings {
content += formatBinding(binding)
}
content += "</pre>\n"
}
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.LabelFromKey(binding.Key),
binding.Description,
binding.Alternative,
)
}
return fmt.Sprintf(" <kbd>%s</kbd>: %s\n", keybindings.LabelFromKey(binding.Key), binding.Description)
}

View File

@@ -0,0 +1,240 @@
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",
Description: "stage file",
},
},
expected: []*bindingSection{
{
title: "Files",
bindings: []*types.Binding{
{
ViewName: "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",
},
{
ViewName: "files",
Description: "unstage file",
},
{
ViewName: "submodules",
Description: "drop submodule",
},
},
expected: []*bindingSection{
{
title: "Files",
bindings: []*types.Binding{
{
ViewName: "files",
Description: "stage file",
},
{
ViewName: "files",
Description: "unstage file",
},
},
},
{
title: "Submodules",
bindings: []*types.Binding{
{
ViewName: "submodules",
Description: "drop submodule",
},
},
},
},
},
{
testName: "with navigation bindings",
bindings: []*types.Binding{
{
ViewName: "files",
Description: "stage file",
},
{
ViewName: "files",
Description: "unstage file",
},
{
ViewName: "files",
Description: "scroll",
Tag: "navigation",
},
{
ViewName: "commits",
Description: "revert commit",
},
},
expected: []*bindingSection{
{
title: "List Panel Navigation",
bindings: []*types.Binding{
{
ViewName: "files",
Description: "scroll",
Tag: "navigation",
},
},
},
{
title: "Commits",
bindings: []*types.Binding{
{
ViewName: "commits",
Description: "revert commit",
},
},
},
{
title: "Files",
bindings: []*types.Binding{
{
ViewName: "files",
Description: "stage file",
},
{
ViewName: "files",
Description: "unstage file",
},
},
},
},
},
{
testName: "with duplicate navigation bindings",
bindings: []*types.Binding{
{
ViewName: "files",
Description: "stage file",
},
{
ViewName: "files",
Description: "unstage file",
},
{
ViewName: "files",
Description: "scroll",
Tag: "navigation",
},
{
ViewName: "commits",
Description: "revert commit",
},
{
ViewName: "commits",
Description: "scroll",
Tag: "navigation",
},
{
ViewName: "commits",
Description: "page up",
Tag: "navigation",
},
},
expected: []*bindingSection{
{
title: "List Panel Navigation",
bindings: []*types.Binding{
{
ViewName: "files",
Description: "scroll",
Tag: "navigation",
},
{
ViewName: "commits",
Description: "page up",
Tag: "navigation",
},
},
},
{
title: "Commits",
bindings: []*types.Binding{
{
ViewName: "commits",
Description: "revert commit",
},
},
},
{
title: "Files",
bindings: []*types.Binding{
{
ViewName: "files",
Description: "stage file",
},
{
ViewName: "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)
})
}
}

View File

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

View File

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

View File

@@ -1,52 +0,0 @@
package commands
import (
"os"
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (c *GitCommand) ConfiguredPager() string {
if os.Getenv("GIT_PAGER") != "" {
return os.Getenv("GIT_PAGER")
}
if os.Getenv("PAGER") != "" {
return os.Getenv("PAGER")
}
output, err := c.OSCommand.RunCommandWithOutput("git config --get-all core.pager")
if err != nil {
return ""
}
trimmedOutput := strings.TrimSpace(output)
return strings.Split(trimmedOutput, "\n")[0]
}
func (c *GitCommand) GetPager(width int) string {
useConfig := c.Config.GetUserConfig().Git.Paging.UseConfig
if useConfig {
pager := c.ConfiguredPager()
return strings.Split(pager, "| less")[0]
}
templateValues := map[string]string{
"columnWidth": strconv.Itoa(width/2 - 6),
}
pagerTemplate := c.Config.GetUserConfig().Git.Paging.Pager
return utils.ResolvePlaceholderString(pagerTemplate, templateValues)
}
func (c *GitCommand) colorArg() string {
return c.Config.GetUserConfig().Git.Paging.ColorArg
}
func (c *GitCommand) GetConfigValue(key string) string {
output, err := c.OSCommand.RunCommandWithOutput("git config --get %s", key)
if err != nil {
// looks like this returns an error if there is no matching value which we're okay with
return ""
}
return strings.TrimSpace(output)
}

View File

@@ -1,26 +0,0 @@
package commands
import (
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// NewDummyGitCommand creates a new dummy GitCommand for testing
func NewDummyGitCommand() *GitCommand {
return NewDummyGitCommandWithOSCommand(oscommands.NewDummyOSCommand())
}
// NewDummyGitCommandWithOSCommand creates a new dummy GitCommand for testing
func NewDummyGitCommandWithOSCommand(osCommand *oscommands.OSCommand) *GitCommand {
return &GitCommand{
Log: utils.NewDummyLog(),
OSCommand: osCommand,
Tr: i18n.NewTranslationSet(utils.NewDummyLog()),
Config: config.NewDummyAppConfig(),
getGlobalGitConfig: func(string) (string, error) { return "", nil },
getLocalGitConfig: func(string) (string, error) { return "", nil },
removeFile: func(string) error { return nil },
}
}

View File

@@ -1,264 +0,0 @@
package commands
import (
"fmt"
"path/filepath"
"strings"
"time"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// CatFile obtains the content of a file
func (c *GitCommand) CatFile(fileName string) (string, error) {
return c.OSCommand.RunCommandWithOutput("%s %s", c.OSCommand.Platform.CatCmd, c.OSCommand.Quote(fileName))
}
// StageFile stages a file
func (c *GitCommand) StageFile(fileName string) error {
// renamed files look like "file1 -> file2"
fileNames := strings.Split(fileName, " -> ")
return c.OSCommand.RunCommand("git add %s", c.OSCommand.Quote(fileNames[len(fileNames)-1]))
}
// StageAll stages all files
func (c *GitCommand) StageAll() error {
return c.OSCommand.RunCommand("git add -A")
}
// UnstageAll stages all files
func (c *GitCommand) UnstageAll() error {
return c.OSCommand.RunCommand("git reset")
}
// UnStageFile unstages a file
func (c *GitCommand) UnStageFile(fileName string, tracked bool) error {
command := "git rm --cached --force %s"
if tracked {
command = "git reset HEAD %s"
}
// renamed files look like "file1 -> file2"
fileNames := strings.Split(fileName, " -> ")
for _, name := range fileNames {
if err := c.OSCommand.RunCommand(command, c.OSCommand.Quote(name)); err != nil {
return err
}
}
return nil
}
func (c *GitCommand) BeforeAndAfterFileForRename(file *models.File) (*models.File, *models.File, error) {
if !file.IsRename() {
return nil, nil, errors.New("Expected renamed file")
}
// we've got a file that represents a rename from one file to another. Unfortunately
// our File abstraction fails to consider this case, so here we will refetch
// all files, passing the --no-renames flag and then recursively call the function
// again for the before file and after file. At some point we should fix the abstraction itself
split := strings.Split(file.Name, " -> ")
filesWithoutRenames := c.GetStatusFiles(GetStatusFileOptions{NoRenames: true})
var beforeFile *models.File
var afterFile *models.File
for _, f := range filesWithoutRenames {
if f.Name == split[0] {
beforeFile = f
}
if f.Name == split[1] {
afterFile = f
}
}
if beforeFile == nil || afterFile == nil {
return nil, nil, errors.New("Could not find deleted file or new file for file rename")
}
if beforeFile.IsRename() || afterFile.IsRename() {
// probably won't happen but we want to ensure we don't get an infinite loop
return nil, nil, errors.New("Nested rename found")
}
return beforeFile, afterFile, nil
}
// DiscardAllFileChanges directly
func (c *GitCommand) DiscardAllFileChanges(file *models.File) error {
if file.IsRename() {
beforeFile, afterFile, err := c.BeforeAndAfterFileForRename(file)
if err != nil {
return err
}
if err := c.DiscardAllFileChanges(beforeFile); err != nil {
return err
}
if err := c.DiscardAllFileChanges(afterFile); err != nil {
return err
}
return nil
}
// if the file isn't tracked, we assume you want to delete it
quotedFileName := c.OSCommand.Quote(file.Name)
if file.HasStagedChanges || file.HasMergeConflicts {
if err := c.OSCommand.RunCommand("git reset -- %s", quotedFileName); err != nil {
return err
}
}
if !file.Tracked {
return c.removeFile(file.Name)
}
return c.DiscardUnstagedFileChanges(file)
}
// DiscardUnstagedFileChanges directly
func (c *GitCommand) DiscardUnstagedFileChanges(file *models.File) error {
quotedFileName := c.OSCommand.Quote(file.Name)
return c.OSCommand.RunCommand("git checkout -- %s", quotedFileName)
}
// Ignore adds a file to the gitignore for the repo
func (c *GitCommand) Ignore(filename string) error {
return c.OSCommand.AppendLineToFile(".gitignore", filename)
}
// WorktreeFileDiff returns the diff of a file
func (c *GitCommand) WorktreeFileDiff(file *models.File, plain bool, cached bool) string {
// for now we assume an error means the file was deleted
s, _ := c.OSCommand.RunCommandWithOutput(c.WorktreeFileDiffCmdStr(file, plain, cached))
return s
}
func (c *GitCommand) WorktreeFileDiffCmdStr(file *models.File, plain bool, cached bool) string {
cachedArg := ""
trackedArg := "--"
colorArg := c.colorArg()
split := strings.Split(file.Name, " -> ") // in case of a renamed file we get the new filename
fileName := c.OSCommand.Quote(split[len(split)-1])
if cached {
cachedArg = "--cached"
}
if !file.Tracked && !file.HasStagedChanges && !cached {
trackedArg = "--no-index /dev/null"
}
if plain {
colorArg = "never"
}
return fmt.Sprintf("git diff --submodule --no-ext-diff --color=%s %s %s %s", colorArg, cachedArg, trackedArg, fileName)
}
func (c *GitCommand) ApplyPatch(patch string, flags ...string) error {
filepath := filepath.Join(c.Config.GetUserConfigDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch")
c.Log.Infof("saving temporary patch to %s", filepath)
if err := c.OSCommand.CreateFileWithContent(filepath, patch); err != nil {
return err
}
flagStr := ""
for _, flag := range flags {
flagStr += " --" + flag
}
return c.OSCommand.RunCommand("git apply %s %s", flagStr, c.OSCommand.Quote(filepath))
}
// ShowFileDiff get the diff of specified from and to. Typically this will be used for a single commit so it'll be 123abc^..123abc
// but when we're in diff mode it could be any 'from' to any 'to'. The reverse flag is also here thanks to diff mode.
func (c *GitCommand) ShowFileDiff(from string, to string, reverse bool, fileName string, plain bool) (string, error) {
cmdStr := c.ShowFileDiffCmdStr(from, to, reverse, fileName, plain)
return c.OSCommand.RunCommandWithOutput(cmdStr)
}
func (c *GitCommand) ShowFileDiffCmdStr(from string, to string, reverse bool, fileName string, plain bool) string {
colorArg := c.colorArg()
if plain {
colorArg = "never"
}
reverseFlag := ""
if reverse {
reverseFlag = " -R "
}
return fmt.Sprintf("git diff --submodule --no-ext-diff --no-renames --color=%s %s %s %s -- %s", colorArg, from, to, reverseFlag, fileName)
}
// CheckoutFile checks out the file for the given commit
func (c *GitCommand) CheckoutFile(commitSha, fileName string) error {
return c.OSCommand.RunCommand("git checkout %s %s", commitSha, fileName)
}
// DiscardOldFileChanges discards changes to a file from an old commit
func (c *GitCommand) DiscardOldFileChanges(commits []*models.Commit, commitIndex int, fileName string) error {
if err := c.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil {
return err
}
// check if file exists in previous commit (this command returns an error if the file doesn't exist)
if err := c.OSCommand.RunCommand("git cat-file -e HEAD^:%s", fileName); err != nil {
if err := c.OSCommand.Remove(fileName); err != nil {
return err
}
if err := c.StageFile(fileName); err != nil {
return err
}
} else if err := c.CheckoutFile("HEAD^", fileName); err != nil {
return err
}
// amend the commit
cmd, err := c.AmendHead()
if cmd != nil {
return errors.New("received unexpected pointer to cmd")
}
if err != nil {
return err
}
// continue
return c.GenericMergeOrRebaseAction("rebase", "continue")
}
// DiscardAnyUnstagedFileChanges discards any unstages file changes via `git checkout -- .`
func (c *GitCommand) DiscardAnyUnstagedFileChanges() error {
return c.OSCommand.RunCommand("git checkout -- .")
}
// RemoveTrackedFiles will delete the given file(s) even if they are currently tracked
func (c *GitCommand) RemoveTrackedFiles(name string) error {
return c.OSCommand.RunCommand("git rm -r --cached %s", name)
}
// RemoveUntrackedFiles runs `git clean -fd`
func (c *GitCommand) RemoveUntrackedFiles() error {
return c.OSCommand.RunCommand("git clean -fd")
}
// ResetAndClean removes all unstaged changes and removes all untracked files
func (c *GitCommand) ResetAndClean() error {
submoduleConfigs, err := c.GetSubmoduleConfigs()
if err != nil {
return err
}
if len(submoduleConfigs) > 0 {
if err := c.ResetSubmodules(submoduleConfigs); err != nil {
return err
}
}
if err := c.ResetHard("HEAD"); err != nil {
return err
}
return c.RemoveUntrackedFiles()
}

View File

@@ -7,64 +7,66 @@ import (
"strings"
"github.com/go-errors/errors"
"github.com/sasha-s/go-deadlock"
gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/env"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
gitconfig "github.com/tcnksm/go-gitconfig"
)
// this takes something like:
// * (HEAD detached at 264fc6f5)
// remotes
// and returns '264fc6f5' as the second match
const CurrentBranchNameRegex = `(?m)^\*.*?([^ ]*?)\)?$`
// GitCommand is our main git interface
type GitCommand struct {
Log *logrus.Entry
OSCommand *oscommands.OSCommand
Repo *gogit.Repository
Tr *i18n.TranslationSet
Config config.AppConfigurer
getGlobalGitConfig func(string) (string, error)
getLocalGitConfig func(string) (string, error)
removeFile func(string) error
DotGitDir string
onSuccessfulContinue func() error
PatchManager *patch.PatchManager
Branch *git_commands.BranchCommands
Commit *git_commands.CommitCommands
Config *git_commands.ConfigCommands
Custom *git_commands.CustomCommands
File *git_commands.FileCommands
Flow *git_commands.FlowCommands
Patch *git_commands.PatchCommands
Rebase *git_commands.RebaseCommands
Remote *git_commands.RemoteCommands
Stash *git_commands.StashCommands
Status *git_commands.StatusCommands
Submodule *git_commands.SubmoduleCommands
Sync *git_commands.SyncCommands
Tag *git_commands.TagCommands
WorkingTree *git_commands.WorkingTreeCommands
Bisect *git_commands.BisectCommands
Gh *git_commands.GhCommands
HostingService *git_commands.HostingService
// Push to current determines whether the user has configured to push to the remote branch of the same name as the current or not
PushToCurrent bool
Loaders Loaders
}
// NewGitCommand it runs git commands
func NewGitCommand(log *logrus.Entry, osCommand *oscommands.OSCommand, tr *i18n.TranslationSet, config config.AppConfigurer) (*GitCommand, error) {
var repo *gogit.Repository
// see what our default push behaviour is
output, err := osCommand.RunCommandWithOutput("git config --get push.default")
pushToCurrent := false
if err != nil {
log.Errorf("error reading git config: %v", err)
} else {
pushToCurrent = strings.TrimSpace(output) == "current"
}
if err := verifyInGitRepo(osCommand.RunCommand); err != nil {
return nil, err
}
type Loaders struct {
Branches *loaders.BranchLoader
CommitFiles *loaders.CommitFileLoader
Commits *loaders.CommitLoader
Files *loaders.FileLoader
ReflogCommits *loaders.ReflogCommitLoader
Remotes *loaders.RemoteLoader
Stash *loaders.StashLoader
Tags *loaders.TagLoader
}
func NewGitCommand(
cmn *common.Common,
osCommand *oscommands.OSCommand,
gitConfig git_config.IGitConfig,
syncMutex *deadlock.Mutex,
) (*GitCommand, error) {
if err := navigateToRepoRootDirectory(os.Stat, os.Chdir); err != nil {
return nil, err
}
if repo, err = setupRepository(gogit.PlainOpen, tr.GitconfigParseErr); err != nil {
repo, err := setupRepository(gogit.PlainOpenWithOptions, gogit.PlainOpenOptions{DetectDotGit: false, EnableDotGitCommonDir: true}, cmn.Tr.GitconfigParseErr)
if err != nil {
return nil, err
}
@@ -73,26 +75,85 @@ func NewGitCommand(log *logrus.Entry, osCommand *oscommands.OSCommand, tr *i18n.
return nil, err
}
gitCommand := &GitCommand{
Log: log,
OSCommand: osCommand,
Tr: tr,
Repo: repo,
Config: config,
getGlobalGitConfig: gitconfig.Global,
getLocalGitConfig: gitconfig.Local,
removeFile: os.RemoveAll,
DotGitDir: dotGitDir,
PushToCurrent: pushToCurrent,
}
gitCommand.PatchManager = patch.NewPatchManager(log, gitCommand.ApplyPatch, gitCommand.ShowFileDiff)
return gitCommand, nil
return NewGitCommandAux(
cmn,
osCommand,
gitConfig,
dotGitDir,
repo,
syncMutex,
), nil
}
func verifyInGitRepo(runCmd func(string, ...interface{}) error) error {
return runCmd("git status")
func NewGitCommandAux(
cmn *common.Common,
osCommand *oscommands.OSCommand,
gitConfig git_config.IGitConfig,
dotGitDir string,
repo *gogit.Repository,
syncMutex *deadlock.Mutex,
) *GitCommand {
cmd := NewGitCmdObjBuilder(cmn.Log, osCommand.Cmd)
// here we're doing a bunch of dependency injection for each of our commands structs.
// This is admittedly messy, but allows us to test each command struct in isolation,
// and allows for better namespacing when compared to having every method living
// 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, syncMutex)
statusCommands := git_commands.NewStatusCommands(gitCommon)
fileLoader := loaders.NewFileLoader(cmn, cmd, configCommands)
flowCommands := git_commands.NewFlowCommands(gitCommon)
remoteCommands := git_commands.NewRemoteCommands(gitCommon)
branchCommands := git_commands.NewBranchCommands(gitCommon)
syncCommands := git_commands.NewSyncCommands(gitCommon)
tagCommands := git_commands.NewTagCommands(gitCommon)
commitCommands := git_commands.NewCommitCommands(gitCommon)
customCommands := git_commands.NewCustomCommands(gitCommon)
fileCommands := git_commands.NewFileCommands(gitCommon)
submoduleCommands := git_commands.NewSubmoduleCommands(gitCommon)
workingTreeCommands := git_commands.NewWorkingTreeCommands(gitCommon, submoduleCommands, fileLoader)
rebaseCommands := git_commands.NewRebaseCommands(gitCommon, commitCommands, workingTreeCommands)
stashCommands := git_commands.NewStashCommands(gitCommon, fileLoader, workingTreeCommands)
// TODO: have patch manager take workingTreeCommands in its entirety
patchManager := patch.NewPatchManager(cmn.Log, workingTreeCommands.ApplyPatch, workingTreeCommands.ShowFileDiff)
patchCommands := git_commands.NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchManager)
bisectCommands := git_commands.NewBisectCommands(gitCommon)
ghCommands := git_commands.NewGhCommand(gitCommon)
hostingServiceCommands := git_commands.NewHostingServiceCommand(gitCommon)
return &GitCommand{
Branch: branchCommands,
Commit: commitCommands,
Config: configCommands,
Custom: customCommands,
File: fileCommands,
Flow: flowCommands,
Patch: patchCommands,
Rebase: rebaseCommands,
Remote: remoteCommands,
Stash: stashCommands,
Status: statusCommands,
Submodule: submoduleCommands,
Sync: syncCommands,
Tag: tagCommands,
Bisect: bisectCommands,
WorkingTree: workingTreeCommands,
Gh: ghCommands,
HostingService: hostingServiceCommands,
Loaders: Loaders{
Branches: loaders.NewBranchLoader(cmn, branchCommands.GetRawBranches, branchCommands.CurrentBranchName, configCommands),
CommitFiles: loaders.NewCommitFileLoader(cmn, cmd),
Commits: loaders.NewCommitLoader(cmn, cmd, dotGitDir, branchCommands.CurrentBranchName, statusCommands.RebaseMode),
Files: fileLoader,
ReflogCommits: loaders.NewReflogCommitLoader(cmn, cmd),
Remotes: loaders.NewRemoteLoader(cmn, cmd, repo.Remotes),
Stash: loaders.NewStashLoader(cmn, cmd),
Tags: loaders.NewTagLoader(cmn, cmd),
},
}
}
func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir func(string) error) error {
@@ -123,6 +184,18 @@ func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir f
if err = chdir(".."); err != nil {
return utils.WrapError(err)
}
currentPath, err := os.Getwd()
if err != nil {
return err
}
atRoot := currentPath == filepath.Dir(currentPath)
if atRoot {
// we should never really land here: the code that creates GitCommand should
// verify we're in a git directory
return errors.New("Must open lazygit in a git repository")
}
}
}
@@ -140,7 +213,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
@@ -155,8 +228,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)
@@ -188,7 +260,11 @@ 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
}
func VerifyInGitRepo(osCommand *oscommands.OSCommand) error {
return osCommand.Cmd.New("git rev-parse --git-dir").DontLog().Run()
}

View File

@@ -0,0 +1,47 @@
package commands
import (
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/sirupsen/logrus"
)
// all we're doing here is wrapping the default command object builder with
// some git-specific stuff: e.g. adding a git-specific env var
type gitCmdObjBuilder struct {
innerBuilder *oscommands.CmdObjBuilder
}
var _ oscommands.ICmdObjBuilder = &gitCmdObjBuilder{}
func NewGitCmdObjBuilder(log *logrus.Entry, innerBuilder *oscommands.CmdObjBuilder) *gitCmdObjBuilder {
// the price of having a convenient interface where we can say .New(...).Run() is that our builder now depends on our runner, so when we want to wrap the default builder/runner in new functionality we need to jump through some hoops. We could avoid the use of a decorator function here by just exporting the runner field on the default builder but that would be misleading because we don't want anybody using that to run commands (i.e. we want there to be a single API used across the codebase)
updatedBuilder := innerBuilder.CloneWithNewRunner(func(runner oscommands.ICmdObjRunner) oscommands.ICmdObjRunner {
return &gitCmdObjRunner{
log: log,
innerRunner: runner,
}
})
return &gitCmdObjBuilder{
innerBuilder: updatedBuilder,
}
}
var defaultEnvVar = "GIT_OPTIONAL_LOCKS=0"
func (self *gitCmdObjBuilder) New(cmdStr string) oscommands.ICmdObj {
return self.innerBuilder.New(cmdStr).AddEnvVars(defaultEnvVar)
}
func (self *gitCmdObjBuilder) NewFromArgs(args []string) oscommands.ICmdObj {
return self.innerBuilder.NewFromArgs(args).AddEnvVars(defaultEnvVar)
}
func (self *gitCmdObjBuilder) NewShell(cmdStr string) oscommands.ICmdObj {
return self.innerBuilder.NewShell(cmdStr).AddEnvVars(defaultEnvVar)
}
func (self *gitCmdObjBuilder) Quote(str string) string {
return self.innerBuilder.Quote(str)
}

View File

@@ -0,0 +1,26 @@
package commands
import (
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/sirupsen/logrus"
)
// here we're wrapping the default command runner in some git-specific stuff e.g. retry logic if we get an error due to the presence of .git/index.lock
type gitCmdObjRunner struct {
log *logrus.Entry
innerRunner oscommands.ICmdObjRunner
}
func (self *gitCmdObjRunner) Run(cmdObj oscommands.ICmdObj) error {
_, err := self.RunWithOutput(cmdObj)
return err
}
func (self *gitCmdObjRunner) RunWithOutput(cmdObj oscommands.ICmdObj) (string, error) {
return self.innerRunner.RunWithOutput(cmdObj)
}
func (self *gitCmdObjRunner) RunAndProcessLines(cmdObj oscommands.ICmdObj, onLine func(line string) (bool, error)) error {
return self.innerRunner.RunAndProcessLines(cmdObj, onLine)
}

View File

@@ -0,0 +1,175 @@
package git_commands
import (
"fmt"
"os"
"path/filepath"
"strings"
)
type BisectCommands struct {
*GitCommon
}
func NewBisectCommands(gitCommon *GitCommon) *BisectCommands {
return &BisectCommands{
GitCommon: gitCommon,
}
}
// This command is pretty cheap to run so we're not storing the result anywhere.
// But if it becomes problematic we can chang that.
func (self *BisectCommands) GetInfo() *BisectInfo {
var err error
info := &BisectInfo{started: false, log: self.Log, newTerm: "bad", oldTerm: "good"}
// we return nil if we're not in a git bisect session.
// we know we're in a session by the presence of a .git/BISECT_START file
bisectStartPath := filepath.Join(self.dotGitDir, "BISECT_START")
exists, err := self.os.FileExists(bisectStartPath)
if err != nil {
self.Log.Infof("error getting git bisect info: %s", err.Error())
return info
}
if !exists {
return info
}
startContent, err := os.ReadFile(bisectStartPath)
if err != nil {
self.Log.Infof("error getting git bisect info: %s", err.Error())
return info
}
info.started = true
info.start = strings.TrimSpace(string(startContent))
termsContent, err := os.ReadFile(filepath.Join(self.dotGitDir, "BISECT_TERMS"))
if err != nil {
// old git versions won't have this file so we default to bad/good
} else {
splitContent := strings.Split(string(termsContent), "\n")
info.newTerm = splitContent[0]
info.oldTerm = splitContent[1]
}
bisectRefsDir := filepath.Join(self.dotGitDir, "refs", "bisect")
files, err := os.ReadDir(bisectRefsDir)
if err != nil {
self.Log.Infof("error getting git bisect info: %s", err.Error())
return info
}
info.statusMap = make(map[string]BisectStatus)
for _, file := range files {
status := BisectStatusSkipped
name := file.Name()
path := filepath.Join(bisectRefsDir, name)
fileContent, err := os.ReadFile(path)
if err != nil {
self.Log.Infof("error getting git bisect info: %s", err.Error())
return info
}
sha := strings.TrimSpace(string(fileContent))
if name == info.newTerm {
status = BisectStatusNew
} else if strings.HasPrefix(name, info.oldTerm+"-") {
status = BisectStatusOld
} else if strings.HasPrefix(name, "skipped-") {
status = BisectStatusSkipped
}
info.statusMap[sha] = status
}
currentContent, err := os.ReadFile(filepath.Join(self.dotGitDir, "BISECT_EXPECTED_REV"))
if err != nil {
self.Log.Infof("error getting git bisect info: %s", err.Error())
return info
}
currentSha := strings.TrimSpace(string(currentContent))
info.current = currentSha
return info
}
func (self *BisectCommands) Reset() error {
return self.cmd.New("git bisect reset").StreamOutput().Run()
}
func (self *BisectCommands) Mark(ref string, term string) error {
return self.cmd.New(
fmt.Sprintf("git bisect %s %s", term, ref),
).
IgnoreEmptyError().
StreamOutput().
Run()
}
func (self *BisectCommands) Skip(ref string) error {
return self.Mark(ref, "skip")
}
func (self *BisectCommands) Start() error {
return self.cmd.New("git bisect start").StreamOutput().Run()
}
// tells us whether we've found our problem commit(s). We return a string slice of
// commit sha's if we're done, and that slice may have more that one item if
// skipped commits are involved.
func (self *BisectCommands) IsDone() (bool, []string, error) {
info := self.GetInfo()
if !info.Bisecting() {
return false, nil, nil
}
newSha := info.GetNewSha()
if newSha == "" {
return false, nil, nil
}
// if we start from the new commit and reach the a good commit without
// coming across any unprocessed commits, then we're done
done := false
candidates := []string{}
err := self.cmd.New(fmt.Sprintf("git rev-list %s", newSha)).RunAndProcessLines(func(line string) (bool, error) {
sha := strings.TrimSpace(line)
if status, ok := info.statusMap[sha]; ok {
switch status {
case BisectStatusSkipped, BisectStatusNew:
candidates = append(candidates, sha)
return false, nil
case BisectStatusOld:
done = true
return true, nil
}
} else {
return true, nil
}
// should never land here
return true, nil
})
if err != nil {
return false, nil, err
}
return done, candidates, nil
}
// tells us whether the 'start' ref that we'll be sent back to after we're done
// bisecting is actually a descendant of our current bisect commit. If it's not, we need to
// render the commits from the bad commit.
func (self *BisectCommands) ReachableFromStart(bisectInfo *BisectInfo) bool {
err := self.cmd.New(
fmt.Sprintf("git merge-base --is-ancestor %s %s", bisectInfo.GetNewSha(), bisectInfo.GetStartSha()),
).DontLog().Run()
return err == nil
}

View File

@@ -0,0 +1,101 @@
package git_commands
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
// their own terms e.g. when you want to used 'fixed', 'unfixed' in the event
// that you're looking for a commit that fixed a bug.
// Git bisect only keeps track of a single 'bad' commit. Once you pick a commit
// that's older than the current bad one, it forgets about the previous one. On
// the other hand, it does keep track of all the good and skipped commits.
type BisectInfo struct {
log *logrus.Entry
// tells us whether all our git bisect files are there meaning we're in bisect mode.
// Doesn't necessarily mean that we've actually picked a good/bad commit yet.
started bool
// this is the ref you started the commit from
start string // this will always be defined
// these will be defined if we've started
newTerm string // 'bad' by default
oldTerm string // 'good' by default
// map of commit sha's to their status
statusMap map[string]BisectStatus
// the sha of the commit that's under test
current string
}
type BisectStatus int
const (
BisectStatusOld BisectStatus = iota
BisectStatusNew
BisectStatusSkipped
)
// null object pattern
func NewNullBisectInfo() *BisectInfo {
return &BisectInfo{started: false}
}
func (self *BisectInfo) GetNewSha() string {
for sha, status := range self.statusMap {
if status == BisectStatusNew {
return sha
}
}
return ""
}
func (self *BisectInfo) GetCurrentSha() string {
return self.current
}
func (self *BisectInfo) GetStartSha() string {
return self.start
}
func (self *BisectInfo) Status(commitSha string) (BisectStatus, bool) {
status, ok := self.statusMap[commitSha]
return status, ok
}
func (self *BisectInfo) NewTerm() string {
return self.newTerm
}
func (self *BisectInfo) OldTerm() string {
return self.oldTerm
}
// this is for when we have called `git bisect start`. It does not
// mean that we have actually started narrowing things down or selecting good/bad commits
func (self *BisectInfo) Started() bool {
return self.started
}
// this is where we have both a good and bad revision and we're actually
// starting to narrow things down
func (self *BisectInfo) Bisecting() bool {
if !self.Started() {
return false
}
if self.GetNewSha() == "" {
return false
}
return slices.Contains(maps.Values(self.statusMap), BisectStatusOld)
}

View File

@@ -0,0 +1,172 @@
package git_commands
import (
"fmt"
"regexp"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// this takes something like:
// * (HEAD detached at 264fc6f5)
// remotes
// and returns '264fc6f5' as the second match
const CurrentBranchNameRegex = `(?m)^\*.*?([^ ]*?)\)?$`
type BranchCommands struct {
*GitCommon
}
func NewBranchCommands(gitCommon *GitCommon) *BranchCommands {
return &BranchCommands{
GitCommon: gitCommon,
}
}
// New creates a new branch
func (self *BranchCommands) New(name string, base string) error {
return self.cmd.New(fmt.Sprintf("git checkout -b %s %s", self.cmd.Quote(name), self.cmd.Quote(base))).Run()
}
// CurrentBranchName get the current branch name and displayname.
// the first returned string is the name and the second is the displayname
// e.g. name is 123asdf and displayname is '(HEAD detached at 123asdf)'
func (self *BranchCommands) CurrentBranchName() (string, string, error) {
branchName, err := self.cmd.New("git symbolic-ref --short HEAD").DontLog().RunWithOutput()
if err == nil && branchName != "HEAD\n" {
trimmedBranchName := strings.TrimSpace(branchName)
return trimmedBranchName, trimmedBranchName, nil
}
output, err := self.cmd.New("git branch --contains").DontLog().RunWithOutput()
if err != nil {
return "", "", err
}
for _, line := range utils.SplitLines(output) {
re := regexp.MustCompile(CurrentBranchNameRegex)
match := re.FindStringSubmatch(line)
if len(match) > 0 {
branchName = match[1]
displayBranchName := match[0][2:]
return branchName, displayBranchName, nil
}
}
return "HEAD", "HEAD", nil
}
// Delete delete branch
func (self *BranchCommands) Delete(branch string, force bool) error {
command := "git branch -d"
if force {
command = "git branch -D"
}
return self.cmd.New(fmt.Sprintf("%s %s", command, self.cmd.Quote(branch))).Run()
}
// Checkout checks out a branch (or commit), with --force if you set the force arg to true
type CheckoutOptions struct {
Force bool
EnvVars []string
}
func (self *BranchCommands) Checkout(branch string, options CheckoutOptions) error {
forceArg := ""
if options.Force {
forceArg = " --force"
}
return self.cmd.New(fmt.Sprintf("git checkout%s %s", forceArg, self.cmd.Quote(branch))).
// prevents git from prompting us for input which would freeze the program
// TODO: see if this is actually needed here
AddEnvVars("GIT_TERMINAL_PROMPT=0").
AddEnvVars(options.EnvVars...).
Run()
}
// GetGraph gets the color-formatted graph of the log for the given branch
// Currently it limits the result to 100 commits, but when we get async stuff
// working we can do lazy loading
func (self *BranchCommands) GetGraph(branchName string) (string, error) {
return self.GetGraphCmdObj(branchName).DontLog().RunWithOutput()
}
func (self *BranchCommands) GetGraphCmdObj(branchName string) oscommands.ICmdObj {
branchLogCmdTemplate := self.UserConfig.Git.BranchLogCmd
templateValues := map[string]string{
"branchName": self.cmd.Quote(branchName),
}
return self.cmd.New(utils.ResolvePlaceholderString(branchLogCmdTemplate, templateValues)).DontLog()
}
func (self *BranchCommands) SetCurrentBranchUpstream(remoteName string, remoteBranchName string) error {
return self.cmd.New(fmt.Sprintf("git branch --set-upstream-to=%s/%s", self.cmd.Quote(remoteName), self.cmd.Quote(remoteBranchName))).Run()
}
func (self *BranchCommands) SetUpstream(remoteName string, remoteBranchName string, branchName string) error {
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}")
}
func (self *BranchCommands) GetUpstreamDifferenceCount(branchName string) (string, string) {
return self.GetCommitDifferences(branchName, branchName+"@{u}")
}
// GetCommitDifferences checks how many pushables/pullables there are for the
// current branch
func (self *BranchCommands) GetCommitDifferences(from, to string) (string, string) {
command := "git rev-list %s..%s --count"
pushableCount, err := self.cmd.New(fmt.Sprintf(command, to, from)).DontLog().RunWithOutput()
if err != nil {
return "?", "?"
}
pullableCount, err := self.cmd.New(fmt.Sprintf(command, from, to)).DontLog().RunWithOutput()
if err != nil {
return "?", "?"
}
return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount)
}
func (self *BranchCommands) IsHeadDetached() bool {
err := self.cmd.New("git symbolic-ref -q HEAD").DontLog().Run()
return err != nil
}
func (self *BranchCommands) Rename(oldName string, newName string) error {
return self.cmd.New(fmt.Sprintf("git branch --move %s %s", self.cmd.Quote(oldName), self.cmd.Quote(newName))).Run()
}
func (self *BranchCommands) GetRawBranches() (string, error) {
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 {
FastForwardOnly bool
}
func (self *BranchCommands) Merge(branchName string, opts MergeOpts) error {
mergeArg := ""
if self.UserConfig.Git.Merging.Args != "" {
mergeArg = " " + self.UserConfig.Git.Merging.Args
}
command := fmt.Sprintf("git merge --no-edit%s %s", mergeArg, self.cmd.Quote(branchName))
if opts.FastForwardOnly {
command = fmt.Sprintf("%s --ff-only", command)
}
return self.cmd.New(command).Run()
}
func (self *BranchCommands) AllBranchesLogCmdObj() oscommands.ICmdObj {
return self.cmd.New(self.UserConfig.Git.AllBranchesLogCmd).DontLog()
}

View File

@@ -0,0 +1,225 @@
package git_commands
import (
"testing"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/stretchr/testify/assert"
)
func TestBranchGetCommitDifferences(t *testing.T) {
type scenario struct {
testName string
runner *oscommands.FakeCmdObjRunner
expectedPushables string
expectedPullables string
}
scenarios := []scenario{
{
"Can't retrieve pushable count",
oscommands.NewFakeRunner(t).
Expect("git rev-list @{u}..HEAD --count", "", errors.New("error")),
"?", "?",
},
{
"Can't retrieve pullable count",
oscommands.NewFakeRunner(t).
Expect("git rev-list @{u}..HEAD --count", "1\n", nil).
Expect("git rev-list HEAD..@{u} --count", "", errors.New("error")),
"?", "?",
},
{
"Retrieve pullable and pushable count",
oscommands.NewFakeRunner(t).
Expect("git rev-list @{u}..HEAD --count", "1\n", nil).
Expect("git rev-list HEAD..@{u} --count", "2\n", nil),
"1", "2",
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildBranchCommands(commonDeps{runner: s.runner})
pushables, pullables := instance.GetCommitDifferences("HEAD", "@{u}")
assert.EqualValues(t, s.expectedPushables, pushables)
assert.EqualValues(t, s.expectedPullables, pullables)
s.runner.CheckForMissingCalls()
})
}
}
func TestBranchNewBranch(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
Expect(`git checkout -b "test" "master"`, "", nil)
instance := buildBranchCommands(commonDeps{runner: runner})
assert.NoError(t, instance.New("test", "master"))
runner.CheckForMissingCalls()
}
func TestBranchDeleteBranch(t *testing.T) {
type scenario struct {
testName string
force bool
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
"Delete a branch",
false,
oscommands.NewFakeRunner(t).Expect(`git branch -d "test"`, "", nil),
func(err error) {
assert.NoError(t, err)
},
},
{
"Force delete a branch",
true,
oscommands.NewFakeRunner(t).Expect(`git branch -D "test"`, "", nil),
func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildBranchCommands(commonDeps{runner: s.runner})
s.test(instance.Delete("test", s.force))
s.runner.CheckForMissingCalls()
})
}
}
func TestBranchMerge(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
Expect(`git merge --no-edit "test"`, "", nil)
instance := buildBranchCommands(commonDeps{runner: runner})
assert.NoError(t, instance.Merge("test", MergeOpts{}))
runner.CheckForMissingCalls()
}
func TestBranchCheckout(t *testing.T) {
type scenario struct {
testName string
runner *oscommands.FakeCmdObjRunner
test func(error)
force bool
}
scenarios := []scenario{
{
"Checkout",
oscommands.NewFakeRunner(t).Expect(`git checkout "test"`, "", nil),
func(err error) {
assert.NoError(t, err)
},
false,
},
{
"Checkout forced",
oscommands.NewFakeRunner(t).Expect(`git checkout --force "test"`, "", nil),
func(err error) {
assert.NoError(t, err)
},
true,
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildBranchCommands(commonDeps{runner: s.runner})
s.test(instance.Checkout("test", CheckoutOptions{Force: s.force}))
s.runner.CheckForMissingCalls()
})
}
}
func TestBranchGetBranchGraph(t *testing.T) {
runner := oscommands.NewFakeRunner(t).ExpectGitArgs([]string{
"log", "--graph", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium", "test", "--",
}, "", nil)
instance := buildBranchCommands(commonDeps{runner: runner})
_, err := instance.GetGraph("test")
assert.NoError(t, err)
}
func TestBranchGetAllBranchGraph(t *testing.T) {
runner := oscommands.NewFakeRunner(t).ExpectGitArgs([]string{
"log", "--graph", "--all", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium",
}, "", nil)
instance := buildBranchCommands(commonDeps{runner: runner})
err := instance.AllBranchesLogCmdObj().Run()
assert.NoError(t, err)
}
func TestBranchCurrentBranchName(t *testing.T) {
type scenario struct {
testName string
runner *oscommands.FakeCmdObjRunner
test func(string, string, error)
}
scenarios := []scenario{
{
"says we are on the master branch if we are",
oscommands.NewFakeRunner(t).Expect(`git symbolic-ref --short HEAD`, "master", nil),
func(name string, displayname string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "master", name)
assert.EqualValues(t, "master", displayname)
},
},
{
"falls back to git `git branch --contains` if symbolic-ref fails",
oscommands.NewFakeRunner(t).
Expect(`git symbolic-ref --short HEAD`, "", errors.New("error")).
Expect(`git branch --contains`, "* master", nil),
func(name string, displayname string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "master", name)
assert.EqualValues(t, "master", displayname)
},
},
{
"handles a detached head",
oscommands.NewFakeRunner(t).
Expect(`git symbolic-ref --short HEAD`, "", errors.New("error")).
Expect(`git branch --contains`, "* (HEAD detached at 123abcd)", nil),
func(name string, displayname string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "123abcd", name)
assert.EqualValues(t, "(HEAD detached at 123abcd)", displayname)
},
},
{
"bubbles up error if there is one",
oscommands.NewFakeRunner(t).
Expect(`git symbolic-ref --short HEAD`, "", errors.New("error")).
Expect(`git branch --contains`, "", errors.New("error")),
func(name string, displayname string, err error) {
assert.Error(t, err)
assert.EqualValues(t, "", name)
assert.EqualValues(t, "", displayname)
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildBranchCommands(commonDeps{runner: s.runner})
s.test(instance.CurrentBranchName())
s.runner.CheckForMissingCalls()
})
}
}

View File

@@ -0,0 +1,164 @@
package git_commands
import (
"fmt"
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
)
type CommitCommands struct {
*GitCommon
}
func NewCommitCommands(gitCommon *GitCommon) *CommitCommands {
return &CommitCommands{
GitCommon: gitCommon,
}
}
// RewordLastCommit rewords the topmost commit with the given message
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)).
// prevents git from prompting us for input which would freeze the program
// TODO: see if this is actually needed here
AddEnvVars("GIT_TERMINAL_PROMPT=0").
AddEnvVars(envVars...).
Run()
}
func (self *CommitCommands) CommitCmdObj(message string) oscommands.ICmdObj {
splitMessage := strings.Split(message, "\n")
lineArgs := ""
for _, line := range splitMessage {
lineArgs += fmt.Sprintf(" -m %s", self.cmd.Quote(line))
}
skipHookPrefix := self.UserConfig.Git.SkipHookPrefix
noVerifyFlag := ""
if skipHookPrefix != "" && strings.HasPrefix(message, skipHookPrefix) {
noVerifyFlag = " --no-verify"
}
return self.cmd.New(fmt.Sprintf("git commit%s%s%s", noVerifyFlag, self.signoffFlag(), lineArgs))
}
// runs git commit without the -m argument meaning it will invoke the user's editor
func (self *CommitCommands) CommitEditorCmdObj() oscommands.ICmdObj {
return self.cmd.New(fmt.Sprintf("git commit%s", self.signoffFlag()))
}
func (self *CommitCommands) signoffFlag() string {
if self.UserConfig.Git.Commit.SignOff {
return " --signoff"
} else {
return ""
}
}
// Get the subject of the HEAD commit
func (self *CommitCommands) GetHeadCommitMessage() (string, error) {
message, err := self.cmd.New("git log -1 --pretty=%s").DontLog().RunWithOutput()
return strings.TrimSpace(message), err
}
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:], "")
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})
}
func (self *CommitCommands) GetCommitMessagesFirstLine(shas []string) (string, error) {
return self.cmd.New(
fmt.Sprintf("git show --no-patch --pretty=format:%%s %s", strings.Join(shas, " ")),
).DontLog().RunWithOutput()
}
func (self *CommitCommands) GetCommitsOneline(shas []string) (string, error) {
return self.cmd.New(
fmt.Sprintf("git show --no-patch --oneline %s", strings.Join(shas, " ")),
).DontLog().RunWithOutput()
}
// AmendHead amends HEAD with whatever is staged in your working tree
func (self *CommitCommands) AmendHead() error {
return self.AmendHeadCmdObj().Run()
}
func (self *CommitCommands) AmendHeadCmdObj() oscommands.ICmdObj {
return self.cmd.New("git commit --amend --no-edit --allow-empty")
}
func (self *CommitCommands) ShowCmdObj(sha string, filterPath string) oscommands.ICmdObj {
contextSize := self.UserConfig.Git.DiffContextSize
filterPathArg := ""
if filterPath != "" {
filterPathArg = fmt.Sprintf(" -- %s", self.cmd.Quote(filterPath))
}
cmdStr := fmt.Sprintf("git show --submodule --color=%s --unified=%d --no-renames --stat -p %s %s", self.UserConfig.Git.Paging.ColorArg, contextSize, sha, filterPathArg)
return self.cmd.New(cmdStr).DontLog()
}
// Revert reverts the selected commit by sha
func (self *CommitCommands) Revert(sha string) error {
return self.cmd.New(fmt.Sprintf("git revert %s", sha)).Run()
}
func (self *CommitCommands) RevertMerge(sha string, parentNumber int) error {
return self.cmd.New(fmt.Sprintf("git revert %s -m %d", sha, parentNumber)).Run()
}
// CreateFixupCommit creates a commit that fixes up a previous commit
func (self *CommitCommands) CreateFixupCommit(sha string) error {
return self.cmd.New(fmt.Sprintf("git commit --fixup=%s", sha)).Run()
}

View File

@@ -0,0 +1,209 @@
package git_commands
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/stretchr/testify/assert"
)
func TestCommitRewordCommit(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, "", nil)
instance := buildCommitCommands(commonDeps{runner: runner})
assert.NoError(t, instance.RewordLastCommit("test"))
runner.CheckForMissingCalls()
}
func TestCommitResetToCommit(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"reset", "--hard", "78976bc"}, "", nil)
instance := buildCommitCommands(commonDeps{runner: runner})
assert.NoError(t, instance.ResetToCommit("78976bc", "hard", []string{}))
runner.CheckForMissingCalls()
}
func TestCommitCommitObj(t *testing.T) {
type scenario struct {
testName string
message string
configSignoff bool
configSkipHookPrefix string
expected string
}
scenarios := []scenario{
{
testName: "Commit",
message: "test",
configSignoff: false,
configSkipHookPrefix: "",
expected: `git commit -m "test"`,
},
{
testName: "Commit with --no-verify flag",
message: "WIP: test",
configSignoff: false,
configSkipHookPrefix: "WIP",
expected: `git commit --no-verify -m "WIP: test"`,
},
{
testName: "Commit with multiline message",
message: "line1\nline2",
configSignoff: false,
configSkipHookPrefix: "",
expected: `git commit -m "line1" -m "line2"`,
},
{
testName: "Commit with signoff",
message: "test",
configSignoff: true,
configSkipHookPrefix: "",
expected: `git commit --signoff -m "test"`,
},
{
testName: "Commit with signoff and no-verify",
message: "WIP: test",
configSignoff: true,
configSkipHookPrefix: "WIP",
expected: `git commit --no-verify --signoff -m "WIP: test"`,
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
userConfig := config.GetDefaultConfig()
userConfig.Git.Commit.SignOff = s.configSignoff
userConfig.Git.SkipHookPrefix = s.configSkipHookPrefix
instance := buildCommitCommands(commonDeps{userConfig: userConfig})
cmdStr := instance.CommitCmdObj(s.message).ToString()
assert.Equal(t, s.expected, cmdStr)
})
}
}
func TestCommitCreateFixupCommit(t *testing.T) {
type scenario struct {
testName string
sha string
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
testName: "valid case",
sha: "12345",
runner: oscommands.NewFakeRunner(t).
Expect(`git commit --fixup=12345`, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildCommitCommands(commonDeps{runner: s.runner})
s.test(instance.CreateFixupCommit(s.sha))
s.runner.CheckForMissingCalls()
})
}
}
func TestCommitShowCmdObj(t *testing.T) {
type scenario struct {
testName string
filterPath string
contextSize int
expected string
}
scenarios := []scenario{
{
testName: "Default case without filter path",
filterPath: "",
contextSize: 3,
expected: "git show --submodule --color=always --unified=3 --no-renames --stat -p 1234567890 ",
},
{
testName: "Default case with filter path",
filterPath: "file.txt",
contextSize: 3,
expected: `git show --submodule --color=always --unified=3 --no-renames --stat -p 1234567890 -- "file.txt"`,
},
{
testName: "Show diff with custom context size",
filterPath: "",
contextSize: 77,
expected: "git show --submodule --color=always --unified=77 --no-renames --stat -p 1234567890 ",
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
userConfig := config.GetDefaultConfig()
userConfig.Git.DiffContextSize = s.contextSize
instance := buildCommitCommands(commonDeps{userConfig: userConfig})
cmdStr := instance.ShowCmdObj("1234567890", s.filterPath).ToString()
assert.Equal(t, s.expected, cmdStr)
})
}
}
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)
})
}
}

View File

@@ -0,0 +1,39 @@
package git_commands
import (
gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/sasha-s/go-deadlock"
)
type GitCommon struct {
*common.Common
cmd oscommands.ICmdObjBuilder
os *oscommands.OSCommand
dotGitDir string
repo *gogit.Repository
config *ConfigCommands
// mutex for doing things like push/pull/fetch
syncMutex *deadlock.Mutex
}
func NewGitCommon(
cmn *common.Common,
cmd oscommands.ICmdObjBuilder,
osCommand *oscommands.OSCommand,
dotGitDir string,
repo *gogit.Repository,
config *ConfigCommands,
syncMutex *deadlock.Mutex,
) *GitCommon {
return &GitCommon{
Common: cmn,
cmd: cmd,
os: osCommand,
dotGitDir: dotGitDir,
repo: repo,
config: config,
syncMutex: syncMutex,
}
}

View File

@@ -0,0 +1,101 @@
package git_commands
import (
"os"
"strconv"
"strings"
gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/go-git/v5/config"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type ConfigCommands struct {
*common.Common
gitConfig git_config.IGitConfig
repo *gogit.Repository
}
func NewConfigCommands(
common *common.Common,
gitConfig git_config.IGitConfig,
repo *gogit.Repository,
) *ConfigCommands {
return &ConfigCommands{
Common: common,
gitConfig: gitConfig,
repo: repo,
}
}
func (self *ConfigCommands) ConfiguredPager() string {
if os.Getenv("GIT_PAGER") != "" {
return os.Getenv("GIT_PAGER")
}
if os.Getenv("PAGER") != "" {
return os.Getenv("PAGER")
}
output := self.gitConfig.Get("core.pager")
return strings.Split(output, "\n")[0]
}
func (self *ConfigCommands) GetPager(width int) string {
useConfig := self.UserConfig.Git.Paging.UseConfig
if useConfig {
pager := self.ConfiguredPager()
return strings.Split(pager, "| less")[0]
}
templateValues := map[string]string{
"columnWidth": strconv.Itoa(width/2 - 6),
}
pagerTemplate := self.UserConfig.Git.Paging.Pager
return utils.ResolvePlaceholderString(pagerTemplate, templateValues)
}
// UsingGpg tells us whether the user has gpg enabled so that we can know
// whether we need to run a subprocess to allow them to enter their password
func (self *ConfigCommands) UsingGpg() bool {
overrideGpg := self.UserConfig.Git.OverrideGpg
if overrideGpg {
return false
}
return self.gitConfig.GetBool("commit.gpgsign")
}
func (self *ConfigCommands) GetCoreEditor() string {
return self.gitConfig.Get("core.editor")
}
// GetRemoteURL returns current repo remote url
func (self *ConfigCommands) GetRemoteURL() string {
return self.gitConfig.Get("remote.origin.url")
}
func (self *ConfigCommands) GetShowUntrackedFiles() string {
return self.gitConfig.Get("status.showUntrackedFiles")
}
// this determines whether the user has configured to push to the remote branch of the same name as the current or not
func (self *ConfigCommands) GetPushToCurrent() bool {
return self.gitConfig.Get("push.default") == "current"
}
// returns the repo's branches as specified in the git config
func (self *ConfigCommands) Branches() (map[string]*config.Branch, error) {
conf, err := self.repo.Config()
if err != nil {
return nil, err
}
return conf.Branches, nil
}
func (self *ConfigCommands) GetGitFlowPrefixes() string {
return self.gitConfig.GetGeneral("--local --get-regexp gitflow.prefix")
}

View File

@@ -0,0 +1,18 @@
package git_commands
type CustomCommands struct {
*GitCommon
}
func NewCustomCommands(gitCommon *GitCommon) *CustomCommands {
return &CustomCommands{
GitCommon: gitCommon,
}
}
// Only to be used for the sake of running custom commands specified by the user.
// If you want to run a new command, try finding a place for it in one of the neighbouring
// files, or creating a new BlahCommands struct to hold it.
func (self *CustomCommands) RunWithOutput(cmdStr string) (string, error) {
return self.cmd.New(cmdStr).RunWithOutput()
}

View File

@@ -0,0 +1,144 @@
package git_commands
import (
"github.com/go-errors/errors"
gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type commonDeps struct {
runner *oscommands.FakeCmdObjRunner
userConfig *config.UserConfig
gitConfig *git_config.FakeGitConfig
getenv func(string) string
removeFile func(string) error
dotGitDir string
common *common.Common
cmd *oscommands.CmdObjBuilder
}
func buildGitCommon(deps commonDeps) *GitCommon {
gitCommon := &GitCommon{}
gitCommon.Common = deps.common
if gitCommon.Common == nil {
gitCommon.Common = utils.NewDummyCommonWithUserConfig(deps.userConfig)
}
runner := deps.runner
if runner == nil {
runner = oscommands.NewFakeRunner(nil)
}
cmd := deps.cmd
// gotta check deps.cmd because it's not an interface type and an interface value of nil is not considered to be nil
if cmd == nil {
cmd = oscommands.NewDummyCmdObjBuilder(runner)
}
gitCommon.cmd = cmd
gitCommon.Common.UserConfig = deps.userConfig
if gitCommon.Common.UserConfig == nil {
gitCommon.Common.UserConfig = config.GetDefaultConfig()
}
gitConfig := deps.gitConfig
if gitConfig == nil {
gitConfig = git_config.NewFakeGitConfig(nil)
}
gitCommon.repo = buildRepo()
gitCommon.config = NewConfigCommands(gitCommon.Common, gitConfig, gitCommon.repo)
getenv := deps.getenv
if getenv == nil {
getenv = func(string) string { return "" }
}
removeFile := deps.removeFile
if removeFile == nil {
removeFile = func(string) error { return errors.New("unexpected call to removeFile") }
}
gitCommon.os = oscommands.NewDummyOSCommandWithDeps(oscommands.OSCommandDeps{
Common: gitCommon.Common,
GetenvFn: getenv,
Cmd: cmd,
RemoveFileFn: removeFile,
})
gitCommon.dotGitDir = deps.dotGitDir
if gitCommon.dotGitDir == "" {
gitCommon.dotGitDir = ".git"
}
return gitCommon
}
func buildRepo() *gogit.Repository {
// TODO: think of a way to actually mock this out
var repo *gogit.Repository = nil
return repo
}
func buildFileLoader(gitCommon *GitCommon) *loaders.FileLoader {
return loaders.NewFileLoader(gitCommon.Common, gitCommon.cmd, gitCommon.config)
}
func buildSubmoduleCommands(deps commonDeps) *SubmoduleCommands {
gitCommon := buildGitCommon(deps)
return NewSubmoduleCommands(gitCommon)
}
func buildCommitCommands(deps commonDeps) *CommitCommands {
gitCommon := buildGitCommon(deps)
return NewCommitCommands(gitCommon)
}
func buildWorkingTreeCommands(deps commonDeps) *WorkingTreeCommands {
gitCommon := buildGitCommon(deps)
submoduleCommands := buildSubmoduleCommands(deps)
fileLoader := buildFileLoader(gitCommon)
return NewWorkingTreeCommands(gitCommon, submoduleCommands, fileLoader)
}
func buildStashCommands(deps commonDeps) *StashCommands {
gitCommon := buildGitCommon(deps)
fileLoader := buildFileLoader(gitCommon)
workingTreeCommands := buildWorkingTreeCommands(deps)
return NewStashCommands(gitCommon, fileLoader, workingTreeCommands)
}
func buildRebaseCommands(deps commonDeps) *RebaseCommands {
gitCommon := buildGitCommon(deps)
workingTreeCommands := buildWorkingTreeCommands(deps)
commitCommands := buildCommitCommands(deps)
return NewRebaseCommands(gitCommon, commitCommands, workingTreeCommands)
}
func buildSyncCommands(deps commonDeps) *SyncCommands {
gitCommon := buildGitCommon(deps)
return NewSyncCommands(gitCommon)
}
func buildFileCommands(deps commonDeps) *FileCommands {
gitCommon := buildGitCommon(deps)
return NewFileCommands(gitCommon)
}
func buildBranchCommands(deps commonDeps) *BranchCommands {
gitCommon := buildGitCommon(deps)
return NewBranchCommands(gitCommon)
}

View File

@@ -0,0 +1,74 @@
package git_commands
import (
"io/ioutil"
"strconv"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type FileCommands struct {
*GitCommon
}
func NewFileCommands(gitCommon *GitCommon) *FileCommands {
return &FileCommands{
GitCommon: gitCommon,
}
}
// Cat obtains the content of a file
func (self *FileCommands) Cat(fileName string) (string, error) {
buf, err := ioutil.ReadFile(fileName)
if err != nil {
return "", nil
}
return string(buf), nil
}
func (self *FileCommands) GetEditCmdStr(filename string, lineNumber int) (string, error) {
editor := self.UserConfig.OS.EditCommand
if editor == "" {
editor = self.config.GetCoreEditor()
}
if editor == "" {
editor = self.os.Getenv("GIT_EDITOR")
}
if editor == "" {
editor = self.os.Getenv("VISUAL")
}
if editor == "" {
editor = self.os.Getenv("EDITOR")
}
if editor == "" {
if err := self.cmd.New("which vi").DontLog().Run(); err == nil {
editor = "vi"
}
}
if editor == "" {
return "", errors.New("No editor defined in config file, $GIT_EDITOR, $VISUAL, $EDITOR, or git config")
}
templateValues := map[string]string{
"editor": editor,
"filename": self.cmd.Quote(filename),
"line": strconv.Itoa(lineNumber),
}
editCmdTemplate := self.UserConfig.OS.EditCommandTemplate
if len(editCmdTemplate) == 0 {
switch editor {
case "emacs", "nano", "vi", "vim", "nvim":
editCmdTemplate = "{{editor}} +{{line}} -- {{filename}}"
case "subl":
editCmdTemplate = "{{editor}} -- {{filename}}:{{line}}"
case "code":
editCmdTemplate = "{{editor}} -r --goto -- {{filename}}:{{line}}"
default:
editCmdTemplate = "{{editor}} -- {{filename}}"
}
}
return utils.ResolvePlaceholderString(editCmdTemplate, templateValues), nil
}

View File

@@ -0,0 +1,178 @@
package git_commands
import (
"testing"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/stretchr/testify/assert"
)
func TestEditFileCmdStr(t *testing.T) {
type scenario struct {
filename string
configEditCommand string
configEditCommandTemplate string
runner *oscommands.FakeCmdObjRunner
getenv func(string) string
gitConfigMockResponses map[string]string
test func(string, error)
}
scenarios := []scenario{
{
filename: "test",
configEditCommand: "",
configEditCommandTemplate: "{{editor}} {{filename}}",
runner: oscommands.NewFakeRunner(t).
Expect(`which vi`, "", errors.New("error")),
getenv: func(env string) string {
return ""
},
gitConfigMockResponses: nil,
test: func(cmdStr string, err error) {
assert.EqualError(t, err, "No editor defined in config file, $GIT_EDITOR, $VISUAL, $EDITOR, or git config")
},
},
{
filename: "test",
configEditCommand: "nano",
configEditCommandTemplate: "{{editor}} {{filename}}",
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, `nano "test"`, cmdStr)
},
},
{
filename: "test",
configEditCommand: "",
configEditCommandTemplate: "{{editor}} {{filename}}",
runner: oscommands.NewFakeRunner(t),
getenv: func(env string) string {
return ""
},
gitConfigMockResponses: map[string]string{"core.editor": "nano"},
test: func(cmdStr string, err error) {
assert.NoError(t, err)
assert.Equal(t, `nano "test"`, cmdStr)
},
},
{
filename: "test",
configEditCommand: "",
configEditCommandTemplate: "{{editor}} {{filename}}",
runner: oscommands.NewFakeRunner(t),
getenv: func(env string) string {
if env == "VISUAL" {
return "nano"
}
return ""
},
gitConfigMockResponses: nil,
test: func(cmdStr string, err error) {
assert.NoError(t, err)
assert.Equal(t, `nano "test"`, cmdStr)
},
},
{
filename: "test",
configEditCommand: "",
configEditCommandTemplate: "{{editor}} {{filename}}",
runner: oscommands.NewFakeRunner(t),
getenv: func(env string) string {
if env == "EDITOR" {
return "emacs"
}
return ""
},
gitConfigMockResponses: nil,
test: func(cmdStr string, err error) {
assert.NoError(t, err)
assert.Equal(t, `emacs "test"`, cmdStr)
},
},
{
filename: "test",
configEditCommand: "",
configEditCommandTemplate: "{{editor}} {{filename}}",
runner: oscommands.NewFakeRunner(t).
Expect(`which vi`, "/usr/bin/vi", nil),
getenv: func(env string) string {
return ""
},
gitConfigMockResponses: nil,
test: func(cmdStr string, err error) {
assert.NoError(t, err)
assert.Equal(t, `vi "test"`, cmdStr)
},
},
{
filename: "file/with space",
configEditCommand: "",
configEditCommandTemplate: "{{editor}} {{filename}}",
runner: oscommands.NewFakeRunner(t).
Expect(`which vi`, "/usr/bin/vi", nil),
getenv: func(env string) string {
return ""
},
gitConfigMockResponses: nil,
test: func(cmdStr string, err error) {
assert.NoError(t, err)
assert.Equal(t, `vi "file/with space"`, cmdStr)
},
},
{
filename: "open file/at line",
configEditCommand: "vim",
configEditCommandTemplate: "{{editor}} +{{line}} {{filename}}",
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 "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 {
userConfig := config.GetDefaultConfig()
userConfig.OS.EditCommand = s.configEditCommand
userConfig.OS.EditCommandTemplate = s.configEditCommandTemplate
instance := buildFileCommands(commonDeps{
runner: s.runner,
userConfig: userConfig,
gitConfig: git_config.NewFakeGitConfig(s.gitConfigMockResponses),
getenv: s.getenv,
})
s.test(instance.GetEditCmdStr(s.filename, 1))
s.runner.CheckForMissingCalls()
}
}

View File

@@ -0,0 +1,56 @@
package git_commands
import (
"regexp"
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
)
type FlowCommands struct {
*GitCommon
}
func NewFlowCommands(
gitCommon *GitCommon,
) *FlowCommands {
return &FlowCommands{
GitCommon: gitCommon,
}
}
func (self *FlowCommands) GitFlowEnabled() bool {
return self.config.GetGitFlowPrefixes() != ""
}
func (self *FlowCommands) FinishCmdObj(branchName string) (oscommands.ICmdObj, error) {
prefixes := self.config.GetGitFlowPrefixes()
// need to find out what kind of branch this is
prefix := strings.SplitAfterN(branchName, "/", 2)[0]
suffix := strings.Replace(branchName, prefix, "", 1)
branchType := ""
for _, line := range strings.Split(strings.TrimSpace(prefixes), "\n") {
if strings.HasPrefix(line, "gitflow.prefix.") && strings.HasSuffix(line, prefix) {
regex := regexp.MustCompile("gitflow.prefix.([^ ]*) .*")
matches := regex.FindAllStringSubmatch(line, 1)
if len(matches) > 0 && len(matches[0]) > 1 {
branchType = matches[0][1]
break
}
}
}
if branchType == "" {
return nil, errors.New(self.Tr.NotAGitFlowBranch)
}
return self.cmd.New("git flow " + branchType + " finish " + suffix), nil
}
func (self *FlowCommands) StartCmdObj(branchType string, name string) oscommands.ICmdObj {
return self.cmd.New("git flow " + branchType + " start " + name)
}

View File

@@ -0,0 +1,146 @@
package git_commands
import (
"encoding/json"
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
)
type GhCommands struct {
*GitCommon
}
func NewGhCommand(gitCommon *GitCommon) *GhCommands {
return &GhCommands{
GitCommon: gitCommon,
}
}
// https://github.com/cli/cli/issues/2300
func (self *GhCommands) BaseRepo() error {
return self.cmd.New("git config --local --get-regexp .gh-resolved").Run()
}
// Ex: git config --local --add "remote.origin.gh-resolved" "jesseduffield/lazygit"
func (self *GhCommands) SetBaseRepo(repository string) (string, error) {
return self.cmd.New(
fmt.Sprintf("git config --local --add \"remote.origin.gh-resolved\" \"%s\"", repository),
).RunWithOutput()
}
func (self *GhCommands) prList() (string, error) {
return self.cmd.New(
"gh pr list --limit 500 --state all --json state,url,number,headRefName,headRepositoryOwner",
).RunWithOutput()
}
func (self *GhCommands) GithubMostRecentPRs() ([]*models.GithubPullRequest, error) {
commandOutput, err := self.prList()
if err != nil {
return nil, err
}
prs := []*models.GithubPullRequest{}
err = json.Unmarshal([]byte(commandOutput), &prs)
if err != nil {
return nil, err
}
return prs, nil
}
func GenerateGithubPullRequestMap(prs []*models.GithubPullRequest, branches []*models.Branch, remotes []*models.Remote) map[*models.Branch]*models.GithubPullRequest {
res := map[*models.Branch]*models.GithubPullRequest{}
if len(prs) == 0 {
return res
}
remotesToOwnersMap := getRemotesToOwnersMap(remotes)
if len(remotesToOwnersMap) == 0 {
return res
}
// A PR can be identified by two things: the owner e.g. 'jesseduffield' and the
// branch name e.g. 'feature/my-feature'. The owner might be different
// to the owner of the repo if the PR is from a fork of that repo.
type prKey struct {
owner string
branchName string
}
prByKey := map[prKey]models.GithubPullRequest{}
for _, pr := range prs {
prByKey[prKey{owner: pr.UserName(), branchName: pr.BranchName()}] = *pr
}
for _, branch := range branches {
if !branch.IsTrackingRemote() {
continue
}
// TODO: support branches whose UpstreamRemote contains a full git
// URL rather than just a remote name.
owner, foundRemoteOwner := remotesToOwnersMap[branch.UpstreamRemote]
if !foundRemoteOwner {
continue
}
pr, hasPr := prByKey[prKey{owner: owner, branchName: branch.UpstreamBranch}]
if !hasPr {
continue
}
res[branch] = &pr
}
return res
}
func getRemotesToOwnersMap(remotes []*models.Remote) map[string]string {
res := map[string]string{}
for _, remote := range remotes {
if len(remote.Urls) == 0 {
continue
}
res[remote.Name] = GetRepoInfoFromURL(remote.Urls[0]).Owner
}
return res
}
type RepoInformation struct {
Owner string
Repository string
}
// TODO: move this into hosting_service.go
func GetRepoInfoFromURL(url string) RepoInformation {
isHTTP := strings.HasPrefix(url, "http")
if isHTTP {
splits := strings.Split(url, "/")
owner := strings.Join(splits[3:len(splits)-1], "/")
repo := strings.TrimSuffix(splits[len(splits)-1], ".git")
return RepoInformation{
Owner: owner,
Repository: repo,
}
}
tmpSplit := strings.Split(url, ":")
splits := strings.Split(tmpSplit[1], "/")
owner := strings.Join(splits[0:len(splits)-1], "/")
repo := strings.TrimSuffix(splits[len(splits)-1], ".git")
return RepoInformation{
Owner: owner,
Repository: repo,
}
}

View File

@@ -0,0 +1,34 @@
package git_commands
import "github.com/jesseduffield/lazygit/pkg/commands/hosting_service"
// a hosting service is something like github, gitlab, bitbucket etc
type HostingService struct {
*GitCommon
}
func NewHostingServiceCommand(gitCommon *GitCommon) *HostingService {
return &HostingService{
GitCommon: gitCommon,
}
}
func (self *HostingService) GetPullRequestURL(from string, to string) (string, error) {
return self.getHostingServiceMgr(self.config.GetRemoteURL()).GetPullRequestURL(from, to)
}
func (self *HostingService) GetCommitURL(commitSha string) (string, error) {
return self.getHostingServiceMgr(self.config.GetRemoteURL()).GetCommitURL(commitSha)
}
func (self *HostingService) GetRepoNameFromRemoteURL(remoteURL string) (string, error) {
return self.getHostingServiceMgr(remoteURL).GetRepoName()
}
// getting this on every request rather than storing it in state in case our remoteURL changes
// from one invocation to the next. Note however that we're currently caching config
// results so we might want to invalidate the cache here if it becomes a problem.
func (self *HostingService) getHostingServiceMgr(remoteURL string) *hosting_service.HostingServiceMgr {
configServices := self.UserConfig.Services
return hosting_service.NewHostingServiceMgr(self.Log, self.Tr, remoteURL, configServices)
}

View File

@@ -0,0 +1,256 @@
package git_commands
import (
"fmt"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
)
type PatchCommands struct {
*GitCommon
rebase *RebaseCommands
commit *CommitCommands
status *StatusCommands
stash *StashCommands
PatchManager *patch.PatchManager
}
func NewPatchCommands(
gitCommon *GitCommon,
rebase *RebaseCommands,
commit *CommitCommands,
status *StatusCommands,
stash *StashCommands,
patchManager *patch.PatchManager,
) *PatchCommands {
return &PatchCommands{
GitCommon: gitCommon,
rebase: rebase,
commit: commit,
status: status,
stash: stash,
PatchManager: patchManager,
}
}
// DeletePatchesFromCommit applies a patch in reverse for a commit
func (self *PatchCommands) DeletePatchesFromCommit(commits []*models.Commit, commitIndex int) error {
if err := self.rebase.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil {
return err
}
// apply each patch in reverse
if err := self.PatchManager.ApplyPatches(true); err != nil {
if err := self.rebase.AbortRebase(); err != nil {
return err
}
return err
}
// time to amend the selected commit
if err := self.commit.AmendHead(); err != nil {
return err
}
self.rebase.onSuccessfulContinue = func() error {
self.PatchManager.Reset()
return nil
}
// continue
return self.rebase.ContinueRebase()
}
func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, sourceCommitIdx int, destinationCommitIdx int) error {
if sourceCommitIdx < destinationCommitIdx {
if err := self.rebase.BeginInteractiveRebaseForCommit(commits, destinationCommitIdx); err != nil {
return err
}
// apply each patch forward
if err := self.PatchManager.ApplyPatches(false); err != nil {
if err := self.rebase.AbortRebase(); err != nil {
return err
}
return err
}
// amend the destination commit
if err := self.commit.AmendHead(); err != nil {
return err
}
self.rebase.onSuccessfulContinue = func() error {
self.PatchManager.Reset()
return nil
}
// continue
return self.rebase.ContinueRebase()
}
if len(commits)-1 < sourceCommitIdx {
return errors.New("index outside of range of commits")
}
// we can make this GPG thing possible it just means we need to do this in two parts:
// one where we handle the possibility of a credential request, and the other
// where we continue the rebase
if self.config.UsingGpg() {
return errors.New(self.Tr.DisabledForGPG)
}
baseIndex := sourceCommitIdx + 1
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
}
// apply each patch in reverse
if err := self.PatchManager.ApplyPatches(true); err != nil {
if err := self.rebase.AbortRebase(); err != nil {
return err
}
return err
}
// amend the source commit
if err := self.commit.AmendHead(); err != nil {
return err
}
if self.rebase.onSuccessfulContinue != nil {
return errors.New("You are midway through another rebase operation. Please abort to start again")
}
self.rebase.onSuccessfulContinue = func() error {
// now we should be up to the destination, so let's apply forward these patches to that.
// ideally we would ensure we're on the right commit but I'm not sure if that check is necessary
if err := self.PatchManager.ApplyPatches(false); err != nil {
if err := self.rebase.AbortRebase(); err != nil {
return err
}
return err
}
// amend the destination commit
if err := self.commit.AmendHead(); err != nil {
return err
}
self.rebase.onSuccessfulContinue = func() error {
self.PatchManager.Reset()
return nil
}
return self.rebase.ContinueRebase()
}
return self.rebase.ContinueRebase()
}
func (self *PatchCommands) MovePatchIntoIndex(commits []*models.Commit, commitIdx int, stash bool) error {
if stash {
if err := self.stash.Save(self.Tr.StashPrefix + commits[commitIdx].Sha); err != nil {
return err
}
}
if err := self.rebase.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil {
return err
}
if err := self.PatchManager.ApplyPatches(true); err != nil {
if self.status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
if err := self.rebase.AbortRebase(); err != nil {
return err
}
}
return err
}
// amend the commit
if err := self.commit.AmendHead(); err != nil {
return err
}
if self.rebase.onSuccessfulContinue != nil {
return errors.New("You are midway through another rebase operation. Please abort to start again")
}
self.rebase.onSuccessfulContinue = func() error {
// add patches to index
if err := self.PatchManager.ApplyPatches(false); err != nil {
if self.status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
if err := self.rebase.AbortRebase(); err != nil {
return err
}
}
return err
}
if stash {
if err := self.stash.Apply(0); err != nil {
return err
}
}
self.PatchManager.Reset()
return nil
}
return self.rebase.ContinueRebase()
}
func (self *PatchCommands) PullPatchIntoNewCommit(commits []*models.Commit, commitIdx int) error {
if err := self.rebase.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil {
return err
}
if err := self.PatchManager.ApplyPatches(true); err != nil {
if err := self.rebase.AbortRebase(); err != nil {
return err
}
return err
}
// amend the commit
if err := self.commit.AmendHead(); err != nil {
return err
}
// add patches to index
if err := self.PatchManager.ApplyPatches(false); err != nil {
if err := self.rebase.AbortRebase(); err != nil {
return err
}
return err
}
head_message, _ := self.commit.GetHeadCommitMessage()
new_message := fmt.Sprintf("Split from \"%s\"", head_message)
err := self.commit.CommitCmdObj(new_message).Run()
if err != nil {
return err
}
if self.rebase.onSuccessfulContinue != nil {
return errors.New("You are midway through another rebase operation. Please abort to start again")
}
self.PatchManager.Reset()
return self.rebase.ContinueRebase()
}

View File

@@ -0,0 +1,403 @@
package git_commands
import (
"fmt"
"io/ioutil"
"path/filepath"
"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"
)
type RebaseCommands struct {
*GitCommon
commit *CommitCommands
workingTree *WorkingTreeCommands
onSuccessfulContinue func() error
}
func NewRebaseCommands(
gitCommon *GitCommon,
commitCommands *CommitCommands,
workingTreeCommands *WorkingTreeCommands,
) *RebaseCommands {
return &RebaseCommands{
GitCommon: gitCommon,
commit: commitCommands,
workingTree: workingTreeCommands,
}
}
func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int, message string) error {
if index == 0 {
// we've selected the top commit so no rebase is required
return self.commit.RewordLastCommit(message)
}
err := self.BeginInteractiveRebaseForCommit(commits, index)
if err != nil {
return err
}
// now the selected commit should be our head so we'll amend it with the new message
err = self.commit.RewordLastCommit(message)
if err != nil {
return err
}
return self.ContinueRebase()
}
func (self *RebaseCommands) RewordCommitInEditor(commits []*models.Commit, index int) (oscommands.ICmdObj, error) {
todo, sha, err := self.BuildSingleActionTodo(commits, index, "reword")
if err != nil {
return nil, err
}
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 {
// assuming they aren't picking the bottom commit
return errors.New(self.Tr.NoRoom)
}
orderedCommits := append(commits[0:index], commits[index+1], commits[index])
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.BuildSingleActionTodo(commits, index, action)
if err != nil {
return err
}
return self.PrepareInteractiveRebaseCommand(sha, todo, true).Run()
}
// 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, todoLines []TodoLine, overrideEditor bool) oscommands.ICmdObj {
todo := self.buildTodo(todoLines)
ex := oscommands.GetLazygitPath()
debug := "FALSE"
if self.Debug {
debug = "TRUE"
}
cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty %s", baseSha)
self.Log.WithField("command", cmdStr).Debug("RunCommand")
cmdObj := self.cmd.New(cmdStr)
gitSequenceEditor := ex
if todo == "" {
gitSequenceEditor = "true"
} else {
self.os.LogCommand(fmt.Sprintf("Creating TODO file for interactive rebase: \n\n%s", todo), false)
}
cmdObj.AddEnvVars(
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
"GIT_SEQUENCE_EDITOR="+gitSequenceEditor,
)
if overrideEditor {
cmdObj.AddEnvVars("GIT_EDITOR=" + ex)
}
return cmdObj
}
// 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 nil, "", errors.New(self.Tr.CannotRebaseOntoFirstCommit)
}
if action == "squash" || action == "fixup" {
baseIndex++
if len(commits) <= baseIndex {
return nil, "", errors.New(self.Tr.CannotSquashOntoSecondCommit)
}
}
todoLines := self.BuildTodoLines(commits[0:baseIndex], func(commit *models.Commit, i int) string {
if i == actionIndex {
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.
return "drop"
} else {
return "pick"
}
})
return todoLines, commits[baseIndex].Sha, nil
}
// AmendTo amends the given commit with whatever files are staged
func (self *RebaseCommands) AmendTo(sha string) error {
if err := self.commit.CreateFixupCommit(sha); err != nil {
return err
}
return self.SquashAllAboveFixupCommits(sha)
}
// EditRebaseTodo sets the action at a given index in the git-rebase-todo file
func (self *RebaseCommands) EditRebaseTodo(index int, action string) error {
fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo")
bytes, err := ioutil.ReadFile(fileName)
if err != nil {
return err
}
content := strings.Split(string(bytes), "\n")
commitCount := self.getTodoCommitCount(content)
// we have the most recent commit at the bottom whereas the todo file has
// it at the bottom, so we need to subtract our index from the commit count
contentIndex := commitCount - 1 - index
splitLine := strings.Split(content[contentIndex], " ")
content[contentIndex] = action + " " + strings.Join(splitLine[1:], " ")
result := strings.Join(content, "\n")
return ioutil.WriteFile(fileName, []byte(result), 0o644)
}
func (self *RebaseCommands) getTodoCommitCount(content []string) int {
// count lines that are not blank and are not comments
commitCount := 0
for _, line := range content {
if line != "" && !strings.HasPrefix(line, "#") {
commitCount++
}
}
return commitCount
}
// MoveTodoDown moves a rebase todo item down by one position
func (self *RebaseCommands) MoveTodoDown(index int) error {
fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo")
bytes, err := ioutil.ReadFile(fileName)
if err != nil {
return err
}
content := strings.Split(string(bytes), "\n")
commitCount := self.getTodoCommitCount(content)
contentIndex := commitCount - 1 - index
rearrangedContent := append(content[0:contentIndex-1], content[contentIndex], content[contentIndex-1])
rearrangedContent = append(rearrangedContent, content[contentIndex+1:]...)
result := strings.Join(rearrangedContent, "\n")
return ioutil.WriteFile(fileName, []byte(result), 0o644)
}
// SquashAllAboveFixupCommits squashes all fixup! commits above the given one
func (self *RebaseCommands) SquashAllAboveFixupCommits(sha string) error {
return self.runSkipEditorCommand(
self.cmd.New(
fmt.Sprintf(
"git rebase --interactive --autostash --autosquash %s^",
sha,
),
),
)
}
// BeginInteractiveRebaseForCommit starts an interactive rebase to edit the current
// commit and pick all others. After this you'll want to call `self.ContinueRebase()
func (self *RebaseCommands) BeginInteractiveRebaseForCommit(commits []*models.Commit, commitIndex int) error {
if len(commits)-1 < commitIndex {
return errors.New("index outside of range of commits")
}
// we can make this GPG thing possible it just means we need to do this in two parts:
// one where we handle the possibility of a credential request, and the other
// where we continue the rebase
if self.config.UsingGpg() {
return errors.New(self.Tr.DisabledForGPG)
}
todo, sha, err := self.BuildSingleActionTodo(commits, commitIndex, "edit")
if err != nil {
return err
}
return self.PrepareInteractiveRebaseCommand(sha, todo, true).Run()
}
// RebaseBranch interactive rebases onto a branch
func (self *RebaseCommands) RebaseBranch(branchName string) error {
return self.PrepareInteractiveRebaseCommand(branchName, nil, false).Run()
}
func (self *RebaseCommands) GenericMergeOrRebaseActionCmdObj(commandType string, command string) oscommands.ICmdObj {
return self.cmd.New("git " + commandType + " --" + command)
}
func (self *RebaseCommands) ContinueRebase() error {
return self.GenericMergeOrRebaseAction("rebase", "continue")
}
func (self *RebaseCommands) AbortRebase() error {
return self.GenericMergeOrRebaseAction("rebase", "abort")
}
// GenericMerge takes a commandType of "merge" or "rebase" and a command of "abort", "skip" or "continue"
// By default we skip the editor in the case where a commit will be made
func (self *RebaseCommands) GenericMergeOrRebaseAction(commandType string, command string) error {
err := self.runSkipEditorCommand(self.GenericMergeOrRebaseActionCmdObj(commandType, command))
if err != nil {
if !strings.Contains(err.Error(), "no rebase in progress") {
return err
}
self.Log.Warn(err)
}
// sometimes we need to do a sequence of things in a rebase but the user needs to
// fix merge conflicts along the way. When this happens we queue up the next step
// so that after the next successful rebase continue we can continue from where we left off
if commandType == "rebase" && command == "continue" && self.onSuccessfulContinue != nil {
f := self.onSuccessfulContinue
self.onSuccessfulContinue = nil
return f()
}
if command == "abort" {
self.onSuccessfulContinue = nil
}
return nil
}
func (self *RebaseCommands) runSkipEditorCommand(cmdObj oscommands.ICmdObj) error {
lazyGitPath := oscommands.GetLazygitPath()
return cmdObj.
AddEnvVars(
daemon.DaemonKindEnvKey+"="+string(daemon.ExitImmediately),
"GIT_EDITOR="+lazyGitPath,
"EDITOR="+lazyGitPath,
"VISUAL="+lazyGitPath,
).
Run()
}
// DiscardOldFileChanges discards changes to a file from an old commit
func (self *RebaseCommands) DiscardOldFileChanges(commits []*models.Commit, commitIndex int, fileName string) error {
if err := self.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil {
return err
}
// check if file exists in previous commit (this command returns an error if the file doesn't exist)
if err := self.cmd.New("git cat-file -e HEAD^:" + self.cmd.Quote(fileName)).Run(); err != nil {
if err := self.os.Remove(fileName); err != nil {
return err
}
if err := self.workingTree.StageFile(fileName); err != nil {
return err
}
} else if err := self.workingTree.CheckoutFile("HEAD^", fileName); err != nil {
return err
}
// amend the commit
err := self.commit.AmendHead()
if err != nil {
return err
}
// continue
return self.ContinueRebase()
}
// CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD
func (self *RebaseCommands) CherryPickCommits(commits []*models.Commit) error {
todoLines := self.BuildTodoLinesSingleAction(commits, "pick")
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"
}

View File

@@ -0,0 +1,152 @@
package git_commands
import (
"regexp"
"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/samber/lo"
"github.com/stretchr/testify/assert"
)
func TestRebaseRebaseBranch(t *testing.T) {
type scenario struct {
testName string
arg string
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
testName: "successful rebase",
arg: "master",
runner: oscommands.NewFakeRunner(t).
Expect(`git rebase --interactive --autostash --keep-empty master`, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
testName: "unsuccessful rebase",
arg: "master",
runner: oscommands.NewFakeRunner(t).
Expect(`git rebase --interactive --autostash --keep-empty master`, "", errors.New("error")),
test: func(err error) {
assert.Error(t, err)
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildRebaseCommands(commonDeps{runner: s.runner})
s.test(instance.RebaseBranch(s.arg))
})
}
}
// TestRebaseSkipEditorCommand confirms that SkipEditorCommand injects
// environment variables that suppress an interactive editor
func TestRebaseSkipEditorCommand(t *testing.T) {
commandStr := "git blah"
runner := oscommands.NewFakeRunner(t).ExpectFunc(func(cmdObj oscommands.ICmdObj) (string, error) {
assert.Equal(t, commandStr, cmdObj.ToString())
envVars := cmdObj.GetEnvVars()
for _, regexStr := range []string{
`^VISUAL=.*$`,
`^EDITOR=.*$`,
`^GIT_EDITOR=.*$`,
"^" + daemon.DaemonKindEnvKey + "=" + string(daemon.ExitImmediately) + "$",
} {
regexStr := regexStr
foundMatch := lo.ContainsBy(envVars, func(envVar string) bool {
return regexp.MustCompile(regexStr).MatchString(envVar)
})
if !foundMatch {
t.Errorf("expected environment variable %s to be set", regexStr)
}
}
return "", nil
})
instance := buildRebaseCommands(commonDeps{runner: runner})
err := instance.runSkipEditorCommand(instance.cmd.New(commandStr))
assert.NoError(t, err)
runner.CheckForMissingCalls()
}
func TestRebaseDiscardOldFileChanges(t *testing.T) {
type scenario struct {
testName string
gitConfigMockResponses map[string]string
commits []*models.Commit
commitIndex int
fileName string
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
testName: "returns error when index outside of range of commits",
gitConfigMockResponses: nil,
commits: []*models.Commit{},
commitIndex: 0,
fileName: "test999.txt",
runner: oscommands.NewFakeRunner(t),
test: func(err error) {
assert.Error(t, err)
},
},
{
testName: "returns error when using gpg",
gitConfigMockResponses: map[string]string{"commit.gpgsign": "true"},
commits: []*models.Commit{{Name: "commit", Sha: "123456"}},
commitIndex: 0,
fileName: "test999.txt",
runner: oscommands.NewFakeRunner(t),
test: func(err error) {
assert.Error(t, err)
},
},
{
testName: "checks out file if it already existed",
gitConfigMockResponses: nil,
commits: []*models.Commit{
{Name: "commit", Sha: "123456"},
{Name: "commit2", Sha: "abcdef"},
},
commitIndex: 0,
fileName: "test999.txt",
runner: oscommands.NewFakeRunner(t).
Expect(`git rebase --interactive --autostash --keep-empty abcdef`, "", nil).
Expect(`git cat-file -e HEAD^:"test999.txt"`, "", nil).
Expect(`git checkout HEAD^ -- "test999.txt"`, "", nil).
Expect(`git commit --amend --no-edit --allow-empty`, "", nil).
Expect(`git rebase --continue`, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
// test for when the file was created within the commit requires a refactor to support proper mocks
// currently we'd need to mock out the os.Remove function and that's gonna introduce tech debt
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildRebaseCommands(commonDeps{
runner: s.runner,
gitConfig: git_config.NewFakeGitConfig(s.gitConfigMockResponses),
})
s.test(instance.DiscardOldFileChanges(s.commits, s.commitIndex, s.fileName))
s.runner.CheckForMissingCalls()
})
}
}

View File

@@ -0,0 +1,58 @@
package git_commands
import (
"fmt"
)
type RemoteCommands struct {
*GitCommon
}
func NewRemoteCommands(gitCommon *GitCommon) *RemoteCommands {
return &RemoteCommands{
GitCommon: gitCommon,
}
}
func (self *RemoteCommands) AddRemote(name string, url string) error {
return self.cmd.
New(fmt.Sprintf("git remote add %s %s", self.cmd.Quote(name), self.cmd.Quote(url))).
Run()
}
func (self *RemoteCommands) RemoveRemote(name string) error {
return self.cmd.
New(fmt.Sprintf("git remote remove %s", self.cmd.Quote(name))).
Run()
}
func (self *RemoteCommands) RenameRemote(oldRemoteName string, newRemoteName string) error {
return self.cmd.
New(fmt.Sprintf("git remote rename %s %s", self.cmd.Quote(oldRemoteName), self.cmd.Quote(newRemoteName))).
Run()
}
func (self *RemoteCommands) UpdateRemoteUrl(remoteName string, updatedUrl string) error {
return self.cmd.
New(fmt.Sprintf("git remote set-url %s %s", self.cmd.Quote(remoteName), self.cmd.Quote(updatedUrl))).
Run()
}
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().WithMutex(self.syncMutex).Run()
}
// CheckRemoteBranchExists Returns remote branch
func (self *RemoteCommands) CheckRemoteBranchExists(branchName string) bool {
_, err := self.cmd.
New(
fmt.Sprintf("git show-ref --verify -- refs/remotes/origin/%s",
self.cmd.Quote(branchName),
),
).
DontLog().
RunWithOutput()
return err == nil
}

View File

@@ -0,0 +1,111 @@
package git_commands
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
)
type StashCommands struct {
*GitCommon
fileLoader *loaders.FileLoader
workingTree *WorkingTreeCommands
}
func NewStashCommands(
gitCommon *GitCommon,
fileLoader *loaders.FileLoader,
workingTree *WorkingTreeCommands,
) *StashCommands {
return &StashCommands{
GitCommon: gitCommon,
fileLoader: fileLoader,
workingTree: workingTree,
}
}
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()
}
func (self *StashCommands) Pop(index int) error {
return self.cmd.New(fmt.Sprintf("git stash pop stash@{%d}", index)).Run()
}
func (self *StashCommands) Apply(index int) error {
return self.cmd.New(fmt.Sprintf("git stash apply stash@{%d}", index)).Run()
}
// Save save stash
func (self *StashCommands) Save(message string) error {
return self.cmd.New("git stash save " + self.cmd.Quote(message)).Run()
}
func (self *StashCommands) ShowStashEntryCmdObj(index int) oscommands.ICmdObj {
cmdStr := fmt.Sprintf("git stash show -p --stat --color=%s --unified=%d stash@{%d}", self.UserConfig.Git.Paging.ColorArg, self.UserConfig.Git.DiffContextSize, index)
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 {
// wrap in 'writing', which uses a mutex
if err := self.cmd.New("git stash --keep-index").Run(); err != nil {
return err
}
if err := self.Save(message); err != nil {
return err
}
if err := self.cmd.New("git stash apply stash@{1}").Run(); err != nil {
return err
}
if err := self.os.PipeCommands("git stash show -p", "git apply -R"); err != nil {
return err
}
if err := self.cmd.New("git stash drop stash@{1}").Run(); err != nil {
return err
}
// if you had staged an untracked file, that will now appear as 'AD' in git status
// meaning it's deleted in your working tree but added in your index. Given that it's
// now safely stashed, we need to remove it.
files := self.fileLoader.
GetStatusFiles(loaders.GetStatusFileOptions{})
for _, file := range files {
if file.ShortStatus == "AD" {
if err := self.workingTree.UnStageFile(file.Names(), false); err != nil {
return err
}
}
}
return nil
}

View File

@@ -0,0 +1,81 @@
package git_commands
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/stretchr/testify/assert"
)
func TestStashDrop(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"stash", "drop", "stash@{1}"}, "", nil)
instance := buildStashCommands(commonDeps{runner: runner})
assert.NoError(t, instance.Drop(1))
runner.CheckForMissingCalls()
}
func TestStashApply(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"stash", "apply", "stash@{1}"}, "", nil)
instance := buildStashCommands(commonDeps{runner: runner})
assert.NoError(t, instance.Apply(1))
runner.CheckForMissingCalls()
}
func TestStashPop(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"stash", "pop", "stash@{1}"}, "", nil)
instance := buildStashCommands(commonDeps{runner: runner})
assert.NoError(t, instance.Pop(1))
runner.CheckForMissingCalls()
}
func TestStashSave(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"stash", "save", "A stash message"}, "", nil)
instance := buildStashCommands(commonDeps{runner: runner})
assert.NoError(t, instance.Save("A stash message"))
runner.CheckForMissingCalls()
}
func TestStashStashEntryCmdObj(t *testing.T) {
type scenario struct {
testName string
index int
contextSize int
expected string
}
scenarios := []scenario{
{
testName: "Default case",
index: 5,
contextSize: 3,
expected: "git stash show -p --stat --color=always --unified=3 stash@{5}",
},
{
testName: "Show diff with custom context size",
index: 5,
contextSize: 77,
expected: "git stash show -p --stat --color=always --unified=77 stash@{5}",
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
userConfig := config.GetDefaultConfig()
userConfig.Git.DiffContextSize = s.contextSize
instance := buildStashCommands(commonDeps{userConfig: userConfig})
cmdStr := instance.ShowStashEntryCmdObj(s.index).ToString()
assert.Equal(t, s.expected, cmdStr)
})
}
}

View File

@@ -0,0 +1,71 @@
package git_commands
import (
"path/filepath"
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
)
type StatusCommands struct {
*GitCommon
}
func NewStatusCommands(
gitCommon *GitCommon,
) *StatusCommands {
return &StatusCommands{
GitCommon: gitCommon,
}
}
// RebaseMode returns "" for non-rebase mode, "normal" for normal rebase
// and "interactive" for interactive rebase
func (self *StatusCommands) RebaseMode() (enums.RebaseMode, error) {
exists, err := self.os.FileExists(filepath.Join(self.dotGitDir, "rebase-apply"))
if err != nil {
return enums.REBASE_MODE_NONE, err
}
if exists {
return enums.REBASE_MODE_NORMAL, nil
}
exists, err = self.os.FileExists(filepath.Join(self.dotGitDir, "rebase-merge"))
if exists {
return enums.REBASE_MODE_INTERACTIVE, err
} else {
return enums.REBASE_MODE_NONE, err
}
}
func (self *StatusCommands) WorkingTreeState() enums.RebaseMode {
rebaseMode, _ := self.RebaseMode()
if rebaseMode != enums.REBASE_MODE_NONE {
return enums.REBASE_MODE_REBASING
}
merging, _ := self.IsInMergeState()
if merging {
return enums.REBASE_MODE_MERGING
}
return enums.REBASE_MODE_NONE
}
func (self *StatusCommands) IsBareRepo() (bool, error) {
return IsBareRepo(self.os)
}
func IsBareRepo(osCommand *oscommands.OSCommand) (bool, error) {
res, err := osCommand.Cmd.New("git rev-parse --is-bare-repository").DontLog().RunWithOutput()
if err != nil {
return false, err
}
// The command returns output with a newline, so we need to strip
return strconv.ParseBool(strings.TrimSpace(res))
}
// IsInMergeState states whether we are still mid-merge
func (self *StatusCommands) IsInMergeState() (bool, error) {
return self.os.FileExists(filepath.Join(self.dotGitDir, "MERGE_HEAD"))
}

View File

@@ -0,0 +1,181 @@
package git_commands
import (
"bufio"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
)
// .gitmodules looks like this:
// [submodule "mysubmodule"]
// path = blah/mysubmodule
// url = git@github.com:subbo.git
type SubmoduleCommands struct {
*GitCommon
}
func NewSubmoduleCommands(gitCommon *GitCommon) *SubmoduleCommands {
return &SubmoduleCommands{
GitCommon: gitCommon,
}
}
func (self *SubmoduleCommands) GetConfigs() ([]*models.SubmoduleConfig, error) {
file, err := os.Open(".gitmodules")
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines)
firstMatch := func(str string, regex string) (string, bool) {
re := regexp.MustCompile(regex)
matches := re.FindStringSubmatch(str)
if len(matches) > 0 {
return matches[1], true
} else {
return "", false
}
}
configs := []*models.SubmoduleConfig{}
for scanner.Scan() {
line := scanner.Text()
if name, ok := firstMatch(line, `\[submodule "(.*)"\]`); ok {
configs = append(configs, &models.SubmoduleConfig{Name: name})
continue
}
if len(configs) > 0 {
lastConfig := configs[len(configs)-1]
if path, ok := firstMatch(line, `\s*path\s*=\s*(.*)\s*`); ok {
lastConfig.Path = path
} else if url, ok := firstMatch(line, `\s*url\s*=\s*(.*)\s*`); ok {
lastConfig.Url = url
}
}
}
return configs, nil
}
func (self *SubmoduleCommands) Stash(submodule *models.SubmoduleConfig) error {
// if the path does not exist then it hasn't yet been initialized so we'll swallow the error
// because the intention here is to have no dirty worktree state
if _, err := os.Stat(submodule.Path); os.IsNotExist(err) {
self.Log.Infof("submodule path %s does not exist, returning", submodule.Path)
return nil
}
return self.cmd.New("git -C " + self.cmd.Quote(submodule.Path) + " stash --include-untracked").Run()
}
func (self *SubmoduleCommands) Reset(submodule *models.SubmoduleConfig) error {
return self.cmd.New("git submodule update --init --force -- " + self.cmd.Quote(submodule.Path)).Run()
}
func (self *SubmoduleCommands) UpdateAll() error {
// not doing an --init here because the user probably doesn't want that
return self.cmd.New("git submodule update --force").Run()
}
func (self *SubmoduleCommands) Delete(submodule *models.SubmoduleConfig) error {
// based on https://gist.github.com/myusuf3/7f645819ded92bda6677
if err := self.cmd.New("git submodule deinit --force -- " + self.cmd.Quote(submodule.Path)).Run(); err != nil {
if strings.Contains(err.Error(), "did not match any file(s) known to git") {
if err := self.cmd.New("git config --file .gitmodules --remove-section submodule." + self.cmd.Quote(submodule.Name)).Run(); err != nil {
return err
}
if err := self.cmd.New("git config --remove-section submodule." + self.cmd.Quote(submodule.Name)).Run(); err != nil {
return err
}
// if there's an error here about it not existing then we'll just continue to do `git rm`
} else {
return err
}
}
if err := self.cmd.New("git rm --force -r " + submodule.Path).Run(); err != nil {
// if the directory isn't there then that's fine
self.Log.Error(err)
}
return os.RemoveAll(filepath.Join(self.dotGitDir, "modules", submodule.Path))
}
func (self *SubmoduleCommands) Add(name string, path string, url string) error {
return self.cmd.
New(
fmt.Sprintf(
"git submodule add --force --name %s -- %s %s ",
self.cmd.Quote(name),
self.cmd.Quote(url),
self.cmd.Quote(path),
)).
Run()
}
func (self *SubmoduleCommands) UpdateUrl(name string, path string, newUrl string) error {
// the set-url command is only for later git versions so we're doing it manually here
if err := self.cmd.New("git config --file .gitmodules submodule." + self.cmd.Quote(name) + ".url " + self.cmd.Quote(newUrl)).Run(); err != nil {
return err
}
if err := self.cmd.New("git submodule sync -- " + self.cmd.Quote(path)).Run(); err != nil {
return err
}
return nil
}
func (self *SubmoduleCommands) Init(path string) error {
return self.cmd.New("git submodule init -- " + self.cmd.Quote(path)).Run()
}
func (self *SubmoduleCommands) Update(path string) error {
return self.cmd.New("git submodule update --init -- " + self.cmd.Quote(path)).Run()
}
func (self *SubmoduleCommands) BulkInitCmdObj() oscommands.ICmdObj {
return self.cmd.New("git submodule init")
}
func (self *SubmoduleCommands) BulkUpdateCmdObj() oscommands.ICmdObj {
return self.cmd.New("git submodule update")
}
func (self *SubmoduleCommands) ForceBulkUpdateCmdObj() oscommands.ICmdObj {
return self.cmd.New("git submodule update --force")
}
func (self *SubmoduleCommands) BulkDeinitCmdObj() oscommands.ICmdObj {
return self.cmd.New("git submodule deinit --all --force")
}
func (self *SubmoduleCommands) ResetSubmodules(submodules []*models.SubmoduleConfig) error {
for _, submodule := range submodules {
if err := self.Stash(submodule); err != nil {
return err
}
}
return self.UpdateAll()
}

View File

@@ -0,0 +1,122 @@
package git_commands
import (
"fmt"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
)
type SyncCommands struct {
*GitCommon
}
func NewSyncCommands(gitCommon *GitCommon) *SyncCommands {
return &SyncCommands{
GitCommon: gitCommon,
}
}
// Push pushes to a branch
type PushOpts struct {
Force bool
UpstreamRemote string
UpstreamBranch string
SetUpstream bool
}
func (self *SyncCommands) PushCmdObj(opts PushOpts) (oscommands.ICmdObj, error) {
cmdStr := "git push"
if opts.Force {
cmdStr += " --force-with-lease"
}
if opts.SetUpstream {
cmdStr += " --set-upstream"
}
if opts.UpstreamRemote != "" {
cmdStr += " " + self.cmd.Quote(opts.UpstreamRemote)
}
if opts.UpstreamBranch != "" {
if opts.UpstreamRemote == "" {
return nil, errors.New(self.Tr.MustSpecifyOriginError)
}
cmdStr += " " + self.cmd.Quote(opts.UpstreamBranch)
}
cmdObj := self.cmd.New(cmdStr).PromptOnCredentialRequest().WithMutex(self.syncMutex)
return cmdObj, nil
}
func (self *SyncCommands) Push(opts PushOpts) error {
cmdObj, err := self.PushCmdObj(opts)
if err != nil {
return err
}
return cmdObj.Run()
}
type FetchOptions struct {
Background bool
RemoteName string
BranchName string
}
// Fetch fetch git repo
func (self *SyncCommands) Fetch(opts FetchOptions) error {
cmdStr := "git fetch"
if opts.RemoteName != "" {
cmdStr = fmt.Sprintf("%s %s", cmdStr, self.cmd.Quote(opts.RemoteName))
}
if opts.BranchName != "" {
cmdStr = fmt.Sprintf("%s %s", cmdStr, self.cmd.Quote(opts.BranchName))
}
cmdObj := self.cmd.New(cmdStr)
if opts.Background {
cmdObj.DontLog().FailOnCredentialRequest()
} else {
cmdObj.PromptOnCredentialRequest()
}
return cmdObj.WithMutex(self.syncMutex).Run()
}
type PullOptions struct {
RemoteName string
BranchName string
FastForwardOnly bool
}
func (self *SyncCommands) Pull(opts PullOptions) error {
cmdStr := "git pull --no-edit"
if opts.FastForwardOnly {
cmdStr += " --ff-only"
}
if opts.RemoteName != "" {
cmdStr = fmt.Sprintf("%s %s", cmdStr, self.cmd.Quote(opts.RemoteName))
}
if opts.BranchName != "" {
cmdStr = fmt.Sprintf("%s %s", cmdStr, self.cmd.Quote(opts.BranchName))
}
// 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().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().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().WithMutex(self.syncMutex).Run()
}

View File

@@ -0,0 +1,94 @@
package git_commands
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/stretchr/testify/assert"
)
func TestSyncPush(t *testing.T) {
type scenario struct {
testName string
opts PushOpts
test func(oscommands.ICmdObj, error)
}
scenarios := []scenario{
{
testName: "Push with force disabled",
opts: PushOpts{Force: false},
test: func(cmdObj oscommands.ICmdObj, err error) {
assert.Equal(t, cmdObj.ToString(), "git push")
assert.NoError(t, err)
},
},
{
testName: "Push with force enabled",
opts: PushOpts{Force: true},
test: func(cmdObj oscommands.ICmdObj, err error) {
assert.Equal(t, cmdObj.ToString(), "git push --force-with-lease")
assert.NoError(t, err)
},
},
{
testName: "Push with force disabled, upstream supplied",
opts: PushOpts{
Force: false,
UpstreamRemote: "origin",
UpstreamBranch: "master",
},
test: func(cmdObj oscommands.ICmdObj, err error) {
assert.Equal(t, cmdObj.ToString(), `git push "origin" "master"`)
assert.NoError(t, err)
},
},
{
testName: "Push with force disabled, setting upstream",
opts: PushOpts{
Force: false,
UpstreamRemote: "origin",
UpstreamBranch: "master",
SetUpstream: true,
},
test: func(cmdObj oscommands.ICmdObj, err error) {
assert.Equal(t, cmdObj.ToString(), `git push --set-upstream "origin" "master"`)
assert.NoError(t, err)
},
},
{
testName: "Push with force enabled, setting upstream",
opts: PushOpts{
Force: true,
UpstreamRemote: "origin",
UpstreamBranch: "master",
SetUpstream: true,
},
test: func(cmdObj oscommands.ICmdObj, err error) {
assert.Equal(t, cmdObj.ToString(), `git push --force-with-lease --set-upstream "origin" "master"`)
assert.NoError(t, err)
},
},
{
testName: "Push with remote branch but no origin",
opts: PushOpts{
Force: true,
UpstreamRemote: "",
UpstreamBranch: "master",
SetUpstream: true,
},
test: func(cmdObj oscommands.ICmdObj, err error) {
assert.Error(t, err)
assert.EqualValues(t, "Must specify a remote if specifying a branch", err.Error())
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildSyncCommands(commonDeps{})
s.test(instance.PushCmdObj(s.opts))
})
}
}

View File

@@ -0,0 +1,31 @@
package git_commands
import (
"fmt"
)
type TagCommands struct {
*GitCommon
}
func NewTagCommands(gitCommon *GitCommon) *TagCommands {
return &TagCommands{
GitCommon: gitCommon,
}
}
func (self *TagCommands) CreateLightweight(tagName string, commitSha string) error {
return self.cmd.New(fmt.Sprintf("git tag -- %s %s", self.cmd.Quote(tagName), commitSha)).Run()
}
func (self *TagCommands) CreateAnnotated(tagName, commitSha, msg string) error {
return self.cmd.New(fmt.Sprintf("git tag %s %s -m %s", tagName, commitSha, self.cmd.Quote(msg))).Run()
}
func (self *TagCommands) Delete(tagName string) error {
return self.cmd.New(fmt.Sprintf("git tag -d %s", self.cmd.Quote(tagName))).Run()
}
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().WithMutex(self.syncMutex).Run()
}

View File

@@ -0,0 +1,377 @@
package git_commands
import (
"fmt"
"os"
"path/filepath"
"strings"
"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"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type WorkingTreeCommands struct {
*GitCommon
submodule *SubmoduleCommands
fileLoader *loaders.FileLoader
}
func NewWorkingTreeCommands(
gitCommon *GitCommon,
submodule *SubmoduleCommands,
fileLoader *loaders.FileLoader,
) *WorkingTreeCommands {
return &WorkingTreeCommands{
GitCommon: gitCommon,
submodule: submodule,
fileLoader: fileLoader,
}
}
func (self *WorkingTreeCommands) OpenMergeToolCmdObj() oscommands.ICmdObj {
return self.cmd.New("git mergetool")
}
func (self *WorkingTreeCommands) OpenMergeTool() error {
return self.OpenMergeToolCmdObj().Run()
}
// StageFile stages a file
func (self *WorkingTreeCommands) StageFile(path string) error {
return self.StageFiles([]string{path})
}
func (self *WorkingTreeCommands) StageFiles(paths []string) error {
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()
}
// StageAll stages all files
func (self *WorkingTreeCommands) StageAll() error {
return self.cmd.New("git add -A").Run()
}
// UnstageAll unstages all files
func (self *WorkingTreeCommands) UnstageAll() error {
return self.cmd.New("git reset").Run()
}
// UnStageFile unstages a file
// we accept an array of filenames for the cases where a file has been renamed i.e.
// we accept the current name and the previous name
func (self *WorkingTreeCommands) UnStageFile(fileNames []string, reset bool) error {
command := "git rm --cached --force -- %s"
if reset {
command = "git reset HEAD -- %s"
}
for _, name := range fileNames {
err := self.cmd.New(fmt.Sprintf(command, self.cmd.Quote(name))).Run()
if err != nil {
return err
}
}
return nil
}
func (self *WorkingTreeCommands) BeforeAndAfterFileForRename(file *models.File) (*models.File, *models.File, error) {
if !file.IsRename() {
return nil, nil, errors.New("Expected renamed file")
}
// we've got a file that represents a rename from one file to another. Here we will refetch
// all files, passing the --no-renames flag and then recursively call the function
// again for the before file and after file.
filesWithoutRenames := self.fileLoader.GetStatusFiles(loaders.GetStatusFileOptions{NoRenames: true})
var beforeFile *models.File
var afterFile *models.File
for _, f := range filesWithoutRenames {
if f.Name == file.PreviousName {
beforeFile = f
}
if f.Name == file.Name {
afterFile = f
}
}
if beforeFile == nil || afterFile == nil {
return nil, nil, errors.New("Could not find deleted file or new file for file rename")
}
if beforeFile.IsRename() || afterFile.IsRename() {
// probably won't happen but we want to ensure we don't get an infinite loop
return nil, nil, errors.New("Nested rename found")
}
return beforeFile, afterFile, nil
}
// DiscardAllFileChanges directly
func (self *WorkingTreeCommands) DiscardAllFileChanges(file *models.File) error {
if file.IsRename() {
beforeFile, afterFile, err := self.BeforeAndAfterFileForRename(file)
if err != nil {
return err
}
if err := self.DiscardAllFileChanges(beforeFile); err != nil {
return err
}
if err := self.DiscardAllFileChanges(afterFile); err != nil {
return err
}
return nil
}
quotedFileName := self.cmd.Quote(file.Name)
if file.ShortStatus == "AA" {
if err := self.cmd.New("git checkout --ours -- " + quotedFileName).Run(); err != nil {
return err
}
if err := self.cmd.New("git add -- " + quotedFileName).Run(); err != nil {
return err
}
return nil
}
if file.ShortStatus == "DU" {
return self.cmd.New("git rm -- " + quotedFileName).Run()
}
// if the file isn't tracked, we assume you want to delete it
if file.HasStagedChanges || file.HasMergeConflicts {
if err := self.cmd.New("git reset -- " + quotedFileName).Run(); err != nil {
return err
}
}
if file.ShortStatus == "DD" || file.ShortStatus == "AU" {
return nil
}
if file.Added {
return self.os.RemoveFile(file.Name)
}
return self.DiscardUnstagedFileChanges(file)
}
type IFileNode interface {
ForEachFile(cb func(*models.File) error) error
GetFilePathsMatching(test func(*models.File) bool) []string
GetPath() string
}
func (self *WorkingTreeCommands) DiscardAllDirChanges(node IFileNode) error {
// this could be more efficient but we would need to handle all the edge cases
return node.ForEachFile(self.DiscardAllFileChanges)
}
func (self *WorkingTreeCommands) DiscardUnstagedDirChanges(node IFileNode) error {
if err := self.RemoveUntrackedDirFiles(node); err != nil {
return err
}
quotedPath := self.cmd.Quote(node.GetPath())
if err := self.cmd.New("git checkout -- " + quotedPath).Run(); err != nil {
return err
}
return nil
}
func (self *WorkingTreeCommands) RemoveUntrackedDirFiles(node IFileNode) error {
untrackedFilePaths := node.GetFilePathsMatching(
func(file *models.File) bool { return !file.GetIsTracked() },
)
for _, path := range untrackedFilePaths {
err := os.Remove(path)
if err != nil {
return err
}
}
return nil
}
// DiscardUnstagedFileChanges directly
func (self *WorkingTreeCommands) DiscardUnstagedFileChanges(file *models.File) error {
quotedFileName := self.cmd.Quote(file.Name)
return self.cmd.New("git checkout -- " + quotedFileName).Run()
}
// Ignore adds a file to the gitignore for the repo
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
s, _ := self.WorktreeFileDiffCmdObj(file, plain, cached, ignoreWhitespace).RunWithOutput()
return s
}
func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain bool, cached bool, ignoreWhitespace bool) oscommands.ICmdObj {
cachedArg := ""
trackedArg := "--"
colorArg := self.UserConfig.Git.Paging.ColorArg
quotedPath := self.cmd.Quote(node.GetPath())
quotedPrevPath := ""
ignoreWhitespaceArg := ""
contextSize := self.UserConfig.Git.DiffContextSize
if cached {
cachedArg = " --cached"
}
if !node.GetIsTracked() && !node.GetHasStagedChanges() && !cached {
trackedArg = "--no-index -- /dev/null"
}
if plain {
colorArg = "never"
}
if ignoreWhitespace {
ignoreWhitespaceArg = " --ignore-all-space"
}
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%s", contextSize, colorArg, ignoreWhitespaceArg, cachedArg, trackedArg, quotedPath, quotedPrevPath)
return self.cmd.New(cmdStr).DontLog()
}
func (self *WorkingTreeCommands) ApplyPatch(patch string, flags ...string) error {
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
}
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) {
return self.ShowFileDiffCmdObj(from, to, reverse, fileName, plain).RunWithOutput()
}
func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reverse bool, fileName string, plain bool) oscommands.ICmdObj {
colorArg := self.UserConfig.Git.Paging.ColorArg
contextSize := self.UserConfig.Git.DiffContextSize
if plain {
colorArg = "never"
}
reverseFlag := ""
if reverse {
reverseFlag = " -R"
}
return self.cmd.
New(
fmt.Sprintf(
"git diff --submodule --no-ext-diff --unified=%d --no-renames --color=%s%s%s%s -- %s",
contextSize, colorArg, pad(from), pad(to), reverseFlag, self.cmd.Quote(fileName)),
).
DontLog()
}
// CheckoutFile checks out the file for the given commit
func (self *WorkingTreeCommands) CheckoutFile(commitSha, fileName string) error {
return self.cmd.New(fmt.Sprintf("git checkout %s -- %s", commitSha, self.cmd.Quote(fileName))).Run()
}
// DiscardAnyUnstagedFileChanges discards any unstages file changes via `git checkout -- .`
func (self *WorkingTreeCommands) DiscardAnyUnstagedFileChanges() error {
return self.cmd.New("git checkout -- .").Run()
}
// RemoveTrackedFiles will delete the given file(s) even if they are currently tracked
func (self *WorkingTreeCommands) RemoveTrackedFiles(name string) error {
return self.cmd.New("git rm -r --cached -- " + self.cmd.Quote(name)).Run()
}
// RemoveUntrackedFiles runs `git clean -fd`
func (self *WorkingTreeCommands) RemoveUntrackedFiles() error {
return self.cmd.New("git clean -fd").Run()
}
// ResetAndClean removes all unstaged changes and removes all untracked files
func (self *WorkingTreeCommands) ResetAndClean() error {
submoduleConfigs, err := self.submodule.GetConfigs()
if err != nil {
return err
}
if len(submoduleConfigs) > 0 {
if err := self.submodule.ResetSubmodules(submoduleConfigs); err != nil {
return err
}
}
if err := self.ResetHard("HEAD"); err != nil {
return err
}
return self.RemoveUntrackedFiles()
}
// ResetHardHead runs `git reset --hard`
func (self *WorkingTreeCommands) ResetHard(ref string) error {
return self.cmd.New("git reset --hard " + self.cmd.Quote(ref)).Run()
}
// ResetSoft runs `git reset --soft HEAD`
func (self *WorkingTreeCommands) ResetSoft(ref string) error {
return self.cmd.New("git reset --soft " + self.cmd.Quote(ref)).Run()
}
func (self *WorkingTreeCommands) ResetMixed(ref string) error {
return self.cmd.New("git reset --mixed " + self.cmd.Quote(ref)).Run()
}
// so that we don't have unnecessary space in our commands we use this helper function to prepend spaces to args so that in the format string we can go '%s%s%s' and if any args are missing we won't have gaps.
func pad(str string) string {
if str == "" {
return ""
}
return " " + str
}

View File

@@ -0,0 +1,586 @@
package git_commands
import (
"fmt"
"io/ioutil"
"regexp"
"testing"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/stretchr/testify/assert"
)
func TestWorkingTreeStageFile(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
Expect(`git add -- "test.txt"`, "", nil)
instance := buildWorkingTreeCommands(commonDeps{runner: runner})
assert.NoError(t, instance.StageFile("test.txt"))
runner.CheckForMissingCalls()
}
func TestWorkingTreeStageFiles(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
Expect(`git add -- "test.txt" "test2.txt"`, "", nil)
instance := buildWorkingTreeCommands(commonDeps{runner: runner})
assert.NoError(t, instance.StageFiles([]string{"test.txt", "test2.txt"}))
runner.CheckForMissingCalls()
}
func TestWorkingTreeUnstageFile(t *testing.T) {
type scenario struct {
testName string
reset bool
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
testName: "Remove an untracked file from staging",
reset: false,
runner: oscommands.NewFakeRunner(t).
Expect(`git rm --cached --force -- "test.txt"`, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
testName: "Remove a tracked file from staging",
reset: true,
runner: oscommands.NewFakeRunner(t).
Expect(`git reset HEAD -- "test.txt"`, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner})
s.test(instance.UnStageFile([]string{"test.txt"}, s.reset))
})
}
}
// these tests don't cover everything, in part because we already have an integration
// test which does cover everything. I don't want to unnecessarily assert on the 'how'
// when the 'what' is what matters
func TestWorkingTreeDiscardAllFileChanges(t *testing.T) {
type scenario struct {
testName string
file *models.File
removeFile func(string) error
runner *oscommands.FakeCmdObjRunner
expectedError string
}
scenarios := []scenario{
{
testName: "An error occurred when resetting",
file: &models.File{
Name: "test",
HasStagedChanges: true,
},
removeFile: func(string) error { return nil },
runner: oscommands.NewFakeRunner(t).
Expect(`git reset -- "test"`, "", errors.New("error")),
expectedError: "error",
},
{
testName: "An error occurred when removing file",
file: &models.File{
Name: "test",
Tracked: false,
Added: true,
},
removeFile: func(string) error {
return fmt.Errorf("an error occurred when removing file")
},
runner: oscommands.NewFakeRunner(t),
expectedError: "an error occurred when removing file",
},
{
testName: "An error occurred with checkout",
file: &models.File{
Name: "test",
Tracked: true,
HasStagedChanges: false,
},
removeFile: func(string) error { return nil },
runner: oscommands.NewFakeRunner(t).
Expect(`git checkout -- "test"`, "", errors.New("error")),
expectedError: "error",
},
{
testName: "Checkout only",
file: &models.File{
Name: "test",
Tracked: true,
HasStagedChanges: false,
},
removeFile: func(string) error { return nil },
runner: oscommands.NewFakeRunner(t).
Expect(`git checkout -- "test"`, "", nil),
expectedError: "",
},
{
testName: "Reset and checkout staged changes",
file: &models.File{
Name: "test",
Tracked: true,
HasStagedChanges: true,
},
removeFile: func(string) error { return nil },
runner: oscommands.NewFakeRunner(t).
Expect(`git reset -- "test"`, "", nil).
Expect(`git checkout -- "test"`, "", nil),
expectedError: "",
},
{
testName: "Reset and checkout merge conflicts",
file: &models.File{
Name: "test",
Tracked: true,
HasMergeConflicts: true,
},
removeFile: func(string) error { return nil },
runner: oscommands.NewFakeRunner(t).
Expect(`git reset -- "test"`, "", nil).
Expect(`git checkout -- "test"`, "", nil),
expectedError: "",
},
{
testName: "Reset and remove",
file: &models.File{
Name: "test",
Tracked: false,
Added: true,
HasStagedChanges: true,
},
removeFile: func(filename string) error {
assert.Equal(t, "test", filename)
return nil
},
runner: oscommands.NewFakeRunner(t).
Expect(`git reset -- "test"`, "", nil),
expectedError: "",
},
{
testName: "Remove only",
file: &models.File{
Name: "test",
Tracked: false,
Added: true,
HasStagedChanges: false,
},
removeFile: func(filename string) error {
assert.Equal(t, "test", filename)
return nil
},
runner: oscommands.NewFakeRunner(t),
expectedError: "",
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner, removeFile: s.removeFile})
err := instance.DiscardAllFileChanges(s.file)
if s.expectedError == "" {
assert.Nil(t, err)
} else {
assert.Equal(t, s.expectedError, err.Error())
}
s.runner.CheckForMissingCalls()
})
}
}
func TestWorkingTreeDiff(t *testing.T) {
type scenario struct {
testName string
file *models.File
plain bool
cached bool
ignoreWhitespace bool
contextSize int
runner *oscommands.FakeCmdObjRunner
}
const expectedResult = "pretend this is an actual git diff"
scenarios := []scenario{
{
testName: "Default case",
file: &models.File{
Name: "test.txt",
HasStagedChanges: false,
Tracked: true,
},
plain: false,
cached: false,
ignoreWhitespace: false,
contextSize: 3,
runner: oscommands.NewFakeRunner(t).
Expect(`git diff --submodule --no-ext-diff --unified=3 --color=always -- "test.txt"`, expectedResult, nil),
},
{
testName: "cached",
file: &models.File{
Name: "test.txt",
HasStagedChanges: false,
Tracked: true,
},
plain: false,
cached: true,
ignoreWhitespace: false,
contextSize: 3,
runner: oscommands.NewFakeRunner(t).
Expect(`git diff --submodule --no-ext-diff --unified=3 --color=always --cached -- "test.txt"`, expectedResult, nil),
},
{
testName: "plain",
file: &models.File{
Name: "test.txt",
HasStagedChanges: false,
Tracked: true,
},
plain: true,
cached: false,
ignoreWhitespace: false,
contextSize: 3,
runner: oscommands.NewFakeRunner(t).
Expect(`git diff --submodule --no-ext-diff --unified=3 --color=never -- "test.txt"`, expectedResult, nil),
},
{
testName: "File not tracked and file has no staged changes",
file: &models.File{
Name: "test.txt",
HasStagedChanges: false,
Tracked: false,
},
plain: false,
cached: false,
ignoreWhitespace: false,
contextSize: 3,
runner: oscommands.NewFakeRunner(t).
Expect(`git diff --submodule --no-ext-diff --unified=3 --color=always --no-index -- /dev/null "test.txt"`, expectedResult, nil),
},
{
testName: "Default case (ignore whitespace)",
file: &models.File{
Name: "test.txt",
HasStagedChanges: false,
Tracked: true,
},
plain: false,
cached: false,
ignoreWhitespace: true,
contextSize: 3,
runner: oscommands.NewFakeRunner(t).
Expect(`git diff --submodule --no-ext-diff --unified=3 --color=always --ignore-all-space -- "test.txt"`, expectedResult, nil),
},
{
testName: "Show diff with custom context size",
file: &models.File{
Name: "test.txt",
HasStagedChanges: false,
Tracked: true,
},
plain: false,
cached: false,
ignoreWhitespace: false,
contextSize: 17,
runner: oscommands.NewFakeRunner(t).
Expect(`git diff --submodule --no-ext-diff --unified=17 --color=always -- "test.txt"`, expectedResult, nil),
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
userConfig := config.GetDefaultConfig()
userConfig.Git.DiffContextSize = s.contextSize
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner, userConfig: userConfig})
result := instance.WorktreeFileDiff(s.file, s.plain, s.cached, s.ignoreWhitespace)
assert.Equal(t, expectedResult, result)
s.runner.CheckForMissingCalls()
})
}
}
func TestWorkingTreeShowFileDiff(t *testing.T) {
type scenario struct {
testName string
from string
to string
reverse bool
plain bool
contextSize int
runner *oscommands.FakeCmdObjRunner
}
const expectedResult = "pretend this is an actual git diff"
scenarios := []scenario{
{
testName: "Default case",
from: "1234567890",
to: "0987654321",
reverse: false,
plain: false,
contextSize: 3,
runner: oscommands.NewFakeRunner(t).
Expect(`git diff --submodule --no-ext-diff --unified=3 --no-renames --color=always 1234567890 0987654321 -- "test.txt"`, expectedResult, nil),
},
{
testName: "Show diff with custom context size",
from: "1234567890",
to: "0987654321",
reverse: false,
plain: false,
contextSize: 123,
runner: oscommands.NewFakeRunner(t).
Expect(`git diff --submodule --no-ext-diff --unified=123 --no-renames --color=always 1234567890 0987654321 -- "test.txt"`, expectedResult, nil),
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
userConfig := config.GetDefaultConfig()
userConfig.Git.DiffContextSize = s.contextSize
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner, userConfig: userConfig})
result, err := instance.ShowFileDiff(s.from, s.to, s.reverse, "test.txt", s.plain)
assert.NoError(t, err)
assert.Equal(t, expectedResult, result)
s.runner.CheckForMissingCalls()
})
}
}
func TestWorkingTreeCheckoutFile(t *testing.T) {
type scenario struct {
testName string
commitSha string
fileName string
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
testName: "typical case",
commitSha: "11af912",
fileName: "test999.txt",
runner: oscommands.NewFakeRunner(t).
Expect(`git checkout 11af912 -- "test999.txt"`, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
testName: "returns error if there is one",
commitSha: "11af912",
fileName: "test999.txt",
runner: oscommands.NewFakeRunner(t).
Expect(`git checkout 11af912 -- "test999.txt"`, "", errors.New("error")),
test: func(err error) {
assert.Error(t, err)
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner})
s.test(instance.CheckoutFile(s.commitSha, s.fileName))
s.runner.CheckForMissingCalls()
})
}
}
func TestWorkingTreeApplyPatch(t *testing.T) {
type scenario struct {
testName string
runner *oscommands.FakeCmdObjRunner
test func(error)
}
expectFn := func(regexStr string, errToReturn error) func(cmdObj oscommands.ICmdObj) (string, error) {
return func(cmdObj oscommands.ICmdObj) (string, error) {
re := regexp.MustCompile(regexStr)
cmdStr := cmdObj.ToString()
matches := re.FindStringSubmatch(cmdStr)
assert.Equal(t, 2, len(matches), fmt.Sprintf("unexpected command: %s", cmdStr))
filename := matches[1]
content, err := ioutil.ReadFile(filename)
assert.NoError(t, err)
assert.Equal(t, "test", string(content))
return "", errToReturn
}
}
scenarios := []scenario{
{
testName: "valid case",
runner: oscommands.NewFakeRunner(t).
ExpectFunc(expectFn(`git apply --cached "(.*)"`, nil)),
test: func(err error) {
assert.NoError(t, err)
},
},
{
testName: "command returns error",
runner: oscommands.NewFakeRunner(t).
ExpectFunc(expectFn(`git apply --cached "(.*)"`, errors.New("error"))),
test: func(err error) {
assert.Error(t, err)
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner})
s.test(instance.ApplyPatch("test", "cached"))
s.runner.CheckForMissingCalls()
})
}
}
func TestWorkingTreeDiscardUnstagedFileChanges(t *testing.T) {
type scenario struct {
testName string
file *models.File
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
testName: "valid case",
file: &models.File{Name: "test.txt"},
runner: oscommands.NewFakeRunner(t).
Expect(`git checkout -- "test.txt"`, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner})
s.test(instance.DiscardUnstagedFileChanges(s.file))
s.runner.CheckForMissingCalls()
})
}
}
func TestWorkingTreeDiscardAnyUnstagedFileChanges(t *testing.T) {
type scenario struct {
testName string
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
testName: "valid case",
runner: oscommands.NewFakeRunner(t).
Expect(`git checkout -- .`, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner})
s.test(instance.DiscardAnyUnstagedFileChanges())
s.runner.CheckForMissingCalls()
})
}
}
func TestWorkingTreeRemoveUntrackedFiles(t *testing.T) {
type scenario struct {
testName string
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
testName: "valid case",
runner: oscommands.NewFakeRunner(t).
Expect(`git clean -fd`, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner})
s.test(instance.RemoveUntrackedFiles())
s.runner.CheckForMissingCalls()
})
}
}
func TestWorkingTreeResetHard(t *testing.T) {
type scenario struct {
testName string
ref string
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
"valid case",
"HEAD",
oscommands.NewFakeRunner(t).
Expect(`git reset --hard "HEAD"`, "", nil),
func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner})
s.test(instance.ResetHard(s.ref))
})
}
}

View File

@@ -0,0 +1,86 @@
package git_config
import (
"os/exec"
"strings"
"github.com/sirupsen/logrus"
)
type IGitConfig interface {
// this is for when you want to pass 'mykey' (it calls `git config --get --null mykey` under the hood)
Get(string) string
// this is for when you want to pass '--local --get-regexp mykey'
GetGeneral(string) string
// this is for when you want to pass 'mykey' and check if the result is truthy
GetBool(string) bool
}
type CachedGitConfig struct {
cache map[string]string
runGitConfigCmd func(*exec.Cmd) (string, error)
log *logrus.Entry
}
func NewStdCachedGitConfig(log *logrus.Entry) *CachedGitConfig {
return NewCachedGitConfig(runGitConfigCmd, log)
}
func NewCachedGitConfig(runGitConfigCmd func(*exec.Cmd) (string, error), log *logrus.Entry) *CachedGitConfig {
return &CachedGitConfig{
cache: make(map[string]string),
runGitConfigCmd: runGitConfigCmd,
log: log,
}
}
func (self *CachedGitConfig) Get(key string) string {
if value, ok := self.cache[key]; ok {
self.log.Debugf("using cache for key " + key)
return value
}
value := self.getAux(key)
self.cache[key] = value
return value
}
func (self *CachedGitConfig) GetGeneral(args string) string {
if value, ok := self.cache[args]; ok {
self.log.Debugf("using cache for args " + args)
return value
}
value := self.getGeneralAux(args)
self.cache[args] = value
return value
}
func (self *CachedGitConfig) getGeneralAux(args string) string {
cmd := getGitConfigGeneralCmd(args)
value, err := self.runGitConfigCmd(cmd)
if err != nil {
self.log.Debugf("Error getting git config value for args: " + args + ". Error: " + err.Error())
return ""
}
return strings.TrimSpace(value)
}
func (self *CachedGitConfig) getAux(key string) string {
cmd := getGitConfigCmd(key)
value, err := self.runGitConfigCmd(cmd)
if err != nil {
self.log.Debugf("Error getting git config value for key: " + key + ". Error: " + err.Error())
return ""
}
return strings.TrimSpace(value)
}
func (self *CachedGitConfig) GetBool(key string) bool {
return isTruthy(self.Get(key))
}
func isTruthy(value string) bool {
lcValue := strings.ToLower(value)
return lcValue == "true" || lcValue == "1" || lcValue == "yes" || lcValue == "on"
}

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,107 @@
package hosting_service
// if you want to make a custom regex for a given service feel free to test it out
// at regoio.herokuapp.com
var defaultUrlRegexStrings = []string{
`^(?:https?|ssh)://[^/]+/(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
`^git@.*:(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
}
var (
defaultRepoURLTemplate = "https://{{.webDomain}}/{{.owner}}/{{.repo}}"
defaultRepoNameTemplate = "{{.owner}}/{{.repo}}"
)
// we've got less type safety using go templates but this lends itself better to
// users adding custom service definitions in their config
var githubServiceDef = ServiceDefinition{
provider: "github",
pullRequestURLIntoDefaultBranch: "/compare/{{.From}}?expand=1",
pullRequestURLIntoTargetBranch: "/compare/{{.To}}...{{.From}}?expand=1",
commitURL: "/commit/{{.CommitSha}}",
regexStrings: defaultUrlRegexStrings,
repoURLTemplate: defaultRepoURLTemplate,
repoNameTemplate: defaultRepoNameTemplate,
}
var bitbucketServiceDef = ServiceDefinition{
provider: "bitbucket",
pullRequestURLIntoDefaultBranch: "/pull-requests/new?source={{.From}}&t=1",
pullRequestURLIntoTargetBranch: "/pull-requests/new?source={{.From}}&dest={{.To}}&t=1",
commitURL: "/commits/{{.CommitSha}}",
regexStrings: []string{
`^(?:https?|ssh)://.*/(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
`^.*@.*:(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
},
repoURLTemplate: defaultRepoURLTemplate,
repoNameTemplate: defaultRepoNameTemplate,
}
var gitLabServiceDef = ServiceDefinition{
provider: "gitlab",
pullRequestURLIntoDefaultBranch: "/merge_requests/new?merge_request[source_branch]={{.From}}",
pullRequestURLIntoTargetBranch: "/merge_requests/new?merge_request[source_branch]={{.From}}&merge_request[target_branch]={{.To}}",
commitURL: "/commit/{{.CommitSha}}",
regexStrings: defaultUrlRegexStrings,
repoURLTemplate: defaultRepoURLTemplate,
repoNameTemplate: defaultRepoNameTemplate,
}
var azdoServiceDef = ServiceDefinition{
provider: "azuredevops",
pullRequestURLIntoDefaultBranch: "/pullrequestcreate?sourceRef={{.From}}",
pullRequestURLIntoTargetBranch: "/pullrequestcreate?sourceRef={{.From}}&targetRef={{.To}}",
commitURL: "/commit/{{.CommitSha}}",
regexStrings: []string{
`^git@ssh.dev.azure.com.*/(?P<org>.*)/(?P<project>.*)/(?P<repo>.*?)(?:\.git)?$`,
`^https://.*@dev.azure.com/(?P<org>.*?)/(?P<project>.*?)/_git/(?P<repo>.*?)(?:\.git)?$`,
},
repoURLTemplate: "https://{{.webDomain}}/{{.org}}/{{.project}}/_git/{{.repo}}",
// TODO: verify this is actually correct
repoNameTemplate: "{{.org}}/{{.project}}/{{.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}}",
// TODO: verify this is actually correct
repoNameTemplate: "{{.project}}/{{.repo}}",
}
var serviceDefinitions = []ServiceDefinition{
githubServiceDef,
bitbucketServiceDef,
gitLabServiceDef,
azdoServiceDef,
bitbucketServerServiceDef,
}
var defaultServiceDomains = []ServiceDomain{
{
serviceDefinition: githubServiceDef,
gitDomain: "github.com",
webDomain: "github.com",
},
{
serviceDefinition: bitbucketServiceDef,
gitDomain: "bitbucket.org",
webDomain: "bitbucket.org",
},
{
serviceDefinition: gitLabServiceDef,
gitDomain: "gitlab.com",
webDomain: "gitlab.com",
},
{
serviceDefinition: azdoServiceDef,
gitDomain: "dev.azure.com",
webDomain: "dev.azure.com",
},
}

View File

@@ -0,0 +1,224 @@
package hosting_service
import (
"net/url"
"regexp"
"strings"
"github.com/go-errors/errors"
"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.
// Different git hosting services have different URL formats for when you want to open a PR or view a commit,
// and this package's responsibility is to determine which service you're using based on the remote URL,
// and then which URL you need for whatever use case you have.
type HostingServiceMgr struct {
log logrus.FieldLogger
tr *i18n.TranslationSet
remoteURL string // e.g. https://github.com/jesseduffield/lazygit
// see https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-pull-request-urls
configServiceDomains map[string]string
}
// NewHostingServiceMgr creates new instance of PullRequest
func NewHostingServiceMgr(log logrus.FieldLogger, tr *i18n.TranslationSet, remoteURL string, configServiceDomains map[string]string) *HostingServiceMgr {
return &HostingServiceMgr{
log: log,
tr: tr,
remoteURL: remoteURL,
configServiceDomains: configServiceDomains,
}
}
func (self *HostingServiceMgr) GetPullRequestURL(from string, to string) (string, error) {
gitService, err := self.getService()
if err != nil {
return "", err
}
if to == "" {
return gitService.getPullRequestURLIntoDefaultBranch(url.QueryEscape(from)), nil
} else {
return gitService.getPullRequestURLIntoTargetBranch(url.QueryEscape(from), url.QueryEscape(to)), nil
}
}
func (self *HostingServiceMgr) GetCommitURL(commitSha string) (string, error) {
gitService, err := self.getService()
if err != nil {
return "", err
}
pullRequestURL := gitService.getCommitURL(commitSha)
return pullRequestURL, nil
}
// e.g. 'jesseduffield/lazygit'
func (self *HostingServiceMgr) GetRepoName() (string, error) {
gitService, err := self.getService()
if err != nil {
return "", err
}
repoName := gitService.repoName
return repoName, nil
}
func (self *HostingServiceMgr) getService() (*Service, error) {
serviceDomain, err := self.getServiceDomain(self.remoteURL)
if err != nil {
return nil, err
}
repoURL, err := serviceDomain.serviceDefinition.getRepoURLFromRemoteURL(self.remoteURL, serviceDomain.webDomain)
if err != nil {
return nil, err
}
repoName, err := serviceDomain.serviceDefinition.getRepoNameFromRemoteURL(self.remoteURL)
if err != nil {
return nil, err
}
return &Service{
repoURL: repoURL,
repoName: repoName,
ServiceDefinition: serviceDomain.serviceDefinition,
}, nil
}
func (self *HostingServiceMgr) getServiceDomain(repoURL string) (*ServiceDomain, error) {
candidateServiceDomains := self.getCandidateServiceDomains()
for _, serviceDomain := range candidateServiceDomains {
if strings.Contains(repoURL, serviceDomain.gitDomain) {
return &serviceDomain, nil
}
}
return nil, errors.New(self.tr.UnsupportedGitService)
}
func (self *HostingServiceMgr) getCandidateServiceDomains() []ServiceDomain {
serviceDefinitionByProvider := map[string]ServiceDefinition{}
for _, serviceDefinition := range serviceDefinitions {
serviceDefinitionByProvider[serviceDefinition.provider] = serviceDefinition
}
serviceDomains := slices.Clone(defaultServiceDomains)
if len(self.configServiceDomains) > 0 {
for gitDomain, typeAndDomain := range self.configServiceDomains {
splitData := strings.Split(typeAndDomain, ":")
if len(splitData) != 2 {
self.log.Errorf("Unexpected format for git service: '%s'. Expected something like 'github.com:github.com'", typeAndDomain)
continue
}
provider := splitData[0]
webDomain := splitData[1]
serviceDefinition, ok := serviceDefinitionByProvider[provider]
if !ok {
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
}
serviceDomains = append(serviceDomains, ServiceDomain{
gitDomain: gitDomain,
webDomain: webDomain,
serviceDefinition: serviceDefinition,
})
}
}
return serviceDomains
}
// a service domains pairs a service definition with the actual domain it's being served from.
// Sometimes the git service is hosted in a custom domains so although it'll use say
// the github service definition, it'll actually be served from e.g. my-custom-github.com
type ServiceDomain struct {
gitDomain string // the one that appears in the git remote url
webDomain string // the one that appears in the web url
serviceDefinition ServiceDefinition
}
type ServiceDefinition struct {
provider string
pullRequestURLIntoDefaultBranch string
pullRequestURLIntoTargetBranch string
commitURL string
regexStrings []string
// can expect 'webdomain' to be passed in. Otherwise, you get to pick what we match in the regex
repoURLTemplate string
repoNameTemplate string
}
func (self ServiceDefinition) getRepoURLFromRemoteURL(url string, webDomain string) (string, error) {
matches, err := self.parseRemoteUrl(url)
if err != nil {
return "", err
}
matches["webDomain"] = webDomain
return utils.ResolvePlaceholderString(self.repoURLTemplate, matches), nil
}
func (self ServiceDefinition) getRepoNameFromRemoteURL(url string) (string, error) {
matches, err := self.parseRemoteUrl(url)
if err != nil {
return "", err
}
return utils.ResolvePlaceholderString(self.repoNameTemplate, matches), nil
}
func (self ServiceDefinition) parseRemoteUrl(url string) (map[string]string, error) {
for _, regexStr := range self.regexStrings {
re := regexp.MustCompile(regexStr)
matches := utils.FindNamedMatches(re, url)
if matches != nil {
return matches, nil
}
}
return nil, errors.New("Failed to parse repo information from url")
}
type Service struct {
repoURL string
// e.g. 'jesseduffield/lazygit'
repoName string
ServiceDefinition
}
func (self *Service) getPullRequestURLIntoDefaultBranch(from string) string {
return self.resolveUrl(self.pullRequestURLIntoDefaultBranch, map[string]string{"From": from})
}
func (self *Service) getPullRequestURLIntoTargetBranch(from string, to string) string {
return self.resolveUrl(self.pullRequestURLIntoTargetBranch, map[string]string{"From": from, "To": to})
}
func (self *Service) getCommitURL(commitSha string) string {
return self.resolveUrl(self.commitURL, map[string]string{"CommitSha": commitSha})
}
func (self *Service) resolveUrl(templateString string, args map[string]string) string {
return self.repoURL + utils.ResolvePlaceholderString(templateString, args)
}

View File

@@ -0,0 +1,337 @@
package hosting_service
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/test"
"github.com/stretchr/testify/assert"
)
func TestGetPullRequestURL(t *testing.T) {
type scenario struct {
testName string
from string
to string
remoteUrl string
configServiceDomains map[string]string
test func(url string, err error)
expectedLoggedErrors []string
}
scenarios := []scenario{
{
testName: "Opens a link to new pull request on bitbucket",
from: "feature/profile-page",
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature%2Fprofile-page&t=1", url)
},
},
{
testName: "Opens a link to new pull request on bitbucket with http remote url",
from: "feature/events",
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature%2Fevents&t=1", url)
},
},
{
testName: "Opens a link to new pull request on github",
from: "feature/sum-operation",
remoteUrl: "git@github.com:peter/calculator.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://github.com/peter/calculator/compare/feature%2Fsum-operation?expand=1", url)
},
},
{
testName: "Opens a link to new pull request on github with https remote url",
from: "feature/sum-operation",
remoteUrl: "https://github.com/peter/calculator.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://github.com/peter/calculator/compare/feature%2Fsum-operation?expand=1", url)
},
},
{
testName: "Opens a link to new pull request on bitbucket with specific target branch",
from: "feature/profile-page/avatar",
to: "feature/profile-page",
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature%2Fprofile-page%2Favatar&dest=feature%2Fprofile-page&t=1", url)
},
},
{
testName: "Opens a link to new pull request on bitbucket with http remote url with specified target branch",
from: "feature/remote-events",
to: "feature/events",
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature%2Fremote-events&dest=feature%2Fevents&t=1", url)
},
},
{
testName: "Opens a link to new pull request on github with specific target branch",
from: "feature/sum-operation",
to: "feature/operations",
remoteUrl: "git@github.com:peter/calculator.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://github.com/peter/calculator/compare/feature%2Foperations...feature%2Fsum-operation?expand=1", url)
},
},
{
testName: "Opens a link to new pull request on github with https remote url with specific target branch",
from: "feature/sum-operation",
to: "feature/operations",
remoteUrl: "https://github.com/peter/calculator.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://github.com/peter/calculator/compare/feature%2Foperations...feature%2Fsum-operation?expand=1", url)
},
},
{
testName: "Opens a link to new pull request on gitlab",
from: "feature/ui",
remoteUrl: "git@gitlab.com:peter/calculator.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature%2Fui", url)
},
},
{
testName: "Opens a link to new pull request on gitlab in nested groups",
from: "feature/ui",
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature%2Fui", url)
},
},
{
testName: "Opens a link to new pull request on gitlab with https remote url in nested groups",
from: "feature/ui",
remoteUrl: "https://gitlab.com/peter/public/calculator.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature%2Fui", url)
},
},
{
testName: "Opens a link to new pull request on gitlab with specific target branch",
from: "feature/commit-ui",
to: "epic/ui",
remoteUrl: "git@gitlab.com:peter/calculator.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature%2Fcommit-ui&merge_request[target_branch]=epic%2Fui", url)
},
},
{
testName: "Opens a link to new pull request on gitlab with specific target branch in nested groups",
from: "feature/commit-ui",
to: "epic/ui",
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature%2Fcommit-ui&merge_request[target_branch]=epic%2Fui", url)
},
},
{
testName: "Opens a link to new pull request on gitlab with https remote url with specific target branch in nested groups",
from: "feature/commit-ui",
to: "epic/ui",
remoteUrl: "https://gitlab.com/peter/public/calculator.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature%2Fcommit-ui&merge_request[target_branch]=epic%2Fui", url)
},
},
{
testName: "Opens a link to new pull request on bitbucket with a custom SSH username",
from: "feature/profile-page",
remoteUrl: "john@bitbucket.org:johndoe/social_network.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature%2Fprofile-page&t=1", url)
},
},
{
testName: "Opens a link to new pull request on Azure DevOps (SSH)",
from: "feature/new",
remoteUrl: "git@ssh.dev.azure.com:v3/myorg/myproject/myrepo",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://dev.azure.com/myorg/myproject/_git/myrepo/pullrequestcreate?sourceRef=feature%2Fnew", url)
},
},
{
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",
test: func(url string, err error) {
assert.NoError(t, err)
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 Azure DevOps (HTTP)",
from: "feature/new",
remoteUrl: "https://myorg@dev.azure.com/myorg/myproject/_git/myrepo",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://dev.azure.com/myorg/myproject/_git/myrepo/pullrequestcreate?sourceRef=feature%2Fnew", url)
},
},
{
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",
test: func(url string, err error) {
assert.NoError(t, err)
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",
remoteUrl: "git@something.com:peter/calculator.git",
test: func(url string, err error) {
assert.EqualError(t, err, "Unsupported git service")
},
},
{
testName: "Does not log error when config service domains are valid",
from: "feature/profile-page",
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
configServiceDomains: map[string]string{
// valid configuration for a custom service URL
"git.work.com": "gitlab:code.work.com",
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature%2Fprofile-page&t=1", url)
},
expectedLoggedErrors: nil,
},
{
testName: "Logs error when config service domain is malformed",
from: "feature/profile-page",
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
configServiceDomains: map[string]string{
"noservice.work.com": "noservice.work.com",
},
test: func(url string, err error) {
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{"Unexpected format for git service: 'noservice.work.com'. Expected something like 'github.com:github.com'"},
},
{
testName: "Logs error when config service domain uses unknown provider",
from: "feature/profile-page",
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
configServiceDomains: map[string]string{
"invalid.work.com": "noservice:invalid.work.com",
},
test: func(url string, err error) {
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, bitbucketServer"},
},
{
testName: "Escapes reserved URL characters in from branch name",
from: "feature/someIssue#123",
to: "master",
remoteUrl: "git@gitlab.com:me/public/repo-with-issues.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/me/public/repo-with-issues/merge_requests/new?merge_request[source_branch]=feature%2FsomeIssue%23123&merge_request[target_branch]=master", url)
},
},
{
testName: "Escapes reserved URL characters in to branch name",
from: "yolo",
to: "archive/never-ending-feature#666",
remoteUrl: "git@gitlab.com:me/public/repo-with-issues.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/me/public/repo-with-issues/merge_requests/new?merge_request[source_branch]=yolo&merge_request[target_branch]=archive%2Fnever-ending-feature%23666", url)
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
tr := i18n.EnglishTranslationSet()
log := &test.FakeFieldLogger{}
hostingServiceMgr := NewHostingServiceMgr(log, &tr, s.remoteUrl, s.configServiceDomains)
s.test(hostingServiceMgr.GetPullRequestURL(s.from, s.to))
log.AssertErrors(t, s.expectedLoggedErrors)
})
}
}

View File

@@ -0,0 +1,204 @@
package loaders
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"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// context:
// we want to only show 'safe' branches (ones that haven't e.g. been deleted)
// which `git branch -a` gives us, but we also want the recency data that
// git reflog gives us.
// So we get the HEAD, then append get the reflog branches that intersect with
// our safe branches, then add the remaining safe branches, ensuring uniqueness
// along the way
// if we find out we need to use one of these functions in the git.go file, we
// can just pull them out of here and put them there and then call them from in here
type BranchLoaderConfigCommands interface {
Branches() (map[string]*config.Branch, error)
}
// BranchLoader returns a list of Branch objects for the current repo
type BranchLoader struct {
*common.Common
getRawBranches func() (string, error)
getCurrentBranchName func() (string, string, error)
config BranchLoaderConfigCommands
}
func NewBranchLoader(
cmn *common.Common,
getRawBranches func() (string, error),
getCurrentBranchName func() (string, string, error),
config BranchLoaderConfigCommands,
) *BranchLoader {
return &BranchLoader{
Common: cmn,
getRawBranches: getRawBranches,
getCurrentBranchName: getCurrentBranchName,
config: config,
}
}
// Load the list of branches for the current repo
func (self *BranchLoader) Load(reflogCommits []*models.Commit) ([]*models.Branch, error) {
branches := self.obtainBranches()
reflogBranches := self.obtainReflogBranches(reflogCommits)
// loop through reflog branches. If there is a match, merge them, then remove it from the branches and keep it in the reflog branches
branchesWithRecency := make([]*models.Branch, 0)
outer:
for _, reflogBranch := range reflogBranches {
for j, branch := range branches {
if branch.Head {
continue
}
if strings.EqualFold(reflogBranch.Name, branch.Name) {
branch.Recency = reflogBranch.Recency
branchesWithRecency = append(branchesWithRecency, branch)
branches = slices.Remove(branches, j)
continue outer
}
}
}
branches = slices.Prepend(branches, branchesWithRecency...)
foundHead := false
for i, branch := range branches {
if branch.Head {
foundHead = true
branch.Recency = " *"
branches = slices.Move(branches, i, 0)
break
}
}
if !foundHead {
currentBranchName, currentBranchDisplayName, err := self.getCurrentBranchName()
if err != nil {
return nil, err
}
branches = slices.Prepend(branches, &models.Branch{Name: currentBranchName, DisplayName: currentBranchDisplayName, Head: true, Recency: " *"})
}
configBranches, err := self.config.Branches()
if err != nil {
return nil, err
}
for _, branch := range branches {
match := configBranches[branch.Name]
if match != nil {
branch.UpstreamRemote = match.Remote
branch.UpstreamBranch = match.Merge.Short()
}
}
return branches, nil
}
func (self *BranchLoader) obtainBranches() []*models.Branch {
output, err := self.getRawBranches()
if err != nil {
panic(err)
}
trimmedOutput := strings.TrimSpace(output)
outputLines := strings.Split(trimmedOutput, "\n")
return slices.FilterMap(outputLines, func(line string) (*models.Branch, bool) {
if line == "" {
return nil, false
}
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
return nil, false
}
return obtainBranch(split), true
})
}
// 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] == "*",
}
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 {
branch.Pushables = match[1]
} else {
branch.Pushables = "0"
}
re = regexp.MustCompile(`behind (\d+)`)
match = re.FindStringSubmatch(track)
if len(match) > 1 {
branch.Pullables = match[1]
} else {
branch.Pullables = "0"
}
}
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 {
foundBranches := set.New[string]()
re := regexp.MustCompile(`checkout: moving from ([\S]+) to ([\S]+)`)
reflogBranches := make([]*models.Branch, 0, len(reflogCommits))
for _, commit := range reflogCommits {
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,
})
}
}
}
return reflogBranches
}

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)
})
}
}

View File

@@ -0,0 +1,56 @@
package loaders
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 {
*common.Common
cmd oscommands.ICmdObjBuilder
}
func NewCommitFileLoader(common *common.Common, cmd oscommands.ICmdObjBuilder) *CommitFileLoader {
return &CommitFileLoader{
Common: common,
cmd: cmd,
}
}
// GetFilesInDiff get the specified commit files
func (self *CommitFileLoader) GetFilesInDiff(from string, to string, reverse bool) ([]*models.CommitFile, error) {
reverseFlag := ""
if reverse {
reverseFlag = " -R "
}
filenames, err := self.cmd.New(fmt.Sprintf("git diff --submodule --no-ext-diff --name-status -z --no-renames %s %s %s", reverseFlag, from, to)).DontLog().RunWithOutput()
if err != nil {
return nil, err
}
return getCommitFilesFromFilenames(filenames), nil
}
// 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")
if len(lines) == 1 {
return []*models.CommitFile{}
}
// 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],
}
})
}

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)
})
}
}

View File

@@ -0,0 +1,460 @@
package loaders
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"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"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/gui/style"
)
// context:
// here we get the commits from git log but format them to show whether they're
// unpushed/pushed/merged into the base branch or not, or if they're yet to
// be processed as part of a rebase (these won't appear in git log but we
// grab them from the rebase-related files in the .git directory to show them
// CommitLoader returns a list of Commit objects for the current repo
type CommitLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
getCurrentBranchName func() (string, string, error)
getRebaseMode func() (enums.RebaseMode, error)
readFile func(filename string) ([]byte, error)
walkFiles func(root string, fn filepath.WalkFunc) error
dotGitDir string
}
// making our dependencies explicit for the sake of easier testing
func NewCommitLoader(
cmn *common.Common,
cmd oscommands.ICmdObjBuilder,
dotGitDir string,
getCurrentBranchName func() (string, string, error),
getRebaseMode func() (enums.RebaseMode, error),
) *CommitLoader {
return &CommitLoader{
Common: cmn,
cmd: cmd,
getCurrentBranchName: getCurrentBranchName,
getRebaseMode: getRebaseMode,
readFile: ioutil.ReadFile,
walkFiles: filepath.Walk,
dotGitDir: dotGitDir,
}
}
type GetCommitsOptions struct {
Limit bool
FilterPath string
IncludeRebaseCommits bool
RefName string // e.g. "HEAD" or "my_branch"
// determines if we show the whole git graph i.e. pass the '--all' flag
All bool
}
// GetCommits obtains the commits of the current branch
func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit, error) {
commits := []*models.Commit{}
var rebasingCommits []*models.Commit
rebaseMode, err := self.getRebaseMode()
if err != nil {
return nil, err
}
if opts.IncludeRebaseCommits && opts.FilterPath == "" {
var err error
rebasingCommits, err = self.MergeRebasingCommits(commits)
if err != nil {
return nil, err
}
commits = append(commits, rebasingCommits...)
}
passedFirstPushedCommit := false
firstPushedCommit, err := self.getFirstPushedCommit(opts.RefName)
if err != nil {
// must have no upstream branch so we'll consider everything as pushed
passedFirstPushedCommit = true
}
err = self.getLogCmd(opts).RunAndProcessLines(func(line string) (bool, error) {
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 {
return nil, err
}
if len(commits) == 0 {
return commits, nil
}
if rebaseMode != enums.REBASE_MODE_NONE {
currentCommit := commits[len(rebasingCommits)]
youAreHere := style.FgYellow.Sprintf("<-- %s ---", self.Tr.YouAreHere)
currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name)
}
commits, err = self.setCommitMergedStatuses(opts.RefName, commits)
if err != nil {
return nil, err
}
return commits, nil
}
func (self *CommitLoader) MergeRebasingCommits(commits []*models.Commit) ([]*models.Commit, error) {
// chances are we have as many commits as last time so we'll set the capacity to be the old length
result := make([]*models.Commit, 0, len(commits))
for i, commit := range commits {
if commit.Status != "rebasing" { // removing the existing rebase commits so we can add the refreshed ones
result = append(result, commits[i:]...)
break
}
}
rebaseMode, err := self.getRebaseMode()
if err != nil {
return nil, err
}
if rebaseMode == enums.REBASE_MODE_NONE {
// not in rebase mode so return original commits
return result, nil
}
rebasingCommits, err := self.getHydratedRebasingCommits(rebaseMode)
if err != nil {
return nil, err
}
if len(rebasingCommits) > 0 {
result = append(rebasingCommits, result...)
}
return result, nil
}
// extractCommitFromLine takes a line from a git log and extracts the sha, message, date, and tag if present
// then puts them into a commit object
// example input:
// 8ad01fe32fcc20f07bc6693f87aa4977c327f1e1|10 hours ago|Jesse Duffield| (HEAD -> master, tag: v0.15.2)|refresh commits when adding a tag
func (self *CommitLoader) extractCommitFromLine(line string) *models.Commit {
split := strings.SplitN(line, "\x00", 7)
sha := split[0]
unixTimestamp := split[1]
authorName := split[2]
authorEmail := split[3]
extraInfo := strings.TrimSpace(split[4])
parentHashes := split[5]
message := split[6]
tags := []string{}
if extraInfo != "" {
re := regexp.MustCompile(`tag: ([^,\)]+)`)
tagMatch := re.FindStringSubmatch(extraInfo)
if len(tagMatch) > 1 {
tags = append(tags, tagMatch[1])
}
}
unitTimestampInt, _ := strconv.Atoi(unixTimestamp)
parents := []string{}
if len(parentHashes) > 0 {
parents = strings.Split(parentHashes, " ")
}
return &models.Commit{
Sha: sha,
Name: message,
Tags: tags,
ExtraInfo: extraInfo,
UnixTimestamp: int64(unitTimestampInt),
AuthorName: authorName,
AuthorEmail: authorEmail,
Parents: parents,
}
}
func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode) ([]*models.Commit, error) {
commits, err := self.getRebasingCommits(rebaseMode)
if err != nil {
return nil, err
}
if len(commits) == 0 {
return nil, nil
}
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 -c log.showSignature=false show %s --no-patch --oneline %s --abbrev=%d",
strings.Join(commitShas, " "),
prettyFormat,
20,
),
).DontLog()
hydratedCommits := make([]*models.Commit, 0, len(commits))
i := 0
err = cmdObj.RunAndProcessLines(func(line string) (bool, error) {
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 {
return nil, err
}
return hydratedCommits, nil
}
// getRebasingCommits obtains the commits that we're in the process of rebasing
func (self *CommitLoader) getRebasingCommits(rebaseMode enums.RebaseMode) ([]*models.Commit, error) {
switch rebaseMode {
case enums.REBASE_MODE_MERGING:
return self.getNormalRebasingCommits()
case enums.REBASE_MODE_INTERACTIVE:
return self.getInteractiveRebasingCommits()
default:
return nil, nil
}
}
func (self *CommitLoader) getNormalRebasingCommits() ([]*models.Commit, error) {
rewrittenCount := 0
bytesContent, err := self.readFile(filepath.Join(self.dotGitDir, "rebase-apply/rewritten"))
if err == nil {
content := string(bytesContent)
rewrittenCount = len(strings.Split(content, "\n"))
}
// we know we're rebasing, so lets get all the files whose names have numbers
commits := []*models.Commit{}
err = self.walkFiles(filepath.Join(self.dotGitDir, "rebase-apply"), func(path string, f os.FileInfo, err error) error {
if rewrittenCount > 0 {
rewrittenCount--
return nil
}
if err != nil {
return err
}
re := regexp.MustCompile(`^\d+$`)
if !re.MatchString(f.Name()) {
return nil
}
bytesContent, err := self.readFile(path)
if err != nil {
return err
}
content := string(bytesContent)
commit := self.commitFromPatch(content)
commits = append([]*models.Commit{commit}, commits...)
return nil
})
if err != nil {
return nil, err
}
return commits, nil
}
// git-rebase-todo example:
// pick ac446ae94ee560bdb8d1d057278657b251aaef17 ac446ae
// pick afb893148791a2fbd8091aeb81deba4930c73031 afb8931
// git-rebase-todo.backup example:
// pick 49cbba374296938ea86bbd4bf4fee2f6ba5cccf6 third commit on master
// pick ac446ae94ee560bdb8d1d057278657b251aaef17 blah commit on master
// pick afb893148791a2fbd8091aeb81deba4930c73031 fourth commit on master
// getInteractiveRebasingCommits takes our git-rebase-todo and our git-rebase-todo.backup files
// and extracts out the sha and names of commits that we still have to go
// in the rebase:
func (self *CommitLoader) getInteractiveRebasingCommits() ([]*models.Commit, error) {
bytesContent, err := self.readFile(filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo"))
if err != nil {
self.Log.Error(fmt.Sprintf("error occurred reading git-rebase-todo: %s", err.Error()))
// we assume an error means the file doesn't exist so we just return
return nil, nil
}
commits := []*models.Commit{}
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
}
commits = slices.Prepend(commits, &models.Commit{
Sha: t.Commit,
Name: t.Msg,
Status: "rebasing",
Action: t.Command.String(),
})
}
return commits, nil
}
// assuming the file starts like this:
// From e93d4193e6dd45ca9cf3a5a273d7ba6cd8b8fb20 Mon Sep 17 00:00:00 2001
// From: Lazygit Tester <test@example.com>
// Date: Wed, 5 Dec 2018 21:03:23 +1100
// Subject: second commit on master
func (self *CommitLoader) commitFromPatch(content string) *models.Commit {
lines := strings.Split(content, "\n")
sha := strings.Split(lines[0], " ")[1]
name := strings.TrimPrefix(lines[3], "Subject: ")
return &models.Commit{
Sha: sha,
Name: name,
Status: "rebasing",
}
}
func (self *CommitLoader) setCommitMergedStatuses(refName string, commits []*models.Commit) ([]*models.Commit, error) {
ancestor, err := self.getMergeBase(refName)
if err != nil {
return nil, err
}
if ancestor == "" {
return commits, nil
}
passedAncestor := false
for i, commit := range commits {
if strings.HasPrefix(ancestor, commit.Sha) {
passedAncestor = true
}
if commit.Status != "pushed" {
continue
}
if passedAncestor {
commits[i].Status = "merged"
}
}
return commits, nil
}
func (self *CommitLoader) getMergeBase(refName string) (string, error) {
currentBranch, _, err := self.getCurrentBranchName()
if err != nil {
return "", err
}
baseBranch := "master"
if strings.HasPrefix(currentBranch, "feature/") {
baseBranch = "develop"
}
// swallowing error because it's not a big deal; probably because there are no commits yet
output, _ := self.cmd.New(fmt.Sprintf("git merge-base %s %s", self.cmd.Quote(refName), self.cmd.Quote(baseBranch))).DontLog().RunWithOutput()
return ignoringWarnings(output), nil
}
func ignoringWarnings(commandOutput string) string {
trimmedOutput := strings.TrimSpace(commandOutput)
split := strings.Split(trimmedOutput, "\n")
// need to get last line in case the first line is a warning about how the error is ambiguous.
// At some point we should find a way to make it unambiguous
lastLine := split[len(split)-1]
return lastLine
}
// getFirstPushedCommit returns the first commit SHA which has been pushed to the ref's upstream.
// all commits above this are deemed unpushed and marked as such.
func (self *CommitLoader) getFirstPushedCommit(refName string) (string, error) {
output, err := self.cmd.
New(
fmt.Sprintf("git merge-base %s %s@{u}", self.cmd.Quote(refName), self.cmd.Quote(refName)),
).
DontLog().
RunWithOutput()
if err != nil {
return "", err
}
return ignoringWarnings(output), nil
}
// getLog gets the git log.
func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj {
limitFlag := ""
if opts.Limit {
limitFlag = " -300"
}
filterFlag := ""
if opts.FilterPath != "" {
filterFlag = fmt.Sprintf(" --follow -- %s", self.cmd.Quote(opts.FilterPath))
}
config := self.UserConfig.Git.Log
orderFlag := "--" + config.Order
allFlag := ""
if opts.All {
allFlag = " --all"
}
return self.cmd.New(
fmt.Sprintf(
"git -c log.showSignature=false log %s %s %s --oneline %s%s --abbrev=%d%s",
self.cmd.Quote(opts.RefName),
orderFlag,
allFlag,
prettyFormat,
limitFlag,
40,
filterFlag,
),
).DontLog()
}
var prettyFormat = fmt.Sprintf(
"--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,
)
const NULL_CODE = "%x00"

View File

@@ -0,0 +1,206 @@
package loaders
import (
"path/filepath"
"strings"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
)
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 {
testName string
runner *oscommands.FakeCmdObjRunner
expectedCommits []*models.Commit
expectedError error
rebaseMode enums.RebaseMode
currentBranchName string
opts GetCommitsOptions
}
scenarios := []scenario{
{
testName: "should return no commits if there are none",
rebaseMode: enums.REBASE_MODE_NONE,
currentBranchName: "master",
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
runner: oscommands.NewFakeRunner(t).
Expect(`git merge-base "HEAD" "HEAD"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", 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,
},
{
testName: "should return commits if they are present",
rebaseMode: enums.REBASE_MODE_NONE,
currentBranchName: "master",
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
runner: oscommands.NewFakeRunner(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 -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),
expectedCommits: []*models.Commit{
{
Sha: "0eea75e8c631fba6b58135697835d58ba4c18dbc",
Name: "better typing for rebase mode",
Status: "unpushed",
Action: "",
Tags: []string{},
ExtraInfo: "(HEAD -> better-tests)",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640826609,
Parents: []string{
"b21997d6b4cbdf84b149",
},
},
{
Sha: "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164",
Name: "fix logging",
Status: "pushed",
Action: "",
Tags: []string{},
ExtraInfo: "(origin/better-tests)",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640824515,
Parents: []string{
"e94e8fc5b6fab4cb755f",
},
},
{
Sha: "e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c",
Name: "refactor",
Status: "pushed",
Action: "",
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640823749,
Parents: []string{
"d8084cd558925eb7c9c3",
},
},
{
Sha: "d8084cd558925eb7c9c38afeed5725c21653ab90",
Name: "WIP",
Status: "pushed",
Action: "",
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640821426,
Parents: []string{
"65f910ebd85283b5cce9",
},
},
{
Sha: "65f910ebd85283b5cce9bf67d03d3f1a9ea3813a",
Name: "WIP",
Status: "pushed",
Action: "",
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640821275,
Parents: []string{
"26c07b1ab33860a1a759",
},
},
{
Sha: "26c07b1ab33860a1a7591a0638f9925ccf497ffa",
Name: "WIP",
Status: "merged",
Action: "",
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640750752,
Parents: []string{
"3d4470a6c072208722e5",
},
},
{
Sha: "3d4470a6c072208722e5ae9a54bcb9634959a1c5",
Name: "WIP",
Status: "merged",
Action: "",
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640748818,
Parents: []string{
"053a66a7be3da43aacdc",
},
},
{
Sha: "053a66a7be3da43aacdc7aa78e1fe757b82c4dd2",
Name: "refactoring the config struct",
Status: "merged",
Action: "",
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
AuthorEmail: "jessedduffield@gmail.com",
UnixTimestamp: 1640739815,
Parents: []string{
"985fe482e806b172aea4",
},
},
},
expectedError: nil,
},
}
for _, scenario := range scenarios {
scenario := scenario
t.Run(scenario.testName, func(t *testing.T) {
builder := &CommitLoader{
Common: utils.NewDummyCommon(),
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
getCurrentBranchName: func() (string, string, error) {
return scenario.currentBranchName, scenario.currentBranchName, nil
},
getRebaseMode: func() (enums.RebaseMode, error) { return scenario.rebaseMode, nil },
dotGitDir: ".git",
readFile: func(filename string) ([]byte, error) {
return []byte(""), nil
},
walkFiles: func(root string, fn filepath.WalkFunc) error {
return nil
},
}
commits, err := builder.GetCommits(scenario.opts)
assert.Equal(t, scenario.expectedCommits, commits)
assert.Equal(t, scenario.expectedError, err)
scenario.runner.CheckForMissingCalls()
})
}
}

View File

@@ -0,0 +1,123 @@
package loaders
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
)
type FileLoaderConfig interface {
GetShowUntrackedFiles() string
}
type FileLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
config FileLoaderConfig
getFileType func(string) string
}
func NewFileLoader(cmn *common.Common, cmd oscommands.ICmdObjBuilder, config FileLoaderConfig) *FileLoader {
return &FileLoader{
Common: cmn,
cmd: cmd,
getFileType: oscommands.FileType,
config: config,
}
}
type GetStatusFileOptions struct {
NoRenames bool
}
func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
// check if config wants us ignoring untracked files
untrackedFilesSetting := self.config.GetShowUntrackedFiles()
if untrackedFilesSetting == "" {
untrackedFilesSetting = "all"
}
untrackedFilesArg := fmt.Sprintf("--untracked-files=%s", untrackedFilesSetting)
statuses, err := self.GitStatus(GitStatusOptions{NoRenames: opts.NoRenames, UntrackedFilesArg: untrackedFilesArg})
if err != nil {
self.Log.Error(err)
}
files := []*models.File{}
for _, status := range statuses {
if strings.HasPrefix(status.StatusString, "warning") {
self.Log.Warningf("warning when calling git status: %s", status.StatusString)
continue
}
file := &models.File{
Name: status.Name,
PreviousName: status.PreviousName,
DisplayString: status.StatusString,
Type: self.getFileType(status.Name),
}
models.SetStatusFields(file, status.Change)
files = append(files, file)
}
return files
}
// GitStatus returns the file status of the repo
type GitStatusOptions struct {
NoRenames bool
UntrackedFilesArg string
}
type FileStatus struct {
StatusString string
Change string // ??, MM, AM, ...
Name string
PreviousName string
}
func (c *FileLoader) GitStatus(opts GitStatusOptions) ([]FileStatus, error) {
noRenamesFlag := ""
if opts.NoRenames {
noRenamesFlag = " --no-renames"
}
statusLines, err := c.cmd.New(fmt.Sprintf("git status %s --porcelain -z%s", opts.UntrackedFilesArg, noRenamesFlag)).DontLog().RunWithOutput()
if err != nil {
return []FileStatus{}, err
}
splitLines := strings.Split(statusLines, "\x00")
response := []FileStatus{}
for i := 0; i < len(splitLines); i++ {
original := splitLines[i]
if len(original) < 3 {
continue
}
status := FileStatus{
StatusString: original,
Change: original[:2],
Name: original[3:],
PreviousName: "",
}
if strings.HasPrefix(status.Change, "R") {
// if a line starts with 'R' then the next line is the original file.
status.PreviousName = splitLines[i+1]
status.StatusString = fmt.Sprintf("%s %s -> %s", status.Change, status.PreviousName, status.Name)
i++
}
response = append(response, status)
}
return response, nil
}

View File

@@ -0,0 +1,210 @@
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"
)
func TestFileGetStatusFiles(t *testing.T) {
type scenario struct {
testName string
runner oscommands.ICmdObjRunner
expectedFiles []*models.File
}
scenarios := []scenario{
{
"No files found",
oscommands.NewFakeRunner(t).
Expect(`git status --untracked-files=yes --porcelain -z`, "", nil),
[]*models.File{},
},
{
"Several files found",
oscommands.NewFakeRunner(t).
Expect(
`git status --untracked-files=yes --porcelain -z`,
"MM file1.txt\x00A file3.txt\x00AM file2.txt\x00?? file4.txt\x00UU file5.txt",
nil,
),
[]*models.File{
{
Name: "file1.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "MM file1.txt",
Type: "file",
ShortStatus: "MM",
},
{
Name: "file3.txt",
HasStagedChanges: true,
HasUnstagedChanges: false,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "A file3.txt",
Type: "file",
ShortStatus: "A ",
},
{
Name: "file2.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "AM file2.txt",
Type: "file",
ShortStatus: "AM",
},
{
Name: "file4.txt",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "?? file4.txt",
Type: "file",
ShortStatus: "??",
},
{
Name: "file5.txt",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: true,
HasInlineMergeConflicts: true,
DisplayString: "UU file5.txt",
Type: "file",
ShortStatus: "UU",
},
},
},
{
"File with new line char",
oscommands.NewFakeRunner(t).
Expect(`git status --untracked-files=yes --porcelain -z`, "MM a\nb.txt", nil),
[]*models.File{
{
Name: "a\nb.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "MM a\nb.txt",
Type: "file",
ShortStatus: "MM",
},
},
},
{
"Renamed files",
oscommands.NewFakeRunner(t).
Expect(
`git status --untracked-files=yes --porcelain -z`,
"R after1.txt\x00before1.txt\x00RM after2.txt\x00before2.txt",
nil,
),
[]*models.File{
{
Name: "after1.txt",
PreviousName: "before1.txt",
HasStagedChanges: true,
HasUnstagedChanges: false,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "R before1.txt -> after1.txt",
Type: "file",
ShortStatus: "R ",
},
{
Name: "after2.txt",
PreviousName: "before2.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "RM before2.txt -> after2.txt",
Type: "file",
ShortStatus: "RM",
},
},
},
{
"File with arrow in name",
oscommands.NewFakeRunner(t).
Expect(
`git status --untracked-files=yes --porcelain -z`,
`?? a -> b.txt`,
nil,
),
[]*models.File{
{
Name: "a -> b.txt",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "?? a -> b.txt",
Type: "file",
ShortStatus: "??",
},
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
cmd := oscommands.NewDummyCmdObjBuilder(s.runner)
loader := &FileLoader{
Common: utils.NewDummyCommon(),
cmd: cmd,
config: &FakeFileLoaderConfig{showUntrackedFiles: "yes"},
getFileType: func(string) string { return "file" },
}
assert.EqualValues(t, s.expectedFiles, loader.GetStatusFiles(GetStatusFileOptions{}))
})
}
}
type FakeFileLoaderConfig struct {
showUntrackedFiles string
}
func (self *FakeFileLoaderConfig) GetShowUntrackedFiles() string {
return self.showUntrackedFiles
}

View File

@@ -0,0 +1,77 @@
package loaders
import (
"fmt"
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
)
type ReflogCommitLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
}
func NewReflogCommitLoader(common *common.Common, cmd oscommands.ICmdObjBuilder) *ReflogCommitLoader {
return &ReflogCommitLoader{
Common: common,
cmd: cmd,
}
}
// GetReflogCommits only returns the new reflog commits since the given lastReflogCommit
// if none is passed (i.e. it's value is nil) then we get all the reflog commits
func (self *ReflogCommitLoader) GetReflogCommits(lastReflogCommit *models.Commit, filterPath string) ([]*models.Commit, bool, error) {
commits := make([]*models.Commit, 0)
filterPathArg := ""
if filterPath != "" {
filterPathArg = fmt.Sprintf(" --follow -- %s", self.cmd.Quote(filterPath))
}
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, "\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 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 {
onlyObtainedNewReflogCommits = true
// after this point we already have these reflogs loaded so we'll simply return the new ones
return true, nil
}
commits = append(commits, commit)
return false, nil
})
if err != nil {
return nil, false, err
}
return commits, onlyObtainedNewReflogCommits, nil
}

View File

@@ -0,0 +1,169 @@
package loaders
import (
"errors"
"strings"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sanity-io/litter"
"github.com/stretchr/testify/assert"
)
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 {
testName string
runner *oscommands.FakeCmdObjRunner
lastReflogCommit *models.Commit
filterPath string
expectedCommits []*models.Commit
expectedOnlyObtainedNew bool
expectedError error
}
scenarios := []scenario{
{
testName: "no reflog entries",
runner: oscommands.NewFakeRunner(t).
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p"`, "", nil),
lastReflogCommit: nil,
expectedCommits: []*models.Commit{},
expectedOnlyObtainedNew: false,
expectedError: nil,
},
{
testName: "some reflog entries",
runner: oscommands.NewFakeRunner(t).
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{
{
Sha: "c3c4b66b64c97ffeecde",
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,
expectedError: nil,
},
{
testName: "some reflog entries where last commit is given",
runner: oscommands.NewFakeRunner(t).
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{
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
},
expectedOnlyObtainedNew: true,
expectedError: nil,
},
{
testName: "when passing filterPath",
runner: oscommands.NewFakeRunner(t).
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{
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
},
expectedOnlyObtainedNew: true,
expectedError: nil,
},
{
testName: "when command returns error",
runner: oscommands.NewFakeRunner(t).
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p"`, "", errors.New("haha")),
lastReflogCommit: nil,
filterPath: "",
expectedCommits: nil,
expectedOnlyObtainedNew: false,
expectedError: errors.New("haha"),
},
}
for _, scenario := range scenarios {
scenario := scenario
t.Run(scenario.testName, func(t *testing.T) {
builder := &ReflogCommitLoader{
Common: utils.NewDummyCommon(),
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
}
commits, onlyObtainednew, err := builder.GetReflogCommits(scenario.lastReflogCommit, scenario.filterPath)
assert.Equal(t, scenario.expectedOnlyObtainedNew, onlyObtainednew)
assert.Equal(t, scenario.expectedError, err)
t.Logf("actual commits: \n%s", litter.Sdump(commits))
assert.Equal(t, scenario.expectedCommits, commits)
scenario.runner.CheckForMissingCalls()
})
}
}

View File

@@ -0,0 +1,77 @@
package loaders
import (
"fmt"
"regexp"
"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"
"github.com/jesseduffield/lazygit/pkg/common"
)
type RemoteLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
getGoGitRemotes func() ([]*gogit.Remote, error)
}
func NewRemoteLoader(
common *common.Common,
cmd oscommands.ICmdObjBuilder,
getGoGitRemotes func() ([]*gogit.Remote, error),
) *RemoteLoader {
return &RemoteLoader{
Common: common,
cmd: cmd,
getGoGitRemotes: getGoGitRemotes,
}
}
func (self *RemoteLoader) GetRemotes() ([]*models.Remote, error) {
remoteBranchesStr, err := self.cmd.New("git branch -r").DontLog().RunWithOutput()
if err != nil {
return nil, err
}
goGitRemotes, err := self.getGoGitRemotes()
if err != nil {
return nil, err
}
// first step is to get our remotes from go-git
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 := slices.Map(matches, func(match []string) *models.RemoteBranch {
return &models.RemoteBranch{
Name: match[1],
RemoteName: remoteName,
}
})
return &models.Remote{
Name: goGitRemote.Config().Name,
Urls: goGitRemote.Config().URLs,
Branches: branches,
}
})
// now lets sort our remotes by name alphabetically
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 a.Name == "origin" {
return true
}
if b.Name == "origin" {
return false
}
return strings.ToLower(a.Name) < strings.ToLower(b.Name)
})
return remotes, nil
}

View File

@@ -0,0 +1,79 @@
package loaders
import (
"regexp"
"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"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type StashLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
}
func NewStashLoader(
common *common.Common,
cmd oscommands.ICmdObjBuilder,
) *StashLoader {
return &StashLoader{
Common: common,
cmd: cmd,
}
}
func (self *StashLoader) GetStashEntries(filterPath string) []*models.StashEntry {
if filterPath == "" {
return self.getUnfilteredStashEntries()
}
rawString, err := self.cmd.New("git stash list --name-only").DontLog().RunWithOutput()
if err != nil {
return self.getUnfilteredStashEntries()
}
stashEntries := []*models.StashEntry{}
var currentStashEntry *models.StashEntry
lines := utils.SplitLines(rawString)
isAStash := func(line string) bool { return strings.HasPrefix(line, "stash@{") }
re := regexp.MustCompile(`stash@\{(\d+)\}`)
outer:
for i := 0; i < len(lines); i++ {
if !isAStash(lines[i]) {
continue
}
match := re.FindStringSubmatch(lines[i])
idx, err := strconv.Atoi(match[1])
if err != nil {
return self.getUnfilteredStashEntries()
}
currentStashEntry = self.stashEntryFromLine(lines[i], idx)
for i+1 < len(lines) && !isAStash(lines[i+1]) {
i++
if lines[i] == filterPath {
stashEntries = append(stashEntries, currentStashEntry)
continue outer
}
}
}
return stashEntries
}
func (self *StashLoader) getUnfilteredStashEntries() []*models.StashEntry {
rawString, _ := self.cmd.New("git stash list --pretty='%gs'").DontLog().RunWithOutput()
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 {
return &models.StashEntry{
Name: line,
Index: index,
}
}

View File

@@ -0,0 +1,60 @@
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"
)
func TestGetStashEntries(t *testing.T) {
type scenario struct {
testName string
filterPath string
runner oscommands.ICmdObjRunner
expectedStashEntries []*models.StashEntry
}
scenarios := []scenario{
{
"No stash entries found",
"",
oscommands.NewFakeRunner(t).
Expect(`git stash list --pretty='%gs'`, "", nil),
[]*models.StashEntry{},
},
{
"Several stash entries found",
"",
oscommands.NewFakeRunner(t).
Expect(
`git stash list --pretty='%gs'`,
"WIP on add-pkg-commands-test: 55c6af2 increase parallel build\nWIP on master: bb86a3f update github template",
nil,
),
[]*models.StashEntry{
{
Index: 0,
Name: "WIP on add-pkg-commands-test: 55c6af2 increase parallel build",
},
{
Index: 1,
Name: "WIP on master: bb86a3f update github template",
},
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
cmd := oscommands.NewDummyCmdObjBuilder(s.runner)
loader := NewStashLoader(utils.NewDummyCommon(), cmd)
assert.EqualValues(t, s.expectedStashEntries, loader.GetStashEntries(s.filterPath))
})
}
}

View File

@@ -0,0 +1,43 @@
package loaders
import (
"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/jesseduffield/lazygit/pkg/utils"
)
type TagLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
}
func NewTagLoader(
common *common.Common,
cmd oscommands.ICmdObjBuilder,
) *TagLoader {
return &TagLoader{
Common: common,
cmd: cmd,
}
}
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
tagsOutput, err := self.cmd.New(`git tag --list --sort=-creatordate`).DontLog().RunWithOutput()
if err != nil {
return nil, err
}
split := utils.SplitLines(tagsOutput)
tags := slices.Map(split, func(tagName string) *models.Tag {
return &models.Tag{
Name: tagName,
}
})
return tags, nil
}

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