Compare commits

...

782 Commits
v0.24 ... v0.34

Author SHA1 Message Date
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
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
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
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
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
Jesse Duffield
9989c96321 better formatting 2021-10-31 22:33:39 +11: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
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
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
4496 changed files with 126977 additions and 43291 deletions

View File

@@ -26,7 +26,7 @@ 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```)
- The last commit id if you built project from sources (run : ```git rev-parse HEAD```)
**Additional context**
Add any other context about the problem here.

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

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

View File

@@ -6,8 +6,8 @@ on:
- 'v*'
jobs:
cd:
runs-on: ubuntu-20.04
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
@@ -16,31 +16,16 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.14.x
go-version: 1.16.x
- name: Run goreleaser
uses: goreleaser/goreleaser-action@v1
env:
GITHUB_TOKEN: ${{secrets.GITHUB_API_TOKEN}}
- name: Bump Homebrew
homebrew:
runs-on: macos-latest
steps:
- name: Bump Homebrew formula
uses: dawidd6/action-homebrew-bump-formula@v3
with:
token: ${{secrets.GITHUB_API_TOKEN}}
formula: lazygit
- 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 update
sudo apt install -y git-buildpackage
git fetch --tags https://github.com/$GITHUB_REPOSITORY
gbp import-ref -u "$version"
gbp dch -D xenial -N "$version"-1
git add debian/changelog
git commit -m "d/changelog: dch $version"
gbp tag
git push --tags origin master

View File

@@ -8,7 +8,14 @@ on:
jobs:
ci:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- windows-latest
name: ci - ${{matrix.os}}
runs-on: ${{matrix.os}}
env:
GOFLAGS: -mod=vendor
steps:
@@ -17,24 +24,82 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.14.x
go-version: 1.16.x
- name: Cache build
uses: actions/cache@v1
with:
path: ~/.cache/go-build
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-test
restore-keys: |
${{runner.os}}-go-
- name: Test code
run: |
bash ./test.sh
build:
runs-on: ubuntu-latest
env:
GOFLAGS: -mod=vendor
GOARCH: amd64
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.16.x
- name: Cache build
uses: actions/cache@v1
with:
path: ~/.cache/go-build
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-build
restore-keys: |
${{runner.os}}-go-
- name: Build linux binary
run: |
GOOS=linux go build
- name: Build windows binary
run: |
GOOS=windows go build
- name: Build darwin binary
run: |
GOOS=darwin go build
check-cheatsheet:
runs-on: ubuntu-latest
env:
GOFLAGS: -mod=vendor
GOARCH: amd64
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.16.x
- name: Cache build
uses: actions/cache@v1
with:
path: ~/.cache/go-build
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-build
restore-keys: |
${{runner.os}}-go-
- name: Check Cheatsheet
run: |
go run scripts/cheatsheet/main.go check
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Lint
uses: golangci/golangci-lint-action@v2
with:
version: latest
- name: Format code
run: |
if [ $(find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;|wc -l) -gt 0 ]; then
find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;
exit 1
fi
- name: Test code
run: |
./test.sh
- name: Build binaries
uses: goreleaser/goreleaser-action@v1
with:
args: --skip-publish --snapshot
- name: errors
run: golangci-lint run
if: ${{ failure() }}

View File

@@ -1,14 +0,0 @@
name: Lint
on: pull_request
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Lint
uses: golangci/golangci-lint-action@v2
with:
version: latest

12
.gitignore vendored
View File

@@ -18,17 +18,21 @@ coverage.txt
# Binaries
lazygit
lazygit.exe
# Exceptions
!.gitignore
!.goreleaser.yml
!.circleci/
!.github/
# these are for our integration tests
!.git_keep
!.gitmodules_keep
test/git_server/data
test/integration/*/actual/
test/integration/*/actual_remote/
test/integration/*/used_config/
# these sample hooks waste too space space
test/integration/*/expected/.git_keep/hooks/
!.git_keep/
lazygit.exe
# these sample hooks waste too much space
test/integration/*/expected/**/hooks/
test/integration/*/expected_remote/**/hooks/

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,110 @@
# 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.
## 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.
## Internationalisation
Boy that's a hard word to spell. Anyway, lazygit is translated into several languages within the pkg/i18n package. If you need to render text to the user, you should add a new field to the TranslationSet struct in `pkg/i18n/english.go` and add the actual content within the `EnglishTranslationSet()` method in the same file. Although it is appreciated if you translate the text into other languages, it's not expected of you (google translate will likely do a bad job anyway!).
## 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 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!
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. Lazygit has its own integration test system where you can build a sandbox repo with a shell script, record yourself doing something, and commit the resulting repo snapshot. It's pretty damn cool! To learn more see [here](https://github.com/jesseduffield/lazygit/blob/master/docs/Integration_Tests.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,10 +2,7 @@
<img src="https://i.imgur.com/oYB7Cj8.png">
</p>
![CI](https://github.com/jesseduffield/lazygit/workflows/Continuous%20Integration/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/jesseduffield/lazygit)](https://goreportcard.com/report/github.com/jesseduffield/lazygit) [![GolangCI](https://golangci.com/badges/github.com/jesseduffield/lazygit.svg)](https://golangci.com) [![GoDoc](https://godoc.org/github.com/jesseduffield/lazygit?status.svg)](http://godoc.org/github.com/jesseduffield/lazygit) [![GitHub tag](https://img.shields.io/github/tag/jesseduffield/lazygit.svg)]() [![TODOs](https://badgen.net/https/api.tickgit.com/badgen/github.com/jesseduffield/lazygit)](https://www.tickgit.com/browse?repo=github.com/jesseduffield/lazygit)
![CI](https://github.com/jesseduffield/lazygit/workflows/Continuous%20Integration/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/jesseduffield/lazygit)](https://goreportcard.com/report/github.com/jesseduffield/lazygit) [![GoDoc](https://godoc.org/github.com/jesseduffield/lazygit?status.svg)](https://godoc.org/github.com/jesseduffield/lazygit) [![GitHub tag](https://img.shields.io/github/tag/jesseduffield/lazygit.svg)](https://github.com/jesseduffield/lazygit/releases) [![TODOs](https://badgen.net/https/api.tickgit.com/badgen/github.com/jesseduffield/lazygit)](https://www.tickgit.com/browse?repo=github.com/jesseduffield/lazygit)
A simple terminal UI for git commands, written in Go with the [gocui](https://github.com/jroimartin/gocui "gocui") library.
@@ -27,6 +24,7 @@ If you're a mere mortal like me and you're tired of hearing how powerful git is
- [Arch Linux](#arch-linux)
- [Fedora and CentOS 7](#fedora-and-centos-7)
- [Solus Linux](#solus-linux)
- [Funtoo Linux](#funtoo-linux)
- [FreeBSD](#freebsd)
- [Conda](#conda)
- [Go](#go)
@@ -53,7 +51,7 @@ Github Sponsors is matching all donations dollar-for-dollar for 12 months so if
### Binary Releases
For Windows, Mac OS(10.10+) or Linux, you can download a binary release [here](../../releases).
For Windows, Mac OS(10.12+) or Linux, you can download a binary release [here](../../releases).
### Homebrew
@@ -82,6 +80,8 @@ sudo port install lazygit
### Ubuntu
**Deprecated**: will no longer receive updates.
Packages for Ubuntu are available via [Launchpad PPA](https://launchpad.net/~lazygit-team).
```sh
@@ -114,12 +114,12 @@ scoop install lazygit
### Arch Linux
Packages for Arch Linux are available via AUR (Arch User Repository).
Packages for Arch Linux are available via pacman and AUR (Arch User Repository).
There are two packages. The stable one which is built with the latest release
and the git version which builds from the most recent commit.
- Stable: <https://aur.archlinux.org/packages/lazygit/>
- Stable: `sudo pacman -S lazygit`
- Development: <https://aur.archlinux.org/packages/lazygit-git/>
Instruction of how to install AUR content can be found here:
@@ -140,13 +140,20 @@ sudo dnf install lazygit
sudo eopkg install lazygit
```
### Funtoo Linux
Funtoo Linux has an autogenerated lazygit package in [dev-kit](https://github.com/funtoo/dev-kit/tree/1.4-release/dev-vcs/lazygit):
```sh
sudo emerge dev-vcs/lazygit
```
### FreeBSD
```sh
pkg install lazygit
```
### Conda
Released versions are available for different platforms, see <https://anaconda.org/conda-forge/lazygit>
@@ -158,14 +165,14 @@ conda install -c conda-forge lazygit
### Go
```sh
go get github.com/jesseduffield/lazygit
go install github.com/jesseduffield/lazygit@latest
```
Please note:
If you get an error claiming that lazygit cannot be found or is not defined, you
may need to add `~/go/bin` to your \$PATH (MacOS/Linux), or `%HOME%\go\bin`
(Windows). Not to be mistaked for `C:\Go\bin` (which is for Go's own binaries,
not apps like Lazygit).
may need to add `~/go/bin` to your $PATH (MacOS/Linux), or `%HOME%\go\bin`
(Windows). Not to be mistaken for `C:\Go\bin` (which is for Go's own binaries,
not apps like lazygit).
### Chocolatey (Windows)
@@ -221,7 +228,7 @@ lg()
}
```
Then `source ~/.zshrc` and from now on when you call `lg` and exit you'll switch directories to whatever you were in inside lazyigt. To override this behaviour you can exit using `shift+Q` rather than just `q`.
Then `source ~/.zshrc` and from now on when you call `lg` and exit you'll switch directories to whatever you were in inside lazygit. To override this behaviour you can exit using `shift+Q` rather than just `q`.
### Undo/Redo
@@ -247,7 +254,6 @@ See the [docs](docs/Custom_Command_Keybindings.md)
- [Rebase Magic Video Tutorial](https://youtu.be/4XaToVut_hs)
- [Twitch Stream](https://www.twitch.tv/jesseduffield)
## Cool features
- Adding files easily
@@ -255,7 +261,7 @@ See the [docs](docs/Custom_Command_Keybindings.md)
- Easily check out recent branches
- Scroll through logs/diffs of branches/commits/stash
- Quick pushing/pulling
- Squash down and rename commits
- Squash down and reword commits
### Resolving merge conflicts
@@ -273,18 +279,18 @@ For contributor discussion about things not better discussed here in the repo, j
[![Slack](../assets/slack_rgb.png)](https://join.slack.com/t/lazygit/shared_invite/zt-5bo2clzo-hB8ZTVN5dWUCqj5QFiQVLA)
### Debugging Locally
Run `lazygit --debug` in one terminal tab and `lazygit --logs` in another to view the program and its log output side by side
## Donate
If you would like to support the development of lazygit, consider [sponsoring me](https://github.com/sponsors/jesseduffield) (github is matching all donations dollar-for-dollar for 12 months)
## Work in progress
## FAQ
This is still a work in progress so there's still bugs to iron out and as this
is my first project in Go the code could no doubt use an increase in quality,
but I'll be improving on it whenever I find the time. If you have any feedback
feel free to [raise an issue](https://github.com/jesseduffield/lazygit/issues)/[submit a PR](https://github.com/jesseduffield/lazygit/pulls).
### I'm struggling to see the selected line
see [here](https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#struggling-to-see-selected-line)
## Social

View File

@@ -2,178 +2,226 @@
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}} --"
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
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
notARepository: 'prompt' # one of: 'prompt' | 'create' | 'skip'
keybinding:
universal:
quit: 'q'
quit-alt1: '<c-c>' # alternative/alias of quit
return: '<esc>' # return to previous menu, will quit if there's nowhere to return
quitWithoutChangingDirectory: 'Q'
togglePanel: '<tab>' # goto the next panel
prevItem: '<up>' # go one line up
nextItem: '<down>' # go one line down
prevItem-alt: 'k' # go one line up
nextItem-alt: 'j' # go one line down
prevPage: ',' # go to next page in list
nextPage: '.' # go to previous page in list
gotoTop: '<' # go to top of list
gotoBottom: '>' # go to bottom of list
prevBlock: '<left>' # goto the previous block / panel
nextBlock: '<right>' # goto the next block / panel
prevBlock-alt: 'h' # goto the previous block / panel
nextBlock-alt: 'l' # goto the next block / panel
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'
theme:
lightTheme: false # For terminals with a light background
activeBorderColor:
- white
- bold
inactiveBorderColor:
- green
optionsTextColor:
- blue
selectedLineBgColor:
- default
selectedRangeBgColor:
- blue
cherryPickedCommitBgColor:
- blue
cherryPickedCommitFgColor:
- cyan
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
showCommandLog: true
commandLogSize: 8
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'
skipHookPrefix: WIP
autoFetch: true
branchLogCmd: 'git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --'
allBranchesLogCmd: 'git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium'
overrideGpg: false # prevents lazygit from spawning a separate process when using GPG
disableForcePushing: false
parseEmoji: false
diffContextSize: 3 # how many lines of context are shown around a change in diffs
os:
editCommand: '' # see 'Configuring File Editing' section
editCommandTemplate: '{{editor}} {{filename}}'
openCommand: ''
refresher:
refreshInterval: 10 # file/submodule refresh interval in seconds
fetchInterval: 60 # re-fetch interval in seconds
update:
method: prompt # can be: prompt | background | never
days: 14 # how often an update is checked for
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
confirmOnQuit: false
# determines whether hitting 'esc' will quit the application when there is nothing to cancel/close
quitOnTopLevelReturn: false
disableStartupPopups: false
notARepository: 'prompt' # one of: 'prompt' | 'create' | 'skip'
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'
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
@@ -181,22 +229,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 a line number you are currently at when in the line-by-line mode.
```yaml
os:
editCommand: 'vim'
editCommandTemplate: '{{editor}} +{{line}} {{filename}}'
```
or
```yaml
os:
editCommand: 'code'
editCommandTemplate: '{{editor}} --goto {{filename}}:{{line}}'
```
`{{editor}}` in `editCommandTemplate` is replaced with the value of `editCommand`.
### Overriding default config file location
To override the default config directory, use `$CONFIG_DIR="~/.config/lazygit"`. This directory contains the config file in addition to some other files lazygit uses to keep track of state across sessions.
To override the individual config file used, use the `--use-config-file` arg or the `LG_CONFIG_FILE` env var.
If you want to merge a specific config file into a more general config file, perhaps for the sake of setting some theme-specific options, you can supply a list of comma-separated config file paths, like so:
```sh
lazygit --use-config-file=~/.base_lg_conf,~/.light_theme_lg_conf
or
LG_CONFIG_FILE="~/.base_lg_conf,~/.light_theme_lg_conf" lazygit
```
### Recommended Config Values
@@ -204,8 +303,8 @@ Default path for the config file:
for users of VSCode
```yaml
os:
openCommand: 'code -rg {{filename}}'
os:
openCommand: 'code -rg {{filename}}'
```
## Color Attributes
@@ -213,7 +312,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
@@ -222,7 +322,12 @@ The available attributes are:
- magenta
- cyan
- white
- '#ff00ff'
**Modifiers**
- bold
- default
- reverse # useful for high-contrast
- underline
@@ -231,29 +336,81 @@ 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
If you struggle to see the selected line I recomment using the reverse attribute on selected lines like so:
If you struggle to see the selected line I recommend using the reverse attribute on selected lines like so:
```yaml
gui:
theme:
selectedLineBgColor:
- reverse
selectedRangeBgColor:
- reverse
gui:
theme:
selectedLineBgColor:
- reverse
selectedRangeBgColor:
- reverse
```
The following has also worked for a couple of people:
```yaml
gui:
theme:
activeBorderColor:
- white
- bold
inactiveBorderColor:
- white
selectedLineBgColor:
- reverse
- blue
```
Alternatively you may have bold fonts disabled in your terminal, in which case enabling bold fonts should solve the problem.
If you're still having trouble please raise an issue.
## 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': '#ff0000' # use red for John Smith
```
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': '#ff0000'
# 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
@@ -267,33 +424,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
@@ -304,7 +461,7 @@ 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:
@@ -314,19 +471,21 @@ Where:
- `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

View File

@@ -35,6 +35,20 @@ customCommands:
command: "git flow {{index .PromptResponses 0}} start {{index .PromptResponses 1}}"
context: 'localBranches'
loadingText: 'creating branch'
- key : 'r'
description: 'Checkout a remote branch as FETCH_HEAD'
command: "git fetch {{index .PromptResponses 0}} {{index .PromptResponses 1}} && git checkout FETCH_HEAD"
context: 'remotes'
prompts:
- type: 'input'
title: 'Remote:'
initialValue: "{{index .SelectedRemote.Name }}"
- type: 'menuFromCommand'
title: 'Remote branch:'
command: 'git branch -r --list {{index .PromptResponses 0}}/*'
filter: '.*{{index .PromptResponses 0}}/(?P<branch>.*)'
valueFormat: '{{ .branch }}'
labelFormat: '{{ .branch | green }}'
```
Looking at the command assigned to the 'n' key, here's what the result looks like:
@@ -55,6 +69,7 @@ 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 |
### Contexts
@@ -79,12 +94,28 @@ The permitted contexts are:
The permitted prompt fields are:
| _field_ | _description_ | _required_ |
| ------------ | -------------------------------------------------------------------------------- | ---------- |
| type | one of 'input' or 'menu' | yes |
| title | the title to display in the popup panel | no |
| initialValue | (only applicable to 'input' prompts) the initial value to appear in the text box | no |
| options | (only applicable to 'menu' prompts) the options to display in the menu | no |
| _field_ | _description_ | _required_ |
| ------------ | -------------------------------------------------------------------------------- | ---------- |
| type | one of 'input' or 'menu' | yes |
| title | the title to display in the popup panel | no |
| initialValue | (only applicable to 'input' prompts) the initial value to appear in the text box | no |
| options | (only applicable to 'menu' prompts) the options to display in the menu | no |
| command | (only applicable to 'menuFromCommand' prompts) the command to run to generate | yes |
| | menu options | |
| filter | (only applicable to 'menuFromCommand' prompts) the regexp to run specifying | yes |
| | groups which are going to be kept from the command's output | |
| valueFormat | (only applicable to 'menuFromCommand' prompts) how to format matched groups from | yes |
| | the filter to construct a menu item's value (What gets appended to prompt | |
| | responses when the item is selected). You can use named groups, | |
| | or `{{ .group_GROUPID }}`. | |
| | PS: named groups keep first match only | |
| labelFormat | (only applicable to 'menuFromCommand' prompts) how to format matched groups from | no |
| | the filter to construct the item's label (What's shown on screen). You can use | |
| | named groups, or `{{ .group_GROUPID }}`. You can also color each match with | |
| | `{{ .group_GROUPID | colorname }}` (Color names from | |
| | [here](https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md)) | |
| | If `labelFormat` is not specified, `valueFormat` is shown instead. | |
| | PS: named groups keep first match only | |
The permitted option fields are:
| _field_ | _description_ | _required_ |
@@ -123,7 +154,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 `{{.SelectedBranch.Name}}`. In the future we will likely introduce a tighter interface that exposes a limited set of fields for each model.
### Keybinding collisions

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

@@ -33,45 +33,68 @@ git commit -am "myfile1"
## Running tests
To run all tests
### From a TUI
You can run/record/sandbox tests via a TUI with the following command:
```
go test pkg/gui/gui_test.go
go run test/lazyintegration/main.go
```
This TUI makes much of the following documentation redundant, but feel free to read through anyway!
### From command line
To run all tests - assuming you're at the project root:
```
go test ./pkg/gui/
```
To run them in parallel
```
PARALLEL=true go test pkg/gui/gui_test.go
PARALLEL=true go test ./pkg/gui
```
To run a single test
```
go test pkg/gui/gui_test.go -run /<test name>
go test ./pkg/gui -run /<test name>
# For example, to run the `tags` test:
go test ./pkg/gui -run /tags
```
To run a test at a certain speed
```
SPEED=2 go test pkg/gui/gui_test.go -run /<test name>
SPEED=2 go test ./pkg/gui -run /<test name>
```
To update a snapshot
```
UPDATE_SNAPSHOTS=true go test pkg/gui/gui_test.go -run /<test name>
MODE=updateSnapshot go test ./pkg/gui -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:
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>
MODE=record go test ./pkg/gui -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.
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)
@@ -83,6 +106,14 @@ recording.json
Feel free to create a hierarchy of directories in the `test/integration` directory to group tests by feature.
## Sandboxing
The integration tests serve a secondary purpose of providing a setup for easy sandboxing. If you want to run a test in sandbox mode (meaning the session won't be recorded and we won't create/update snapshots), go:
```
MODE=sandbox go test ./pkg/gui -run /<test name>
```
## Feedback
If you think this process can be improved, let me know! It shouldn't be too hard to change things.

View File

@@ -1,8 +1,11 @@
_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>ctrl+r</kbd>: switch to a recent repo (<c-r>)
<kbd>pgup</kbd>: scroll up main panel (fn+up)
<kbd>pgdown</kbd>: scroll down main panel (fn+down)
<kbd>m</kbd>: view merge/rebase options
@@ -16,21 +19,24 @@
<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
</pre>
## List Panel Navigation
<pre>
<kbd>.</kbd>: next page
<kbd>,</kbd>: previous page
<kbd><</kbd>: scroll to top
<kbd>></kbd>: scroll to bottom
<kbd>/</kbd>: start search
<kbd>]</kbd>: next tab
<kbd>[</kbd>: previous tab
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Branches Panel (Branches Tab)
@@ -38,6 +44,8 @@
<pre>
<kbd>space</kbd>: checkout
<kbd>o</kbd>: create pull request
<kbd>O</kbd>: create pull request options
<kbd>ctrl+y</kbd>: copy pull request URL to clipboard
<kbd>c</kbd>: checkout by name
<kbd>F</kbd>: force checkout
<kbd>n</kbd>: new branch
@@ -108,19 +116,21 @@
<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
<kbd>enter</kbd>: enter file to add selected lines to the patch (or toggle directory collapsed)
<kbd>`</kbd>: toggle file tree view
</pre>
## Commits Panel (Commits)
<pre>
<kbd>ctrl+l</kbd>: open log menu
<kbd>s</kbd>: squash down
<kbd>r</kbd>: reword commit
<kbd>R</kbd>: rename commit with editor
<kbd>R</kbd>: reword 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>S</kbd>: squash all 'fixup!' commits above selected commit (autosquash)
<kbd>d</kbd>: delete commit
<kbd>ctrl+j</kbd>: move commit down one
<kbd>ctrl+k</kbd>: move commit up one
@@ -137,6 +147,9 @@
<kbd>n</kbd>: create new branch off of commit
<kbd>T</kbd>: tag commit
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>ctrl+y</kbd>: copy commit message to clipboard
<kbd>o</kbd>: open commit in browser
<kbd>b</kbd>: view bisect options
</pre>
## Commits Panel (Reflog Tab)
@@ -151,6 +164,18 @@
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
</pre>
## Extras Panel
<pre>
<kbd>@</kbd>: open command log menu
</pre>
## Files Panel
<pre>
<kbd>ctrl+b</kbd>: Filter commit files
</pre>
## Files Panel (Files)
<pre>
@@ -168,10 +193,13 @@
<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>enter</kbd>: stage individual hunks/lines for file, or collapse/expand for directory
<kbd>f</kbd>: fetch
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
<kbd>g</kbd>: view upstream reset options
<kbd>`</kbd>: toggle file tree view
<kbd>M</kbd>: open external merge tool (git mergetool)
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
</pre>
## Files Panel (Submodules)
@@ -179,7 +207,7 @@
<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
@@ -190,21 +218,24 @@
## Main Panel (Merging)
<pre>
<kbd>H</kbd>: scroll left
<kbd>L</kbd>: scroll right
<kbd>esc</kbd>: return to files panel
<kbd>M</kbd>: open external merge tool (git mergetool)
<kbd>space</kbd>: pick hunk
<kbd>b</kbd>: pick both hunks
<kbd>b</kbd>: pick all hunks
<kbd>◄</kbd>: select previous conflict
<kbd>►</kbd>: select next conflict
<kbd>▲</kbd>: select top hunk
<kbd>▼</kbd>: select bottom hunk
<kbd>▲</kbd>: select previous hunk
<kbd>▼</kbd>: select next hunk
<kbd>z</kbd>: undo
</pre>
## Main Panel (Normal)
<pre>
<kbd></kbd>: scroll down (fn+up)
<kbd></kbd>: scroll up (fn+down)
<kbd>Ő</kbd>: scroll down (fn+up)
<kbd>ő</kbd>: scroll up (fn+down)
</pre>
## Main Panel (Patch Building)
@@ -216,10 +247,13 @@
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
<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
<kbd>H</kbd>: scroll left
<kbd>L</kbd>: scroll right
</pre>
## Main Panel (Staging)
@@ -234,11 +268,14 @@
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
<kbd>e</kbd>: edit file
<kbd>o</kbd>: open file
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
<kbd>H</kbd>: scroll left
<kbd>L</kbd>: scroll right
<kbd>c</kbd>: commit changes
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>C</kbd>: commit changes using git editor
@@ -267,4 +304,5 @@
<kbd>o</kbd>: open config file
<kbd>u</kbd>: check for update
<kbd>enter</kbd>: switch to a recent repo
<kbd>a</kbd>: show all branch logs
</pre>

View File

@@ -1,10 +1,13 @@
_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 (<c-r>)
<kbd>pgup</kbd>: scroll naar beneden vanaf hoofdpaneel (fn+up)
<kbd>pgdown</kbd>: scroll naar beneden vanaf hoofdpaneel (fn+down)
<kbd>m</kbd>: bekijk merge/rebase opties
<kbd>ctrl+p</kbd>: bekijk aangepaste patch opties
<kbd>P</kbd>: push
@@ -13,31 +16,36 @@
<kbd>x</kbd>: open menu
<kbd>z</kbd>: ongedaan maken (via reflog) (experimenteel)
<kbd>ctrl+z</kbd>: redo (via reflog) (experimenteel)
<kbd>+</kbd>: volgende schermmode (normaal/half/groot )
<kbd>_</kbd>: vorige schermmode
<kbd>:</kbd>: voor aangepast commando uit
<kbd>|</kbd>: bekijk scoping opties
<kbd>+</kbd>: volgende scherm modus (normaal/half/groot)
<kbd>_</kbd>: vorige scherm modus
<kbd>:</kbd>: voer aangepaste commando uit
<kbd>ctrl+s</kbd>: bekijk scoping opties
<kbd>W</kbd>: open diff menu
<kbd>ctrl+e</kbd>: open diff menu
<kbd>@</kbd>: open command log menu
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
</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>: vorige pagina
<kbd><</kbd>: scroll naar boven
<kbd>/</kbd>: start met zoekken
<kbd>></kbd>: scroll naar beneden
<kbd>/</kbd>: start met zoeken
<kbd>]</kbd>: volgende tabblad
<kbd>[</kbd>: vorige tabblad
</pre>
## Branches Paneel (Branches Tab)
## Branches Paneel (Branches Tabblad)
<pre>
<kbd>space</kbd>: uitchecken
<kbd>o</kbd>: maak een pull-aanvraag
<kbd>o</kbd>: maak een pull-request
<kbd>O</kbd>: bekijk opties voor pull-aanvraag
<kbd>ctrl+y</kbd>: kopieer de URL van het pull-verzoek naar het klembord
<kbd>c</kbd>: uitchecken bij naam
<kbd>F</kbd>: forceer checkout
<kbd>n</kbd>: nieuwe branch
@@ -48,16 +56,16 @@
<kbd>f</kbd>: fast-forward deze branch vanaf zijn upstream
<kbd>g</kbd>: bekijk reset opties
<kbd>R</kbd>: hernoem branch
<kbd>ctrl+o</kbd>: copieer branch name naar clipboard
<kbd>enter</kbd>: view commits
<kbd>ctrl+o</kbd>: kopieer branch name naar klembord
<kbd>enter</kbd>: bekijk commits
</pre>
## Branches Paneel (Remote Branches (in Remotes tab))
## Branches Paneel (Remote Branches (in Remotes tabblad))
<pre>
<kbd>esc</kbd>: Ga terug naar remotes lijst
<kbd>esc</kbd>: ga terug naar remotes lijst
<kbd>g</kbd>: bekijk reset opties
<kbd>enter</kbd>: view commits
<kbd>enter</kbd>: bekijk commits
<kbd>space</kbd>: uitchecken
<kbd>n</kbd>: nieuwe branch
<kbd>M</kbd>: merge in met huidige checked out branch
@@ -66,7 +74,7 @@
<kbd>u</kbd>: stel in als upstream van uitgecheckte branch
</pre>
## Branches Paneel (Remotes Tab)
## Branches Paneel (Remotes Tabblad)
<pre>
<kbd>f</kbd>: fetch remote
@@ -82,13 +90,13 @@
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: bekijk reset opties
<kbd>n</kbd>: nieuwe branch
<kbd>c</kbd>: kopiëer commit (cherry-pick)
<kbd>C</kbd>: kopiëer commit reeks (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (gecopieerde) commits selectie
<kbd>ctrl+o</kbd>: copieer commit SHA naar clipboard
<kbd>c</kbd>: kopieer commit (cherry-pick)
<kbd>C</kbd>: kopieer commit reeks (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
</pre>
## Branches Paneel (Tags Tab)
## Branches Paneel (Tags Tabblad)
<pre>
<kbd>space</kbd>: uitchecken
@@ -96,7 +104,7 @@
<kbd>P</kbd>: push tag
<kbd>n</kbd>: creëer tag
<kbd>g</kbd>: bekijk reset opties
<kbd>enter</kbd>: view commits
<kbd>enter</kbd>: bekijk commits
</pre>
## Commit bestanden Paneel
@@ -108,12 +116,14 @@
<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>enter</kbd>: enter bestand om geselecteerde regels toe te voegen aan de patch
<kbd>`</kbd>: toggle bestandsboom weergave
</pre>
## Commits Paneel (Commits)
<pre>
<kbd>ctrl+l</kbd>: open log menu
<kbd>s</kbd>: squash beneden
<kbd>r</kbd>: hernoem commit
<kbd>R</kbd>: hernoem commit met editor
@@ -128,33 +138,48 @@
<kbd>A</kbd>: wijzig commit met staged veranderingen
<kbd>p</kbd>: kies commit (wanneer midden in rebase)
<kbd>t</kbd>: commit ongedaan maken
<kbd>c</kbd>: kopiëer commit (cherry-pick)
<kbd>ctrl+o</kbd>: copieer commit SHA naar clipboard
<kbd>C</kbd>: kopiëer commit reeks (cherry-pick)
<kbd>c</kbd>: kopieer commit (cherry-pick)
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
<kbd>C</kbd>: kopieer commit reeks (cherry-pick)
<kbd>v</kbd>: plak commits (cherry-pick)
<kbd>enter</kbd>: bekijk gecommite bestanden
<kbd>space</kbd>: checkout commit
<kbd>n</kbd>: create new branch off of commit
<kbd>n</kbd>: creëer nieuwe branch van commit
<kbd>T</kbd>: tag commit
<kbd>ctrl+r</kbd>: reset cherry-picked (gecopieerde) commits selectie
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
<kbd>ctrl+y</kbd>: kopieer commit bericht naar klembord
<kbd>o</kbd>: open commit in browser
<kbd>b</kbd>: view bisect options
</pre>
## Commits Paneel (Reflog Tab)
## Commits Paneel (Reflog Tabblad)
<pre>
<kbd>enter</kbd>: bekijk gecommite bestanden
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: bekijk reset opties
<kbd>c</kbd>: kopiëer commit (cherry-pick)
<kbd>C</kbd>: kopiëer commit reeks (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (gecopieerde) commits selectie
<kbd>ctrl+o</kbd>: copieer commit SHA naar clipboard
<kbd>c</kbd>: kopieer commit (cherry-pick)
<kbd>C</kbd>: kopieer commit reeks (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
</pre>
## Extras Paneel
<pre>
<kbd>@</kbd>: open command log menu
</pre>
## Bestanden Paneel
<pre>
<kbd>ctrl+b</kbd>: Commit dossiers filteren
</pre>
## Bestanden Paneel (Bestanden)
<pre>
<kbd>c</kbd>: Commit veranderingen
<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
@@ -172,25 +197,31 @@
<kbd>f</kbd>: fetch
<kbd>ctrl+o</kbd>: kopieer de bestandsnaam naar het klembord
<kbd>g</kbd>: bekijk upstream reset opties
<kbd>`</kbd>: toggle bestandsboom weergave
<kbd>M</kbd>: open external merge tool (git mergetool)
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
</pre>
## Bestanden Paneel (Submodules)
<pre>
<kbd>ctrl+o</kbd>: copy submodule name to clipboard
<kbd>ctrl+o</kbd>: kopieer submodule naam naar klembord
<kbd>enter</kbd>: enter submodule
<kbd>d</kbd>: view reset and remove submodule options
<kbd>d</kbd>: remove submodule
<kbd>u</kbd>: update submodule
<kbd>n</kbd>: add new submodule
<kbd>n</kbd>: voeg nieuwe submodule toe
<kbd>e</kbd>: update submodule URL
<kbd>i</kbd>: initialize submodule
<kbd>b</kbd>: view bulk submodule options
<kbd>i</kbd>: initialiseer submodule
<kbd>b</kbd>: bekijk bulk submodule opties
</pre>
## Hooft Paneel (Merggen)
## Hoofd Paneel (Mergen)
<pre>
<kbd>H</kbd>: scroll left
<kbd>L</kbd>: scroll right
<kbd>esc</kbd>: ga terug naar het bestanden paneel
<kbd>M</kbd>: open external merge tool (git mergetool)
<kbd>space</kbd>: kies hunk
<kbd>b</kbd>: kies bijde hunks
<kbd>◄</kbd>: selecteer voorgaand conflict
@@ -200,29 +231,32 @@
<kbd>z</kbd>: ongedaan maken
</pre>
## Hooft Paneel (Normaal)
## Hoofd Paneel (Normaal)
<pre>
<kbd></kbd>: scroll omlaag (fn+up)
<kbd></kbd>: scroll omhoog (fn+down)
<kbd>Ő</kbd>: scroll omlaag (fn+up)
<kbd>ő</kbd>: scroll omhoog (fn+down)
</pre>
## Hooft Paneel (Patch Bouwen)
## Hoofd Paneel (Patch Bouwen)
<pre>
<kbd>esc</kbd>: sluit lijn-bij-lijn mode
<kbd>esc</kbd>: sluit lijn-bij-lijn modus
<kbd>o</kbd>: open bestand
<kbd>▲</kbd>: selecteer de vorige lijn
<kbd>▼</kbd>: selecteer de volgende lijn
<kbd>◄</kbd>: selecteer de vorige hunk
<kbd>►</kbd>: selecteer de volgende hunk
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
<kbd>space</kbd>: voeg toe/verwijder lijn(en) in patch
<kbd>v</kbd>: toggle drag selecteer
<kbd>V</kbd>: toggle drag selecteer
<kbd>a</kbd>: toggle selecteer hunk
<kbd>v</kbd>: toggle drag selecteer
<kbd>V</kbd>: toggle drag selecteer
<kbd>a</kbd>: toggle selecteer hunk
<kbd>H</kbd>: scroll left
<kbd>L</kbd>: scroll right
</pre>
## Hooft Paneel (Staging)
## Hoofd Paneel (Staging)
<pre>
<kbd>esc</kbd>: ga terug naar het bestanden paneel
@@ -234,12 +268,15 @@
<kbd>▼</kbd>: selecteer de volgende lijn
<kbd>◄</kbd>: selecteer de vorige hunk
<kbd>►</kbd>: selecteer de volgende hunk
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
<kbd>e</kbd>: verander bestand
<kbd>o</kbd>: open bestand
<kbd>v</kbd>: toggle drag selecteer
<kbd>V</kbd>: toggle drag selecteer
<kbd>a</kbd>: toggle selecteer hunk
<kbd>c</kbd>: Commit veranderingen
<kbd>v</kbd>: toggle drag selecteer
<kbd>V</kbd>: toggle drag selecteer
<kbd>a</kbd>: toggle selecteer hunk
<kbd>H</kbd>: scroll left
<kbd>L</kbd>: scroll right
<kbd>c</kbd>: commit veranderingen
<kbd>w</kbd>: commit veranderingen zonder pre-commit hook
<kbd>C</kbd>: commit veranderingen met de git editor
</pre>
@@ -253,7 +290,7 @@
## Stash Paneel
<pre>
<kbd>enter</kbd>: view stash entry's files
<kbd>enter</kbd>: bekijk bestanden van stash entry
<kbd>space</kbd>: toepassen
<kbd>g</kbd>: pop
<kbd>d</kbd>: laten vallen
@@ -267,4 +304,5 @@
<kbd>o</kbd>: open config bestand
<kbd>u</kbd>: check voor updates
<kbd>enter</kbd>: wissel naar een recente repo
<kbd>a</kbd>: alle logs van de branch laten zien
</pre>

View File

@@ -1,11 +1,14 @@
_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>ctrl+r</kbd>: switch to a recent repo (<c-r>)
<kbd>pgup</kbd>: scroll up main panel (fn+up)
<kbd>pgdown</kbd>: scroll down main panel (fn+down)
<kbd>m</kbd>: view merge/rebase options
<kbd>m</kbd>: widok scalenia/opcje zmiany bazy
<kbd>ctrl+p</kbd>: view custom patch options
<kbd>P</kbd>: push
<kbd>p</kbd>: pull
@@ -15,38 +18,43 @@
<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>:</kbd>: wykonaj własną komendę
<kbd>ctrl+s</kbd>: view filter-by-path options
<kbd>W</kbd>: open diff menu
<kbd>ctrl+e</kbd>: open diff menu
<kbd>@</kbd>: open command log menu
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
</pre>
## List Panel Navigation
<pre>
<kbd>.</kbd>: next page
<kbd>,</kbd>: previous page
<kbd><</kbd>: scroll to top
<kbd>></kbd>: scroll to bottom
<kbd>/</kbd>: start search
<kbd>]</kbd>: next tab
<kbd>[</kbd>: previous tab
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Gałęzie Panel (Branches Tab)
<pre>
<kbd>space</kbd>: przełącz
<kbd>o</kbd>: utwórz żądanie wyciągnięcia
<kbd>o</kbd>: utwórz żą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>enter</kbd>: view commits
@@ -55,14 +63,14 @@
## Gałęzie Panel (Remote Branches (in Remotes tab))
<pre>
<kbd>esc</kbd>: return to remotes list
<kbd>g</kbd>: view reset options
<kbd>esc</kbd>: wróć do listy repozytoriów zdalnych
<kbd>g</kbd>: wyświetl opcje resetu
<kbd>enter</kbd>: view commits
<kbd>space</kbd>: przełącz
<kbd>n</kbd>: nowa gałąź
<kbd>M</kbd>: scal do obecnej gałęzi
<kbd>d</kbd>: usuń gałąź
<kbd>r</kbd>: rebase branch
<kbd>r</kbd>: zmiana bazy gałęzi
<kbd>u</kbd>: set as upstream of checked-out branch
</pre>
@@ -78,12 +86,12 @@
## Gałęzie Panel (Sub-commits)
<pre>
<kbd>enter</kbd>: view commit's files
<kbd>enter</kbd>: przeglądaj pliki commita
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: view reset options
<kbd>g</kbd>: wyświetl opcje resetu
<kbd>n</kbd>: nowa gałąź
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>c</kbd>: kopiuj commit (przebieranie)
<kbd>C</kbd>: kopiuj zakres commitów (przebieranie)
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
</pre>
@@ -95,83 +103,103 @@
<kbd>d</kbd>: delete tag
<kbd>P</kbd>: push tag
<kbd>n</kbd>: create tag
<kbd>g</kbd>: view reset options
<kbd>g</kbd>: wyświetl opcje resetu
<kbd>enter</kbd>: view commits
</pre>
## Commit files Panel
## Pliki commita 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>c</kbd>: plik wybierania
<kbd>d</kbd>: porzuć zmiany commita dla tego pliku
<kbd>o</kbd>: otwórz plik
<kbd>e</kbd>: edytuj plik
<kbd>space</kbd>: toggle file included in patch
<kbd>enter</kbd>: enter file to add selected lines to the patch
<kbd>enter</kbd>: enter file to add selected lines to the patch (or toggle directory collapsed)
<kbd>`</kbd>: toggle file tree view
</pre>
## Commity Panel (Commity)
<pre>
<kbd>s</kbd>: ściśnij w dół
<kbd>r</kbd>: przemianuj commit
<kbd>R</kbd>: przemianuj commit w edytorze
<kbd>ctrl+l</kbd>: open log menu
<kbd>s</kbd>: ściśnij
<kbd>r</kbd>: zmień nazwę commita
<kbd>R</kbd>: zmień nazwę commita w edytorze
<kbd>g</kbd>: zresetuj do tego commita
<kbd>f</kbd>: napraw commit
<kbd>F</kbd>: 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>F</kbd>: utwórz commit naprawczy dla tego commita
<kbd>S</kbd>: spłaszcz wszystkie commity naprawcze powyżej zaznaczonych commitów (autosquash)
<kbd>d</kbd>: usuń commit
<kbd>ctrl+j</kbd>: przenieś commit 1 w dół
<kbd>ctrl+k</kbd>: przenieś commit 1 w górę
<kbd>e</kbd>: edytuj commit
<kbd>A</kbd>: popraw commit zmianami z poczekalni
<kbd>p</kbd>: wybierz commit (podczas zmiany bazy)
<kbd>t</kbd>: odwróć commit
<kbd>c</kbd>: kopiuj commit (przebieranie)
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>v</kbd>: paste commits (cherry-pick)
<kbd>enter</kbd>: view commit's files
<kbd>C</kbd>: kopiuj zakres commitów (przebieranie)
<kbd>v</kbd>: wklej commity (przebieranie)
<kbd>enter</kbd>: przeglądaj pliki commita
<kbd>space</kbd>: checkout commit
<kbd>n</kbd>: create new branch off of commit
<kbd>T</kbd>: tag commit
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>ctrl+y</kbd>: copy commit message to clipboard
<kbd>o</kbd>: open commit in browser
<kbd>b</kbd>: view bisect options
</pre>
## Commity Panel (Reflog Tab)
<pre>
<kbd>enter</kbd>: view commit's files
<kbd>enter</kbd>: przeglądaj pliki commita
<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>g</kbd>: wyświetl opcje resetu
<kbd>c</kbd>: kopiuj commit (przebieranie)
<kbd>C</kbd>: kopiuj zakres commitów (przebieranie)
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
</pre>
## Extras Panel
<pre>
<kbd>@</kbd>: open command log menu
</pre>
## Pliki Panel
<pre>
<kbd>ctrl+b</kbd>: Filtrowanie commitów
</pre>
## Pliki Panel (Pliki)
<pre>
<kbd>c</kbd>: 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>c</kbd>: Zatwierdź zmiany
<kbd>w</kbd>: zatwierdź zmiany bez skryptu pre-commit
<kbd>A</kbd>: Zmień ostatni commit
<kbd>C</kbd>: Zatwierdź zmiany używając edytora
<kbd>space</kbd>: przełącz stan poczekalni
<kbd>d</kbd>: pokaż opcje porzucania zmian
<kbd>e</kbd>: edytuj plik
<kbd>o</kbd>: otwórz plik
<kbd>i</kbd>: dodaj do .gitignore
<kbd>r</kbd>: odśwież pliki
<kbd>s</kbd>: przechowaj pliki
<kbd>S</kbd>: view stash options
<kbd>a</kbd>: przełącz wszystkie zatwierdzenia
<kbd>D</kbd>: view reset options
<kbd>s</kbd>: przechowaj zmiany
<kbd>S</kbd>: wyświetl opcje schowka
<kbd>a</kbd>: przełącz stan poczekalni wszystkich
<kbd>D</kbd>: wyświetl opcje resetu
<kbd>enter</kbd>: zatwierdź pojedyncze linie
<kbd>f</kbd>: fetch
<kbd>f</kbd>: pobierz
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
<kbd>g</kbd>: view upstream reset options
<kbd>`</kbd>: toggle file tree view
<kbd>M</kbd>: open external merge tool (git mergetool)
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
</pre>
## Pliki Panel (Submodules)
@@ -179,7 +207,7 @@
<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,42 +215,48 @@
<kbd>b</kbd>: view bulk submodule options
</pre>
## Main Panel (Merging)
## Główne Panel (Scalanie)
<pre>
<kbd>H</kbd>: scroll left
<kbd>L</kbd>: scroll right
<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>M</kbd>: open external merge tool (git mergetool)
<kbd>space</kbd>: wybierz kawałek
<kbd>b</kbd>: wybierz wszystkie kawałki
<kbd></kbd>: poprzedni konflikt
<kbd></kbd>: następny konflikt
<kbd></kbd>: wybierz poprzedni kawałek
<kbd>▼</kbd>: wybierz następny kawałek
<kbd>z</kbd>: cofnij
</pre>
## Main Panel (Normal)
## Główne Panel (Zwykłe)
<pre>
<kbd></kbd>: scroll down (fn+up)
<kbd></kbd>: scroll up (fn+down)
<kbd>Ő</kbd>: przewiń w dół (fn+up)
<kbd>ő</kbd>: przewiń w górę (fn+down)
</pre>
## Main Panel (Patch Building)
## Główne Panel (Patch Building)
<pre>
<kbd>esc</kbd>: exit line-by-line mode
<kbd>esc</kbd>: wyście z trybu "linia po linii"
<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>▲</kbd>: poprzednia linia
<kbd>▼</kbd>: następna linia
<kbd>◄</kbd>: poprzedni kawałek
<kbd>►</kbd>: następny kawałek
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
<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
<kbd>H</kbd>: scroll left
<kbd>L</kbd>: scroll right
</pre>
## Main Panel (Zatwierdzanie)
## Główne Panel (Poczekalnia)
<pre>
<kbd>esc</kbd>: wróć do panelu plików
@@ -230,18 +264,21 @@
<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>▲</kbd>: poprzednia linia
<kbd>▼</kbd>: następna linia
<kbd>◄</kbd>: poprzedni kawałek
<kbd>►</kbd>: następny kawałek
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
<kbd>e</kbd>: edytuj plik
<kbd>o</kbd>: otwórz plik
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
<kbd>c</kbd>: commituj zmiany
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>C</kbd>: commituj zmiany używając edytora z gita
<kbd>H</kbd>: scroll left
<kbd>L</kbd>: scroll right
<kbd>c</kbd>: Zatwierdź zmiany
<kbd>w</kbd>: zatwierdź zmiany bez skryptu pre-commit
<kbd>C</kbd>: Zatwierdź zmiany używając edytora
</pre>
## Menu Panel
@@ -263,8 +300,9 @@
## Status Panel
<pre>
<kbd>e</kbd>: edytuj plik konfiguracyjny
<kbd>o</kbd>: otwórz plik konfiguracyjny
<kbd>e</kbd>: edytuj konfigurac
<kbd>o</kbd>: otwórz konfigurac
<kbd>u</kbd>: sprawdź aktualizacje
<kbd>enter</kbd>: switch to a recent repo
<kbd>a</kbd>: pokaż wszystkie logi gałęzi
</pre>

View File

@@ -0,0 +1,308 @@
_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>: 切换到最近的仓库 (<c-r>)
<kbd>pgup</kbd>: 向上滚动主面板 (fn+up)
<kbd>pgdown</kbd>: 向下滚动主面板 (fn+down)
<kbd>m</kbd>: 查看 合并/变基 选项
<kbd>ctrl+p</kbd>: 查看自定义补丁选项
<kbd>P</kbd>: 推送
<kbd>p</kbd>: 拉取
<kbd>R</kbd>: 刷新
<kbd>x</kbd>: 打开菜单
<kbd>z</kbd>: (通过 reflog撤销「实验功能」
<kbd>ctrl+z</kbd>: (通过 reflog重做「实验功能」
<kbd>+</kbd>: 下一屏模式(正常/半屏/全屏)
<kbd>_</kbd>: 上一屏模式
<kbd>:</kbd>: 执行自定义命令
<kbd>ctrl+s</kbd>: 查看按路径过滤选项
<kbd>W</kbd>: 打开 diff 菜单
<kbd>ctrl+e</kbd>: 打开 diff 菜单
<kbd>@</kbd>: 打开命令日志菜单
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
</pre>
## 列表面板导航
<pre>
<kbd>.</kbd>: 下一页
<kbd>,</kbd>: 上一页
<kbd><</kbd>: 滚动到顶部
<kbd>></kbd>: 滚动到底部
<kbd>/</kbd>: 开始搜索
<kbd>]</kbd>: 下一个标签
<kbd>[</kbd>: 上一个标签
</pre>
## 分支 面板 (分支标签)
<pre>
<kbd>space</kbd>: 检出
<kbd>o</kbd>: 创建抓取请求
<kbd>O</kbd>: 创建抓取请求选项
<kbd>ctrl+y</kbd>: 将抓取请求 URL 复制到剪贴板
<kbd>c</kbd>: 按名称检出
<kbd>F</kbd>: 强制检出
<kbd>n</kbd>: 新分支
<kbd>d</kbd>: 删除分支
<kbd>r</kbd>: 将已检出的分支变基到该分支
<kbd>M</kbd>: 合并到当前检出的分支
<kbd>i</kbd>: 显示 git-flow 选项
<kbd>f</kbd>: 从上游快进此分支
<kbd>g</kbd>: 查看重置选项
<kbd>R</kbd>: 重命名分支
<kbd>ctrl+o</kbd>: 将分支名称复制到剪贴板
<kbd>enter</kbd>: 查看提交
</pre>
## 分支 面板 (远程分支(在远程页面中))
<pre>
<kbd>esc</kbd>: 返回远程仓库列表
<kbd>g</kbd>: 查看重置选项
<kbd>enter</kbd>: 查看提交
<kbd>space</kbd>: 检出
<kbd>n</kbd>: 新分支
<kbd>M</kbd>: 合并到当前检出的分支
<kbd>d</kbd>: 删除分支
<kbd>r</kbd>: 将已检出的分支变基到该分支
<kbd>u</kbd>: 设置为检出分支的上游
</pre>
## 分支 面板 (远程页面)
<pre>
<kbd>f</kbd>: 抓取远程仓库
<kbd>n</kbd>: 添加新的远程仓库
<kbd>d</kbd>: 删除远程
<kbd>e</kbd>: 编辑远程仓库
</pre>
## 分支 面板 (子提交)
<pre>
<kbd>enter</kbd>: 查看提交的文件
<kbd>space</kbd>: 检出提交
<kbd>g</kbd>: 查看重置选项
<kbd>n</kbd>: 新分支
<kbd>c</kbd>: 复制提交(拣选)
<kbd>C</kbd>: 复制提交范围(拣选)
<kbd>ctrl+r</kbd>: 重置已拣选(复制)的提交
<kbd>ctrl+o</kbd>: 将提交的 SHA 复制到剪贴板
</pre>
## 分支 面板 (标签页面)
<pre>
<kbd>space</kbd>: 检出
<kbd>d</kbd>: 删除标签
<kbd>P</kbd>: 推送标签
<kbd>n</kbd>: 创建标签
<kbd>g</kbd>: 查看重置选项
<kbd>enter</kbd>: 查看提交
</pre>
## 提交文件 面板
<pre>
<kbd>ctrl+o</kbd>: 将提交的文件名复制到剪贴板
<kbd>c</kbd>: 检出文件
<kbd>d</kbd>: 放弃对此文件的提交更改
<kbd>o</kbd>: 打开文件
<kbd>e</kbd>: 编辑文件
<kbd>space</kbd>: 补丁中包含的切换文件
<kbd>enter</kbd>: 输入文件以将所选行添加到补丁中(或切换目录折叠)
<kbd>`</kbd>: 切换文件树视图
</pre>
## 提交 面板 (提交)
<pre>
<kbd>ctrl+l</kbd>: open log menu
<kbd>s</kbd>: 向下压缩
<kbd>r</kbd>: 改写提交
<kbd>R</kbd>: 使用编辑器重命名提交
<kbd>g</kbd>: 重置为此提交
<kbd>f</kbd>: 修正提交fixup
<kbd>F</kbd>: 为此提交创建修正
<kbd>S</kbd>: 压缩在所选提交之上的所有“fixup!”提交(自动压缩)
<kbd>d</kbd>: 删除提交
<kbd>ctrl+j</kbd>: 下移提交
<kbd>ctrl+k</kbd>: 上移提交
<kbd>e</kbd>: 编辑提交
<kbd>A</kbd>: 用已暂存的更改来修补提交
<kbd>p</kbd>: 选择提交(变基过程中)
<kbd>t</kbd>: 还原提交
<kbd>c</kbd>: 复制提交(拣选)
<kbd>ctrl+o</kbd>: 将提交的 SHA 复制到剪贴板
<kbd>C</kbd>: 复制提交范围(拣选)
<kbd>v</kbd>: 粘贴提交(拣选)
<kbd>enter</kbd>: 查看提交的文件
<kbd>space</kbd>: 检出提交
<kbd>n</kbd>: 从提交创建新分支
<kbd>T</kbd>: 标签提交
<kbd>ctrl+r</kbd>: 重置已拣选(复制)的提交
<kbd>ctrl+y</kbd>: 将提交消息复制到剪贴板
<kbd>o</kbd>: open commit in browser
<kbd>b</kbd>: view bisect options
</pre>
## 提交 面板 (Reflog)
<pre>
<kbd>enter</kbd>: 查看提交的文件
<kbd>space</kbd>: 检出提交
<kbd>g</kbd>: 查看重置选项
<kbd>c</kbd>: 复制提交(拣选)
<kbd>C</kbd>: 复制提交范围(拣选)
<kbd>ctrl+r</kbd>: 重置已拣选(复制)的提交
<kbd>ctrl+o</kbd>: 将提交的 SHA 复制到剪贴板
</pre>
## Extras 面板
<pre>
<kbd>@</kbd>: 打开命令日志菜单
</pre>
## 文件 面板
<pre>
<kbd>ctrl+b</kbd>: 过滤提交文件
</pre>
## 文件 面板 (文件)
<pre>
<kbd>c</kbd>: 提交更改
<kbd>w</kbd>: 提交更改而无需预先提交钩子
<kbd>A</kbd>: 修补最后一次提交
<kbd>C</kbd>: 提交更改(使用编辑器编辑提交信息)
<kbd>space</kbd>: 切换暂存状态
<kbd>d</kbd>: 查看'放弃更改‘选项
<kbd>e</kbd>: 编辑文件
<kbd>o</kbd>: 打开文件
<kbd>i</kbd>: 添加到 .gitignore
<kbd>r</kbd>: 刷新文件
<kbd>s</kbd>: 将所有更改加入贮藏
<kbd>S</kbd>: 查看隐藏选项
<kbd>a</kbd>: 切换所有文件的暂存状态
<kbd>D</kbd>: 查看重置选项
<kbd>enter</kbd>: 暂存单个 块/行 用于文件, 或 折叠/展开 目录
<kbd>f</kbd>: 抓取
<kbd>ctrl+o</kbd>: 将文件名复制到剪贴板
<kbd>g</kbd>: 查看上游重置选项
<kbd>`</kbd>: 切换文件树视图
<kbd>M</kbd>: 打开合并工具
<kbd>ctrl+w</kbd>: 切换是否在差异视图中显示空白更改
</pre>
## 文件 面板 (子模块)
<pre>
<kbd>ctrl+o</kbd>: 将子模块名称复制到剪贴板
<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>H</kbd>: scroll left
<kbd>L</kbd>: scroll right
<kbd>esc</kbd>: 返回文件面板
<kbd>M</kbd>: 打开合并工具
<kbd>space</kbd>: 选中区块
<kbd>b</kbd>: 选中所有区块
<kbd>◄</kbd>: 选择上一个冲突
<kbd>►</kbd>: 选择下一个冲突
<kbd>▲</kbd>: 选择顶部块
<kbd>▼</kbd>: 选择底部块
<kbd>z</kbd>: 撤销
</pre>
## 主要 面板 (正常)
<pre>
<kbd>Ő</kbd>: 向下滚动 (fn+up)
<kbd>ő</kbd>: 向上滚动 (fn+down)
</pre>
## 主要 面板 (构建补丁中)
<pre>
<kbd>esc</kbd>: 退出逐行模式
<kbd>o</kbd>: 打开文件
<kbd>▲</kbd>: 选择上一行
<kbd>▼</kbd>: 选择下一行
<kbd>◄</kbd>: 选择上一个区块
<kbd>►</kbd>: 选择下一个区块
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
<kbd>space</kbd>: 添加/移除 行到补丁
<kbd>v</kbd>: 切换拖动选择
<kbd>V</kbd>: 切换拖动选择
<kbd>a</kbd>: 切换选择区块
<kbd>H</kbd>: scroll left
<kbd>L</kbd>: scroll right
</pre>
## 主要 面板 (正在暂存)
<pre>
<kbd>esc</kbd>: 返回文件面板
<kbd>space</kbd>: 切换行暂存状态
<kbd>d</kbd>: 取消变更 (git reset)
<kbd>tab</kbd>: 切换到其他面板
<kbd>o</kbd>: 打开文件
<kbd>▲</kbd>: 选择上一行
<kbd>▼</kbd>: 选择下一行
<kbd>◄</kbd>: 选择上一个区块
<kbd>►</kbd>: 选择下一个区块
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
<kbd>e</kbd>: 编辑文件
<kbd>o</kbd>: 打开文件
<kbd>v</kbd>: 切换拖动选择
<kbd>V</kbd>: 切换拖动选择
<kbd>a</kbd>: 切换选择区块
<kbd>H</kbd>: scroll left
<kbd>L</kbd>: scroll right
<kbd>c</kbd>: 提交更改
<kbd>w</kbd>: 提交更改而无需预先提交钩子
<kbd>C</kbd>: 提交更改(使用编辑器编辑提交信息)
</pre>
## 菜单 面板
<pre>
<kbd>esc</kbd>: 关闭菜单
</pre>
## 贮藏 面板
<pre>
<kbd>enter</kbd>: 查看贮藏条目中的文件
<kbd>space</kbd>: 应用
<kbd>g</kbd>: 应用并删除
<kbd>d</kbd>: 删除
<kbd>n</kbd>: 新分支
</pre>
## 状态 面板
<pre>
<kbd>e</kbd>: 编辑配置文件
<kbd>o</kbd>: 打开配置文件
<kbd>u</kbd>: 检查更新
<kbd>enter</kbd>: 切换到最近的仓库
<kbd>a</kbd>: 显示所有分支的日志
</pre>

34
go.mod
View File

@@ -6,37 +6,45 @@ 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/fatih/color v1.9.0 // indirect
github.com/fsnotify/fsnotify v1.4.7
github.com/go-errors/errors v1.1.1
github.com/gdamore/tcell/v2 v2.4.1-0.20220313203054-2a1a1b586447 // indirect
github.com/go-errors/errors v1.4.1
github.com/go-logfmt/logfmt v0.5.0 // indirect
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 // indirect
github.com/golang/protobuf v1.3.2 // indirect
github.com/google/go-cmp v0.3.1 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/gookit/color v1.4.2
github.com/imdario/mergo v0.3.11
github.com/integrii/flaggy v1.4.0
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.20220108045521-1945d7b9ed8b
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
github.com/jesseduffield/yaml v2.1.0+incompatible
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/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-colorable v0.1.11 // indirect
github.com/mattn/go-runewidth v0.0.13
github.com/mgutz/str v1.2.0
github.com/onsi/ginkgo v1.10.3 // indirect
github.com/onsi/gomega v1.7.1 // indirect
github.com/pmezard/go-difflib v1.0.0
github.com/sahilm/fuzzy v0.1.0
github.com/sanity-io/litter v1.5.2
github.com/sirupsen/logrus v1.4.2
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
github.com/stretchr/testify v1.4.0
github.com/tcnksm/go-gitconfig v0.1.2
github.com/stretchr/testify v1.7.0
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 // indirect
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c // indirect
golang.org/x/sys v0.0.0-20201005172224-997123666555 // indirect
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0
)
replace github.com/go-git/go-git/v5 => github.com/jesseduffield/go-git/v5 v5.1.1

100
go.sum
View File

@@ -11,17 +11,19 @@ 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=
@@ -29,31 +31,42 @@ github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjr
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.4.0 h1:W6dxJEmaxYvhICFoTY3WrLLEXsQ11SaFnKGVEXW57KM=
github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
github.com/gdamore/tcell/v2 v2.4.1-0.20210926162909-66f061b1fc9b h1:eoaSI4eEwM5eTx/HvmRSwmicxuMhL73AyoEfM1oCJLc=
github.com/gdamore/tcell/v2 v2.4.1-0.20210926162909-66f061b1fc9b/go.mod h1:ZPwXnysybtQqdqKcWMWXux9aGdtMHe+kr+cwEZEe+A4=
github.com/gdamore/tcell/v2 v2.4.1-0.20220313203054-2a1a1b586447 h1:4idf9699cuWAc7ZIB+2RzuDWU30oRkB0X/FZTUlWOVY=
github.com/gdamore/tcell/v2 v2.4.1-0.20220313203054-2a1a1b586447/go.mod h1:I8YJFI9gzgl4dHi9UlRDZosCW+jYkDA37AXmXvL51w4=
github.com/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.1 h1:IvVlgbzSsaUNudsw5dcXSzF3EWyXTi5XrAdngnuhRyg=
github.com/go-errors/errors v1.4.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM=
github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12 h1:PbKy9zOy4aAKrJ5pibIRpVO2BXnK1Tlcg+caKI7Ox5M=
github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 h1:zN2lZNZRflqFyxVaTIU61KNKQ9C0055u9CAfpmqUvo4=
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3/go.mod h1:nPpo7qLxd6XL3hWJG/O60sR8ZKfMCiIoNap5GvD12KU=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
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=
@@ -63,10 +76,10 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4 h1:GOQrmaE8i+KEdB8NzAegKYd4tPn/inM0I1uo0NXFerg=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
github.com/jesseduffield/gocui v0.3.1-0.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.20220108045521-1945d7b9ed8b h1:AUK5nDiPiaahBtGIsf8rITgZ9SC+uddvnNKs0/mrYA8=
github.com/jesseduffield/gocui v0.3.1-0.20220108045521-1945d7b9ed8b/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
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,28 +92,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=
@@ -114,10 +129,16 @@ github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
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 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/sanity-io/litter v1.5.2 h1:AnC8s9BMORWH5a4atZ4D6FPVvKGzHcnc5/IVTa87myw=
github.com/sanity-io/litter v1.5.2/go.mod h1:5Z71SvaYy5kcGtyglXOC9rrUi3c1E8CamFWjQsazTh0=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
@@ -127,24 +148,25 @@ 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/urfave/cli v1.20.1-0.20180226030253-8e01ec4cd3e2/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c h1:dk0ukUIHmGHqASjP0iue2261isepFCC6XRCSd1nHgDw=
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c/go.mod h1:iQL9McJNjoIa5mjH6nYTCTZXUN6RP+XW3eib7Ya3XcI=
@@ -157,24 +179,34 @@ 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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211113001501-0c823b97ae02/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.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 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 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=
@@ -183,3 +215,5 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

18
main.go
View File

@@ -12,6 +12,7 @@ import (
"github.com/integrii/flaggy"
"github.com/jesseduffield/lazygit/pkg/app"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/constants"
"github.com/jesseduffield/lazygit/pkg/env"
yaml "github.com/jesseduffield/yaml"
)
@@ -60,6 +61,9 @@ func main() {
gitDir := ""
flaggy.String(&gitDir, "g", "git-dir", "equivalent of the --git-dir git argument")
customConfig := ""
flaggy.String(&customConfig, "ucf", "use-config-file", "Comma seperated list to custom config file(s)")
flaggy.Parse()
if repoPath != "" {
@@ -67,8 +71,16 @@ func main() {
log.Fatal("--path option is incompatible with the --work-tree and --git-dir options")
}
workTree = repoPath
gitDir = filepath.Join(repoPath, ".git")
absRepoPath, err := filepath.Abs(repoPath)
if err != nil {
log.Fatal(err)
}
workTree = absRepoPath
gitDir = filepath.Join(absRepoPath, ".git")
}
if customConfig != "" {
os.Setenv("LG_CONFIG_FILE", customConfig)
}
if useConfigDir != "" {
@@ -134,6 +146,6 @@ func main() {
stackTrace := newErr.ErrorStack()
app.Log.Error(stackTrace)
log.Fatal(fmt.Sprintf("%s\n\n%s", app.Tr.ErrorOccurred, stackTrace))
log.Fatal(fmt.Sprintf("%s: %s\n\n%s", app.Tr.ErrorOccurred, constants.Links.Issues, stackTrace))
}
}

View File

@@ -8,7 +8,6 @@ import (
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
@@ -16,7 +15,9 @@ import (
"github.com/aybabtme/humanlog"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/env"
"github.com/jesseduffield/lazygit/pkg/gui"
@@ -27,14 +28,11 @@ import (
// App struct
type App struct {
closers []io.Closer
*common.Common
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
}
@@ -44,7 +42,7 @@ type errorMapping struct {
newError string
}
func newProductionLogger(config config.AppConfigurer) *logrus.Logger {
func newProductionLogger() *logrus.Logger {
log := logrus.New()
log.Out = ioutil.Discard
log.SetLevel(logrus.ErrorLevel)
@@ -60,7 +58,7 @@ func getLogLevel() logrus.Level {
return level
}
func newDevelopmentLogger(configurer config.AppConfigurer) *logrus.Logger {
func newDevelopmentLogger() *logrus.Logger {
logger := logrus.New()
logger.SetLevel(getLogLevel())
logPath, err := config.LogPath()
@@ -69,7 +67,7 @@ func newDevelopmentLogger(configurer config.AppConfigurer) *logrus.Logger {
}
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
panic("unable to log to file") // TODO: don't panic (also, remove this call to the `panic` function)
log.Fatalf("Unable to log to log file: %v", err)
}
logger.SetOutput(file)
return logger
@@ -78,9 +76,9 @@ func newDevelopmentLogger(configurer config.AppConfigurer) *logrus.Logger {
func newLogger(config config.AppConfigurer) *logrus.Entry {
var log *logrus.Logger
if config.GetDebug() || os.Getenv("DEBUG") == "TRUE" {
log = newDevelopmentLogger(config)
log = newDevelopmentLogger()
} else {
log = newProductionLogger(config)
log = newProductionLogger()
}
// highly recommended: tail -f development.log | humanlog
@@ -97,13 +95,25 @@ func newLogger(config config.AppConfigurer) *logrus.Entry {
// NewApp bootstrap a new application
func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
userConfig := config.GetUserConfig()
app := &App{
closers: []io.Closer{},
Config: config,
}
var err error
app.Log = newLogger(config)
app.Tr = i18n.NewTranslationSet(app.Log)
log := newLogger(config)
tr, err := i18n.NewTranslationSetFromConfig(log, userConfig.Gui.Language)
if err != nil {
return app, err
}
app.Common = &common.Common{
Log: log,
Tr: tr,
UserConfig: userConfig,
Debug: config.GetDebug(),
}
// if we are being called in 'demon' mode, we can just return here
app.ClientContext = os.Getenv("LAZYGIT_CLIENT_COMMAND")
@@ -111,9 +121,9 @@ func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
return app, nil
}
app.OSCommand = oscommands.NewOSCommand(app.Log, config)
app.OSCommand = oscommands.NewOSCommand(app.Common, oscommands.GetPlatform(), oscommands.NewNullGuiIO(log))
app.Updater, err = updates.NewUpdater(app.Log, config, app.OSCommand, app.Tr)
app.Updater, err = updates.NewUpdater(app.Common, config, app.OSCommand)
if err != nil {
return app, err
}
@@ -123,12 +133,9 @@ 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)
if err != nil {
return app, err
}
gitConfig := git_config.NewStdCachedGitConfig(app.Log)
app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config, app.Updater, filterPath, showRecentRepos)
app.Gui, err = gui.NewGui(app.Common, config, gitConfig, app.Updater, filterPath, showRecentRepos)
if err != nil {
return app, err
}
@@ -136,7 +143,7 @@ func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
}
func (app *App) validateGitVersion() error {
output, err := app.OSCommand.RunCommandWithOutput("git --version")
output, err := app.OSCommand.Cmd.New("git --version").RunWithOutput()
// if we get an error anywhere here we'll show the same status
minVersionError := errors.New(app.Tr.MinGitVersionError)
if err != nil {
@@ -177,12 +184,12 @@ func (app *App) setupRepo() (bool, error) {
}
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
@@ -193,7 +200,7 @@ func (app *App) setupRepo() (bool, error) {
}
shouldInitRepo := true
notARepository := app.Config.GetUserConfig().NotARepository
notARepository := app.UserConfig.NotARepository
if notARepository == "prompt" {
// Offer to initialize a new repository in current directory.
fmt.Print(app.Tr.CreateRepo)
@@ -221,10 +228,11 @@ func (app *App) setupRepo() (bool, error) {
os.Exit(1)
}
if err := app.OSCommand.RunCommand("git init"); err != nil {
if err := app.OSCommand.Cmd.New("git init").Run(); err != nil {
return false, err
}
}
return false, nil
}
@@ -237,7 +245,7 @@ func (app *App) Run() error {
os.Exit(0)
}
err := app.Gui.RunWithSubprocesses()
err := app.Gui.RunAndHandleError()
return err
}
@@ -316,6 +324,9 @@ func TailLogs() {
fmt.Printf("Tailing log file %s\n\n", logFilePath)
opts := humanlog.DefaultOptions
opts.Truncates = false
_, err = os.Stat(logFilePath)
if err != nil {
if os.IsNotExist(err) {
@@ -324,22 +335,5 @@ func TailLogs() {
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)
TailLogsForPlatform(logFilePath, opts)
}

View File

@@ -36,6 +36,7 @@ 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)

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

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

View File

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

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

@@ -0,0 +1,79 @@
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 occured while checking if cheatsheets are up to date: %v", err)
}
err = os.Mkdir(tmpDir, 0700)
if err != nil {
log.Fatalf("Error occured 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 occured while checking if cheatsheets are up to date: %v", err)
}
fmt.Printf("\nCheatsheets are out of date. Please run `%s` at the project root and commit the changes\n", CommandToRun())
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 occured 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 occured while checking if cheatsheets are up to date: %v", err)
}
return content
}

View File

@@ -4,20 +4,21 @@
// The content of this generated file is a keybindings cheatsheet.
//
// To generate cheatsheet in english run:
// LANG=en go run scripts/generate_cheatsheet.go
// go run scripts/generate_cheatsheet.go
package main
package cheatsheet
import (
"fmt"
"log"
"os"
"sort"
"strings"
"github.com/jesseduffield/lazygit/pkg/app"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/integration"
)
type bindingSection struct {
@@ -25,24 +26,39 @@ type bindingSection struct {
bindings []*gui.Binding
}
func main() {
langs := []string{"pl", "nl", "en"}
mConfig, _ := config.NewAppConfig("", "", "", "", "", true)
func CommandToRun() string {
return "go run scripts/cheatsheet/main.go generate"
}
for _, lang := range langs {
os.Setenv("LC_ALL", lang)
func GetDir() string {
return integration.GetRootDirectory() + "/docs/keybindings"
}
func generateAtDir(cheatsheetDir string) {
translationSetsByLang := i18n.GetTranslationSets()
mConfig := config.NewDummyAppConfig()
for lang := range translationSetsByLang {
mConfig.GetUserConfig().Gui.Language = lang
mApp, _ := app.NewApp(mConfig, "")
file, err := os.Create(getProjectRoot() + "/docs/keybindings/Keybindings_" + lang + ".md")
path := cheatsheetDir + "/Keybindings_" + lang + ".md"
file, err := os.Create(path)
if err != nil {
panic(err)
}
bindingSections := getBindingSections(mApp)
content := formatSections(mApp, bindingSections)
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 {
@@ -81,6 +97,8 @@ func localisedTitle(mApp *app.App, str string) string {
"search": tr.SearchTitle,
"secondary": tr.SecondaryTitle,
"stash": tr.StashTitle,
"suggestions": tr.SuggestionsCheatsheetTitle,
"extras": tr.ExtrasTitle,
}
title, ok := contextTitleMap[str]
@@ -229,8 +247,8 @@ func addBinding(title string, bindingSections []*bindingSection, binding *gui.Bi
return append(bindingSections, section)
}
func formatSections(mApp *app.App, bindingSections []*bindingSection) string {
content := fmt.Sprintf("# Lazygit %s\n", mApp.Tr.Keybindings)
func formatSections(tr *i18n.TranslationSet, bindingSections []*bindingSection) string {
content := fmt.Sprintf("# Lazygit %s\n", tr.Keybindings)
for _, section := range bindingSections {
content += formatTitle(section.title)
@@ -243,11 +261,3 @@ func formatSections(mApp *app.App, bindingSections []*bindingSection) string {
return content
}
func getProjectRoot() string {
dir, err := os.Getwd()
if err != nil {
panic(err)
}
return strings.Split(dir, "lazygit")[0] + "lazygit"
}

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,55 +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 {
value, _ := c.getLocalGitConfig(key)
// we get an error if the key doesn't exist which we don't care about
if value != "" {
return value
}
value, _ = c.getGlobalGitConfig(key)
return value
}

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,291 +0,0 @@
package commands
import (
"fmt"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/mgutz/str"
)
// 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()
}
// EditFile opens a file in a subprocess using whatever editor is available,
// falling back to core.editor, VISUAL, EDITOR, then vi
func (c *GitCommand) EditFile(filename string) (*exec.Cmd, error) {
editor, _ := c.getGlobalGitConfig("core.editor")
if editor == "" {
editor = c.OSCommand.Getenv("VISUAL")
}
if editor == "" {
editor = c.OSCommand.Getenv("EDITOR")
}
if editor == "" {
if err := c.OSCommand.RunCommand("which vi"); err == nil {
editor = "vi"
}
}
if editor == "" {
return nil, errors.New("No editor defined in $VISUAL, $EDITOR, or git config")
}
splitCmd := str.ToArgv(fmt.Sprintf("%s %s", editor, c.OSCommand.Quote(filename)))
return c.OSCommand.PrepareSubProcess(splitCmd[0], splitCmd[1:]...), nil
}

View File

@@ -9,62 +9,60 @@ import (
"github.com/go-errors/errors"
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
// 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,
) (*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.PlainOpen, cmn.Tr.GitconfigParseErr)
if err != nil {
return nil, err
}
@@ -73,26 +71,79 @@ 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,
), 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,
) *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)
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)
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,
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 +174,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")
}
}
}
@@ -192,3 +255,7 @@ func findDotGitDir(stat func(string) (os.FileInfo, error), readFile func(filenam
}
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,103 @@
package git_commands
import "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
}
for _, status := range self.statusMap {
if status == BisectStatusOld {
return true
}
}
return false
}

View File

@@ -0,0 +1,168 @@
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) 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)|%(refname:short)|%(upstream:short)|%(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,125 @@
package git_commands
import (
"fmt"
"strings"
"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()
}
// 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:], "\n")
return strings.TrimSpace(message), 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,163 @@
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)
})
}
}

View File

@@ -0,0 +1,34 @@
package git_commands
import (
gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
)
type GitCommon struct {
*common.Common
cmd oscommands.ICmdObjBuilder
os *oscommands.OSCommand
dotGitDir string
repo *gogit.Repository
config *ConfigCommands
}
func NewGitCommon(
cmn *common.Common,
cmd oscommands.ICmdObjBuilder,
osCommand *oscommands.OSCommand,
dotGitDir string,
repo *gogit.Repository,
config *ConfigCommands,
) *GitCommon {
return &GitCommon{
Common: cmn,
cmd: cmd,
os: osCommand,
dotGitDir: dotGitDir,
repo: repo,
config: config,
}
}

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,73 @@
package git_commands
import (
"io/ioutil"
"strconv"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/config"
"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 editCmdTemplate == config.DefaultEditCommandTemplate {
switch editor {
case "emacs", "nano", "vi", "vim":
editCmdTemplate = "{{editor}} +{{line}} {{filename}}"
case "subl":
editCmdTemplate = "{{editor}} {{filename}}:{{line}}"
case "code":
editCmdTemplate = "{{editor}} -r --goto {{filename}}:{{line}}"
}
}
return utils.ResolvePlaceholderString(editCmdTemplate, templateValues), nil
}

View File

@@ -0,0 +1,164 @@
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 +1 "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 +1 "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 +1 "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 +1 "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 +1 "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 +1 "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)
},
},
}
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,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
todo := ""
for i, commit := range commits[0:baseIndex] {
a := "pick"
if i == sourceCommitIdx || i == destinationCommitIdx {
a = "edit"
}
todo = a + " " + commit.Sha + " " + commit.Name + "\n" + todo
}
err := self.rebase.PrepareInteractiveRebaseCommand(commits[baseIndex].Sha, todo, true).Run()
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,345 @@
package git_commands
import (
"fmt"
"io/ioutil"
"path/filepath"
"strings"
"github.com/go-errors/errors"
"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.GenerateGenericRebaseTodo(commits, index, "reword")
if err != nil {
return nil, err
}
return self.PrepareInteractiveRebaseCommand(sha, todo, false), nil
}
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)
}
todo := ""
orderedCommits := append(commits[0:index], commits[index+1], commits[index])
for _, commit := range orderedCommits {
todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo
}
return self.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todo, true).Run()
}
func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, index int, action string) error {
todo, sha, err := self.GenerateGenericRebaseTodo(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, todo string, overrideEditor bool) oscommands.ICmdObj {
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).Info("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(
"LAZYGIT_CLIENT_COMMAND=INTERACTIVE_REBASE",
"LAZYGIT_REBASE_TODO="+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
}
func (self *RebaseCommands) GenerateGenericRebaseTodo(commits []*models.Commit, actionIndex int, action string) (string, string, error) {
baseIndex := actionIndex + 1
if len(commits) <= baseIndex {
return "", "", errors.New(self.Tr.CannotRebaseOntoFirstCommit)
}
if action == "squash" || action == "fixup" {
baseIndex++
if len(commits) <= baseIndex {
return "", "", errors.New(self.Tr.CannotSquashOntoSecondCommit)
}
}
todo := ""
for i, commit := range commits[0:baseIndex] {
var commitAction string
if i == actionIndex {
commitAction = action
} else if commit.IsMerge() {
// your typical interactive rebase will actually drop merge commits by default. Damn git CLI, you scary!
// doing this means we don't need to worry about rebasing over merges which always causes problems.
// you typically shouldn't be doing rebases that pass over merge commits anyway.
commitAction = "drop"
} else {
commitAction = "pick"
}
todo = commitAction + " " + commit.Sha + " " + commit.Name + "\n" + todo
}
return todo, 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), 0644)
}
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), 0644)
}
// 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.GenerateGenericRebaseTodo(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, "", 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(
"LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY",
"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 {
todo := ""
for _, commit := range commits {
todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo
}
return self.PrepareInteractiveRebaseCommand("HEAD", todo, false).Run()
}

