Compare commits

...

1405 Commits

Author SHA1 Message Date
Jesse Duffield
c5ff39ba5f new version 2020-03-04 09:41:04 +11:00
Jesse Duffield
31e201ca52 allow configuring side panel width 2020-03-04 00:12:23 +11:00
Jesse Duffield
0abd7ad6be update config 2020-03-04 00:12:23 +11:00
Jesse Duffield
b3522c48d9 refactor 2020-03-04 00:12:23 +11:00
Jesse Duffield
0fc58a7986 fix test 2020-03-04 00:12:23 +11:00
Jesse Duffield
54241d8ab9 more generic way of supporting custom pagers 2020-03-04 00:12:23 +11:00
Jesse Duffield
355f1615ab supporing custom pagers step 1 2020-03-04 00:12:23 +11:00
Jesse Duffield
113252b0ae bump gocui 2020-03-04 00:12:23 +11:00
Jesse Duffield
1cd7d14029 Update README.md 2020-03-04 00:11:32 +11:00
Jesse Duffield
87c2fb6a4a Update Custom_Pagers.md 2020-03-04 00:06:49 +11:00
Jesse Duffield
9912998bb7 Create Custom_Pagers.md 2020-03-03 23:07:12 +11:00
Patrick DeVivo
e223d3d8de Add TODOs badge to README
Closes #685
2020-03-02 22:39:19 +11:00
William Wagner Moraes Artero
ec31fc4cc7 docs: moved services conf docs to config.md 2020-03-01 10:57:12 +11:00
William Wagner Moraes Artero
3ce2b9b79a chore: keeping coverage up :D 2020-03-01 10:57:12 +11:00
William Wagner Moraes Artero
a79182e50d fix: accidentally escaped %s 2020-03-01 10:57:12 +11:00
William Wagner Moraes Artero
6f4c595dde docs: configuration and pull request services' settings 2020-03-01 10:57:12 +11:00
William Wagner Moraes Artero
0eb3090ad6 fix: owner groups (GitLab) 2020-03-01 10:57:12 +11:00
William Wagner Moraes Artero
6ea25bd259 feat: flexible service configuration 2020-03-01 10:57:12 +11:00
William Wagner Moraes Artero
fe5f087f9c feat: configurable services 2020-03-01 10:57:12 +11:00
Jesse Duffield
79299be3b2 better keybindings for patch building mode 2020-02-29 18:48:10 +11:00
Jesse Duffield
4c9b620bd0 better keybindings for staging by line 2020-02-29 18:48:10 +11:00
Jesse Duffield
a7508a5dfd fix cheatsheet script to support different contexts 2020-02-29 17:46:00 +11:00
hitsuji no shippo
1a3d765c4c fix keybinds document 2020-02-28 23:08:14 +11:00
Dawid Dziurla
4058c71ca0 Merge pull request #684 from ueberBrot/add-scoop-install-option
Add scoop install option to README.
2020-02-27 18:31:21 +01:00
Maurice de Bruyn
3fc22a6010 Add scoop install option to README.
Adds the install option scoop on Windows to the README.
2020-02-27 18:12:23 +01:00
David Chen
a9fe0b8000 set --abbrev-commit to return 8-digit hash strings 2020-02-27 18:05:41 +11:00
David Chen
5af7b0235e fix #680: unpushed commits still appear to be green instead of red 2020-02-27 18:05:41 +11:00
Corentin Rossignon
bf946200e9 Fix OutOfBound array access when looking for ReflogCommits
refs #679
2020-02-27 09:34:40 +11:00
Jesse Duffield
890cc87724 fix bug where commits appeared as green despite not being pushed 2020-02-27 09:33:09 +11:00
Jesse Duffield
8eb0b0f4ca do not close over variables in a function 2020-02-25 22:09:43 +11:00
Jesse Duffield
e6a8dc0bcf better logic for checking if we're rebasing 2020-02-25 22:09:43 +11:00
Jesse Duffield
02c497fad6 show file list when diffing commits 2020-02-25 21:38:38 +11:00
Jesse Duffield
d0ab747479 color active frames green by default 2020-02-25 21:27:50 +11:00
Jesse Duffield
f94d0be2c9 refactor the way we render lists 2020-02-25 21:21:07 +11:00
Jesse Duffield
9fd9fd6816 better commit lines in fullscreen mode 2020-02-25 21:21:07 +11:00
David Chen
b8717d750a keybinding doc for nextMatch/prevMatch in Config.md (#659) 2020-02-25 09:37:28 +11:00
Jesse Duffield
8ad01fe32f refresh commits when adding a tag 2020-02-25 09:10:23 +11:00
Jesse Duffield
fdb543fa7d add half and fullscreen modes 2020-02-25 08:45:30 +11:00
Jesse Duffield
52b5a6410c show item counts in frames 2020-02-25 07:19:46 +11:00
Jesse Duffield
0034cfef5c show tags in commits panel 2020-02-24 23:13:54 +11:00
Jesse Duffield
78b62be96f better handling of clearing the search 2020-02-24 22:18:04 +11:00
Jesse Duffield
1f5ccab1ce eagerload commits when searching 2020-02-24 22:18:04 +11:00
Jesse Duffield
46be280c92 support searching in side panels
For now we're just doing side panels, because it will take more work
to support this in the various main panel contexts
2020-02-24 22:18:04 +11:00
Jesse Duffield
2a5763a771 switch custom command keybinding to ':' 2020-02-24 22:04:39 +11:00
Jesse Duffield
370cec098b show diff stat 2020-02-24 09:20:50 +11:00
Dawid Dziurla
49a2f0191f tasks: don't use a function that requires Go 1.12 2020-02-24 09:09:27 +11:00
Jesse Duffield
fabdda0492 allow customizing background color in staging mode 2020-02-23 18:37:19 +11:00
Glenn Vriesman
6fc3290a05 Reflog: Use 20 sha digits instead of 7
Signed-off-by: Glenn Vriesman <glenn.vriesman@gmail.com>
2020-02-20 08:34:01 +11:00
Jesse Duffield
66e6369c28 allow fastforwarding the current branch 2020-02-18 23:07:38 +11:00
Jesse Duffield
0f0da9c32a fix wording 2020-02-16 09:57:49 +11:00
Jesse Duffield
0a69c1a02d add reset to reflog commit menu 2020-02-16 09:57:49 +11:00
Jesse Duffield
feaf98bd01 add reset to upstream option on files panel 2020-02-16 09:57:49 +11:00
Jesse Duffield
0fe9c15ce8 add mixed option to HEAD resetting, remove @{upstream} 2020-02-16 09:57:49 +11:00
Jesse Duffield
f528e12c83 allow resetting to tag 2020-02-16 09:57:49 +11:00
Jesse Duffield
8ca9f93ccf allow resetting to remote branch 2020-02-16 09:57:49 +11:00
Jesse Duffield
73d8064837 allow resetting to branch 2020-02-16 09:57:49 +11:00
Jesse Duffield
5b1f60b7eb refactor create reset menu logic 2020-02-16 09:57:49 +11:00
Jesse Duffield
2e1344f611 fix specs 2020-02-15 08:47:36 +11:00
Jesse Duffield
5b9996b16f remove old createMenu function 2020-02-15 08:47:36 +11:00
Jesse Duffield
6fdc1791e4 refactor stash options menu 2020-02-15 08:47:36 +11:00
Jesse Duffield
fd4f37b5c3 refactor git flow menu 2020-02-15 08:47:36 +11:00
Jesse Duffield
d76e8887e5 refactor patch options menu panel 2020-02-15 08:47:36 +11:00
Jesse Duffield
eb9134685a refactor rebase menu panel 2020-02-15 08:47:36 +11:00
Jesse Duffield
d929b84786 refactor recent repos menu panel 2020-02-15 08:47:36 +11:00
Jesse Duffield
8ef3297b11 refactor reflog reset options panel 2020-02-15 08:47:36 +11:00
Jesse Duffield
27c7aeb117 refactor workspace reset options panel 2020-02-15 08:47:36 +11:00
Jesse Duffield
c9714600e8 refactor commit reset menu 2020-02-15 08:47:36 +11:00
Jesse Duffield
665fdded14 continue refactor of menu panel 2020-02-15 08:47:36 +11:00
Jesse Duffield
814a0ea36f begin refactor of menu panel 2020-02-15 08:47:36 +11:00
Jesse Duffield
71e018a3dd get whole commit SHA from rebase commits 2020-02-13 18:10:14 +11:00
Jesse Duffield
efb26f8b60 refresh current branch graph when side panels refresh 2020-02-10 19:05:55 +11:00
Glenn Vriesman
d9eb6e2682 Fixed tests
Signed-off-by: Glenn Vriesman <glenn.vriesman@gmail.com>
2020-02-09 23:47:22 +11:00
Glenn Vriesman
b74107f2ba Use 8 instead of 7 digit long sha
Signed-off-by: Glenn Vriesman <glenn.vriesman@gmail.com>
2020-02-09 23:47:22 +11:00
Glenn Vriesman
0cd91a10c6 Increase internal sha size
This does not change the sha size that is displayed to the user

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

package main

import "fmt"

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

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

swallow error when adding file to watcher
2019-11-14 22:22:47 +11:00
Jesse Duffield
7995d56a85 allow editing or opening a file while resolving merge conflicts 2019-11-14 22:22:47 +11:00
Jesse Duffield
30aed94aa8 update go git 2019-11-14 09:41:56 +11:00
Jesse Duffield
3b1d705473 show upstream branch for branch 2019-11-13 22:25:42 +11:00
Jesse Duffield
f43ba728e3 prompt to set upstream when pulling on untracked branch
prompt to set upstream when pulling on untracked branch
2019-11-13 21:36:16 +11:00
Dawid Dziurla
b662362570 Merge pull request #538 from jesseduffield/dawidd6-patch-1
README: fix typo
2019-11-12 23:46:27 +01:00
Dawid Dziurla
99ece6fc35 README: fix typo
945fccd211
2019-11-12 23:42:57 +01:00
Jesse Duffield
8287659584 fix up pty fork 2019-11-12 22:58:01 +11:00
Jesse Duffield
cf95ab9a28 go mod vendor 2019-11-12 22:58:01 +11:00
Jesse Duffield
b907c74386 remove go-getter 2019-11-12 22:58:01 +11:00
Dawid Dziurla
a68fb4fb8f Merge pull request #533 from JaanJah/patch-1
Fix typo in README `.zhsrc` -> `.zshrc`
2019-11-11 18:09:30 +01:00
jaanjahimees
945fccd211 Fix typo in README .zhsrc -> .zshrc
Fix typo in `Changing Directory On Exit` section
2019-11-11 18:07:11 +02:00
Jesse Duffield
12b84307ac specify upstream when pushing a branch for the first time 2019-11-11 23:30:30 +11:00
Jesse Duffield
6843741d9e Update README.md 2019-11-11 21:48:39 +11:00
Jesse Duffield
945edb253b Update README.md 2019-11-11 21:45:37 +11:00
Jesse Duffield
cbc82cd3c1 allow for changing the current directory on exit
For this to work you'll need to put this in your ~/.zshrc (or equivalent rc file):

lg()
{
    export LAZYGIT_NEW_DIR_FILE=/Users/jesseduffieldduffield/Library/Application\ Support/jesseduffield/lazygit/.lastd

    lazygit "$@"

    if [ -f $LAZYGIT_NEW_DIR_FILE ]; then
            cd "$(cat $LAZYGIT_NEW_DIR_FILE)"
            rm -f $LAZYGIT_NEW_DIR_FILE > /dev/null
    fi
}
2019-11-11 21:45:31 +11:00
Jesse Duffield
29ee239987 Update README.md 2019-11-10 23:31:19 +11:00
Glenn Vriesman
3f7e107d09 Vendor: Updated dependencies
* Updated go.mod
 * Updated go.sum
 * Updated vendor packages

Signed-off-by: Glenn Vriesman <glenn.vriesman@gmail.com>
2019-11-10 23:23:20 +11:00
Jesse Duffield
22c0d79e2d rm go.sum && go mod vendor 2019-11-10 22:37:06 +11:00
Jesse Duffield
e174e5254d support clicking through to commit files panel 2019-11-10 22:32:13 +11:00
Jesse Duffield
de5bcb8b9c add some shameless self promotion 2019-11-10 22:32:13 +11:00
Jesse Duffield
98666186ee add '?' keybinding for opening options menu 2019-11-10 22:32:13 +11:00
Jesse Duffield
941d3c6648 allow secondary view to be scrolled 2019-11-10 22:32:13 +11:00
Jesse Duffield
5c518eda0a bump gocui (this better work or so hope me god I'm switching back to go dep)
jks I'm that that close to the edge... but I am getting there haha
2019-11-10 22:32:13 +11:00
Jesse Duffield
df72eee201 don't try to give a logrus entry object to gocui 2019-11-10 22:32:13 +11:00
Jesse Duffield
131113b065 simplify how the context system works 2019-11-10 22:32:13 +11:00
Jesse Duffield
e85310c0a9 add mouse support 2019-11-10 22:32:13 +11:00
Jesse Duffield
cd17b46b55 reset patch builder when we've escaped from the building phase and nothing has been added 2019-11-10 16:18:25 +11:00
Jesse Duffield
d0d92c7697 remove old add patch keybinding 2019-11-10 15:01:40 +11:00
Jesse Duffield
89a9b4e6d5 Update README.md 2019-11-10 10:17:35 +11:00
Jesse Duffield
592a2ff196 Update README.md 2019-11-10 10:07:43 +11:00
Jesse Duffield
17ed90c790 Update README.md 2019-11-10 10:02:04 +11:00
Jesse Duffield
9e0b335669 Update README.md 2019-11-10 09:02:00 +11:00
Jesse Duffield
194c554357 support ours/theirs merge conflict headers 2019-11-08 09:31:27 +11:00
Jesse Duffield
c65790b53d Update README.md 2019-11-06 23:18:03 +11:00
Jesse Duffield
2f37c0caaf fix tests 2019-11-05 19:22:01 +11:00
Jesse Duffield
86a39e3aea only test with non-original header 2019-11-05 19:22:01 +11:00
Jesse Duffield
326b1ca8c9 better titles 2019-11-05 19:22:01 +11:00
Jesse Duffield
72fe770974 better interface for ApplyPatch function 2019-11-05 19:22:01 +11:00
Jesse Duffield
db8c398fa3 strip whitespace when there is nothing else 2019-11-05 19:22:01 +11:00
Jesse Duffield
861bcc38be fix ambiguous condition 2019-11-05 19:22:01 +11:00
Jesse Duffield
cd3874ffb7 don't let patch manager ever be nil 2019-11-05 19:22:01 +11:00
Jesse Duffield
10fe88a2cf more work on managing focus when applying patch command 2019-11-05 19:22:01 +11:00
Jesse Duffield
1a38bfb76d do not return focus to commitsFiles view after selecting to start a new patch 2019-11-05 19:22:01 +11:00
Jesse Duffield
beaebb7dc7 handling when to show the split panel 2019-11-05 19:22:01 +11:00
Jesse Duffield
6d5d054c30 support line by line additions in staging and patch building contexts 2019-11-05 19:22:01 +11:00
Jesse Duffield
2344155379 handle empty commit in rebase 2019-11-05 19:22:01 +11:00
Jesse Duffield
48347d4d86 use fallback approach for applying patch 2019-11-05 19:22:01 +11:00
Jesse Duffield
61deaaddb7 reorder patch command options 2019-11-05 19:22:01 +11:00
Jesse Duffield
0046e9c469 create backups of patch files in case something goes wrong 2019-11-05 19:22:01 +11:00
Jesse Duffield
733145d132 clear patch after successful patch operation 2019-11-05 19:22:01 +11:00
Jesse Duffield
f285d80d0e move PatchManager to GitCommand 2019-11-05 19:22:01 +11:00
Jesse Duffield
0ffccbd3ee checks for if we're in a normal working tree state 2019-11-05 19:22:01 +11:00
Jesse Duffield
1fc120de2d better rebase args 2019-11-05 19:22:01 +11:00
Jesse Duffield
d5e443e8e3 Support building and moving patches
WIP
2019-11-05 19:22:01 +11:00
Jesse Duffield
a3c84296bf use array of ints instead of range 2019-11-05 19:22:01 +11:00
Jesse Duffield
cc039d1f9b don't unsplit main panel unconditionally on focus lost 2019-11-05 19:22:01 +11:00
Dawid Dziurla
2484ec9c11 fix headerRegexp 2019-11-05 19:22:01 +11:00
Dawid Dziurla
5f9de1f034 please golang-ci 2019-11-05 19:22:01 +11:00
Dawid Dziurla
66eaaf9cbb go mod vendor 2019-11-05 19:22:01 +11:00
Dawid Dziurla
87ac193b5e fix module checksum mismatch 2019-11-05 19:22:01 +11:00
Jesse Duffield
11e57edbb3 use v keybindings instead of c 2019-11-05 19:22:01 +11:00
Jesse Duffield
4f2c42ea47 bump gocui 2019-11-05 19:22:01 +11:00
Jesse Duffield
820f3d5cbb support split view in staging panel and staging ranges 2019-11-05 19:22:01 +11:00
Jesse Duffield
081598d989 rewrite staging to support line ranges and reversing
Now we can stage lines by range and we can also stage reversals
meaning we can delete lines or move lines from the working tree
to the index and vice versa.

I looked at how a few different git guis achieved this to iron out
some edge cases, notably ungit and git cola. The end result is
disstinct  from both those repos, but I know people care about
licensing and stuff so I'm happy to revisit this if somebody
considers it derivative.
2019-11-05 19:22:01 +11:00
Jesse Duffield
09f268befc Update FUNDING.yml 2019-10-28 09:43:46 +11:00
Jesse Duffield
4bc974c83c Update FUNDING.yml 2019-10-28 09:43:36 +11:00
Dawid Dziurla
63da8f48da Merge pull request #522 from chenrui333/go-1.13 2019-10-27 08:51:03 +01:00
Rui Chen
32d6a17240 Upgrade to go v1.13 2019-10-26 21:53:22 -04:00
Rui Chen
84d869a3a0 Anchor image tag to specific version 2019-10-26 21:50:27 -04:00
Giorgio Previtera
a1c6619401 \#480 Close popup panels before switching to a side view
Reusing the `onNewPopupPanel` function to close existing popup panels
(if any) before switching to a new side view. Alse closing any
confirmation prompt.
2019-10-27 12:39:08 +11:00
Giorgio Previtera
3524f6baa9 480 - remove duplication by using a decorator
Also use a for loop to append the new keybindings
2019-10-27 12:39:08 +11:00
Giorgio Previtera
ac5cbc1d2c #480 Allow cycling side panels with number keys 2019-10-27 12:39:08 +11:00
mjarkk
a045313e08 Removed the pkg/gui/theme.go file
Moved most functions to the new theme/theme.go
2019-10-20 12:32:57 +11:00
mjarkk
9bd2dc3050 Updated the config.md 2019-10-20 12:32:57 +11:00
mjarkk
02fef3136f Added light theme option to the settings 2019-10-20 12:32:57 +11:00
Dawid Dziurla
8fe0e00cd9 Merge pull request #516 from glvr182/hotfix/path-not-positional
#514 Fix positional flag issue
2019-09-30 21:31:43 +02:00
Glenn Vriesman
f7f19bbc02 Main: Use --path instead of positional
* Also puts a placeholder for the merge-todo argument

Signed-off-by: Glenn Vriesman <glenn.vriesman@gmail.com>
2019-09-30 15:08:20 +02:00
Dawid Dziurla
95ae806e09 Merge pull request #414 from glvr182/feature/dir-as-arg
Provide git directory as argument to Lazygit
2019-09-24 19:45:46 +02:00
Glenn Vriesman
d8a6f173c3 Mod: Added flaggy to vendor directory
Signed-off-by: Glenn Vriesman <glenn.vriesman@gmail.com>
2019-09-24 18:52:52 +02:00
Glenn Vriesman
431f1aa766 Main: Added directory argument
*  Added a positional argument that allows the user to change the dir

Signed-off-by: Glenn Vriesman <glenn.vriesman@gmail.com>
2019-09-24 18:52:46 +02:00
Dawid Dziurla
379dcf0972 UserConfigPath -> UserConfigDir 2019-09-24 19:01:40 +10:00
Dawid Dziurla
0d25d113c9 download updated binary to config dir rather than /tmp 2019-09-24 19:01:40 +10:00
Dawid Dziurla
7c70913e8d Merge pull request #513 from jesseduffield/go.sum
update go.sum again
2019-09-21 18:30:16 +02:00
Dawid Dziurla
1c5858c515 update go.sum again 2019-09-21 18:02:23 +02:00
Jesse Duffield
c3767bb3b3 update go.sum 2019-09-15 21:16:19 +10:00
Jesse Duffield
b92d27ee7f force underlying go commands under gox to use the vendor directory 2019-09-15 21:16:19 +10:00
Jesse Duffield
6eff139c40 use vendor directory in test.sh 2019-09-15 21:16:19 +10:00
Jesse Duffield
07462303ab bump gocui 2019-09-15 21:16:19 +10:00
Jesse Duffield
d12f81b44e add autoFetch to config doc 2019-09-08 11:20:35 +10:00
matejcik
600112780c use git.autoFetch config option 2019-09-08 11:20:15 +10:00
matejcik
4c73c8889f move git config options to top-level in default config 2019-09-08 11:20:15 +10:00
matejcik
68d5c2bc10 use gui.g directly 2019-09-08 11:20:15 +10:00
matejcik
7db1fee877 startBackgroundFetch does not return errors 2019-09-08 11:20:15 +10:00
matejcik
8f786e3fd9 configurable auto-fetch 2019-09-08 11:20:15 +10:00
Dawid Dziurla
1c704e11f2 adjust CI to Go modules
relatively brought in line with lazydocker's config
2019-09-01 21:24:03 +10:00
Dawid Dziurla
e0dd1cb29d switch to Go modules 2019-09-01 21:24:03 +10:00
Giorgio Previtera
827837b0b9 477 Remove unnecessary variable check
hasInlineMergeConflicts is always true with hasMergeConflicts is true
2019-07-27 11:05:23 +10:00
Giorgio Previtera
e83ef9858b #477 Remove NeedMerge boolean
Instead of storing the status in a new variable, derive it from
the existing three fields
2019-07-27 11:05:23 +10:00
Giorgio Previtera
504d506575 477 Add new NeedReset property to File and update tests
Use a boolean to determin if a file needs to be reset. We want to reset
the file when discrading changes if there is a conflict.
2019-07-27 11:05:23 +10:00
Giorgio Previtera
823b436b53 477 Remove duplicate checkout
We already checout the file calling `c.DiscardUnstagedFileChanges`
2019-07-27 11:05:23 +10:00
Giorgio Previtera
212327d746 #477 Discard changes when there are merge conflicts
If there are merge conflicts, reset the file and discard all changes
2019-07-27 11:05:23 +10:00
Christian Muehlhaeuser
cc138fc70e Simplified boolean comparison 2019-07-27 10:55:21 +10:00
Christian Muehlhaeuser
d953712377 err was assigned but never checked 2019-07-27 10:55:01 +10:00
Christian Muehlhaeuser
69ac0036e6 Swallow errors entirely, instead of assigning and ignoring them 2019-07-27 10:53:19 +10:00
Christian Muehlhaeuser
975a5315b0 Simplified code a bit 2019-07-27 10:52:06 +10:00
Christian Muehlhaeuser
8f734b11e3 Removed unnecessary string conversion 2019-07-27 10:51:17 +10:00
Tomáš Horáček
17b4cabc71 Add syntax highlighting to Config.md
It's easier to read...
2019-07-27 10:48:30 +10:00
Jesse Duffield
75db4faf69 show actual error when trying to check out a branch that doesn't exist 2019-07-14 14:31:48 +10:00
haowei
e1f5601d4b fix typo 2019-07-14 14:24:59 +10:00
Giorgio Previtera
b60ecdaa24 472 - Update error message 2019-07-14 14:24:18 +10:00
Giorgio Previtera
9fb9962ce7 472 - Don't panic if not in a repository
Display a friendly message and exit with an error if not
in a Git repository. Using the same approach used in this PR:
https://github.com/jesseduffield/lazydocker/pull/14/files
2019-07-14 14:24:18 +10:00
Jesse Duffield
25c93c678d Create FUNDING.yml 2019-06-30 09:50:28 +10:00
Jesse Duffield
b8baef7b8f use fork of roll that doesn't care about errors 2019-06-29 20:56:46 +10:00
Jesse Duffield
235a47bb80 Revert "emergency situation: we're not logging to rollrus while we're past the request quota"
This reverts commit c107eed890.
2019-06-29 20:56:46 +10:00
Jesse Duffield
c107eed890 emergency situation: we're not logging to rollrus while we're past the request quota 2019-06-24 12:29:44 +10:00
Jesse Duffield
abddea060e revert menu panel error panel usage 2019-06-08 20:29:25 +10:00
Jesse Duffield
3e40369fd2 add GIT_OPTIONAL_LOCKS=0 env var to all commands 2019-06-06 20:53:35 +10:00
Jesse Duffield
0f0fda1660 allow stashing staged changes
reinstate old stash functionality with the 's' keybinding
2019-06-06 20:50:19 +10:00
Jesse Duffield
bd2170a99c request explicit return from subprocess
Previously we were recording output from subprocesses using a multiwriter
and hooking that up to the cmd's stdout to write to both os.Stdout and
a buffer. We would then display the output after the program finished.

This worked well for commands like 'ls' but not for commands like 'vi'
which expect you to be in a tty, and when you've got the cmd's stdout
pointing at a multiwriter, the subprogram thinks we're not in a tty
and then things like terminal corruption can happen. This was the case
with neovim, and even in vim a warning was given with a pause before
starting the program.

Now we're chucking out the multiwriter and instead making it that you
need to press enter after the program has finished to return to lazygit.
This allows you to view the output of the program (e.g. if it's ls) and
then decide that you want to return. It's one level of unnecessary
redirection for editors like vim, but even they could potentially have
output to stderr/stdout that you want to look at before returning.

 Please enter the commit message for your changes. Lines starting
2019-05-26 21:19:54 +10:00
Jesse Duffield
c039e5bed0 support going to start/end of line and deleting lines in simple editor 2019-05-26 12:42:17 +10:00
Jesse Duffield
527c025a0c use shift+j/k to scroll main, ctrl+j/k to move commits 2019-05-25 16:48:17 +10:00
Jesse Duffield
53cded77f1 fix padding with coloures strings 2019-05-19 15:25:33 +10:00
Jesse Duffield
4a4dc676fc simplify code for logging output of subprocess 2019-05-18 11:30:10 +10:00
Jesse Duffield
c61bfbdd4c Support opening lazygit in a submodule 2019-05-12 17:59:49 +10:00
Suhas Karanth
e38d9d5f22 Add alternatives for scroll actions to context map 2019-05-12 16:20:42 +10:00
Suhas Karanth
97f060d38d Add field Alternative to gui.Binding
Document and use alternative keybinding for generating cheatsheet. Add
alt keybinding fn+up/down for scroll up/down actions.

Also run `go run scripts/generate_cheatsheet.go`.
2019-05-12 16:20:42 +10:00
Jesse Duffield
357b8fa98f Bump gocui fork 2019-05-09 21:27:35 +10:00
mjarkk
8754d766e2 Made not enough space pannel looks better on 1 height 2019-05-07 08:47:41 +02:00
mjarkk
2388c3ee9a Fixed some sugestions from jesseduffield 2019-05-06 20:04:54 +02:00
Mark Kopenga
61890cb9de Merge branch 'master' into smaller-ui 2019-05-06 15:24:36 +02:00
Jesse Duffield
5a0d0bb299 support resetting to a commit in either soft, hard, or mixed mode 2019-05-06 22:44:38 +10:00
Jesse Duffield
2746b1bd38 Prevent crash when opening in small window
We were crashing when opening lazygit in a small window because the limit view
was the only view that got created, and there were two functions that referenced
either the 'current' view or the files view, neither of which existed.

Now those functions just return nil if the view does not exist
2019-05-06 22:39:35 +10:00
Suhas Karanth
e09aac6450 Improve directory check for .git
Return error if the .git exists but is not a directory. This provides a
slightly better failure message for git repo with submodules in case
the '.git' is a file which provides the reference to the parent's .git
folder with the submodule inside.
2019-05-06 21:37:42 +10:00
mjarkk
19a6368377 Changed the way how the view height are set 2019-05-05 15:57:35 +02:00
mjarkk
e6122122e9 Updated the gocui package 2019-05-05 11:50:51 +02:00
mjarkk
492614ebc7 Made the ui even smaller 2019-04-26 08:24:14 +02:00
Mark Kopenga
d31f0ed39b Merge branch 'master' into smaller-ui 2019-04-26 07:46:45 +02:00
mjarkk
b505c295d2 Fixed another view things 2019-04-26 13:44:37 +10:00
mjarkk
0b9d7edd47 Fixed sugestions 2019-04-26 13:44:37 +10:00
mjarkk
e9fbb608a8 Translated missing sentences 2019-04-26 13:44:37 +10:00
mjarkk
6ba05c94ea Added another resizing step 2019-04-25 21:37:19 +02:00
mjarkk
07fec6d00e Made the go bot happy 2019-04-20 16:51:50 +02:00
mjarkk
a69b985086 Better UI on small screens 2019-04-20 15:56:23 +02:00
Jesse Duffield
471733fe03 add english translations to dutch/polish i18n files for translation later 2019-04-13 14:39:46 +10:00
Jesse Duffield
0d3a193ab5 Add 'w' keybinding in files panel to commit as a WIP
If your git.skipHookPrefix is set to, say, WIP, in your config, then
hitting 'w' in the files panel will bring up the commit message panel
with 'WIP' pre-filled, so you just need to hit enter to confirm
(or add some more to the message) in order to commit your changes
with the --no-verify flag, meaning the pre-commit hook will be skipped
2019-04-13 14:38:17 +10:00
Jesse Duffield
ab9fa291a8 Add skipHookPrefix to config
allows a user to specify a commit message prefix that will tell lazygit to skip
the pre-commit hook. This defaults to WIP. Setting it to the empty string will
disable the feature.

So if my message goes 'WIP: do the thing' then the pre-commit hook will not run
2019-04-13 14:38:17 +10:00
Jesse Duffield
cadc74eeec Update issue templates 2019-04-13 12:35:09 +10:00
Peter Lundberg
fc3a57b5e2 Change expected sha for DiscardOldFileChanges 2019-04-10 17:17:31 +10:00
Peter Lundberg
7ff07e1454 Always include atleast 2 commits when doing squash and fixup 2019-04-10 17:17:31 +10:00
Jesse Duffield
3e779bca8d bump gocui to fix invalid point crashing issue 2019-04-10 10:03:35 +10:00
Jesse Duffield
0f1abcb10c remove subprocess channel stuff 2019-04-07 17:15:01 +10:00
Jesse Duffield
55538a3695 support custom commands 2019-04-07 17:15:01 +10:00
Jesse Duffield
878a15aff4 remove verbose flag from go test 2019-04-07 13:13:40 +10:00
Jesse Duffield
60e33f5d8c Allow for creating fixup! commits 2019-04-07 13:13:40 +10:00
Claudia
b422692746 Remove custom Homebrew tap from instructions
Now that lazygit has been added to the Homebrew core repository
([1] [2]), Homebrew users no longer need to tap into
`jesseduffield/lazygit` to install it.

[1]: https://github.com/jesseduffield/lazygit/issues/256

[2]: https://github.com/Homebrew/homebrew-core/pull/37614
2019-04-06 18:33:27 +11:00
skanehira
f34be1896a fixed some #397 2019-04-06 13:02:20 +11:00
skanehira
c350cdba43 add feature of display diff between specific commits #397 2019-04-06 13:02:20 +11:00
Jesse Duffield
1a933eaa73 pass length of options to createMenu 2019-03-23 13:26:17 +11:00
Jesse Duffield
bd46e9c5b0 delete menu keybinding before setting new one 2019-03-23 13:26:17 +11:00
Jesse Duffield
09b7ae21bc always attempt to discard changes from current file 2019-03-23 13:26:17 +11:00
Jesse Duffield
acfc961909 move soft reset keybinding into reset options 2019-03-23 13:26:17 +11:00
Jesse Duffield
f502f75e1f add more options for resetting files in the working tree 2019-03-23 13:26:17 +11:00
Jesse Duffield
ff97ef7b94 support discarding unstaged changes 2019-03-23 13:26:17 +11:00
Jesse Duffield
a2c780b085 retain commit message if precommit hook fails 2019-03-23 13:07:36 +11:00
Jesse Duffield
b99305c909 push codecov result before compiling on all platforms 2019-03-23 12:00:43 +11:00
Jesse Duffield
d84dfc23e7 Rely on model rather than view to focus a point
Currently when we want to focus a point on a view (i.e. highlight a
line and ensure it's within the bounds of a view's box, we use the
LinesHeight method on the view to work out how many lines in total
there are.

This is bad because for example if we come back from editing a file,
the view will have no contents so LinesHeight == 0, but we might
be trying to select line 10 because there are actual ten things we
expect to be rendered already. This causes a crash when e.g. 10 is
greater than the height of the view.

So we need to pass in to our FocusPoint method the actual number of
items we want to render, rather than having the method rely on the
LinesHeight, so that the method knows to scroll a bit before setting
the cursor's y position.

Unfortunately this makes for some awkward code with our current setup.
We don't have a good interface type on these state objects so we now
need to explicitly obtain the len() of whatever array we're rendering.

In the case of the menu panel this is even more awkward because the items
list is just an interface{} and it's not easy to get the list of that, so
now when we instantiate a menu we need to pass in the count of items
as well.

The better solution would be to define an interface with a getItems
and getLength method and have all these item arrays become structs
implementing the interface, but I am too lazy to do this right now :)
2019-03-23 11:54:25 +11:00
Jesse Duffield
9d8fd3594e remove go modules
Perhaps one day we'll revisit this, but right now supporting go modules is just a headache.
Dep does everything we need and it's really easy to work with, and given that supporting both simultaneously is too cumbersome, and I'm too lazy to make the switch to go modules properly, I'm sticking with go dep for now.
2019-03-22 21:28:25 +11:00
Jesse Duffield
e68dbeb7eb organise keybindings better 2019-03-22 20:20:06 +11:00
skanehira
32ddf0c296 generate commit files keybind 2019-03-18 09:49:23 +11:00
skanehira
c453bfeb32 generate the cheatsheet for each supported language 2019-03-18 09:49:23 +11:00
skanehira
f6ca450d36 add commit files keybind to Keybindings_en.md 2019-03-18 09:49:23 +11:00
Jesse Duffield
d5f617ec92 show some more errors in the gui rather than panicking 2019-03-16 12:51:48 +11:00
Jesse Duffield
6d104bfa91 show file remove error in gui rather than panic 2019-03-16 12:51:48 +11:00
Jesse Duffield
e583cc2519 allow autostashing changes when checking out a branch 2019-03-16 12:51:48 +11:00
Kristijan Husak
0d208b7957 Update bitbucket pull request url. 2019-03-16 11:58:26 +11:00
Jesse Duffield
43e5c042a2 prompt user to git init when outside a repo 2019-03-16 11:38:16 +11:00
Jesse Duffield
39844ffef9 allow a LOG_LEVEL env var to be set 2019-03-16 10:52:30 +11:00
Jesse Duffield
f5c8aac97d add two more tests 2019-03-16 10:20:27 +11:00
Jesse Duffield
b6447ebdbb allow adding a file viewed from the commit files panel 2019-03-16 10:20:27 +11:00
Jesse Duffield
466fc4227e fix tests 2019-03-16 10:20:27 +11:00
Jesse Duffield
c034c88be4 display test name when running tests 2019-03-16 10:20:27 +11:00
Jesse Duffield
72830efc45 add some tests 2019-03-16 10:20:27 +11:00
Jesse Duffield
c98eddc185 appease golangci 2019-03-16 10:20:27 +11:00
Jesse Duffield
3b2353b5ae remove redundant call to refreshCommitFilesView
We already call this function inside the refreshCommitsView function.
We call it there because it's logical that A) one occurs whenever the other does and
B) the commit files only get refreshed after we've updated the commits themselves
2019-03-16 10:20:27 +11:00
Jesse Duffield
3f567c952c i18n for error message about a feature being disabled for GPG users 2019-03-16 10:20:27 +11:00
Jesse Duffield
4f7f6a073c allow user to discard old file changes for a given commit 2019-03-16 10:20:27 +11:00
Jesse Duffield
0e008cc15f allow user to checkout old files 2019-03-16 10:20:27 +11:00
Jesse Duffield
1ad9c6faac minor cleanup 2019-03-16 10:20:27 +11:00
skanehira
06fe726ee7 Add feature of display committed file list #383 2019-03-16 10:20:27 +11:00
skanehira
1b6e46973e remove the -o option from Dockerfile 2019-03-08 15:51:23 +11:00
Jesse Duffield
63e2ccfccf bump go version in CI 2019-03-05 21:56:23 +11:00
Jesse Duffield
6fd4d49db7 update go.sum 2019-03-05 21:56:23 +11:00
Jesse Duffield
2393bc791d fix cpu drainage issue 2019-03-05 21:56:23 +11:00
Jesse Duffield
398f91decb Merge branch 'master' of https://github.com/jesseduffield/lazygit 2019-03-04 20:40:51 +11:00
Jesse Duffield
c937a93f79 yet another rebase image for the readme 2019-03-04 20:40:46 +11:00
Jesse Duffield
9402d8b0c0 Update README.md 2019-03-04 20:39:00 +11:00
Jesse Duffield
2a0615da10 get higher res image of rebasing 2019-03-04 20:37:25 +11:00
Jesse Duffield
4be5eaae7b update readme again 2019-03-04 20:32:17 +11:00
Jesse Duffield
038dcb546e update readme with new keybindings link 2019-03-03 23:44:50 +11:00
Jesse Duffield
1184990e16 move updated keybindings file 2019-03-03 23:27:19 +11:00
Jesse Duffield
ac5088eee6 allow both enter and space to execute menu item 2019-03-03 23:18:28 +11:00
Jesse Duffield
e36899d5c5 prevent crashes when scrolling up 2019-03-03 23:08:07 +11:00
Jesse Duffield
403526bc50 bump go mod 2019-03-03 16:15:52 +11:00
Jesse Duffield
a5d27764cd support user configuring mouse events to be enabled 2019-03-03 16:15:20 +11:00
Jesse Duffield
43758cbb5f i18n for rebase loading states 2019-03-03 16:11:20 +11:00
Jesse Duffield
0079015102 distinguish between inline and non-inline merge conflicts 2019-03-03 15:58:01 +11:00
Jesse Duffield
7a2176f479 acknowledge 'DU' statuses as being merge conflicts 2019-03-03 15:48:16 +11:00
Jesse Duffield
e0bdfad63a don't crash if we have no lines to stage 2019-03-03 15:48:01 +11:00
Jesse Duffield
f07fc31f8b fixup layout issue that was causing crashes when the window was too small 2019-03-03 15:34:53 +11:00
Jesse Duffield
4bb577ab7d show loading status for rebasing events 2019-03-03 15:21:33 +11:00
Jesse Duffield
8305d8e72f hide donate button if mouse events are disabled 2019-03-03 15:21:20 +11:00
Jesse Duffield
f68166e858 bump gocui to stop polling events after closing the gui when switching to a subprocess 2019-03-03 14:20:25 +11:00
Jesse Duffield
8925b161a7 windows support for skipping the editor 2019-03-03 12:44:10 +11:00
Jesse Duffield
0a1298765c use sh intead of bash for the sake of testing on the docker image 2019-03-02 21:31:48 +11:00
Jesse Duffield
273678f081 fix issue where you couldn't rearrange commits while rebasing onto a branch 2019-03-02 21:31:48 +11:00
Jesse Duffield
790235f64b add another match on the error message to tell us we've encountered merge conflicts 2019-03-02 21:31:48 +11:00
Jesse Duffield
abc0f7f0aa copy lazygit directory into docker container 2019-03-02 21:31:48 +11:00
Jesse Duffield
ab81f27fc7 don't show stack trace if lazygit is started outside of a git repo 2019-03-02 21:31:48 +11:00
Jesse Duffield
dbb01b028d populate dutch and polish i18n files with new messages 2019-03-02 21:31:48 +11:00
Jesse Duffield
0c886eddfb Revert "remove old rebase code now that we're only ever interactively rebasing"
This reverts commit 1a19b1412d.
2019-03-02 20:00:26 +11:00
Jesse Duffield
399346c2ee disable mouse feature until its ready 2019-03-02 20:00:17 +11:00
Jesse Duffield
7a170bbccf extend cheatsheet generator to contain context based keybindings 2019-03-02 19:05:21 +11:00
Jesse Duffield Duffield
8c0ea8f45f mouse support 2019-03-02 17:49:30 +11:00
Jesse Duffield
afbc028ad6 revert to the old keybinding for stash: I don't want anybody accidentally deleting changes they are trying to stash 2019-03-02 17:46:56 +11:00
Jesse Duffield
e331dfcaf8 update i18n 2019-03-02 17:46:56 +11:00
Jesse Duffield
1337f6e76a appease golangci 2019-03-02 17:45:53 +11:00
Jesse Duffield
4de31da4be fix up tests
This fixes up some git and oscommand tests, and pulls some tests into commit_list_builder_test.go

I've also made the NewDummyBlah functions public so that I didn't need to duplicate them across packages
I've also given OSCommand a SetCommand() method for setting the command on the struct
I've also created a file utils.go in the test package for creating convient 'CommandSwapper's, which
basically enable you to assert a sequence of commands on the command line, and swap each one out for
a different one to actually be executed
2019-03-02 13:39:09 +11:00
Jesse Duffield Duffield
23c51ba708 cleanup 2019-02-24 18:34:18 +11:00
Jesse Duffield Duffield
19a3ac603d improve script for making a test repo 2019-02-24 17:54:56 +11:00
Jesse Duffield Duffield
f4938deaae change type of cherryPickedCommits from []string to []*Commit 2019-02-24 17:34:19 +11:00
Jesse Duffield Duffield
639df512f3 decolorise strings before calculating padwidths 2019-02-24 17:05:17 +11:00
Jesse Duffield Duffield
a8858cbd12 support cherry picking commits 2019-02-24 13:51:52 +11:00
Jesse Duffield Duffield
1a19b1412d remove old rebase code now that we're only ever interactively rebasing 2019-02-24 11:03:14 +11:00
Jesse Duffield Duffield
95d451e59a Make it easier to run sync/async commands, switch to interactive rebase when rebasing on branches 2019-02-24 10:58:15 +11:00
Jesse Duffield
6c1d2d45ef some i18n and restricting rewording during interactive rebase 2019-02-24 09:42:35 +11:00
Jesse Duffield
f6b3a9b184 rearranging todo items while interactively rebasing 2019-02-24 09:42:34 +11:00
Jesse Duffield
cdc50e8557 more support for files with spaces 2019-02-24 09:42:34 +11:00
Jesse Duffield
0173fdb9df support file renames 2019-02-24 09:42:32 +11:00
Jesse Duffield
9661ea04f3 wrap amend command in a confirmation 2019-02-20 19:46:27 +11:00
Jesse Duffield
0228e25084 work towards more interactive rebase options 2019-02-19 23:36:36 +11:00
Jesse Duffield
935f774834 don't autostash when editing 2019-02-19 09:34:24 +11:00
Jesse Duffield
dcc7855fd0 pull commit list builder functions into their own builder struct 2019-02-19 09:18:30 +11:00
Jesse Duffield
a8e22ed82f show interactive rebase commits that are yet to go 2019-02-19 09:03:29 +11:00
Jesse Duffield
d44638130c add various interactive rebase commands 2019-02-18 23:27:54 +11:00
Jesse Duffield
76a27f417f rename any commit 2019-02-18 21:29:43 +11:00
Jesse Duffield
adc2529019 dealing better with errors at the top level 2019-02-18 19:42:23 +11:00
Jesse Duffield
43ab7318d3 remove HasMergeConflicts struct instance variables 2019-02-18 19:28:02 +11:00
Jesse Duffield
cb372d469f fix golangci errors 2019-02-16 21:30:29 +11:00
Jesse Duffield
88ba6efdd5 remove outdated TODO 2019-02-16 21:20:10 +11:00
Jesse Duffield
e011e9bc42 more work on rebasing feature 2019-02-16 21:01:17 +11:00
Jesse Duffield
ad93b4c863 consider whether the view has focus when rendering the contents of a view 2019-02-16 15:17:44 +11:00
Jesse Duffield
198cbee498 introduce panel contexts and more work on rebasing 2019-02-16 12:07:27 +11:00
Jesse Duffield
daca07eaca add loading panel 2019-02-16 12:03:22 +11:00
Jesse Duffield
34acaf7ac4 support users with gotest for coloured test output 2019-02-16 11:35:35 +11:00
Jesse Duffield
d967f65329 fix git tests 2019-02-16 11:24:47 +11:00
Jesse Duffield
306ac41fd8 bump gocui to support loader animations on views 2019-02-15 20:54:03 +11:00
Jesse Duffield
c101993405 post-merge cleanup 2019-02-11 22:47:14 +11:00
Jesse Duffield
6430ab6ac9 Merge branch 'master' into feature/rebasing 2019-02-11 22:46:27 +11:00
Jesse Duffield
e09f3905e9 update go.mod 2019-02-11 22:39:17 +11:00
Jesse Duffield
53e73313a2 bump gocui to version that uses go-errors as well 2019-02-11 22:39:17 +11:00
Jesse Duffield
0891797bf8 bump dep to include go-errors package 2019-02-11 22:39:17 +11:00
Jesse Duffield
cfe3605e6b use go-errors package to display stacktrace of errors that cause panics 2019-02-11 22:39:17 +11:00
Jesse Duffield
75ab8ec4d9 catch rebase errors and show in error panels 2019-02-11 21:29:47 +11:00
Jesse Duffield
77faf85cfc post-merge cleanup 2019-02-11 21:07:12 +11:00
Jesse Duffield
3d343e9b57 Merge branch 'master' into feature/rebasing 2019-02-11 21:02:53 +11:00
Jesse Duffield
3a607061a2 Only reset origin in main view when handling the selection of a new file 2019-01-18 09:32:15 +11:00
Jesse Duffield
695b092c41 Directly send wrap argument rather than the view 2019-01-17 10:29:52 +11:00
Jesse Duffield
a38d1a3b68 Explicitly refer confirmation panel view
as @jesseduffield pointed in #358, need to refer
confirmation panel view explicitly in case something
else has focus

Co-Authored-By: KOREAN139 <korean139@gmail.com>
2019-01-17 10:29:52 +11:00
KOREAN139
2dc5e6d503 Fix recent repo view size issue
getMessageHeight() calculates height under assumption that given view's
wrap option (view.Wrap) is true, and createMenu() does not set wrap
option as true. this causes gocui set improper view's height when lines
in view needs to be wrapped.
add *gocui.View as parameter in getMessageHeight(), and calculates
view's height depend on its wrap option.

resolve issue #354
2019-01-17 10:29:52 +11:00
Dawid Dziurla
0dcfa09ff2 run go fmt against generator 2019-01-17 10:11:17 +11:00
Dawid Dziurla
d5401ab200 add script generating keybindings cheatsheet 2019-01-17 10:11:17 +11:00
Dawid Dziurla
b6f8ebc0ca delete KeyReadable field from Binding struct
also rewrite GetKey function
2019-01-17 10:11:17 +11:00
KimMachineGune
3e24069722 pkg: Fix typo 2019-01-16 18:06:11 +11:00
Jesse Duffield
c722ea5afc log in config directory rather than wherever you opened lazygit 2019-01-16 08:54:12 +11:00
Jesse Duffield
c759c7ac65 bump gocui to prevent truncating the contents of wrapped views 2019-01-15 19:56:32 +11:00
Jesse Duffield
e50bd812fc fix checksum mismatches 2019-01-14 22:12:43 +11:00
KimMachineGune
7ff022f1e7 pkg/updates: Refactoring 2019-01-14 21:52:03 +11:00
Mark Kopenga
1db8801771 Update dutch.go 2018-12-20 09:17:10 +11:00
Mark Kopenga
666ea3a4a0 Fixed suggestion from glvr182 2018-12-20 09:17:10 +11:00
mjarkk
47d50989c4 Added dutch translations 2018-12-20 09:17:10 +11:00
Dawid Dziurla
e4f70278dd i18n: pl translation 2018-12-20 09:13:41 +11:00
Jesse Duffield
0afffd03ca remove comment 2018-12-19 21:46:48 +11:00
Jesse Duffield
6c5e409ffa send direct error message 2018-12-19 20:12:35 +11:00
mjarkk
800b40ecc4 Translated credentials error with git push/pull/fetch 2018-12-19 10:06:58 +01:00
mjarkk
097f687efe hopefully fixed circleci 2018-12-19 08:56:59 +01:00
Mark Kopenga
aa30e00643 Merge branch 'master' into https-ask-for-username-password 2018-12-19 08:44:14 +01:00
Jesse Duffield
cf56dcf9ff refresh files every 10 seconds rather than 2 so that we're not getting a heap of index lock errors 2018-12-18 23:04:32 +11:00
Jesse Duffield
c14a4eed0e bump modules and add bump_modules script 2018-12-18 23:03:26 +11:00
Jesse Duffield
a1b688f070 bump gocui fork to convert tabs to spaces 2018-12-18 23:03:26 +11:00
Cameron Nemo
4793232a35 Update gomodule checksums for go 1.11.4 2018-12-18 22:49:12 +11:00
Jesse Duffield
7835fce708 fix tests 2018-12-18 22:40:36 +11:00
Jesse Duffield
535152e15e bump go.mod 2018-12-18 22:37:10 +11:00
Jesse Duffield
160af3bb99 fix typo 2018-12-18 22:29:07 +11:00
Jesse Duffield
328b57e2cf no longer checking for 'exit status 128' because we're directly returning stderr 2018-12-18 22:27:50 +11:00
Jesse Duffield
20a94447d7 explicitly return newlines to our live command stdin 2018-12-18 22:23:17 +11:00
Jesse Duffield
865c7c2332 minor refactor of credentials panel into its own file 2018-12-18 22:19:32 +11:00
Jesse Duffield
11c7cbe3ac bump pty fork 2018-12-18 22:18:48 +11:00
Jesse Duffield
276ac3a92e decrease frequency of refreshing files because it's causing index lock errors 2018-12-18 21:28:09 +11:00
Jesse Duffield
a4beabf4b9 improved pre-push test script 2018-12-18 21:27:39 +11:00
Jesse Duffield
c35255b7a9 set stderr ourselves so that we only read the error output if there is any 2018-12-18 21:25:49 +11:00
Jesse Duffield
319064f040 switch to our own fork of pty which lets us set our own stdout and stderr 2018-12-18 21:25:28 +11:00
mjarkk
f5f726e9c4 A try to hide the password from the error output 2018-12-17 08:58:09 +01:00
Jesse Duffield
c56b303b29 add pre-hook to test credentials panel 2018-12-16 17:55:37 +11:00
Jesse Duffield
4886b8350e always hide rather than delete the credentials view, and don't log on error in case there is a user password in the error 2018-12-16 17:28:04 +11:00
mjarkk
af26b5f3e0 Tried to fix circleci 2018-12-14 13:45:43 +01:00
mjarkk
70cd6700e7 Tried to fix circleci 2018-12-14 13:43:13 +01:00
mjarkk
d11f8989d9 Merge branch 'https-ask-for-username-password' of https://github.com/mjarkk/lazygit into https-ask-for-username-password 2018-12-14 13:40:40 +01:00
mjarkk
0fca27d022 Tried to fix circleci 2018-12-14 13:40:29 +01:00
Mark Kopenga
255319e597 Merge branch 'master' into https-ask-for-username-password 2018-12-14 11:13:29 +01:00
mjarkk
5d038dfd33 Removed the wired error handling 2018-12-12 22:11:31 +01:00
mjarkk
0577d3b97f Removed the username / password savety check
This check is not realy needed because the change that it will show up a second time is so low that this is more work to check than the change it actualy might happen
2018-12-12 21:08:53 +01:00
Jesse Duffield
a26c15dafa some fixes for issues around the credentials panel 2018-12-12 22:34:20 +11:00
Jesse Duffield
c71bcc64ed move fetch keybinding to files view to make way for branch-specific fetched 2018-12-12 22:33:42 +11:00
Jesse Duffield
a365615490 only use subprocess for merging, not rebasing 2018-12-11 22:16:48 +11:00
Jesse Duffield
9489a94473 Make merge panel its own panel 2018-12-11 22:02:12 +11:00
Jesse Duffield
e0ff46fe53 more work on rebasing including visual indicators 2018-12-11 09:39:54 +11:00
Glenn Vriesman
cce6f405a5 Making ci happier 2018-12-11 09:39:54 +11:00
Glenn Vriesman
e39d2ed44b Added check to invoke continue/refresh 2018-12-11 09:39:54 +11:00
Glenn Vriesman
7a7e885773 Added rebase support commands 2018-12-11 09:39:54 +11:00
Glenn Vriesman
34fd18a395 Error handling 2018-12-11 09:39:54 +11:00
Glenn Vriesman
a1ee11e54e Added error check to satisfy ci 2018-12-11 09:39:54 +11:00
Glenn Vriesman
7b850c56c4 Added some translations 2018-12-11 09:39:54 +11:00
Glenn Vriesman
88c01c1ded Added rebase keybinding 2018-12-11 09:39:54 +11:00
Glenn Vriesman
27994f7de8 Added rebase handler 2018-12-11 09:39:54 +11:00
Glenn Vriesman
670f0e37c7 Added rebase functions 2018-12-11 09:39:54 +11:00
Glenn Vriesman
822dc5dada Moved push-pullables status to the end 2018-12-11 09:38:33 +11:00
mjarkk
e20d8366e1 Made gobot happy 2018-12-10 14:21:00 +01:00
mjarkk
76e9582739 Not always git fetch 2018-12-10 13:45:03 +01:00
mjarkk
50f20de8f3 Removed a lot of duplicated code 2018-12-10 08:22:52 +01:00
mjarkk
8e3f5e19e0 Changed some other names 2018-12-10 08:04:22 +01:00
mjarkk
61c2778de1 Changed pushPassUname name to credentials 2018-12-10 07:51:06 +01:00
mjarkk
3c17bf761a Better name for this function 2018-12-10 07:46:26 +01:00
mjarkk
696d6dc20c Fixed loop before error check 2018-12-10 07:43:32 +01:00
mjarkk
f14effe5f5 Worked and fixed a view comments 2018-12-09 13:04:19 +01:00
Tommy Nguyen
b95abd95ef Dockerfile: add alias 2018-12-09 16:36:12 +11:00
Tommy Nguyen
ea6712dec8 Remove -a -installsuffix; copy to /bin/ 2018-12-09 16:36:12 +11:00
Tommy Nguyen
de37a66ef3 Dockerfile improvements 2018-12-09 16:36:12 +11:00
mjarkk
efb82a58ae Tried to fix circleci error 2018-12-08 16:54:00 +01:00
Mark Kopenga
19a6a32625 Merge branch 'master' into https-ask-for-username-password 2018-12-08 16:41:39 +01:00
mjarkk
270658fc00 Made code ready to merge to master's latest commit 2018-12-08 16:40:22 +01:00
Jesse Duffield
ff856b7630 fetching branches without checking out 2018-12-08 11:51:47 +11:00
Jesse Duffield
ca3afa2a39 standardising how list panels deal with cursor movement 2018-12-08 11:51:47 +11:00
Jesse Duffield
99a8b1ae8b making a start on unidirectional data binding to fix these UI bugs 2018-12-08 11:51:47 +11:00
Jesse Duffield
ccc771d8b1 Merge pull request #342 from jesseduffield/hotfix/faster-reset-hard-head
Faster reset --hard head and clean as well
2018-12-08 10:04:27 +11:00
mjarkk
cf5a85b80f Intro message stays on screen now 2018-12-07 19:22:22 +01:00
mjarkk
2f7bd2896c Fixed error when there is no state.yml 2018-12-07 15:46:49 +01:00
mjarkk
8f904ffd72 Working popup 2018-12-07 14:56:29 +01:00
mjarkk
ced81e11f0 Only show private repo popup when opening repo for first time 2018-12-06 22:05:16 +01:00
mjarkk
6d0fa8bc29 Made some small inprovements 2018-12-06 09:05:51 +01:00
mjarkk
21a808a52b Renamed branch to branches 2018-12-06 08:39:49 +01:00
mjarkk
89c272eed5 Removed the tabs for spaces 2018-12-06 08:33:04 +01:00
Mark Kopenga
1b6d34e76a Merge branch 'master' into https-ask-for-username-password 2018-12-06 08:31:12 +01:00
mjarkk
6711543634 Made the gobot happy again 2018-12-06 08:28:56 +01:00
mjarkk
f6e83cdbdf Started working on the popup 2018-12-06 08:26:05 +01:00
Jesse Duffield
3b51d7cd00 clean as well as reset (I'm hoping this is a good design decision) 2018-12-05 20:06:47 +11:00
Jesse Duffield
66512ca253 use porcelain git rather than go-git for reset --hard HEAD because go-git takes over 5 seconds 2018-12-05 19:49:07 +11:00
Jesse Duffield
1a6a69a8f1 You can now stage changes one line at a time
Staging Lines
2018-12-05 19:40:22 +11:00
Jesse Duffield
933874fb25 dutch and polish translations to be updated 2018-12-05 19:33:54 +11:00
Jesse Duffield
c0f9795910 staging lines and hunks 2018-12-05 19:33:46 +11:00
Jesse Duffield
658e5a9faf initial support for staging individual lines 2018-12-04 22:11:48 +11:00
Jesse Duffield
99824c8a7b add patch modifier struct 2018-12-04 22:11:48 +11:00
Jesse Duffield
60060551bf Merge pull request #336 from BlakeMScurr/auto-fix-function-comments
Fix function comments with CodeLingo.
2018-12-03 09:33:42 +11:00
mjarkk
c269ad1370 Made the bot happy 2018-12-02 15:06:51 +01:00
mjarkk
2edd2b74ff Removed a lot of useless code 2018-12-02 14:58:18 +01:00
BlakeMScurr
181f91d2ef Add full stops to new comments. 2018-11-30 13:47:14 +13:00
BlakeMScurr
643cdd3461 Add simple comments to uncommented functions. 2018-11-30 11:04:08 +13:00
BlakeMScurr
5c70d2724b Fix function comments with CodeLingo. 2018-11-28 15:31:22 +13:00
Jesse Duffield
55712f509c Merge pull request #333 from jirfag/master
fix 'main' redefinition in scripts/ dir
2018-11-26 19:54:08 +11:00
mjarkk
d91493b587 Forgot to set the git fetch timeout back to 60s 2018-11-25 13:23:32 +01:00
mjarkk
9da1382e09 Added credentials popup 2018-11-25 13:15:36 +01:00
Denis Isaev
4e8e4612bd fix 'main' redefinition in scripts/ dir 2018-11-24 12:23:46 +03:00
mjarkk
adfc00bcdc Changed the waitForGroup to a channel 2018-11-23 13:58:30 +01:00
Mark Kopenga
b0eaf507a5 Merge branch 'master' into https-ask-for-username-password 2018-11-14 13:40:17 +01:00
Jesse Duffield
b9ecb82cb7 Merge pull request #328 from jesseduffield/feature/detached-heads
support Detached heads
2018-11-14 22:07:33 +11:00
mjarkk
448d9caf1b Fixed typo 2018-11-14 11:40:32 +01:00
Mark Kopenga
6d2bf0b0b5 Merge branch 'master' into https-ask-for-username-password 2018-11-14 11:34:39 +01:00
Jesse Duffield
5160668efd Merge branch 'master' into feature/detached-heads 2018-11-14 21:23:44 +11:00
Jesse Duffield
0eb1e4a86b change how we build our list of branches to support detached heads 2018-11-14 21:19:12 +11:00
mjarkk
0c4c00c1bf Removed useless channel read 2018-11-14 11:14:31 +01:00
Jesse Duffield
cc7d78f1ee Merge pull request #315 from glvr182/feature/translations
Fixed and added some dutch translations
2018-11-14 20:50:08 +11:00
Jesse Duffield
b8d5adcb84 Merge branch 'master' into feature/translations 2018-11-14 19:35:51 +11:00
Jesse Duffield
a5f483fae9 refactor obtaining current branch name 2018-11-14 19:08:42 +11:00
Jesse Duffield
775d910bdc Merge pull request #326 from KOREAN139/master
add scroll-past-bottom configuration option
2018-11-14 18:49:21 +11:00
mjarkk
18a1070c2c Trying to fix circleci 2018-11-10 18:24:37 +01:00
mjarkk
9fafd7ebc1 Fixed case that a commit message will break git push 2018-11-10 18:10:53 +01:00
mjarkk
bc14b01d03 just a test :) 2018-11-10 17:25:35 +01:00
mjarkk
80c6e0a8c4 Fixed pushing forever 2018-11-10 17:02:39 +01:00
mjarkk
8742c4c110 Removed some variables and placed them inside the gui struct 2018-11-10 09:27:03 +01:00
mjarkk
32ecc6d745 Removed getPushPassUnameView function 2018-11-10 09:09:18 +01:00
mjarkk
834e42897d Switched back to github.com/mgutz/str instaid of a copy of ToArgv 2018-11-10 08:57:02 +01:00
mjarkk
500267417b Removed some duplicated code 2018-11-10 08:46:42 +01:00
mjarkk
18bcc0df4d Fixed no error text on windows
when executing a command live on windows it detects errors but did just show a empty string
2018-11-10 08:43:02 +01:00
mjarkk
5ae0e75e5e Switched to channels instaid of a mutex 2018-11-10 08:39:09 +01:00
mjarkk
1fd8cadd9e Replaced regex with trim 2018-11-10 08:14:35 +01:00
KOREAN139
9d79d32c94 add scroll-past-bottom configuration option
with gui.scrollPastBottom option true, lazygit let user scroll past the
bottom - which is default
if option is false, user cannot scroll further when bottom of file has
appeared in mainView
2018-11-08 19:35:05 +09:00
Mark Kopenga
17b4b4cb33 Merge branch 'master' into https-ask-for-username-password 2018-11-08 10:31:10 +01:00
Jesse Duffield
79ef98739d Merge branch 'master' into feature/translations 2018-11-08 17:57:01 +11:00
Jesse Duffield
c2eaeab1f0 Merge pull request #314 from glvr182/feature/fix-keybind-crash
Fix bug related to limit view
2018-11-08 17:55:54 +11:00
Jesse Duffield
32d1289af7 Merge branch 'master' into feature/fix-keybind-crash 2018-11-08 17:49:21 +11:00
Jesse Duffield
ea55643cb2 Merge branch 'master' into feature/translations 2018-11-08 17:47:00 +11:00
Jesse Duffield
dcb6216713 Merge pull request #319 from mjarkk/fix-yt-link
Change the youtube video tutorial link to a short youtu.be link
2018-11-08 17:46:15 +11:00
mjarkk
9c8b241292 Removed some useless comments 2018-11-06 20:37:59 +01:00
mjarkk
7c4d360645 Better error for code 128 2018-11-06 20:25:11 +01:00
mjarkk
ad77ac639e Working new lines in live pty output 2018-11-06 20:24:10 +01:00
mjarkk
cf1e9f79b1 hopefully fixed the test now 2018-11-03 09:36:38 +01:00
mjarkk
8f0741a458 Changed the youtube link to a youtu.be short link 2018-11-03 09:19:47 +01:00
Mark Kopenga
6f2b62f729 Merge pull request #15 from jesseduffield/master
Update to latest master
2018-11-03 09:17:43 +01:00
mjarkk
8469239d84 Fixed test 2018-11-03 09:12:45 +01:00
mjarkk
af54d7f015 Fixed view not defined error with git push and pull 2018-11-02 15:07:10 +01:00
mjarkk
cb9ad5bc73 Fixed golangcibot surgestion 2018-11-02 10:06:10 +01:00
mjarkk
5470bb4121 Added username password detect to git pull 2018-11-02 09:54:54 +01:00
mjarkk
0e53a26d6f Maybe fixed the test this time 2018-11-01 07:06:34 +01:00
mjarkk
3938138ebc Hopefully fixed circleci 2018-10-31 19:25:52 +01:00
mjarkk
05f0e5120a Fixed one text 2018-10-31 17:55:02 +01:00
mjarkk
5532289086 Fixed some tests 2018-10-31 17:36:20 +01:00
mjarkk
78b2bc4f60 Made a better way of test pushing 2018-10-31 16:23:58 +01:00
Glenn Vriesman
d33f89fd60 Merge branch 'master' into feature/fix-keybind-crash 2018-10-31 15:04:34 +01:00
Glenn Vriesman
c0da212f54 Merge branch 'master' into feature/translations 2018-10-31 15:04:17 +01:00
mjarkk
9585f49490 Made error handling better 2018-10-29 08:23:56 +01:00
Mark Kopenga
1e0310a86d Merge pull request #14 from jesseduffield/master
Merged master into https-ask-for-username-password
2018-10-29 07:50:44 +01:00
Jesse Duffield
bf45e5b0e3 Merge pull request #313 from dbast/master
Add conda to readme as possibility to install on different platforms
2018-10-29 16:11:27 +11:00
Glenn Vriesman
9a0f094f58 Changed some things after marks feedback 2018-10-28 20:33:42 +01:00
Glenn Vriesman
ee89ad6ae7 Fixed test 2018-10-28 20:13:55 +01:00
Glenn Vriesman
22e5aafd59 Fixed and added some dutch translations 2018-10-28 19:58:20 +01:00
Glenn Vriesman
abd0803ef4 Set current view to limit view 2018-10-28 19:15:34 +01:00
mjarkk
372b333662 Added copy note at top of string-to-args.go 2018-10-27 17:08:25 +02:00
mjarkk
18f09a14e6 Removed package github.com/mgutz/str for better code coverage 2018-10-27 17:06:33 +02:00
mjarkk
ed564adb4a Removed github.com/ionrock/procs for better code coverage 2018-10-27 16:57:34 +02:00
mjarkk
9a99748d3b Fixed bug where username input didn't go away 2018-10-27 16:29:16 +02:00
mjarkk
9163110640 Removed error check for regex
Regexp only returns an error when regex string is in-corret
2018-10-27 15:52:12 +02:00
mjarkk
6c1c110ce0 Made tests pass
Git constandly exits with error code 1 for some reason it might be because of the wrong username and password but i don't think error 1 is for wrong credentials
2018-10-27 15:32:12 +02:00
Mark Kopenga
45c249acca Merge branch 'master' into https-ask-for-username-password 2018-10-27 15:06:51 +02:00
mjarkk
1df1053947 Fixed test 2018-10-27 15:01:16 +02:00
mjarkk
14cff0bd07 Fixed circleci build 2018-10-27 14:57:44 +02:00
mjarkk
87d1b9a547 Fixed circleci build 2018-10-27 14:56:15 +02:00
mjarkk
959d6fa2ca Made it possible to build for windows again 2018-10-27 14:37:31 +02:00
mjarkk
e47c597b3a Made it possible to build again for windows 2018-10-27 14:35:07 +02:00
Daniel Bast
bfcb348923 Add conda to readme as possibility to install on different platforms 2018-10-25 09:54:01 +02:00
Jesse Duffield
c6da4c8a47 Merge pull request #301 from rozuur/master
Initial version of delete named branch added
2018-10-24 09:24:16 +11:00
Mark Kopenga
1fedda6a75 Merge branch 'master' into https-ask-for-username-password 2018-10-23 13:52:50 +02:00
Jesse Duffield
467775fc1c Merge branch 'master' into master 2018-10-23 09:43:25 +11:00
Jesse Duffield
3b6b68a2a1 Merge pull request #305 from kristijanhusak/feature/create-pull-request
Add option to create pull request from branches panel.
2018-10-22 11:51:16 +11:00
Naveen Vardhi
3a23cb87b7 Remove force delete keybinding
And force delete messages gives feedback about merge status
2018-10-21 21:13:24 +05:30
mjarkk
ac03665df3 Handled some errors 2018-10-20 19:44:56 +02:00
mjarkk
1be44eae84 Fixed surgestion from golangcibot 2018-10-20 19:03:51 +02:00
mjarkk
b72841ca0c Fixed golangcibot surgestions 2018-10-20 18:58:37 +02:00
mjarkk
12425f0aa7 First good success 2018-10-20 17:37:55 +02:00
mjarkk
727ba9f42e test 2018-10-20 17:21:17 +02:00
mjarkk
73a0a65ee1 test 2018-10-20 17:20:52 +02:00
mjarkk
ac5696574c added some translations 2018-10-20 17:18:42 +02:00
mjarkk
1a43d64de3 Added extra translations 2018-10-20 16:25:48 +02:00
Kristijan Husak
990dc8c4ea Add separate open command for links and check if branch exists on remote before opening pull request link. 2018-10-20 11:58:08 +02:00
Jesse Duffield
59cdd7d46e Merge branch 'master' into master 2018-10-20 10:53:02 +11:00
mjarkk
4451cbc50b handled golangcibot 2018-10-17 21:14:24 +02:00
mjarkk
01fa106de3 Added files to commit pannelsidjfjlisdjfilisldjfiljisldjilfjisdjilfjlsidMoved some code around 2018-10-17 21:12:33 +02:00
mjarkk
9fc4262887 small code addition 2018-10-17 20:41:22 +02:00
mjarkk
cecd5733a8 Basic file for getting the fix working 2018-10-17 20:38:13 +02:00
Mark Kopenga
1d733f3adc Merge pull request #13 from jesseduffield/master
Updated to latest master
2018-10-17 20:22:41 +02:00
Kristijan Husak
c69fce2e9d Remove unnecessary nil error in NewPullRequest. 2018-10-15 11:00:19 +02:00
Kristijan Husak
df0e3e52fe Add option to create pull request form branches panel. 2018-10-13 22:54:51 +02:00
Jesse Duffield
d5f64602a8 Merge pull request #283 from kristijanhusak/feature/commit-amend
Add action for amending a commit
2018-10-10 17:50:55 +11:00
Kristijan Husak
4287f8ae90 Fix tests and add test scenarios for amend. 2018-10-08 22:19:42 +02:00
Kristijan Husak
190309e5c1 Check if there is any commit to amend and use 'A' instead of 'M' as shortcut. 2018-10-08 21:19:45 +02:00
Jesse Duffield
ac65586bd5 Merge branch 'master' into feature/commit-amend 2018-10-07 21:08:46 +11:00
Naveen Vardhi
af8d362caa Initial version of delete named branch added 2018-10-06 17:04:33 +05:30
Kristijan Husak
5f7ac97a39 Refresh side panels and use uppercase HEAD in all git commands that requires it. 2018-10-06 09:53:54 +02:00
Jesse Duffield
b8b59baa27 Merge pull request #290 from jesseduffield/feature/informative-commit-colors
Color merged and unmerged commits differently
2018-10-05 09:37:55 +10:00
Jesse Duffield
2be613679e more test coverage 2018-10-05 09:11:19 +10:00
Kristijan Husak
28fe3d6cf9 Use confirmation popup for amending last commit. 2018-09-25 22:11:51 +02:00
Kristijan Husak
b6b21bc98e Merge branch 'master' of git://github.com/jesseduffield/lazygit into feature/commit-amend 2018-09-25 21:17:53 +02:00
Jesse Duffield
eb69d98f99 add test for CurrentBranchName 2018-09-25 20:31:19 +10:00
Jesse Duffield
fb9596a3ff add test for getMergeBase 2018-09-25 20:25:04 +10:00
Jesse Duffield
0d33a746ba Merge branch 'feature/informative-commit-colors' of https://github.com/jesseduffield/lazygit into feature/informative-commit-colors 2018-09-25 20:11:36 +10:00
Jesse Duffield
f3fc98a3d0 support git flow when colouring commits 2018-09-25 20:11:33 +10:00
Jesse Duffield
17d7bcdeaf Merge branch 'master' into feature/informative-commit-colors 2018-09-25 20:10:12 +10:00
Jesse Duffield
d0a3f1eecf Merge pull request #291 from antham/add-tests-part-7
Add tests to pkg/commands/git - Part 7
2018-09-25 19:13:35 +10:00
Jesse Duffield
7164f37266 Merge branch 'master' into feature/commit-amend 2018-09-25 19:05:24 +10:00
Jesse Duffield
e9245cd53b Merge branch 'master' into add-tests-part-7 2018-09-25 19:03:29 +10:00
Jesse Duffield
80d6bbef86 Merge branch 'master' of https://github.com/jesseduffield/lazygit 2018-09-23 14:13:14 +10:00
Jesse Duffield
3d751c03fe add donation link to status panel 2018-09-23 14:13:10 +10:00
Jesse Duffield
4e0d0f7d75 Update README.md 2018-09-23 13:09:54 +10:00
Jesse Duffield
b8b3eee961 Merge pull request #300 from jesseduffield/hotfix/273-cursor-scrolling
Fix slow scrolling
2018-09-22 13:59:49 +10:00
Jesse Duffield
7947668e18 Merge branch 'hotfix/273-cursor-scrolling' of https://github.com/jesseduffield/lazygit into hotfix/273-cursor-scrolling 2018-09-22 13:50:01 +10:00
Jesse Duffield
619c28ce56 use lineheight rather than buffer length 2018-09-22 13:49:58 +10:00
Jesse Duffield
53aef7846a Merge branch 'master' into hotfix/273-cursor-scrolling 2018-09-22 13:47:38 +10:00
Jesse Duffield
227067fdd1 use lineheight rather than buffer length 2018-09-22 13:44:48 +10:00
Jesse Duffield
3df4a9484f Merge pull request #295 from jesseduffield/hotfix/commit-message-panel-focus
Fix issues with commit message panel losing focus
2018-09-22 13:42:27 +10:00
Jesse Duffield
2229a6e133 Merge branch 'master' into hotfix/commit-message-panel-focus 2018-09-21 16:43:37 +10:00
Jesse Duffield
3101c50582 Merge pull request #297 from jesseduffield/hotfix/remove-files
add removeAll to git
2018-09-21 09:25:55 +10:00
Jesse Duffield
70ee4faf15 add removeAll to git 2018-09-21 09:23:00 +10:00
Anthony HAMON
360b7c1def commands/git : refactor test to Diff, refactor function 2018-09-20 09:11:47 +02:00
Anthony HAMON
bdeb78c9a0 commands/git : returns an error instead of panicing 2018-09-20 09:09:37 +02:00
Anthony HAMON
9481920101 commands/git : add test to GetLog 2018-09-20 09:09:37 +02:00
Jesse Duffield
a2b3cd0823 add removeAll to git 2018-09-20 09:48:56 +10:00
Jesse Duffield
8fac19c175 Merge branch 'master' into feature/informative-commit-colors 2018-09-20 09:41:29 +10:00
Jesse Duffield
b9708c9f88 fix issues with commit message panel losing focus 2018-09-19 20:36:40 +10:00
Jesse Duffield
7b90d2496b Merge pull request #294 from jesseduffield/feature/recent-repos
Recent Repos Menu
2018-09-19 20:22:36 +10:00
Jesse Duffield
0367399cf3 bump deps to use forked termbox which doesn't crash as easily 2018-09-19 20:16:22 +10:00
Jesse Duffield
3072c93e13 Merge pull request #280 from jesseduffield/hotfix/cursor-positioning
Fix cursor positioning bugs
2018-09-19 19:55:14 +10:00
Jesse Duffield
4ea446205c Merge branch 'hotfix/cursor-positioning' into feature/recent-repos 2018-09-19 19:32:11 +10:00
Jesse Duffield
5a76b57952 one more spec to increase coverage 2018-09-19 19:31:29 +10:00
Jesse Duffield
6de291ff44 Merge branch 'hotfix/cursor-positioning' into feature/recent-repos 2018-09-19 19:23:42 +10:00
Jesse Duffield
64f0eeb42e fix specs 2018-09-19 19:23:31 +10:00
Jesse Duffield
baa9eff318 Merge branch 'hotfix/cursor-positioning' into feature/recent-repos 2018-09-19 19:17:05 +10:00
Jesse Duffield
fcaf4e339c fix specs 2018-09-19 19:16:55 +10:00
Jesse Duffield
e91fb21233 add recent repos menu option 2018-09-19 19:15:29 +10:00
Jesse Duffield
99a6439641 Merge branch 'master' into hotfix/cursor-positioning 2018-09-19 18:42:25 +10:00
Jesse Duffield
768b9453f8 Merge branch 'hotfix/cursor-positioning' into feature/recent-repos 2018-09-19 18:40:41 +10:00
Jesse Duffield
e95b2e5f0b update specs 2018-09-19 18:31:54 +10:00
Jesse Duffield
950cfeff6f add specs for menu utils 2018-09-19 18:19:26 +10:00
Jesse Duffield
fce895ed0d Merge pull request #287 from antham/add-tests-part-6
Add tests part 6
2018-09-18 21:47:46 +10:00
Jesse Duffield
c789bba673 color merged and unmerged commits differently 2018-09-18 21:45:35 +10:00
Jesse Duffield
b384fcf6af generalise popup menu panel 2018-09-18 21:07:25 +10:00
Anthony HAMON
60cf549a32 commands/git : reverse the logic 2018-09-18 09:23:41 +02:00
Anthony HAMON
6f0b32f95e commands/git : add GetCommits tests refactor
* switch GetCommitsToPush scope to private
* return a map instead of slice for look up
* remove useless includesString function
2018-09-17 21:19:17 +02:00
Jesse Duffield
f89bc10af1 appease golangci 2018-09-17 21:32:19 +10:00
Jesse Duffield
a66ac8092e minor refactor 2018-09-17 21:27:53 +10:00
Jesse Duffield
bd04ecff69 Merge branch 'master' into hotfix/cursor-positioning 2018-09-17 21:03:29 +10:00
Jesse Duffield
c00c834b35 standardise rendering of lists in panels 2018-09-17 21:02:30 +10:00
Anthony HAMON
9d9d775f50 circle : remove new line 2018-09-16 22:49:17 +02:00
Anthony HAMON
38036e0d20 circle : kill old cache 2018-09-16 22:41:41 +02:00
Anthony HAMON
9713a15167 commands/git : add test to GetBranchGraph, refactor 2018-09-16 22:12:03 +02:00
Anthony HAMON
b641d6bd96 commands/git : add test to Checkout, refactor 2018-09-16 22:08:23 +02:00
Anthony HAMON
67a42f49b4 commands/git : add test to RemoveFile, refactor 2018-09-16 22:03:56 +02:00
Anthony HAMON
bbc88071e9 gui : remove unreachable code 2018-09-16 20:46:25 +02:00
Jesse Duffield
ca2eec60fe Merge pull request #285 from antham/add-tests-part-5
Add tests to pkg/commands/git - Part 5
2018-09-16 22:03:10 +10:00
Anthony HAMON
c1b7a21631 commands/git : move tests 2018-09-16 11:11:09 +02:00
Anthony HAMON
91832f2c5e commands/git : add tests, refactor a bit 2018-09-16 11:11:09 +02:00
Mark Kopenga
c64fb87b2b Merge pull request #12 from jesseduffield/master
Update to latest master
2018-09-14 11:50:49 +02:00
Jesse Duffield
fa08c6c2a2 Merge pull request #284 from mingrammer/main-error
main: display an error message instead of panic when setup fails
2018-09-14 09:38:21 +10:00
mingrammer
3cf84a5af1 main: display an error message instead of panic when setup fails 2018-09-14 00:23:11 +09:00
Kristijan Husak
61f0801bd3 Add ammend commit action. 2018-09-13 12:44:32 +02:00
Jesse Duffield
eb4b5cd43b Merge pull request #282 from antham/add-tests-part-4
Add tests to pkg/commands/git - Part 4
2018-09-13 09:34:28 +10:00
Anthony HAMON
c92510ceba commands/git : add tests on SquashFixupCommit and refactor 2018-09-12 22:45:52 +02:00
Anthony HAMON
65a24d70c3 commands/git : add tests on SquashPreviousTwoCommits 2018-09-12 20:43:03 +02:00
Jesse Duffield
7fb2cafd0c Merge pull request #279 from jesseduffield/hotfix/file-ordering
Restore old file merging algorithm
2018-09-12 15:20:20 +02:00
Jesse Duffield
3b765e5417 add test for min method 2018-09-12 19:39:36 +10:00
Jesse Duffield
57f6a552d2 Merge branch 'hotfix/cursor-positioning' of https://github.com/jesseduffield/lazygit into hotfix/cursor-positioning 2018-09-12 18:49:14 +10:00
Jesse Duffield
35cae80de9 more efficient building of branch displaystrings 2018-09-12 18:49:09 +10:00
Jesse Duffield
b4b4cd83dd Merge branch 'master' into hotfix/cursor-positioning 2018-09-12 18:47:57 +10:00
Jesse Duffield
31c33dfdcb remove redundant comments 2018-09-12 18:47:37 +10:00
Jesse Duffield
79940b7ba9 Merge pull request #279 from jesseduffield/hotfix/file-ordering
Restore old file merging algorithm
2018-09-12 18:26:29 +10:00
Jesse Duffield
2ce8ac5850 restore old file sorting algorithm 2018-09-12 18:24:03 +10:00
Jesse Duffield
f8b484f638 don't use newlines at the end of panel buffers 2018-09-12 18:23:25 +10:00
Jesse Duffield
73e2c1005a Merge pull request #277 from antham/add-tests-part-3
Add tests to pkg/commands/git - Part 3
2018-09-12 17:35:09 +10:00
Anthony HAMON
97e0a6dc45 commands/git : remove extra space 2018-09-12 07:51:14 +02:00
Anthony HAMON
9bad0337fe commands/git : swap global/local get config 2018-09-12 07:50:49 +02:00
Anthony HAMON
f03544f392 commands/git : fix test 2018-09-11 22:20:59 +02:00
Anthony HAMON
0aba49af2b commands/git : fix typo 2018-09-11 21:56:17 +02:00
Anthony HAMON
ccbc5e569c commands/git : add test to Push func, refactor 2018-09-11 21:56:17 +02:00
Anthony HAMON
415aad600c commands/git : add test to Commit func, refactor 2018-09-11 21:56:17 +02:00
Anthony HAMON
d23577168f commands/git : remove dependency on gocui 2018-09-11 21:56:17 +02:00
Anthony HAMON
5c204b2813 commands/git: rewrite UsingGpg, add tests 2018-09-11 21:56:17 +02:00
Jesse Duffield
7d86278507 Merge pull request #274 from sascha-andres/master
fix: escape quote character on Linux
2018-09-11 09:07:36 +10:00
Sascha Andres
985196f5aa docs: add comments for new test code 2018-09-10 17:36:59 +02:00
Jesse Duffield
52b132fe01 better handling of cursor and origin positionings 2018-09-10 20:17:39 +10:00
Sascha Andres
9ec5a04cf5 merge: remote-tracking branch 'upstream/master' 2018-09-10 10:33:20 +02:00
Sascha Andres
bb9698810d refactor: move fallback to platform struct
Co-authored-by: Jesse Duffield <jessedduffield@gmail.com>
2018-09-10 10:31:15 +02:00
Jesse Duffield
7f4371ad71 Merge pull request #275 from mjarkk/master
Translated untranslated dutch phrases
2018-09-10 18:15:15 +10:00
Sascha Andres
74144e3892 fix: remove call to fmt.Println 2018-09-10 10:10:10 +02:00
mjarkk
07f87eb7cf Added missing dutch translations 2018-09-10 08:08:53 +02:00
Mark Kopenga
5600da9ee7 Merge pull request #11 from jesseduffield/master
updated to latest master
2018-09-10 07:28:11 +02:00
Sascha Andres
ba0cc20e22 feat: add test cases 2018-09-10 06:51:19 +02:00
Sascha Andres
717913e64c fix: escape quote character on Linux
Co-authored-by: Dawid Dziurla <dawidd0811@gmail.com>

Closes #269
2018-09-10 06:36:15 +02:00
Jesse Duffield
5a0431bb62 Merge pull request #272 from antham/master
Add missing go.sum file
2018-09-10 10:58:22 +10:00
Jesse Duffield
1e357e2362 Merge branch 'master' into master 2018-09-10 10:02:06 +10:00
Jesse Duffield
c3b62a555c Merge pull request #265 from antham/add-tests-part-2
Add tests to pkg/commands/git - Part 2
2018-09-10 10:01:55 +10:00
Jesse Duffield
557461c016 Merge branch 'master' into add-tests-part-2 2018-09-10 09:47:14 +10:00
Jesse Duffield
c2e670104e Merge branch 'master' into master 2018-09-10 09:46:01 +10:00
Jesse Duffield
bea9971f9c Merge pull request #271 from glvr182/feature/ignore-hidden-files
Change the gitignore a bit to cover more
2018-09-10 09:45:29 +10:00
Jesse Duffield
a8fdf1a646 Merge branch 'master' into feature/ignore-hidden-files 2018-09-10 09:45:18 +10:00
Jesse Duffield
f8ab4f4073 Merge pull request #234 from dawidd6/feature/help
WIP: Add menu panel (cheatsheet)
2018-09-10 09:43:28 +10:00
Anthony HAMON
24f15742d0 commands/git : rename variable 2018-09-09 20:08:46 +02:00
Glenn Vriesman
700f8c7e79 REPO: Added TODO entries to the gitignore for jesse 2018-09-09 13:37:38 +02:00
Anthony HAMON
28e8d6e472 add go.sum 2018-09-09 11:13:18 +02:00
Anthony HAMON
6076a75643 commands/git : fix function call 2018-09-09 10:52:34 +02:00
Anthony HAMON
b46e4b4976 commands/git : add several tests, do some cleanup 2018-09-09 10:52:34 +02:00
Anthony HAMON
99eca7b000 commands/git : replace make function 2018-09-09 10:52:34 +02:00
Anthony HAMON
a0faaf6893 commands/git : remove includes function 2018-09-09 10:52:34 +02:00
Anthony HAMON
56ad07ebab commands/git : rename functions 2018-09-09 10:52:34 +02:00
Anthony HAMON
1ecd74c357 commands/git : add tests for GetCommitsToPush 2018-09-09 10:52:34 +02:00
Anthony HAMON
ceab9706cb commands/git : add tests for UpstreamDifferentCount 2018-09-09 10:52:34 +02:00
Anthony HAMON
1cc7e9c02a rewrite to use subtests 2018-09-09 10:52:34 +02:00
Dawid Dziurla
63e400647a shorter english string 2018-09-09 10:47:13 +02:00
Dawid Dziurla
6f7de83bce Merge branch 'master' into feature/help
conflicts resolved
2018-09-09 10:41:01 +02:00
Jesse Duffield
5af03b6820 Merge pull request #264 from antham/master
Add go.mod to support modules landed in go1.11
2018-09-09 11:35:48 +10:00
Glenn Vriesman
a8866b158b REPO: Added and modified some ignore statements 2018-09-08 14:25:33 +02:00
antham
0d7a697e86 add go.mod 2018-09-07 21:35:04 +02:00
Dawid Dziurla
e80371fc6f satisfy golangci 2018-09-07 14:41:01 +02:00
Dawid Dziurla
9cef98f779 ladies and gentlemen...
this is fmt number x+1
2018-09-07 14:23:08 +02:00
Dawid Dziurla
ba6dedfb22 rewrite some of menu panel logic
panel keybindings are now on top and
global keybindings are below separated with empty newline
2018-09-07 14:19:16 +02:00
Jesse Duffield
ca715c5b23 support switching to recent repo 2018-09-07 09:41:15 +10:00
Jesse Duffield
ba7e6add86 Merge pull request #267 from glvr182/feature/add-effgo-link
Added entry to the contributing guide
2018-09-07 09:30:29 +10:00
Glenn Vriesman
a820189450 Added entry to the contributing guide 2018-09-06 17:31:19 +02:00
Jesse Duffield
ce95e6771a Merge pull request #263 from dawidd6/feature/confirm_quit
Add confirmOnQuit config option
2018-09-06 09:33:06 +10:00
Dawid Dziurla
e9268d1828 add confirmOnQuit config option 2018-09-05 19:56:11 +02:00
Dawid Dziurla
db2e2160a9 change menu keybinding from ? to x 2018-09-05 15:55:24 +02:00
Dawid Dziurla
08395ae76c workaround to include menu keybinding in cheatsheet 2018-09-05 15:45:20 +02:00
Dawid Dziurla
4188786749 update pl translation 2018-09-05 15:20:34 +02:00
Jesse Duffield
cf41338a9f Merge pull request #262 from jesseduffield/feature/50/jesse
Add commit counter using subtitle
2018-09-05 23:09:20 +10:00
Jesse Duffield
a2d40cfbf1 allow users to configure whether the commit length is shown 2018-09-05 23:02:13 +10:00
Dawid Dziurla
34d1648bd3 fmt strikes again 2018-09-05 13:23:06 +02:00
Dawid Dziurla
906f8e252e include global keybindings in menu 2018-09-05 13:16:40 +02:00
Jesse Duffield
986774e5c7 add commit count via gocui subtitle 2018-09-05 20:43:45 +10:00
Dawid Dziurla
98763e98cb initial commit message counter 2018-09-05 11:26:54 +02:00
Jesse Duffield
f777c60ea4 Merge pull request #260 from jesseduffield/hotfix/258-commit-message-newlines
Fix popup panel resizing
2018-09-05 19:15:39 +10:00
Dawid Dziurla
557009e660 help -> menu 2018-09-05 11:12:11 +02:00
Jesse Duffield
af876d2be2 Merge branch 'master' into hotfix/258-commit-message-newlines 2018-09-05 19:11:30 +10:00
Jesse Duffield
422b263df4 fix popup panel resizing 2018-09-05 19:10:46 +10:00
Jesse Duffield
4fc290b101 Update README.md 2018-09-05 18:44:24 +10:00
Jesse Duffield
c1bf1e52b0 Merge pull request #257 from antham/master
Increase circleci compile speed
2018-09-05 17:12:29 +10:00
Anthony HAMON
4d745fa525 update cache path in circleci 2018-09-05 08:55:15 +02:00
Jesse Duffield
f0e19690f5 Merge pull request #254 from jesseduffield/feature/dockerfile-for-alpine
Dockerfile for testing on alpine linux
2018-09-05 14:20:51 +10:00
Jesse Duffield
580c1cbc89 Merge branch 'master' into feature/dockerfile-for-alpine 2018-09-05 11:03:17 +10:00
Dawid Dziurla
e21f739f4f add renderGlobalOptions
render only global options for all panels
2018-09-04 16:07:31 +02:00
Dawid Dziurla
97ad4a1643 delete options 2018-09-04 15:40:29 +02:00
Dawid Dziurla
cbafadd48e move keys slice to guiState struct 2018-09-04 15:29:43 +02:00
Dawid Dziurla
7b84c162f4 set help panel fgcolor to white 2018-09-04 15:25:54 +02:00
Dawid Dziurla
f29c81fb5c add getMaxKeyLength 2018-09-04 15:25:02 +02:00
Jesse Duffield
59003c8bbf Merge pull request #253 from jesseduffield/hotfix/238-xdg-open-2
Update xdg-open command
2018-09-04 22:21:03 +10:00
Jesse Duffield
6c1d133315 dockerfile for testing on alpine linux 2018-09-04 22:19:08 +10:00
Jesse Duffield
33ea093d88 Merge branch 'master' into hotfix/238-xdg-open-2 2018-09-04 20:37:35 +10:00
Jesse Duffield
a81f8b84e3 Merge pull request #245 from antham/master
Rewrite SetupGit in GitCommand
2018-09-04 19:29:21 +10:00
Jesse Duffield
3f68fe42cb update xdg-open command 2018-09-04 19:18:18 +10:00
Anthony HAMON
172cd7c687 fix tests locally 2018-09-04 08:32:43 +02:00
Anthony HAMON
df3e7abd68 use RunCommand 2018-09-04 08:32:40 +02:00
Anthony HAMON
8c67578063 replace fmt with errors 2018-09-04 06:21:58 +02:00
Anthony HAMON
06846ef3ae rename NewApp to Setup 2018-09-04 06:21:58 +02:00
Anthony HAMON
43ad9a81c2 merge setup in function that create a new git command 2018-09-04 06:21:58 +02:00
Anthony HAMON
9f7775df26 pkg/git : remove unused Map function 2018-09-04 06:21:58 +02:00
Anthony HAMON
c1984528c8 pkg/git : add tests for SetupGit 2018-09-04 06:21:58 +02:00
Anthony HAMON
624d63d2fa pkg/git : remove panic in SetupGit method 2018-09-04 06:21:58 +02:00
Dawid Dziurla
67d99a24ea get selected branch from correct panel 2018-09-03 18:45:52 +02:00
Dawid Dziurla
bf8514f5e2 helperize spaces 2018-09-03 18:44:56 +02:00
Dawid Dziurla
93e88ea8fe add missing translated polish strings 2018-09-03 18:34:04 +02:00
Dawid Dziurla
a2073528f4 add missing untranslated dutch strings
@mjarkk
2018-09-03 18:33:38 +02:00
Dawid Dziurla
230a5afa4c remove capitalization of keybindings descriptions 2018-09-03 18:16:54 +02:00
Dawid Dziurla
c49e4dc287 get item position from correct panel 2018-09-03 18:07:38 +02:00
Dawid Dziurla
59f50010b6 apply fmt again 2018-09-03 18:01:07 +02:00
Dawid Dziurla
b5827b7d80 merge conflict effect fix 2018-09-03 17:57:03 +02:00
Dawid Dziurla
36874be45b apply very important fmt 2018-09-03 17:54:06 +02:00
Dawid Dziurla
83b7c60246 add untranslated dutch strings
@mjarkk
2018-09-03 17:54:06 +02:00
Dawid Dziurla
323016aa01 polish translation 2018-09-03 17:54:06 +02:00
Dawid Dziurla
b403b6e46d better english string 2018-09-03 17:54:06 +02:00
Dawid Dziurla
359636c1aa add generate_cheatsheet script
script is generating markdown document with small cheatsheet
in selected language
2018-09-03 17:54:06 +02:00
Dawid Dziurla
1fa55875e2 remove testing content 2018-09-03 17:54:06 +02:00
Dawid Dziurla
5177e458ef use Fprint instead of renderString
renderString is wrapping content
because of that lines are being select wrong
2018-09-03 17:54:06 +02:00
Dawid Dziurla
314c8c279a apply fmt on keybindings 2018-09-03 17:54:06 +02:00
Dawid Dziurla
20073d0293 don't panic
"panic: runtime error: index out of range"

when executing stash pop 'g' from help menu
2018-09-03 17:54:06 +02:00
Dawid Dziurla
90a4cada82 add missing descriptions 2018-09-03 17:54:06 +02:00
Dawid Dziurla
e376de6d1a explicitly delete 'help' view 2018-09-03 17:54:06 +02:00
Dawid Dziurla
265d7e121a use Key if it's a rune, otherwise KeyReadable 2018-09-03 17:54:06 +02:00
Dawid Dziurla
7ec5b6cc30 indent keybindings 2018-09-03 17:54:06 +02:00
Dawid Dziurla
9ceaf5b9a9 move descriptions to i18n 2018-09-03 17:52:05 +02:00
Dawid Dziurla
cc3fa4b79d make '?' key visible on every panel 2018-09-03 17:52:05 +02:00
Dawid Dziurla
653d590157 help panel size from getConfirmationPanelDimensions 2018-09-03 17:52:05 +02:00
Dawid Dziurla
8a01d11202 more error checks 2018-09-03 17:52:05 +02:00
Dawid Dziurla
28a9594ef7 update help panel
- delete scrolling ability
- lines are now selectable
- implemented handler execution when space is pressed
- add example descriptions for status panel keybindings
2018-09-03 17:52:05 +02:00
Dawid Dziurla
77623db1d0 apply fmt 2018-09-03 17:52:05 +02:00
Dawid Dziurla
6a99d36ae1 change key from 'H' to '?' 2018-09-03 17:52:05 +02:00
Dawid Dziurla
2416f585ce initial help panel 2018-09-03 17:52:05 +02:00
Dawid Dziurla
741e28d01a move bindings to getKeybindings() 2018-09-03 17:52:05 +02:00
Jesse Duffield
19a8029795 Merge pull request #250 from dawidd6/fix/tests
Fix testing with localized rmdir + don't test scripts directory
2018-09-03 22:04:16 +10:00
Jesse Duffield
796f17eef4 Merge branch 'master' into fix/tests 2018-09-03 21:20:17 +10:00
Jesse Duffield
8fbb3aea4f Merge pull request #242 from d-dorazio/rename-commits-in-user-editor
add keybinding to open user editor when renaming last commit
2018-09-03 20:11:13 +10:00
Jesse Duffield
6fc4cb1b96 Merge branch 'master' into rename-commits-in-user-editor 2018-09-03 19:53:16 +10:00
Jesse Duffield
3c1935fee4 Merge pull request #249 from jesseduffield/hotfix/238-xdg-open
238: opening with xdg-open
2018-09-03 19:48:42 +10:00
Jesse Duffield
dfb87d34dc Merge branch 'master' into hotfix/238-xdg-open 2018-09-03 19:42:06 +10:00
Jesse Duffield
4ab1a1f72b Merge branch 'master' into hotfix/238-xdg-open 2018-09-03 19:33:37 +10:00
Jesse Duffield
a9cd277070 add test for ResolvePlaceholderString 2018-09-03 19:31:27 +10:00
Dawid Dziurla
5c1463313d respect localized output of rmdir 2018-09-01 17:22:49 +02:00
Dawid Dziurla
0355fc3008 don't run tests on scripts/ directory 2018-09-01 17:22:23 +02:00
Daniele D'Orazio
39f065207e add simple test for PrepareCommitAmendSubProcess 2018-09-01 12:29:43 +02:00
Daniele D'Orazio
9e6a4a529a add keybinding to open user editor when renaming last commit 2018-09-01 12:14:42 +02:00
Jesse Duffield
87de803a6c update default config header 2018-09-01 14:38:30 +10:00
Jesse Duffield
3cafa2bb12 update config to reflect platform specific defaults 2018-09-01 14:35:46 +10:00
Jesse Duffield
d31520261f introduce platform specific defaults 2018-09-01 14:33:01 +10:00
Jesse Duffield
ad880e2d56 wrap windows start command in shell 2018-09-01 13:28:10 +10:00
Jesse Duffield
865809e625 better error handling for commands 2018-09-01 13:27:58 +10:00
Jesse Duffield
04d5a473d7 use start instead of cygstart to open files on windows 2018-09-01 12:53:51 +10:00
Jesse Duffield
f127ae62bb update config 2018-09-01 12:18:16 +10:00
Jesse Duffield
3f14b764d5 update tests 2018-09-01 12:13:41 +10:00
Jesse Duffield
ae0d88f855 WIP using runDirectCommand with xdg-open 2018-09-01 11:38:32 +10:00
Jesse Duffield
b65fa852f1 Merge pull request #248 from jesseduffield/hotfix/better-backwards-compatibility-error
Better error for nonbackwards compatible changes
2018-09-01 11:26:58 +10:00
Jesse Duffield
42500817e0 better error for nonbackwards compatible changes 2018-09-01 10:36:55 +10:00
Jesse Duffield
d8aba3aeee Merge pull request #241 from antham/master
Fix linting issues
2018-08-29 23:05:14 +10:00
antham
1b8836e92d fix fmt issue 2018-08-29 13:46:51 +02:00
Anthony HAMON
54326907c3 fix linting issues 2018-08-29 13:46:51 +02:00
Anthony HAMON
cda7b374e2 fix linting issues 2018-08-29 13:46:51 +02:00
Anthony HAMON
e889a40caf fix golint issue 2018-08-29 13:46:51 +02:00
Jesse Duffield
3f26ddc06f Merge pull request #225 from antham/master
Add tests to pkg/commands/git - Part 1
2018-08-29 20:25:39 +10:00
Anthony HAMON
dac7c90483 add cache based on Gopkg.lock checksum 2018-08-29 12:03:32 +02:00
Anthony HAMON
66e5dacf5e fix git tests 2018-08-29 12:03:32 +02:00
Anthony HAMON
e3ed899b20 refactor MergeStatusFiles 2018-08-29 12:03:32 +02:00
Anthony HAMON
d6b4d4b063 add tests for MergesStatusFiles 2018-08-29 12:03:32 +02:00
Anthony HAMON
45fa257128 add test for StashSave and refactor StashSave method 2018-08-29 12:03:32 +02:00
Anthony HAMON
99840d8fc4 add test for StashDo and refactor StashDo method 2018-08-29 12:03:32 +02:00
Anthony HAMON
85012dbc8f add tests for GetStatusFiles 2018-08-29 12:03:32 +02:00
Anthony HAMON
13f9073552 add test for GetStashEntryDiff 2018-08-29 12:03:32 +02:00
Anthony HAMON
49b507d2ff replace make 2018-08-29 12:03:32 +02:00
Anthony HAMON
8247fd69c9 add test for GetStashEntries 2018-08-29 12:03:32 +02:00
Anthony HAMON
983d0bd586 replace make 2018-08-29 12:03:32 +02:00
Anthony HAMON
ca9ce22693 use assert in tests, rename testing method 2018-08-29 12:03:32 +02:00
Jesse Duffield
cff1dee6dc more lenient version comparison 2018-08-29 09:37:47 +10:00
Jesse Duffield
2181a91fea Merge pull request #231 from jesseduffield/feature/24-support-unicode-characters
Support unicode characters
2018-08-28 20:09:27 +10:00
Jesse Duffield
8c2b8cfb51 support unicode characters 2018-08-28 20:08:35 +10:00
Jesse Duffield
145cba34a0 Merge pull request #228 from jesseduffield/hotfix/226-dont-panic-when-catting
226: dont panic when catting directories
2018-08-28 19:19:03 +10:00
Jesse Duffield
7e1e97d050 dont panic when catting directories 2018-08-28 19:12:35 +10:00
Jesse Duffield
320ccdb22a when panicking due to malformed gitconfig, show a more useful error 2018-08-28 18:01:53 +10:00
Jesse Duffield
db1d5328f2 Merge pull request #213 from jesseduffield/feature/add-all
Add 'a' keybinding to toggle staged/unstaged for all files
2018-08-27 21:01:52 +10:00
Jesse Duffield
da2d3f253f better translation 2018-08-27 20:58:17 +10:00
Jesse Duffield
b4323c029f Merge branch 'master' into feature/add-all 2018-08-27 20:57:50 +10:00
Jesse Duffield
203ad29349 use logrus entry rather than logger 2018-08-27 20:51:21 +10:00
Jesse Duffield
04735d0601 update call to update user config function 2018-08-27 20:49:47 +10:00
Jesse Duffield
bbe603ff5b separate english translation structs 2018-08-27 20:46:06 +10:00
Jesse Duffield
23a9f41d9d Merge branch 'feature/anonymous-reporting' 2018-08-27 20:35:55 +10:00
Jesse Duffield
1901901d24 Merge pull request #189 from jesseduffield/feature/auto-updates
Auto updates
2018-08-27 20:33:02 +10:00
Jesse Duffield
f861175f83 move update quit confirmation function into updates file 2018-08-27 20:26:49 +10:00
Jesse Duffield
25c60b1854 localize update errors 2018-08-27 20:23:47 +10:00
Jesse Duffield
38557f131d record last update check as soon as you begin checking 2018-08-27 20:16:26 +10:00
Jesse Duffield
96eef7838e better auto update logic 2018-08-27 20:08:10 +10:00
Jesse Duffield
2bf536265a disable updating on windows for now 2018-08-27 19:37:01 +10:00
Jesse Duffield
43f612feb1 Merge branch 'master' into feature/auto-updates 2018-08-27 19:20:07 +10:00
Jesse Duffield
a1c6adab59 Merge branch 'master' into feature/anonymous-reporting 2018-08-27 18:50:24 +10:00
Jesse Duffield
e72d090c5c Merge pull request #221 from antham/fix-master
Fix format issue
2018-08-27 18:05:18 +10:00
Anthony HAMON
a2a9e0c478 fix format issue 2018-08-27 10:01:05 +02:00
Jesse Duffield
d3953a1440 Merge pull request #206 from antham/add-pkg-commands-test
Add test to pkg/commands, refactor some methods
2018-08-27 17:58:02 +10:00
Jesse Duffield
a251cffb9a Merge pull request #220 from dawidd6/flag/config
Add config flag
2018-08-27 17:56:19 +10:00
Dawid Dziurla
b7f6bcb3ca add config flag 2018-08-26 17:31:28 +02:00
Anthony HAMON
55c6af258d increase parallel build 2018-08-26 11:30:03 +02:00
Jesse Duffield
540edc0c35 anonymous reporting data 2018-08-26 16:53:31 +10:00
Jesse Duffield
12261ceb05 update config docs for auto updates 2018-08-26 13:03:37 +10:00
Anthony HAMON
f6ab11e4ee run gofmt 2018-08-26 02:20:01 +02:00
Anthony HAMON
2d9f7009fa add gofmt to circle 2018-08-26 02:17:46 +02:00
Anthony HAMON
4b19b4108e add circleci cache 2018-08-26 01:58:20 +02:00
Anthony HAMON
a23753dc18 exclude darwin arm from building 2018-08-26 01:58:20 +02:00
antham
caf99d2d64 exclude openbsd cause of go-git bugs on this platform 2018-08-26 01:58:20 +02:00
antham
2273f4c0a5 add compile step in circleci 2018-08-26 01:58:20 +02:00
Anthony HAMON
23fe0290ad add openFile to gui struct 2018-08-26 01:58:20 +02:00
Anthony HAMON
ed2dcd9e46 add tests 2018-08-26 01:58:20 +02:00
Anthony HAMON
75e08993ea extract dependencies 2018-08-26 01:58:20 +02:00
Anthony HAMON
0b07cd19f7 switch GetOpenCommand scope to private 2018-08-26 01:58:20 +02:00
Anthony HAMON
38f11f1f4a move dummy functions, rename functions 2018-08-26 01:58:19 +02:00
Anthony HAMON
f91e2b12db add tests to pkg/commands 2018-08-26 01:58:19 +02:00
Anthony HAMON
364c1ac5e7 remove useless returned variable 2018-08-26 01:58:19 +02:00
Anthony HAMON
883fcf1083 remove useless returned variable 2018-08-26 01:58:19 +02:00
Anthony HAMON
a891bc90b7 simplify method 2018-08-26 01:58:19 +02:00
Anthony HAMON
7a74bc504b avoid useless allocation 2018-08-26 01:58:19 +02:00
Anthony HAMON
32f4d09e89 move platform specific code to dedicated platform files 2018-08-26 01:58:19 +02:00
Anthony HAMON
a5adfaee8a remove useless returned variable 2018-08-26 01:58:19 +02:00
Jesse Duffield
57decdd11d check error from setViewOnBottom 2018-08-25 17:38:03 +10:00
Jesse Duffield
87dc2bcad9 bump dependencies 2018-08-25 17:36:16 +10:00
Jesse Duffield
21f6e9ba87 auto-updates 2018-08-25 17:32:34 +10:00
Jesse Duffield
f24c95aede Merge branch 'master' into feature/auto-updates 2018-08-25 11:02:46 +10:00
Jesse Duffield
93ab892bdd Merge pull request #214 from jesseduffield/hotfix/global-rune-keybindings
Ignore global rune keybindings when in editable view
2018-08-25 09:14:05 +10:00
Jesse Duffield
ee7f88e123 ignore global rune keybindings when in editable view 2018-08-25 09:13:29 +10:00
Jesse Duffield
60422912c8 add 'a' keybinding to toggle staged/unstaged for all files 2018-08-25 08:59:51 +10:00
Jesse Duffield
6c389df57d Merge pull request #200 from remyabel/feature/esc-quits
Esc will quit when not in popup, fixes #197
2018-08-24 09:16:56 +10:00
Jesse Duffield
22de5e7b23 Merge pull request #210 from dawidd6/sirupsen/imports
Sirupsen -> sirupsen
2018-08-24 07:58:47 +10:00
Dawid Dziurla
bcbeec1a56 Sirupsen -> sirupsen 2018-08-23 14:22:03 +02:00
Jesse Duffield
5628eae502 WIP 2018-08-23 18:43:16 +10:00
Jesse Duffield
2cdd439286 Merge pull request #204 from remyabel/hotfix/fix-dutch-errors
Fix typos causing tests to fail
2018-08-23 10:19:31 +10:00
Tommy Nguyen
110ff38c0d Remove accidentally checked in code 2018-08-22 11:34:16 -04:00
Tommy Nguyen
2680b2f4fe Merge branch 'hotfix/fix-dutch-errors' of github.com:remyabel/lazygit into hotfix/fix-dutch-errors 2018-08-22 11:28:09 -04:00
Tommy Nguyen
3e8ef0d12d Remove space before punctuation 2018-08-22 11:27:45 -04:00
remyabel
bee4d98f52 Remove space before punctuation 2018-08-21 21:27:20 -04:00
Jesse Duffield
584d6b241c Merge pull request #198 from antham/fix-various-errors
Fix various errors reported by goreportcard
2018-08-22 09:28:19 +10:00
Anthony HAMON
37681627ab remove useless check 2018-08-21 20:54:48 +02:00
Anthony HAMON
810155ef2f fix documentation and reference issues 2018-08-21 20:54:46 +02:00
Tommy Nguyen
924a9bb2c9 Fix typos causing tests to fail 2018-08-21 10:55:33 -04:00
Jesse Duffield
4d635cd1cd Merge pull request #151 from alcohol/force-delete-branch
add option to force delete given branch
2018-08-21 23:45:27 +10:00
Jesse Duffield
3d49ab6666 Merge pull request #202 from dawidd6/test
Fix testPath for debian packaging
2018-08-21 23:33:25 +10:00
Tommy Nguyen
eff931a138 Update gocui fork 2018-08-21 09:00:16 -04:00
Tommy Nguyen
cd4063c763 s/escape/quit 2018-08-21 08:54:51 -04:00
Tommy Nguyen
646c205227 s/quit/escape, don't use special handling for views 2018-08-21 07:50:37 -04:00
Tommy Nguyen
dc911906b3 Esc will quit when not in popup, fixes #197 2018-08-21 06:36:20 -04:00
Rob Bast
182e475116 correct variable assignment 2018-08-21 12:09:13 +02:00
Rob Bast
5f6b61d28c adjust translation(s) for forced branch deletion 2018-08-21 12:06:42 +02:00
Rob Bast
810540edfa add return 2018-08-21 11:32:17 +02:00
Jesse Duffield
042c83387e make bom.sh executable for easier testing 2018-08-21 17:41:18 +10:00
Jesse Duffield
c6a8899060 Merge pull request #160 from remyabel/157_remove_bom
#157: clean BOM, allowing CSV files to display correctly
2018-08-21 17:39:55 +10:00
Jesse Duffield
da4c12bf9e Merge pull request #193 from antham/add-tests
Add tests to i18n package
2018-08-21 17:34:13 +10:00
Jesse Duffield
d6ee413587 Update Config.md 2018-08-21 13:42:17 +10:00
Jesse Duffield
9b63887867 add colored borders example image 2018-08-21 13:27:27 +10:00
Jesse Duffield
d35eaa062b remove org-global context in circle ci config 2018-08-21 12:57:19 +10:00
Jesse Duffield
0c27776a83 Merge pull request #179 from antham/master
Update circleci config, add badges
2018-08-21 12:49:22 +10:00
Anthony HAMON
4972a4c218 rewrite addBundles 2018-08-20 21:18:50 +02:00
Anthony HAMON
e4070ccb4f rewrite language detection, rewrite tests 2018-08-20 21:12:42 +02:00
Dawid Dziurla
483b4d939d fix testPath for debian packaging 2018-08-20 15:21:05 +02:00
Tommy Nguyen
45fea83771 Convert \r\n to \n; don't depend on unix2dos 2018-08-20 09:16:35 -04:00
Jesse Duffield
2aa89ade0d Merge pull request #195 from jesseduffield/hotfix/194-support-empty-version
194: Support empty version string
2018-08-20 20:54:23 +10:00
Jesse Duffield
37029f7db3 support empty version string 2018-08-20 20:52:32 +10:00
Jesse Duffield
954dfb12e4 Merge branch 'master' into feature/auto-updates 2018-08-20 19:53:53 +10:00
Jesse Duffield
8364509d1f Merge pull request #188 from jesseduffield/feature/force-push
Force push confirmation panel
2018-08-20 19:52:41 +10:00
Jesse Duffield
d938a437a2 WIP auto updates 2018-08-20 19:52:20 +10:00
Anthony HAMON
3c0fb9b324 add tests to i18n package 2018-08-20 09:02:57 +02:00
Rob Bast
8e3df6b981 add option to force delete given branch 2018-08-20 08:37:16 +02:00
Tommy Nguyen
5dd049eb82 Convert test to use new library 2018-08-19 23:39:57 -04:00
Tommy Nguyen
f1a4a7e1ff synchronize deps 2018-08-19 23:34:58 -04:00
Tommy Nguyen
108815c790 Add missing brace 2018-08-19 23:31:26 -04:00
remyabel
5c65066cbb Merge branch 'master' into 157_remove_bom 2018-08-19 23:29:47 -04:00
Jesse Duffield
64cf8f5b10 update dependencies 2018-08-20 13:25:33 +10:00
Jesse Duffield
5fcbe4ff8a Merge pull request #191 from jesseduffield/hotfix/174-language-not-found
174: Use fallback language if language not detected on startup
2018-08-20 13:23:26 +10:00
Jesse Duffield
fa8248bd70 use fallback language if language not detected on startup 2018-08-20 13:20:59 +10:00
Jesse Duffield
65c5a6014b Merge pull request #185 from antham/add-tests
Add test to utils package
2018-08-20 09:24:54 +10:00
Anthony HAMON
fc126a1718 remove test 2018-08-19 21:39:23 +02:00
Anthony HAMON
bb86a3ff7c update github template 2018-08-19 20:21:19 +02:00
Anthony HAMON
bbf7a9d790 add various badges
* golangci (https://golangci.com)
* circleci (https://circleci.com/)
* codecov.io (https://codecov.io)
* godoc
* tag release
2018-08-19 20:21:19 +02:00
Anthony HAMON
fbfa48f0fc update circleci
* define release worflow when a tag is created
* add dep install
* run tests with coverage
* add goreleaser
2018-08-19 20:21:14 +02:00
Tommy Nguyen
e8b12a086c Fix ineffectual assignment 2018-08-19 08:52:08 -04:00
Tommy Nguyen
766197de9d NormalizeLinefeeds removes rather than converts Window/Mac style lf's 2018-08-19 08:48:03 -04:00
Jesse Duffield
1e44001910 Merge branch 'master' into feature/auto-updates 2018-08-19 21:36:40 +10:00
Jesse Duffield
317926c808 fix golangci lint 2018-08-19 21:34:24 +10:00
Jesse Duffield
4d2346f80a popup force push confirmation panel if the local branch has diverged from the upstream branch 2018-08-19 21:28:13 +10:00
Tommy Nguyen
d2bdac29aa Merge branch 'master' into 157_remove_bom 2018-08-19 07:22:48 -04:00
Tommy Nguyen
cea736e6e9 Factor out into NormalizeLinefeeds; add tests 2018-08-19 07:20:50 -04:00
Jesse Duffield
e6712832b5 Merge pull request #187 from jesseduffield/hotfix/177-fix-gitignore
177: Fix ignore feature
2018-08-19 20:42:51 +10:00
Jesse Duffield
aa4d739577 fix ignore feature 2018-08-19 20:41:04 +10:00
Jesse Duffield
51558f51ab Merge pull request #186 from jesseduffield/hotfix/169-more-filenames-with-spaces
Handle filenames with spaces better
2018-08-19 20:19:53 +10:00
Jesse Duffield
35884f81e9 handle filenames with spaces better 2018-08-19 20:13:29 +10:00
Jesse Duffield
2008607108 remove VERSION file 2018-08-19 20:04:57 +10:00
Jesse Duffield
21be9fb3b1 bump version to v0.1.72 2018-08-19 19:48:31 +10:00
Jesse Duffield
80fac559c2 revert changes to ldflags 2018-08-19 19:48:21 +10:00
Jesse Duffield
e6a6301144 bump version to v0.1.71 2018-08-19 19:47:04 +10:00
Jesse Duffield
c5d4024d58 compare error message rather than error itself on no-commits error 2018-08-19 18:51:42 +10:00
Anthony HAMON
f91b4067f4 add test to utils package 2018-08-19 10:37:03 +02:00
Tommy Nguyen
b46d174f70 view_helpers.go: don't ignore return value 2018-08-19 02:21:33 -04:00
Tommy Nguyen
cdc6d45fa4 view_helpers.go: replace \r with \r\n 2018-08-19 02:19:19 -04:00
Jesse Duffield
81b07daa01 Merge branch 'hotfix/167-ambiguous-name-in-diff' 2018-08-19 15:06:06 +10:00
Jesse Duffield
93266cc2a4 support forked branches when getting project root 2018-08-19 15:05:36 +10:00
Jesse Duffield
60fc24eada Merge pull request #172 from jesseduffield/hotfix/167-ambiguous-name-in-diff
167: Support File names that match Branch names
2018-08-19 14:53:14 +10:00
Jesse Duffield
6978785ccf add user email and config to test repo generators 2018-08-19 14:52:08 +10:00
Jesse Duffield
cd9eada0c6 add test for variety of potential git diff situations 2018-08-19 14:48:39 +10:00
Jesse Duffield
7b3679d546 Merge pull request #181 from glvr182/feature/ignore-unused
Cleaned the gitignore and added the linux binary
2018-08-19 13:26:35 +10:00
Jesse Duffield
c8e63894b4 Merge pull request #182 from glvr182/feature/docs-images
Feature/docs images
2018-08-19 13:25:23 +10:00
Jesse Duffield
af93d04479 Merge pull request #183 from mjarkk/master
Removed logrus logger import from i18n
2018-08-19 13:23:29 +10:00
Mark Kopenga
29f2bdbba3 removed logrus logger import 2018-08-18 14:03:20 +02:00
Mark Kopenga
c90e865e34 Merge pull request #10 from jesseduffield/master
Updated to latest master
2018-08-18 14:00:41 +02:00
Jesse Duffield
b104dccab2 Merge pull request #163 from dawidd6/translation/pl
Add Polish translation
2018-08-18 21:08:56 +10:00
Glenn Vriesman
2dbd5be24e Removed duplicates 2018-08-18 13:04:44 +02:00
Glenn Vriesman
0a4cf6a544 Docs: Fixed typo 2018-08-18 12:56:15 +02:00
Glenn Vriesman
5f5e275a0e Docs: Made commit-diffs example a local file 2018-08-18 12:55:13 +02:00
Glenn Vriesman
7b85d146af Docs: Made resolving merge conflicts a local file 2018-08-18 12:53:26 +02:00
Glenn Vriesman
fd01cdb137 Docs: Made the example gif a local file 2018-08-18 12:51:15 +02:00
Glenn Vriesman
9e8dc37308 Docs: Moved slack image to docs/resources 2018-08-18 12:46:50 +02:00
Jesse Duffield
08666889f4 improve remove file logic 2018-08-18 20:14:44 +10:00
Jesse Duffield
13ac1d151a WIP updater package 2018-08-18 19:54:44 +10:00
Glenn Vriesman
c091401571 Cleaned the gitignore and added the linux binary 2018-08-18 11:49:37 +02:00
Jesse Duffield
0174375562 Merge pull request #180 from jesseduffield/hotfix/174-nil-pointer-reference-on-startup
Hotfix for issue 174: avoid nil pointer reference on startup
2018-08-18 19:45:28 +10:00
Jesse Duffield
1f756d3d0a avoid nil pointer reference on startup 2018-08-18 19:43:58 +10:00
Jesse Duffield
6473e5ca3c use runtime package to get GOOS and GOARCH 2018-08-18 17:28:03 +10:00
Jesse Duffield
896dda3adf bump version to v0.1.70 2018-08-18 17:17:22 +10:00
Jesse Duffield
b9ef1a4d67 leave bump commits out of release notes 2018-08-18 17:17:05 +10:00
Jesse Duffield
671c693459 bump version to v0.1.67 2018-08-18 17:12:44 +10:00
Jesse Duffield
41171304b2 add platform/os details to goreleaser ldflags 2018-08-18 17:10:27 +10:00
Jesse Duffield
f025b289f0 enforce white text color in prompt panels 2018-08-18 16:23:19 +10:00
Tommy Nguyen
018b43163c synchronize deps 2018-08-18 02:20:10 -04:00
remyabel
9a923eb300 Merge branch 'master' into 157_remove_bom 2018-08-18 02:11:13 -04:00
Jesse Duffield
3f5c1a4243 bump version to v0.1.66 2018-08-18 16:00:30 +10:00
Jesse Duffield
db7e623c0c bump version to v0.1.65 2018-08-18 16:00:12 +10:00
Jesse Duffield
ad8f02b986 add 386 architecture to releases 2018-08-18 16:00:00 +10:00
Jesse Duffield
29431ddc8e create dashboard 2018-08-18 15:30:56 +10:00
Jesse Duffield
a1a828a781 support opening and editing config file 2018-08-18 14:54:05 +10:00
Jesse Duffield
6b150a4be0 bump dependencies 2018-08-18 14:20:19 +10:00
Jesse Duffield
284c534251 user configurable border colors 2018-08-18 13:53:58 +10:00
Jesse Duffield
10fdb5a609 support writing back to user config 2018-08-18 13:22:05 +10:00
Jesse Duffield
4dc6d40b5a merge master 2018-08-18 12:08:21 +10:00
Jesse Duffield
99d40c2f8e run codecov report after other commands on circle ci 2018-08-18 11:17:31 +10:00
Mark Kopenga
0f145093fe Here i a fix for the error go complain on your pull 2018-08-17 20:43:51 +02:00
Jesse Duffield
bd91b9e1e9 add test repo for all the kinds of files that show up when diffing 2018-08-17 22:46:10 +10:00
Dawid Dziurla
bf5f3bb972 pl translation 3 2018-08-17 14:27:13 +02:00
Jesse Duffield
03a7e32694 support filenames that match branchnames 2018-08-17 22:25:53 +10:00
Dawid Dziurla
b0e08491ac pl translation 2 2018-08-17 14:11:48 +02:00
Jesse Duffield
aaa8558de8 Merge pull request #159 from remyabel/158_escape_backticks
#158: escapes backticks, which is a problem in shells like Bash
2018-08-17 21:11:20 +10:00
Mark Kopenga
633553bcf8 Merge pull request #7 from jesseduffield/master
Update to latest master
2018-08-17 10:33:10 +02:00
Jesse Duffield
933aae7da1 bump version to v0.1.64 2018-08-17 11:15:41 +10:00
Jesse Duffield
3b1689727a bump version to v0.1.63 2018-08-17 11:15:19 +10:00
Jesse Duffield
c08e6d9999 bump version to v0.1.62 2018-08-17 11:14:54 +10:00
Jesse Duffield
dcd3bb6bbd use platform independent command to remove a file or directory 2018-08-17 11:13:21 +10:00
Jesse Duffield
bd15f9d27a Merge pull request #164 from dawidd6/ubuntu/release-ppa
Add info about release PPA
2018-08-17 09:05:31 +10:00
Tommy Nguyen
52033b32f7 Use strings.Replace instead of regexp 2018-08-16 17:04:39 -04:00
Mark Kopenga
5c738fa493 Removed duplicates 2018-08-16 20:59:32 +02:00
Mark Kopenga
9a0f29ff5b Merge pull request #5 from jesseduffield/master
Updated to latest master
2018-08-16 20:08:14 +02:00
Dawid Dziurla
36eed80228 add info about release PPA 2018-08-16 19:26:43 +02:00
Dawid Dziurla
2d80a83d27 pl translation 1 2018-08-16 18:56:16 +02:00
Jesse Duffield
f4e701611b Merge pull request #162 from ponsfrilus/patch-3
Typo path → patch
2018-08-16 23:40:37 +10:00
Nicolas Borboën
ff15d86ced Typo path → patch 2018-08-16 14:15:55 +02:00
Jesse Duffield
8b57b21d79 add circle ci config 2018-08-16 22:07:02 +10:00
remyabel
64d8a55dbd Merge branch 'master' into 157_remove_bom 2018-08-16 07:59:31 -04:00
Jesse Duffield
563efc41c7 bump dependencies for i18n 2018-08-16 21:48:09 +10:00
Jesse Duffield
090537a1f1 Merge pull request #137 from mjarkk/master
Added a view basic translation functions and translation file
2018-08-16 21:46:42 +10:00
Mark Kopenga
fcf616bd62 Fixed it 2018-08-16 13:35:04 +02:00
Tommy Nguyen
74d81ae080 [rebase] Fix errors; update dependencies
Argument must be []byte not string

Don't commit bomtest.txt
2018-08-16 07:17:47 -04:00
Tommy Nguyen
a7755ab184 reformat 2018-08-16 07:00:13 -04:00
Mark Kopenga
faf218f465 Fixed comments from jesseduffield on issue #137 2018-08-16 11:31:50 +02:00
Mark Kopenga
90746502df Fixed comments from jesseduffield on issue #137 2018-08-16 11:31:03 +02:00
Jesse Duffield
df7d1df4cb bump dependencies 2018-08-16 19:20:59 +10:00
Jesse Duffield
5819e04c53 use shibukawa/configdir package to follow xdg spec config directory structure 2018-08-16 19:17:38 +10:00
Jesse Duffield
fd3ce21576 use yaml for config file rather than json 2018-08-16 18:43:46 +10:00
Tommy Nguyen
3a31b84d1a add BOM test generator 2018-08-16 02:00:34 -04:00
Tommy Nguyen
f09515867d #157: clean BOM, allowing CSV files to display correctly 2018-08-16 01:53:53 -04:00
Mark Kopenga
88e1a815fe Fixed comment on issue #137 from @jesseduffield 2018-08-16 07:16:32 +02:00
Tommy Nguyen
db94dde114 fix formatting 2018-08-15 23:58:44 -04:00
Tommy Nguyen
ee4660af97 #158: escapes backticks, which is a problem in shells like Bash 2018-08-15 23:55:55 -04:00
Jesse Duffield
59ab38fff6 Update README.md 2018-08-16 10:37:23 +10:00
Jesse Duffield
adc906bb87 add slack image file 2018-08-16 10:32:52 +10:00
Mark Kopenga
9abbfe5a43 Fully translated pkg/gui/confirmation_panel.go 2018-08-15 15:12:55 +02:00
Mark Kopenga
9112278ab7 Merge pull request #4 from jesseduffield/master
Update to latest master
2018-08-15 15:03:27 +02:00
Mark Kopenga
d00c46a712 Added all english translations to a file and fixed some typos 2018-08-15 14:57:20 +02:00
Jesse Duffield
29ed971558 add user configuration in json file 2018-08-15 22:06:37 +10:00
Jesse Duffield
8d99b400fd factor out code for new popup panels 2018-08-15 21:49:38 +10:00
Jesse Duffield
7c33c02930 dont panic if unable to close confirmation prompt 2018-08-15 21:43:31 +10:00
Mark Kopenga
50b41bfccc Translated pkg/gui/view_helpers.go 2018-08-15 11:49:43 +02:00
Mark Kopenga
295093a432 Translated pkg/gui/stash_panel.go 2018-08-15 11:12:46 +02:00
Mark Kopenga
10c53162ca Merge pull request #3 from jesseduffield/master
Updated to latest master
2018-08-15 10:54:43 +02:00
Mark Kopenga
7e926cf41d Added translation for pkg/gui/ confirmation_panel.go gui.go merge_panel.go 2018-08-15 10:53:05 +02:00
Mark Kopenga
d12cc5a74e Fully translated pkg/gui/commits_panel.go 2018-08-15 10:30:29 +02:00
Mark Kopenga
8418fa17a5 Fully translated pkg/gui/commit_message_panel.go 2018-08-15 09:15:31 +02:00
Jesse Duffield
905e6c16ba add credential helper to config of test repo 2018-08-15 15:15:21 +10:00
Jesse Duffield
db140842f3 bump version to v0.1.61 2018-08-15 09:56:15 +10:00
Jesse Duffield
7002ee5d9b bump version to v0.1.60 2018-08-15 09:52:23 +10:00
Jesse Duffield
0687c07f50 more robust reading of reflog to get branch name 2018-08-15 09:45:24 +10:00
Jesse Duffield
922f4ed5a4 try rev-parse of git symbolic-ref returns nothing 2018-08-15 09:42:02 +10:00
Mark Kopenga
3dba246029 Added translations for files_panel.go and fixed some typos 2018-08-14 22:29:17 +02:00
Mark Kopenga
be3f5846e4 Added more translations 2018-08-14 21:06:50 +02:00
Mark Kopenga
38a1a00cf1 Fixed comment from myself on issue: 137 2018-08-14 16:38:25 +02:00
Mark Kopenga
9c97b75aad Merge remote-tracking branch 'origin/master' 2018-08-14 16:14:11 +02:00
Mark Kopenga
883f436b0f can't go any further because of an error 2018-08-14 16:12:21 +02:00
Jesse Duffield
d923796cff Merge branch 'master' of https://github.com/mjarkk/lazygit 2018-08-14 23:48:18 +10:00
Jesse Duffield
ba2b6fbf1f pull errors out of package scope and store sentinel errors on the gui struct 2018-08-14 23:47:14 +10:00
Mark Kopenga
73a1682540 fixed package naming and added tr object to file_panel.go 2018-08-14 15:26:25 +02:00
Mark Kopenga
8e22d569a0 Merge pull request #2 from jesseduffield/master
Updated to latest master
2018-08-14 15:10:59 +02:00
Jesse Duffield
4d0702fba5 Merge branch 'master' of https://github.com/mjarkk/lazygit 2018-08-14 22:12:11 +10:00
Jesse Duffield
5cbacb0c67 make local i18n package confirm to project structure 2018-08-14 22:12:07 +10:00
Mark Kopenga
0568b32f0b Added more translations 2018-08-14 13:31:23 +02:00
Mark Kopenga
6e518142b4 added some commands 2018-08-14 12:56:11 +02:00
Mark Kopenga
0c39347224 Added auto detection for the system language 2018-08-14 12:52:26 +02:00
Mark Kopenga
dd7e93ac8d Added all the missing translations from dutch.go 2018-08-14 11:35:39 +02:00
Mark Kopenga
f792f74f0f Merge remote-tracking branch 'origin/master' 2018-08-14 11:28:11 +02:00
Mark Kopenga
5ad97add08 Added the translation to some words again 2018-08-14 11:27:46 +02:00
Jesse Duffield
652237d48f bump version to v0.1.59 2018-08-14 19:23:05 +10:00
Jesse Duffield
6a3f8eefa5 send version number through app 2018-08-14 19:22:38 +10:00
Mark Kopenga
17dcfbcc6f Merge pull request #1 from jesseduffield/master
Update to latest master
2018-08-14 11:06:34 +02:00
Mark Kopenga
dfafb98871 tried to update to latest master 2018-08-14 11:05:26 +02:00
Jesse Duffield
8cc8c4c228 bump version to v0.1.58 2018-08-14 18:49:15 +10:00
Jesse Duffield
574b34930c support files with spaces in name 2018-08-14 18:48:08 +10:00
Jesse Duffield
77080f44a4 bump version to v0.1.57 2018-08-14 18:38:04 +10:00
Jesse Duffield
97daa5eeb2 bump version to v0.1.56 2018-08-14 18:36:51 +10:00
Jesse Duffield
c476dfc1cb fix open command 2018-08-14 18:35:47 +10:00
Jesse Duffield
bde6182c94 better git squash logic 2018-08-14 18:30:06 +10:00
Jesse Duffield
efb049cd24 better error handling for stashing files 2018-08-14 18:29:25 +10:00
Jesse Duffield
88af0fb1b6 add gpg repo generator 2018-08-14 18:12:09 +10:00
Jesse Duffield
5f30f07ea5 Merge branch 'master' into feature/quoted-messages 2018-08-14 18:10:44 +10:00
Jesse Duffield
c02f474bf3 update dependencies 2018-08-14 18:05:18 +10:00
Jesse Duffield
b32173b0c7 output to development.log in append mode 2018-08-14 18:02:27 +10:00
Jesse Duffield
1c750fdb40 fix reflog command 2018-08-14 18:02:14 +10:00
Jesse Duffield
9ecd7908aa refactor commands to depend less on the shell 2018-08-14 17:47:33 +10:00
Jesse Duffield
ad3d332e05 Merge pull request #143 from jesseduffield/feature/clean_up
Platform should only be present once
2018-08-14 12:33:47 +10:00
Andrei Miulescu
842ceec9b0 Platform should only be present once 2018-08-14 12:24:32 +10:00
Jesse Duffield
95c7df4c61 use platform-specific message quoting 2018-08-14 11:16:52 +10:00
Jesse Duffield
c0a1f90604 Merge pull request #132 from jesseduffield/feature/project_restructure
Project Restructure Episode 1
2018-08-14 09:02:38 +10:00
Jesse Duffield
15528dc341 Merge branch 'master' into feature/project_restructure 2018-08-14 08:44:39 +10:00
Jesse Duffield
7ecbd7fbb3 update pre-commit hook 2018-08-14 08:42:08 +10:00
Jesse Duffield
45f640941c update dependencies 2018-08-14 08:34:31 +10:00
Jesse Duffield
047892962a centralise subprocess code to gui.go 2018-08-14 08:33:40 +10:00
Jesse Duffield
d4f4b46a1f check both local and global config for gpgsign 2018-08-14 08:33:27 +10:00
Jesse Duffield
f549ad0f37 use git command with message in subprocess if using gpgsign 2018-08-14 07:27:59 +10:00
Mark Kopenga
f2dfcb6e12 Added more messages and text issue: #137 2018-08-13 22:00:44 +02:00
Mark Kopenga
65eb3780a0 fixed typo 2018-08-13 20:48:49 +02:00
Mark Kopenga
d9959eb998 fixed @jesseduffield comment #137 2018-08-13 20:46:53 +02:00
Jesse Duffield
ebfed34145 add PR #135 keybindings to this branch 2018-08-13 23:46:08 +10:00
Jesse Duffield
450f269f02 Merge pull request #135 from jon-grangien/feature/ctrl-keybindings
Add C-u and C-d keybindings for scrolling up and down diff
2018-08-13 23:43:54 +10:00
Jesse Duffield
95d2b02664 update gitignore 2018-08-13 23:36:06 +10:00
Jesse Duffield
59e5545f38 discard log output when not in debug mode 2018-08-13 23:35:01 +10:00
mjarkk
62fc831407 Added a view basic translation functions + tranlation file 2018-08-13 15:33:45 +02:00
Jesse Duffield
fb0004481b correct cursor before returning item position 2018-08-13 21:42:56 +10:00
Jesse Duffield
3bd0246e4d add test repo contents to gitignore 2018-08-13 21:42:31 +10:00
Jesse Duffield
12de0345e4 minor cleanup 2018-08-13 21:35:54 +10:00
Jesse Duffield
9e725ae24e got this bad boy compiling again 2018-08-13 21:16:21 +10:00
Jesse Duffield
97cff65612 progress on refactor 2018-08-13 20:26:02 +10:00
codingInSpace
a32fc34a49 Add C-u and C-d keybindings for scrolling up and down diff 2018-08-13 04:09:32 -04:00
Jesse Duffield
f9c39ad64b add newline to version output 2018-08-13 17:20:03 +10:00
Jesse Duffield
1571e6b962 add newline to version output 2018-08-13 12:27:26 +10:00
Jesse Duffield
84b27d402d Update issue templates 2018-08-12 23:46:38 +10:00
Jesse Duffield
e6beb5d50b no more go-git for committing (reflecting the change in master) 2018-08-12 23:29:58 +10:00
Jesse Duffield
1354885db0 bump version to v0.1.55 2018-08-12 23:20:10 +10:00
Jesse Duffield
360bfa4642 bump version to v0.1.54 2018-08-12 23:17:47 +10:00
Jesse Duffield
bb0e580a27 bump version to v0.1.53 2018-08-12 23:17:25 +10:00
Jesse Duffield
1eb0061357 bump dependencies 2018-08-12 23:16:58 +10:00
Jesse Duffield
4cd2d70659 revert to using cli git command rather than go-git for committing 2018-08-12 23:14:37 +10:00
Jesse Duffield
35d695fd0c update gitignore to reflect new test repo directory structure 2018-08-12 23:00:51 +10:00
Andrei Miulescu
e8eb78617c Mid refactor change some more stuff 2018-08-12 21:04:47 +10:00
Andrei Miulescu
e65ddd7b6f Move some commands around 2018-08-12 20:22:20 +10:00
Jesse Duffield
c01bc09442 WIP refactor 2018-08-12 19:50:55 +10:00
Andrei Miulescu
dcd461d29f Restrucure project in a way where it is more modular 2018-08-12 19:31:27 +10:00
Jesse Duffield
98c22a36fd rearrange test repo generators 2018-08-12 15:54:59 +10:00
Jesse Duffield
7bfd8155c2 bump version to v0.1.52 2018-08-12 12:01:50 +10:00
Jesse Duffield
ed1518fd66 fix CPU issues caused by resizing popup panels 2018-08-12 12:00:30 +10:00
Jesse Duffield
400301e8fa bump gocui fork to support getting view dimensions 2018-08-12 11:39:37 +10:00
Jesse Duffield
f97a098c6f Merge branch 'master' of https://github.com/jesseduffield/lazygit 2018-08-12 11:05:24 +10:00
Jesse Duffield
2b3bdc8b8f rename merge conflict test repo 2018-08-12 11:05:21 +10:00
Jesse Duffield
7323e08fdb Merge pull request #127 from ponsfrilus/master
Proposal for a slightly better README.md
2018-08-12 00:51:31 +10:00
Jesse Duffield
838ed4f170 Merge pull request #104 from maxice8/add-void-readme
README.md: Add Void Linux instructions
2018-08-12 00:43:43 +10:00
Jesse Duffield
feba6b5318 Merge pull request #129 from ponsfrilus/patch-2
Use <kbd> tags
2018-08-12 00:42:50 +10:00
Nicolas Borboën
27234c1cab Use <kbd> tags
and that close #128
2018-08-11 12:13:14 +02:00
Jesse Duffield
8f6329c402 if you are using gpgsign, must use git editor to commit 2018-08-11 19:28:32 +10:00
Nicolas Borboën
0ac56f5ade Proposal for a slighty bettre README.md
* Text wrapped to 80 chars;
* Kind of table of content;
* Launchpad PPA link added;
* AUR package on a bullet list;
* Missing ponctuation added;
* Youtube tutorial and keybindings on a bullet list.
2018-08-11 11:11:55 +02:00
Jesse Duffield
471a7de418 bump version to v0.1.51 2018-08-11 17:08:20 +10:00
Jesse Duffield
77191ea67c show output line by line in deploy script 2018-08-11 17:07:56 +10:00
Jesse Duffield
176aa62fe0 bump version to v0.1.50 2018-08-11 17:06:50 +10:00
Jesse Duffield
1419be1aab support commit message via git editor using shift+C keybinding 2018-08-11 16:55:39 +10:00
Jesse Duffield
0ac306fe2a fix whitespace 2018-08-11 16:55:15 +10:00
Jesse Duffield
f0840c0f46 fallback to vi when trying to edit file and no EDITOR/VISUAL is defined 2018-08-11 16:55:05 +10:00
Jesse Duffield
6d4d6d3ac0 bump version to v0.1.49 2018-08-11 16:22:49 +10:00
Jesse Duffield
2dba6f6733 support case insensitive branch names 2018-08-11 16:11:17 +10:00
Jesse Duffield
aec125df70 Merge branch 'master' into add-void-readme 2018-08-11 15:50:42 +10:00
Jesse Duffield
9626ebdf35 correct typo in keybinding for delete branch 2018-08-11 15:45:38 +10:00
Jesse Duffield
2c140445e5 Merge branch 'master' into feature/deleting-branches 2018-08-11 15:43:56 +10:00
Jesse Duffield
62a231abb7 handle commit select on any commits panel refresh 2018-08-11 15:34:57 +10:00
Jesse Duffield
753ca75e55 clear commit panel after confirming 2018-08-11 15:26:02 +10:00
Jesse Duffield
5c8f0d4c9d gitignore test repo 2018-08-11 15:23:57 +10:00
Jesse Duffield
000a709783 update test repo generators including test for unicode characters 2018-08-11 15:22:41 +10:00
Jesse Duffield
5c185572a7 delete test file that was accidentally committed 2018-08-11 15:12:30 +10:00
Jesse Duffield
bb86d527e1 Merge branch 'master' of https://github.com/jesseduffield/lazygit 2018-08-11 15:12:10 +10:00
Jesse Duffield
0e3dec1069 Merge branch 'master' into feature/add-fixup-option-into-commits-panel 2018-08-11 15:11:19 +10:00
Jesse Duffield
7e141283f6 merge branch master 2018-08-11 15:09:37 +10:00
Jesse Duffield
e46f3b3393 add spew dependency to vendor 2018-08-11 15:04:42 +10:00
Jesse Duffield
73e740d1ba clean up fixup code
reduce log clutter
add log dumping with spew
2018-08-11 15:04:02 +10:00
Jesse Duffield
f1eaeec9ee remove time logging 2018-08-11 14:47:11 +10:00
Jesse Duffield
a713fff362 Merge pull request #98 from frealgagu/master
Adding arch linux packages to README.md
2018-08-11 14:04:44 +10:00
Jesse Duffield
6d3d40c41f handle error on confirmation panel resize 2018-08-11 13:27:34 +10:00
Jesse Duffield
2137d3f6d2 Merge branch 'feature/multiline-commit-restoring' of https://github.com/jesseduffield/lazygit into feature/multiline-commit-restoring 2018-08-11 13:24:15 +10:00
Jesse Duffield
8ae346787a revert use of stored values in confirmation panels 2018-08-11 13:24:05 +10:00
Jesse Duffield
b8daf71db6 Delete ZHgalGrWSF 2018-08-11 13:19:54 +10:00
Jesse Duffield
fd13cf14b4 Merge branch 'feature/multiline-commit-restoring' 2018-08-11 13:19:17 +10:00
Jesse Duffield
47bf649a69 switch focus back to files view after confirming commit message 2018-08-11 13:18:33 +10:00
Jesse Duffield
3b018e040f make
commit
messages
multilined
and add ability save commit message between edits
2018-08-11 13:17:20 +10:00
Jesse Duffield
b2fbccd392 remove time logging 2018-08-11 12:35:17 +10:00
Fredy Alberto García Güiza
542fd8e88a Fixing grammar about arch linux packages in README.md 2018-08-10 19:47:16 -05:00
Jesse Duffield
9fbc332c9a Merge pull request #114 from glvr182/feature/add-repo-name
Feature: Added repository name to status
2018-08-11 09:51:11 +10:00
Jesse Duffield
1ef794e09f Update README.md 2018-08-11 09:43:22 +10:00
Hubert Baumgartner
bfa47d3b91 extenden delete branch with error message and confirmation 2018-08-10 20:19:58 +02:00
Hubert Baumgartner
48cea4e1c4 added branch delete functionallity 2018-08-10 16:46:03 +02:00
Jesse Duffield
7aa884ed8f step one on restoring multiline commits 2018-08-10 23:36:54 +10:00
Glenn Vriesman
7eb673e574 Feature: Added repository name to status 2018-08-10 15:10:15 +02:00
Jesse Duffield
f20121cb0b add FIXME note about trimming the trailing newline upon confirming in a prompt panel 2018-08-10 23:08:03 +10:00
Jesse Duffield
aa4160d57a merge feature/clearing-commit-panel into master 2018-08-10 22:49:54 +10:00
Jesse Duffield
8bc6832546 Merge branch 'master' of https://github.com/jesseduffield/lazygit 2018-08-10 22:46:24 +10:00
Jesse Duffield
4853d186ee add git plumbing to Gopkg.lock 2018-08-10 22:46:04 +10:00
Jesse Duffield
c3611df8d8 Merge pull request #112 from jesseduffield/feature/case-insensitive-branch-name-comparisons
101: Compare branches for name equality regardless of case
2018-08-10 22:26:44 +10:00
Jesse Duffield
59650cff26 compare branches for name equality regardless of case 2018-08-10 22:24:10 +10:00
Jesse Duffield
f9abcdb159 Merge branch 'feature/branch-refactor' 2018-08-10 22:13:48 +10:00
Jesse Duffield
28505dddaf move color functions into utils 2018-08-10 22:09:10 +10:00
Jesse Duffield
95b7c1d0a4 revert to using the direct git command for getting the current branch because it breaks if you've just done a git init 2018-08-10 22:08:12 +10:00
Jesse Duffield
eb9f01ecfa move withPadding into utils file 2018-08-10 21:51:21 +10:00
Jesse Duffield
a8c6f31bc6 Merge pull request #107 from antham/master
Apply gofmt
2018-08-10 21:42:50 +10:00
Jesse Duffield
1fded005c4 revert to using default border color 2018-08-10 21:40:21 +10:00
Jesse Duffield
c470c1f575 keep asterisk at the checked out branch 2018-08-10 21:38:51 +10:00
Jesse Duffield
d08241b2ea Obtain branches in a more robust way. Begin refactor work on gitcommands 2018-08-10 21:34:17 +10:00
Anthony HAMON
de7ab80539 apply gofmt -s -w 2018-08-10 09:54:21 +02:00
Ching-Hsuan Yen
d3abd9eab1 Add fixup option into commits panel 2018-08-10 13:44:42 +08:00
Jesse Duffield
3f89b5bf71 Merge pull request #105 from jesseduffield/feature/add-go-git
Explicitly add go-git dependency
2018-08-10 11:51:51 +10:00
Jesse Duffield
87872f5514 explicitly add go-git dependency 2018-08-10 11:33:49 +10:00
maxice8
bf9a53fada README.md: Add Void Linux instructions 2018-08-09 19:39:18 -03:00
frealgagu
3adf5368dd Adding arch linux packages to README.md 2018-08-09 09:13:24 -05:00
Jesse Duffield
ed6f21ee74 bump version to v0.1.48 2018-08-09 23:35:06 +10:00
Jesse Duffield
75a186614a add scroll keybinding to displayed files panel options 2018-08-09 23:34:50 +10:00
Jesse Duffield
18aa5b1bbe bump version to v0.1.47 2018-08-09 23:34:16 +10:00
Jesse Duffield
89209d1253 bump version to v0.1.46 2018-08-09 23:27:11 +10:00
Jesse Duffield
bcad80250a add basic vim keybindings
add keybinding for the tab key to begin a newline when writing a commit message
it would have been shift+enter but currently that is not supported by gocui
2018-08-09 23:26:31 +10:00
Jesse Duffield
44eaccfd14 switch to using the log package rather than directly writing to file 2018-08-09 20:29:58 +10:00
Jesse Duffield
9537645d0c dynamic width for confirmation panel and better handling of a squashed terminal 2018-08-09 19:49:36 +10:00
Jesse Duffield
d08e3a55a1 WIP hard reset 2018-08-09 19:33:35 +10:00
Jesse Duffield
2cd0bd8125 Merge branch 'master' of https://github.com/jesseduffield/lazygit 2018-08-09 19:02:19 +10:00
Jesse Duffield
cc31cb1abe remove double quote escapes in commit message for auto version bumper 2018-08-09 19:02:16 +10:00
Jesse Duffield
a56643fe64 refactor repeater functions and refresh file panel every ten seconds 2018-08-09 19:01:42 +10:00
Jesse Duffield
2386283e45 Update README.md 2018-08-09 16:07:25 +10:00
Jesse Duffield
58977ed7f3 remove the dist folder after goreleaser runs 2018-08-09 15:47:55 +10:00
Jesse Duffield
a068548bcb remove speed monitoring 2018-08-09 15:44:28 +10:00
Jesse Duffield
b2f8b8b345 "bump version to v0.1.45" 2018-08-09 15:24:39 +10:00
Jesse Duffield
5f70b2d9cd throw error when git username not configured 2018-08-09 15:24:12 +10:00
Jesse Duffield
f33b2b2277 "bump version to v0.1.44" 2018-08-09 14:46:10 +10:00
Jesse Duffield
dbf65a422a bump dependencies 2018-08-09 14:41:58 +10:00
Jesse Duffield
ace8544512 create log if debugging and the file doesn't already exist 2018-08-09 14:36:26 +10:00
Jesse Duffield
4832d365f1 use go-git for commits 2018-08-09 14:33:51 +10:00
Jesse Duffield
bebe94b4b3 don't call projectPath until we know we're in debug mode 2018-08-09 13:37:48 +10:00
Jesse Duffield
750445dc8b use platform agnostic filepaths 2018-08-09 13:29:25 +10:00
Jesse Duffield
1f5f80b1bf platform independent path reading 2018-08-09 13:21:30 +10:00
Jesse Duffield
5cc34e7801 factor out platform specific logic into a struct on state 2018-08-09 13:14:24 +10:00
Jesse Duffield
ce8884f509 "bump version to v0.1.43" 2018-08-09 12:56:11 +10:00
Jesse Duffield
481a05f116 check for VERSION file in project directory rather than current directory 2018-08-09 12:55:41 +10:00
Jesse Duffield
6e8abbcdda use fallback file VERSION 2018-08-09 12:25:32 +10:00
Jesse Duffield
273eb6244b "bump version to v0.1.42" 2018-08-09 12:10:33 +10:00
Jesse Duffield
467741fa54 add basic script to bump patch number and call goreleaser 2018-08-09 12:10:22 +10:00
Jesse Duffield
a00bbf709f "bump version to v0.1.41" 2018-08-09 12:09:30 +10:00
Jesse Duffield
b90e66dca1 "bump version to v0.1.40" 2018-08-09 12:02:37 +10:00
Jesse Duffield
77eea90b0b "bump version to v0.1.39" 2018-08-09 12:00:23 +10:00
Jesse Duffield
79b012990b Update README.md 2018-08-09 11:07:41 +10:00
Jesse Duffield
45c0b9404a use zip for windows binaries 2018-08-09 11:05:10 +10:00
Jesse Duffield
50924f31b0 Merge branch 'master' of https://github.com/jesseduffield/lazygit 2018-08-09 11:00:57 +10:00
Jesse Duffield
705d273ad3 test comment to see if goreleaser only releases on new commits 2018-08-09 11:00:50 +10:00
Jesse Duffield
30c6fc2300 Update README.md 2018-08-09 10:48:57 +10:00
Jesse Duffield
16b202f227 Update README.md 2018-08-09 10:46:57 +10:00
Jesse Duffield
7c1dbb7c66 Update README.md 2018-08-09 10:46:27 +10:00
Jesse Duffield
41152881db Merge branch 'master' of https://github.com/jesseduffield/lazygit 2018-08-09 10:32:18 +10:00
Jesse Duffield
d33fff54a3 point to homebrew repo 2018-08-09 10:32:12 +10:00
Jesse Duffield
3bf621de65 Update README.md 2018-08-09 10:21:45 +10:00
Jesse Duffield
0c84055abd show error on git add --patching untracked file 2018-08-09 09:33:18 +10:00
Jesse Duffield
5d9df3ca07 Merge pull request #86 from dawidd6/patch-1
Precise supported ubuntu releases by ppa
2018-08-09 09:25:21 +10:00
Dawid Dziurla
6581bec1ce precise supported ubuntu releases by ppa 2018-08-08 23:21:41 +02:00
Jesse Duffield
ce3f81a2d3 remove github token 2018-08-09 00:26:32 +10:00
Jesse Duffield
bc479a9bf5 correct repo name 2018-08-08 23:29:59 +10:00
Jesse Duffield
83a9a6d1f5 Merge branch 'master' of https://github.com/jesseduffield/lazygit 2018-08-08 23:28:59 +10:00
Jesse Duffield
ca0b3debe9 attempt brew formula 2018-08-08 23:28:52 +10:00
Jesse Duffield
84a2b1ceb9 Update README.md 2018-08-08 23:21:35 +10:00
Jesse Duffield
5951811a8b fix releaser 2018-08-08 23:16:52 +10:00
Jesse Duffield
968f3b2c1c revert to stock standard goreleaser yml 2018-08-08 23:13:26 +10:00
Jesse Duffield
aacbb72e6c use env github token 2018-08-08 23:09:39 +10:00
Jesse Duffield
48ab294922 remove newline in github token 2018-08-08 23:06:50 +10:00
Jesse Duffield
555996642d try just pointing at the directory 2018-08-08 23:04:51 +10:00
Jesse Duffield
aad4269128 fix yml 2018-08-08 23:03:10 +10:00
Jesse Duffield
b3fbe8453f fix yml format 2018-08-08 23:00:27 +10:00
Jesse Duffield
ffac0e6420 Merge branch 'feature/gorelease' 2018-08-08 22:54:49 +10:00
Jesse Duffield
7371765d47 bring back white borders for everybody 2018-08-08 22:53:24 +10:00
Jesse Duffield
7f75808499 cleanup for goreleaser 2018-08-08 22:51:18 +10:00
Jesse Duffield
778cb14dc4 update to use goreleaser ldflags 2018-08-08 22:48:37 +10:00
Jesse Duffield
00c679a3f0 swap travis for goreleaser 2018-08-08 22:40:01 +10:00
Jesse Duffield
d9ec6f9890 switch border color back to black for osx 2018-08-08 21:53:28 +10:00
Jesse Duffield
9858bfe4c3 use brighter panels for linux 2018-08-08 21:46:56 +10:00
Jesse Duffield
feada164d0 use file glob 2018-08-08 21:22:08 +10:00
Jesse Duffield
a1706ef2ee remove 1.7 because I'm not using it 2018-08-08 21:09:51 +10:00
Jesse Duffield
1bd3aa9708 remove version number from filename and use regex to pick files to deploy 2018-08-08 21:08:05 +10:00
Jesse Duffield
d0aaf94068 use default platforms 2018-08-08 21:04:04 +10:00
Jesse Duffield
8054d85782 add more archs to travis.yml 2018-08-08 20:59:32 +10:00
Jesse Duffield
31a8be5f32 update keybindings 2018-08-08 20:53:14 +10:00
Jesse Duffield
ca04655a09 merge master/better-file-opening into master 2018-08-08 20:50:45 +10:00
Jesse Duffield
4281cc2884 add git config check and editing ability 2018-08-08 20:45:12 +10:00
Jesse Duffield
8013f18177 add test script that reads user input for subprocess testing 2018-08-08 20:44:06 +10:00
Jesse Duffield
c965333756 "opening and editing" 2018-08-08 19:46:21 +10:00
Jesse Duffield
54408a912c merge moving keybindings to its own file 2018-08-08 19:24:33 +10:00
Jesse Duffield
3839719154 "move keybindings into its own file" 2018-08-08 19:18:41 +10:00
Jesse Duffield
e8c4bf20f6 merge master 2018-08-08 19:13:13 +10:00
Jesse Duffield
fbadbdd771 version with tag 2018-08-08 18:57:27 +10:00
Jesse Duffield
c6aee678c0 bump dep 2018-08-08 18:57:03 +10:00
Jesse Duffield
c4c27262f2 allow go 1.7 to fail 2018-08-08 18:02:18 +10:00
Jesse Duffield
a253b0b897 Merge branch 'master' of https://github.com/jesseduffield/lazygit 2018-08-08 08:59:09 +10:00
Jesse Duffield
5ca47c3f47 merge master 2018-08-08 08:58:24 +10:00
Jesse Duffield
456c593c72 Update README.md 2018-08-08 08:49:37 +10:00
Jesse Duffield
3b264806ef add version number 2018-08-08 08:32:52 +10:00
Jesse Duffield
92e75d4602 "step one towards dealing with gpgsign" 2018-08-08 08:24:24 +10:00
Jesse Duffield
7add598235 give everybody permission 2018-08-08 08:13:40 +10:00
Jesse Duffield
2853aba951 appending version number to binaries 2018-08-08 08:07:43 +10:00
Jesse Duffield
8cc444182f make binaries executable 2018-08-08 08:02:33 +10:00
Jesse Duffield
2a25ce014b use correct filenames 2018-08-08 07:56:55 +10:00
Jesse Duffield
411c02324f use gox default output 2018-08-08 07:54:46 +10:00
Jesse Duffield
2f50cbf2b8 use gox default output 2018-08-08 07:54:11 +10:00
Jesse Duffield
417cb97dc6 add file to command 2018-08-08 07:49:45 +10:00
Jesse Duffield
fb5d25c9e9 Merge branch 'master' into feature/better-file-opening 2018-08-08 07:41:55 +10:00
Jesse Duffield
f580572e70 Merge branch 'master' of https://github.com/jesseduffield/lazygit 2018-08-08 07:40:04 +10:00
Jesse Duffield
ba30e9e450 log some travis ci stuff 2018-08-08 07:39:58 +10:00
Nicolas Borboën
30d79bfa2c Merge remote-tracking branch 'upstream/master' into ponsfrilus/cli-version 2018-08-07 16:17:01 +02:00
Nicolas Borboën
4d9d2b134f Add lazygit --v version
* Recover Rev and Build Date from build args
* Moved `debuggingPointer` and `versionFlag` to `var()`
* Print version and exit in case of `-v` or `--v`
2018-08-07 16:15:23 +02:00
Nicolas Borboën
6cb0a14f33 Add build date to build cmd 2018-08-07 16:11:53 +02:00
Jesse Duffield
51d3e679bf Merge pull request #66 from ponsfrilus/patch-1
[fix] link to code of conduct
2018-08-07 23:21:22 +10:00
Jesse Duffield
bab884f1df nonrelative path to binaries 2018-08-07 23:18:18 +10:00
Jesse Duffield
1730089b09 try master branch for dep 2018-08-07 23:04:55 +10:00
Nicolas Borboën
8c823965ab [fix] link to code of conduct 2018-08-07 14:59:22 +02:00
Jesse Duffield
aa6a0bd43b Merge branch 'master' of https://github.com/jesseduffield/lazygit 2018-08-07 22:53:38 +10:00
Jesse Duffield
d781a8e650 if you had one shot 2018-08-07 22:53:29 +10:00
Jesse Duffield
4528cf95b9 fix wrapping 2018-08-07 22:48:40 +10:00
Jesse Duffield
dd6cdc02f0 Merge pull request #65 from ponsfrilus/ponsfrilus/coc-cg
Code of Conduct + Contributing guide proposal
2018-08-07 22:46:43 +10:00
Nicolas Borboën
2311675d08 It appears that markdown files are better handeled by GitHub with extensions. 2018-08-07 14:38:58 +02:00
Nicolas Borboën
cda365a93f Contributing guide proposal 2018-08-07 14:32:04 +02:00
Jesse Duffield
b567a86ee5 another day another attempt at CI 2018-08-07 22:30:06 +10:00
Nicolas Borboën
a6cb0feff1 Contributing guide added
This is a mix between
  * https://gist.github.com/PurpleBooth/b24679402957c63ec426
  *
https://github.com/thoughtbot/factory_bot_rails/blob/master/CONTRIBUTING.md
  * https://gist.github.com/briandk/3d2e8b3ec8daf5a27a62
  * https://github.com/nayafia/contributing-template
2018-08-07 14:28:52 +02:00
Jesse Duffield
af766c848c reverting to 386 because it worked before 2018-08-07 22:27:33 +10:00
Nicolas Borboën
ade0dc447e Code of conduct added
* Based on contributor-covenant.org
2018-08-07 14:27:11 +02:00
Jesse Duffield
8aca30ee4c Merge branch 'master' of https://github.com/jesseduffield/lazygit 2018-08-07 22:26:12 +10:00
Jesse Duffield
d5a73c0166 experimenting 2018-08-07 22:26:08 +10:00
Jesse Duffield
53044e20c9 Merge pull request #63 from dawidd6/ubuntu
Set minimal ubuntu version supported by ppa
2018-08-07 22:20:45 +10:00
Jesse Duffield
59ea1491db back to amd64 2018-08-07 22:20:04 +10:00
Jesse Duffield
69f47fddc4 back to go 1.7 2018-08-07 22:17:42 +10:00
Jesse Duffield
75e1a2d62f support other go again 2018-08-07 22:14:30 +10:00
Jesse Duffield
b715236c0a get gox 2018-08-07 22:13:42 +10:00
Dawid Dziurla
6925ceeb6e set minimal ubuntu version supported by ppa 2018-08-07 13:42:39 +02:00
Jesse Duffield
5a624764a8 support patched adding 2018-08-07 19:50:35 +10:00
Jesse Duffield
b541987007 fixup 2018-08-07 18:37:48 +10:00
Jesse Duffield
2d4801c39d merge updated keybinding structure 2018-08-07 18:14:49 +10:00
Jesse Duffield
7fe54e889d merge master 2018-08-07 18:13:45 +10:00
Jesse Duffield
f6a9c727fa run subprocess cleanly 2018-08-07 18:05:43 +10:00
Jesse Duffield
9067c3be3e handling file edit 2018-08-06 23:29:00 +10:00
1312 changed files with 327064 additions and 40106 deletions

62
.circleci/config.yml Normal file
View File

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

5
.github/FUNDING.yml vendored Normal file
View File

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

32
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,32 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. Windows]
- Lazygit Version [e.g. v0.1.45]
- The last commit id if you built project from sources (run : ```git-rev parse HEAD```)
**Additional context**
Add any other context about the problem here.

14
.github/ISSUE_TEMPLATE/discussion.md vendored Normal file
View File

@@ -0,0 +1,14 @@
---
name: Discussion
about: Begin a discussion
title: ''
labels: discussion
assignees: ''
---
**Topic**
A clear and concise description of what you want to discuss
**Your thoughts**
What you have to say about the topic

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

34
.gitignore vendored
View File

@@ -1,6 +1,28 @@
development.log
commands.log
extra/lgit.rb
notes/go.notes
TODO.notes
TODO.md
# Please do not add personal files
# Logs
*.log
# Hidden
.*
# TODO
TODO.*
# Notes
*.notes
# Tests
test/repos/repo
coverage.txt
# Binaries
lazygit
# Exceptions
!.gitignore
!.goreleaser.yml
!.circleci/
!.github/
test/git_server/data

65
.goreleaser.yml Normal file
View File

@@ -0,0 +1,65 @@
# This is an example goreleaser.yaml file with some sane defaults.
# Make sure to check the documentation at http://goreleaser.com
builds:
- env:
- CGO_ENABLED=0
goos:
- freebsd
- windows
- darwin
- linux
goarch:
- amd64
- arm
- arm64
- 386
# Default is `-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}`.
ldflags:
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.buildSource=binaryRelease
archives:
- replacements:
darwin: Darwin
linux: Linux
windows: Windows
386: 32-bit
amd64: x86_64
format_overrides:
- goos: windows
format: zip
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: '{{ .Tag }}-next'
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
- '^bump'
brews:
-
# Reporitory to push the tap to.
github:
owner: jesseduffield
name: homebrew-lazygit
# Your app's homepage.
# Default is empty.
homepage: 'https://github.com/jesseduffield/lazygit/'
# Your app's description.
# Default is empty.
description: 'A simple terminal UI for git commands, written in Go'
# # Packages your package depends on.
# dependencies:
# - git
# - zsh
# # Packages that conflict with your package.
# conflicts:
# - svn
# - bash
# test comment to see if goreleaser only releases on new commits

View File

@@ -1,38 +0,0 @@
language: go
sudo: false
env:
- DEP_VERSION="0.5.0"
matrix:
include:
- go: 1.7
env: LATEST=true
- go: tip
allow_failures:
- go: tip
before_install:
# Download the binary to bin folder in $GOPATH
- curl -L -s https://github.com/golang/dep/releases/download/v${DEP_VERSION}/dep-linux-386 -o $GOPATH/bin/dep
# Make the binary executable
- chmod +x $GOPATH/bin/dep
- dep ensure
install:
-
script:
# - go get -v ./...
# - diff -u <(echo -n) <(gofmt -d .) # can't make gofmt ignore vendor directory
# - go vet $(go list ./... | grep -v /vendor/)
- if [ "${LATEST}" = "true" ]; then gox -os="linux darwin windows" -arch="amd64" -output="lazygit.."
-ldflags "-X main.Rev=`git rev-parse --short HEAD`" -verbose ./...; fi
deploy:
provider: releases
skip_cleanup: true
api_key:
secure: TnB8I+swjicHuGTXk3ncm1Aaa12eIJqWV/Lhcnbb01i39p6+fyn3vDMdWPcejt3R8gcJqv4wyP8UQVO9G1qkLppt6V/qAuY5x6nX0MgEa3t+8JLJnGYHZYsuIgan/ecAmeu5+6dgUhr9Oq6zQOEv/O88NsALzMlqnEQNXI8XSoScfhkiVDIp3zWov0vBizCdThnNgTx9zRpJVoqxmhWvgt+me2+fOhSx1Y+3ZA2gE7zq8IFAbxp36d0rsR5lKqmTuF+YsF9iQ7Ar+xCjbRunLsZx+VwGqGfpS/qS7EwsEqBI0vEO76eFJkwEsIzOvJiFNhBDUu3upquBFMT4uzxRxH3eV+J4mZtu29UDLdvKI5Q730Lk9AgmH4now+RmP08M0SEXJa+AnHeuBv2u1iU5bu+sI6CORVQzKQwOph9AABDjSZ54wrXIpYEeIW2sz8nx+hiG6QL1mqfM/l+55BR69u3vxKYMryQBxPuzhZCTOqqI4uahlb6GIUNZJ9vGZeIA9HFJq3ymW8cdrpYzhKf3Nx9jK+Yb81h5/AHq9iChXEC63VPCDXXGRllh2UYWNYCaAdtk+ekpLR8299e4CaEregy6g5U2S3/xrBKl87miu1uJ/fquXoxGdSU+JcmsmXZ26sGIU2TCYdNjSfIgpOyfMmB4JNtKHqWRHA9Fe42CRpA=
file:
- lazygit.windows.amd64.exe
- lazygit.darwin.amd64
- lazygit.linux.amd64
on:
repo: jesseduffield/lazygit
tags: true
condition: $LATEST = true

74
CODE-OF-CONDUCT.md Normal file
View File

@@ -0,0 +1,74 @@
# Contributor Covenant 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

35
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,35 @@
# 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.
## 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!
## 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!

15
Dockerfile Normal file
View File

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

77
Gopkg.lock generated
View File

@@ -1,77 +0,0 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
digest = "1:865079840386857c809b72ce300be7580cb50d3d3129ce11bf9aa6ca2bc1934a"
name = "github.com/fatih/color"
packages = ["."]
pruneopts = "UT"
revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4"
version = "v1.7.0"
[[projects]]
branch = "master"
digest = "1:4a8ed9b8cf22bd03bee5d74179fa06a282e4a73b6de949f7a865ff56cd2537e0"
name = "github.com/golang-collections/collections"
packages = ["stack"]
pruneopts = "UT"
revision = "604e922904d35e97f98a774db7881f049cd8d970"
[[projects]]
branch = "master"
digest = "1:f086cf183e423bf1926f4de05f47bf57132fe5db9c99e464f733ce925280fc81"
name = "github.com/jesseduffield/gocui"
packages = ["."]
pruneopts = "UT"
revision = "3c923f53ac9952af649af04a067405843558d56f"
[[projects]]
digest = "1:c658e84ad3916da105a761660dcaeb01e63416c8ec7bc62256a9b411a05fcd67"
name = "github.com/mattn/go-colorable"
packages = ["."]
pruneopts = "UT"
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
version = "v0.0.9"
[[projects]]
digest = "1:d4d17353dbd05cb52a2a52b7fe1771883b682806f68db442b436294926bbfafb"
name = "github.com/mattn/go-isatty"
packages = ["."]
pruneopts = "UT"
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
version = "v0.0.3"
[[projects]]
digest = "1:e2d1d410fb367567c2b53ed9e2d719d3c1f0891397bb2fa49afd747cfbf1e8e4"
name = "github.com/mattn/go-runewidth"
packages = ["."]
pruneopts = "UT"
revision = "9e777a8366cce605130a531d2cd6363d07ad7317"
version = "v0.0.2"
[[projects]]
branch = "master"
digest = "1:c9b6e36dbd23f8403a04493376916ca5dad8c01b2da5ae0a05e6a468eb0b6f24"
name = "github.com/nsf/termbox-go"
packages = ["."]
pruneopts = "UT"
revision = "5c94acc5e6eb520f1bcd183974e01171cc4c23b3"
[[projects]]
branch = "master"
digest = "1:9ec6ad2659635ba0974dd7e55bf84233523eb4e7535c9a2fddaefc4cc62a3eac"
name = "golang.org/x/sys"
packages = ["unix"]
pruneopts = "UT"
revision = "0ffbfd41fbef8ffcf9b62b0b0aa3a5873ed7a4fe"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = [
"github.com/fatih/color",
"github.com/golang-collections/collections/stack",
"github.com/jesseduffield/gocui",
]
solver-name = "gps-cdcl"
solver-version = 1

207
README.md
View File

@@ -1,37 +1,171 @@
# lazygit [![Go Report Card](https://goreportcard.com/badge/github.com/jesseduffield/lazygit)](https://goreportcard.com/report/github.com/jesseduffield/lazygit)
# lazygit [![CircleCI](https://circleci.com/gh/jesseduffield/lazygit.svg?style=svg)](https://circleci.com/gh/jesseduffield/lazygit) [![codecov](https://codecov.io/gh/jesseduffield/lazygit/branch/master/graph/badge.svg)](https://codecov.io/gh/jesseduffield/lazygit) [![Go Report Card](https://goreportcard.com/badge/github.com/jesseduffield/lazygit)](https://goreportcard.com/report/github.com/jesseduffield/lazygit) [![GolangCI](https://golangci.com/badges/github.com/jesseduffield/lazygit.svg)](https://golangci.com) [![GoDoc](https://godoc.org/github.com/jesseduffield/lazygit?status.svg)](http://godoc.org/github.com/jesseduffield/lazygit) [![GitHub tag](https://img.shields.io/github/tag/jesseduffield/lazygit.svg)]() [![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.
A simple terminal UI for git commands, written in Go with the [gocui](https://github.com/jroimartin/gocui 'gocui') library.
Are YOU tired of typing every git command directly into the terminal, but you're too stubborn to use Sourcetree because you'll never forgive Atlassian for making Jira? This is the app for you!
Rant time: You've heard it before, git is _powerful_, but what good is that power when everything is so damn hard to do? Interactive rebasing requires you to edit a goddamn TODO file in your editor? *Are you kidding me?* To stage part of a file you need to use a command line program stepping through each hunk and if a hunk can't be split down any further but contains code you don't want to stage, bad luck? *Are you KIDDING me?!* Sometimes you get asked to stash your changes when switching branches only to realise that after you switch and unstash that there weren't even any conflicts and it would have been fine to just checkout the branch directly? *YOU HAVE GOT TO BE KIDDING ME!*
[Tutorial](https://www.youtube.com/watch?v=VDXvbHZYeKY)
If you're a mere mortal like me and you're tired of hearing how powerful git is when in your daily life it's a powerful pain in your ass, lazygit might be for you.
![Gif](https://image.ibb.co/mmeXho/optimisedgif.gif)
![Gif](/docs/resources/lazygit-example.gif)
- [Installation](https://github.com/jesseduffield/lazygit#installation)
- [Usage](https://github.com/jesseduffield/lazygit#usage),
[Keybindings](/docs/keybindings)
- [Cool Features](https://github.com/jesseduffield/lazygit#cool-features)
- [Contributing](https://github.com/jesseduffield/lazygit#contributing)
- [Video Tutorial](https://youtu.be/VDXvbHZYeKY)
- [Rebase Magic Video Tutorial](https://youtu.be/4XaToVut_hs)
- [Twitch Stream](https://www.twitch.tv/jesseduffield)
[<img src="https://i.imgur.com/sVEktDn.png">](https://youtu.be/CPLdltN7wgE)
Github Sponsors is matching all donations dollar-for-dollar for 12 months so if you're feeling generous consider [sponsoring me](https://github.com/sponsors/jesseduffield)
## Installation
In a terminal call this command:
`go get github.com/jesseduffield/lazygit`
(if you don't have Go installed, you can follow the installation guide [here](https://golang.org/doc/install).
Then just call `lazygit` in your terminal inside a git repository.
### Homebrew
If you want, you can also add an alias for this with `echo "alias lg='lazygit'" >> ~/.zshrc` (or whichever rc file you're using).
Normally the lazygit formula can be found in the Homebrew core but we suggest you tap our formula to get the frequently updated one. It works with Linux, too.
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)
Tap:
```
brew install jesseduffield/lazygit/lazygit
```
Core:
```
brew install lazygit
```
### MacPorts
Latest version built from github releases.
Tap:
```
sudo port install lazygit
```
### Ubuntu
Packages for Ubuntu 14.04 and up are available via Launchpad PPA.
They are built daily, straight from master branch.
Packages for Ubuntu are available via [Launchpad PPA](https://launchpad.net/~lazygit-team).
```sh
sudo add-apt-repository ppa:lazygit-team/daily
sudo add-apt-repository ppa:lazygit-team/release
sudo apt-get update
sudo apt-get install lazygit
```
### Void Linux
Packages for Void Linux are available in the distro repo
They follow upstream latest releases
```sh
sudo xbps-install -S lazygit
```
### Scoop (Windows)
You can install `lazygit` using [scoop](https://scoop.sh/). It's in the `extras` bucket:
```sh
# Add the extras bucket
scoop bucket add extras
# Install lazygit
scoop install lazygit
```
### Arch Linux
Packages for Arch Linux are available via AUR (Arch User Repository).
There are two packages. The stable one which is built with the latest release
and the git version which builds from the most recent commit.
- Stable: <https://aur.archlinux.org/packages/lazygit/>
- Development: <https://aur.archlinux.org/packages/lazygit-git/>
Instruction of how to install AUR content can be found here:
<https://wiki.archlinux.org/index.php/Arch_User_Repository>
### Fedora and CentOS 7
Packages for Fedora and CentOS 7 are available via [Copr](https://copr.fedorainfracloud.org/coprs/atim/lazygit/) (Cool Other Package Repo).
```sh
sudo dnf copr enable atim/lazygit -y
sudo dnf install lazygit
```
### Conda
Released versions are available for different platforms, see <https://anaconda.org/conda-forge/lazygit>
```sh
conda install -c conda-forge lazygit
```
### Binary Release (Windows/Linux/OSX)
You can download a binary release [here](https://github.com/jesseduffield/lazygit/releases).
### Go
```sh
go get github.com/jesseduffield/lazygit
```
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).
## Usage
Call `lazygit` in your terminal inside a git repository. If you want, you can
also add an alias for this with `echo "alias lg='lazygit'" >> ~/.zshrc` (or
whichever rc file you're using).
- Basic video tutorial [here](https://youtu.be/VDXvbHZYeKY).
- Rebase Magic tutorial [here](https://youtu.be/4XaToVut_hs)
- List of keybindings
[here](/docs/keybindings).
## Changing Directory On Exit
If you change repos in lazygit and want your shell to change directory into that repo on exiting lazygit, add this to your `~/.zshrc` (or other rc file):
```
lg()
{
export LAZYGIT_NEW_DIR_FILE=~/.lazygit/newdir
lazygit "$@"
if [ -f $LAZYGIT_NEW_DIR_FILE ]; then
cd "$(cat $LAZYGIT_NEW_DIR_FILE)"
rm -f $LAZYGIT_NEW_DIR_FILE > /dev/null
fi
}
```
Then `source ~/.zshrc` and from now on when you call `lg` and exit you'll switch directories to whatever you were in inside lazyigt. To override this behaviour you can exit using `shift+Q` rather than just `q`.
## Configuration
Check the [configuration docs](docs/Config.md).
## Custom Pagers
See the [docs](docs/Custom_Pagers.md)
## Cool features
- Adding files easily
- Resolving merge conflicts
- Easily check out recent branches
@@ -40,24 +174,39 @@ sudo apt-get install lazygit
- Squash down and rename commits
### Resolving merge conflicts
![Gif](https://image.ibb.co/iyxUTT/shortermerging.gif)
### Viewing commit diffs
![Viewing Commit Diffs](https://image.ibb.co/gPD02o/capture.png)
![Gif](/docs/resources/resolving-merge-conflicts.gif)
## Docs
[Keybindings](https://github.com/jesseduffield/lazygit/blob/master/docs/Keybindings.md)
### Interactive Rebasing
## Milestones
- [ ] Easy Installation (homebrew, release binaries)
- [ ] Configurable Keybindings
- [ ] Configurable Color Themes
- [ ] Spawning Subprocesses (help needed - have a look at https://github.com/jesseduffield/lazygit/pull/18)
- [ ] Performance
- [ ] i18n
![Interactive Rebasing](/docs/resources/interactive-rebase.png)
## Contributing
I'll find a good template for contributing and then add it to the repo (or if somebody has a suggestion please put up a PR)
We love your input! Please check out the [contributing guide](CONTRIBUTING.md).
For contributor discussion about things not better discussed here in the repo, join the slack channel
[![Slack](/docs/resources/slack_rgb.png)](https://join.slack.com/t/lazygit/shared_invite/enQtNDE3MjIwNTYyMDA0LTM3Yjk3NzdiYzhhNTA1YjM4Y2M4MWNmNDBkOTI0YTE4YjQ1ZmI2YWRhZTgwNjg2YzhhYjg3NDBlMmQyMTI5N2M)
## 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
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).
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).
## Social
If you want to see what I (Jesse) am up to in terms of development, follow me on
[twitter](https://twitter.com/DuffieldJesse) or watch me program on
[twitch](https://www.twitch.tv/jesseduffield).
## Alternatives
If you find that lazygit doesn't quite satisfy your requirements, these may be a better fit:
- [tig](https://github.com/jonas/tig)

View File

@@ -1,120 +0,0 @@
package main
import (
"fmt"
"strings"
"github.com/jesseduffield/gocui"
)
func handleBranchPress(g *gocui.Gui, v *gocui.View) error {
index := getItemPosition(v)
if index == 0 {
return createErrorPanel(g, "You have already checked out this branch")
}
branch := getSelectedBranch(v)
if output, err := gitCheckout(branch.Name, false); err != nil {
createErrorPanel(g, output)
}
return refreshSidePanels(g)
}
func handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
branch := getSelectedBranch(v)
return createConfirmationPanel(g, v, "Force Checkout Branch", "Are you sure you want force checkout? You will lose all local changes", func(g *gocui.Gui, v *gocui.View) error {
if output, err := gitCheckout(branch.Name, true); err != nil {
createErrorPanel(g, output)
}
return refreshSidePanels(g)
}, nil)
}
func handleCheckoutByName(g *gocui.Gui, v *gocui.View) error {
createPromptPanel(g, v, "Branch Name:", func(g *gocui.Gui, v *gocui.View) error {
if output, err := gitCheckout(trimmedContent(v), false); err != nil {
return createErrorPanel(g, output)
}
return refreshSidePanels(g)
})
return nil
}
func handleNewBranch(g *gocui.Gui, v *gocui.View) error {
branch := state.Branches[0]
createPromptPanel(g, v, "New Branch Name (Branch is off of "+branch.Name+")", func(g *gocui.Gui, v *gocui.View) error {
if output, err := gitNewBranch(trimmedContent(v)); err != nil {
return createErrorPanel(g, output)
}
refreshSidePanels(g)
return handleBranchSelect(g, v)
})
return nil
}
func handleMerge(g *gocui.Gui, v *gocui.View) error {
checkedOutBranch := state.Branches[0]
selectedBranch := getSelectedBranch(v)
defer refreshSidePanels(g)
if checkedOutBranch.Name == selectedBranch.Name {
return createErrorPanel(g, "You cannot merge a branch into itself")
}
if output, err := gitMerge(selectedBranch.Name); err != nil {
return createErrorPanel(g, output)
}
return nil
}
func getSelectedBranch(v *gocui.View) Branch {
lineNumber := getItemPosition(v)
return state.Branches[lineNumber]
}
func renderBranchesOptions(g *gocui.Gui) error {
return renderOptionsMap(g, map[string]string{
"space": "checkout",
"f": "force checkout",
"m": "merge",
"c": "checkout by name",
"n": "new branch",
"← → ↑ ↓": "navigate",
})
}
// may want to standardise how these select methods work
func handleBranchSelect(g *gocui.Gui, v *gocui.View) error {
if err := renderBranchesOptions(g); err != nil {
return err
}
// This really shouldn't happen: there should always be a master branch
if len(state.Branches) == 0 {
return renderString(g, "main", "No branches for this repo")
}
go func() {
branch := getSelectedBranch(v)
diff, err := getBranchGraph(branch.Name, branch.BaseBranch)
if err != nil && strings.HasPrefix(diff, "fatal: ambiguous argument") {
diff = "There is no tracking for this branch"
}
renderString(g, "main", diff)
}()
return nil
}
// refreshStatus is called at the end of this because that's when we can
// be sure there is a state.Branches array to pick the current branch from
func refreshBranches(g *gocui.Gui) error {
g.Update(func(g *gocui.Gui) error {
v, err := g.View("branches")
if err != nil {
panic(err)
}
state.Branches = getGitBranches()
v.Clear()
for _, branch := range state.Branches {
fmt.Fprintln(v, branch.DisplayString)
}
resetOrigin(v)
return refreshStatus(g)
})
return nil
}

View File

@@ -1,138 +0,0 @@
package main
import (
"errors"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
)
var (
// ErrNoCommits : When no commits are found for the branch
ErrNoCommits = errors.New("No commits for this branch")
)
func refreshCommits(g *gocui.Gui) error {
g.Update(func(*gocui.Gui) error {
state.Commits = getCommits()
v, err := g.View("commits")
if err != nil {
panic(err)
}
v.Clear()
red := color.New(color.FgRed)
yellow := color.New(color.FgYellow)
white := color.New(color.FgWhite)
shaColor := white
for _, commit := range state.Commits {
if commit.Pushed {
shaColor = red
} else {
shaColor = yellow
}
shaColor.Fprint(v, commit.Sha+" ")
white.Fprintln(v, commit.Name)
}
refreshStatus(g)
return nil
})
return nil
}
func handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error {
return createConfirmationPanel(g, commitView, "Reset To Commit", "Are you sure you want to reset to this commit?", func(g *gocui.Gui, v *gocui.View) error {
commit, err := getSelectedCommit(g)
devLog(commit)
if err != nil {
panic(err)
}
if output, err := gitResetToCommit(commit.Sha); err != nil {
return createErrorPanel(g, output)
}
if err := refreshCommits(g); err != nil {
panic(err)
}
if err := refreshFiles(g); err != nil {
panic(err)
}
resetOrigin(commitView)
return handleCommitSelect(g, nil)
}, nil)
}
func renderCommitsOptions(g *gocui.Gui) error {
return renderOptionsMap(g, map[string]string{
"s": "squash down",
"r": "rename",
"g": "reset to this commit",
"← → ↑ ↓": "navigate",
})
}
func handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
if err := renderCommitsOptions(g); err != nil {
return err
}
commit, err := getSelectedCommit(g)
if err != nil {
if err != ErrNoCommits {
return err
}
return renderString(g, "main", "No commits for this branch")
}
commitText := gitShow(commit.Sha)
return renderString(g, "main", commitText)
}
func handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
if getItemPosition(v) != 0 {
return createErrorPanel(g, "Can only squash topmost commit")
}
if len(state.Commits) == 1 {
return createErrorPanel(g, "You have no commits to squash with")
}
commit, err := getSelectedCommit(g)
if err != nil {
return err
}
if output, err := gitSquashPreviousTwoCommits(commit.Name); err != nil {
return createErrorPanel(g, output)
}
if err := refreshCommits(g); err != nil {
panic(err)
}
refreshStatus(g)
return handleCommitSelect(g, v)
}
func handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
if getItemPosition(v) != 0 {
return createErrorPanel(g, "Can only rename topmost commit")
}
createPromptPanel(g, v, "Rename Commit", func(g *gocui.Gui, v *gocui.View) error {
if output, err := gitRenameCommit(v.Buffer()); err != nil {
return createErrorPanel(g, output)
}
if err := refreshCommits(g); err != nil {
panic(err)
}
return handleCommitSelect(g, v)
})
return nil
}
func getSelectedCommit(g *gocui.Gui) (Commit, error) {
v, err := g.View("commits")
if err != nil {
panic(err)
}
if len(state.Commits) == 0 {
return Commit{}, ErrNoCommits
}
lineNumber := getItemPosition(v)
if lineNumber > len(state.Commits)-1 {
colorLog(color.FgRed, "potential error in getSelected Commit (mismatched ui and state)", state.Commits, lineNumber)
return state.Commits[len(state.Commits)-1], nil
}
return state.Commits[lineNumber], nil
}

View File

@@ -1,119 +0,0 @@
// lots of this has been directly ported from one of the example files, will brush up later
// Copyright 2014 The gocui Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
)
func wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.View) error) func(*gocui.Gui, *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
if function != nil {
if err := function(g, v); err != nil {
panic(err)
}
}
return closeConfirmationPrompt(g)
}
}
func closeConfirmationPrompt(g *gocui.Gui) error {
view, err := g.View("confirmation")
if err != nil {
panic(err)
}
if err := returnFocus(g, view); err != nil {
panic(err)
}
g.DeleteKeybindings("confirmation")
return g.DeleteView("confirmation")
}
func getMessageHeight(message string, width int) int {
lines := strings.Split(message, "\n")
lineCount := 0
for _, line := range lines {
lineCount += len(line)/width + 1
}
return lineCount
}
func getConfirmationPanelDimensions(g *gocui.Gui, prompt string) (int, int, int, int) {
width, height := g.Size()
panelWidth := 60
panelHeight := getMessageHeight(prompt, panelWidth)
return width/2 - panelWidth/2,
height/2 - panelHeight/2 - panelHeight%2 - 1,
width/2 + panelWidth/2,
height/2 + panelHeight/2
}
func createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, handleYes func(*gocui.Gui, *gocui.View) error) error {
// only need to fit one line
x0, y0, x1, y1 := getConfirmationPanelDimensions(g, "")
if confirmationView, err := g.SetView("confirmation", x0, y0, x1, y1, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
g.Cursor = true
confirmationView.Editable = true
confirmationView.Title = title
confirmationView.FgColor = gocui.ColorWhite
switchFocus(g, currentView, confirmationView)
return setKeyBindings(g, handleYes, nil)
}
return nil
}
func createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, handleYes, handleNo func(*gocui.Gui, *gocui.View) error) error {
g.Update(func(g *gocui.Gui) error {
// delete the existing confirmation panel if it exists
if view, _ := g.View("confirmation"); view != nil {
if err := closeConfirmationPrompt(g); err != nil {
panic(err)
}
}
x0, y0, x1, y1 := getConfirmationPanelDimensions(g, prompt)
if confirmationView, err := g.SetView("confirmation", x0, y0, x1, y1, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
confirmationView.Title = title
confirmationView.FgColor = gocui.ColorWhite
renderString(g, "confirmation", prompt)
switchFocus(g, currentView, confirmationView)
return setKeyBindings(g, handleYes, handleNo)
}
return nil
})
return nil
}
func setKeyBindings(g *gocui.Gui, handleYes, handleNo func(*gocui.Gui, *gocui.View) error) error {
renderString(g, "options", "esc: close, enter: confirm")
if err := g.SetKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone, wrappedConfirmationFunction(handleYes)); err != nil {
return err
}
return g.SetKeybinding("confirmation", gocui.KeyEsc, gocui.ModNone, wrappedConfirmationFunction(handleNo))
}
func createMessagePanel(g *gocui.Gui, currentView *gocui.View, title, prompt string) error {
return createConfirmationPanel(g, currentView, title, prompt, nil, nil)
}
func createErrorPanel(g *gocui.Gui, message string) error {
currentView := g.CurrentView()
colorFunction := color.New(color.FgRed).SprintFunc()
coloredMessage := colorFunction(strings.TrimSpace(message))
return createConfirmationPanel(g, currentView, "Error", coloredMessage, nil, nil)
}

261
docs/Config.md Normal file
View File

@@ -0,0 +1,261 @@
# User Config
Default path for the config file:
* Linux: `~/.config/jesseduffield/lazygit/config.yml`
* MacOS: `~/Library/Application Support/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
theme:
lightTheme: false # For terminals with a light background
activeBorderColor:
- white
- bold
inactiveBorderColor:
- green
optionsTextColor:
- blue
selectedLineBgColor:
- blue
commitLength:
show: true
mouseEvents: true
skipUnstageLineWarning: false
git:
paging:
colorArg: always
useConfig: false
merging:
# only applicable to unix users
manualCommit: false
skipHookPrefix: WIP
autoFetch: true
update:
method: prompt # can be: prompt | background | never
days: 14 # how often an update is checked for
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
confirmOnQuit: false
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
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>'
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: '_'
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'
toggleDiffCommit: 'i'
checkoutCommit: '<space>'
stash:
popStash: 'g'
commitFiles:
checkoutCommitFile: 'c'
main:
toggleDragSelect: 'v'
toggleDragSelect-alt: 'V'
toggleSelectHunk: 'a'
pickBothHunks: 'b'
undo: 'z'
```
## Platform Defaults
### Windows
```yaml
os:
openCommand: 'cmd /c "start "" {{filename}}"'
```
### Linux
```yaml
os:
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'
```
### OSX
```yaml
os:
openCommand: 'open {{filename}}'
```
### Recommended Config Values
for users of VSCode
```yaml
os:
openCommand: 'code -r {{filename}}'
```
## Color Attributes
For color attributes you can choose an array of attributes (with max one color attribute)
The available attributes are:
- default
- black
- red
- green
- yellow
- blue
- magenta
- cyan
- white
- bold
- reverse # useful for high-contrast
- underline
## Light terminal theme
If you have issues with a light terminal theme where you can't read / see the text add these settings
```yaml
gui:
theme:
lightTheme: true
activeBorderColor:
- black
- bold
inactiveBorderColor:
- black
selectedLineBgColor:
- blue
```
## Example Coloring
![border example](/docs/resources/colored-border-example.png)
## Keybindings
For all possible keybinding options, check [Custom_Keybinding.md](https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybinding.md)
### Example Keybindings For Colemak Users
```yaml
keybinding:
universal:
prevItem-alt: 'u'
nextItem-alt: 'e'
prevBlock-alt: 'n'
nextBlock-alt: 'i'
nextMatch: '='
prevMatch: '-'
new: 'k'
edit: 'o'
openFile: 'O'
scrollUpMain-alt1: 'U'
scrollDownMain-alt1: 'E'
scrollUpMain-alt2: '<c-u>'
scrollDownMain-alt2: '<c-e>'
files:
ignoreFile: 'I'
commits:
moveDownCommit: '<c-e>'
moveUpCommit: '<c-u>'
toggleDiffCommit: 'l'
branches:
viewGitFlowOptions: 'I'
```
## Custom pull request URLs
Some git provider setups (e.g. on-premises GitLab) can have distinct URLs for git-related calls and
the web interface/API itself. To work with those, Lazygit needs to know where it needs to create
the pull request. You can do so on your `config.yml` file using the following syntax:
```yaml
services:
"<gitDomain>": "<provider>:<webDomain>"
```
Where:
- `gitDomain` stands for the domain used by git itself (i.e. the one present on clone URLs), e.g. `git.work.com`
- `provider` is one of `github`, `bitbucket` or `gitlab`
- `webDomain` is the URL where your git service exposes a web interface and APIs, e.g. `gitservice.work.com`

64
docs/Custom_Pagers.md Normal file
View File

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

View File

@@ -1,54 +0,0 @@
# Keybindings:
## Global:
← → ↑ ↓: navigate
PgUp/PgDn: scroll diff panel (use fn+up/down on osx)
q: quit
p: pull
shift+P: push
## Files Panel:
space: toggle staged
c: commit changes
shift+S: stash files
o: open (osx only)
s: open in sublime (requires 'subl' command)
v: open in vscode (requires 'code' command)
i: add to .gitignore
d: delete if untracked checkout if tracked (aka go away)
shift+R: refresh files
## Branches Panel:
space: checkout branch
f: force checkout branch
m: merge into currently checked out branch
c: checkout by name
n: new branch
## Commits Panel:
s: squash down (only available for topmost commit)
r: rename commit
g: reset to this commit
## Stash Panel:
space: apply
k: pop
d: drop
## Popup Panel:
esc: close/cancel
enter: confirm
## Resolving Merge Conflicts (Diff Panel):
← →: navigate conflicts
↑ ↓: select hunk
space: pick hunk
b: pick both hunks
z: undo (only available while still inside diff panel)

View File

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

View File

@@ -0,0 +1,202 @@
# Lazygit Keybindings
## Global Keybindings
<pre>
<kbd>pgup</kbd>: scroll up main panel (fn+up)
<kbd>pgdown</kbd>: scroll down main panel (fn+down)
<kbd>m</kbd>: view merge/rebase options
<kbd>ctrl+p</kbd>: view custom patch options
<kbd>P</kbd>: push
<kbd>p</kbd>: pull
<kbd>R</kbd>: refresh
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode
<kbd>:</kbd>: execute custom command
</pre>
## Branches Panel (Branches Tab)
<pre>
<kbd>space</kbd>: checkout
<kbd>o</kbd>: create pull request
<kbd>c</kbd>: checkout by name
<kbd>F</kbd>: force checkout
<kbd>n</kbd>: new branch
<kbd>d</kbd>: delete branch
<kbd>r</kbd>: rebase checked-out branch onto this branch
<kbd>M</kbd>: merge into currently checked out branch
<kbd>i</kbd>: show git-flow options
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>g</kbd>: view reset options
</pre>
## Branches Panel (Remote Branches (in Remotes tab))
<pre>
<kbd>esc</kbd>: return to remotes list
<kbd>g</kbd>: view reset options
<kbd>space</kbd>: checkout
<kbd>M</kbd>: merge into currently checked out branch
<kbd>d</kbd>: delete branch
<kbd>r</kbd>: rebase checked-out branch onto this branch
<kbd>u</kbd>: set as upstream of checked-out branch
</pre>
## Branches Panel (Remotes Tab)
<pre>
<kbd>f</kbd>: fetch remote
<kbd>n</kbd>: add new remote
<kbd>d</kbd>: remove remote
<kbd>e</kbd>: edit remote
</pre>
## Branches Panel (Tags Tab)
<pre>
<kbd>space</kbd>: checkout
<kbd>d</kbd>: delete tag
<kbd>P</kbd>: push tag
<kbd>n</kbd>: create tag
<kbd>g</kbd>: view reset options
</pre>
## Commit Files Panel
<pre>
<kbd>esc</kbd>: go back
<kbd>c</kbd>: checkout file
<kbd>d</kbd>: discard this commit's changes to this file
<kbd>o</kbd>: open file
<kbd>space</kbd>: toggle file included in patch
<kbd>enter</kbd>: enter file to add selected lines to the patch
</pre>
## Commits Panel (Commits Tab)
<pre>
<kbd>s</kbd>: squash down
<kbd>r</kbd>: reword commit
<kbd>R</kbd>: rename commit with editor
<kbd>g</kbd>: reset to this commit
<kbd>f</kbd>: fixup commit
<kbd>F</kbd>: create fixup commit for this commit
<kbd>S</kbd>: squash above commits
<kbd>d</kbd>: delete commit
<kbd>ctrl+j</kbd>: move commit down one
<kbd>ctrl+k</kbd>: move commit up one
<kbd>e</kbd>: edit commit
<kbd>A</kbd>: amend commit with staged changes
<kbd>p</kbd>: pick commit (when mid-rebase)
<kbd>t</kbd>: revert commit
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>v</kbd>: paste commits (cherry-pick)
<kbd>enter</kbd>: view commit's files
<kbd>space</kbd>: checkout commit
<kbd>i</kbd>: select commit to diff with another commit
<kbd>T</kbd>: tag commit
</pre>
## Commits Panel (Reflog Tab)
<pre>
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: view reset options
</pre>
## Files Panel
<pre>
<kbd>c</kbd>: commit changes
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>A</kbd>: amend last commit
<kbd>C</kbd>: commit changes using git editor
<kbd>space</kbd>: toggle staged
<kbd>d</kbd>: view 'discard changes' options
<kbd>e</kbd>: edit file
<kbd>o</kbd>: open file
<kbd>i</kbd>: add to .gitignore
<kbd>r</kbd>: refresh files
<kbd>s</kbd>: stash changes
<kbd>S</kbd>: view stash options
<kbd>a</kbd>: stage/unstage all
<kbd>D</kbd>: view reset options
<kbd>enter</kbd>: stage individual hunks/lines
<kbd>f</kbd>: fetch
<kbd>g</kbd>: view upstream reset options
</pre>
## Main Panel (Merging)
<pre>
<kbd>esc</kbd>: return to files panel
<kbd>space</kbd>: pick hunk
<kbd>b</kbd>: pick both hunks
<kbd>◄</kbd>: select previous conflict
<kbd>►</kbd>: select next conflict
<kbd>▲</kbd>: select top hunk
<kbd>▼</kbd>: select bottom hunk
<kbd>z</kbd>: undo
</pre>
## Main Panel (Normal)
<pre>
<kbd> ̄</kbd>: scroll down (fn+up)
<kbd>¦</kbd>: scroll up (fn+down)
</pre>
## Main Panel (Patch Building)
<pre>
<kbd>esc</kbd>: exit line-by-line mode
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>space</kbd>: stage selection
<kbd>d</kbd>: reset selection
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
</pre>
## Main Panel (Staging)
<pre>
<kbd>esc</kbd>: return to files panel
<kbd>space</kbd>: stage selection
<kbd>d</kbd>: reset selection
<kbd>tab</kbd>: switch to other panel
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>e</kbd>: edit file
<kbd>o</kbd>: open file
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
<kbd>c</kbd>: commit changes
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>C</kbd>: commit changes using git editor
</pre>
## Stash Panel
<pre>
<kbd>space</kbd>: apply
<kbd>g</kbd>: pop
<kbd>d</kbd>: drop
</pre>
## Status Panel
<pre>
<kbd>e</kbd>: edit config file
<kbd>o</kbd>: open config file
<kbd>u</kbd>: check for update
<kbd>enter</kbd>: switch to a recent repo
</pre>

View File

@@ -0,0 +1,202 @@
# Lazygit Keybindings
## Global
<pre>
<kbd>pgup</kbd>: scroll up main panel (fn+up)
<kbd>pgdown</kbd>: scroll down main panel (fn+down)
<kbd>m</kbd>: bekijk merge/rebase opties
<kbd>ctrl+p</kbd>: view custom patch options
<kbd>P</kbd>: push
<kbd>p</kbd>: pull
<kbd>R</kbd>: verversen
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode
<kbd>:</kbd>: voor aangepast commando uit
</pre>
## Branches Panel (Branches Tab)
<pre>
<kbd>space</kbd>: uitchecken
<kbd>o</kbd>: maak een pull-aanvraag
<kbd>c</kbd>: uitchecken bij naam
<kbd>F</kbd>: forceer checkout
<kbd>n</kbd>: nieuwe branch
<kbd>d</kbd>: verwijder branch
<kbd>r</kbd>: rebase branch
<kbd>M</kbd>: merge in met huidige checked out branch
<kbd>i</kbd>: show git-flow options
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>g</kbd>: bekijk reset opties
</pre>
## Branches Panel (Remote Branches (in Remotes tab))
<pre>
<kbd>esc</kbd>: return to remotes list
<kbd>g</kbd>: bekijk reset opties
<kbd>space</kbd>: uitchecken
<kbd>M</kbd>: merge in met huidige checked out branch
<kbd>d</kbd>: verwijder branch
<kbd>r</kbd>: rebase branch
<kbd>u</kbd>: set as upstream of checked-out branch
</pre>
## Branches Panel (Remotes Tab)
<pre>
<kbd>f</kbd>: fetch remote
<kbd>n</kbd>: add new remote
<kbd>d</kbd>: remove remote
<kbd>e</kbd>: edit remote
</pre>
## Branches Panel (Tags Tab)
<pre>
<kbd>space</kbd>: uitchecken
<kbd>d</kbd>: delete tag
<kbd>P</kbd>: push tag
<kbd>n</kbd>: create tag
<kbd>g</kbd>: bekijk reset opties
</pre>
## Commit bestanden Panel
<pre>
<kbd>esc</kbd>: ga terug
<kbd>c</kbd>: bestand uitchecken
<kbd>d</kbd>: uitsluit deze commit zijn veranderingen aan dit bestand
<kbd>o</kbd>: open bestand
<kbd>space</kbd>: toggle file included in patch
<kbd>enter</kbd>: enter file to add selected lines to the patch
</pre>
## Commits Panel (Commits Tab)
<pre>
<kbd>s</kbd>: squash beneden
<kbd>r</kbd>: hernoem commit
<kbd>R</kbd>: rename commit with editor
<kbd>g</kbd>: reset naar deze commit
<kbd>f</kbd>: Fixup commit
<kbd>F</kbd>: creëer fixup commit voor deze commit
<kbd>S</kbd>: squash bovenstaande commits
<kbd>d</kbd>: verwijder commit
<kbd>ctrl+j</kbd>: verplaats commit 1 omlaag
<kbd>ctrl+k</kbd>: verplaats commit 1 omhoog
<kbd>e</kbd>: verander commit
<kbd>A</kbd>: wijzig commit met staged veranderingen
<kbd>p</kbd>: pick commit (when mid-rebase)
<kbd>t</kbd>: commit omgedaan maken
<kbd>c</kbd>: kopiëer commit (cherry-pick)
<kbd>C</kbd>: kopiëer commit reeks (cherry-pick)
<kbd>v</kbd>: plak commits (cherry-pick)
<kbd>enter</kbd>: bekijk gecommite bestanden
<kbd>space</kbd>: checkout commit
<kbd>i</kbd>: select commit to diff with another commit
<kbd>T</kbd>: tag commit
</pre>
## Commits Panel (Reflog Tab)
<pre>
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: bekijk reset opties
</pre>
## Bestanden Panel
<pre>
<kbd>c</kbd>: Commit veranderingen
<kbd>w</kbd>: commit veranderingen zonder pre-commit hook
<kbd>A</kbd>: wijzig laatste commit
<kbd>C</kbd>: commit veranderingen met de git editor
<kbd>space</kbd>: toggle staged
<kbd>d</kbd>: bekijk 'veranderingen ongedaan maken' opties
<kbd>e</kbd>: verander bestand
<kbd>o</kbd>: open bestand
<kbd>i</kbd>: voeg toe aan .gitignore
<kbd>r</kbd>: refresh bestanden
<kbd>s</kbd>: stash-bestanden
<kbd>S</kbd>: view stash options
<kbd>a</kbd>: toggle staged alle
<kbd>D</kbd>: bekijk reset opties
<kbd>enter</kbd>: stage individuele hunks/lijnen
<kbd>f</kbd>: fetch
<kbd>g</kbd>: view upstream reset options
</pre>
## Hoofd Panel (Merging)
<pre>
<kbd>esc</kbd>: ga terug naar het bestanden paneel
<kbd>space</kbd>: pick hunk
<kbd>b</kbd>: pick beide hunks
<kbd>◄</kbd>: selecteer voorgaand conflict
<kbd>►</kbd>: selecteer volgende conflict
<kbd>▲</kbd>: selecteer bovenste hunk
<kbd>▼</kbd>: selecteer onderste hunk
<kbd>z</kbd>: ongedaan maken
</pre>
## Hoofd Panel (Normaal)
<pre>
<kbd> ̄</kbd>: scroll omlaag (fn+up)
<kbd>¦</kbd>: scroll omhoog (fn+down)
</pre>
## Hoofd Panel (Patch Building)
<pre>
<kbd>esc</kbd>: exit line-by-line mode
<kbd>▲</kbd>: selecteer de vorige lijn
<kbd>▼</kbd>: selecteer de volgende lijn
<kbd>◄</kbd>: selecteer de vorige hunk
<kbd>►</kbd>: selecteer de volgende hunk
<kbd>space</kbd>: stage selection
<kbd>d</kbd>: reset selection
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
</pre>
## Hoofd Panel (Stage Lines/Hunks)
<pre>
<kbd>esc</kbd>: ga terug naar het bestanden paneel
<kbd>space</kbd>: stage selection
<kbd>d</kbd>: reset selection
<kbd>tab</kbd>: switch to other panel
<kbd>▲</kbd>: selecteer de vorige lijn
<kbd>▼</kbd>: selecteer de volgende lijn
<kbd>◄</kbd>: selecteer de vorige hunk
<kbd>►</kbd>: selecteer de volgende hunk
<kbd>e</kbd>: verander bestand
<kbd>o</kbd>: open bestand
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
<kbd>c</kbd>: Commit veranderingen
<kbd>w</kbd>: commit veranderingen zonder pre-commit hook
<kbd>C</kbd>: commit veranderingen met de git editor
</pre>
## Stash Panel
<pre>
<kbd>space</kbd>: toepassen
<kbd>g</kbd>: pop
<kbd>d</kbd>: drop
</pre>
## Status Panel
<pre>
<kbd>e</kbd>: verander config file
<kbd>o</kbd>: open config file
<kbd>u</kbd>: check voor updates
<kbd>enter</kbd>: wissel naar een recente repo
</pre>

View File

@@ -0,0 +1,202 @@
# Lazygit Keybindings
## Globalne
<pre>
<kbd>pgup</kbd>: scroll up main panel (fn+up)
<kbd>pgdown</kbd>: scroll down main panel (fn+down)
<kbd>m</kbd>: view merge/rebase options
<kbd>ctrl+p</kbd>: view custom patch options
<kbd>P</kbd>: push
<kbd>p</kbd>: pull
<kbd>R</kbd>: odśwież
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode
<kbd>:</kbd>: execute custom command
</pre>
## Gałęzie Panel (Branches Tab)
<pre>
<kbd>space</kbd>: przełącz
<kbd>o</kbd>: utwórz żądanie wyciągnięcia
<kbd>c</kbd>: przełącz używając nazwy
<kbd>F</kbd>: wymuś przełączenie
<kbd>n</kbd>: nowa gałąź
<kbd>d</kbd>: usuń gałąź
<kbd>r</kbd>: rebase branch
<kbd>M</kbd>: scal do obecnej gałęzi
<kbd>i</kbd>: show git-flow options
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>g</kbd>: view reset options
</pre>
## Gałęzie Panel (Remote Branches (in Remotes tab))
<pre>
<kbd>esc</kbd>: return to remotes list
<kbd>g</kbd>: view reset options
<kbd>space</kbd>: przełącz
<kbd>M</kbd>: scal do obecnej gałęzi
<kbd>d</kbd>: usuń gałąź
<kbd>r</kbd>: rebase branch
<kbd>u</kbd>: set as upstream of checked-out branch
</pre>
## Gałęzie Panel (Remotes Tab)
<pre>
<kbd>f</kbd>: fetch remote
<kbd>n</kbd>: add new remote
<kbd>d</kbd>: remove remote
<kbd>e</kbd>: edit remote
</pre>
## Gałęzie Panel (Tags Tab)
<pre>
<kbd>space</kbd>: przełącz
<kbd>d</kbd>: delete tag
<kbd>P</kbd>: push tag
<kbd>n</kbd>: create tag
<kbd>g</kbd>: view reset options
</pre>
## Commit files Panel
<pre>
<kbd>esc</kbd>: go back
<kbd>c</kbd>: checkout file
<kbd>d</kbd>: discard this commit's changes to this file
<kbd>o</kbd>: otwórz plik
<kbd>space</kbd>: toggle file included in patch
<kbd>enter</kbd>: enter file to add selected lines to the patch
</pre>
## Commity Panel (Commits Tab)
<pre>
<kbd>s</kbd>: ściśnij w dół
<kbd>r</kbd>: przemianuj commit
<kbd>R</kbd>: przemianuj commit w edytorze
<kbd>g</kbd>: zresetuj do tego commita
<kbd>f</kbd>: napraw commit
<kbd>F</kbd>: create fixup commit for this commit
<kbd>S</kbd>: squash above commits
<kbd>d</kbd>: delete commit
<kbd>ctrl+j</kbd>: move commit down one
<kbd>ctrl+k</kbd>: move commit up one
<kbd>e</kbd>: edit commit
<kbd>A</kbd>: amend commit with staged changes
<kbd>p</kbd>: pick commit (when mid-rebase)
<kbd>t</kbd>: revert commit
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>v</kbd>: paste commits (cherry-pick)
<kbd>enter</kbd>: view commit's files
<kbd>space</kbd>: checkout commit
<kbd>i</kbd>: select commit to diff with another commit
<kbd>T</kbd>: tag commit
</pre>
## Commity Panel (Reflog Tab)
<pre>
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: view reset options
</pre>
## Pliki Panel
<pre>
<kbd>c</kbd>: commituj zmiany
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>A</kbd>: zmień ostatnie zatwierdzenie
<kbd>C</kbd>: commituj zmiany używając edytora z gita
<kbd>space</kbd>: przełącz zatwierdzenie
<kbd>d</kbd>: view 'discard changes' options
<kbd>e</kbd>: edytuj plik
<kbd>o</kbd>: otwórz plik
<kbd>i</kbd>: dodaj do .gitignore
<kbd>r</kbd>: odśwież pliki
<kbd>s</kbd>: przechowaj pliki
<kbd>S</kbd>: view stash options
<kbd>a</kbd>: przełącz wszystkie zatwierdzenia
<kbd>D</kbd>: view reset options
<kbd>enter</kbd>: zatwierdź pojedyncze linie
<kbd>f</kbd>: fetch
<kbd>g</kbd>: view upstream reset options
</pre>
## Main Panel (Merging)
<pre>
<kbd>esc</kbd>: wróć do panelu plików
<kbd>space</kbd>: pick hunk
<kbd>b</kbd>: pick both hunks
<kbd>◄</kbd>: select previous conflict
<kbd>►</kbd>: select next conflict
<kbd>▲</kbd>: select top hunk
<kbd>▼</kbd>: select bottom hunk
<kbd>z</kbd>: undo
</pre>
## Main Panel (Normal)
<pre>
<kbd> ̄</kbd>: scroll down (fn+up)
<kbd>¦</kbd>: scroll up (fn+down)
</pre>
## Main Panel (Patch Building)
<pre>
<kbd>esc</kbd>: exit line-by-line mode
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>space</kbd>: stage selection
<kbd>d</kbd>: reset selection
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
</pre>
## Main Panel (Zatwierdzanie)
<pre>
<kbd>esc</kbd>: wróć do panelu plików
<kbd>space</kbd>: stage selection
<kbd>d</kbd>: reset selection
<kbd>tab</kbd>: switch to other panel
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>e</kbd>: edytuj plik
<kbd>o</kbd>: otwórz plik
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
<kbd>c</kbd>: commituj zmiany
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>C</kbd>: commituj zmiany używając edytora z gita
</pre>
## Schowek Panel
<pre>
<kbd>space</kbd>: zastosuj
<kbd>g</kbd>: wyciągnij
<kbd>d</kbd>: porzuć
</pre>
## Status Panel
<pre>
<kbd>e</kbd>: edytuj plik konfiguracyjny
<kbd>o</kbd>: otwórz plik konfiguracyjny
<kbd>u</kbd>: sprawdź aktualizacje
<kbd>enter</kbd>: switch to a recent repo
</pre>

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View File

@@ -1,329 +0,0 @@
package main
import (
// "io"
// "io/ioutil"
// "strings"
"errors"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
)
var (
// ErrNoFiles : when there are no modified files in the repo
ErrNoFiles = errors.New("No changed files")
)
func stagedFiles(files []GitFile) []GitFile {
result := make([]GitFile, 0)
for _, file := range files {
if file.HasStagedChanges {
result = append(result, file)
}
}
return result
}
func stageSelectedFile(g *gocui.Gui) error {
file, err := getSelectedFile(g)
if err != nil {
return err
}
return stageFile(file.Name)
}
func handleFilePress(g *gocui.Gui, v *gocui.View) error {
file, err := getSelectedFile(g)
if err != nil {
if err == ErrNoFiles {
return nil
}
return err
}
if file.HasMergeConflicts {
return handleSwitchToMerge(g, v)
}
if file.HasUnstagedChanges {
stageFile(file.Name)
} else {
unStageFile(file.Name, file.Tracked)
}
if err := refreshFiles(g); err != nil {
return err
}
return handleFileSelect(g, v)
}
func getSelectedFile(g *gocui.Gui) (GitFile, error) {
if len(state.GitFiles) == 0 {
return GitFile{}, ErrNoFiles
}
filesView, err := g.View("files")
if err != nil {
panic(err)
}
lineNumber := getItemPosition(filesView)
return state.GitFiles[lineNumber], nil
}
func handleFileRemove(g *gocui.Gui, v *gocui.View) error {
file, err := getSelectedFile(g)
if err != nil {
if err == ErrNoFiles {
return nil
}
return err
}
var deleteVerb string
if file.Tracked {
deleteVerb = "checkout"
} else {
deleteVerb = "delete"
}
return createConfirmationPanel(g, v, strings.Title(deleteVerb)+" file", "Are you sure you want to "+deleteVerb+" "+file.Name+" (you will lose your changes)?", func(g *gocui.Gui, v *gocui.View) error {
if err := removeFile(file); err != nil {
panic(err)
}
return refreshFiles(g)
}, nil)
}
func handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
file, err := getSelectedFile(g)
if err != nil {
return createErrorPanel(g, err.Error())
}
if file.Tracked {
return createErrorPanel(g, "Cannot ignore tracked files")
}
gitIgnore(file.Name)
return refreshFiles(g)
}
func renderfilesOptions(g *gocui.Gui, gitFile *GitFile) error {
optionsMap := map[string]string{
"← → ↑ ↓": "navigate",
"S": "stash files",
"c": "commit changes",
"o": "open",
"s": "sublime",
"v": "vscode",
"i": "ignore",
"d": "delete",
"space": "toggle staged",
"R": "refresh",
}
if state.HasMergeConflicts {
optionsMap["a"] = "abort merge"
optionsMap["m"] = "resolve merge conflicts"
}
if gitFile == nil {
return renderOptionsMap(g, optionsMap)
}
if gitFile.Tracked {
optionsMap["d"] = "checkout"
}
return renderOptionsMap(g, optionsMap)
}
func handleFileSelect(g *gocui.Gui, v *gocui.View) error {
gitFile, err := getSelectedFile(g)
if err != nil {
if err != ErrNoFiles {
return err
}
renderString(g, "main", "No changed files")
return renderfilesOptions(g, nil)
}
renderfilesOptions(g, &gitFile)
var content string
if gitFile.HasMergeConflicts {
return refreshMergePanel(g)
}
content = getDiff(gitFile)
return renderString(g, "main", content)
}
func handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
if len(stagedFiles(state.GitFiles)) == 0 && !state.HasMergeConflicts {
return createErrorPanel(g, "There are no staged files to commit")
}
createPromptPanel(g, filesView, "Commit message", func(g *gocui.Gui, v *gocui.View) error {
message := trimmedContent(v)
if message == "" {
return createErrorPanel(g, "You cannot commit without a commit message")
}
if output, err := gitCommit(message); err != nil {
return createErrorPanel(g, output)
}
refreshFiles(g)
return refreshCommits(g)
})
return nil
}
func genericFileOpen(g *gocui.Gui, v *gocui.View, open func(string) (string, error)) error {
file, err := getSelectedFile(g)
if err != nil {
if err != ErrNoFiles {
return err
}
return nil
}
if output, err := open(file.Name); err != nil {
return createErrorPanel(g, output)
}
return nil
}
func handleFileOpen(g *gocui.Gui, v *gocui.View) error {
return genericFileOpen(g, v, openFile)
}
func handleSublimeFileOpen(g *gocui.Gui, v *gocui.View) error {
return genericFileOpen(g, v, sublimeOpenFile)
}
func handleVsCodeFileOpen(g *gocui.Gui, v *gocui.View) error {
return genericFileOpen(g, v, vsCodeOpenFile)
}
func handleRefreshFiles(g *gocui.Gui, v *gocui.View) error {
return refreshFiles(g)
}
func refreshStateGitFiles() {
// get files to stage
gitFiles := getGitStatusFiles()
state.GitFiles = mergeGitStatusFiles(state.GitFiles, gitFiles)
updateHasMergeConflictStatus()
}
func updateHasMergeConflictStatus() error {
merging, err := isInMergeState()
if err != nil {
return err
}
state.HasMergeConflicts = merging
return nil
}
func renderGitFile(gitFile GitFile, filesView *gocui.View) {
// potentially inefficient to be instantiating these color
// objects with each render
red := color.New(color.FgRed)
green := color.New(color.FgGreen)
if !gitFile.Tracked && !gitFile.HasStagedChanges {
red.Fprintln(filesView, gitFile.DisplayString)
return
}
green.Fprint(filesView, gitFile.DisplayString[0:1])
red.Fprint(filesView, gitFile.DisplayString[1:3])
if gitFile.HasUnstagedChanges {
red.Fprintln(filesView, gitFile.Name)
} else {
green.Fprintln(filesView, gitFile.Name)
}
}
func catSelectedFile(g *gocui.Gui) (string, error) {
item, err := getSelectedFile(g)
if err != nil {
if err != ErrNoFiles {
return "", err
}
return "", renderString(g, "main", "No file to display")
}
cat, err := catFile(item.Name)
if err != nil {
panic(err)
}
return cat, nil
}
func refreshFiles(g *gocui.Gui) error {
filesView, err := g.View("files")
if err != nil {
return err
}
refreshStateGitFiles()
filesView.Clear()
for _, gitFile := range state.GitFiles {
renderGitFile(gitFile, filesView)
}
correctCursor(filesView)
if filesView == g.CurrentView() {
handleFileSelect(g, filesView)
}
return nil
}
func pullFiles(g *gocui.Gui, v *gocui.View) error {
devLog("pulling...")
createMessagePanel(g, v, "", "Pulling...")
go func() {
if output, err := gitPull(); err != nil {
createErrorPanel(g, output)
} else {
closeConfirmationPrompt(g)
refreshCommits(g)
refreshStatus(g)
devLog("pulled.")
}
refreshFiles(g)
}()
return nil
}
func pushFiles(g *gocui.Gui, v *gocui.View) error {
devLog("pushing...")
createMessagePanel(g, v, "", "Pushing...")
go func() {
if output, err := gitPush(); err != nil {
createErrorPanel(g, output)
} else {
closeConfirmationPrompt(g)
refreshCommits(g)
refreshStatus(g)
devLog("pushed.")
}
}()
return nil
}
func handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error {
mergeView, err := g.View("main")
if err != nil {
return err
}
file, err := getSelectedFile(g)
if err != nil {
if err != ErrNoFiles {
return err
}
return nil
}
if !file.HasMergeConflicts {
return createErrorPanel(g, "This file has no merge conflicts")
}
switchFocus(g, v, mergeView)
return refreshMergePanel(g)
}
func handleAbortMerge(g *gocui.Gui, v *gocui.View) error {
output, err := gitAbortMerge()
if err != nil {
return createErrorPanel(g, output)
}
createMessagePanel(g, v, "", "Merge aborted")
refreshStatus(g)
return refreshFiles(g)
}

View File

@@ -1,586 +0,0 @@
package main
import (
// "log"
"errors"
"fmt"
"os"
"os/exec"
"regexp"
"runtime"
"strings"
"time"
"github.com/fatih/color"
)
var (
// ErrNoCheckedOutBranch : When we have no checked out branch
ErrNoCheckedOutBranch = errors.New("No currently checked out branch")
)
// GitFile : A staged/unstaged file
// TODO: decide whether to give all of these the Git prefix
type GitFile struct {
Name string
HasStagedChanges bool
HasUnstagedChanges bool
Tracked bool
Deleted bool
HasMergeConflicts bool
DisplayString string
}
// Branch : A git branch
type Branch struct {
Name string
Type string
BaseBranch string
DisplayString string
}
// Commit : A git commit
type Commit struct {
Sha string
Name string
Pushed bool
DisplayString string
}
// StashEntry : A git stash entry
type StashEntry struct {
Index int
Name string
DisplayString string
}
// Map (from https://gobyexample.com/collection-functions)
func Map(vs []string, f func(string) string) []string {
vsm := make([]string, len(vs))
for i, v := range vs {
vsm[i] = f(v)
}
return vsm
}
func includesString(list []string, a string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
// not sure how to genericise this because []interface{} doesn't accept e.g.
// []int arguments
func includesInt(list []int, a int) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
func mergeGitStatusFiles(oldGitFiles, newGitFiles []GitFile) []GitFile {
if len(oldGitFiles) == 0 {
return newGitFiles
}
appendedIndexes := make([]int, 0)
// retain position of files we already could see
result := make([]GitFile, 0)
for _, oldGitFile := range oldGitFiles {
for newIndex, newGitFile := range newGitFiles {
if oldGitFile.Name == newGitFile.Name {
result = append(result, newGitFile)
appendedIndexes = append(appendedIndexes, newIndex)
break
}
}
}
// append any new files to the end
for index, newGitFile := range newGitFiles {
if !includesInt(appendedIndexes, index) {
result = append(result, newGitFile)
}
}
return result
}
func platformShell() (string, string) {
if runtime.GOOS == "windows" {
return "cmd", "/c"
}
return "bash", "-c"
}
func runDirectCommand(command string) (string, error) {
timeStart := time.Now()
commandLog(command)
shell, shellArg := platformShell()
cmdOut, err := exec.
Command(shell, shellArg, command).
CombinedOutput()
devLog("run direct command time for command: ", command, time.Now().Sub(timeStart))
return sanitisedCommandOutput(cmdOut, err)
}
func branchStringParts(branchString string) (string, string) {
// expect string to be something like '4w master`
splitBranchName := strings.Split(branchString, "\t")
// if we have no \t then we have no recency, so just output that as blank
if len(splitBranchName) == 1 {
return "", branchString
}
return splitBranchName[0], splitBranchName[1]
}
// branchPropertiesFromName : returns branch type, base, and color
func branchPropertiesFromName(name string) (string, string, color.Attribute) {
if strings.Contains(name, "feature/") {
return "feature", "develop", color.FgGreen
} else if strings.Contains(name, "bugfix/") {
return "bugfix", "develop", color.FgYellow
} else if strings.Contains(name, "hotfix/") {
return "hotfix", "master", color.FgRed
}
return "other", name, color.FgWhite
}
func coloredString(str string, colour *color.Color) string {
return colour.SprintFunc()(fmt.Sprint(str))
}
func withPadding(str string, padding int) string {
if padding-len(str) < 0 {
return str
}
return str + strings.Repeat(" ", padding-len(str))
}
// TODO: DRY up this function and getGitBranches
func getGitStashEntries() []StashEntry {
stashEntries := make([]StashEntry, 0)
rawString, _ := runDirectCommand("git stash list --pretty='%gs'")
for i, line := range splitLines(rawString) {
stashEntries = append(stashEntries, stashEntryFromLine(line, i))
}
return stashEntries
}
func stashEntryFromLine(line string, index int) StashEntry {
return StashEntry{
Name: line,
Index: index,
DisplayString: line,
}
}
func getStashEntryDiff(index int) (string, error) {
return runCommand("git stash show -p --color stash@{" + fmt.Sprint(index) + "}")
}
func includes(array []string, str string) bool {
for _, arrayStr := range array {
if arrayStr == str {
return true
}
}
return false
}
func getGitStatusFiles() []GitFile {
statusOutput, _ := getGitStatus()
statusStrings := splitLines(statusOutput)
gitFiles := make([]GitFile, 0)
for _, statusString := range statusStrings {
change := statusString[0:2]
stagedChange := change[0:1]
unstagedChange := statusString[1:2]
filename := statusString[3:]
tracked := !includes([]string{"??", "A "}, change)
gitFile := GitFile{
Name: filename,
DisplayString: statusString,
HasStagedChanges: !includes([]string{" ", "U", "?"}, stagedChange),
HasUnstagedChanges: unstagedChange != " ",
Tracked: tracked,
Deleted: unstagedChange == "D" || stagedChange == "D",
HasMergeConflicts: change == "UU",
}
devLog("tracked", gitFile.Tracked)
devLog("hasUnstagedChanges", gitFile.HasUnstagedChanges)
devLog("HasStagedChanges", gitFile.HasStagedChanges)
devLog("DisplayString", gitFile.DisplayString)
gitFiles = append(gitFiles, gitFile)
}
devLog(gitFiles)
return gitFiles
}
func gitStashDo(index int, method string) (string, error) {
return runCommand("git stash " + method + " stash@{" + fmt.Sprint(index) + "}")
}
func gitStashSave(message string) (string, error) {
output, err := runCommand("git stash save \"" + message + "\"")
if err != nil {
return output, err
}
// if there are no local changes to save, the exit code is 0, but we want
// to raise an error
if output == "No local changes to save\n" {
return output, errors.New(output)
}
return output, nil
}
func gitCheckout(branch string, force bool) (string, error) {
forceArg := ""
if force {
forceArg = "--force "
}
return runCommand("git checkout " + forceArg + branch)
}
func sanitisedCommandOutput(output []byte, err error) (string, error) {
outputString := string(output)
if outputString == "" && err != nil {
return err.Error(), err
}
return outputString, err
}
func runCommand(command string) (string, error) {
commandStartTime := time.Now()
commandLog(command)
splitCmd := strings.Split(command, " ")
cmdOut, err := exec.Command(splitCmd[0], splitCmd[1:]...).CombinedOutput()
devLog("run command time: ", time.Now().Sub(commandStartTime))
return sanitisedCommandOutput(cmdOut, err)
}
func openFile(filename string) (string, error) {
return runCommand("open " + filename)
}
func vsCodeOpenFile(filename string) (string, error) {
return runCommand("code -r " + filename)
}
func sublimeOpenFile(filename string) (string, error) {
return runCommand("subl " + filename)
}
func getBranchGraph(branch string, baseBranch string) (string, error) {
return runCommand("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium -100 " + branch)
// Leaving this guy commented out in case there's backlash from the design
// change and I want to make this configurable
// return runCommand("git log -p -30 --color --no-merges " + branch)
}
func verifyInGitRepo() {
if output, err := runCommand("git status"); err != nil {
fmt.Println(output)
os.Exit(1)
}
}
func getCommits() []Commit {
pushables := gitCommitsToPush()
log := getLog()
commits := make([]Commit, 0)
// now we can split it up and turn it into commits
lines := splitLines(log)
for _, line := range lines {
splitLine := strings.Split(line, " ")
sha := splitLine[0]
pushed := includesString(pushables, sha)
commits = append(commits, Commit{
Sha: sha,
Name: strings.Join(splitLine[1:], " "),
Pushed: pushed,
DisplayString: strings.Join(splitLine, " "),
})
}
return commits
}
func getLog() string {
// currently limiting to 30 for performance reasons
// TODO: add lazyloading when you scroll down
result, err := runDirectCommand("git log --oneline -30")
if err != nil {
// assume if there is an error there are no commits yet for this branch
return ""
}
return result
}
func gitIgnore(filename string) {
if _, err := runDirectCommand("echo '" + filename + "' >> .gitignore"); err != nil {
panic(err)
}
}
func gitShow(sha string) string {
result, err := runDirectCommand("git show --color " + sha)
if err != nil {
panic(err)
}
return result
}
func getDiff(file GitFile) string {
cachedArg := ""
if file.HasStagedChanges && !file.HasUnstagedChanges {
cachedArg = "--cached "
}
deletedArg := ""
if file.Deleted {
deletedArg = "-- "
}
trackedArg := ""
if !file.Tracked && !file.HasStagedChanges {
trackedArg = "--no-index /dev/null "
}
command := "git diff --color " + cachedArg + deletedArg + trackedArg + file.Name
// for now we assume an error means the file was deleted
s, _ := runCommand(command)
return s
}
func catFile(file string) (string, error) {
return runDirectCommand("cat " + file)
}
func stageFile(file string) error {
_, err := runCommand("git add " + file)
return err
}
func unStageFile(file string, tracked bool) error {
var command string
if tracked {
command = "git reset HEAD "
} else {
command = "git rm --cached "
}
devLog(command)
_, err := runCommand(command + file)
return err
}
func getGitStatus() (string, error) {
return runCommand("git status --untracked-files=all --short")
}
func isInMergeState() (bool, error) {
output, err := runCommand("git status --untracked-files=all")
if err != nil {
return false, err
}
return strings.Contains(output, "conclude merge") || strings.Contains(output, "unmerged paths"), nil
}
func removeFile(file GitFile) error {
// if the file isn't tracked, we assume you want to delete it
if !file.Tracked {
_, err := runCommand("rm -rf ./" + file.Name)
return err
}
// if the file is tracked, we assume you want to just check it out
_, err := runCommand("git checkout " + file.Name)
return err
}
func gitCommit(message string) (string, error) {
return runDirectCommand("git commit -m \"" + message + "\"")
}
func gitPull() (string, error) {
return runDirectCommand("git pull --no-edit")
}
func gitPush() (string, error) {
branchName := gitCurrentBranchName()
if branchName == "" {
return "", ErrNoCheckedOutBranch
}
return runDirectCommand("git push -u origin " + branchName)
}
func gitSquashPreviousTwoCommits(message string) (string, error) {
return runDirectCommand("git reset --soft HEAD^ && git commit --amend -m \"" + message + "\"")
}
func gitRenameCommit(message string) (string, error) {
return runDirectCommand("git commit --allow-empty --amend -m \"" + message + "\"")
}
func gitFetch() (string, error) {
return runDirectCommand("git fetch")
}
func gitResetToCommit(sha string) (string, error) {
return runDirectCommand("git reset " + sha)
}
func gitNewBranch(name string) (string, error) {
return runDirectCommand("git checkout -b " + name)
}
func gitListStash() (string, error) {
return runDirectCommand("git stash list")
}
func gitMerge(branchName string) (string, error) {
return runDirectCommand("git merge --no-edit " + branchName)
}
func gitAbortMerge() (string, error) {
return runDirectCommand("git merge --abort")
}
func gitUpstreamDifferenceCount() (string, string) {
pushableCount, err := runDirectCommand("git rev-list @{u}..head --count")
if err != nil {
return "?", "?"
}
pullableCount, err := runDirectCommand("git rev-list head..@{u} --count")
if err != nil {
return "?", "?"
}
return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount)
}
func gitCommitsToPush() []string {
pushables, err := runDirectCommand("git rev-list @{u}..head --abbrev-commit")
if err != nil {
return make([]string, 0)
}
return splitLines(pushables)
}
func gitCurrentBranchName() string {
branchName, err := runDirectCommand("git symbolic-ref --short HEAD")
// if there is an error, assume there are no branches yet
if err != nil {
return ""
}
return strings.TrimSpace(branchName)
}
// A line will have the form '10 days ago master' so we need to strip out the
// useful information from that into timeNumber, timeUnit, and branchName
func branchInfoFromLine(line string) (string, string, string) {
r := regexp.MustCompile("\\|.*\\s")
line = r.ReplaceAllString(line, " ")
words := strings.Split(line, " ")
return words[0], words[1], words[3]
}
func abbreviatedTimeUnit(timeUnit string) string {
r := regexp.MustCompile("s$")
timeUnit = r.ReplaceAllString(timeUnit, "")
timeUnitMap := map[string]string{
"hour": "h",
"minute": "m",
"second": "s",
"week": "w",
"year": "y",
"day": "d",
"month": "m",
}
return timeUnitMap[timeUnit]
}
func getBranches() []Branch {
branches := make([]Branch, 0)
rawString, err := runDirectCommand("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD")
if err != nil {
return branches
}
branchLines := splitLines(rawString)
for i, line := range branchLines {
timeNumber, timeUnit, branchName := branchInfoFromLine(line)
timeUnit = abbreviatedTimeUnit(timeUnit)
if branchAlreadyStored(branchName, branches) {
continue
}
branch := constructBranch(timeNumber+timeUnit, branchName, i)
branches = append(branches, branch)
}
return branches
}
func constructBranch(prefix, name string, index int) Branch {
branchType, branchBase, colourAttr := branchPropertiesFromName(name)
if index == 0 {
prefix = " *"
}
colour := color.New(colourAttr)
displayString := withPadding(prefix, 4) + coloredString(name, colour)
return Branch{
Name: name,
Type: branchType,
BaseBranch: branchBase,
DisplayString: displayString,
}
}
func getGitBranches() []Branch {
// check if there are any branches
branchCheck, _ := runCommand("git branch")
if branchCheck == "" {
return []Branch{constructBranch("", gitCurrentBranchName(), 0)}
}
branches := getBranches()
if len(branches) == 0 {
branches = append(branches, constructBranch("", gitCurrentBranchName(), 0))
}
branches = getAndMergeFetchedBranches(branches)
return branches
}
func branchAlreadyStored(branchName string, branches []Branch) bool {
for _, existingBranch := range branches {
if existingBranch.Name == branchName {
return true
}
}
return false
}
// here branches contains all the branches that we've checked out, along with
// the recency. In this function we append the branches that are in our heads
// directory i.e. things we've fetched but haven't necessarily checked out.
// Worth mentioning this has nothing to do with the 'git merge' operation
func getAndMergeFetchedBranches(branches []Branch) []Branch {
rawString, err := runDirectCommand("git branch --sort=-committerdate --no-color")
if err != nil {
return branches
}
branchLines := splitLines(rawString)
for _, line := range branchLines {
line = strings.Replace(line, "* ", "", -1)
line = strings.TrimSpace(line)
if branchAlreadyStored(line, branches) {
continue
}
branches = append(branches, constructBranch("", line, len(branches)))
}
return branches
}

43
go.mod Normal file
View File

@@ -0,0 +1,43 @@
module github.com/jesseduffield/lazygit
go 1.13
require (
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
github.com/fatih/color v1.7.0
github.com/fsnotify/fsnotify v1.4.7
github.com/go-errors/errors v1.0.1
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
github.com/golang/protobuf v1.3.2 // indirect
github.com/google/go-cmp v0.3.1 // indirect
github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/gocui v0.3.1-0.20200301081700-d6e485450113
github.com/jesseduffield/pty v1.2.1
github.com/jesseduffield/termbox-go v0.0.0-20200130214842-1d31d1faa3c9 // indirect
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/mattn/go-colorable v0.1.4 // indirect
github.com/mattn/go-isatty v0.0.11 // indirect
github.com/mattn/go-runewidth v0.0.8
github.com/mgutz/str v1.2.0
github.com/nicksnyder/go-i18n/v2 v2.0.3
github.com/onsi/ginkgo v1.10.3 // indirect
github.com/onsi/gomega v1.7.1 // indirect
github.com/pelletier/go-toml v1.6.0 // indirect
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0
github.com/sirupsen/logrus v1.4.2
github.com/spf13/afero v1.2.2 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.6.1
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
github.com/stretchr/testify v1.4.0
github.com/tcnksm/go-gitconfig v0.1.2
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 // indirect
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 // indirect
golang.org/x/text v0.3.2
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/src-d/go-git.v4 v4.13.1
gopkg.in/yaml.v2 v2.2.7
)

285
go.sum Normal file
View File

@@ -0,0 +1,285 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 h1:zN2lZNZRflqFyxVaTIU61KNKQ9C0055u9CAfpmqUvo4=
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3/go.mod h1:nPpo7qLxd6XL3hWJG/O60sR8ZKfMCiIoNap5GvD12KU=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/integrii/flaggy v1.4.0 h1:A1x7SYx4jqu5NSrY14z8Z+0UyX2S5ygfJJrfolWR3zM=
github.com/integrii/flaggy v1.4.0/go.mod h1:tnTxHeTJbah0gQ6/K0RW0J7fMUBk9MCF5blhm43LNpI=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jesseduffield/gocui v0.3.1-0.20200224201655-5024a02682ed h1:glGs+mzPZOl1iHiUsBW3918WeFwqsbQQ/jtLkkQXDi4=
github.com/jesseduffield/gocui v0.3.1-0.20200224201655-5024a02682ed/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20200301081700-d6e485450113 h1:jHZRVJUWsU8HaQ0crocz0i0BkpOqFLDJEO/AtBp+Ecs=
github.com/jesseduffield/gocui v0.3.1-0.20200301081700-d6e485450113/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/pty v1.2.1 h1:7xYBiwNH0PpWqC8JmvrPq1a/ksNqyCavzWu9pbBGYWI=
github.com/jesseduffield/pty v1.2.1/go.mod h1:7jlS40+UhOqkZJDIG1B/H21xnuET/+fvbbnHCa8wSIo=
github.com/jesseduffield/termbox-go v0.0.0-20200130214842-1d31d1faa3c9 h1:iBBk1lhFwjwJw//J2m1yyz9S368GeXQTpMVACTyQMh0=
github.com/jesseduffield/termbox-go v0.0.0-20200130214842-1d31d1faa3c9/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
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-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0=
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
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=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nicksnyder/go-i18n/v2 v2.0.3 h1:ks/JkQiOEhhuF6jpNvx+Wih1NIiXzUnZeZVnJuI8R8M=
github.com/nicksnyder/go-i18n/v2 v2.0.3/go.mod h1:oDab7q8XCYMRlcrBnaY/7B1eOectbvj6B1UPBT+p5jo=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY=
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 h1:Xuk8ma/ibJ1fOy4Ee11vHhUFHQNpHhrBneOCNHVXS5w=
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0/go.mod h1:7AwjWCpdPhkSmNAgUv5C7EJ4AbmjEB3r047r3DXWu3Y=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk=
github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4=
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-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-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 h1:gSbV7h1NRL2G1xTg/owz62CST1oJBmxy4QpMMregXVQ=
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg=
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE=
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

297
gui.go
View File

@@ -1,297 +0,0 @@
package main
import (
// "io"
// "io/ioutil"
"log"
"runtime"
"strings"
"time"
// "strings"
"github.com/golang-collections/collections/stack"
"github.com/jesseduffield/gocui"
)
// OverlappingEdges determines if panel edges overlap
var OverlappingEdges = false
type stateType struct {
GitFiles []GitFile
Branches []Branch
Commits []Commit
StashEntries []StashEntry
PreviousView string
HasMergeConflicts bool
ConflictIndex int
ConflictTop bool
Conflicts []conflict
EditHistory *stack.Stack
}
type conflict struct {
start int
middle int
end int
}
var state = stateType{
GitFiles: make([]GitFile, 0),
PreviousView: "files",
Commits: make([]Commit, 0),
StashEntries: make([]StashEntry, 0),
ConflictIndex: 0,
ConflictTop: true,
Conflicts: make([]conflict, 0),
EditHistory: stack.New(),
}
func scrollUpMain(g *gocui.Gui, v *gocui.View) error {
mainView, _ := g.View("main")
ox, oy := mainView.Origin()
if oy >= 1 {
return mainView.SetOrigin(ox, oy-1)
}
return nil
}
func scrollDownMain(g *gocui.Gui, v *gocui.View) error {
mainView, _ := g.View("main")
ox, oy := mainView.Origin()
if oy < len(mainView.BufferLines()) {
return mainView.SetOrigin(ox, oy+1)
}
return nil
}
func handleRefresh(g *gocui.Gui, v *gocui.View) error {
return refreshSidePanels(g)
}
// Binding - a keybinding mapping a key and modifier to a handler. The keypress
// is only handled if the given view has focus, or handled globally if the view
// is ""
type Binding struct {
ViewName string
Handler func(*gocui.Gui, *gocui.View) error
Key interface{} // FIXME: find out how to get `gocui.Key | rune`
Modifier gocui.Modifier
}
func keybindings(g *gocui.Gui) error {
bindings := []Binding{
Binding{ViewName: "", Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: previousView},
Binding{ViewName: "", Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: nextView},
Binding{ViewName: "", Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: nextView},
Binding{ViewName: "", Key: 'q', Modifier: gocui.ModNone, Handler: quit},
Binding{ViewName: "", Key: gocui.KeyCtrlC, Modifier: gocui.ModNone, Handler: quit},
Binding{ViewName: "", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: cursorDown},
Binding{ViewName: "", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: cursorUp},
Binding{ViewName: "", Key: gocui.KeyPgup, Modifier: gocui.ModNone, Handler: scrollUpMain},
Binding{ViewName: "", Key: gocui.KeyPgdn, Modifier: gocui.ModNone, Handler: scrollDownMain},
Binding{ViewName: "", Key: 'P', Modifier: gocui.ModNone, Handler: pushFiles},
Binding{ViewName: "", Key: 'p', Modifier: gocui.ModNone, Handler: pullFiles},
Binding{ViewName: "", Key: 'R', Modifier: gocui.ModNone, Handler: handleRefresh},
Binding{ViewName: "files", Key: 'c', Modifier: gocui.ModNone, Handler: handleCommitPress},
Binding{ViewName: "files", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleFilePress},
Binding{ViewName: "files", Key: 'd', Modifier: gocui.ModNone, Handler: handleFileRemove},
Binding{ViewName: "files", Key: 'm', Modifier: gocui.ModNone, Handler: handleSwitchToMerge},
Binding{ViewName: "files", Key: 'o', Modifier: gocui.ModNone, Handler: handleFileOpen},
Binding{ViewName: "files", Key: 's', Modifier: gocui.ModNone, Handler: handleSublimeFileOpen},
Binding{ViewName: "files", Key: 'v', Modifier: gocui.ModNone, Handler: handleVsCodeFileOpen},
Binding{ViewName: "files", Key: 'i', Modifier: gocui.ModNone, Handler: handleIgnoreFile},
Binding{ViewName: "files", Key: 'r', Modifier: gocui.ModNone, Handler: handleRefreshFiles},
Binding{ViewName: "files", Key: 'S', Modifier: gocui.ModNone, Handler: handleStashSave},
Binding{ViewName: "files", Key: 'a', Modifier: gocui.ModNone, Handler: handleAbortMerge},
Binding{ViewName: "main", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: handleSelectTop},
Binding{ViewName: "main", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: handleSelectBottom},
Binding{ViewName: "main", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: handleEscapeMerge},
Binding{ViewName: "main", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handlePickHunk},
Binding{ViewName: "main", Key: 'b', Modifier: gocui.ModNone, Handler: handlePickBothHunks},
Binding{ViewName: "main", Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: handleSelectPrevConflict},
Binding{ViewName: "main", Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: handleSelectNextConflict},
Binding{ViewName: "main", Key: 'z', Modifier: gocui.ModNone, Handler: handlePopFileSnapshot},
Binding{ViewName: "branches", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleBranchPress},
Binding{ViewName: "branches", Key: 'c', Modifier: gocui.ModNone, Handler: handleCheckoutByName},
Binding{ViewName: "branches", Key: 'F', Modifier: gocui.ModNone, Handler: handleForceCheckout},
Binding{ViewName: "branches", Key: 'n', Modifier: gocui.ModNone, Handler: handleNewBranch},
Binding{ViewName: "branches", Key: 'm', Modifier: gocui.ModNone, Handler: handleMerge},
Binding{ViewName: "commits", Key: 's', Modifier: gocui.ModNone, Handler: handleCommitSquashDown},
Binding{ViewName: "commits", Key: 'r', Modifier: gocui.ModNone, Handler: handleRenameCommit},
Binding{ViewName: "commits", Key: 'g', Modifier: gocui.ModNone, Handler: handleResetToCommit},
Binding{ViewName: "stash", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleStashApply},
Binding{ViewName: "stash", Key: 'k', Modifier: gocui.ModNone, Handler: handleStashPop},
Binding{ViewName: "stash", Key: 'd', Modifier: gocui.ModNone, Handler: handleStashDrop},
}
for _, binding := range bindings {
if err := g.SetKeybinding(binding.ViewName, binding.Key, binding.Modifier, binding.Handler); err != nil {
return err
}
}
return nil
}
func layout(g *gocui.Gui) error {
g.Highlight = true
g.SelFgColor = gocui.ColorWhite | gocui.AttrBold
if runtime.GOOS != "windows" {
g.FgColor = gocui.ColorBlack
}
width, height := g.Size()
leftSideWidth := width / 3
statusFilesBoundary := 2
filesBranchesBoundary := 2 * height / 5 // height - 20
commitsBranchesBoundary := 3 * height / 5 // height - 10
commitsStashBoundary := height - 5 // height - 5
minimumHeight := 16
panelSpacing := 1
if OverlappingEdges {
panelSpacing = 0
}
if height < minimumHeight {
v, err := g.SetView("limit", 0, 0, width-1, height-1, 0)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = "Not enough space to render panels"
v.Wrap = true
}
return nil
}
g.DeleteView("limit")
optionsTop := height - 2
// hiding options if there's not enough space
if height < 30 {
optionsTop = height - 1
}
v, err := g.SetView("main", leftSideWidth+panelSpacing, 0, width-1, optionsTop, gocui.LEFT)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = "Diff"
v.Wrap = true
v.FgColor = gocui.ColorWhite
}
if v, err := g.SetView("status", 0, 0, leftSideWidth, statusFilesBoundary, gocui.BOTTOM|gocui.RIGHT); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = "Status"
v.FgColor = gocui.ColorWhite
}
filesView, err := g.SetView("files", 0, statusFilesBoundary+panelSpacing, leftSideWidth, filesBranchesBoundary, gocui.TOP|gocui.BOTTOM)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
filesView.Highlight = true
filesView.Title = "Files"
v.FgColor = gocui.ColorWhite
}
if v, err := g.SetView("branches", 0, filesBranchesBoundary+panelSpacing, leftSideWidth, commitsBranchesBoundary, gocui.TOP|gocui.BOTTOM); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = "Branches"
v.FgColor = gocui.ColorWhite
}
if v, err := g.SetView("commits", 0, commitsBranchesBoundary+panelSpacing, leftSideWidth, commitsStashBoundary, gocui.TOP|gocui.BOTTOM); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = "Commits"
v.FgColor = gocui.ColorWhite
}
if v, err := g.SetView("stash", 0, commitsStashBoundary+panelSpacing, leftSideWidth, optionsTop, gocui.TOP|gocui.RIGHT); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = "Stash"
v.FgColor = gocui.ColorWhite
}
if v, err := g.SetView("options", -1, optionsTop, width, optionsTop+2, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.BgColor = gocui.ColorDefault
v.FgColor = gocui.ColorBlue
v.Frame = false
v.Title = "Options"
// these are only called once
handleFileSelect(g, filesView)
refreshFiles(g)
refreshBranches(g)
refreshCommits(g)
refreshStashEntries(g)
nextView(g, nil)
}
return nil
}
func fetch(g *gocui.Gui) {
gitFetch()
refreshStatus(g)
}
func updateLoader(g *gocui.Gui) {
if confirmationView, _ := g.View("confirmation"); confirmationView != nil {
content := trimmedContent(confirmationView)
if strings.Contains(content, "...") {
staticContent := strings.Split(content, "...")[0] + "..."
renderString(g, "confirmation", staticContent+" "+loader())
}
}
}
func run() {
g, err := gocui.NewGui(gocui.OutputNormal, OverlappingEdges)
if err != nil {
log.Panicln(err)
}
defer g.Close()
// periodically fetching to check for upstream differences
go func() {
for range time.Tick(time.Second * 60) {
fetch(g)
}
}()
go func() {
for range time.Tick(time.Millisecond * 10) {
updateLoader(g)
}
}()
g.SetManagerFunc(layout)
if err := keybindings(g); err != nil {
log.Panicln(err)
}
if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
log.Panicln(err)
}
}
func quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}

123
main.go
View File

@@ -1,69 +1,86 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"os/user"
"time"
"path/filepath"
"runtime"
"github.com/fatih/color"
"github.com/go-errors/errors"
"github.com/integrii/flaggy"
"github.com/jesseduffield/lazygit/pkg/app"
"github.com/jesseduffield/lazygit/pkg/config"
)
var (
startTime time.Time
debugging bool
commit string
version = "unversioned"
date string
buildSource = "unknown"
)
func homeDirectory() string {
usr, err := user.Current()
if err != nil {
log.Fatal(err)
}
return usr.HomeDir
}
func devLog(objects ...interface{}) {
localLog(color.FgWhite, homeDirectory()+"/go/src/github.com/jesseduffield/lazygit/development.log", objects...)
}
func colorLog(colour color.Attribute, objects ...interface{}) {
localLog(colour, homeDirectory()+"/go/src/github.com/jesseduffield/lazygit/development.log", objects...)
}
func commandLog(objects ...interface{}) {
localLog(color.FgWhite, homeDirectory()+"/go/src/github.com/jesseduffield/lazygit/commands.log", objects...)
}
func localLog(colour color.Attribute, path string, objects ...interface{}) {
if !debugging {
return
}
f, _ := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0644)
defer f.Close()
for _, object := range objects {
colorFunction := color.New(colour).SprintFunc()
f.WriteString(colorFunction(fmt.Sprint(object)) + "\n")
}
}
func navigateToRepoRootDirectory() {
_, err := os.Stat(".git")
for os.IsNotExist(err) {
devLog("going up a directory to find the root")
os.Chdir("..")
_, err = os.Stat(".git")
}
func projectPath(path string) string {
gopath := os.Getenv("GOPATH")
return filepath.FromSlash(gopath + "/src/github.com/jesseduffield/lazygit/" + path)
}
func main() {
debuggingPointer := flag.Bool("debug", false, "a boolean")
flag.Parse()
debugging = *debuggingPointer
devLog("\n\n\n\n\n\n\n\n\n\n")
startTime = time.Now()
verifyInGitRepo()
navigateToRepoRootDirectory()
run()
flaggy.DefaultParser.ShowVersionWithVersionFlag = false
repoPath := "."
flaggy.String(&repoPath, "p", "path", "Path of git repo")
dump := ""
flaggy.AddPositionalValue(&dump, "gitargs", 1, false, "Todo file")
flaggy.DefaultParser.PositionalFlags[0].Hidden = true
versionFlag := false
flaggy.Bool(&versionFlag, "v", "version", "Print the current version")
debuggingFlag := false
flaggy.Bool(&debuggingFlag, "d", "debug", "Run in debug mode with logging")
configFlag := false
flaggy.Bool(&configFlag, "c", "config", "Print the current default config")
flaggy.Parse()
if versionFlag {
fmt.Printf("commit=%s, build date=%s, build source=%s, version=%s, os=%s, arch=%s\n", commit, date, buildSource, version, runtime.GOOS, runtime.GOARCH)
os.Exit(0)
}
if configFlag {
fmt.Printf("%s\n", config.GetDefaultConfig())
os.Exit(0)
}
if repoPath != "." {
if err := os.Chdir(repoPath); err != nil {
log.Fatal(err.Error())
}
}
appConfig, err := config.NewAppConfig("lazygit", version, commit, date, buildSource, debuggingFlag)
if err != nil {
log.Fatal(err.Error())
}
app, err := app.NewApp(appConfig)
if err == nil {
err = app.Run()
}
if err != nil {
if errorMessage, known := app.KnownError(err); known {
log.Fatal(errorMessage)
}
newErr := errors.Wrap(err, 0)
stackTrace := newErr.ErrorStack()
app.Log.Error(stackTrace)
log.Fatal(fmt.Sprintf("%s\n\n%s", app.Tr.SLocalize("ErrorOccurred"), stackTrace))
}
}

View File

@@ -1,264 +0,0 @@
// though this panel is called the merge panel, it's really going to use the main panel. This may change in the future
package main
import (
"bufio"
"bytes"
"io/ioutil"
"math"
"os"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
)
func findConflicts(content string) ([]conflict, error) {
conflicts := make([]conflict, 0)
var newConflict conflict
for i, line := range splitLines(content) {
if line == "<<<<<<< HEAD" || line == "<<<<<<< MERGE_HEAD" || line == "<<<<<<< Updated upstream" {
newConflict = conflict{start: i}
} else if line == "=======" {
newConflict.middle = i
} else if strings.HasPrefix(line, ">>>>>>> ") {
newConflict.end = i
conflicts = append(conflicts, newConflict)
}
}
return conflicts, nil
}
func shiftConflict(conflicts []conflict) (conflict, []conflict) {
return conflicts[0], conflicts[1:]
}
func shouldHighlightLine(index int, conflict conflict, top bool) bool {
return (index >= conflict.start && index <= conflict.middle && top) || (index >= conflict.middle && index <= conflict.end && !top)
}
func coloredConflictFile(content string, conflicts []conflict, conflictIndex int, conflictTop, hasFocus bool) (string, error) {
if len(conflicts) == 0 {
return content, nil
}
conflict, remainingConflicts := shiftConflict(conflicts)
var outputBuffer bytes.Buffer
for i, line := range splitLines(content) {
colourAttr := color.FgWhite
if i == conflict.start || i == conflict.middle || i == conflict.end {
colourAttr = color.FgRed
}
colour := color.New(colourAttr)
if hasFocus && conflictIndex < len(conflicts) && conflicts[conflictIndex] == conflict && shouldHighlightLine(i, conflict, conflictTop) {
colour.Add(color.Bold)
}
if i == conflict.end && len(remainingConflicts) > 0 {
conflict, remainingConflicts = shiftConflict(remainingConflicts)
}
outputBuffer.WriteString(coloredString(line, colour) + "\n")
}
return outputBuffer.String(), nil
}
func handleSelectTop(g *gocui.Gui, v *gocui.View) error {
state.ConflictTop = true
return refreshMergePanel(g)
}
func handleSelectBottom(g *gocui.Gui, v *gocui.View) error {
state.ConflictTop = false
return refreshMergePanel(g)
}
func handleSelectNextConflict(g *gocui.Gui, v *gocui.View) error {
if state.ConflictIndex >= len(state.Conflicts)-1 {
return nil
}
state.ConflictIndex++
return refreshMergePanel(g)
}
func handleSelectPrevConflict(g *gocui.Gui, v *gocui.View) error {
if state.ConflictIndex <= 0 {
return nil
}
state.ConflictIndex--
return refreshMergePanel(g)
}
func isIndexToDelete(i int, conflict conflict, pick string) bool {
return i == conflict.middle ||
i == conflict.start ||
i == conflict.end ||
pick != "both" &&
(pick == "bottom" && i > conflict.start && i < conflict.middle) ||
(pick == "top" && i > conflict.middle && i < conflict.end)
}
func resolveConflict(g *gocui.Gui, conflict conflict, pick string) error {
gitFile, err := getSelectedFile(g)
if err != nil {
return err
}
file, err := os.Open(gitFile.Name)
if err != nil {
return err
}
defer file.Close()
reader := bufio.NewReader(file)
output := ""
for i := 0; true; i++ {
line, err := reader.ReadString('\n')
if err != nil {
break
}
if !isIndexToDelete(i, conflict, pick) {
output += line
}
}
devLog(output)
return ioutil.WriteFile(gitFile.Name, []byte(output), 0644)
}
func pushFileSnapshot(g *gocui.Gui) error {
gitFile, err := getSelectedFile(g)
if err != nil {
return err
}
content, err := catFile(gitFile.Name)
if err != nil {
return err
}
state.EditHistory.Push(content)
return nil
}
func handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error {
colorLog(color.FgCyan, "IM HERE")
if state.EditHistory.Len() == 0 {
return nil
}
prevContent := state.EditHistory.Pop().(string)
gitFile, err := getSelectedFile(g)
if err != nil {
return err
}
ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644)
return refreshMergePanel(g)
}
func handlePickHunk(g *gocui.Gui, v *gocui.View) error {
conflict := state.Conflicts[state.ConflictIndex]
pushFileSnapshot(g)
pick := "bottom"
if state.ConflictTop {
pick = "top"
}
err := resolveConflict(g, conflict, pick)
if err != nil {
panic(err)
}
refreshMergePanel(g)
return nil
}
func handlePickBothHunks(g *gocui.Gui, v *gocui.View) error {
conflict := state.Conflicts[state.ConflictIndex]
pushFileSnapshot(g)
err := resolveConflict(g, conflict, "both")
if err != nil {
panic(err)
}
return refreshMergePanel(g)
}
func currentViewName(g *gocui.Gui) string {
currentView := g.CurrentView()
return currentView.Name()
}
func refreshMergePanel(g *gocui.Gui) error {
cat, err := catSelectedFile(g)
if err != nil {
return err
}
state.Conflicts, err = findConflicts(cat)
if err != nil {
return err
}
if len(state.Conflicts) == 0 {
return handleCompleteMerge(g)
} else if state.ConflictIndex > len(state.Conflicts)-1 {
state.ConflictIndex = len(state.Conflicts) - 1
}
hasFocus := currentViewName(g) == "main"
if hasFocus {
renderMergeOptions(g)
}
content, err := coloredConflictFile(cat, state.Conflicts, state.ConflictIndex, state.ConflictTop, hasFocus)
if err != nil {
return err
}
if err := scrollToConflict(g); err != nil {
return err
}
return renderString(g, "main", content)
}
func scrollToConflict(g *gocui.Gui) error {
mainView, err := g.View("main")
if err != nil {
return err
}
if len(state.Conflicts) == 0 {
return nil
}
conflict := state.Conflicts[state.ConflictIndex]
ox, _ := mainView.Origin()
_, height := mainView.Size()
conflictMiddle := (conflict.end + conflict.start) / 2
newOriginY := int(math.Max(0, float64(conflictMiddle-(height/2))))
return mainView.SetOrigin(ox, newOriginY)
}
func switchToMerging(g *gocui.Gui) error {
state.ConflictIndex = 0
state.ConflictTop = true
_, err := g.SetCurrentView("main")
if err != nil {
return err
}
return refreshMergePanel(g)
}
func renderMergeOptions(g *gocui.Gui) error {
return renderOptionsMap(g, map[string]string{
"↑ ↓": "select hunk",
"← →": "navigate conflicts",
"space": "pick hunk",
"b": "pick both hunks",
"z": "undo",
})
}
func handleEscapeMerge(g *gocui.Gui, v *gocui.View) error {
filesView, err := g.View("files")
if err != nil {
return err
}
refreshFiles(g)
return switchFocus(g, v, filesView)
}
func handleCompleteMerge(g *gocui.Gui) error {
filesView, err := g.View("files")
if err != nil {
return err
}
stageSelectedFile(g)
refreshFiles(g)
return switchFocus(g, nil, filesView)
}

211
pkg/app/app.go Normal file
View File

@@ -0,0 +1,211 @@
package app
import (
"bufio"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/updates"
"github.com/shibukawa/configdir"
"github.com/sirupsen/logrus"
)
// App struct
type App struct {
closers []io.Closer
Config config.AppConfigurer
Log *logrus.Entry
OSCommand *commands.OSCommand
GitCommand *commands.GitCommand
Gui *gui.Gui
Tr *i18n.Localizer
Updater *updates.Updater // may only need this on the Gui
ClientContext string
}
type errorMapping struct {
originalError string
newError string
}
func newProductionLogger(config config.AppConfigurer) *logrus.Logger {
log := logrus.New()
log.Out = ioutil.Discard
log.SetLevel(logrus.ErrorLevel)
return log
}
func globalConfigDir() string {
configDirs := configdir.New("jesseduffield", "lazygit")
configDir := configDirs.QueryFolders(configdir.Global)[0]
return configDir.Path
}
func getLogLevel() logrus.Level {
strLevel := os.Getenv("LOG_LEVEL")
level, err := logrus.ParseLevel(strLevel)
if err != nil {
return logrus.DebugLevel
}
return level
}
func newDevelopmentLogger(config config.AppConfigurer) *logrus.Logger {
log := logrus.New()
log.SetLevel(getLogLevel())
file, err := os.OpenFile(filepath.Join(globalConfigDir(), "development.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
panic("unable to log to file") // TODO: don't panic (also, remove this call to the `panic` function)
}
log.SetOutput(file)
return log
}
func newLogger(config config.AppConfigurer) *logrus.Entry {
var log *logrus.Logger
if config.GetDebug() || os.Getenv("DEBUG") == "TRUE" {
log = newDevelopmentLogger(config)
} else {
log = newProductionLogger(config)
}
// highly recommended: tail -f development.log | humanlog
// https://github.com/aybabtme/humanlog
log.Formatter = &logrus.JSONFormatter{}
return log.WithFields(logrus.Fields{
"debug": config.GetDebug(),
"version": config.GetVersion(),
"commit": config.GetCommit(),
"buildDate": config.GetBuildDate(),
})
}
// NewApp bootstrap a new application
func NewApp(config config.AppConfigurer) (*App, error) {
app := &App{
closers: []io.Closer{},
Config: config,
}
var err error
app.Log = newLogger(config)
app.Tr = i18n.NewLocalizer(app.Log)
// if we are being called in 'demon' mode, we can just return here
app.ClientContext = os.Getenv("LAZYGIT_CLIENT_COMMAND")
if app.ClientContext != "" {
return app, nil
}
app.OSCommand = commands.NewOSCommand(app.Log, config)
app.Updater, err = updates.NewUpdater(app.Log, config, app.OSCommand, app.Tr)
if err != nil {
return app, err
}
if err := app.setupRepo(); err != nil {
return app, err
}
app.GitCommand, err = commands.NewGitCommand(app.Log, app.OSCommand, app.Tr, app.Config)
if err != nil {
return app, err
}
app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config, app.Updater)
if err != nil {
return app, err
}
return app, nil
}
func (app *App) setupRepo() error {
// if we are not in a git repo, we ask if we want to `git init`
if err := app.OSCommand.RunCommand("git status"); err != nil {
if !strings.Contains(err.Error(), "Not a git repository") {
return err
}
fmt.Print(app.Tr.SLocalize("CreateRepo"))
response, _ := bufio.NewReader(os.Stdin).ReadString('\n')
if strings.Trim(response, " \n") != "y" {
os.Exit(1)
}
if err := app.OSCommand.RunCommand("git init"); err != nil {
return err
}
}
return nil
}
func (app *App) Run() error {
if app.ClientContext == "INTERACTIVE_REBASE" {
return app.Rebase()
}
if app.ClientContext == "EXIT_IMMEDIATELY" {
os.Exit(0)
}
err := app.Gui.RunWithSubprocesses()
return err
}
// Rebase contains logic for when we've been run in demon mode, meaning we've
// given lazygit as a command for git to call e.g. to edit a file
func (app *App) Rebase() error {
app.Log.Info("Lazygit invoked as interactive rebase demon")
app.Log.Info("args: ", os.Args)
if strings.HasSuffix(os.Args[1], "git-rebase-todo") {
if err := ioutil.WriteFile(os.Args[1], []byte(os.Getenv("LAZYGIT_REBASE_TODO")), 0644); err != nil {
return err
}
} else if strings.HasSuffix(os.Args[1], ".git/COMMIT_EDITMSG") {
// if we are rebasing and squashing, we'll see a COMMIT_EDITMSG
// but in this case we don't need to edit it, so we'll just return
} else {
app.Log.Info("Lazygit demon did not match on any use cases")
}
return nil
}
// Close closes any resources
func (app *App) Close() error {
for _, closer := range app.closers {
err := closer.Close()
if err != nil {
return err
}
}
return nil
}
// KnownError takes an error and tells us whether it's an error that we know about where we can print a nicely formatted version of it rather than panicking with a stack trace
func (app *App) KnownError(err error) (string, bool) {
errorMessage := err.Error()
mappings := []errorMapping{
{
originalError: "fatal: not a git repository (or any of the parent directories): .git",
newError: app.Tr.SLocalize("notARepository"),
},
}
for _, mapping := range mappings {
if strings.Contains(errorMessage, mapping.originalError) {
return mapping.newError, true
}
}
return "", false
}

10
pkg/commands/branch.go Normal file
View File

@@ -0,0 +1,10 @@
package commands
// Branch : A git branch
// duplicating this for now
type Branch struct {
Name string
Recency string
Pushables string
Pullables string
}

View File

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

15
pkg/commands/commit.go Normal file
View File

@@ -0,0 +1,15 @@
package commands
// Commit : A git commit
type Commit struct {
Sha string
Name string
Status string // one of "unpushed", "pushed", "merged", "rebasing" or "selected"
DisplayString string
Action string // one of "", "pick", "edit", "squash", "reword", "drop", "fixup"
Copied bool // to know if this commit is ready to be cherry-picked somewhere
Tags []string
ExtraInfo string // something like 'HEAD -> master, tag: v0.15.2'
Author string
Date string
}

View File

@@ -0,0 +1,19 @@
package commands
// CommitFile : A git commit file
type CommitFile struct {
Sha string
Name string
DisplayString string
Status int // one of 'WHOLE' 'PART' 'NONE'
}
const (
// UNSELECTED is for when the commit file has not been added to the patch in any way
UNSELECTED = iota
// WHOLE is for when you want to add the whole diff of a file to the patch,
// including e.g. if it was deleted
WHOLE = iota
// PART is for when you're only talking about specific lines that have been modified
PART
)

View File

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

View File

@@ -0,0 +1,151 @@
package commands
import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/stretchr/testify/assert"
)
// NewDummyCommitListBuilder creates a new dummy CommitListBuilder for testing
func NewDummyCommitListBuilder() *CommitListBuilder {
osCommand := NewDummyOSCommand()
return &CommitListBuilder{
Log: NewDummyLog(),
GitCommand: NewDummyGitCommandWithOSCommand(osCommand),
OSCommand: osCommand,
Tr: i18n.NewLocalizer(NewDummyLog()),
CherryPickedCommits: []*Commit{},
}
}
// TestCommitListBuilderGetUnpushedCommits is a function.
func TestCommitListBuilderGetUnpushedCommits(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(map[string]bool)
}
scenarios := []scenario{
{
"Can't retrieve pushable commits",
func(string, ...string) *exec.Cmd {
return exec.Command("test")
},
func(pushables map[string]bool) {
assert.EqualValues(t, map[string]bool{}, pushables)
},
},
{
"Retrieve pushable commits",
func(cmd string, args ...string) *exec.Cmd {
return exec.Command("echo", "8a2bb0e\n78976bc")
},
func(pushables map[string]bool) {
assert.Len(t, pushables, 2)
assert.EqualValues(t, map[string]bool{"8a2bb0e": true, "78976bc": true}, pushables)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.getUnpushedCommits())
})
}
}
// TestCommitListBuilderGetMergeBase is a function.
func TestCommitListBuilderGetMergeBase(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(string, error)
}
scenarios := []scenario{
{
"swallows an error if the call to merge-base returns an error",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("test")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "", output)
},
},
{
"returns the commit when master",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("echo", "blah")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.Equal(t, "blah\n", output)
},
},
{
"checks against develop when a feature branch",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "feature/test")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "develop"}, args)
return exec.Command("echo", "blah")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.Equal(t, "blah\n", output)
},
},
{
"bubbles up error if there is one",
func(cmd string, args ...string) *exec.Cmd {
return exec.Command("test")
},
func(output string, err error) {
assert.Error(t, err)
assert.Equal(t, "", output)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.getMergeBase())
})
}
}

58
pkg/commands/dummies.go Normal file
View File

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

14
pkg/commands/errors.go Normal file
View File

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

View File

@@ -0,0 +1,95 @@
// +build !windows
package commands
import (
"bufio"
"bytes"
"strings"
"unicode/utf8"
"github.com/go-errors/errors"
"github.com/jesseduffield/pty"
)
// RunCommandWithOutputLiveWrapper runs a command and return every word that gets written in stdout
// Output is a function that executes by every word that gets read by bufio
// As return of output you need to give a string that will be written to stdin
// NOTE: If the return data is empty it won't written anything to stdin
func RunCommandWithOutputLiveWrapper(c *OSCommand, command string, output func(string) string) error {
cmd := c.ExecutableFromString(command)
cmd.Env = append(cmd.Env, "LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8")
var stderr bytes.Buffer
cmd.Stderr = &stderr
ptmx, err := pty.Start(cmd)
if err != nil {
return err
}
go 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

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

16
pkg/commands/file.go Normal file
View File

@@ -0,0 +1,16 @@
package commands
// File : A file from git status
// duplicating this for now
type File struct {
Name string
HasStagedChanges bool
HasUnstagedChanges bool
Tracked bool
Deleted bool
HasMergeConflicts bool
HasInlineMergeConflicts bool
DisplayString string
Type string // one of 'file', 'directory', and 'other'
ShortStatus string // e.g. 'AD', ' A', 'M ', '??'
}

1165
pkg/commands/git.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
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
}

2209
pkg/commands/git_test.go Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,87 @@
package commands
import (
"regexp"
"sort"
"strconv"
"strings"
"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() ([]*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([]*Tag, len(split))
for i, tagName := range split {
tags[i] = &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
}

411
pkg/commands/os.go Normal file
View File

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

View File

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

377
pkg/commands/os_test.go Normal file
View File

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

View File

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

View File

@@ -0,0 +1,229 @@
package commands
import (
"sort"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
type fileInfo struct {
mode int // one of WHOLE/PART
includedLineIndices []int
diff string
}
type applyPatchFunc func(patch string, flags ...string) error
// PatchManager manages the building of a patch for a commit to be applied to another commit (or the working tree, or removed from the current commit)
type PatchManager struct {
CommitSha string
fileInfoMap map[string]*fileInfo
Log *logrus.Entry
ApplyPatch applyPatchFunc
}
// NewPatchManager returns a new PatchModifier
func NewPatchManager(log *logrus.Entry, applyPatch applyPatchFunc) *PatchManager {
return &PatchManager{
Log: log,
ApplyPatch: applyPatch,
}
}
// NewPatchManager returns a new PatchModifier
func (p *PatchManager) Start(commitSha string, diffMap map[string]string) {
p.CommitSha = commitSha
p.fileInfoMap = map[string]*fileInfo{}
for filename, diff := range diffMap {
p.fileInfoMap[filename] = &fileInfo{
mode: UNSELECTED,
diff: diff,
}
}
}
func (p *PatchManager) AddFile(filename string) {
p.fileInfoMap[filename].mode = WHOLE
p.fileInfoMap[filename].includedLineIndices = nil
}
func (p *PatchManager) RemoveFile(filename string) {
p.fileInfoMap[filename].mode = UNSELECTED
p.fileInfoMap[filename].includedLineIndices = nil
}
func (p *PatchManager) ToggleFileWhole(filename string) {
info := p.fileInfoMap[filename]
switch info.mode {
case UNSELECTED:
p.AddFile(filename)
case WHOLE:
p.RemoveFile(filename)
case PART:
p.AddFile(filename)
}
}
func getIndicesForRange(first, last int) []int {
indices := []int{}
for i := first; i <= last; i++ {
indices = append(indices, i)
}
return indices
}
func (p *PatchManager) AddFileLineRange(filename string, firstLineIdx, lastLineIdx int) {
info := p.fileInfoMap[filename]
info.mode = PART
info.includedLineIndices = utils.UnionInt(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx))
}
func (p *PatchManager) RemoveFileLineRange(filename string, firstLineIdx, lastLineIdx int) {
info := p.fileInfoMap[filename]
info.mode = PART
info.includedLineIndices = utils.DifferenceInt(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx))
if len(info.includedLineIndices) == 0 {
p.RemoveFile(filename)
}
}
func (p *PatchManager) RenderPlainPatchForFile(filename string, reverse bool, keepOriginalHeader bool) string {
info := p.fileInfoMap[filename]
if info == nil {
return ""
}
switch info.mode {
case WHOLE:
// use the whole diff
// the reverse flag is only for part patches so we're ignoring it here
return info.diff
case PART:
// generate a new diff with just the selected lines
m := NewPatchModifier(p.Log, filename, info.diff)
return m.ModifiedPatchForLines(info.includedLineIndices, reverse, keepOriginalHeader)
default:
return ""
}
}
func (p *PatchManager) RenderPatchForFile(filename string, plain bool, reverse bool, keepOriginalHeader bool) string {
patch := p.RenderPlainPatchForFile(filename, reverse, keepOriginalHeader)
if plain {
return patch
}
parser, err := NewPatchParser(p.Log, patch)
if err != nil {
// swallowing for now
return ""
}
// not passing included lines because we don't want to see them in the secondary panel
return parser.Render(-1, -1, nil)
}
func (p *PatchManager) RenderEachFilePatch(plain bool) []string {
// sort files by name then iterate through and render each patch
filenames := make([]string, len(p.fileInfoMap))
index := 0
for filename := range p.fileInfoMap {
filenames[index] = filename
index++
}
sort.Strings(filenames)
output := []string{}
for _, filename := range filenames {
patch := p.RenderPatchForFile(filename, plain, false, true)
if patch != "" {
output = append(output, patch)
}
}
return output
}
func (p *PatchManager) RenderAggregatedPatchColored(plain bool) string {
result := ""
for _, patch := range p.RenderEachFilePatch(plain) {
if patch != "" {
result += patch + "\n"
}
}
return result
}
func (p *PatchManager) GetFileStatus(filename string) int {
info := p.fileInfoMap[filename]
if info == nil {
return UNSELECTED
}
return info.mode
}
func (p *PatchManager) GetFileIncLineIndices(filename string) []int {
info := p.fileInfoMap[filename]
if info == nil {
return []int{}
}
return info.includedLineIndices
}
func (p *PatchManager) ApplyPatches(reverse bool) error {
// for whole patches we'll apply the patch in reverse
// but for part patches we'll apply a reverse patch forwards
for filename, info := range p.fileInfoMap {
if info.mode == UNSELECTED {
continue
}
applyFlags := []string{"index", "3way"}
reverseOnGenerate := false
if reverse {
if info.mode == WHOLE {
applyFlags = append(applyFlags, "reverse")
} else {
reverseOnGenerate = true
}
}
var err error
// first run we try with the original header, then without
for _, keepOriginalHeader := range []bool{true, false} {
patch := p.RenderPatchForFile(filename, true, reverseOnGenerate, keepOriginalHeader)
if patch == "" {
continue
}
if err = p.ApplyPatch(patch, applyFlags...); err != nil {
continue
}
break
}
if err != nil {
return err
}
}
return nil
}
// clears the patch
func (p *PatchManager) Reset() {
p.CommitSha = ""
p.fileInfoMap = map[string]*fileInfo{}
}
func (p *PatchManager) CommitSelected() bool {
return p.CommitSha != ""
}
func (p *PatchManager) IsEmpty() bool {
for _, fileInfo := range p.fileInfoMap {
if fileInfo.mode == WHOLE || (fileInfo.mode == PART && len(fileInfo.includedLineIndices) > 0) {
return false
}
}
return true
}

View File

@@ -0,0 +1,260 @@
package commands
import (
"fmt"
"regexp"
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
var hunkHeaderRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)[^\+]+\+(\d+)[^@]+@@(.*)$`)
var patchHeaderRegexp = regexp.MustCompile(`(?ms)(^diff.*?)^@@`)
type PatchHunk struct {
header string
FirstLineIdx int
LastLineIdx int
bodyLines []string
}
func newHunk(header string, body string, firstLineIdx int) *PatchHunk {
bodyLines := strings.SplitAfter(header+body, "\n")[1:] // dropping the header line
return &PatchHunk{
header: header,
FirstLineIdx: firstLineIdx,
LastLineIdx: firstLineIdx + len(bodyLines),
bodyLines: bodyLines,
}
}
func (hunk *PatchHunk) updatedLines(lineIndices []int, reverse bool) []string {
skippedNewlineMessageIndex := -1
newLines := []string{}
lineIdx := hunk.FirstLineIdx
for _, line := range hunk.bodyLines {
lineIdx++ // incrementing at the start to skip the header line
if line == "" {
break
}
isLineSelected := utils.IncludesInt(lineIndices, lineIdx)
firstChar, content := line[:1], line[1:]
transformedFirstChar := transformedFirstChar(firstChar, reverse, isLineSelected)
if isLineSelected || (transformedFirstChar == "\\" && skippedNewlineMessageIndex != lineIdx) || transformedFirstChar == " " {
newLines = append(newLines, transformedFirstChar+content)
continue
}
if transformedFirstChar == "+" {
// we don't want to include the 'newline at end of file' line if it involves an addition we're not including
skippedNewlineMessageIndex = lineIdx + 1
}
}
return newLines
}
func transformedFirstChar(firstChar string, reverse bool, isLineSelected bool) string {
if reverse {
if !isLineSelected && firstChar == "+" {
return " "
} else if firstChar == "-" {
return "+"
} else if firstChar == "+" {
return "-"
} else {
return firstChar
}
}
if !isLineSelected && firstChar == "-" {
return " "
}
return firstChar
}
func (hunk *PatchHunk) formatHeader(oldStart int, oldLength int, newStart int, newLength int, heading string) string {
return fmt.Sprintf("@@ -%d,%d +%d,%d @@%s\n", oldStart, oldLength, newStart, newLength, heading)
}
func (hunk *PatchHunk) formatWithChanges(lineIndices []int, reverse bool, startOffset int) (int, string) {
bodyLines := hunk.updatedLines(lineIndices, reverse)
startOffset, header, ok := hunk.updatedHeader(bodyLines, startOffset, reverse)
if !ok {
return startOffset, ""
}
return startOffset, header + strings.Join(bodyLines, "")
}
func (hunk *PatchHunk) updatedHeader(newBodyLines []string, startOffset int, reverse bool) (int, string, bool) {
changeCount := 0
oldLength := 0
newLength := 0
for _, line := range newBodyLines {
switch line[:1] {
case "+":
newLength++
changeCount++
case "-":
oldLength++
changeCount++
case " ":
oldLength++
newLength++
}
}
if changeCount == 0 {
// if nothing has changed we just return nothing
return startOffset, "", false
}
// get oldstart, newstart, and heading from header
match := hunkHeaderRegexp.FindStringSubmatch(hunk.header)
var oldStart int
if reverse {
oldStart = mustConvertToInt(match[2])
} else {
oldStart = mustConvertToInt(match[1])
}
heading := match[3]
var newStartOffset int
// if the hunk went from zero to positive length, we need to increment the starting point by one
// if the hunk went from positive to zero length, we need to decrement the starting point by one
if oldLength == 0 {
newStartOffset = 1
} else if newLength == 0 {
newStartOffset = -1
} else {
newStartOffset = 0
}
newStart := oldStart + startOffset + newStartOffset
newStartOffset = startOffset + newLength - oldLength
formattedHeader := hunk.formatHeader(oldStart, oldLength, newStart, newLength, heading)
return newStartOffset, formattedHeader, true
}
func mustConvertToInt(s string) int {
i, err := strconv.Atoi(s)
if err != nil {
panic(err)
}
return i
}
func GetHeaderFromDiff(diff string) string {
match := patchHeaderRegexp.FindStringSubmatch(diff)
if len(match) <= 1 {
return ""
}
return match[1]
}
func GetHunksFromDiff(diff string) []*PatchHunk {
headers := hunkHeaderRegexp.FindAllString(diff, -1)
bodies := hunkHeaderRegexp.Split(diff, -1)[1:] // discarding top bit
headerFirstLineIndices := []int{}
for lineIdx, line := range strings.Split(diff, "\n") {
if strings.HasPrefix(line, "@@ -") {
headerFirstLineIndices = append(headerFirstLineIndices, lineIdx)
}
}
hunks := make([]*PatchHunk, len(headers))
for index, header := range headers {
hunks[index] = newHunk(header, bodies[index], headerFirstLineIndices[index])
}
return hunks
}
type PatchModifier struct {
Log *logrus.Entry
filename string
hunks []*PatchHunk
header string
}
func NewPatchModifier(log *logrus.Entry, filename string, diffText string) *PatchModifier {
return &PatchModifier{
Log: log,
filename: filename,
hunks: GetHunksFromDiff(diffText),
header: GetHeaderFromDiff(diffText),
}
}
func (d *PatchModifier) ModifiedPatchForLines(lineIndices []int, reverse bool, keepOriginalHeader bool) string {
// step one is getting only those hunks which we care about
hunksInRange := []*PatchHunk{}
outer:
for _, hunk := range d.hunks {
// if there is any line in our lineIndices array that the hunk contains, we append it
for _, lineIdx := range lineIndices {
if lineIdx >= hunk.FirstLineIdx && lineIdx <= hunk.LastLineIdx {
hunksInRange = append(hunksInRange, hunk)
continue outer
}
}
}
// step 2 is collecting all the hunks with new headers
startOffset := 0
formattedHunks := ""
var formattedHunk string
for _, hunk := range hunksInRange {
startOffset, formattedHunk = hunk.formatWithChanges(lineIndices, reverse, startOffset)
formattedHunks += formattedHunk
}
if formattedHunks == "" {
return ""
}
var fileHeader string
// for staging/unstaging lines we don't want the original header because
// it makes git confused e.g. when dealing with deleted/added files
// but with building and applying patches the original header gives git
// information it needs to cleanly apply patches
if keepOriginalHeader {
fileHeader = d.header
} else {
fileHeader = fmt.Sprintf("--- a/%s\n+++ b/%s\n", d.filename, d.filename)
}
return fileHeader + formattedHunks
}
func (d *PatchModifier) ModifiedPatchForRange(firstLineIdx int, lastLineIdx int, reverse bool, keepOriginalHeader bool) string {
// generate array of consecutive line indices from our range
selectedLines := []int{}
for i := firstLineIdx; i <= lastLineIdx; i++ {
selectedLines = append(selectedLines, i)
}
return d.ModifiedPatchForLines(selectedLines, reverse, keepOriginalHeader)
}
func (d *PatchModifier) OriginalPatchLength() int {
if len(d.hunks) == 0 {
return 0
}
return d.hunks[len(d.hunks)-1].LastLineIdx
}
func ModifiedPatchForRange(log *logrus.Entry, filename string, diffText string, firstLineIdx int, lastLineIdx int, reverse bool, keepOriginalHeader bool) string {
p := NewPatchModifier(log, filename, diffText)
return p.ModifiedPatchForRange(firstLineIdx, lastLineIdx, reverse, keepOriginalHeader)
}

View File

@@ -0,0 +1,511 @@
package commands
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
const simpleDiff = `diff --git a/filename b/filename
index dcd3485..1ba5540 100644
--- a/filename
+++ b/filename
@@ -1,5 +1,5 @@
apple
-orange
+grape
...
...
...
`
const addNewlineToEndOfFile = `diff --git a/filename b/filename
index 80a73f1..e48a11c 100644
--- a/filename
+++ b/filename
@@ -60,4 +60,4 @@ grape
...
...
...
-last line
\ No newline at end of file
+last line
`
const removeNewlinefromEndOfFile = `diff --git a/filename b/filename
index e48a11c..80a73f1 100644
--- a/filename
+++ b/filename
@@ -60,4 +60,4 @@ grape
...
...
...
-last line
+last line
\ No newline at end of file
`
const twoHunks = `diff --git a/filename b/filename
index e48a11c..b2ab81b 100644
--- a/filename
+++ b/filename
@@ -1,5 +1,5 @@
apple
-grape
+orange
...
...
...
@@ -8,6 +8,8 @@ grape
...
...
...
+pear
+lemon
...
...
...
`
const newFile = `diff --git a/newfile b/newfile
new file mode 100644
index 0000000..4e680cc
--- /dev/null
+++ b/newfile
@@ -0,0 +1,3 @@
+apple
+orange
+grape
`
const addNewlineToPreviouslyEmptyFile = `diff --git a/newfile b/newfile
index e69de29..c6568ea 100644
--- a/newfile
+++ b/newfile
@@ -0,0 +1 @@
+new line
\ No newline at end of file
`
// TestModifyPatchForRange is a function.
func TestModifyPatchForRange(t *testing.T) {
type scenario struct {
testName string
filename string
diffText string
firstLineIndex int
lastLineIndex int
reverse bool
expected string
}
scenarios := []scenario{
{
testName: "nothing selected",
filename: "filename",
firstLineIndex: -1,
lastLineIndex: -1,
reverse: false,
diffText: simpleDiff,
expected: "",
},
{
testName: "only context selected",
filename: "filename",
firstLineIndex: 5,
lastLineIndex: 5,
reverse: false,
diffText: simpleDiff,
expected: "",
},
{
testName: "whole range selected",
filename: "filename",
firstLineIndex: 0,
lastLineIndex: 11,
reverse: false,
diffText: simpleDiff,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,5 @@
apple
-orange
+grape
...
...
...
`,
},
{
testName: "only removal selected",
filename: "filename",
firstLineIndex: 6,
lastLineIndex: 6,
reverse: false,
diffText: simpleDiff,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,4 @@
apple
-orange
...
...
...
`,
},
{
testName: "only addition selected",
filename: "filename",
firstLineIndex: 7,
lastLineIndex: 7,
reverse: false,
diffText: simpleDiff,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,6 @@
apple
orange
+grape
...
...
...
`,
},
{
testName: "range that extends beyond diff bounds",
filename: "filename",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: false,
diffText: simpleDiff,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,5 @@
apple
-orange
+grape
...
...
...
`,
},
{
testName: "whole range reversed",
filename: "filename",
firstLineIndex: 0,
lastLineIndex: 11,
reverse: true,
diffText: simpleDiff,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,5 @@
apple
+orange
-grape
...
...
...
`,
},
{
testName: "removal reversed",
filename: "filename",
firstLineIndex: 6,
lastLineIndex: 6,
reverse: true,
diffText: simpleDiff,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,6 @@
apple
+orange
grape
...
...
...
`,
},
{
testName: "removal reversed",
filename: "filename",
firstLineIndex: 7,
lastLineIndex: 7,
reverse: true,
diffText: simpleDiff,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,4 @@
apple
-grape
...
...
...
`,
},
{
testName: "add newline to end of file",
filename: "filename",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: false,
diffText: addNewlineToEndOfFile,
expected: `--- a/filename
+++ b/filename
@@ -60,4 +60,4 @@ grape
...
...
...
-last line
\ No newline at end of file
+last line
`,
},
{
testName: "add newline to end of file, addition only",
filename: "filename",
firstLineIndex: 8,
lastLineIndex: 8,
reverse: true,
diffText: addNewlineToEndOfFile,
expected: `--- a/filename
+++ b/filename
@@ -60,4 +60,5 @@ grape
...
...
...
+last line
\ No newline at end of file
last line
`,
},
{
testName: "add newline to end of file, removal only",
filename: "filename",
firstLineIndex: 10,
lastLineIndex: 10,
reverse: true,
diffText: addNewlineToEndOfFile,
expected: `--- a/filename
+++ b/filename
@@ -60,4 +60,3 @@ grape
...
...
...
-last line
`,
},
{
testName: "remove newline from end of file",
filename: "filename",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: false,
diffText: removeNewlinefromEndOfFile,
expected: `--- a/filename
+++ b/filename
@@ -60,4 +60,4 @@ grape
...
...
...
-last line
+last line
\ No newline at end of file
`,
},
{
testName: "remove newline from end of file, removal only",
filename: "filename",
firstLineIndex: 8,
lastLineIndex: 8,
reverse: false,
diffText: removeNewlinefromEndOfFile,
expected: `--- a/filename
+++ b/filename
@@ -60,4 +60,3 @@ grape
...
...
...
-last line
`,
},
{
testName: "remove newline from end of file, addition only",
filename: "filename",
firstLineIndex: 9,
lastLineIndex: 9,
reverse: false,
diffText: removeNewlinefromEndOfFile,
expected: `--- a/filename
+++ b/filename
@@ -60,4 +60,5 @@ grape
...
...
...
last line
+last line
\ No newline at end of file
`,
},
{
testName: "staging two whole hunks",
filename: "filename",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: false,
diffText: twoHunks,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,5 @@
apple
-grape
+orange
...
...
...
@@ -8,6 +8,8 @@ grape
...
...
...
+pear
+lemon
...
...
...
`,
},
{
testName: "staging part of both hunks",
filename: "filename",
firstLineIndex: 7,
lastLineIndex: 15,
reverse: false,
diffText: twoHunks,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,6 @@
apple
grape
+orange
...
...
...
@@ -8,6 +9,7 @@ grape
...
...
...
+pear
...
...
...
`,
},
{
testName: "staging part of both hunks, reversed",
filename: "filename",
firstLineIndex: 7,
lastLineIndex: 15,
reverse: true,
diffText: twoHunks,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,4 @@
apple
-orange
...
...
...
@@ -8,8 +7,7 @@ grape
...
...
...
-pear
lemon
...
...
...
`,
},
{
testName: "adding a new file",
filename: "newfile",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: false,
diffText: newFile,
expected: `--- a/newfile
+++ b/newfile
@@ -0,0 +1,3 @@
+apple
+orange
+grape
`,
},
{
testName: "adding part of a new file",
filename: "newfile",
firstLineIndex: 6,
lastLineIndex: 7,
reverse: false,
diffText: newFile,
expected: `--- a/newfile
+++ b/newfile
@@ -0,0 +1,2 @@
+apple
+orange
`,
},
{
testName: "adding a new file, reversed",
filename: "newfile",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: true,
diffText: newFile,
expected: `--- a/newfile
+++ b/newfile
@@ -1,3 +0,0 @@
-apple
-orange
-grape
`,
},
{
testName: "adding a new line to a previously empty file",
filename: "newfile",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: false,
diffText: addNewlineToPreviouslyEmptyFile,
expected: `--- a/newfile
+++ b/newfile
@@ -0,0 +1,1 @@
+new line
\ No newline at end of file
`,
},
{
testName: "adding a new line to a previously empty file, reversed",
filename: "newfile",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: true,
diffText: addNewlineToPreviouslyEmptyFile,
expected: `--- a/newfile
+++ b/newfile
@@ -1,1 +0,0 @@
-new line
\ No newline at end of file
`,
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
result := ModifiedPatchForRange(nil, s.filename, s.diffText, s.firstLineIndex, s.lastLineIndex, s.reverse, false)
if !assert.Equal(t, s.expected, result) {
fmt.Println(result)
}
})
}
}

View File

@@ -0,0 +1,213 @@
package commands
import (
"regexp"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
const (
PATCH_HEADER = iota
COMMIT_SHA
COMMIT_DESCRIPTION
HUNK_HEADER
ADDITION
DELETION
CONTEXT
NEWLINE_MESSAGE
)
// the job of this file is to parse a diff, find out where the hunks begin and end, which lines are stageable, and how to find the next hunk from the current position or the next stageable line from the current position.
type PatchLine struct {
Kind int
Content string // something like '+ hello' (note the first character is not removed)
}
type PatchParser struct {
Log *logrus.Entry
PatchLines []*PatchLine
PatchHunks []*PatchHunk
HunkStarts []int
StageableLines []int // rename to mention we're talking about indexes
}
// NewPatchParser builds a new branch list builder
func NewPatchParser(log *logrus.Entry, patch string) (*PatchParser, error) {
hunkStarts, stageableLines, patchLines, err := parsePatch(patch)
if err != nil {
return nil, err
}
patchHunks := GetHunksFromDiff(patch)
return &PatchParser{
Log: log,
HunkStarts: hunkStarts, // deprecated
StageableLines: stageableLines,
PatchLines: patchLines,
PatchHunks: patchHunks,
}, nil
}
// GetHunkContainingLine takes a line index and an offset and finds the hunk
// which contains the line index, then returns the hunk considering the offset.
// e.g. if the offset is 1 it will return the next hunk.
func (p *PatchParser) GetHunkContainingLine(lineIndex int, offset int) *PatchHunk {
if len(p.PatchHunks) == 0 {
return nil
}
for index, hunk := range p.PatchHunks {
if lineIndex >= hunk.FirstLineIdx && lineIndex <= hunk.LastLineIdx {
resultIndex := index + offset
if resultIndex < 0 {
resultIndex = 0
} else if resultIndex > len(p.PatchHunks)-1 {
resultIndex = len(p.PatchHunks) - 1
}
return p.PatchHunks[resultIndex]
}
}
// if your cursor is past the last hunk, select the last hunk
if lineIndex > p.PatchHunks[len(p.PatchHunks)-1].LastLineIdx {
return p.PatchHunks[len(p.PatchHunks)-1]
}
// otherwise select the first
return p.PatchHunks[0]
}
// selected means you've got it highlighted with your cursor
// included means the line has been included in the patch (only applicable when
// building a patch)
func (l *PatchLine) render(selected bool, included bool) string {
content := l.Content
if len(content) == 0 {
content = " " // using the space so that we can still highlight if necessary
}
// for hunk headers we need to start off cyan and then use white for the message
if l.Kind == HUNK_HEADER {
re := regexp.MustCompile("(@@.*?@@)(.*)")
match := re.FindStringSubmatch(content)
return coloredString(color.FgCyan, match[1], selected, included) + coloredString(theme.DefaultTextColor, match[2], selected, false)
}
var colorAttr color.Attribute
switch l.Kind {
case PATCH_HEADER:
colorAttr = color.Bold
case ADDITION:
colorAttr = color.FgGreen
case DELETION:
colorAttr = color.FgRed
case COMMIT_SHA:
colorAttr = color.FgYellow
default:
colorAttr = theme.DefaultTextColor
}
return coloredString(colorAttr, content, selected, included)
}
func coloredString(colorAttr color.Attribute, str string, selected bool, included bool) string {
var cl *color.Color
attributes := []color.Attribute{colorAttr}
if selected {
attributes = append(attributes, theme.SelectedLineBgColor)
}
cl = color.New(attributes...)
var clIncluded *color.Color
if included {
clIncluded = color.New(append(attributes, color.BgGreen)...)
} else {
clIncluded = color.New(attributes...)
}
if len(str) < 2 {
return utils.ColoredStringDirect(str, clIncluded)
}
return utils.ColoredStringDirect(str[:1], clIncluded) + utils.ColoredStringDirect(str[1:], cl)
}
func parsePatch(patch string) ([]int, []int, []*PatchLine, error) {
lines := strings.Split(patch, "\n")
hunkStarts := []int{}
stageableLines := []int{}
pastFirstHunkHeader := false
pastCommitDescription := true
patchLines := make([]*PatchLine, len(lines))
var lineKind int
var firstChar string
for index, line := range lines {
firstChar = " "
if len(line) > 0 {
firstChar = line[:1]
}
if index == 0 && strings.HasPrefix(line, "commit") {
lineKind = COMMIT_SHA
pastCommitDescription = false
} else if !pastCommitDescription {
if strings.HasPrefix(line, "diff") || strings.HasPrefix(line, "---") {
pastCommitDescription = true
lineKind = PATCH_HEADER
} else {
lineKind = COMMIT_DESCRIPTION
}
} else if firstChar == "@" {
pastFirstHunkHeader = true
hunkStarts = append(hunkStarts, index)
lineKind = HUNK_HEADER
} else if pastFirstHunkHeader {
switch firstChar {
case "-":
lineKind = DELETION
stageableLines = append(stageableLines, index)
case "+":
lineKind = ADDITION
stageableLines = append(stageableLines, index)
case "\\":
lineKind = NEWLINE_MESSAGE
case " ":
lineKind = CONTEXT
}
} else {
lineKind = PATCH_HEADER
}
patchLines[index] = &PatchLine{Kind: lineKind, Content: line}
}
return hunkStarts, stageableLines, patchLines, nil
}
// Render returns the coloured string of the diff with any selected lines highlighted
func (p *PatchParser) Render(firstLineIndex int, lastLineIndex int, incLineIndices []int) string {
renderedLines := make([]string, len(p.PatchLines))
for index, patchLine := range p.PatchLines {
selected := index >= firstLineIndex && index <= lastLineIndex
included := utils.IncludesInt(incLineIndices, index)
renderedLines[index] = patchLine.render(selected, included)
}
result := strings.Join(renderedLines, "\n")
if strings.TrimSpace(utils.Decolorise(result)) == "" {
return ""
}
return result
}
// GetNextStageableLineIndex takes a line index and returns the line index of the next stageable line
// note this will actually include the current index if it is stageable
func (p *PatchParser) GetNextStageableLineIndex(currentIndex int) int {
for _, lineIndex := range p.StageableLines {
if lineIndex >= currentIndex {
return lineIndex
}
}
return p.StageableLines[len(p.StageableLines)-1]
}

View File

@@ -0,0 +1,169 @@
package commands
import "github.com/go-errors/errors"
// DeletePatchesFromCommit applies a patch in reverse for a commit
func (c *GitCommand) DeletePatchesFromCommit(commits []*Commit, commitIndex int, p *PatchManager) error {
if err := c.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil {
return err
}
// apply each patch in reverse
if err := p.ApplyPatches(true); err != nil {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
}
return err
}
// time to amend the selected commit
if _, err := c.AmendHead(); err != nil {
return err
}
c.onSuccessfulContinue = func() error {
c.PatchManager.Reset()
return nil
}
// continue
return c.GenericMerge("rebase", "continue")
}
func (c *GitCommand) MovePatchToSelectedCommit(commits []*Commit, sourceCommitIdx int, destinationCommitIdx int, p *PatchManager) error {
if sourceCommitIdx < destinationCommitIdx {
if err := c.BeginInteractiveRebaseForCommit(commits, destinationCommitIdx); err != nil {
return err
}
// apply each patch forward
if err := p.ApplyPatches(false); err != nil {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
}
return err
}
// amend the destination commit
if _, err := c.AmendHead(); err != nil {
return err
}
c.onSuccessfulContinue = func() error {
c.PatchManager.Reset()
return nil
}
// continue
return c.GenericMerge("rebase", "continue")
}
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 c.usingGpg() {
return errors.New(c.Tr.SLocalize("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
}
cmd, err := c.PrepareInteractiveRebaseCommand(commits[baseIndex].Sha, todo, true)
if err != nil {
return err
}
if err := c.OSCommand.RunPreparedCommand(cmd); err != nil {
return err
}
// apply each patch in reverse
if err := p.ApplyPatches(true); err != nil {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
}
return err
}
// amend the source commit
if _, err := c.AmendHead(); err != nil {
return err
}
if c.onSuccessfulContinue != nil {
return errors.New("You are midway through another rebase operation. Please abort to start again")
}
c.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 := p.ApplyPatches(false); err != nil {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
}
return err
}
// amend the destination commit
if _, err := c.AmendHead(); err != nil {
return err
}
c.onSuccessfulContinue = func() error {
c.PatchManager.Reset()
return nil
}
return c.GenericMerge("rebase", "continue")
}
return c.GenericMerge("rebase", "continue")
}
func (c *GitCommand) PullPatchIntoIndex(commits []*Commit, commitIdx int, p *PatchManager) error {
if err := c.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil {
return err
}
if err := p.ApplyPatches(true); err != nil {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
}
return err
}
// amend the commit
if _, err := c.AmendHead(); err != nil {
return err
}
if c.onSuccessfulContinue != nil {
return errors.New("You are midway through another rebase operation. Please abort to start again")
}
c.onSuccessfulContinue = func() error {
// add patches to index
if err := p.ApplyPatches(false); err != nil {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
}
return err
}
c.PatchManager.Reset()
return nil
}
return c.GenericMerge("rebase", "continue")
}

View File

@@ -0,0 +1,143 @@
package commands
import (
"fmt"
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/config"
)
// Service is a service that repository is on (Github, Bitbucket, ...)
type Service struct {
Name string
PullRequestURL string
}
// PullRequest opens a link in browser to create new pull request
// with selected branch
type PullRequest struct {
GitServices []*Service
GitCommand *GitCommand
}
// RepoInformation holds some basic information about the repo
type RepoInformation struct {
Owner string
Repository string
}
// NewService builds a Service based on the host type
func NewService(typeName string, repositoryDomain string, siteDomain string) *Service {
var service *Service
switch typeName {
case "github":
service = &Service{
Name: repositoryDomain,
PullRequestURL: fmt.Sprintf("https://%s%s", siteDomain, "/%s/%s/compare/%s?expand=1"),
}
case "bitbucket":
service = &Service{
Name: repositoryDomain,
PullRequestURL: fmt.Sprintf("https://%s%s", siteDomain, "/%s/%s/pull-requests/new?source=%s&t=1"),
}
case "gitlab":
service = &Service{
Name: repositoryDomain,
PullRequestURL: fmt.Sprintf("https://%s%s", siteDomain, "/%s/%s/merge_requests/new?merge_request[source_branch]=%s"),
}
}
return service
}
func getServices(config config.AppConfigurer) []*Service {
services := []*Service{
NewService("github", "github.com", "github.com"),
NewService("bitbucket", "bitbucket.org", "bitbucket.org"),
NewService("gitlab", "gitlab.com", "gitlab.com"),
}
configServices := config.GetUserConfig().GetStringMapString("services")
for repoDomain, typeAndDomain := range configServices {
splitData := strings.Split(typeAndDomain, ":")
if len(splitData) != 2 {
// TODO log this misconfiguration
continue
}
service := NewService(splitData[0], repoDomain, splitData[1])
if service == nil {
// TODO log this unsupported service
continue
}
services = append(services, service)
}
return services
}
// NewPullRequest creates new instance of PullRequest
func NewPullRequest(gitCommand *GitCommand) *PullRequest {
return &PullRequest{
GitServices: getServices(gitCommand.Config),
GitCommand: gitCommand,
}
}
// Create opens link to new pull request in browser
func (pr *PullRequest) Create(branch *Branch) error {
branchExistsOnRemote := pr.GitCommand.CheckRemoteBranchExists(branch)
if !branchExistsOnRemote {
return errors.New(pr.GitCommand.Tr.SLocalize("NoBranchOnRemote"))
}
repoURL := pr.GitCommand.GetRemoteURL()
var gitService *Service
for _, service := range pr.GitServices {
if strings.Contains(repoURL, service.Name) {
gitService = service
break
}
}
if gitService == nil {
return errors.New(pr.GitCommand.Tr.SLocalize("UnsupportedGitService"))
}
repoInfo := getRepoInfoFromURL(repoURL)
return pr.GitCommand.OSCommand.OpenLink(fmt.Sprintf(
gitService.PullRequestURL, repoInfo.Owner, repoInfo.Repository, branch.Name,
))
}
func getRepoInfoFromURL(url string) *RepoInformation {
isHTTP := strings.HasPrefix(url, "http")
if isHTTP {
splits := strings.Split(url, "/")
owner := strings.Join(splits[3:len(splits)-1], "/")
repo := strings.TrimSuffix(splits[len(splits)-1], ".git")
return &RepoInformation{
Owner: owner,
Repository: repo,
}
}
tmpSplit := strings.Split(url, ":")
splits := strings.Split(tmpSplit[1], "/")
owner := strings.Join(splits[0:len(splits)-1], "/")
repo := strings.TrimSuffix(splits[len(splits)-1], ".git")
return &RepoInformation{
Owner: owner,
Repository: repo,
}
}

View File

@@ -0,0 +1,161 @@
package commands
import (
"os/exec"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
// TestGetRepoInfoFromURL is a function.
func TestGetRepoInfoFromURL(t *testing.T) {
type scenario struct {
testName string
repoURL string
test func(*RepoInformation)
}
scenarios := []scenario{
{
"Returns repository information for git remote url",
"git@github.com:petersmith/super_calculator",
func(repoInfo *RepoInformation) {
assert.EqualValues(t, repoInfo.Owner, "petersmith")
assert.EqualValues(t, repoInfo.Repository, "super_calculator")
},
},
{
"Returns repository information for http remote url",
"https://my_username@bitbucket.org/johndoe/social_network.git",
func(repoInfo *RepoInformation) {
assert.EqualValues(t, repoInfo.Owner, "johndoe")
assert.EqualValues(t, repoInfo.Repository, "social_network")
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
s.test(getRepoInfoFromURL(s.repoURL))
})
}
}
// TestCreatePullRequest is a function.
func TestCreatePullRequest(t *testing.T) {
type scenario struct {
testName string
branch *Branch
command func(string, ...string) *exec.Cmd
test func(err error)
}
scenarios := []scenario{
{
"Opens a link to new pull request on bitbucket",
&Branch{
Name: "feature/profile-page",
},
func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return exec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
}
assert.Equal(t, cmd, "open")
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1"})
return exec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
},
{
"Opens a link to new pull request on bitbucket with http remote url",
&Branch{
Name: "feature/events",
},
func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return exec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
}
assert.Equal(t, cmd, "open")
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1"})
return exec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
},
{
"Opens a link to new pull request on github",
&Branch{
Name: "feature/sum-operation",
},
func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return exec.Command("echo", "git@github.com:peter/calculator.git")
}
assert.Equal(t, cmd, "open")
assert.Equal(t, args, []string{"https://github.com/peter/calculator/compare/feature/sum-operation?expand=1"})
return exec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
},
{
"Opens a link to new pull request on gitlab",
&Branch{
Name: "feature/ui",
},
func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return exec.Command("echo", "git@gitlab.com:peter/calculator.git")
}
assert.Equal(t, cmd, "open")
assert.Equal(t, args, []string{"https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"})
return exec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
},
{
"Throws an error if git service is unsupported",
&Branch{
Name: "feature/divide-operation",
},
func(cmd string, args ...string) *exec.Cmd {
return exec.Command("echo", "git@something.com:peter/calculator.git")
},
func(err error) {
assert.Error(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCommand := NewDummyGitCommand()
gitCommand.OSCommand.command = s.command
gitCommand.OSCommand.Config.GetUserConfig().Set("os.openLinkCommand", "open {{link}}")
gitCommand.Config.GetUserConfig().Set("services", map[string]string{
// valid configuration for a custom service URL
"git.work.com": "gitlab:code.work.com",
// invalid configurations for a custom service URL
"invalid.work.com": "noservice:invalid.work.com",
"noservice.work.com": "noservice.work.com",
})
dummyPullRequest := NewPullRequest(gitCommand)
s.test(dummyPullRequest.Create(s.branch))
})
}
}

24
pkg/commands/remote.go Normal file
View File

@@ -0,0 +1,24 @@
package commands
import (
"fmt"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// Remote : A git remote
type Remote struct {
Name string
Urls []string
Selected bool
Branches []*RemoteBranch
}
// GetDisplayStrings returns the display string of a remote
func (r *Remote) GetDisplayStrings(isFocused bool) []string {
branchCount := len(r.Branches)
return []string{r.Name, utils.ColoredString(fmt.Sprintf("%d branches", branchCount), color.FgBlue)}
}

View File

@@ -0,0 +1,7 @@
package commands
// Remote Branch : A git remote branch
type RemoteBranch struct {
Name string
RemoteName string
}

View File

@@ -0,0 +1,13 @@
package commands
// StashEntry : A git stash entry
type StashEntry struct {
Index int
Name string
DisplayString string
}
// GetDisplayStrings returns the display string of branch
func (s *StashEntry) GetDisplayStrings(isFocused bool) []string {
return []string{s.DisplayString}
}

6
pkg/commands/tag.go Normal file
View File

@@ -0,0 +1,6 @@
package commands
// Tag : A git tag
type Tag struct {
Name string
}

0
pkg/commands/testdata/a_dir/file vendored Normal file
View File

0
pkg/commands/testdata/a_file vendored Normal file
View File

396
pkg/config/app_config.go Normal file
View File

@@ -0,0 +1,396 @@
package config
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"github.com/shibukawa/configdir"
"github.com/spf13/viper"
yaml "gopkg.in/yaml.v2"
)
// AppConfig contains the base configuration fields required for lazygit.
type AppConfig struct {
Debug bool `long:"debug" env:"DEBUG" default:"false"`
Version string `long:"version" env:"VERSION" default:"unversioned"`
Commit string `long:"commit" env:"COMMIT"`
BuildDate string `long:"build-date" env:"BUILD_DATE"`
Name string `long:"name" env:"NAME" default:"lazygit"`
BuildSource string `long:"build-source" env:"BUILD_SOURCE" default:""`
UserConfig *viper.Viper
UserConfigDir string
AppState *AppState
IsNewRepo bool
}
// AppConfigurer interface allows individual app config structs to inherit Fields
// from AppConfig and still be used by lazygit.
type AppConfigurer interface {
GetDebug() bool
GetVersion() string
GetCommit() string
GetBuildDate() string
GetName() string
GetBuildSource() string
GetUserConfig() *viper.Viper
GetUserConfigDir() string
GetAppState() *AppState
WriteToUserConfig(string, interface{}) error
SaveAppState() error
LoadAppState() error
SetIsNewRepo(bool)
GetIsNewRepo() bool
}
// NewAppConfig makes a new app config
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag bool) (*AppConfig, error) {
userConfig, userConfigPath, err := LoadConfig("config", true)
if err != nil {
return nil, err
}
if os.Getenv("DEBUG") == "TRUE" {
debuggingFlag = true
}
appConfig := &AppConfig{
Name: "lazygit",
Version: version,
Commit: commit,
BuildDate: date,
Debug: debuggingFlag,
BuildSource: buildSource,
UserConfig: userConfig,
UserConfigDir: filepath.Dir(userConfigPath),
AppState: &AppState{},
IsNewRepo: false,
}
if err := appConfig.LoadAppState(); err != nil {
return nil, err
}
return appConfig, nil
}
// GetIsNewRepo returns known repo boolean
func (c *AppConfig) GetIsNewRepo() bool {
return c.IsNewRepo
}
// SetIsNewRepo set if the current repo is known
func (c *AppConfig) SetIsNewRepo(toSet bool) {
c.IsNewRepo = toSet
}
// GetDebug returns debug flag
func (c *AppConfig) GetDebug() bool {
return c.Debug
}
// GetVersion returns debug flag
func (c *AppConfig) GetVersion() string {
return c.Version
}
// GetCommit returns debug flag
func (c *AppConfig) GetCommit() string {
return c.Commit
}
// GetBuildDate returns debug flag
func (c *AppConfig) GetBuildDate() string {
return c.BuildDate
}
// GetName returns debug flag
func (c *AppConfig) GetName() string {
return c.Name
}
// GetBuildSource returns the source of the build. For builds from goreleaser
// this will be binaryBuild
func (c *AppConfig) GetBuildSource() string {
return c.BuildSource
}
// GetUserConfig returns the user config
func (c *AppConfig) GetUserConfig() *viper.Viper {
return c.UserConfig
}
// GetAppState returns the app state
func (c *AppConfig) GetAppState() *AppState {
return c.AppState
}
func (c *AppConfig) GetUserConfigDir() string {
return c.UserConfigDir
}
func newViper(filename string) (*viper.Viper, error) {
v := viper.New()
v.SetConfigType("yaml")
v.SetConfigName(filename)
return v, nil
}
// LoadConfig gets the user's config
func LoadConfig(filename string, withDefaults bool) (*viper.Viper, string, error) {
v, err := newViper(filename)
if err != nil {
return nil, "", err
}
if withDefaults {
if err = LoadDefaults(v, GetDefaultConfig()); err != nil {
return nil, "", err
}
if err = LoadDefaults(v, GetPlatformDefaultConfig()); err != nil {
return nil, "", err
}
}
configPath, err := LoadAndMergeFile(v, filename+".yml")
if err != nil {
return nil, "", err
}
return v, configPath, nil
}
// LoadDefaults loads in the defaults defined in this file
func LoadDefaults(v *viper.Viper, defaults []byte) error {
return v.MergeConfig(bytes.NewBuffer(defaults))
}
func prepareConfigFile(filename string) (string, error) {
// chucking my name there is not for vanity purposes, the xdg spec (and that
// function) requires a vendor name. May as well line up with github
configDirs := configdir.New("jesseduffield", "lazygit")
folder := configDirs.QueryFolderContainsFile(filename)
if folder == nil {
// create the file as empty
folders := configDirs.QueryFolders(configdir.Global)
if err := folders[0].WriteFile(filename, []byte{}); err != nil {
return "", err
}
folder = configDirs.QueryFolderContainsFile(filename)
}
return filepath.Join(folder.Path, filename), nil
}
// LoadAndMergeFile Loads the config/state file, creating
// the file has an empty one if it does not exist
func LoadAndMergeFile(v *viper.Viper, filename string) (string, error) {
configPath, err := prepareConfigFile(filename)
if err != nil {
return "", err
}
v.AddConfigPath(filepath.Dir(configPath))
return configPath, v.MergeInConfig()
}
// WriteToUserConfig adds a key/value pair to the user's config and saves it
func (c *AppConfig) WriteToUserConfig(key string, value interface{}) error {
// reloading the user config directly (without defaults) so that we're not
// writing any defaults back to the user's config
v, _, err := LoadConfig("config", false)
if err != nil {
return err
}
v.Set(key, value)
return v.WriteConfig()
}
// SaveAppState marshalls the AppState struct and writes it to the disk
func (c *AppConfig) SaveAppState() error {
marshalledAppState, err := yaml.Marshal(c.AppState)
if err != nil {
return err
}
filepath, err := prepareConfigFile("state.yml")
if err != nil {
return err
}
return ioutil.WriteFile(filepath, marshalledAppState, 0644)
}
// LoadAppState loads recorded AppState from file
func (c *AppConfig) LoadAppState() error {
filepath, err := prepareConfigFile("state.yml")
if err != nil {
return err
}
appStateBytes, err := ioutil.ReadFile(filepath)
if err != nil {
return err
}
if len(appStateBytes) == 0 {
return yaml.Unmarshal(getDefaultAppState(), c.AppState)
}
return yaml.Unmarshal(appStateBytes, c.AppState)
}
// GetDefaultConfig returns the application default configuration
func GetDefaultConfig() []byte {
return []byte(
`gui:
## stuff relating to the UI
scrollHeight: 2
scrollPastBottom: true
mouseEvents: true
skipUnstageLineWarning: false
sidePanelWidth: 0.3333
theme:
lightTheme: false
activeBorderColor:
- green
- bold
inactiveBorderColor:
- white
optionsTextColor:
- blue
selectedLineBgColor:
- blue
commitLength:
show: true
git:
paging:
colorArg: always
useConfig: false
merging:
manualCommit: false
skipHookPrefix: 'WIP'
autoFetch: true
update:
method: prompt # can be: prompt | background | never
days: 14 # how often a update is checked for
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
splashUpdatesIndex: 0
confirmOnQuit: false
keybinding:
universal:
quit: 'q'
quit-alt1: '<c-c>'
return: '<esc>'
quitWithoutChangingDirectory: 'Q'
togglePanel: '<tab>'
prevItem: '<up>'
nextItem: '<down>'
prevItem-alt: 'k'
nextItem-alt: 'j'
prevBlock: '<left>'
nextBlock: '<right>'
prevBlock-alt: 'h'
nextBlock-alt: 'l'
nextMatch: 'n'
prevMatch: 'N'
startSearch: '/'
optionMenu: 'x'
optionMenu-alt1: '?'
select: '<space>'
goInto: '<enter>'
remove: 'd'
new: 'n'
edit: 'e'
openFile: 'o'
scrollUpMain: '<pgup>'
scrollDownMain: '<pgdown>'
scrollUpMain-alt1: 'K'
scrollDownMain-alt1: 'J'
scrollUpMain-alt2: '<c-u>'
scrollDownMain-alt2: '<c-d>'
executeCustomCommand: ':'
createRebaseOptionsMenu: 'm'
pushFiles: 'P'
pullFiles: 'p'
refresh: 'R'
createPatchOptionsMenu: '<c-p>'
nextTab: ']'
prevTab: '['
nextScreenMode: '+'
prevScreenMode: '_'
status:
checkForUpdate: 'u'
recentRepos: '<enter>'
files:
commitChanges: 'c'
commitChangesWithoutHook: 'w'
amendLastCommit: 'A'
commitChangesWithEditor: 'C'
ignoreFile: 'i'
refreshFiles: 'r'
stashAllChanges: 's'
viewStashOptions: 'S'
toggleStagedAll: 'a'
viewResetOptions: 'D'
fetch: 'f'
branches:
createPullRequest: 'o'
checkoutBranchByName: 'c'
forceCheckoutBranch: 'F'
rebaseBranch: 'r'
mergeIntoCurrentBranch: 'M'
viewGitFlowOptions: 'i'
fastForward: 'f'
pushTag: 'P'
setUpstream: 'u'
fetchRemote: 'f'
commits:
squashDown: 's'
renameCommit: 'r'
renameCommitWithEditor: 'R'
viewResetOptions: 'g'
markCommitAsFixup: 'f'
createFixupCommit: 'F'
squashAboveCommits: 'S'
moveDownCommit: '<c-j>'
moveUpCommit: '<c-k>'
amendToCommit: 'A'
pickCommit: 'p'
revertCommit: 't'
cherryPickCopy: 'c'
cherryPickCopyRange: 'C'
pasteCommits: 'v'
tagCommit: 'T'
toggleDiffCommit: 'i'
checkoutCommit: '<space>'
stash:
popStash: 'g'
commitFiles:
checkoutCommitFile: 'c'
main:
toggleDragSelect: 'v'
toggleDragSelect-alt: 'V'
toggleSelectHunk: 'a'
pickBothHunks: 'b'
undo: 'z'
`)
}
// AppState stores data between runs of the app like when the last update check
// was performed and which other repos have been checked out
type AppState struct {
LastUpdateCheck int64
RecentRepos []string
}
func getDefaultAppState() []byte {
return []byte(`
lastUpdateCheck: 0
recentRepos: []
`)
}
// // commenting this out until we use it again
// func homeDirectory() string {
// usr, err := user.Current()
// if err != nil {
// log.Fatal(err)
// }
// return usr.HomeDir
// }

View File

@@ -0,0 +1,11 @@
// +build !windows,!linux
package config
// GetPlatformDefaultConfig gets the defaults for the platform
func GetPlatformDefaultConfig() []byte {
return []byte(
`os:
openCommand: 'open {{filename}}'
openLinkCommand: 'open {{link}}'`)
}

View File

@@ -0,0 +1,9 @@
package config
// GetPlatformDefaultConfig gets the defaults for the platform
func GetPlatformDefaultConfig() []byte {
return []byte(
`os:
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'
openLinkCommand: 'sh -c "xdg-open {{link}} >/dev/null"'`)
}

View File

@@ -0,0 +1,9 @@
package config
// GetPlatformDefaultConfig gets the defaults for the platform
func GetPlatformDefaultConfig() []byte {
return []byte(
`os:
openCommand: 'cmd /c "start "" {{filename}}"'
openLinkCommand: 'cmd /c "start "" {{link}}"'`)
}

View File

@@ -0,0 +1,83 @@
package gui
import (
"time"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type appStatus struct {
name string
statusType string
duration int
}
type statusManager struct {
statuses []appStatus
}
func (m *statusManager) removeStatus(name string) {
newStatuses := []appStatus{}
for _, status := range m.statuses {
if status.name != name {
newStatuses = append(newStatuses, status)
}
}
m.statuses = newStatuses
}
func (m *statusManager) addWaitingStatus(name string) {
m.removeStatus(name)
newStatus := appStatus{
name: name,
statusType: "waiting",
duration: 0,
}
m.statuses = append([]appStatus{newStatus}, m.statuses...)
}
func (m *statusManager) getStatusString() string {
if len(m.statuses) == 0 {
return ""
}
topStatus := m.statuses[0]
if topStatus.statusType == "waiting" {
return topStatus.name + " " + utils.Loader()
}
return topStatus.name
}
// WithWaitingStatus wraps a function and shows a waiting status while the function is still executing
func (gui *Gui) WithWaitingStatus(name string, f func() error) error {
go func() {
gui.statusManager.addWaitingStatus(name)
defer func() {
gui.statusManager.removeStatus(name)
}()
go func() {
ticker := time.NewTicker(time.Millisecond * 50)
defer ticker.Stop()
for range ticker.C {
appStatus := gui.statusManager.getStatusString()
gui.Log.Warn(appStatus)
if appStatus == "" {
return
}
if err := gui.renderString(gui.g, "appStatus", appStatus); err != nil {
gui.Log.Warn(err)
}
}
}()
if err := f(); err != nil {
gui.g.Update(func(g *gocui.Gui) error {
return gui.createErrorPanel(gui.g, err.Error())
})
}
}()
return nil
}

460
pkg/gui/branches_panel.go Normal file
View File

@@ -0,0 +1,460 @@
package gui
import (
"fmt"
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// list panel functions
func (gui *Gui) getSelectedBranch() *commands.Branch {
selectedLine := gui.State.Panels.Branches.SelectedLine
if selectedLine == -1 {
return nil
}
return gui.State.Branches[selectedLine]
}
// may want to standardise how these select methods work
func (gui *Gui) handleBranchSelect(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
gui.State.SplitMainPanel = false
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
gui.getMainView().Title = "Log"
// This really shouldn't happen: there should always be a master branch
if len(gui.State.Branches) == 0 {
return gui.newStringTask("main", gui.Tr.SLocalize("NoBranchesThisRepo"))
}
branch := gui.getSelectedBranch()
if err := gui.focusPoint(0, gui.State.Panels.Branches.SelectedLine, len(gui.State.Branches), v); err != nil {
return err
}
if err := gui.RenderSelectedBranchUpstreamDifferences(); err != nil {
return err
}
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.GetBranchGraphCmdStr(branch.Name),
)
if err := gui.newCmdTask("main", cmd); err != nil {
gui.Log.Error(err)
}
return nil
}
func (gui *Gui) RenderSelectedBranchUpstreamDifferences() error {
return gui.newTask("branches", func(stop chan struct{}) error {
branch := gui.getSelectedBranch()
branch.Pushables, branch.Pullables = gui.GitCommand.GetBranchUpstreamDifferenceCount(branch.Name)
select {
case <-stop:
return nil
default:
}
branchesView := gui.getBranchesView()
displayStrings := presentation.GetBranchListDisplayStrings(gui.State.Branches, gui.currentViewName() == "branches", gui.State.Panels.Branches.SelectedLine)
gui.renderDisplayStrings(branchesView, displayStrings)
return nil
})
}
// gui.refreshStatus is called at the end of this because that's when we can
// be sure there is a state.Branches array to pick the current branch from
func (gui *Gui) refreshBranches(g *gocui.Gui) error {
if err := gui.refreshRemotes(); err != nil {
return err
}
if err := gui.refreshTags(); err != nil {
return err
}
g.Update(func(g *gocui.Gui) error {
builder, err := commands.NewBranchListBuilder(gui.Log, gui.GitCommand)
if err != nil {
return err
}
gui.State.Branches = builder.Build()
// TODO: if we're in the remotes view and we've just deleted a remote we need to refresh accordingly
if gui.getBranchesView().Context == "local-branches" {
if err := gui.renderLocalBranchesWithSelection(); err != nil {
return err
}
}
return gui.refreshStatus(g)
})
return nil
}
func (gui *Gui) renderLocalBranchesWithSelection() error {
branchesView := gui.getBranchesView()
gui.refreshSelectedLine(&gui.State.Panels.Branches.SelectedLine, len(gui.State.Branches))
if err := gui.RenderSelectedBranchUpstreamDifferences(); err != nil {
return err
}
if gui.g.CurrentView() == branchesView {
if err := gui.handleBranchSelect(gui.g, branchesView); err != nil {
return err
}
}
return nil
}
// specific functions
func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error {
if gui.State.Panels.Branches.SelectedLine == -1 {
return nil
}
if gui.State.Panels.Branches.SelectedLine == 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("AlreadyCheckedOutBranch"))
}
branch := gui.getSelectedBranch()
return gui.handleCheckoutRef(branch.Name)
}
func (gui *Gui) handleCreatePullRequestPress(g *gocui.Gui, v *gocui.View) error {
pullRequest := commands.NewPullRequest(gui.GitCommand)
branch := gui.getSelectedBranch()
if err := pullRequest.Create(branch); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return nil
}
func (gui *Gui) handleGitFetch(g *gocui.Gui, v *gocui.View) error {
if err := gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("FetchWait")); err != nil {
return err
}
go func() {
unamePassOpend, err := gui.fetch(g, v, true)
gui.HandleCredentialsPopup(g, unamePassOpend, err)
}()
return nil
}
func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
branch := gui.getSelectedBranch()
message := gui.Tr.SLocalize("SureForceCheckout")
title := gui.Tr.SLocalize("ForceCheckoutBranch")
return gui.createConfirmationPanel(g, v, true, title, message, func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.Checkout(branch.Name, true); err != nil {
gui.createErrorPanel(g, err.Error())
}
return gui.refreshSidePanels(g)
}, nil)
}
func (gui *Gui) handleCheckoutRef(ref string) error {
if err := gui.GitCommand.Checkout(ref, false); err != nil {
// note, this will only work for english-language git commands. If we force git to use english, and the error isn't this one, then the user will receive an english command they may not understand. I'm not sure what the best solution to this is. Running the command once in english and a second time in the native language is one option
if strings.Contains(err.Error(), "Please commit your changes or stash them before you switch branch") {
// offer to autostash changes
return gui.createConfirmationPanel(gui.g, gui.getBranchesView(), true, gui.Tr.SLocalize("AutoStashTitle"), gui.Tr.SLocalize("AutoStashPrompt"), func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.StashSave(gui.Tr.SLocalize("StashPrefix") + ref); err != nil {
return gui.createErrorPanel(g, err.Error())
}
if err := gui.GitCommand.Checkout(ref, false); err != nil {
return gui.createErrorPanel(g, err.Error())
}
// checkout successful so we select the new branch
gui.State.Panels.Branches.SelectedLine = 0
if err := gui.GitCommand.StashDo(0, "pop"); err != nil {
if err := gui.refreshSidePanels(g); err != nil {
return err
}
return gui.createErrorPanel(g, err.Error())
}
return gui.refreshSidePanels(g)
}, nil)
}
if err := gui.createErrorPanel(gui.g, err.Error()); err != nil {
return err
}
}
gui.State.Panels.Branches.SelectedLine = 0
gui.State.Panels.Commits.SelectedLine = 0
return gui.refreshSidePanels(gui.g)
}
func (gui *Gui) handleCheckoutByName(g *gocui.Gui, v *gocui.View) error {
gui.createPromptPanel(g, v, gui.Tr.SLocalize("BranchName")+":", "", func(g *gocui.Gui, v *gocui.View) error {
return gui.handleCheckoutRef(gui.trimmedContent(v))
})
return nil
}
func (gui *Gui) getCheckedOutBranch() *commands.Branch {
if len(gui.State.Branches) == 0 {
return nil
}
return gui.State.Branches[0]
}
func (gui *Gui) handleNewBranch(g *gocui.Gui, v *gocui.View) error {
branch := gui.getCheckedOutBranch()
message := gui.Tr.TemplateLocalize(
"NewBranchNameBranchOff",
Teml{
"branchName": branch.Name,
},
)
gui.createPromptPanel(g, v, message, "", func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.NewBranch(gui.trimmedContent(v)); err != nil {
return gui.createErrorPanel(g, err.Error())
}
gui.refreshSidePanels(g)
return gui.handleBranchSelect(g, v)
})
return nil
}
func (gui *Gui) handleDeleteBranch(g *gocui.Gui, v *gocui.View) error {
return gui.deleteBranch(g, v, false)
}
func (gui *Gui) handleForceDeleteBranch(g *gocui.Gui, v *gocui.View) error {
return gui.deleteBranch(g, v, true)
}
func (gui *Gui) deleteBranch(g *gocui.Gui, v *gocui.View, force bool) error {
selectedBranch := gui.getSelectedBranch()
if selectedBranch == nil {
return nil
}
checkedOutBranch := gui.getCheckedOutBranch()
if checkedOutBranch.Name == selectedBranch.Name {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantDeleteCheckOutBranch"))
}
return gui.deleteNamedBranch(g, v, selectedBranch, force)
}
func (gui *Gui) deleteNamedBranch(g *gocui.Gui, v *gocui.View, selectedBranch *commands.Branch, force bool) error {
title := gui.Tr.SLocalize("DeleteBranch")
var messageID string
if force {
messageID = "ForceDeleteBranchMessage"
} else {
messageID = "DeleteBranchMessage"
}
message := gui.Tr.TemplateLocalize(
messageID,
Teml{
"selectedBranchName": selectedBranch.Name,
},
)
return gui.createConfirmationPanel(g, v, true, title, message, func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.DeleteBranch(selectedBranch.Name, force); err != nil {
errMessage := err.Error()
if !force && strings.Contains(errMessage, "is not fully merged") {
return gui.deleteNamedBranch(g, v, selectedBranch, true)
}
return gui.createErrorPanel(g, errMessage)
}
return gui.refreshSidePanels(g)
}, nil)
}
func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error {
if gui.GitCommand.IsHeadDetached() {
return gui.createErrorPanel(gui.g, "Cannot merge branch in detached head state. You might have checked out a commit directly or a remote branch, in which case you should checkout the local branch you want to be on")
}
checkedOutBranchName := gui.getCheckedOutBranch().Name
if checkedOutBranchName == branchName {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("CantMergeBranchIntoItself"))
}
prompt := gui.Tr.TemplateLocalize(
"ConfirmMerge",
Teml{
"checkedOutBranch": checkedOutBranchName,
"selectedBranch": branchName,
},
)
return gui.createConfirmationPanel(gui.g, gui.getBranchesView(), true, gui.Tr.SLocalize("MergingTitle"), prompt,
func(g *gocui.Gui, v *gocui.View) error {
err := gui.GitCommand.Merge(branchName)
return gui.handleGenericMergeCommandResult(err)
}, nil)
}
func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error {
selectedBranchName := gui.getSelectedBranch().Name
return gui.mergeBranchIntoCheckedOutBranch(selectedBranchName)
}
func (gui *Gui) handleRebaseOntoLocalBranch(g *gocui.Gui, v *gocui.View) error {
selectedBranchName := gui.getSelectedBranch().Name
return gui.handleRebaseOntoBranch(selectedBranchName)
}
func (gui *Gui) handleRebaseOntoBranch(selectedBranchName string) error {
checkedOutBranch := gui.getCheckedOutBranch().Name
if selectedBranchName == checkedOutBranch {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("CantRebaseOntoSelf"))
}
prompt := gui.Tr.TemplateLocalize(
"ConfirmRebase",
Teml{
"checkedOutBranch": checkedOutBranch,
"selectedBranch": selectedBranchName,
},
)
return gui.createConfirmationPanel(gui.g, gui.getBranchesView(), true, gui.Tr.SLocalize("RebasingTitle"), prompt,
func(g *gocui.Gui, v *gocui.View) error {
err := gui.GitCommand.RebaseBranch(selectedBranchName)
return gui.handleGenericMergeCommandResult(err)
}, nil)
}
func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
branch := gui.getSelectedBranch()
if branch == nil {
return nil
}
if branch.Pushables == "" {
return nil
}
if branch.Pushables == "?" {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("FwdNoUpstream"))
}
if branch.Pushables != "0" {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("FwdCommitsToPush"))
}
upstream, err := gui.GitCommand.GetUpstreamForBranch(branch.Name)
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
split := strings.Split(upstream, "/")
remoteName := split[0]
remoteBranchName := strings.Join(split[1:], "/")
message := gui.Tr.TemplateLocalize(
"Fetching",
Teml{
"from": fmt.Sprintf("%s/%s", remoteName, remoteBranchName),
"to": branch.Name,
},
)
go func() {
_ = gui.createLoaderPanel(gui.g, v, message)
if gui.State.Panels.Branches.SelectedLine == 0 {
if err := gui.GitCommand.PullWithoutPasswordCheck("--ff-only"); err != nil {
_ = gui.createErrorPanel(gui.g, err.Error())
}
_ = gui.refreshSidePanels(gui.g)
} else {
if err := gui.GitCommand.FastForward(branch.Name, remoteName, remoteBranchName); err != nil {
_ = gui.createErrorPanel(gui.g, err.Error())
}
_ = gui.RenderSelectedBranchUpstreamDifferences()
}
_ = gui.closeConfirmationPrompt(gui.g, true)
}()
return nil
}
func (gui *Gui) onBranchesTabClick(tabIndex int) error {
contexts := []string{"local-branches", "remotes", "tags"}
branchesView := gui.getBranchesView()
branchesView.TabIndex = tabIndex
return gui.switchBranchesPanelContext(contexts[tabIndex])
}
func (gui *Gui) switchBranchesPanelContext(context string) error {
branchesView := gui.getBranchesView()
branchesView.Context = context
gui.onSearchEscape()
contextTabIndexMap := map[string]int{
"local-branches": 0,
"remotes": 1,
"remote-branches": 1,
"tags": 2,
}
branchesView.TabIndex = contextTabIndexMap[context]
switch context {
case "local-branches":
return gui.renderLocalBranchesWithSelection()
case "remotes":
return gui.renderRemotesWithSelection()
case "remote-branches":
return gui.renderRemoteBranchesWithSelection()
case "tags":
return gui.renderTagsWithSelection()
}
return nil
}
func (gui *Gui) handleNextBranchesTab(g *gocui.Gui, v *gocui.View) error {
return gui.onBranchesTabClick(
utils.ModuloWithWrap(v.TabIndex+1, len(v.Tabs)),
)
}
func (gui *Gui) handlePrevBranchesTab(g *gocui.Gui, v *gocui.View) error {
return gui.onBranchesTabClick(
utils.ModuloWithWrap(v.TabIndex-1, len(v.Tabs)),
)
}
func (gui *Gui) handleCreateResetToBranchMenu(g *gocui.Gui, v *gocui.View) error {
branch := gui.getSelectedBranch()
if branch == nil {
return nil
}
return gui.createResetMenu(branch.Name)
}
func (gui *Gui) onBranchesPanelSearchSelect(selectedLine int) error {
branchesView := gui.getBranchesView()
switch branchesView.Context {
case "local-branches":
gui.State.Panels.Branches.SelectedLine = selectedLine
return gui.handleBranchSelect(gui.g, branchesView)
case "remotes":
gui.State.Panels.Remotes.SelectedLine = selectedLine
return gui.handleRemoteSelect(gui.g, branchesView)
case "remote-branches":
gui.State.Panels.RemoteBranches.SelectedLine = selectedLine
return gui.handleRemoteBranchSelect(gui.g, branchesView)
}
return nil
}

View File

@@ -0,0 +1,220 @@
package gui
import (
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
)
func (gui *Gui) getSelectedCommitFile(g *gocui.Gui) *commands.CommitFile {
selectedLine := gui.State.Panels.CommitFiles.SelectedLine
if selectedLine == -1 {
return nil
}
return gui.State.CommitFiles[selectedLine]
}
func (gui *Gui) handleCommitFilesClick(g *gocui.Gui, v *gocui.View) error {
itemCount := len(gui.State.CommitFiles)
handleSelect := gui.handleCommitFileSelect
selectedLine := &gui.State.Panels.CommitFiles.SelectedLine
return gui.handleClick(v, itemCount, selectedLine, handleSelect)
}
func (gui *Gui) handleCommitFileSelect(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
gui.getMainView().Title = "Patch"
if gui.currentViewName() == "commitFiles" {
gui.handleEscapeLineByLinePanel()
}
commitFile := gui.getSelectedCommitFile(g)
if commitFile == nil {
return gui.renderString(g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
}
if err := gui.refreshSecondaryPatchPanel(); err != nil {
return err
}
if err := gui.focusPoint(0, gui.State.Panels.CommitFiles.SelectedLine, len(gui.State.CommitFiles), v); err != nil {
return err
}
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowCommitFileCmdStr(commitFile.Sha, commitFile.Name, false),
)
if err := gui.newPtyTask("main", cmd); err != nil {
gui.Log.Error(err)
}
return nil
}
func (gui *Gui) handleSwitchToCommitsPanel(g *gocui.Gui, v *gocui.View) error {
return gui.switchFocus(g, v, gui.getCommitsView())
}
func (gui *Gui) handleCheckoutCommitFile(g *gocui.Gui, v *gocui.View) error {
file := gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLine]
if err := gui.GitCommand.CheckoutFile(file.Sha, file.Name); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshFiles()
}
func (gui *Gui) handleDiscardOldFileChange(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
return err
}
fileName := gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLine].Name
return gui.createConfirmationPanel(gui.g, v, true, gui.Tr.SLocalize("DiscardFileChangesTitle"), gui.Tr.SLocalize("DiscardFileChangesPrompt"), func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
if err := gui.GitCommand.DiscardOldFileChanges(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, fileName); err != nil {
if err := gui.handleGenericMergeCommandResult(err); err != nil {
return err
}
}
return gui.refreshSidePanels(gui.g)
})
}, nil)
}
func (gui *Gui) refreshCommitFilesView() error {
if err := gui.refreshSecondaryPatchPanel(); err != nil {
return err
}
if err := gui.refreshPatchBuildingPanel(-1); err != nil {
return err
}
commit := gui.getSelectedCommit(gui.g)
if commit == nil {
return nil
}
files, err := gui.GitCommand.GetCommitFiles(commit.Sha, gui.GitCommand.PatchManager)
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
gui.State.CommitFiles = files
gui.refreshSelectedLine(&gui.State.Panels.CommitFiles.SelectedLine, len(gui.State.CommitFiles))
commitsFileView := gui.getCommitFilesView()
displayStrings := presentation.GetCommitFileListDisplayStrings(gui.State.CommitFiles)
gui.renderDisplayStrings(commitsFileView, displayStrings)
return gui.handleCommitFileSelect(gui.g, commitsFileView)
}
func (gui *Gui) handleOpenOldCommitFile(g *gocui.Gui, v *gocui.View) error {
file := gui.getSelectedCommitFile(g)
return gui.openFile(file.Name)
}
func (gui *Gui) handleToggleFileForPatch(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
return err
}
commitFile := gui.getSelectedCommitFile(g)
if commitFile == nil {
return gui.renderString(g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
}
toggleTheFile := func() error {
if !gui.GitCommand.PatchManager.CommitSelected() {
if err := gui.startPatchManager(); err != nil {
return err
}
}
gui.GitCommand.PatchManager.ToggleFileWhole(commitFile.Name)
return gui.refreshCommitFilesView()
}
if gui.GitCommand.PatchManager.CommitSelected() && gui.GitCommand.PatchManager.CommitSha != commitFile.Sha {
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("DiscardPatch"), gui.Tr.SLocalize("DiscardPatchConfirm"), func(g *gocui.Gui, v *gocui.View) error {
gui.GitCommand.PatchManager.Reset()
return toggleTheFile()
}, nil)
}
return toggleTheFile()
}
func (gui *Gui) startPatchManager() error {
diffMap := map[string]string{}
for _, commitFile := range gui.State.CommitFiles {
commitText, err := gui.GitCommand.ShowCommitFile(commitFile.Sha, commitFile.Name, true)
if err != nil {
return err
}
diffMap[commitFile.Name] = commitText
}
commit := gui.getSelectedCommit(gui.g)
if commit == nil {
return errors.New("No commit selected")
}
gui.GitCommand.PatchManager.Start(commit.Sha, diffMap)
return nil
}
func (gui *Gui) handleEnterCommitFile(g *gocui.Gui, v *gocui.View) error {
return gui.enterCommitFile(-1)
}
func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
return err
}
commitFile := gui.getSelectedCommitFile(gui.g)
if commitFile == nil {
return gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
}
enterTheFile := func(selectedLineIdx int) error {
if !gui.GitCommand.PatchManager.CommitSelected() {
if err := gui.startPatchManager(); err != nil {
return err
}
}
gui.changeMainViewsContext("patch-building")
if err := gui.switchFocus(gui.g, gui.getCommitFilesView(), gui.getMainView()); err != nil {
return err
}
return gui.refreshPatchBuildingPanel(selectedLineIdx)
}
if gui.GitCommand.PatchManager.CommitSelected() && gui.GitCommand.PatchManager.CommitSha != commitFile.Sha {
return gui.createConfirmationPanel(gui.g, gui.getCommitFilesView(), false, gui.Tr.SLocalize("DiscardPatch"), gui.Tr.SLocalize("DiscardPatchConfirm"), func(g *gocui.Gui, v *gocui.View) error {
gui.GitCommand.PatchManager.Reset()
return enterTheFile(selectedLineIdx)
}, nil)
}
return enterTheFile(selectedLineIdx)
}
func (gui *Gui) onCommitFilesPanelSearchSelect(selectedLine int) error {
gui.State.Panels.CommitFiles.SelectedLine = selectedLine
return gui.handleCommitFileSelect(gui.g, gui.getCommitFilesView())
}

View File

@@ -0,0 +1,120 @@
package gui
import (
"os/exec"
"strconv"
"strings"
"github.com/jesseduffield/gocui"
)
// runSyncOrAsyncCommand takes the output of a command that may have returned
// either no error, an error, or a subprocess to execute, and if a subprocess
// needs to be set on the gui object, it does so, and then returns the error
// the bool returned tells us whether the calling code should continue
func (gui *Gui) runSyncOrAsyncCommand(sub *exec.Cmd, err error) (bool, error) {
if err != nil {
if err != gui.Errors.ErrSubProcess {
return false, gui.createErrorPanel(gui.g, err.Error())
}
}
if sub != nil {
gui.SubProcess = sub
return false, gui.Errors.ErrSubProcess
}
return true, nil
}
func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
message := gui.trimmedContent(v)
if message == "" {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CommitWithoutMessageErr"))
}
flags := ""
skipHookPrefix := gui.Config.GetUserConfig().GetString("git.skipHookPrefix")
if skipHookPrefix != "" && strings.HasPrefix(message, skipHookPrefix) {
flags = "--no-verify"
}
ok, err := gui.runSyncOrAsyncCommand(gui.GitCommand.Commit(message, flags))
if err != nil {
return err
}
if !ok {
return nil
}
v.Clear()
_ = v.SetCursor(0, 0)
_ = v.SetOrigin(0, 0)
_, _ = g.SetViewOnBottom("commitMessage")
_ = gui.switchFocus(g, v, gui.getFilesView())
return gui.refreshSidePanels(g)
}
func (gui *Gui) handleCommitClose(g *gocui.Gui, v *gocui.View) error {
g.SetViewOnBottom("commitMessage")
return gui.switchFocus(g, v, gui.getFilesView())
}
func (gui *Gui) handleCommitFocused(g *gocui.Gui, v *gocui.View) error {
if _, err := g.SetViewOnTop("commitMessage"); err != nil {
return err
}
message := gui.Tr.TemplateLocalize(
"CloseConfirm",
Teml{
"keyBindClose": "esc",
"keyBindConfirm": "enter",
},
)
return gui.renderString(g, "options", message)
}
func (gui *Gui) getBufferLength(view *gocui.View) string {
return " " + strconv.Itoa(strings.Count(view.Buffer(), "")-1) + " "
}
// RenderCommitLength is a function.
func (gui *Gui) RenderCommitLength() {
if !gui.Config.GetUserConfig().GetBool("gui.commitLength.show") {
return
}
v := gui.getCommitMessageView()
v.Subtitle = gui.getBufferLength(v)
}
// we've just copy+pasted the editor from gocui to here so that we can also re-
// render the commit message length on each keypress
func (gui *Gui) commitMessageEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
switch {
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
v.EditDelete(true)
case key == gocui.KeyDelete:
v.EditDelete(false)
case key == gocui.KeyArrowDown:
v.MoveCursor(0, 1, false)
case key == gocui.KeyArrowUp:
v.MoveCursor(0, -1, false)
case key == gocui.KeyArrowLeft:
v.MoveCursor(-1, 0, false)
case key == gocui.KeyArrowRight:
v.MoveCursor(1, 0, false)
case key == gocui.KeyTab:
v.EditNewLine()
case key == gocui.KeySpace:
v.EditWrite(' ')
case key == gocui.KeyInsert:
v.Overwrite = !v.Overwrite
case key == gocui.KeyCtrlU:
v.EditDeleteToStartOfLine()
case key == gocui.KeyCtrlA:
v.EditGotoToStartOfLine()
case key == gocui.KeyCtrlE:
v.EditGotoToEndOfLine()
default:
v.EditWrite(ch)
}
gui.RenderCommitLength()
}

695
pkg/gui/commits_panel.go Normal file
View File

@@ -0,0 +1,695 @@
package gui
import (
"strconv"
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// list panel functions
func (gui *Gui) getSelectedCommit(g *gocui.Gui) *commands.Commit {
selectedLine := gui.State.Panels.Commits.SelectedLine
if selectedLine == -1 {
return nil
}
return gui.State.Commits[selectedLine]
}
func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
// this probably belongs in an 'onFocus' function than a 'commit selected' function
if err := gui.refreshSecondaryPatchPanel(); err != nil {
return err
}
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
state := gui.State.Panels.Commits
if state.SelectedLine > 20 && state.LimitCommits {
state.LimitCommits = false
go func() {
if err := gui.refreshCommitsWithLimit(); err != nil {
_ = gui.createErrorPanel(gui.g, err.Error())
}
}()
}
gui.getMainView().Title = "Patch"
gui.getSecondaryView().Title = "Custom Patch"
gui.handleEscapeLineByLinePanel()
commit := gui.getSelectedCommit(g)
if commit == nil {
return gui.newStringTask("main", gui.Tr.SLocalize("NoCommitsThisBranch"))
}
if err := gui.focusPoint(0, gui.State.Panels.Commits.SelectedLine, len(gui.State.Commits), v); err != nil {
return err
}
// if specific diff mode is on, don't show diff
if gui.State.Panels.Commits.SpecificDiffMode {
return nil
}
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowCmdStr(commit.Sha),
)
if err := gui.newPtyTask("main", cmd); err != nil {
gui.Log.Error(err)
}
return nil
}
func (gui *Gui) refreshCommits(g *gocui.Gui) error {
g.Update(func(*gocui.Gui) error {
// I think this is here for the sake of some kind of rebasing thing
gui.refreshStatus(g)
if err := gui.refreshCommitsWithLimit(); err != nil {
return err
}
// doing this async because it shouldn't hold anything up
go func() {
if err := gui.refreshReflogCommits(); err != nil {
_ = gui.createErrorPanel(gui.g, err.Error())
}
}()
if g.CurrentView() == gui.getCommitFilesView() || (g.CurrentView() == gui.getMainView() || gui.State.MainContext == "patch-building") {
return gui.refreshCommitFilesView()
}
return nil
})
return nil
}
func (gui *Gui) refreshCommitsWithLimit() error {
builder, err := commands.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr, gui.State.CherryPickedCommits, gui.State.DiffEntries)
if err != nil {
return err
}
commits, err := builder.GetCommits(gui.State.Panels.Commits.LimitCommits)
if err != nil {
return err
}
gui.State.Commits = commits
if gui.getCommitsView().Context == "branch-commits" {
if err := gui.renderBranchCommitsWithSelection(); err != nil {
return err
}
}
return nil
}
// specific functions
func (gui *Gui) handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error {
return gui.createConfirmationPanel(g, commitView, true, gui.Tr.SLocalize("ResetToCommit"), gui.Tr.SLocalize("SureResetThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedCommit(g)
if commit == nil {
panic(errors.New(gui.Tr.SLocalize("NoCommitsThisBranch")))
}
if err := gui.GitCommand.ResetToCommit(commit.Sha, "mixed"); err != nil {
return gui.createErrorPanel(g, err.Error())
}
if err := gui.refreshCommits(g); err != nil {
panic(err)
}
if err := gui.refreshFiles(); err != nil {
panic(err)
}
gui.resetOrigin(commitView)
gui.State.Panels.Commits.SelectedLine = 0
return gui.handleCommitSelect(g, commitView)
}, nil)
}
func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
if len(gui.State.Commits) <= 1 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash"))
}
applied, err := gui.handleMidRebaseCommand("squash")
if err != nil {
return err
}
if applied {
return nil
}
gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("Squash"), gui.Tr.SLocalize("SureSquashThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("SquashingStatus"), func() error {
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "squash")
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
return nil
}
// TODO: move to files panel
func (gui *Gui) anyUnStagedChanges(files []*commands.File) bool {
for _, file := range files {
if file.Tracked && file.HasUnstagedChanges {
return true
}
}
return false
}
func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
if len(gui.State.Commits) <= 1 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash"))
}
applied, err := gui.handleMidRebaseCommand("fixup")
if err != nil {
return err
}
if applied {
return nil
}
gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("Fixup"), gui.Tr.SLocalize("SureFixupThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("FixingStatus"), func() error {
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "fixup")
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
return nil
}
func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
applied, err := gui.handleMidRebaseCommand("reword")
if err != nil {
return err
}
if applied {
return nil
}
if gui.State.Panels.Commits.SelectedLine != 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlyRenameTopCommit"))
}
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("renameCommit"), "", func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.RenameCommit(v.Buffer()); err != nil {
return gui.createErrorPanel(g, err.Error())
}
if err := gui.refreshCommits(g); err != nil {
panic(err)
}
return gui.handleCommitSelect(g, v)
})
}
func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
applied, err := gui.handleMidRebaseCommand("reword")
if err != nil {
return err
}
if applied {
return nil
}
subProcess, err := gui.GitCommand.RewordCommit(gui.State.Commits, gui.State.Panels.Commits.SelectedLine)
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
if subProcess != nil {
gui.SubProcess = subProcess
return gui.Errors.ErrSubProcess
}
return nil
}
// handleMidRebaseCommand sees if the selected commit is in fact a rebasing
// commit meaning you are trying to edit the todo file rather than actually
// begin a rebase. It then updates the todo file with that action
func (gui *Gui) handleMidRebaseCommand(action string) (bool, error) {
selectedCommit := gui.State.Commits[gui.State.Panels.Commits.SelectedLine]
if selectedCommit.Status != "rebasing" {
return false, nil
}
// for now we do not support setting 'reword' because it requires an editor
// and that means we either unconditionally wait around for the subprocess to ask for
// our input or we set a lazygit client as the EDITOR env variable and have it
// request us to edit the commit message when prompted.
if action == "reword" {
return true, gui.createErrorPanel(gui.g, gui.Tr.SLocalize("rewordNotSupported"))
}
if err := gui.GitCommand.EditRebaseTodo(gui.State.Panels.Commits.SelectedLine, action); err != nil {
return false, gui.createErrorPanel(gui.g, err.Error())
}
return true, gui.refreshCommits(gui.g)
}
// handleMoveTodoDown like handleMidRebaseCommand but for moving an item up in the todo list
func (gui *Gui) handleMoveTodoDown(index int) (bool, error) {
selectedCommit := gui.State.Commits[index]
if selectedCommit.Status != "rebasing" {
return false, nil
}
if gui.State.Commits[index+1].Status != "rebasing" {
return true, nil
}
if err := gui.GitCommand.MoveTodoDown(index); err != nil {
return true, gui.createErrorPanel(gui.g, err.Error())
}
return true, gui.refreshCommits(gui.g)
}
func (gui *Gui) handleCommitDelete(g *gocui.Gui, v *gocui.View) error {
applied, err := gui.handleMidRebaseCommand("drop")
if err != nil {
return err
}
if applied {
return nil
}
return gui.createConfirmationPanel(gui.g, v, true, gui.Tr.SLocalize("DeleteCommitTitle"), gui.Tr.SLocalize("DeleteCommitPrompt"), func(*gocui.Gui, *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("DeletingStatus"), func() error {
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "drop")
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
}
func (gui *Gui) handleCommitMoveDown(g *gocui.Gui, v *gocui.View) error {
index := gui.State.Panels.Commits.SelectedLine
selectedCommit := gui.State.Commits[index]
if selectedCommit.Status == "rebasing" {
if gui.State.Commits[index+1].Status != "rebasing" {
return nil
}
if err := gui.GitCommand.MoveTodoDown(index); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
gui.State.Panels.Commits.SelectedLine++
return gui.refreshCommits(gui.g)
}
return gui.WithWaitingStatus(gui.Tr.SLocalize("MovingStatus"), func() error {
err := gui.GitCommand.MoveCommitDown(gui.State.Commits, index)
if err == nil {
gui.State.Panels.Commits.SelectedLine++
}
return gui.handleGenericMergeCommandResult(err)
})
}
func (gui *Gui) handleCommitMoveUp(g *gocui.Gui, v *gocui.View) error {
index := gui.State.Panels.Commits.SelectedLine
if index == 0 {
return nil
}
selectedCommit := gui.State.Commits[index]
if selectedCommit.Status == "rebasing" {
if err := gui.GitCommand.MoveTodoDown(index - 1); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
gui.State.Panels.Commits.SelectedLine--
return gui.refreshCommits(gui.g)
}
return gui.WithWaitingStatus(gui.Tr.SLocalize("MovingStatus"), func() error {
err := gui.GitCommand.MoveCommitDown(gui.State.Commits, index-1)
if err == nil {
gui.State.Panels.Commits.SelectedLine--
}
return gui.handleGenericMergeCommandResult(err)
})
}
func (gui *Gui) handleCommitEdit(g *gocui.Gui, v *gocui.View) error {
applied, err := gui.handleMidRebaseCommand("edit")
if err != nil {
return err
}
if applied {
return nil
}
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
err = gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "edit")
return gui.handleGenericMergeCommandResult(err)
})
}
func (gui *Gui) handleCommitAmendTo(g *gocui.Gui, v *gocui.View) error {
return gui.createConfirmationPanel(gui.g, v, true, gui.Tr.SLocalize("AmendCommitTitle"), gui.Tr.SLocalize("AmendCommitPrompt"), func(*gocui.Gui, *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("AmendingStatus"), func() error {
err := gui.GitCommand.AmendTo(gui.State.Commits[gui.State.Panels.Commits.SelectedLine].Sha)
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
}
func (gui *Gui) handleCommitPick(g *gocui.Gui, v *gocui.View) error {
applied, err := gui.handleMidRebaseCommand("pick")
if err != nil {
return err
}
if applied {
return nil
}
// at this point we aren't actually rebasing so we will interpret this as an
// attempt to pull. We might revoke this later after enabling configurable keybindings
return gui.handlePullFiles(g, v)
}
func (gui *Gui) handleCommitRevert(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.Revert(gui.State.Commits[gui.State.Panels.Commits.SelectedLine].Sha); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
gui.State.Panels.Commits.SelectedLine++
return gui.refreshCommits(gui.g)
}
func (gui *Gui) handleCopyCommit(g *gocui.Gui, v *gocui.View) error {
// get currently selected commit, add the sha to state.
commit := gui.State.Commits[gui.State.Panels.Commits.SelectedLine]
// we will un-copy it if it's already copied
for index, cherryPickedCommit := range gui.State.CherryPickedCommits {
if commit.Sha == cherryPickedCommit.Sha {
gui.State.CherryPickedCommits = append(gui.State.CherryPickedCommits[0:index], gui.State.CherryPickedCommits[index+1:]...)
return gui.refreshCommits(gui.g)
}
}
gui.addCommitToCherryPickedCommits(gui.State.Panels.Commits.SelectedLine)
return gui.refreshCommits(gui.g)
}
func (gui *Gui) addCommitToCherryPickedCommits(index int) {
// not super happy with modifying the state of the Commits array here
// but the alternative would be very tricky
gui.State.Commits[index].Copied = true
newCommits := []*commands.Commit{}
for _, commit := range gui.State.Commits {
if commit.Copied {
// duplicating just the things we need to put in the rebase TODO list
newCommits = append(newCommits, &commands.Commit{Name: commit.Name, Sha: commit.Sha})
}
}
gui.State.CherryPickedCommits = newCommits
}
func (gui *Gui) handleCopyCommitRange(g *gocui.Gui, v *gocui.View) error {
// whenever I add a commit, I need to make sure I retain its order
// find the last commit that is copied that's above our position
// if there are none, startIndex = 0
startIndex := 0
for index, commit := range gui.State.Commits[0:gui.State.Panels.Commits.SelectedLine] {
if commit.Copied {
startIndex = index
}
}
gui.Log.Info("commit copy start index: " + strconv.Itoa(startIndex))
for index := startIndex; index <= gui.State.Panels.Commits.SelectedLine; index++ {
gui.addCommitToCherryPickedCommits(index)
}
return gui.refreshCommits(gui.g)
}
// HandlePasteCommits begins a cherry-pick rebase with the commits the user has copied
func (gui *Gui) HandlePasteCommits(g *gocui.Gui, v *gocui.View) error {
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("CherryPick"), gui.Tr.SLocalize("SureCherryPick"), func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("CherryPickingStatus"), func() error {
err := gui.GitCommand.CherryPickCommits(gui.State.CherryPickedCommits)
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
}
func (gui *Gui) handleSwitchToCommitFilesPanel(g *gocui.Gui, v *gocui.View) error {
if err := gui.refreshCommitFilesView(); err != nil {
return err
}
return gui.switchFocus(g, gui.getCommitsView(), gui.getCommitFilesView())
}
func (gui *Gui) handleToggleDiffCommit(g *gocui.Gui, v *gocui.View) error {
selectLimit := 2
// get selected commit
commit := gui.getSelectedCommit(g)
if commit == nil {
return gui.newStringTask("main", gui.Tr.SLocalize("NoCommitsThisBranch"))
}
// if already selected commit delete
if idx, has := gui.hasCommit(gui.State.DiffEntries, commit.Sha); has {
gui.State.DiffEntries = gui.unchooseCommit(gui.State.DiffEntries, idx)
} else {
if len(gui.State.DiffEntries) == selectLimit {
gui.State.DiffEntries = gui.unchooseCommit(gui.State.DiffEntries, 0)
}
gui.State.DiffEntries = append(gui.State.DiffEntries, commit)
}
gui.setDiffMode()
// if selected two commits, display diff between
if len(gui.State.DiffEntries) == selectLimit {
commitText, err := gui.GitCommand.DiffCommits(gui.State.DiffEntries[0].Sha, gui.State.DiffEntries[1].Sha)
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.newStringTask("main", commitText)
}
return nil
}
func (gui *Gui) setDiffMode() {
v := gui.getCommitsView()
if len(gui.State.DiffEntries) != 0 {
gui.State.Panels.Commits.SpecificDiffMode = true
v.Title = gui.Tr.SLocalize("CommitsDiffTitle")
} else {
gui.State.Panels.Commits.SpecificDiffMode = false
v.Title = gui.Tr.SLocalize("CommitsTitle")
}
gui.refreshCommits(gui.g)
}
func (gui *Gui) hasCommit(commits []*commands.Commit, target string) (int, bool) {
for idx, commit := range commits {
if commit.Sha == target {
return idx, true
}
}
return -1, false
}
func (gui *Gui) unchooseCommit(commits []*commands.Commit, i int) []*commands.Commit {
return append(commits[:i], commits[i+1:]...)
}
func (gui *Gui) handleCreateFixupCommit(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedCommit(g)
if commit == nil {
return nil
}
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("CreateFixupCommit"), gui.Tr.TemplateLocalize(
"SureCreateFixupCommit",
Teml{
"commit": commit.Sha,
},
), func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.CreateFixupCommit(commit.Sha); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return gui.refreshSidePanels(gui.g)
}, nil)
}
func (gui *Gui) handleSquashAllAboveFixupCommits(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedCommit(g)
if commit == nil {
return nil
}
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("SquashAboveCommits"), gui.Tr.TemplateLocalize(
"SureSquashAboveCommits",
Teml{
"commit": commit.Sha,
},
), func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("SquashingStatus"), func() error {
err := gui.GitCommand.SquashAllAboveFixupCommits(commit.Sha)
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
}
func (gui *Gui) handleTagCommit(g *gocui.Gui, v *gocui.View) error {
// TODO: bring up menu asking if you want to make a lightweight or annotated tag
// if annotated, switch to a subprocess to create the message
commit := gui.getSelectedCommit(g)
if commit == nil {
return nil
}
return gui.handleCreateLightweightTag(commit.Sha)
}
func (gui *Gui) handleCreateLightweightTag(commitSha string) error {
return gui.createPromptPanel(gui.g, gui.getCommitsView(), gui.Tr.SLocalize("TagNameTitle"), "", func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.CreateLightweightTag(v.Buffer(), commitSha); err != nil {
return gui.createErrorPanel(g, err.Error())
}
if err := gui.refreshCommits(g); err != nil {
return gui.createErrorPanel(g, err.Error())
}
if err := gui.refreshTags(); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return gui.handleCommitSelect(g, v)
})
}
func (gui *Gui) handleCheckoutCommit(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedCommit(g)
if commit == nil {
return nil
}
return gui.createConfirmationPanel(g, gui.getCommitsView(), true, gui.Tr.SLocalize("checkoutCommit"), gui.Tr.SLocalize("SureCheckoutThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
return gui.handleCheckoutRef(commit.Sha)
}, nil)
}
func (gui *Gui) renderBranchCommitsWithSelection() error {
commitsView := gui.getCommitsView()
gui.refreshSelectedLine(&gui.State.Panels.Commits.SelectedLine, len(gui.State.Commits))
displayStrings := presentation.GetCommitListDisplayStrings(gui.State.Commits, gui.State.ScreenMode != SCREEN_NORMAL)
gui.renderDisplayStrings(commitsView, displayStrings)
if gui.g.CurrentView() == commitsView && commitsView.Context == "branch-commits" {
if err := gui.handleCommitSelect(gui.g, commitsView); err != nil {
return err
}
}
return nil
}
func (gui *Gui) onCommitsTabClick(tabIndex int) error {
contexts := []string{"branch-commits", "reflog-commits"}
commitsView := gui.getCommitsView()
commitsView.TabIndex = tabIndex
return gui.switchCommitsPanelContext(contexts[tabIndex])
}
func (gui *Gui) switchCommitsPanelContext(context string) error {
commitsView := gui.getCommitsView()
commitsView.Context = context
gui.onSearchEscape()
contextTabIndexMap := map[string]int{
"branch-commits": 0,
"reflog-commits": 1,
}
commitsView.TabIndex = contextTabIndexMap[context]
return gui.refreshCommitsViewWithSelection()
}
func (gui *Gui) refreshCommitsViewWithSelection() error {
commitsView := gui.getCommitsView()
switch commitsView.Context {
case "branch-commits":
return gui.renderBranchCommitsWithSelection()
case "reflog-commits":
return gui.renderReflogCommitsWithSelection()
}
return nil
}
func (gui *Gui) handleNextCommitsTab(g *gocui.Gui, v *gocui.View) error {
return gui.onCommitsTabClick(
utils.ModuloWithWrap(v.TabIndex+1, len(v.Tabs)),
)
}
func (gui *Gui) handlePrevCommitsTab(g *gocui.Gui, v *gocui.View) error {
return gui.onCommitsTabClick(
utils.ModuloWithWrap(v.TabIndex-1, len(v.Tabs)),
)
}
func (gui *Gui) handleCreateCommitResetMenu(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedCommit(g)
if commit == nil {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NoCommitsThisBranch"))
}
return gui.createResetMenu(commit.Sha)
}
func (gui *Gui) onCommitsPanelSearchSelect(selectedLine int) error {
commitsView := gui.getCommitsView()
switch commitsView.Context {
case "branch-commits":
gui.State.Panels.Commits.SelectedLine = selectedLine
return gui.handleCommitSelect(gui.g, commitsView)
case "reflog-commits":
gui.State.Panels.ReflogCommits.SelectedLine = selectedLine
return gui.handleReflogCommitSelect(gui.g, commitsView)
}
return nil
}
func (gui *Gui) handleOpenSearchForCommitsPanel(g *gocui.Gui, v *gocui.View) error {
// we usually lazyload these commits but now that we're searching we need to load them now
if gui.State.Panels.Commits.LimitCommits {
gui.State.Panels.Commits.LimitCommits = false
if err := gui.refreshCommits(gui.g); err != nil {
return err
}
}
return gui.handleOpenSearch(gui.g, v)
}

View File

@@ -0,0 +1,190 @@
// lots of this has been directly ported from one of the example files, will brush up later
// Copyright 2014 The gocui Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gui
import (
"strings"
"time"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/theme"
)
func (gui *Gui) wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.View) error, returnFocusOnClose bool) func(*gocui.Gui, *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
if function != nil {
if err := function(g, v); err != nil {
return err
}
}
return gui.closeConfirmationPrompt(g, returnFocusOnClose)
}
}
func (gui *Gui) closeConfirmationPrompt(g *gocui.Gui, returnFocusOnClose bool) error {
view, err := g.View("confirmation")
if err != nil {
return nil // if it's already been closed we can just return
}
view.Editable = false
if returnFocusOnClose {
if err := gui.returnFocus(g, view); err != nil {
panic(err)
}
}
g.DeleteKeybindings("confirmation")
return g.DeleteView("confirmation")
}
func (gui *Gui) getMessageHeight(wrap bool, message string, width int) int {
lines := strings.Split(message, "\n")
lineCount := 0
// if we need to wrap, calculate height to fit content within view's width
if wrap {
for _, line := range lines {
lineCount += len(line)/width + 1
}
} else {
lineCount = len(lines)
}
return lineCount
}
func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, wrap bool, prompt string) (int, int, int, int) {
width, height := g.Size()
panelWidth := 4 * width / 7
panelHeight := gui.getMessageHeight(wrap, prompt, panelWidth)
return width/2 - panelWidth/2,
height/2 - panelHeight/2 - panelHeight%2 - 1,
width/2 + panelWidth/2,
height/2 + panelHeight/2
}
func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt string, hasLoader bool) (*gocui.View, error) {
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, true, prompt)
confirmationView, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0)
if err != nil {
if err.Error() != "unknown view" {
return nil, err
}
confirmationView.HasLoader = hasLoader
if hasLoader {
gui.g.StartTicking()
}
confirmationView.Title = title
confirmationView.Wrap = true
confirmationView.FgColor = theme.GocuiDefaultTextColor
}
gui.g.Update(func(g *gocui.Gui) error {
return gui.switchFocus(gui.g, currentView, confirmationView)
})
return confirmationView, nil
}
func (gui *Gui) onNewPopupPanel() {
viewNames := []string{"commitMessage",
"credentials",
"menu"}
for _, viewName := range viewNames {
_, _ = gui.g.SetViewOnBottom(viewName)
}
}
func (gui *Gui) createPopupPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, hasLoader bool, returnFocusOnClose bool, editable bool, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
gui.onNewPopupPanel()
g.Update(func(g *gocui.Gui) error {
// delete the existing confirmation panel if it exists
if view, _ := g.View("confirmation"); view != nil {
if err := gui.closeConfirmationPrompt(g, true); err != nil {
gui.Log.Error(err)
}
}
confirmationView, err := gui.prepareConfirmationPanel(currentView, title, prompt, hasLoader)
if err != nil {
return err
}
confirmationView.Editable = editable
if editable {
go func() {
// TODO: remove this wait (right now if you remove it the EditGotoToEndOfLine method doesn't seem to work)
time.Sleep(time.Millisecond)
gui.g.Update(func(g *gocui.Gui) error {
confirmationView.EditGotoToEndOfLine()
return nil
})
}()
}
if err := gui.renderString(g, "confirmation", prompt); err != nil {
return err
}
return gui.setKeyBindings(g, handleConfirm, handleClose, returnFocusOnClose)
})
return nil
}
func (gui *Gui) createLoaderPanel(g *gocui.Gui, currentView *gocui.View, prompt string) error {
return gui.createPopupPanel(g, currentView, "", prompt, true, true, false, nil, nil)
}
// it is very important that within this function we never include the original prompt in any error messages, because it may contain e.g. a user password
func (gui *Gui) createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, returnFocusOnClose bool, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
return gui.createPopupPanel(g, currentView, title, prompt, false, returnFocusOnClose, false, handleConfirm, handleClose)
}
func (gui *Gui) createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, initialContent string, handleConfirm func(*gocui.Gui, *gocui.View) error) error {
return gui.createPopupPanel(gui.g, currentView, title, initialContent, false, true, true, handleConfirm, nil)
}
func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error, returnFocusOnClose bool) error {
actions := gui.Tr.TemplateLocalize(
"CloseConfirm",
Teml{
"keyBindClose": "esc",
"keyBindConfirm": "enter",
},
)
if err := gui.renderString(g, "options", actions); err != nil {
return err
}
if err := g.SetKeybinding("confirmation", nil, gocui.KeyEnter, gocui.ModNone, gui.wrappedConfirmationFunction(handleConfirm, returnFocusOnClose)); err != nil {
return err
}
return g.SetKeybinding("confirmation", nil, gocui.KeyEsc, gocui.ModNone, gui.wrappedConfirmationFunction(handleClose, returnFocusOnClose))
}
func (gui *Gui) createMessagePanel(g *gocui.Gui, currentView *gocui.View, title, prompt string) error {
return gui.createPopupPanel(g, currentView, title, prompt, false, true, false, nil, nil)
}
// createSpecificErrorPanel allows you to create an error popup, specifying the
// view to be focused when the user closes the popup, and a boolean specifying
// whether we will log the error. If the message may include a user password,
// this function is to be used over the more generic createErrorPanel, with
// willLog set to false
func (gui *Gui) createSpecificErrorPanel(message string, nextView *gocui.View, willLog bool) error {
if willLog {
go func() {
// when reporting is switched on this log call sometimes introduces
// a delay on the error panel popping up. Here I'm adding a second wait
// so that the error is logged while the user is reading the error message
time.Sleep(time.Second)
gui.Log.Error(message)
}()
}
colorFunction := color.New(color.FgRed).SprintFunc()
coloredMessage := colorFunction(strings.TrimSpace(message))
return gui.createConfirmationPanel(gui.g, nextView, true, gui.Tr.SLocalize("Error"), coloredMessage, nil, nil)
}
func (gui *Gui) createErrorPanel(g *gocui.Gui, message string) error {
return gui.createSpecificErrorPanel(message, g.CurrentView(), true)
}

20
pkg/gui/context.go Normal file
View File

@@ -0,0 +1,20 @@
package gui
// changeContext is a helper function for when we want to change a 'main' context
// which currently just means a context that affects both the main and secondary views
// other views can have their context changed directly but this function helps
// keep the main and secondary views in sync
func (gui *Gui) changeMainViewsContext(context string) {
if gui.State.MainContext == context {
return
}
switch context {
case "normal", "patch-building", "staging", "merging":
gui.getMainView().Context = context
gui.getSecondaryView().Context = context
}
gui.State.MainContext = context
return
}

View File

@@ -0,0 +1,104 @@
package gui
import (
"strings"
"github.com/jesseduffield/gocui"
)
type credentials chan string
// waitForPassUname wait for a username or password input from the credentials popup
func (gui *Gui) waitForPassUname(g *gocui.Gui, currentView *gocui.View, passOrUname string) string {
gui.credentials = make(chan string)
g.Update(func(g *gocui.Gui) error {
credentialsView, _ := g.View("credentials")
if passOrUname == "username" {
credentialsView.Title = gui.Tr.SLocalize("CredentialsUsername")
credentialsView.Mask = 0
} else {
credentialsView.Title = gui.Tr.SLocalize("CredentialsPassword")
credentialsView.Mask = '*'
}
err := gui.switchFocus(g, currentView, credentialsView)
if err != nil {
return err
}
gui.RenderCommitLength()
return nil
})
// wait for username/passwords input
userInput := <-gui.credentials
return userInput + "\n"
}
func (gui *Gui) handleSubmitCredential(g *gocui.Gui, v *gocui.View) error {
message := gui.trimmedContent(v)
gui.credentials <- message
err := gui.refreshFiles()
if err != nil {
return err
}
v.Clear()
err = v.SetCursor(0, 0)
if err != nil {
return err
}
_, err = g.SetViewOnBottom("credentials")
if err != nil {
return err
}
nextView, err := gui.g.View("confirmation")
if err != nil {
nextView = gui.getFilesView()
}
err = gui.switchFocus(g, nil, nextView)
if err != nil {
return err
}
return gui.refreshCommits(g)
}
func (gui *Gui) handleCloseCredentialsView(g *gocui.Gui, v *gocui.View) error {
_, err := g.SetViewOnBottom("credentials")
if err != nil {
return err
}
gui.credentials <- ""
return gui.switchFocus(g, nil, gui.getFilesView())
}
func (gui *Gui) handleCredentialsViewFocused(g *gocui.Gui, v *gocui.View) error {
if _, err := g.SetViewOnTop("credentials"); err != nil {
return err
}
message := gui.Tr.TemplateLocalize(
"CloseConfirm",
Teml{
"keyBindClose": "esc",
"keyBindConfirm": "enter",
},
)
return gui.renderString(g, "options", message)
}
// HandleCredentialsPopup handles the views after executing a command that might ask for credentials
func (gui *Gui) HandleCredentialsPopup(g *gocui.Gui, popupOpened bool, cmdErr error) {
if popupOpened {
_, _ = gui.g.SetViewOnBottom("credentials")
}
if cmdErr != nil {
errMessage := cmdErr.Error()
if strings.Contains(errMessage, "Invalid username or password") {
errMessage = gui.Tr.SLocalize("PassUnameWrong")
}
// we are not logging this error because it may contain a password
_ = gui.createSpecificErrorPanel(errMessage, gui.getFilesView(), false)
} else {
_ = gui.closeConfirmationPrompt(g, true)
_ = gui.refreshSidePanels(g)
}
}

View File

@@ -0,0 +1,42 @@
package gui
import (
"github.com/jesseduffield/gocui"
)
func (gui *Gui) handleCreateDiscardMenu(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
}
return nil
}
menuItems := []*menuItem{
{
displayString: gui.Tr.SLocalize("discardAllChanges"),
onPress: func() error {
if err := gui.GitCommand.DiscardAllFileChanges(file); err != nil {
return err
}
return gui.refreshFiles()
},
},
}
if file.HasStagedChanges && file.HasUnstagedChanges {
menuItems = append(menuItems, &menuItem{
displayString: gui.Tr.SLocalize("discardUnstagedChanges"),
onPress: func() error {
if err := gui.GitCommand.DiscardUnstagedFileChanges(file); err != nil {
return err
}
return gui.refreshFiles()
},
})
}
return gui.createMenu(file.Name, menuItems, createMenuOptions{showCancel: true})
}

149
pkg/gui/file_watching.go Normal file
View File

@@ -0,0 +1,149 @@
package gui
import (
"os"
"path/filepath"
"github.com/fsnotify/fsnotify"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/sirupsen/logrus"
)
// macs for some bizarre reason cap the number of watchable files to 256.
// there's no obvious platform agonstic way to check the situation of the user's
// computer so we're just arbitrarily capping at 200. This isn't so bad because
// file watching is only really an added bonus for faster refreshing.
const MAX_WATCHED_FILES = 50
type fileWatcher struct {
Watcher *fsnotify.Watcher
WatchedFilenames []string
Log *logrus.Entry
Disabled bool
}
func NewFileWatcher(log *logrus.Entry) *fileWatcher {
watcher, err := fsnotify.NewWatcher()
log.Error(err)
return &fileWatcher{
Disabled: true,
}
if err != nil {
log.Error(err)
return &fileWatcher{
Disabled: true,
}
}
return &fileWatcher{
Watcher: watcher,
Log: log,
WatchedFilenames: make([]string, 0, MAX_WATCHED_FILES),
}
}
func (w *fileWatcher) watchingFilename(filename string) bool {
for _, watchedFilename := range w.WatchedFilenames {
if watchedFilename == filename {
return true
}
}
return false
}
func (w *fileWatcher) popOldestFilename() {
// shift the last off the array to make way for this one
oldestFilename := w.WatchedFilenames[0]
w.WatchedFilenames = w.WatchedFilenames[1:]
if err := w.Watcher.Remove(oldestFilename); err != nil {
// swallowing errors here because it doesn't really matter if we can't unwatch a file
w.Log.Warn(err)
}
}
func (w *fileWatcher) watchFilename(filename string) {
w.Log.Warn(filename)
if err := w.Watcher.Add(filename); err != nil {
// swallowing errors here because it doesn't really matter if we can't watch a file
w.Log.Warn(err)
}
// assume we're watching it now to be safe
w.WatchedFilenames = append(w.WatchedFilenames, filename)
}
func (w *fileWatcher) addFilesToFileWatcher(files []*commands.File) error {
if w.Disabled {
return nil
}
if len(files) == 0 {
return nil
}
// watch the files for changes
dirName, err := os.Getwd()
if err != nil {
return err
}
for _, file := range files[0:min(MAX_WATCHED_FILES, len(files))] {
if file.Deleted {
continue
}
filename := filepath.Join(dirName, file.Name)
if w.watchingFilename(filename) {
continue
}
if len(w.WatchedFilenames) > MAX_WATCHED_FILES {
w.popOldestFilename()
}
w.watchFilename(filename)
}
return nil
}
func min(a int, b int) int {
if a < b {
return a
}
return b
}
// NOTE: given that we often edit files ourselves, this may make us end up refreshing files too often
// TODO: consider watching the whole directory recursively (could be more expensive)
func (gui *Gui) watchFilesForChanges() {
gui.fileWatcher = NewFileWatcher(gui.Log)
if gui.fileWatcher.Disabled {
return
}
go func() {
for {
select {
// watch for events
case event := <-gui.fileWatcher.Watcher.Events:
if event.Op == fsnotify.Chmod {
// for some reason we pick up chmod events when they don't actually happen
continue
}
// only refresh if we're not already
if !gui.State.IsRefreshingFiles {
if err := gui.refreshFiles(); err != nil {
err = gui.createErrorPanel(gui.g, err.Error())
if err != nil {
gui.Log.Error(err)
}
}
}
// watch for errors
case err := <-gui.fileWatcher.Watcher.Errors:
if err != nil {
gui.Log.Warn(err)
}
}
}
}()
}

575
pkg/gui/files_panel.go Normal file
View File

@@ -0,0 +1,575 @@
package gui
import (
// "io"
// "io/ioutil"
// "strings"
"fmt"
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
)
// list panel functions
func (gui *Gui) getSelectedFile(g *gocui.Gui) (*commands.File, error) {
selectedLine := gui.State.Panels.Files.SelectedLine
if selectedLine == -1 {
return &commands.File{}, gui.Errors.ErrNoFiles
}
return gui.State.Files[selectedLine], nil
}
func (gui *Gui) selectFile(alreadySelected bool) error {
file, err := gui.getSelectedFile(gui.g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
}
gui.State.SplitMainPanel = false
gui.getMainView().Title = ""
return gui.newStringTask("main", gui.Tr.SLocalize("NoChangedFiles"))
}
if err := gui.focusPoint(0, gui.State.Panels.Files.SelectedLine, len(gui.State.Files), gui.getFilesView()); err != nil {
return err
}
if file.HasInlineMergeConflicts {
gui.getMainView().Title = gui.Tr.SLocalize("MergeConflictsTitle")
gui.State.SplitMainPanel = false
return gui.refreshMergePanel()
}
if !alreadySelected {
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
if err := gui.resetOrigin(gui.getSecondaryView()); err != nil {
return err
}
}
if file.HasStagedChanges && file.HasUnstagedChanges {
gui.State.SplitMainPanel = true
gui.getMainView().Title = gui.Tr.SLocalize("UnstagedChanges")
gui.getSecondaryView().Title = gui.Tr.SLocalize("StagedChanges")
cmdStr := gui.GitCommand.DiffCmdStr(file, false, true)
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
if err := gui.newPtyTask("secondary", cmd); err != nil {
return err
}
} else {
gui.State.SplitMainPanel = false
if file.HasUnstagedChanges {
gui.getMainView().Title = gui.Tr.SLocalize("UnstagedChanges")
} else {
gui.getMainView().Title = gui.Tr.SLocalize("StagedChanges")
}
}
cmdStr := gui.GitCommand.DiffCmdStr(file, false, !file.HasUnstagedChanges && file.HasStagedChanges)
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
if err := gui.newPtyTask("main", cmd); err != nil {
return err
}
return nil
}
func (gui *Gui) refreshFiles() error {
gui.State.RefreshingFilesMutex.Lock()
gui.State.IsRefreshingFiles = true
defer func() {
gui.State.IsRefreshingFiles = false
gui.State.RefreshingFilesMutex.Unlock()
}()
selectedFile, _ := gui.getSelectedFile(gui.g)
filesView := gui.getFilesView()
if filesView == nil {
// if the filesView hasn't been instantiated yet we just return
return nil
}
if err := gui.refreshStateFiles(); err != nil {
return err
}
gui.g.Update(func(g *gocui.Gui) error {
displayStrings := presentation.GetFileListDisplayStrings(gui.State.Files)
gui.renderDisplayStrings(filesView, displayStrings)
if g.CurrentView() == filesView || (g.CurrentView() == gui.getMainView() && g.CurrentView().Context == "merging") {
newSelectedFile, _ := gui.getSelectedFile(gui.g)
alreadySelected := newSelectedFile.Name == selectedFile.Name
return gui.selectFile(alreadySelected)
}
return nil
})
return nil
}
// specific functions
func (gui *Gui) stagedFiles() []*commands.File {
files := gui.State.Files
result := make([]*commands.File, 0)
for _, file := range files {
if file.HasStagedChanges {
result = append(result, file)
}
}
return result
}
func (gui *Gui) trackedFiles() []*commands.File {
files := gui.State.Files
result := make([]*commands.File, 0)
for _, file := range files {
if file.Tracked {
result = append(result, file)
}
}
return result
}
func (gui *Gui) stageSelectedFile(g *gocui.Gui) error {
file, err := gui.getSelectedFile(g)
if err != nil {
return err
}
return gui.GitCommand.StageFile(file.Name)
}
func (gui *Gui) handleEnterFile(g *gocui.Gui, v *gocui.View) error {
return gui.enterFile(false, -1)
}
func (gui *Gui) enterFile(forceSecondaryFocused bool, selectedLineIdx int) error {
file, err := gui.getSelectedFile(gui.g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
}
return nil
}
if file.HasInlineMergeConflicts {
return gui.handleSwitchToMerge(gui.g, gui.getFilesView())
}
if file.HasMergeConflicts {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("FileStagingRequirements"))
}
gui.changeMainViewsContext("staging")
if err := gui.switchFocus(gui.g, gui.getFilesView(), gui.getMainView()); err != nil {
return err
}
return gui.refreshStagingPanel(forceSecondaryFocused, selectedLineIdx)
}
func (gui *Gui) handleFilePress(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
if err == gui.Errors.ErrNoFiles {
return nil
}
return err
}
if file.HasInlineMergeConflicts {
return gui.handleSwitchToMerge(g, v)
}
if file.HasUnstagedChanges {
gui.GitCommand.StageFile(file.Name)
} else {
gui.GitCommand.UnStageFile(file.Name, file.Tracked)
}
if err := gui.refreshFiles(); err != nil {
return err
}
return gui.selectFile(true)
}
func (gui *Gui) allFilesStaged() bool {
for _, file := range gui.State.Files {
if file.HasUnstagedChanges {
return false
}
}
return true
}
func (gui *Gui) focusAndSelectFile(g *gocui.Gui, v *gocui.View) error {
if _, err := gui.g.SetCurrentView("files"); err != nil {
return err
}
return gui.selectFile(false)
}
func (gui *Gui) handleStageAll(g *gocui.Gui, v *gocui.View) error {
var err error
if gui.allFilesStaged() {
err = gui.GitCommand.UnstageAll()
} else {
err = gui.GitCommand.StageAll()
}
if err != nil {
_ = gui.createErrorPanel(g, err.Error())
}
if err := gui.refreshFiles(); err != nil {
return err
}
return gui.selectFile(false)
}
func (gui *Gui) handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(gui.g)
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
if file.Tracked {
return gui.createConfirmationPanel(gui.g, gui.g.CurrentView(), true, gui.Tr.SLocalize("IgnoreTracked"), gui.Tr.SLocalize("IgnoreTrackedPrompt"),
// On confirmation
func(_ *gocui.Gui, _ *gocui.View) error {
if err := gui.GitCommand.Ignore(file.Name); err != nil {
return err
}
if err := gui.GitCommand.RemoveTrackedFiles(file.Name); err != nil {
return err
}
return gui.refreshFiles()
}, nil)
}
if err := gui.GitCommand.Ignore(file.Name); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshFiles()
}
func (gui *Gui) handleWIPCommitPress(g *gocui.Gui, filesView *gocui.View) error {
skipHookPreifx := gui.Config.GetUserConfig().GetString("git.skipHookPrefix")
if skipHookPreifx == "" {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("SkipHookPrefixNotConfigured"))
}
if err := gui.renderString(g, "commitMessage", skipHookPreifx); err != nil {
return err
}
if err := gui.getCommitMessageView().SetCursor(len(skipHookPreifx), 0); err != nil {
return err
}
return gui.handleCommitPress(g, filesView)
}
func (gui *Gui) handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
if len(gui.stagedFiles()) == 0 && gui.State.WorkingTreeState == "normal" {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit"))
}
commitMessageView := gui.getCommitMessageView()
g.Update(func(g *gocui.Gui) error {
g.SetViewOnTop("commitMessage")
gui.switchFocus(g, filesView, commitMessageView)
gui.RenderCommitLength()
return nil
})
return nil
}
func (gui *Gui) handleAmendCommitPress(g *gocui.Gui, filesView *gocui.View) error {
if len(gui.stagedFiles()) == 0 && gui.State.WorkingTreeState == "normal" {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit"))
}
if len(gui.State.Commits) == 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoCommitToAmend"))
}
title := strings.Title(gui.Tr.SLocalize("AmendLastCommit"))
question := gui.Tr.SLocalize("SureToAmend")
return gui.createConfirmationPanel(g, filesView, true, title, question, func(g *gocui.Gui, v *gocui.View) error {
ok, err := gui.runSyncOrAsyncCommand(gui.GitCommand.AmendHead())
if err != nil {
return err
}
if !ok {
return nil
}
return gui.refreshSidePanels(g)
}, nil)
}
// handleCommitEditorPress - handle when the user wants to commit changes via
// their editor rather than via the popup panel
func (gui *Gui) handleCommitEditorPress(g *gocui.Gui, filesView *gocui.View) error {
if len(gui.stagedFiles()) == 0 && gui.State.WorkingTreeState == "normal" {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit"))
}
gui.PrepareSubProcess(g, "git", "commit")
return nil
}
// PrepareSubProcess - prepare a subprocess for execution and tell the gui to switch to it
func (gui *Gui) PrepareSubProcess(g *gocui.Gui, commands ...string) {
gui.SubProcess = gui.GitCommand.PrepareCommitSubProcess()
g.Update(func(g *gocui.Gui) error {
return gui.Errors.ErrSubProcess
})
}
func (gui *Gui) editFile(filename string) error {
_, err := gui.runSyncOrAsyncCommand(gui.OSCommand.EditFile(filename))
return err
}
func (gui *Gui) handleFileEdit(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.editFile(file.Name)
}
func (gui *Gui) handleFileOpen(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.openFile(file.Name)
}
func (gui *Gui) handleRefreshFiles(g *gocui.Gui, v *gocui.View) error {
return gui.refreshFiles()
}
func (gui *Gui) refreshStateFiles() error {
// get files to stage
files := gui.GitCommand.GetStatusFiles()
gui.State.Files = gui.GitCommand.MergeStatusFiles(gui.State.Files, files)
if err := gui.fileWatcher.addFilesToFileWatcher(files); err != nil {
return err
}
gui.refreshSelectedLine(&gui.State.Panels.Files.SelectedLine, len(gui.State.Files))
return gui.updateWorkTreeState()
}
func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) {
item, err := gui.getSelectedFile(g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
return "", err
}
return "", gui.newStringTask("main", gui.Tr.SLocalize("NoFilesDisplay"))
}
if item.Type != "file" {
return "", gui.newStringTask("main", gui.Tr.SLocalize("NotAFile"))
}
cat, err := gui.GitCommand.CatFile(item.Name)
if err != nil {
gui.Log.Error(err)
return "", gui.newStringTask("main", err.Error())
}
return cat, nil
}
func (gui *Gui) handlePullFiles(g *gocui.Gui, v *gocui.View) error {
// if we have no upstream branch we need to set that first
_, pullables := gui.GitCommand.GetCurrentBranchUpstreamDifferenceCount()
currentBranchName, err := gui.GitCommand.CurrentBranchName()
if err != nil {
return err
}
if pullables == "?" {
// see if we have this branch in our config with an upstream
conf, err := gui.GitCommand.Repo.Config()
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
for branchName, branch := range conf.Branches {
if branchName == currentBranchName {
return gui.pullFiles(v, fmt.Sprintf("%s %s", branch.Remote, branchName))
}
}
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("EnterUpstream"), "origin/"+currentBranchName, func(g *gocui.Gui, v *gocui.View) error {
upstream := gui.trimmedContent(v)
if err := gui.GitCommand.SetUpstreamBranch(upstream); err != nil {
errorMessage := err.Error()
if strings.Contains(errorMessage, "does not exist") {
errorMessage = fmt.Sprintf("upstream branch %s not found.\nIf you expect it to exist, you should fetch (with 'f').\nOtherwise, you should push (with 'shift+P')", upstream)
}
return gui.createErrorPanel(gui.g, errorMessage)
}
return gui.pullFiles(v, "")
})
}
return gui.pullFiles(v, "")
}
func (gui *Gui) pullFiles(v *gocui.View, args string) error {
if err := gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("PullWait")); err != nil {
return err
}
go func() {
unamePassOpend := false
err := gui.GitCommand.Pull(args, func(passOrUname string) string {
unamePassOpend = true
return gui.waitForPassUname(gui.g, v, passOrUname)
})
gui.HandleCredentialsPopup(gui.g, unamePassOpend, err)
}()
return nil
}
func (gui *Gui) pushWithForceFlag(g *gocui.Gui, v *gocui.View, force bool, upstream string, args string) error {
if err := gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("PushWait")); err != nil {
return err
}
go func() {
unamePassOpend := false
branchName := gui.getCheckedOutBranch().Name
err := gui.GitCommand.Push(branchName, force, upstream, args, func(passOrUname string) string {
unamePassOpend = true
return gui.waitForPassUname(g, v, passOrUname)
})
gui.HandleCredentialsPopup(g, unamePassOpend, err)
}()
return nil
}
func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error {
// if we have pullables we'll ask if the user wants to force push
_, pullables := gui.GitCommand.GetCurrentBranchUpstreamDifferenceCount()
currentBranchName, err := gui.GitCommand.CurrentBranchName()
if err != nil {
return err
}
if pullables == "?" {
// see if we have this branch in our config with an upstream
conf, err := gui.GitCommand.Repo.Config()
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
for branchName, branch := range conf.Branches {
if branchName == currentBranchName {
return gui.pushWithForceFlag(g, v, false, "", fmt.Sprintf("%s %s", branch.Remote, branchName))
}
}
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("EnterUpstream"), "origin "+currentBranchName, func(g *gocui.Gui, v *gocui.View) error {
return gui.pushWithForceFlag(g, v, false, gui.trimmedContent(v), "")
})
} else if pullables == "0" {
return gui.pushWithForceFlag(g, v, false, "", "")
}
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("ForcePush"), gui.Tr.SLocalize("ForcePushPrompt"), func(g *gocui.Gui, v *gocui.View) error {
return gui.pushWithForceFlag(g, v, true, "", "")
}, nil)
}
func (gui *Gui) handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
return gui.createErrorPanel(gui.g, err.Error())
}
return nil
}
if !file.HasInlineMergeConflicts {
return gui.createErrorPanel(g, gui.Tr.SLocalize("FileNoMergeCons"))
}
gui.changeMainViewsContext("merging")
if err := gui.switchFocus(g, v, gui.getMainView()); err != nil {
return err
}
return gui.refreshMergePanel()
}
func (gui *Gui) handleAbortMerge(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.AbortMerge(); err != nil {
return gui.createErrorPanel(g, err.Error())
}
gui.createMessagePanel(g, v, "", gui.Tr.SLocalize("MergeAborted"))
gui.refreshStatus(g)
return gui.refreshFiles()
}
func (gui *Gui) openFile(filename string) error {
if err := gui.OSCommand.OpenFile(filename); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return nil
}
func (gui *Gui) anyFilesWithMergeConflicts() bool {
for _, file := range gui.State.Files {
if file.HasMergeConflicts {
return true
}
}
return false
}
func (gui *Gui) handleCustomCommand(g *gocui.Gui, v *gocui.View) error {
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("CustomCommand"), "", func(g *gocui.Gui, v *gocui.View) error {
command := gui.trimmedContent(v)
gui.SubProcess = gui.OSCommand.RunCustomCommand(command)
return gui.Errors.ErrSubProcess
})
}
func (gui *Gui) handleCreateStashMenu(g *gocui.Gui, v *gocui.View) error {
menuItems := []*menuItem{
{
displayString: gui.Tr.SLocalize("stashAllChanges"),
onPress: func() error {
return gui.handleStashSave(gui.GitCommand.StashSave)
},
},
{
displayString: gui.Tr.SLocalize("stashStagedChanges"),
onPress: func() error {
return gui.handleStashSave(gui.GitCommand.StashSaveStagedChanges)
},
},
}
return gui.createMenu(gui.Tr.SLocalize("stashOptions"), menuItems, createMenuOptions{showCancel: true})
}
func (gui *Gui) handleStashChanges(g *gocui.Gui, v *gocui.View) error {
return gui.handleStashSave(gui.GitCommand.StashSave)
}
func (gui *Gui) handleCreateResetToUpstreamMenu(g *gocui.Gui, v *gocui.View) error {
return gui.createResetMenu("@{upstream}")
}
func (gui *Gui) onFilesPanelSearchSelect(selectedLine int) error {
gui.State.Panels.Files.SelectedLine = selectedLine
return gui.focusAndSelectFile(gui.g, gui.getFilesView())
}

86
pkg/gui/git_flow.go Normal file
View File

@@ -0,0 +1,86 @@
package gui
import (
"fmt"
"regexp"
"strings"
"github.com/jesseduffield/gocui"
)
func (gui *Gui) gitFlowFinishBranch(gitFlowConfig string, branchName string) error {
// 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(gitFlowConfig), "\n") {
if strings.HasPrefix(line, "gitflow.prefix.") && strings.HasSuffix(line, prefix) {
// now I just need to how do you say
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 gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NotAGitFlowBranch"))
}
subProcess := gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "finish", suffix)
gui.SubProcess = subProcess
return gui.Errors.ErrSubProcess
}
func (gui *Gui) handleCreateGitFlowMenu(g *gocui.Gui, v *gocui.View) error {
branch := gui.getSelectedBranch()
if branch == nil {
return nil
}
// get config
gitFlowConfig, err := gui.OSCommand.RunCommandWithOutput("git config --local --get-regexp gitflow")
if err != nil {
return gui.createErrorPanel(gui.g, "You need to install git-flow and enable it in this repo to use git-flow features")
}
startHandler := func(branchType string) func() error {
return func() error {
title := gui.Tr.TemplateLocalize("NewBranchNamePrompt", map[string]interface{}{"branchType": branchType})
return gui.createPromptPanel(gui.g, gui.getMenuView(), title, "", func(g *gocui.Gui, v *gocui.View) error {
name := gui.trimmedContent(v)
subProcess := gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "start", name)
gui.SubProcess = subProcess
return gui.Errors.ErrSubProcess
})
}
}
menuItems := []*menuItem{
{
// not localising here because it's one to one with the actual git flow commands
displayString: fmt.Sprintf("finish branch '%s'", branch.Name),
onPress: func() error {
return gui.gitFlowFinishBranch(gitFlowConfig, branch.Name)
},
},
{
displayString: "start feature",
onPress: startHandler("feature"),
},
{
displayString: "start hotfix",
onPress: startHandler("hotfix"),
},
{
displayString: "start release",
onPress: startHandler("release"),
},
}
return gui.createMenu("git flow", menuItems, createMenuOptions{})
}

1127
pkg/gui/gui.go Normal file

File diff suppressed because it is too large Load Diff

1485
pkg/gui/keybindings.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,325 @@
package gui
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
)
// Currently there are two 'pseudo-panels' that make use of this 'pseudo-panel'.
// One is the staging panel where we stage files line-by-line, the other is the
// patch building panel where we add lines of an old commit's file to a patch.
// This file contains the logic around selecting lines and displaying the diffs
// staging_panel.go and patch_building_panel.go have functions specific to their
// use cases
// these represent what select mode we're in
const (
LINE = iota
RANGE
HUNK
)
// returns whether the patch is empty so caller can escape if necessary
// both diffs should be non-coloured because we'll parse them and colour them here
func (gui *Gui) refreshLineByLinePanel(diff string, secondaryDiff string, secondaryFocused bool, selectedLineIdx int) (bool, error) {
state := gui.State.Panels.LineByLine
patchParser, err := commands.NewPatchParser(gui.Log, diff)
if err != nil {
return false, nil
}
if len(patchParser.StageableLines) == 0 {
return true, nil
}
var firstLineIdx int
var lastLineIdx int
selectMode := LINE
// if we have clicked from the outside to focus the main view we'll pass in a non-negative line index so that we can instantly select that line
if selectedLineIdx >= 0 {
selectMode = RANGE
firstLineIdx, lastLineIdx = selectedLineIdx, selectedLineIdx
} else if state != nil {
if state.SelectMode == HUNK {
// this is tricky: we need to find out which hunk we just staged based on our old `state.PatchParser` (as opposed to the new `patchParser`)
// we do this by getting the first line index of the original hunk, then
// finding the next stageable line, then getting its containing hunk
// in the new diff
selectMode = HUNK
prevNewHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0)
selectedLineIdx = patchParser.GetNextStageableLineIndex(prevNewHunk.FirstLineIdx)
newHunk := patchParser.GetHunkContainingLine(selectedLineIdx, 0)
firstLineIdx, lastLineIdx = newHunk.FirstLineIdx, newHunk.LastLineIdx
} else {
selectedLineIdx = patchParser.GetNextStageableLineIndex(state.SelectedLineIdx)
firstLineIdx, lastLineIdx = selectedLineIdx, selectedLineIdx
}
} else {
selectedLineIdx = patchParser.StageableLines[0]
firstLineIdx, lastLineIdx = selectedLineIdx, selectedLineIdx
}
gui.State.Panels.LineByLine = &lineByLinePanelState{
PatchParser: patchParser,
SelectedLineIdx: selectedLineIdx,
SelectMode: selectMode,
FirstLineIdx: firstLineIdx,
LastLineIdx: lastLineIdx,
Diff: diff,
SecondaryFocused: secondaryFocused,
}
if err := gui.refreshMainView(); err != nil {
return false, err
}
if err := gui.focusSelection(selectMode == HUNK); err != nil {
return false, err
}
secondaryView := gui.getSecondaryView()
secondaryView.Highlight = true
secondaryView.Wrap = false
secondaryPatchParser, err := commands.NewPatchParser(gui.Log, secondaryDiff)
if err != nil {
return false, nil
}
gui.g.Update(func(*gocui.Gui) error {
return gui.setViewContent(gui.g, gui.getSecondaryView(), secondaryPatchParser.Render(-1, -1, nil))
})
return false, nil
}
func (gui *Gui) handleSelectPrevLine(g *gocui.Gui, v *gocui.View) error {
return gui.handleCycleLine(-1)
}
func (gui *Gui) handleSelectNextLine(g *gocui.Gui, v *gocui.View) error {
return gui.handleCycleLine(+1)
}
func (gui *Gui) handleSelectPrevHunk(g *gocui.Gui, v *gocui.View) error {
state := gui.State.Panels.LineByLine
newHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, -1)
return gui.selectNewHunk(newHunk)
}
func (gui *Gui) handleSelectNextHunk(g *gocui.Gui, v *gocui.View) error {
state := gui.State.Panels.LineByLine
newHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 1)
return gui.selectNewHunk(newHunk)
}
func (gui *Gui) selectNewHunk(newHunk *commands.PatchHunk) error {
state := gui.State.Panels.LineByLine
state.SelectedLineIdx = state.PatchParser.GetNextStageableLineIndex(newHunk.FirstLineIdx)
if state.SelectMode == HUNK {
state.FirstLineIdx, state.LastLineIdx = newHunk.FirstLineIdx, newHunk.LastLineIdx
} else {
state.FirstLineIdx, state.LastLineIdx = state.SelectedLineIdx, state.SelectedLineIdx
}
if err := gui.refreshMainView(); err != nil {
return err
}
return gui.focusSelection(true)
}
func (gui *Gui) handleCycleLine(change int) error {
state := gui.State.Panels.LineByLine
if state.SelectMode == HUNK {
newHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, change)
return gui.selectNewHunk(newHunk)
}
return gui.handleSelectNewLine(state.SelectedLineIdx + change)
}
func (gui *Gui) handleSelectNewLine(newSelectedLineIdx int) error {
state := gui.State.Panels.LineByLine
if newSelectedLineIdx < 0 {
newSelectedLineIdx = 0
} else if newSelectedLineIdx > len(state.PatchParser.PatchLines)-1 {
newSelectedLineIdx = len(state.PatchParser.PatchLines) - 1
}
state.SelectedLineIdx = newSelectedLineIdx
if state.SelectMode == RANGE {
if state.SelectedLineIdx < state.FirstLineIdx {
state.FirstLineIdx = state.SelectedLineIdx
} else {
state.LastLineIdx = state.SelectedLineIdx
}
} else {
state.LastLineIdx = state.SelectedLineIdx
state.FirstLineIdx = state.SelectedLineIdx
}
if err := gui.refreshMainView(); err != nil {
return err
}
return gui.focusSelection(false)
}
func (gui *Gui) handleMouseDown(g *gocui.Gui, v *gocui.View) error {
state := gui.State.Panels.LineByLine
if gui.popupPanelFocused() {
return nil
}
newSelectedLineIdx := v.SelectedLineIdx()
state.FirstLineIdx = newSelectedLineIdx
state.LastLineIdx = newSelectedLineIdx
state.SelectMode = RANGE
return gui.handleSelectNewLine(newSelectedLineIdx)
}
func (gui *Gui) handleMouseDrag(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
return gui.handleSelectNewLine(v.SelectedLineIdx())
}
func (gui *Gui) handleMouseScrollUp(g *gocui.Gui, v *gocui.View) error {
state := gui.State.Panels.LineByLine
if gui.popupPanelFocused() {
return nil
}
state.SelectMode = LINE
return gui.handleCycleLine(-1)
}
func (gui *Gui) handleMouseScrollDown(g *gocui.Gui, v *gocui.View) error {
state := gui.State.Panels.LineByLine
if gui.popupPanelFocused() {
return nil
}
state.SelectMode = LINE
return gui.handleCycleLine(1)
}
func (gui *Gui) getSelectedCommitFileName() string {
return gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLine].Name
}
func (gui *Gui) refreshMainView() error {
state := gui.State.Panels.LineByLine
var includedLineIndices []int
// I'd prefer not to have knowledge of contexts using this file but I'm not sure
// how to get around this
if gui.State.MainContext == "patch-building" {
filename := gui.getSelectedCommitFileName()
includedLineIndices = gui.GitCommand.PatchManager.GetFileIncLineIndices(filename)
}
colorDiff := state.PatchParser.Render(state.FirstLineIdx, state.LastLineIdx, includedLineIndices)
mainView := gui.getMainView()
mainView.Highlight = true
mainView.Wrap = false
gui.g.Update(func(*gocui.Gui) error {
return gui.setViewContent(gui.g, gui.getMainView(), colorDiff)
})
return nil
}
// focusSelection works out the best focus for the staging panel given the
// selected line and size of the hunk
func (gui *Gui) focusSelection(includeCurrentHunk bool) error {
stagingView := gui.getMainView()
state := gui.State.Panels.LineByLine
_, viewHeight := stagingView.Size()
bufferHeight := viewHeight - 1
_, origin := stagingView.Origin()
firstLineIdx := state.SelectedLineIdx
lastLineIdx := state.SelectedLineIdx
if includeCurrentHunk {
hunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0)
firstLineIdx = hunk.FirstLineIdx
lastLineIdx = hunk.LastLineIdx
}
margin := 0 // we may want to have a margin in place to show context but right now I'm thinking we keep this at zero
var newOrigin int
if firstLineIdx-origin < margin {
newOrigin = firstLineIdx - margin
} else if lastLineIdx-origin > bufferHeight-margin {
newOrigin = lastLineIdx - bufferHeight + margin
} else {
newOrigin = origin
}
gui.g.Update(func(*gocui.Gui) error {
if err := stagingView.SetOrigin(0, newOrigin); err != nil {
return err
}
return stagingView.SetCursor(0, state.SelectedLineIdx-newOrigin)
})
return nil
}
func (gui *Gui) handleToggleSelectRange(g *gocui.Gui, v *gocui.View) error {
state := gui.State.Panels.LineByLine
if state.SelectMode == RANGE {
state.SelectMode = LINE
} else {
state.SelectMode = RANGE
}
state.FirstLineIdx, state.LastLineIdx = state.SelectedLineIdx, state.SelectedLineIdx
return gui.refreshMainView()
}
func (gui *Gui) handleToggleSelectHunk(g *gocui.Gui, v *gocui.View) error {
state := gui.State.Panels.LineByLine
if state.SelectMode == HUNK {
state.SelectMode = LINE
state.FirstLineIdx, state.LastLineIdx = state.SelectedLineIdx, state.SelectedLineIdx
} else {
state.SelectMode = HUNK
selectedHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0)
state.FirstLineIdx, state.LastLineIdx = selectedHunk.FirstLineIdx, selectedHunk.LastLineIdx
}
if err := gui.refreshMainView(); err != nil {
return err
}
return gui.focusSelection(state.SelectMode == HUNK)
}
func (gui *Gui) handleEscapeLineByLinePanel() {
gui.changeMainViewsContext("normal")
gui.State.Panels.LineByLine = nil
}

178
pkg/gui/list_view.go Normal file
View File

@@ -0,0 +1,178 @@
package gui
import "github.com/jesseduffield/gocui"
type listView struct {
viewName string
context string
getItemsLength func() int
getSelectedLineIdxPtr func() *int
handleFocus func(g *gocui.Gui, v *gocui.View) error
handleItemSelect func(g *gocui.Gui, v *gocui.View) error
handleClickSelectedItem func(g *gocui.Gui, v *gocui.View) error
gui *Gui
rendersToMainView bool
}
func (lv *listView) handlePrevLine(g *gocui.Gui, v *gocui.View) error {
return lv.handleLineChange(-1)
}
func (lv *listView) handleNextLine(g *gocui.Gui, v *gocui.View) error {
return lv.handleLineChange(1)
}
func (lv *listView) handleLineChange(change int) error {
if !lv.gui.isPopupPanel(lv.viewName) && lv.gui.popupPanelFocused() {
return nil
}
lv.gui.changeSelectedLine(lv.getSelectedLineIdxPtr(), lv.getItemsLength(), change)
if lv.rendersToMainView {
if err := lv.gui.resetOrigin(lv.gui.getMainView()); err != nil {
return err
}
}
view, err := lv.gui.g.View(lv.viewName)
if err != nil {
return err
}
return lv.handleItemSelect(lv.gui.g, view)
}
func (lv *listView) handleClick(g *gocui.Gui, v *gocui.View) error {
if !lv.gui.isPopupPanel(lv.viewName) && lv.gui.popupPanelFocused() {
return nil
}
selectedLineIdxPtr := lv.getSelectedLineIdxPtr()
prevSelectedLineIdx := *selectedLineIdxPtr
newSelectedLineIdx := v.SelectedLineIdx()
if newSelectedLineIdx > lv.getItemsLength()-1 {
return lv.handleFocus(lv.gui.g, v)
}
*selectedLineIdxPtr = newSelectedLineIdx
if lv.rendersToMainView {
if err := lv.gui.resetOrigin(lv.gui.getMainView()); err != nil {
return err
}
}
prevViewName := lv.gui.currentViewName()
if prevSelectedLineIdx == newSelectedLineIdx && prevViewName == lv.viewName && lv.handleClickSelectedItem != nil {
return lv.handleClickSelectedItem(lv.gui.g, v)
}
return lv.handleItemSelect(lv.gui.g, v)
}
func (gui *Gui) getListViews() []*listView {
return []*listView{
{
viewName: "menu",
getItemsLength: func() int { return gui.getMenuView().LinesHeight() },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Menu.SelectedLine },
handleFocus: gui.handleMenuSelect,
handleItemSelect: gui.handleMenuSelect,
// need to add a layer of indirection here because the callback changes during runtime
handleClickSelectedItem: gui.wrappedHandler(func() error { return gui.State.Panels.Menu.OnPress(gui.g, nil) }),
gui: gui,
rendersToMainView: false,
},
{
viewName: "files",
getItemsLength: func() int { return len(gui.State.Files) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Files.SelectedLine },
handleFocus: gui.focusAndSelectFile,
handleItemSelect: gui.focusAndSelectFile,
handleClickSelectedItem: gui.handleFilePress,
gui: gui,
rendersToMainView: true,
},
{
viewName: "branches",
context: "local-branches",
getItemsLength: func() int { return len(gui.State.Branches) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Branches.SelectedLine },
handleFocus: gui.handleBranchSelect,
handleItemSelect: gui.handleBranchSelect,
gui: gui,
rendersToMainView: true,
},
{
viewName: "branches",
context: "remotes",
getItemsLength: func() int { return len(gui.State.Remotes) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Remotes.SelectedLine },
handleFocus: gui.wrappedHandler(gui.renderRemotesWithSelection),
handleItemSelect: gui.handleRemoteSelect,
handleClickSelectedItem: gui.handleRemoteEnter,
gui: gui,
rendersToMainView: true,
},
{
viewName: "branches",
context: "remote-branches",
getItemsLength: func() int { return len(gui.State.RemoteBranches) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.RemoteBranches.SelectedLine },
handleFocus: gui.handleRemoteBranchSelect,
handleItemSelect: gui.handleRemoteBranchSelect,
gui: gui,
rendersToMainView: true,
},
{
viewName: "branches",
context: "tags",
getItemsLength: func() int { return len(gui.State.Tags) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Tags.SelectedLine },
handleFocus: gui.handleTagSelect,
handleItemSelect: gui.handleTagSelect,
gui: gui,
rendersToMainView: true,
},
{
viewName: "commits",
context: "branch-commits",
getItemsLength: func() int { return len(gui.State.Commits) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Commits.SelectedLine },
handleFocus: gui.handleCommitSelect,
handleItemSelect: gui.handleCommitSelect,
handleClickSelectedItem: gui.handleSwitchToCommitFilesPanel,
gui: gui,
rendersToMainView: true,
},
{
viewName: "commits",
context: "reflog-commits",
getItemsLength: func() int { return len(gui.State.ReflogCommits) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.ReflogCommits.SelectedLine },
handleFocus: gui.handleReflogCommitSelect,
handleItemSelect: gui.handleReflogCommitSelect,
gui: gui,
rendersToMainView: true,
},
{
viewName: "stash",
getItemsLength: func() int { return len(gui.State.StashEntries) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Stash.SelectedLine },
handleFocus: gui.handleStashEntrySelect,
handleItemSelect: gui.handleStashEntrySelect,
gui: gui,
rendersToMainView: true,
},
{
viewName: "commitFiles",
getItemsLength: func() int { return len(gui.State.CommitFiles) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.CommitFiles.SelectedLine },
handleFocus: gui.handleCommitFileSelect,
handleItemSelect: gui.handleCommitFileSelect,
gui: gui,
rendersToMainView: true,
},
}
}

119
pkg/gui/menu_panel.go Normal file
View File

@@ -0,0 +1,119 @@
package gui
import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type menuItem struct {
displayString string
displayStrings []string
onPress func() error
}
// list panel functions
func (gui *Gui) handleMenuSelect(g *gocui.Gui, v *gocui.View) error {
return gui.focusPoint(0, gui.State.Panels.Menu.SelectedLine, gui.State.MenuItemCount, v)
}
// specific functions
func (gui *Gui) renderMenuOptions() error {
optionsMap := map[string]string{
fmt.Sprintf("%s/%s", gui.getKeyDisplay("universal.return"), gui.getKeyDisplay("universal.quit")): gui.Tr.SLocalize("close"),
fmt.Sprintf("%s %s", gui.getKeyDisplay("universal.prevItem"), gui.getKeyDisplay("universal.nextItem")): gui.Tr.SLocalize("navigate"),
gui.getKeyDisplay("universal.select"): gui.Tr.SLocalize("execute"),
}
return gui.renderOptionsMap(optionsMap)
}
func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error {
for _, key := range []gocui.Key{gocui.KeySpace, gocui.KeyEnter} {
if err := g.DeleteKeybinding("menu", key, gocui.ModNone); err != nil {
return err
}
}
err := g.DeleteView("menu")
if err != nil {
return err
}
return gui.returnFocus(g, v)
}
type createMenuOptions struct {
showCancel bool
}
func (gui *Gui) createMenu(title string, items []*menuItem, createMenuOptions createMenuOptions) error {
if createMenuOptions.showCancel {
// this is mutative but I'm okay with that for now
items = append(items, &menuItem{
displayStrings: []string{gui.Tr.SLocalize("cancel")},
onPress: func() error {
return nil
},
})
}
gui.State.MenuItemCount = len(items)
stringArrays := make([][]string, len(items))
for i, item := range items {
if item.displayStrings == nil {
stringArrays[i] = []string{item.displayString}
} else {
stringArrays[i] = item.displayStrings
}
}
list := utils.RenderDisplayStrings(stringArrays)
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, false, list)
menuView, _ := gui.g.SetView("menu", x0, y0, x1, y1, 0)
menuView.Title = title
menuView.FgColor = theme.GocuiDefaultTextColor
menuView.ContainsList = true
menuView.Clear()
fmt.Fprint(menuView, list)
gui.State.Panels.Menu.SelectedLine = 0
wrappedHandlePress := func(g *gocui.Gui, v *gocui.View) error {
selectedLine := gui.State.Panels.Menu.SelectedLine
if err := items[selectedLine].onPress(); err != nil {
return err
}
if _, err := gui.g.View("menu"); err == nil {
if _, err := gui.g.SetViewOnBottom("menu"); err != nil {
return err
}
}
return gui.returnFocus(gui.g, menuView)
}
gui.State.Panels.Menu.OnPress = wrappedHandlePress
for _, key := range []gocui.Key{gocui.KeySpace, gocui.KeyEnter, 'y'} {
_ = gui.g.DeleteKeybinding("menu", key, gocui.ModNone)
if err := gui.g.SetKeybinding("menu", nil, key, gocui.ModNone, wrappedHandlePress); err != nil {
return err
}
}
gui.g.Update(func(g *gocui.Gui) error {
if _, err := gui.g.View("menu"); err == nil {
if _, err := g.SetViewOnTop("menu"); err != nil {
return err
}
}
currentView := gui.g.CurrentView()
return gui.switchFocus(gui.g, currentView, menuView)
})
return nil
}

293
pkg/gui/merge_panel.go Normal file
View File

@@ -0,0 +1,293 @@
// though this panel is called the merge panel, it's really going to use the main panel. This may change in the future
package gui
import (
"bufio"
"bytes"
"fmt"
"io/ioutil"
"math"
"os"
"strings"
"github.com/fatih/color"
"github.com/golang-collections/collections/stack"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) findConflicts(content string) ([]commands.Conflict, error) {
conflicts := make([]commands.Conflict, 0)
var newConflict commands.Conflict
for i, line := range utils.SplitLines(content) {
trimmedLine := strings.TrimPrefix(line, "++")
gui.Log.Info(trimmedLine)
if trimmedLine == "<<<<<<< HEAD" || trimmedLine == "<<<<<<< MERGE_HEAD" || trimmedLine == "<<<<<<< Updated upstream" || trimmedLine == "<<<<<<< ours" {
newConflict = commands.Conflict{Start: i}
} else if trimmedLine == "=======" {
newConflict.Middle = i
} else if strings.HasPrefix(trimmedLine, ">>>>>>> ") {
newConflict.End = i
conflicts = append(conflicts, newConflict)
}
}
return conflicts, nil
}
func (gui *Gui) shiftConflict(conflicts []commands.Conflict) (commands.Conflict, []commands.Conflict) {
return conflicts[0], conflicts[1:]
}
func (gui *Gui) shouldHighlightLine(index int, conflict commands.Conflict, top bool) bool {
return (index >= conflict.Start && index <= conflict.Middle && top) || (index >= conflict.Middle && index <= conflict.End && !top)
}
func (gui *Gui) coloredConflictFile(content string, conflicts []commands.Conflict, conflictIndex int, conflictTop, hasFocus bool) (string, error) {
if len(conflicts) == 0 {
return content, nil
}
conflict, remainingConflicts := gui.shiftConflict(conflicts)
var outputBuffer bytes.Buffer
for i, line := range utils.SplitLines(content) {
colourAttr := theme.DefaultTextColor
if i == conflict.Start || i == conflict.Middle || i == conflict.End {
colourAttr = color.FgRed
}
colour := color.New(colourAttr)
if hasFocus && conflictIndex < len(conflicts) && conflicts[conflictIndex] == conflict && gui.shouldHighlightLine(i, conflict, conflictTop) {
colour.Add(color.Bold)
}
if i == conflict.End && len(remainingConflicts) > 0 {
conflict, remainingConflicts = gui.shiftConflict(remainingConflicts)
}
outputBuffer.WriteString(utils.ColoredStringDirect(line, colour) + "\n")
}
return outputBuffer.String(), nil
}
func (gui *Gui) handleSelectTop(g *gocui.Gui, v *gocui.View) error {
gui.State.Panels.Merging.ConflictTop = true
return gui.refreshMergePanel()
}
func (gui *Gui) handleSelectBottom(g *gocui.Gui, v *gocui.View) error {
gui.State.Panels.Merging.ConflictTop = false
return gui.refreshMergePanel()
}
func (gui *Gui) handleSelectNextConflict(g *gocui.Gui, v *gocui.View) error {
if gui.State.Panels.Merging.ConflictIndex >= len(gui.State.Panels.Merging.Conflicts)-1 {
return nil
}
gui.State.Panels.Merging.ConflictIndex++
return gui.refreshMergePanel()
}
func (gui *Gui) handleSelectPrevConflict(g *gocui.Gui, v *gocui.View) error {
if gui.State.Panels.Merging.ConflictIndex <= 0 {
return nil
}
gui.State.Panels.Merging.ConflictIndex--
return gui.refreshMergePanel()
}
func (gui *Gui) isIndexToDelete(i int, conflict commands.Conflict, pick string) bool {
return i == conflict.Middle ||
i == conflict.Start ||
i == conflict.End ||
pick != "both" &&
(pick == "bottom" && i > conflict.Start && i < conflict.Middle) ||
(pick == "top" && i > conflict.Middle && i < conflict.End)
}
func (gui *Gui) resolveConflict(g *gocui.Gui, conflict commands.Conflict, pick string) error {
gitFile, err := gui.getSelectedFile(g)
if err != nil {
return err
}
file, err := os.Open(gitFile.Name)
if err != nil {
return err
}
defer file.Close()
reader := bufio.NewReader(file)
output := ""
for i := 0; true; i++ {
line, err := reader.ReadString('\n')
if err != nil {
break
}
if !gui.isIndexToDelete(i, conflict, pick) {
output += line
}
}
gui.Log.Info(output)
return ioutil.WriteFile(gitFile.Name, []byte(output), 0644)
}
func (gui *Gui) pushFileSnapshot(g *gocui.Gui) error {
gitFile, err := gui.getSelectedFile(g)
if err != nil {
return err
}
content, err := gui.GitCommand.CatFile(gitFile.Name)
if err != nil {
return err
}
gui.State.Panels.Merging.EditHistory.Push(content)
return nil
}
func (gui *Gui) handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error {
if gui.State.Panels.Merging.EditHistory.Len() == 0 {
return nil
}
prevContent := gui.State.Panels.Merging.EditHistory.Pop().(string)
gitFile, err := gui.getSelectedFile(g)
if err != nil {
return err
}
ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644)
return gui.refreshMergePanel()
}
func (gui *Gui) handlePickHunk(g *gocui.Gui, v *gocui.View) error {
conflict := gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex]
gui.pushFileSnapshot(g)
pick := "bottom"
if gui.State.Panels.Merging.ConflictTop {
pick = "top"
}
err := gui.resolveConflict(g, conflict, pick)
if err != nil {
panic(err)
}
// if that was the last conflict, finish the merge for this file
if len(gui.State.Panels.Merging.Conflicts) == 1 {
if err := gui.handleCompleteMerge(); err != nil {
return err
}
}
return gui.refreshMergePanel()
}
func (gui *Gui) handlePickBothHunks(g *gocui.Gui, v *gocui.View) error {
conflict := gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex]
gui.pushFileSnapshot(g)
err := gui.resolveConflict(g, conflict, "both")
if err != nil {
panic(err)
}
return gui.refreshMergePanel()
}
func (gui *Gui) refreshMergePanel() error {
panelState := gui.State.Panels.Merging
cat, err := gui.catSelectedFile(gui.g)
if err != nil {
return err
}
if cat == "" {
return nil
}
panelState.Conflicts, err = gui.findConflicts(cat)
if err != nil {
return err
}
// handle potential fixes that the user made in their editor since we last refreshed
if len(panelState.Conflicts) == 0 {
return gui.handleCompleteMerge()
} else if panelState.ConflictIndex > len(panelState.Conflicts)-1 {
panelState.ConflictIndex = len(panelState.Conflicts) - 1
}
hasFocus := gui.currentViewName() == "main"
content, err := gui.coloredConflictFile(cat, panelState.Conflicts, panelState.ConflictIndex, panelState.ConflictTop, hasFocus)
if err != nil {
return err
}
mainView := gui.getMainView()
mainView.Wrap = false
if err := gui.setViewContent(gui.g, mainView, content); err != nil {
return err
}
gui.Log.Warn("scrolling to conflict")
if err := gui.scrollToConflict(gui.g); err != nil {
return err
}
return nil
}
func (gui *Gui) scrollToConflict(g *gocui.Gui) error {
panelState := gui.State.Panels.Merging
if len(panelState.Conflicts) == 0 {
return nil
}
mergingView := gui.getMainView()
conflict := panelState.Conflicts[panelState.ConflictIndex]
ox, _ := mergingView.Origin()
_, height := mergingView.Size()
conflictMiddle := (conflict.End + conflict.Start) / 2
newOriginY := int(math.Max(0, float64(conflictMiddle-(height/2))))
gui.g.Update(func(g *gocui.Gui) error {
return mergingView.SetOrigin(ox, newOriginY)
})
return nil
}
func (gui *Gui) renderMergeOptions() error {
return gui.renderOptionsMap(map[string]string{
fmt.Sprintf("%s %s", gui.getKeyDisplay("universal.prevItem"), gui.getKeyDisplay("universal.nextItem")): gui.Tr.SLocalize("selectHunk"),
fmt.Sprintf("%s %s", gui.getKeyDisplay("universal.prevBlock"), gui.getKeyDisplay("universal.nextBlock")): gui.Tr.SLocalize("navigateConflicts"),
gui.getKeyDisplay("universal.select"): gui.Tr.SLocalize("pickHunk"),
gui.getKeyDisplay("main.pickBothHunks"): gui.Tr.SLocalize("pickBothHunks"),
gui.getKeyDisplay("main.undo"): gui.Tr.SLocalize("undo"),
})
}
func (gui *Gui) handleEscapeMerge(g *gocui.Gui, v *gocui.View) error {
gui.State.Panels.Merging.EditHistory = stack.New()
if err := gui.refreshFiles(); err != nil {
return err
}
// it's possible this method won't be called from the merging view so we need to
// ensure we only 'return' focus if we already have it
if gui.g.CurrentView() == gui.getMainView() {
return gui.switchFocus(g, v, gui.getFilesView())
}
return nil
}
func (gui *Gui) handleCompleteMerge() error {
if err := gui.stageSelectedFile(gui.g); err != nil {
return err
}
if err := gui.refreshFiles(); err != nil {
return err
}
// if we got conflicts after unstashing, we don't want to call any git
// commands to continue rebasing/merging here
if gui.State.WorkingTreeState == "normal" {
return gui.handleEscapeMerge(gui.g, gui.getMainView())
}
// if there are no more files with merge conflicts, we should ask whether the user wants to continue
if !gui.anyFilesWithMergeConflicts() {
return gui.promptToContinue()
}
return gui.handleEscapeMerge(gui.g, gui.getMainView())
}
// promptToContinue asks the user if they want to continue the rebase/merge that's in progress
func (gui *Gui) promptToContinue() error {
return gui.createConfirmationPanel(gui.g, gui.getFilesView(), true, "continue", gui.Tr.SLocalize("ConflictsResolved"), func(g *gocui.Gui, v *gocui.View) error {
return gui.genericMergeCommand("continue")
}, nil)
}

View File

@@ -0,0 +1,58 @@
package gui
import (
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) getBindings(v *gocui.View) []*Binding {
var (
bindingsGlobal, bindingsPanel []*Binding
)
bindings := gui.GetInitialKeybindings()
for _, binding := range bindings {
if GetKeyDisplay(binding.Key) != "" && binding.Description != "" {
switch binding.ViewName {
case "":
bindingsGlobal = append(bindingsGlobal, binding)
case v.Name():
if len(binding.Contexts) == 0 || utils.IncludesString(binding.Contexts, v.Context) {
bindingsPanel = append(bindingsPanel, binding)
}
}
}
}
// append dummy element to have a separator between
// panel and global keybindings
bindingsPanel = append(bindingsPanel, &Binding{})
return append(bindingsPanel, bindingsGlobal...)
}
func (gui *Gui) handleCreateOptionsMenu(g *gocui.Gui, v *gocui.View) error {
bindings := gui.getBindings(v)
menuItems := make([]*menuItem, len(bindings))
for i, binding := range bindings {
innerBinding := binding // note to self, never close over loop variables
menuItems[i] = &menuItem{
displayStrings: []string{GetKeyDisplay(innerBinding.Key), innerBinding.Description},
onPress: func() error {
if innerBinding.Key == nil {
return nil
}
if err := gui.handleMenuClose(g, v); err != nil {
return err
}
return innerBinding.Handler(g, v)
},
}
}
return gui.createMenu(strings.Title(gui.Tr.SLocalize("menu")), menuItems, createMenuOptions{})
}

View File

@@ -0,0 +1,102 @@
package gui
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int) error {
if !gui.GitCommand.PatchManager.CommitSelected() {
return gui.handleEscapePatchBuildingPanel(gui.g, nil)
}
gui.State.SplitMainPanel = true
gui.getMainView().Title = "Patch"
gui.getSecondaryView().Title = "Custom Patch"
// get diff from commit file that's currently selected
commitFile := gui.getSelectedCommitFile(gui.g)
if commitFile == nil {
return gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
}
diff, err := gui.GitCommand.ShowCommitFile(commitFile.Sha, commitFile.Name, true)
if err != nil {
return err
}
secondaryDiff := gui.GitCommand.PatchManager.RenderPatchForFile(commitFile.Name, true, false, true)
if err != nil {
return err
}
empty, err := gui.refreshLineByLinePanel(diff, secondaryDiff, false, selectedLineIdx)
if err != nil {
return err
}
if empty {
return gui.handleEscapePatchBuildingPanel(gui.g, nil)
}
return nil
}
func (gui *Gui) handleToggleSelectionForPatch(g *gocui.Gui, v *gocui.View) error {
state := gui.State.Panels.LineByLine
toggleFunc := gui.GitCommand.PatchManager.AddFileLineRange
filename := gui.getSelectedCommitFileName()
includedLineIndices := gui.GitCommand.PatchManager.GetFileIncLineIndices(filename)
currentLineIsStaged := utils.IncludesInt(includedLineIndices, state.SelectedLineIdx)
if currentLineIsStaged {
toggleFunc = gui.GitCommand.PatchManager.RemoveFileLineRange
}
// add range of lines to those set for the file
commitFile := gui.getSelectedCommitFile(gui.g)
if commitFile == nil {
return gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
}
toggleFunc(commitFile.Name, state.FirstLineIdx, state.LastLineIdx)
if err := gui.refreshCommitFilesView(); err != nil {
return err
}
if err := gui.refreshPatchBuildingPanel(-1); err != nil {
return err
}
return nil
}
func (gui *Gui) handleEscapePatchBuildingPanel(g *gocui.Gui, v *gocui.View) error {
gui.handleEscapeLineByLinePanel()
if gui.GitCommand.PatchManager.IsEmpty() {
gui.GitCommand.PatchManager.Reset()
gui.State.SplitMainPanel = false
}
return gui.switchFocus(gui.g, nil, gui.getCommitFilesView())
}
func (gui *Gui) refreshSecondaryPatchPanel() error {
if gui.GitCommand.PatchManager.CommitSelected() {
gui.State.SplitMainPanel = true
secondaryView := gui.getSecondaryView()
secondaryView.Highlight = true
secondaryView.Wrap = false
gui.g.Update(func(*gocui.Gui) error {
return gui.setViewContent(gui.g, gui.getSecondaryView(), gui.GitCommand.PatchManager.RenderAggregatedPatchColored(false))
})
} else {
gui.State.SplitMainPanel = false
}
return nil
}

View File

@@ -0,0 +1,122 @@
package gui
import (
"fmt"
"github.com/jesseduffield/gocui"
)
func (gui *Gui) handleCreatePatchOptionsMenu(g *gocui.Gui, v *gocui.View) error {
if !gui.GitCommand.PatchManager.CommitSelected() {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NoPatchError"))
}
menuItems := []*menuItem{
{
displayString: fmt.Sprintf("remove patch from original commit (%s)", gui.GitCommand.PatchManager.CommitSha),
onPress: gui.handleDeletePatchFromCommit,
},
{
displayString: "pull patch out into index",
onPress: gui.handlePullPatchIntoWorkingTree,
},
{
displayString: "reset patch",
onPress: gui.handleResetPatch,
},
}
selectedCommit := gui.getSelectedCommit(gui.g)
if selectedCommit != nil && gui.GitCommand.PatchManager.CommitSha != selectedCommit.Sha {
// adding this option to index 1
menuItems = append(
menuItems[:1],
append(
[]*menuItem{
{
displayString: fmt.Sprintf("move patch to selected commit (%s)", selectedCommit.Sha),
onPress: gui.handleMovePatchToSelectedCommit,
},
}, menuItems[1:]...,
)...,
)
}
return gui.createMenu(gui.Tr.SLocalize("PatchOptionsTitle"), menuItems, createMenuOptions{showCancel: true})
}
func (gui *Gui) getPatchCommitIndex() int {
for index, commit := range gui.State.Commits {
if commit.Sha == gui.GitCommand.PatchManager.CommitSha {
return index
}
}
return -1
}
func (gui *Gui) validateNormalWorkingTreeState() (bool, error) {
if gui.State.WorkingTreeState != "normal" {
return false, gui.createErrorPanel(gui.g, gui.Tr.SLocalize("CantPatchWhileRebasingError"))
}
return true, nil
}
func (gui *Gui) returnFocusFromLineByLinePanelIfNecessary() error {
if gui.State.MainContext == "patch-building" {
return gui.handleEscapePatchBuildingPanel(gui.g, nil)
}
return nil
}
func (gui *Gui) handleDeletePatchFromCommit() error {
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
return err
}
if err := gui.returnFocusFromLineByLinePanelIfNecessary(); err != nil {
return err
}
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
commitIndex := gui.getPatchCommitIndex()
err := gui.GitCommand.DeletePatchesFromCommit(gui.State.Commits, commitIndex, gui.GitCommand.PatchManager)
return gui.handleGenericMergeCommandResult(err)
})
}
func (gui *Gui) handleMovePatchToSelectedCommit() error {
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
return err
}
if err := gui.returnFocusFromLineByLinePanelIfNecessary(); err != nil {
return err
}
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
commitIndex := gui.getPatchCommitIndex()
err := gui.GitCommand.MovePatchToSelectedCommit(gui.State.Commits, commitIndex, gui.State.Panels.Commits.SelectedLine, gui.GitCommand.PatchManager)
return gui.handleGenericMergeCommandResult(err)
})
}
func (gui *Gui) handlePullPatchIntoWorkingTree() error {
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
return err
}
if err := gui.returnFocusFromLineByLinePanelIfNecessary(); err != nil {
return err
}
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
commitIndex := gui.getPatchCommitIndex()
err := gui.GitCommand.PullPatchIntoIndex(gui.State.Commits, commitIndex, gui.GitCommand.PatchManager)
return gui.handleGenericMergeCommandResult(err)
})
}
func (gui *Gui) handleResetPatch() error {
gui.GitCommand.PatchManager.Reset()
return gui.refreshCommitFilesView()
}

View File

@@ -0,0 +1,48 @@
package presentation
import (
"fmt"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func GetBranchListDisplayStrings(branches []*commands.Branch, isFocused bool, selectedLine int) [][]string {
lines := make([][]string, len(branches))
for i := range branches {
showUpstreamDifferences := isFocused && i == selectedLine
lines[i] = getBranchDisplayStrings(branches[i], showUpstreamDifferences)
}
return lines
}
// getBranchDisplayStrings returns the display string of branch
func getBranchDisplayStrings(b *commands.Branch, showUpstreamDifferences bool) []string {
displayName := utils.ColoredString(b.Name, GetBranchColor(b.Name))
if showUpstreamDifferences && b.Pushables != "" && b.Pullables != "" {
displayName = fmt.Sprintf("%s ↑%s↓%s", displayName, b.Pushables, b.Pullables)
}
return []string{b.Recency, displayName}
}
// GetBranchColor branch color
func GetBranchColor(name string) color.Attribute {
branchType := strings.Split(name, "/")[0]
switch branchType {
case "feature":
return color.FgGreen
case "bugfix":
return color.FgYellow
case "hotfix":
return color.FgRed
default:
return theme.DefaultTextColor
}
}

View File

@@ -0,0 +1,35 @@
package presentation
import (
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/theme"
)
func GetCommitFileListDisplayStrings(branches []*commands.CommitFile) [][]string {
lines := make([][]string, len(branches))
for i := range branches {
lines[i] = getCommitFileDisplayStrings(branches[i])
}
return lines
}
// getCommitFileDisplayStrings returns the display string of branch
func getCommitFileDisplayStrings(f *commands.CommitFile) []string {
yellow := color.New(color.FgYellow)
green := color.New(color.FgGreen)
defaultColor := color.New(theme.DefaultTextColor)
var colour *color.Color
switch f.Status {
case commands.UNSELECTED:
colour = defaultColor
case commands.WHOLE:
colour = green
case commands.PART:
colour = yellow
}
return []string{colour.Sprint(f.DisplayString)}
}

View File

@@ -0,0 +1,126 @@
package presentation
import (
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func GetCommitListDisplayStrings(commits []*commands.Commit, fullDescription bool) [][]string {
lines := make([][]string, len(commits))
var displayFunc func(*commands.Commit) []string
if fullDescription {
displayFunc = getFullDescriptionDisplayStringsForCommit
} else {
displayFunc = getDisplayStringsForCommit
}
for i := range commits {
lines[i] = displayFunc(commits[i])
}
return lines
}
func getFullDescriptionDisplayStringsForCommit(c *commands.Commit) []string {
red := color.New(color.FgRed)
yellow := color.New(color.FgYellow)
green := color.New(color.FgGreen)
blue := color.New(color.FgBlue)
cyan := color.New(color.FgCyan)
defaultColor := color.New(theme.DefaultTextColor)
magenta := color.New(color.FgMagenta)
// for some reason, setting the background to blue pads out the other commits
// horizontally. For the sake of accessibility I'm considering this a feature,
// not a bug
copied := color.New(color.FgCyan, color.BgBlue)
var shaColor *color.Color
switch c.Status {
case "unpushed":
shaColor = red
case "pushed":
shaColor = yellow
case "merged":
shaColor = green
case "rebasing":
shaColor = blue
case "reflog":
shaColor = blue
case "selected":
shaColor = magenta
default:
shaColor = defaultColor
}
if c.Copied {
shaColor = copied
}
tagString := ""
truncatedDate := utils.TruncateWithEllipsis(c.Date, 15)
secondColumnString := blue.Sprint(truncatedDate)
if c.Action != "" {
secondColumnString = cyan.Sprint(c.Action)
} else if c.ExtraInfo != "" {
tagColor := color.New(color.FgMagenta, color.Bold)
tagString = utils.ColoredStringDirect(c.ExtraInfo, tagColor) + " "
}
truncatedAuthor := utils.TruncateWithEllipsis(c.Author, 17)
return []string{shaColor.Sprint(c.Sha[:8]), secondColumnString, yellow.Sprint(truncatedAuthor), tagString + defaultColor.Sprint(c.Name)}
}
func getDisplayStringsForCommit(c *commands.Commit) []string {
red := color.New(color.FgRed)
yellow := color.New(color.FgYellow)
green := color.New(color.FgGreen)
blue := color.New(color.FgBlue)
cyan := color.New(color.FgCyan)
defaultColor := color.New(theme.DefaultTextColor)
magenta := color.New(color.FgMagenta)
// for some reason, setting the background to blue pads out the other commits
// horizontally. For the sake of accessibility I'm considering this a feature,
// not a bug
copied := color.New(color.FgCyan, color.BgBlue)
var shaColor *color.Color
switch c.Status {
case "unpushed":
shaColor = red
case "pushed":
shaColor = yellow
case "merged":
shaColor = green
case "rebasing":
shaColor = blue
case "reflog":
shaColor = blue
case "selected":
shaColor = magenta
default:
shaColor = defaultColor
}
if c.Copied {
shaColor = copied
}
actionString := ""
tagString := ""
if c.Action != "" {
actionString = cyan.Sprint(utils.WithPadding(c.Action, 7)) + " "
} else if len(c.Tags) > 0 {
tagColor := color.New(color.FgMagenta, color.Bold)
tagString = utils.ColoredStringDirect(strings.Join(c.Tags, " "), tagColor) + " "
}
return []string{shaColor.Sprint(c.Sha[:8]), actionString + tagString + defaultColor.Sprint(c.Name)}
}

View File

@@ -0,0 +1,36 @@
package presentation
import (
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/commands"
)
func GetFileListDisplayStrings(files []*commands.File) [][]string {
lines := make([][]string, len(files))
for i := range files {
lines[i] = getFileDisplayStrings(files[i])
}
return lines
}
// getFileDisplayStrings returns the display string of branch
func getFileDisplayStrings(f *commands.File) []string {
// potentially inefficient to be instantiating these color
// objects with each render
red := color.New(color.FgRed)
green := color.New(color.FgGreen)
if !f.Tracked && !f.HasStagedChanges {
return []string{red.Sprint(f.DisplayString)}
}
output := green.Sprint(f.DisplayString[0:1])
output += red.Sprint(f.DisplayString[1:3])
if f.HasUnstagedChanges {
output += red.Sprint(f.Name)
} else {
output += green.Sprint(f.Name)
}
return []string{output}
}

View File

@@ -0,0 +1,23 @@
package presentation
import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func GetRemoteBranchListDisplayStrings(branches []*commands.RemoteBranch) [][]string {
lines := make([][]string, len(branches))
for i := range branches {
lines[i] = getRemoteBranchDisplayStrings(branches[i])
}
return lines
}
// getRemoteBranchDisplayStrings returns the display string of branch
func getRemoteBranchDisplayStrings(b *commands.RemoteBranch) []string {
displayName := utils.ColoredString(b.Name, GetBranchColor(b.Name))
return []string{displayName}
}

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