View File

@@ -0,0 +1,151 @@
package git_commands
import (
"regexp"
"testing"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/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=.*$`,
"^LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY$",
} {
regexStr := regexStr
foundMatch := utils.IncludesStringFunc(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().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,91 @@
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) 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
// TODO: before calling this, check if there is anything to save
func (self *StashCommands) Save(message string) error {
return self.cmd.New("git stash save " + self.cmd.Quote(message)).Run()
}
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()
}
// 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,61 @@
package git_commands
import (
"path/filepath"
gogit "github.com/jesseduffield/go-git/v5"
"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
}
// IsInMergeState states whether we are still mid-merge
func (self *StatusCommands) IsInMergeState() (bool, error) {
return self.os.FileExists(filepath.Join(self.dotGitDir, "MERGE_HEAD"))
}
func (self *StatusCommands) IsBareRepo() bool {
// note: could use `git rev-parse --is-bare-repository` if we wanna drop go-git
_, err := self.repo.Worktree()
return err == gogit.ErrIsBareRepository
}

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()
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.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().Run()
}
func (self *SyncCommands) FastForward(branchName string, remoteName string, remoteBranchName string) error {
cmdStr := fmt.Sprintf("git fetch %s %s:%s", self.cmd.Quote(remoteName), self.cmd.Quote(remoteBranchName), self.cmd.Quote(branchName))
return self.cmd.New(cmdStr).PromptOnCredentialRequest().Run()
}
func (self *SyncCommands) FetchRemote(remoteName string) error {
cmdStr := fmt.Sprintf("git fetch %s", self.cmd.Quote(remoteName))
return self.cmd.New(cmdStr).PromptOnCredentialRequest().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().Run()
}

View File

@@ -0,0 +1,356 @@
package git_commands
import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/go-errors/errors"
"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 := make([]string, len(paths))
for i, path := range paths {
quotedPaths[i] = 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)
}
// 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())
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"
}
cmdStr := fmt.Sprintf("git diff --submodule --no-ext-diff --unified=%d --color=%s%s%s %s %s", contextSize, colorArg, ignoreWhitespaceArg, cachedArg, trackedArg, quotedPath)
return self.cmd.New(cmdStr).DontLog()
}
func (self *WorkingTreeCommands) ApplyPatch(patch string, flags ...string) error {
filepath := filepath.Join(oscommands.GetTempDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch")
self.Log.Infof("saving temporary patch to %s", filepath)
if err := self.os.CreateFileWithContent(filepath, patch); err != nil {
return err
}
flagStr := ""
for _, flag := range flags {
flagStr += " --" + flag
}
return self.cmd.New(fmt.Sprintf("git apply%s %s", flagStr, self.cmd.Quote(filepath))).Run()
}
// 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,80 @@
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}}"
// 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,
}
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: defaultUrlRegexStrings,
repoURLTemplate: defaultRepoURLTemplate,
}
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,
}
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}}",
}
var serviceDefinitions = []ServiceDefinition{
githubServiceDef,
bitbucketServiceDef,
gitLabServiceDef,
azdoServiceDef,
}
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,184 @@
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"
)
// 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
}
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
}
return &Service{
repoURL: repoURL,
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
}
var serviceDomains = make([]ServiceDomain, len(defaultServiceDomains))
copy(serviceDomains, 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 := []string{}
for _, serviceDefinition := range serviceDefinitions {
providerNames = append(providerNames, 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
}
func (self ServiceDefinition) getRepoURLFromRemoteURL(url string, webDomain string) (string, error) {
for _, regexStr := range self.regexStrings {
re := regexp.MustCompile(regexStr)
input := utils.FindNamedMatches(re, url)
if input != nil {
input["webDomain"] = webDomain
return utils.ResolvePlaceholderString(self.repoURLTemplate, input), nil
}
}
return "", errors.New("Failed to parse repo information from url")
}
type Service struct {
repoURL 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,236 @@
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 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 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 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 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 specifc 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 specifc 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: "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"},
},
{
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

@@ -1,12 +1,13 @@
package commands
package loaders
import (
"regexp"
"strings"
"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"
"github.com/sirupsen/logrus"
)
// context:
@@ -20,83 +21,37 @@ import (
// if we find out we need to use one of these functions in the git.go file, we
// can just pull them out of here and put them there and then call them from in here
// BranchListBuilder returns a list of Branch objects for the current repo
type BranchListBuilder struct {
Log *logrus.Entry
GitCommand *GitCommand
ReflogCommits []*models.Commit
type BranchLoaderConfigCommands interface {
Branches() (map[string]*config.Branch, error)
}
// NewBranchListBuilder builds a new branch list builder
func NewBranchListBuilder(log *logrus.Entry, gitCommand *GitCommand, reflogCommits []*models.Commit) (*BranchListBuilder, error) {
return &BranchListBuilder{
Log: log,
GitCommand: gitCommand,
ReflogCommits: reflogCommits,
}, nil
// 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 (b *BranchListBuilder) obtainBranches() []*models.Branch {
cmdStr := `git for-each-ref --sort=-committerdate --format="%(HEAD)|%(refname:short)|%(upstream:short)|%(upstream:track)" refs/heads`
output, err := b.GitCommand.OSCommand.RunCommandWithOutput(cmdStr)
if err != nil {
panic(err)
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,
}
trimmedOutput := strings.TrimSpace(output)
outputLines := strings.Split(trimmedOutput, "\n")
branches := make([]*models.Branch, 0, len(outputLines))
for _, line := range outputLines {
if line == "" {
continue
}
split := strings.Split(line, SEPARATION_CHAR)
name := strings.TrimPrefix(split[1], "heads/")
branch := &models.Branch{
Name: name,
Pullables: "?",
Pushables: "?",
Head: split[0] == "*",
}
upstreamName := split[2]
if upstreamName == "" {
branches = append(branches, branch)
continue
}
branch.UpstreamName = upstreamName
track := split[3]
re := regexp.MustCompile(`ahead (\d+)`)
match := re.FindStringSubmatch(track)
if len(match) > 1 {
branch.Pushables = match[1]
} else {
branch.Pushables = "0"
}
re = regexp.MustCompile(`behind (\d+)`)
match = re.FindStringSubmatch(track)
if len(match) > 1 {
branch.Pullables = match[1]
} else {
branch.Pullables = "0"
}
branches = append(branches, branch)
}
return branches
}
// Build the list of branches for the current repo
func (b *BranchListBuilder) Build() []*models.Branch {
branches := b.obtainBranches()
// Load the list of branches for the current repo
func (self *BranchLoader) Load(reflogCommits []*models.Commit) ([]*models.Branch, error) {
branches := self.obtainBranches()
reflogBranches := b.obtainReflogBranches()
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)
@@ -128,22 +83,98 @@ outer:
}
}
if !foundHead {
currentBranchName, currentBranchDisplayName, err := b.GitCommand.CurrentBranchName()
currentBranchName, currentBranchDisplayName, err := self.getCurrentBranchName()
if err != nil {
panic(err)
return nil, err
}
branches = append([]*models.Branch{{Name: currentBranchName, DisplayName: currentBranchDisplayName, Head: true, Recency: " *"}}, branches...)
}
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")
branches := make([]*models.Branch, 0, len(outputLines))
for _, line := range outputLines {
if line == "" {
continue
}
split := strings.Split(line, SEPARATION_CHAR)
if len(split) != 4 {
// Ignore line if it isn't separated into 4 parts
// This is probably a warning message, for more info see:
// https://github.com/jesseduffield/lazygit/issues/1385#issuecomment-885580439
continue
}
name := strings.TrimPrefix(split[1], "heads/")
branch := &models.Branch{
Name: name,
Pullables: "?",
Pushables: "?",
Head: split[0] == "*",
}
upstreamName := split[2]
if upstreamName == "" {
// if we're here then it means we do not have a local version of the remote.
// The branch might still be tracking a remote though, we just don't know
// how many commits ahead/behind it is
branches = append(branches, branch)
continue
}
track := split[3]
re := regexp.MustCompile(`ahead (\d+)`)
match := re.FindStringSubmatch(track)
if len(match) > 1 {
branch.Pushables = match[1]
} else {
branch.Pushables = "0"
}
re = regexp.MustCompile(`behind (\d+)`)
match = re.FindStringSubmatch(track)
if len(match) > 1 {
branch.Pullables = match[1]
} else {
branch.Pullables = "0"
}
branches = append(branches, branch)
}
return branches
}
// TODO: only look at the new reflog commits, and otherwise store the recencies in
// int form against the branch to recalculate the time ago
func (b *BranchListBuilder) obtainReflogBranches() []*models.Branch {
func (self *BranchLoader) obtainReflogBranches(reflogCommits []*models.Commit) []*models.Branch {
foundBranchesMap := map[string]bool{}
re := regexp.MustCompile(`checkout: moving from ([\S]+) to ([\S]+)`)
reflogBranches := make([]*models.Branch, 0, len(b.ReflogCommits))
for _, commit := range b.ReflogCommits {
reflogBranches := make([]*models.Branch, 0, len(reflogCommits))
for _, commit := range reflogCommits {
if match := re.FindStringSubmatch(commit.Name); len(match) == 3 {
recency := utils.UnixToTimeAgo(commit.UnixTimestamp)
for _, branchName := range match[1:] {

View File

@@ -0,0 +1,57 @@
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 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 self.getCommitFilesFromFilenames(filenames), nil
}
// filenames string is something like "file1\nfile2\nfile3"
func (self *CommitFileLoader) getCommitFilesFromFilenames(filenames string) []*models.CommitFile {
commitFiles := make([]*models.CommitFile, 0)
lines := strings.Split(strings.TrimRight(filenames, "\x00"), "\x00")
n := len(lines)
for i := 0; i < n-1; i += 2 {
// typical result looks like 'A my_file' meaning my_file was added
changeStatus := lines[i]
name := lines[i+1]
commitFiles = append(commitFiles, &models.CommitFile{
Name: name,
ChangeStatus: changeStatus,
})
}
return commitFiles
}

View File

@@ -1,20 +1,19 @@
package commands
package loaders
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/sirupsen/logrus"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/gui/style"
)
// context:
@@ -23,34 +22,142 @@ import (
// be processed as part of a rebase (these won't appear in git log but we
// grab them from the rebase-related files in the .git directory to show them
// if we find out we need to use one of these functions in the git.go file, we
// can just pull them out of here and put them there and then call them from in here
const SEPARATION_CHAR = "|"
// CommitListBuilder returns a list of Branch objects for the current repo
type CommitListBuilder struct {
Log *logrus.Entry
GitCommand *GitCommand
OSCommand *oscommands.OSCommand
Tr *i18n.TranslationSet
// 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
}
// NewCommitListBuilder builds a new commit list builder
func NewCommitListBuilder(log *logrus.Entry, gitCommand *GitCommand, osCommand *oscommands.OSCommand, tr *i18n.TranslationSet) *CommitListBuilder {
return &CommitListBuilder{
Log: log,
GitCommand: gitCommand,
OSCommand: osCommand,
Tr: tr,
// 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) {
if canExtractCommit(line) {
commit := self.extractCommitFromLine(line)
if commit.Sha == firstPushedCommit {
passedFirstPushedCommit = true
}
commit.Status = map[bool]string{true: "unpushed", false: "pushed"}[!passedFirstPushedCommit]
commits = append(commits, commit)
}
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 (c *CommitListBuilder) extractCommitFromLine(line string) *models.Commit {
func (self *CommitLoader) extractCommitFromLine(line string) *models.Commit {
split := strings.Split(line, SEPARATION_CHAR)
sha := split[0]
@@ -72,10 +179,6 @@ func (c *CommitListBuilder) extractCommitFromLine(line string) *models.Commit {
unitTimestampInt, _ := strconv.Atoi(unixTimestamp)
// Any commit with multiple parents is a merge commit.
// If there's a space then it means there must be more than one parent hash
isMerge := strings.Contains(parentHashes, " ")
return &models.Commit{
Sha: sha,
Name: message,
@@ -83,120 +186,70 @@ func (c *CommitListBuilder) extractCommitFromLine(line string) *models.Commit {
ExtraInfo: extraInfo,
UnixTimestamp: int64(unitTimestampInt),
Author: author,
IsMerge: isMerge,
Parents: strings.Split(parentHashes, " "),
}
}
type GetCommitsOptions struct {
Limit bool
FilterPath string
IncludeRebaseCommits bool
RefName string // e.g. "HEAD" or "my_branch"
}
func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode) ([]*models.Commit, error) {
commits, err := self.getRebasingCommits(rebaseMode)
if err != nil {
return nil, err
}
func (c *CommitListBuilder) MergeRebasingCommits(commits []*models.Commit) ([]*models.Commit, error) {
// chances are we have as many commits as last time so we'll set the capacity to be the old length
result := make([]*models.Commit, 0, len(commits))
if len(commits) == 0 {
return nil, nil
}
commitShas := make([]string, 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
}
commitShas[i] = commit.Sha
}
rebaseMode, err := c.GitCommand.RebaseMode()
if err != nil {
return nil, err
}
// note that we're not filtering these as we do non-rebasing commits just because
// I suspect that will cause some damage
cmdObj := self.cmd.New(
fmt.Sprintf(
"git show %s --no-patch --oneline %s --abbrev=%d",
strings.Join(commitShas, " "),
prettyFormat,
20,
),
).DontLog()
if rebaseMode == "" {
// not in rebase mode so return original commits
return result, nil
}
rebasingCommits, err := c.getRebasingCommits(rebaseMode)
if err != nil {
return nil, err
}
if len(rebasingCommits) > 0 {
result = append(rebasingCommits, result...)
}
return result, nil
}
// GetCommits obtains the commits of the current branch
func (c *CommitListBuilder) GetCommits(opts GetCommitsOptions) ([]*models.Commit, error) {
commits := []*models.Commit{}
var rebasingCommits []*models.Commit
rebaseMode, err := c.GitCommand.RebaseMode()
if err != nil {
return nil, err
}
if opts.IncludeRebaseCommits && opts.FilterPath == "" {
var err error
rebasingCommits, err = c.MergeRebasingCommits(commits)
if err != nil {
return nil, err
}
commits = append(commits, rebasingCommits...)
}
passedFirstPushedCommit := false
firstPushedCommit, err := c.getFirstPushedCommit(opts.RefName)
if err != nil {
// must have no upstream branch so we'll consider everything as pushed
passedFirstPushedCommit = true
}
cmd := c.getLogCmd(opts)
err = oscommands.RunLineOutputCmd(cmd, func(line string) (bool, error) {
if strings.Split(line, " ")[0] != "gpg:" {
commit := c.extractCommitFromLine(line)
if commit.Sha == firstPushedCommit {
passedFirstPushedCommit = true
}
commit.Status = map[bool]string{true: "unpushed", false: "pushed"}[!passedFirstPushedCommit]
commits = append(commits, commit)
hydratedCommits := make([]*models.Commit, 0, len(commits))
i := 0
err = cmdObj.RunAndProcessLines(func(line string) (bool, error) {
if canExtractCommit(line) {
commit := self.extractCommitFromLine(line)
matchingCommit := commits[i]
commit.Action = matchingCommit.Action
commit.Status = matchingCommit.Status
hydratedCommits = append(hydratedCommits, commit)
i++
}
return false, nil
})
if err != nil {
return nil, err
}
if rebaseMode != "" {
currentCommit := commits[len(rebasingCommits)]
blue := color.New(color.FgYellow)
youAreHere := blue.Sprintf("<-- %s ---", c.Tr.YouAreHere)
currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name)
}
commits, err = c.setCommitMergedStatuses(opts.RefName, commits)
if err != nil {
return nil, err
}
return commits, nil
return hydratedCommits, nil
}
// getRebasingCommits obtains the commits that we're in the process of rebasing
func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*models.Commit, error) {
func (self *CommitLoader) getRebasingCommits(rebaseMode enums.RebaseMode) ([]*models.Commit, error) {
switch rebaseMode {
case REBASE_MODE_MERGING:
return c.getNormalRebasingCommits()
case REBASE_MODE_INTERACTIVE:
return c.getInteractiveRebasingCommits()
case enums.REBASE_MODE_MERGING:
return self.getNormalRebasingCommits()
case enums.REBASE_MODE_INTERACTIVE:
return self.getInteractiveRebasingCommits()
default:
return nil, nil
}
}
func (c *CommitListBuilder) getNormalRebasingCommits() ([]*models.Commit, error) {
func (self *CommitLoader) getNormalRebasingCommits() ([]*models.Commit, error) {
rewrittenCount := 0
bytesContent, err := ioutil.ReadFile(filepath.Join(c.GitCommand.DotGitDir, "rebase-apply/rewritten"))
bytesContent, err := self.readFile(filepath.Join(self.dotGitDir, "rebase-apply/rewritten"))
if err == nil {
content := string(bytesContent)
rewrittenCount = len(strings.Split(content, "\n"))
@@ -204,7 +257,7 @@ func (c *CommitListBuilder) getNormalRebasingCommits() ([]*models.Commit, error)
// we know we're rebasing, so lets get all the files whose names have numbers
commits := []*models.Commit{}
err = filepath.Walk(filepath.Join(c.GitCommand.DotGitDir, "rebase-apply"), func(path string, f os.FileInfo, err error) error {
err = self.walkFiles(filepath.Join(self.dotGitDir, "rebase-apply"), func(path string, f os.FileInfo, err error) error {
if rewrittenCount > 0 {
rewrittenCount--
return nil
@@ -216,15 +269,12 @@ func (c *CommitListBuilder) getNormalRebasingCommits() ([]*models.Commit, error)
if !re.MatchString(f.Name()) {
return nil
}
bytesContent, err := ioutil.ReadFile(path)
bytesContent, err := self.readFile(path)
if err != nil {
return err
}
content := string(bytesContent)
commit, err := c.commitFromPatch(content)
if err != nil {
return err
}
commit := self.commitFromPatch(content)
commits = append([]*models.Commit{commit}, commits...)
return nil
})
@@ -247,10 +297,10 @@ func (c *CommitListBuilder) getNormalRebasingCommits() ([]*models.Commit, error)
// getInteractiveRebasingCommits takes our git-rebase-todo and our git-rebase-todo.backup files
// and extracts out the sha and names of commits that we still have to go
// in the rebase:
func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*models.Commit, error) {
bytesContent, err := ioutil.ReadFile(filepath.Join(c.GitCommand.DotGitDir, "rebase-merge/git-rebase-todo"))
func (self *CommitLoader) getInteractiveRebasingCommits() ([]*models.Commit, error) {
bytesContent, err := self.readFile(filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo"))
if err != nil {
c.Log.Error(fmt.Sprintf("error occurred reading git-rebase-todo: %s", err.Error()))
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
}
@@ -281,7 +331,7 @@ func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*models.Commit, e
// From: Lazygit Tester <test@example.com>
// Date: Wed, 5 Dec 2018 21:03:23 +1100
// Subject: second commit on master
func (c *CommitListBuilder) commitFromPatch(content string) (*models.Commit, error) {
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: ")
@@ -289,11 +339,11 @@ func (c *CommitListBuilder) commitFromPatch(content string) (*models.Commit, err
Sha: sha,
Name: name,
Status: "rebasing",
}, nil
}
}
func (c *CommitListBuilder) setCommitMergedStatuses(refName string, commits []*models.Commit) ([]*models.Commit, error) {
ancestor, err := c.getMergeBase(refName)
func (self *CommitLoader) setCommitMergedStatuses(refName string, commits []*models.Commit) ([]*models.Commit, error) {
ancestor, err := self.getMergeBase(refName)
if err != nil {
return nil, err
}
@@ -315,8 +365,8 @@ func (c *CommitListBuilder) setCommitMergedStatuses(refName string, commits []*m
return commits, nil
}
func (c *CommitListBuilder) getMergeBase(refName string) (string, error) {
currentBranch, _, err := c.GitCommand.CurrentBranchName()
func (self *CommitLoader) getMergeBase(refName string) (string, error) {
currentBranch, _, err := self.getCurrentBranchName()
if err != nil {
return "", err
}
@@ -327,7 +377,7 @@ func (c *CommitListBuilder) getMergeBase(refName string) (string, error) {
}
// swallowing error because it's not a big deal; probably because there are no commits yet
output, _ := c.OSCommand.RunCommandWithOutput("git merge-base %s %s", refName, baseBranch)
output, _ := self.cmd.New(fmt.Sprintf("git merge-base %s %s", self.cmd.Quote(refName), self.cmd.Quote(baseBranch))).DontLog().RunWithOutput()
return ignoringWarnings(output), nil
}
@@ -343,8 +393,13 @@ func ignoringWarnings(commandOutput string) string {
// getFirstPushedCommit returns the first commit SHA which has been pushed to the ref's upstream.
// all commits above this are deemed unpushed and marked as such.
func (c *CommitListBuilder) getFirstPushedCommit(refName string) (string, error) {
output, err := c.OSCommand.RunCommandWithOutput("git merge-base %s %s@{u}", refName, refName)
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
}
@@ -353,29 +408,48 @@ func (c *CommitListBuilder) getFirstPushedCommit(refName string) (string, error)
}
// getLog gets the git log.
func (c *CommitListBuilder) getLogCmd(opts GetCommitsOptions) *exec.Cmd {
func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj {
limitFlag := ""
if opts.Limit {
limitFlag = "-300"
limitFlag = " -300"
}
filterFlag := ""
if opts.FilterPath != "" {
filterFlag = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(opts.FilterPath))
filterFlag = fmt.Sprintf(" --follow -- %s", self.cmd.Quote(opts.FilterPath))
}
return c.OSCommand.ExecutableFromString(
config := self.UserConfig.Git.Log
orderFlag := "--" + config.Order
allFlag := ""
if opts.All {
allFlag = " --all"
}
return self.cmd.New(
fmt.Sprintf(
"git log %s --oneline --pretty=format:\"%%H%s%%at%s%%aN%s%%d%s%%p%s%%s\" %s --abbrev=%d --date=unix %s",
opts.RefName,
SEPARATION_CHAR,
SEPARATION_CHAR,
SEPARATION_CHAR,
SEPARATION_CHAR,
SEPARATION_CHAR,
"git log %s %s %s --oneline %s%s --abbrev=%d%s",
self.cmd.Quote(opts.RefName),
orderFlag,
allFlag,
prettyFormat,
limitFlag,
20,
filterFlag,
),
)
).DontLog()
}
var prettyFormat = fmt.Sprintf(
"--pretty=format:\"%%H%s%%at%s%%aN%s%%d%s%%p%s%%s\"",
SEPARATION_CHAR,
SEPARATION_CHAR,
SEPARATION_CHAR,
SEPARATION_CHAR,
SEPARATION_CHAR,
)
func canExtractCommit(line string) bool {
return line != "" && strings.Split(line, " ")[0] != "gpg:"
}

View File

@@ -0,0 +1,215 @@
package loaders
import (
"path/filepath"
"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"
)
func NewDummyCommitLoader() *CommitLoader {
cmn := utils.NewDummyCommon()
return &CommitLoader{
Common: cmn,
cmd: nil,
getCurrentBranchName: func() (string, string, error) { return "master", "master", nil },
getRebaseMode: func() (enums.RebaseMode, error) { return enums.REBASE_MODE_NONE, nil },
dotGitDir: ".git",
readFile: func(filename string) ([]byte, error) {
return []byte(""), nil
},
walkFiles: func(root string, fn filepath.WalkFunc) error {
return nil
},
}
}
const commitsOutput = `0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield| (HEAD -> better-tests)|b21997d6b4cbdf84b149|better typing for rebase mode
b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164|1640824515|Jesse Duffield| (origin/better-tests)|e94e8fc5b6fab4cb755f|fix logging
e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c|1640823749|Jesse Duffield||d8084cd558925eb7c9c3|refactor
d8084cd558925eb7c9c38afeed5725c21653ab90|1640821426|Jesse Duffield||65f910ebd85283b5cce9|WIP
65f910ebd85283b5cce9bf67d03d3f1a9ea3813a|1640821275|Jesse Duffield||26c07b1ab33860a1a759|WIP
26c07b1ab33860a1a7591a0638f9925ccf497ffa|1640750752|Jesse Duffield||3d4470a6c072208722e5|WIP
3d4470a6c072208722e5ae9a54bcb9634959a1c5|1640748818|Jesse Duffield||053a66a7be3da43aacdc|WIP
053a66a7be3da43aacdc7aa78e1fe757b82c4dd2|1640739815|Jesse Duffield||985fe482e806b172aea4|refactoring the config struct`
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 log "HEAD" --topo-order --oneline --pretty=format:"%H|%at|%aN|%d|%p|%s" --abbrev=20`, "", 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 log "HEAD" --topo-order --oneline --pretty=format:"%H|%at|%aN|%d|%p|%s" --abbrev=20`, 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)",
Author: "Jesse Duffield",
UnixTimestamp: 1640826609,
Parents: []string{
"b21997d6b4cbdf84b149",
},
},
{
Sha: "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164",
Name: "fix logging",
Status: "pushed",
Action: "",
Tags: []string{},
ExtraInfo: "(origin/better-tests)",
Author: "Jesse Duffield",
UnixTimestamp: 1640824515,
Parents: []string{
"e94e8fc5b6fab4cb755f",
},
},
{
Sha: "e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c",
Name: "refactor",
Status: "pushed",
Action: "",
Tags: []string{},
ExtraInfo: "",
Author: "Jesse Duffield",
UnixTimestamp: 1640823749,
Parents: []string{
"d8084cd558925eb7c9c3",
},
},
{
Sha: "d8084cd558925eb7c9c38afeed5725c21653ab90",
Name: "WIP",
Status: "pushed",
Action: "",
Tags: []string{},
ExtraInfo: "",
Author: "Jesse Duffield",
UnixTimestamp: 1640821426,
Parents: []string{
"65f910ebd85283b5cce9",
},
},
{
Sha: "65f910ebd85283b5cce9bf67d03d3f1a9ea3813a",
Name: "WIP",
Status: "pushed",
Action: "",
Tags: []string{},
ExtraInfo: "",
Author: "Jesse Duffield",
UnixTimestamp: 1640821275,
Parents: []string{
"26c07b1ab33860a1a759",
},
},
{
Sha: "26c07b1ab33860a1a7591a0638f9925ccf497ffa",
Name: "WIP",
Status: "merged",
Action: "",
Tags: []string{},
ExtraInfo: "",
Author: "Jesse Duffield",
UnixTimestamp: 1640750752,
Parents: []string{
"3d4470a6c072208722e5",
},
},
{
Sha: "3d4470a6c072208722e5ae9a54bcb9634959a1c5",
Name: "WIP",
Status: "merged",
Action: "",
Tags: []string{},
ExtraInfo: "",
Author: "Jesse Duffield",
UnixTimestamp: 1640748818,
Parents: []string{
"053a66a7be3da43aacdc",
},
},
{
Sha: "053a66a7be3da43aacdc7aa78e1fe757b82c4dd2",
Name: "refactoring the config struct",
Status: "merged",
Action: "",
Tags: []string{},
ExtraInfo: "",
Author: "Jesse Duffield",
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,137 @@
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"
"github.com/jesseduffield/lazygit/pkg/utils"
)
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
}
change := status.Change
stagedChange := change[0:1]
unstagedChange := change[1:2]
untracked := utils.IncludesString([]string{"??", "A ", "AM"}, change)
hasNoStagedChanges := utils.IncludesString([]string{" ", "U", "?"}, stagedChange)
hasInlineMergeConflicts := utils.IncludesString([]string{"UU", "AA"}, change)
hasMergeConflicts := hasInlineMergeConflicts || utils.IncludesString([]string{"DD", "AU", "UA", "UD", "DU"}, change)
file := &models.File{
Name: status.Name,
PreviousName: status.PreviousName,
DisplayString: status.StatusString,
HasStagedChanges: !hasNoStagedChanges,
HasUnstagedChanges: unstagedChange != " ",
Tracked: !untracked,
Deleted: unstagedChange == "D" || stagedChange == "D",
Added: unstagedChange == "A" || untracked,
HasMergeConflicts: hasMergeConflicts,
HasInlineMergeConflicts: hasInlineMergeConflicts,
Type: self.getFileType(status.Name),
ShortStatus: change,
}
files = append(files, file)
}
return files
}
// GitStatus returns the file status of the repo
type GitStatusOptions struct {
NoRenames bool
UntrackedFilesArg string
}
type FileStatus struct {
StatusString string
Change string // ??, MM, AM, ...
Name string
PreviousName string
}
func (c *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 = strings.TrimSpace(splitLines[i+1])
status.StatusString = fmt.Sprintf("%s %s -> %s", status.Change, status.PreviousName, status.Name)
i++
}
response = append(response, status)
}
return response, nil
}

View File

@@ -0,0 +1,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,70 @@
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 log -g --abbrev=20 --format="%%h %%ct %%gs"%s`, filterPathArg)).DontLog()
onlyObtainedNewReflogCommits := false
err := cmdObj.RunAndProcessLines(func(line string) (bool, error) {
fields := strings.SplitN(line, " ", 3)
if len(fields) <= 2 {
return false, nil
}
unixTimestamp, _ := strconv.Atoi(fields[1])
commit := &models.Commit{
Sha: fields[0],
Name: fields[2],
UnixTimestamp: int64(unixTimestamp),
Status: "reflog",
}
// note that the unix timestamp here is the timestamp of the COMMIT, not the reflog entry itself,
// so two consequetive reflog entries may have both the same SHA and therefore same timestamp.
// 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,159 @@
package loaders
import (
"errors"
"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"
)
const reflogOutput = `c3c4b66b64c97ffeecde 1643150483 checkout: moving from A to B
c3c4b66b64c97ffeecde 1643150483 checkout: moving from B to A
c3c4b66b64c97ffeecde 1643150483 checkout: moving from A to B
c3c4b66b64c97ffeecde 1643150483 checkout: moving from master to A
f4ddf2f0d4be4ccc7efa 1643149435 checkout: moving from A to master
`
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 log -g --abbrev=20 --format="%h %ct %gs"`, "", nil),
lastReflogCommit: nil,
expectedCommits: []*models.Commit{},
expectedOnlyObtainedNew: false,
expectedError: nil,
},
{
testName: "some reflog entries",
runner: oscommands.NewFakeRunner(t).
Expect(`git log -g --abbrev=20 --format="%h %ct %gs"`, reflogOutput, nil),
lastReflogCommit: nil,
expectedCommits: []*models.Commit{
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
Status: "reflog",
UnixTimestamp: 1643150483,
},
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from B to A",
Status: "reflog",
UnixTimestamp: 1643150483,
},
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
Status: "reflog",
UnixTimestamp: 1643150483,
},
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from master to A",
Status: "reflog",
UnixTimestamp: 1643150483,
},
{
Sha: "f4ddf2f0d4be4ccc7efa",
Name: "checkout: moving from A to master",
Status: "reflog",
UnixTimestamp: 1643149435,
},
},
expectedOnlyObtainedNew: false,
expectedError: nil,
},
{
testName: "some reflog entries where last commit is given",
runner: oscommands.NewFakeRunner(t).
Expect(`git log -g --abbrev=20 --format="%h %ct %gs"`, reflogOutput, nil),
lastReflogCommit: &models.Commit{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from B to A",
Status: "reflog",
UnixTimestamp: 1643150483,
},
expectedCommits: []*models.Commit{
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
Status: "reflog",
UnixTimestamp: 1643150483,
},
},
expectedOnlyObtainedNew: true,
expectedError: nil,
},
{
testName: "when passing filterPath",
runner: oscommands.NewFakeRunner(t).
Expect(`git log -g --abbrev=20 --format="%h %ct %gs" --follow -- "path"`, reflogOutput, nil),
lastReflogCommit: &models.Commit{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from B to A",
Status: "reflog",
UnixTimestamp: 1643150483,
},
filterPath: "path",
expectedCommits: []*models.Commit{
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
Status: "reflog",
UnixTimestamp: 1643150483,
},
},
expectedOnlyObtainedNew: true,
expectedError: nil,
},
{
testName: "when command returns error",
runner: oscommands.NewFakeRunner(t).
Expect(`git log -g --abbrev=20 --format="%h %ct %gs"`, "", 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

@@ -1,4 +1,4 @@
package commands
package loaders
import (
"fmt"
@@ -6,18 +6,37 @@ import (
"sort"
"strings"
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"
)
func (c *GitCommand) GetRemotes() ([]*models.Remote, error) {
// get remote branches
unescaped := "git branch -r"
remoteBranchesStr, err := c.OSCommand.RunCommandWithOutput(unescaped)
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 := c.Repo.Remotes()
goGitRemotes, err := self.getGoGitRemotes()
if err != nil {
return nil, err
}
@@ -27,7 +46,7 @@ func (c *GitCommand) GetRemotes() ([]*models.Remote, error) {
for i, goGitRemote := range goGitRemotes {
remoteName := goGitRemote.Config().Name
re := regexp.MustCompile(fmt.Sprintf(`%s\/([\S]+)`, remoteName))
re := regexp.MustCompile(fmt.Sprintf(`(?m)^\s*%s\/([\S]+)`, remoteName))
matches := re.FindAllStringSubmatch(remoteBranchesStr, -1)
branches := make([]*models.RemoteBranch, len(matches))
for j, match := range matches {

View File

@@ -0,0 +1,80 @@
package loaders
import (
"regexp"
"strconv"
"strings"
"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()
stashEntries := []*models.StashEntry{}
for i, line := range utils.SplitLines(rawString) {
stashEntries = append(stashEntries, self.stashEntryFromLine(line, i))
}
return stashEntries
}
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,51 @@
package loaders
import (
"strings"
"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
remoteBranchesStr, err := self.cmd.New(`git tag --list --sort=-creatordate`).DontLog().RunWithOutput()
if err != nil {
return nil, err
}
content := utils.TrimTrailingNewline(remoteBranchesStr)
if content == "" {
return nil, nil
}
split := strings.Split(content, "\n")
// first step is to get our remotes from go-git
tags := make([]*models.Tag, len(split))
for i, tagName := range split {
tags[i] = &models.Tag{
Name: tagName,
}
}
return tags, nil
}

View File

@@ -1,50 +0,0 @@
package commands
import (
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
)
// GetFilesInDiff get the specified commit files
func (c *GitCommand) GetFilesInDiff(from string, to string, reverse bool, patchManager *patch.PatchManager) ([]*models.CommitFile, error) {
reverseFlag := ""
if reverse {
reverseFlag = " -R "
}
filenames, err := c.OSCommand.RunCommandWithOutput("git diff --submodule --no-ext-diff --name-status %s %s %s", reverseFlag, from, to)
if err != nil {
return nil, err
}
return c.getCommitFilesFromFilenames(filenames, to, patchManager), nil
}
// filenames string is something like "file1\nfile2\nfile3"
func (c *GitCommand) getCommitFilesFromFilenames(filenames string, parent string, patchManager *patch.PatchManager) []*models.CommitFile {
commitFiles := make([]*models.CommitFile, 0)
for _, line := range strings.Split(strings.TrimRight(filenames, "\n"), "\n") {
// typical result looks like 'A my_file' meaning my_file was added
if line == "" {
continue
}
changeStatus := line[0:1]
name := line[2:]
status := patch.UNSELECTED
if patchManager != nil && patchManager.To == parent {
status = patchManager.GetFileStatus(name)
}
commitFiles = append(commitFiles, &models.CommitFile{
Parent: parent,
Name: name,
ChangeStatus: changeStatus,
PatchStatus: status,
})
}
return commitFiles
}

View File

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

View File

@@ -1,111 +0,0 @@
package commands
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// GetStatusFiles git status files
type GetStatusFileOptions struct {
NoRenames bool
}
func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
// check if config wants us ignoring untracked files
untrackedFilesSetting := c.GetConfigValue("status.showUntrackedFiles")
if untrackedFilesSetting == "" {
untrackedFilesSetting = "all"
}
untrackedFilesArg := fmt.Sprintf("--untracked-files=%s", untrackedFilesSetting)
statusOutput, err := c.GitStatus(GitStatusOptions{NoRenames: opts.NoRenames, UntrackedFilesArg: untrackedFilesArg})
if err != nil {
c.Log.Error(err)
}
statusStrings := utils.SplitLines(statusOutput)
files := []*models.File{}
for _, statusString := range statusStrings {
if strings.HasPrefix(statusString, "warning") {
c.Log.Warningf("warning when calling git status: %s", statusString)
continue
}
change := statusString[0:2]
stagedChange := change[0:1]
unstagedChange := statusString[1:2]
filename := c.OSCommand.Unquote(statusString[3:])
untracked := utils.IncludesString([]string{"??", "A ", "AM"}, change)
hasNoStagedChanges := utils.IncludesString([]string{" ", "U", "?"}, stagedChange)
hasMergeConflicts := utils.IncludesString([]string{"DD", "AA", "UU", "AU", "UA", "UD", "DU"}, change)
hasInlineMergeConflicts := utils.IncludesString([]string{"UU", "AA"}, change)
file := &models.File{
Name: filename,
DisplayString: statusString,
HasStagedChanges: !hasNoStagedChanges,
HasUnstagedChanges: unstagedChange != " ",
Tracked: !untracked,
Deleted: unstagedChange == "D" || stagedChange == "D",
HasMergeConflicts: hasMergeConflicts,
HasInlineMergeConflicts: hasInlineMergeConflicts,
Type: c.OSCommand.FileType(filename),
ShortStatus: change,
}
files = append(files, file)
}
return files
}
// GitStatus returns the plaintext short status of the repo
type GitStatusOptions struct {
NoRenames bool
UntrackedFilesArg string
}
func (c *GitCommand) GitStatus(opts GitStatusOptions) (string, error) {
noRenamesFlag := ""
if opts.NoRenames {
noRenamesFlag = "--no-renames"
}
return c.OSCommand.RunCommandWithOutput("git status %s --porcelain %s", opts.UntrackedFilesArg, noRenamesFlag)
}
// MergeStatusFiles merge status files
func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []*models.File, selectedFile *models.File) []*models.File {
if len(oldFiles) == 0 {
return newFiles
}
appendedIndexes := []int{}
// retain position of files we already could see
result := []*models.File{}
for _, oldFile := range oldFiles {
for newIndex, newFile := range newFiles {
if utils.IncludesInt(appendedIndexes, newIndex) {
continue
}
// if we just staged B and in doing so created 'A -> B' and we are currently have oldFile: A and newFile: 'A -> B', we want to wait until we come across B so the our cursor isn't jumping anywhere
waitForMatchingFile := selectedFile != nil && newFile.IsRename() && !selectedFile.IsRename() && newFile.Matches(selectedFile) && !oldFile.Matches(selectedFile)
if oldFile.Matches(newFile) && !waitForMatchingFile {
result = append(result, newFile)
appendedIndexes = append(appendedIndexes, newIndex)
}
}
}
// append any new files to the end
for index, newFile := range newFiles {
if !utils.IncludesInt(appendedIndexes, index) {
result = append(result, newFile)
}
}
return result
}

View File

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

View File

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

View File

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

View File

@@ -5,12 +5,16 @@ package models
type Branch struct {
Name string
// the displayname is something like '(HEAD detached at 123asdf)', whereas in that case the name would be '123asdf'
DisplayName string
Recency string
Pushables string
Pullables string
UpstreamName string
Head bool
DisplayName string
Recency string
Pushables string
Pullables string
Head bool
// if we have a named remote locally this will be the name of that remote e.g.
// 'origin' or 'tiwood'. If we don't have the remote locally it'll look like
// 'git@github.com:tiwood/lazygit.git'
UpstreamRemote string
UpstreamBranch string
}
func (b *Branch) RefName() string {
@@ -24,3 +28,30 @@ func (b *Branch) ID() string {
func (b *Branch) Description() string {
return b.RefName()
}
func (b *Branch) IsTrackingRemote() bool {
return b.UpstreamRemote != ""
}
// we know that the remote branch is not stored locally based on our pushable/pullable
// count being question marks.
func (b *Branch) RemoteBranchStoredLocally() bool {
return b.IsTrackingRemote() && b.Pushables != "?" && b.Pullables != "?"
}
func (b *Branch) MatchesUpstream() bool {
return b.RemoteBranchStoredLocally() && b.Pushables == "0" && b.Pullables == "0"
}
func (b *Branch) HasCommitsToPush() bool {
return b.RemoteBranchStoredLocally() && b.Pushables != "0"
}
func (b *Branch) HasCommitsToPull() bool {
return b.RemoteBranchStoredLocally() && b.Pullables != "0"
}
// for when we're in a detached head state
func (b *Branch) IsRealBranch() bool {
return b.Pushables != "" && b.Pullables != ""
}

View File

@@ -1,6 +1,10 @@
package models
import "fmt"
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// Commit : A git commit
type Commit struct {
@@ -13,15 +17,12 @@ type Commit struct {
Author string
UnixTimestamp int64
// IsMerge tells us whether we're dealing with a merge commit i.e. a commit with two parents
IsMerge bool
// SHAs of parent commits (will be multiple if it's a merge commit)
Parents []string
}
func (c *Commit) ShortSha() string {
if len(c.Sha) < 8 {
return c.Sha
}
return c.Sha[:8]
return utils.ShortSha(c.Sha)
}
func (c *Commit) RefName() string {
@@ -35,3 +36,13 @@ func (c *Commit) ID() string {
func (c *Commit) Description() string {
return fmt.Sprintf("%s %s", c.Sha[:7], c.Name)
}
func (c *Commit) IsMerge() bool {
return len(c.Parents) > 1
}
// returns true if this commit is not actually in the git log but instead
// is from a TODO file for an interactive rebase.
func (c *Commit) IsTODO() bool {
return c.Action != ""
}

View File

@@ -2,12 +2,8 @@ package models
// CommitFile : A git commit file
type CommitFile struct {
// Parent is the identifier of the parent object e.g. a commit SHA if this commit file is for a commit, or a stash entry ref like 'stash@{1}'
Parent string
Name string
// PatchStatus tells us whether the file has been wholly or partially added to a patch. We might want to pull this logic up into the gui package and make it a map like we do with cherry picked commits
PatchStatus int // one of 'WHOLE' 'PART' 'NONE'
// TODO: rename this to Path
Name string
ChangeStatus string // e.g. 'A' for added or 'M' for modified. This is based on the result from git diff --name-status
}

View File

@@ -1,8 +1,6 @@
package models
import (
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -10,9 +8,11 @@ import (
// duplicating this for now
type File struct {
Name string
PreviousName string
HasStagedChanges bool
HasUnstagedChanges bool
Tracked bool
Added bool
Deleted bool
HasMergeConflicts bool
HasInlineMergeConflicts bool
@@ -21,15 +21,25 @@ type File struct {
ShortStatus string // e.g. 'AD', ' A', 'M ', '??'
}
const RENAME_SEPARATOR = " -> "
// sometimes we need to deal with either a node (which contains a file) or an actual file
type IFile interface {
GetHasUnstagedChanges() bool
GetHasStagedChanges() bool
GetIsTracked() bool
GetPath() string
}
func (f *File) IsRename() bool {
return strings.Contains(f.Name, RENAME_SEPARATOR)
return f.PreviousName != ""
}
// Names returns an array containing just the filename, or in the case of a rename, the after filename and the before filename
func (f *File) Names() []string {
return strings.Split(f.Name, RENAME_SEPARATOR)
result := []string{f.Name}
if f.PreviousName != "" {
result = append(result, f.PreviousName)
}
return result
}
// returns true if the file names are the same or if a a file rename includes the filename of the other
@@ -51,10 +61,27 @@ func (f *File) IsSubmodule(configs []*SubmoduleConfig) bool {
func (f *File) SubmoduleConfig(configs []*SubmoduleConfig) *SubmoduleConfig {
for _, config := range configs {
if f.Name == config.Name {
if f.Name == config.Path {
return config
}
}
return nil
}
func (f *File) GetHasUnstagedChanges() bool {
return f.HasUnstagedChanges
}
func (f *File) GetHasStagedChanges() bool {
return f.HasStagedChanges
}
func (f *File) GetIsTracked() bool {
return f.Tracked
}
func (f *File) GetPath() string {
// TODO: remove concept of name; just use path
return f.Name
}

View File

@@ -0,0 +1,165 @@
package oscommands
import (
"os/exec"
)
// A command object is a general way to represent a command to be run on the
// command line.
type ICmdObj interface {
GetCmd() *exec.Cmd
// outputs string representation of command. Note that if the command was built
// using NewFromArgs, the output won't be quite the same as what you would type
// into a terminal e.g. 'sh -c git commit' as opposed to 'sh -c "git commit"'
ToString() string
AddEnvVars(...string) ICmdObj
GetEnvVars() []string
// runs the command and returns an error if any
Run() error
// runs the command and returns the output as a string, and an error if any
RunWithOutput() (string, error)
// runs the command and runs a callback function on each line of the output. If the callback returns true for the boolean value, we kill the process and return.
RunAndProcessLines(onLine func(line string) (bool, error)) error
// Be calling DontLog(), we're saying that once we call Run(), we don't want to
// log the command in the UI (it'll still be logged in the log file). The general rule
// is that if a command doesn't change the git state (e.g. read commands like `git diff`)
// then we don't want to log it. If we are changing something (e.g. `git add .`) then
// we do. The only exception is if we're running a command in the background periodically
// like `git fetch`, which technically does mutate stuff but isn't something we need
// to notify the user about.
DontLog() ICmdObj
// This returns false if DontLog() was called
ShouldLog() bool
// when you call this, then call Run(), we'll stream the output to the cmdWriter (i.e. the command log panel)
StreamOutput() ICmdObj
// returns true if StreamOutput() was called
ShouldStreamOutput() bool
// if you call this before ShouldStreamOutput we'll consider an error with no
// stderr content as a non-error. Not yet supported for Run or RunWithOutput (
// but adding support is trivial)
IgnoreEmptyError() ICmdObj
// returns true if IgnoreEmptyError() was called
ShouldIgnoreEmptyError() bool
PromptOnCredentialRequest() ICmdObj
FailOnCredentialRequest() ICmdObj
GetCredentialStrategy() CredentialStrategy
}
type CmdObj struct {
cmdStr string
cmd *exec.Cmd
runner ICmdObjRunner
// see DontLog()
dontLog bool
// see StreamOutput()
streamOutput bool
// see IgnoreEmptyError()
ignoreEmptyError bool
// if set to true, it means we might be asked to enter a username/password by this command.
credentialStrategy CredentialStrategy
}
type CredentialStrategy int
const (
// do not expect a credential request. If we end up getting one
// we'll be in trouble because the command will hang indefinitely
NONE CredentialStrategy = iota
// expect a credential request and if we get one, prompt the user to enter their username/password
PROMPT
// in this case we will check for a credential request (i.e. the command pauses to ask for
// username/password) and if we get one, we just submit a newline, forcing the
// command to fail. We use this e.g. for a background `git fetch` to prevent it
// from hanging indefinitely.
FAIL
)
var _ ICmdObj = &CmdObj{}
func (self *CmdObj) GetCmd() *exec.Cmd {
return self.cmd
}
func (self *CmdObj) ToString() string {
return self.cmdStr
}
func (self *CmdObj) AddEnvVars(vars ...string) ICmdObj {
self.cmd.Env = append(self.cmd.Env, vars...)
return self
}
func (self *CmdObj) GetEnvVars() []string {
return self.cmd.Env
}
func (self *CmdObj) DontLog() ICmdObj {
self.dontLog = true
return self
}
func (self *CmdObj) ShouldLog() bool {
return !self.dontLog
}
func (self *CmdObj) StreamOutput() ICmdObj {
self.streamOutput = true
return self
}
func (self *CmdObj) ShouldStreamOutput() bool {
return self.streamOutput
}
func (self *CmdObj) IgnoreEmptyError() ICmdObj {
self.ignoreEmptyError = true
return self
}
func (self *CmdObj) ShouldIgnoreEmptyError() bool {
return self.ignoreEmptyError
}
func (self *CmdObj) Run() error {
return self.runner.Run(self)
}
func (self *CmdObj) RunWithOutput() (string, error) {
return self.runner.RunWithOutput(self)
}
func (self *CmdObj) RunAndProcessLines(onLine func(line string) (bool, error)) error {
return self.runner.RunAndProcessLines(self, onLine)
}
func (self *CmdObj) PromptOnCredentialRequest() ICmdObj {
self.credentialStrategy = PROMPT
return self
}
func (self *CmdObj) FailOnCredentialRequest() ICmdObj {
self.credentialStrategy = FAIL
return self
}
func (self *CmdObj) GetCredentialStrategy() CredentialStrategy {
return self.credentialStrategy
}

View File

@@ -0,0 +1,101 @@
package oscommands
import (
"fmt"
"os"
"strings"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/mgutz/str"
)
type ICmdObjBuilder interface {
// New returns a new command object based on the string provided
New(cmdStr string) ICmdObj
// NewShell takes a string like `git commit` and returns an executable shell command for it e.g. `sh -c 'git commit'`
NewShell(commandStr string) ICmdObj
// NewFromArgs takes a slice of strings like []string{"git", "commit"} and returns a new command object. This can be useful when you don't want to worry about whitespace and quoting and stuff.
NewFromArgs(args []string) ICmdObj
// Quote wraps a string in quotes with any necessary escaping applied. The reason for bundling this up with the other methods in this interface is that we basically always need to make use of this when creating new command objects.
Quote(str string) string
}
type CmdObjBuilder struct {
runner ICmdObjRunner
platform *Platform
}
// poor man's version of explicitly saying that struct X implements interface Y
var _ ICmdObjBuilder = &CmdObjBuilder{}
func (self *CmdObjBuilder) New(cmdStr string) ICmdObj {
args := str.ToArgv(cmdStr)
cmd := secureexec.Command(args[0], args[1:]...)
cmd.Env = os.Environ()
return &CmdObj{
cmdStr: cmdStr,
cmd: cmd,
runner: self.runner,
}
}
func (self *CmdObjBuilder) NewFromArgs(args []string) ICmdObj {
cmd := secureexec.Command(args[0], args[1:]...)
cmd.Env = os.Environ()
return &CmdObj{
cmdStr: strings.Join(args, " "),
cmd: cmd,
runner: self.runner,
}
}
func (self *CmdObjBuilder) NewShell(commandStr string) ICmdObj {
quotedCommand := ""
// Windows does not seem to like quotes around the command
if self.platform.OS == "windows" {
quotedCommand = strings.NewReplacer(
"^", "^^",
"&", "^&",
"|", "^|",
"<", "^<",
">", "^>",
"%", "^%",
).Replace(commandStr)
} else {
quotedCommand = self.Quote(commandStr)
}
shellCommand := fmt.Sprintf("%s %s %s", self.platform.Shell, self.platform.ShellArg, quotedCommand)
return self.New(shellCommand)
}
func (self *CmdObjBuilder) CloneWithNewRunner(decorate func(ICmdObjRunner) ICmdObjRunner) *CmdObjBuilder {
decoratedRunner := decorate(self.runner)
return &CmdObjBuilder{
runner: decoratedRunner,
platform: self.platform,
}
}
func (self *CmdObjBuilder) Quote(message string) string {
var quote string
if self.platform.OS == "windows" {
quote = `\"`
message = strings.NewReplacer(
`"`, `"'"'"`,
`\"`, `\\"`,
).Replace(message)
} else {
quote = `"`
message = strings.NewReplacer(
`\`, `\\`,
`"`, `\"`,
`$`, `\$`,
"`", "\\`",
).Replace(message)
}
return quote + message + quote
}

View File

@@ -0,0 +1,272 @@
package oscommands
import (
"bufio"
"bytes"
"io"
"regexp"
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
type ICmdObjRunner interface {
Run(cmdObj ICmdObj) error
RunWithOutput(cmdObj ICmdObj) (string, error)
RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error
}
type CredentialType int
const (
Password CredentialType = iota
Username
Passphrase
)
type cmdObjRunner struct {
log *logrus.Entry
guiIO *guiIO
}
var _ ICmdObjRunner = &cmdObjRunner{}
func (self *cmdObjRunner) Run(cmdObj ICmdObj) error {
if cmdObj.GetCredentialStrategy() != NONE {
return self.runWithCredentialHandling(cmdObj)
}
if cmdObj.ShouldStreamOutput() {
return self.runAndStream(cmdObj)
}
_, err := self.RunWithOutput(cmdObj)
return err
}
func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
if cmdObj.ShouldStreamOutput() {
err := self.runAndStream(cmdObj)
// for now we're not capturing output, just because it would take a little more
// effort and there's currently no use case for it. Some commands call RunWithOutput
// but ignore the output, hence why we've got this check here.
return "", err
}
if cmdObj.GetCredentialStrategy() != NONE {
err := self.runWithCredentialHandling(cmdObj)
// for now we're not capturing output, just because it would take a little more
// effort and there's currently no use case for it. Some commands call RunWithOutput
// but ignore the output, hence why we've got this check here.
return "", err
}
self.log.WithField("command", cmdObj.ToString()).Debug("RunCommand")
if cmdObj.ShouldLog() {
self.logCmdObj(cmdObj)
}
output, err := sanitisedCommandOutput(cmdObj.GetCmd().CombinedOutput())
if err != nil {
self.log.WithField("command", cmdObj.ToString()).Error(output)
}
return output, err
}
func (self *cmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error {
if cmdObj.GetCredentialStrategy() != NONE {
return errors.New("cannot call RunAndProcessLines with credential strategy. If you're seeing this then a contributor to Lazygit has accidentally called this method! Please raise an issue")
}
if cmdObj.ShouldLog() {
self.logCmdObj(cmdObj)
}
cmd := cmdObj.GetCmd()
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
return err
}
scanner := bufio.NewScanner(stdoutPipe)
scanner.Split(bufio.ScanLines)
if err := cmd.Start(); err != nil {
return err
}
for scanner.Scan() {
line := scanner.Text()
stop, err := onLine(line)
if err != nil {
return err
}
if stop {
_ = cmd.Process.Kill()
break
}
}
_ = cmd.Wait()
return nil
}
// Whenever we're asked for a password we just enter a newline, which will
// eventually cause the command to fail.
var failPromptFn = func(CredentialType) string { return "\n" }
func (self *cmdObjRunner) runWithCredentialHandling(cmdObj ICmdObj) error {
var promptFn func(CredentialType) string
switch cmdObj.GetCredentialStrategy() {
case PROMPT:
promptFn = self.guiIO.promptForCredentialFn
case FAIL:
promptFn = failPromptFn
case NONE:
// we should never land here
return errors.New("runWithCredentialHandling called but cmdObj does not have a a credential strategy")
}
return self.runAndDetectCredentialRequest(cmdObj, promptFn)
}
func (self *cmdObjRunner) logCmdObj(cmdObj ICmdObj) {
self.guiIO.logCommandFn(cmdObj.ToString(), true)
}
func sanitisedCommandOutput(output []byte, err error) (string, error) {
outputString := string(output)
if err != nil {
// errors like 'exit status 1' are not very useful so we'll create an error
// from the combined output
if outputString == "" {
return "", utils.WrapError(err)
}
return outputString, errors.New(outputString)
}
return outputString, nil
}
type cmdHandler struct {
stdoutPipe io.Reader
stdinPipe io.Writer
close func() error
}
func (self *cmdObjRunner) runAndStream(cmdObj ICmdObj) error {
return self.runAndStreamAux(cmdObj, func(handler *cmdHandler, cmdWriter io.Writer) {
go func() {
_, _ = io.Copy(cmdWriter, handler.stdoutPipe)
}()
})
}
// runAndDetectCredentialRequest detect a username / password / passphrase question in a command
// promptUserForCredential is a function that gets executed when this function detect you need to fillin a password or passphrase
// The promptUserForCredential argument will be "username", "password" or "passphrase" and expects the user's password/passphrase or username back
func (self *cmdObjRunner) runAndDetectCredentialRequest(
cmdObj ICmdObj,
promptUserForCredential func(CredentialType) string,
) error {
// setting the output to english so we can parse it for a username/password request
cmdObj.AddEnvVars("LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8")
return self.runAndStreamAux(cmdObj, func(handler *cmdHandler, cmdWriter io.Writer) {
tr := io.TeeReader(handler.stdoutPipe, cmdWriter)
go utils.Safe(func() {
self.processOutput(tr, handler.stdinPipe, promptUserForCredential)
})
})
}
func (self *cmdObjRunner) runAndStreamAux(
cmdObj ICmdObj,
onRun func(*cmdHandler, io.Writer),
) error {
cmdWriter := self.guiIO.newCmdWriterFn()
if cmdObj.ShouldLog() {
self.logCmdObj(cmdObj)
}
self.log.WithField("command", cmdObj.ToString()).Info("RunCommand")
cmd := cmdObj.GetCmd()
var stderr bytes.Buffer
cmd.Stderr = io.MultiWriter(cmdWriter, &stderr)
handler, err := self.getCmdHandler(cmd)
if err != nil {
return err
}
defer func() {
if closeErr := handler.close(); closeErr != nil {
self.log.Error(closeErr)
}
}()
onRun(handler, cmdWriter)
err = cmd.Wait()
if err != nil {
errStr := stderr.String()
if cmdObj.ShouldIgnoreEmptyError() && errStr == "" {
return nil
}
return errors.New(stderr.String())
}
return nil
}
func (self *cmdObjRunner) processOutput(reader io.Reader, writer io.Writer, promptUserForCredential func(CredentialType) string) {
checkForCredentialRequest := self.getCheckForCredentialRequestFunc()
scanner := bufio.NewScanner(reader)
scanner.Split(bufio.ScanBytes)
for scanner.Scan() {
newBytes := scanner.Bytes()
askFor, ok := checkForCredentialRequest(newBytes)
if ok {
toInput := promptUserForCredential(askFor)
// If the return data is empty we don't write anything to stdin
if toInput != "" {
_, _ = writer.Write([]byte(toInput))
}
}
}
}
// having a function that returns a function because we need to maintain some state inbetween calls hence the closure
func (self *cmdObjRunner) getCheckForCredentialRequestFunc() func([]byte) (CredentialType, bool) {
var ttyText strings.Builder
// this function takes each word of output from the command and builds up a string to see if we're being asked for a password
return func(newBytes []byte) (CredentialType, bool) {
_, err := ttyText.Write(newBytes)
if err != nil {
self.log.Error(err)
}
prompts := map[string]CredentialType{
`Password:`: Password,
`.+'s password:`: Password,
`Password\s*for\s*'.+':`: Password,
`Username\s*for\s*'.+':`: Username,
`Enter\s*passphrase\s*for\s*key\s*'.+':`: Passphrase,
}
for pattern, askFor := range prompts {
if match, _ := regexp.MatchString(pattern, ttyText.String()); match {
ttyText.Reset()
return askFor, true
}
}
return 0, false
}
}

View File

@@ -0,0 +1,25 @@
//go:build !windows
// +build !windows
package oscommands
import (
"os/exec"
"github.com/creack/pty"
)
// we define this separately for windows and non-windows given that windows does
// not have great PTY support and we need a PTY to handle a credential request
func (self *cmdObjRunner) getCmdHandler(cmd *exec.Cmd) (*cmdHandler, error) {
ptmx, err := pty.Start(cmd)
if err != nil {
return nil, err
}
return &cmdHandler{
stdoutPipe: ptmx,
stdinPipe: ptmx,
close: ptmx.Close,
}, nil
}

View File

@@ -0,0 +1,49 @@
//go:build windows
// +build windows
package oscommands
import (
"bytes"
"io"
"os/exec"
"sync"
)
type Buffer struct {
b bytes.Buffer
m sync.Mutex
}
func (b *Buffer) Read(p []byte) (n int, err error) {
b.m.Lock()
defer b.m.Unlock()
return b.b.Read(p)
}
func (b *Buffer) Write(p []byte) (n int, err error) {
b.m.Lock()
defer b.m.Unlock()
return b.b.Write(p)
}
// TODO: Remove this hack and replace it with a proper way to run commands live on windows. We still have an issue where if a password is requested, the request for a password is written straight to stdout because we can't control the stdout of a subprocess of a subprocess. Keep an eye on https://github.com/creack/pty/pull/109
func (self *cmdObjRunner) getCmdHandler(cmd *exec.Cmd) (*cmdHandler, error) {
stdoutReader, stdoutWriter := io.Pipe()
cmd.Stdout = stdoutWriter
buf := &Buffer{}
cmd.Stdin = buf
if err := cmd.Start(); err != nil {
return nil, err
}
// because we don't yet have windows support for a pty, we instead just
// pass our standard stream handlers and because there's no pty to close
// we pass a no-op function for that.
return &cmdHandler{
stdoutPipe: stdoutReader,
stdinPipe: buf,
close: func() error { return nil },
}, nil
}

View File

@@ -1,11 +1,63 @@
package oscommands
import (
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// NewDummyOSCommand creates a new dummy OSCommand for testing
func NewDummyOSCommand() *OSCommand {
return NewOSCommand(utils.NewDummyLog(), config.NewDummyAppConfig())
osCmd := NewOSCommand(utils.NewDummyCommon(), dummyPlatform, NewNullGuiIO(utils.NewDummyLog()))
return osCmd
}
type OSCommandDeps struct {
Common *common.Common
Platform *Platform
GetenvFn func(string) string
RemoveFileFn func(string) error
Cmd *CmdObjBuilder
}
func NewDummyOSCommandWithDeps(deps OSCommandDeps) *OSCommand {
common := deps.Common
if common == nil {
common = utils.NewDummyCommon()
}
platform := deps.Platform
if platform == nil {
platform = dummyPlatform
}
return &OSCommand{
Common: common,
Platform: platform,
getenvFn: deps.GetenvFn,
removeFileFn: deps.RemoveFileFn,
guiIO: NewNullGuiIO(utils.NewDummyLog()),
}
}
func NewDummyCmdObjBuilder(runner ICmdObjRunner) *CmdObjBuilder {
return &CmdObjBuilder{
runner: runner,
platform: dummyPlatform,
}
}
var dummyPlatform = &Platform{
OS: "darwin",
Shell: "bash",
ShellArg: "-c",
OpenCommand: "open {{filename}}",
OpenLinkCommand: "open {{link}}",
}
func NewDummyOSCommandWithRunner(runner *FakeCmdObjRunner) *OSCommand {
osCommand := NewOSCommand(utils.NewDummyCommon(), dummyPlatform, NewNullGuiIO(utils.NewDummyLog()))
osCommand.Cmd = NewDummyCmdObjBuilder(runner)
return osCommand
}

View File

@@ -1,97 +0,0 @@
// +build !windows
package oscommands
import (
"bufio"
"bytes"
"strings"
"unicode/utf8"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/creack/pty"
)
// RunCommandWithOutputLiveWrapper runs a command and return every word that gets written in stdout
// Output is a function that executes by every word that gets read by bufio
// As return of output you need to give a string that will be written to stdin
// NOTE: If the return data is empty it won't written anything to stdin
func RunCommandWithOutputLiveWrapper(c *OSCommand, command string, output func(string) string) error {
c.Log.WithField("command", command).Info("RunCommand")
cmd := c.ExecutableFromString(command)
cmd.Env = append(cmd.Env, "LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8")
var stderr bytes.Buffer
cmd.Stderr = &stderr
ptmx, err := pty.Start(cmd)
if err != nil {
return err
}
go utils.Safe(func() {
scanner := bufio.NewScanner(ptmx)
scanner.Split(scanWordsWithNewLines)
for scanner.Scan() {
toOutput := strings.Trim(scanner.Text(), " ")
_, _ = ptmx.WriteString(output(toOutput))
}
})
err = cmd.Wait()
ptmx.Close()
if err != nil {
return errors.New(stderr.String())
}
return nil
}
// scanWordsWithNewLines is a copy of bufio.ScanWords but this also captures new lines
// For specific comments about this function take a look at: bufio.ScanWords
func scanWordsWithNewLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
start := 0
for width := 0; start < len(data); start += width {
var r rune
r, width = utf8.DecodeRune(data[start:])
if !isSpace(r) {
break
}
}
for width, i := 0, start; i < len(data); i += width {
var r rune
r, width = utf8.DecodeRune(data[i:])
if isSpace(r) {
return i + width, data[start:i], nil
}
}
if atEOF && len(data) > start {
return len(data), data[start:], nil
}
return start, nil, nil
}
// isSpace is also copied from the bufio package and has been modified to also captures new lines
// For specific comments about this function take a look at: bufio.isSpace
func isSpace(r rune) bool {
if r <= '\u00FF' {
switch r {
case ' ', '\t', '\v', '\f':
return true
case '\u0085', '\u00A0':
return true
}
return false
}
if '\u2000' <= r && r <= '\u200a' {
return true
}
switch r {
case '\u1680', '\u2028', '\u2029', '\u202f', '\u205f', '\u3000':
return true
}
return false
}

View File

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

View File

@@ -0,0 +1,117 @@
package oscommands
import (
"bufio"
"fmt"
"regexp"
"strings"
"testing"
"github.com/go-errors/errors"
"github.com/stretchr/testify/assert"
)
// for use in testing
type FakeCmdObjRunner struct {
t *testing.T
expectedCmds []func(ICmdObj) (string, error)
expectedCmdIndex int
}
var _ ICmdObjRunner = &FakeCmdObjRunner{}
func NewFakeRunner(t *testing.T) *FakeCmdObjRunner {
return &FakeCmdObjRunner{t: t}
}
func (self *FakeCmdObjRunner) Run(cmdObj ICmdObj) error {
_, err := self.RunWithOutput(cmdObj)
return err
}
func (self *FakeCmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
if self.expectedCmdIndex > len(self.expectedCmds)-1 {
self.t.Errorf("ran too many commands. Unexpected command: `%s`", cmdObj.ToString())
return "", errors.New("ran too many commands")
}
expectedCmd := self.expectedCmds[self.expectedCmdIndex]
output, err := expectedCmd(cmdObj)
self.expectedCmdIndex++
return output, err
}
func (self *FakeCmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error {
output, err := self.RunWithOutput(cmdObj)
if err != nil {
return err
}
scanner := bufio.NewScanner(strings.NewReader(output))
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
line := scanner.Text()
stop, err := onLine(line)
if err != nil {
return err
}
if stop {
break
}
}
return nil
}
func (self *FakeCmdObjRunner) ExpectFunc(fn func(cmdObj ICmdObj) (string, error)) *FakeCmdObjRunner {
self.expectedCmds = append(self.expectedCmds, fn)
return self
}
func (self *FakeCmdObjRunner) Expect(expectedCmdStr string, output string, err error) *FakeCmdObjRunner {
self.ExpectFunc(func(cmdObj ICmdObj) (string, error) {
cmdStr := cmdObj.ToString()
assert.Equal(self.t, expectedCmdStr, cmdStr, fmt.Sprintf("expected command %d to be %s, but was %s", self.expectedCmdIndex+1, expectedCmdStr, cmdStr))
return output, err
})
return self
}
func (self *FakeCmdObjRunner) ExpectArgs(expectedArgs []string, output string, err error) *FakeCmdObjRunner {
self.ExpectFunc(func(cmdObj ICmdObj) (string, error) {
args := cmdObj.GetCmd().Args
assert.EqualValues(self.t, expectedArgs, args, fmt.Sprintf("command %d did not match expectation", self.expectedCmdIndex+1))
return output, err
})
return self
}
func (self *FakeCmdObjRunner) ExpectGitArgs(expectedArgs []string, output string, err error) *FakeCmdObjRunner {
self.ExpectFunc(func(cmdObj ICmdObj) (string, error) {
// first arg is 'git' on unix and something like '"C:\\Program Files\\Git\\mingw64\\bin\\git.exe" on windows so we'll just ensure it ends in either 'git' or 'git.exe'
re := regexp.MustCompile(`git(\.exe)?$`)
args := cmdObj.GetCmd().Args
if !re.MatchString(args[0]) {
self.t.Errorf("expected first arg to end in .git or .git.exe but was %s", args[0])
}
assert.EqualValues(self.t, expectedArgs, args[1:], fmt.Sprintf("command %d did not match expectation", self.expectedCmdIndex+1))
return output, err
})
return self
}
func (self *FakeCmdObjRunner) CheckForMissingCalls() {
if self.expectedCmdIndex < len(self.expectedCmds) {
self.t.Errorf("expected command %d to be called, but was not", self.expectedCmdIndex+1)
}
}

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