mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-29 02:35:39 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
196c83d058 | ||
|
|
806bee9646 | ||
|
|
45a0378c01 | ||
|
|
afd669194a | ||
|
|
1494a3863d | ||
|
|
f5c55f066b | ||
|
|
bd8f198beb | ||
|
|
f05adb4f99 | ||
|
|
3ebb91c07a | ||
|
|
771e87ebeb | ||
|
|
2598ce1d4b | ||
|
|
e2f3b2b41f | ||
|
|
7ebb8343d1 | ||
|
|
42479a75af | ||
|
|
22c7110349 | ||
|
|
44ee28bb2e | ||
|
|
f172f20219 | ||
|
|
0f7003d939 | ||
|
|
d2d88fe64e | ||
|
|
fa2a385a0c | ||
|
|
bd9579983e | ||
|
|
66bd86b9b7 | ||
|
|
364bdcf532 | ||
|
|
ba7e098373 | ||
|
|
9f71c8d2b9 | ||
|
|
fce7cdcc0a | ||
|
|
4fb52ce2ab | ||
|
|
2915134007 | ||
|
|
2f893bf361 | ||
|
|
f815c5607c | ||
|
|
59d61f00a6 | ||
|
|
262ff24c5b | ||
|
|
1189c2fab7 | ||
|
|
3eb3de3edc | ||
|
|
94601b4dc9 | ||
|
|
9ca0073cd7 | ||
|
|
55e6366529 | ||
|
|
bd66162972 | ||
|
|
5cdfd41dca | ||
|
|
a95fd581fd | ||
|
|
fda9f4ea7a | ||
|
|
f876d8fdc8 | ||
|
|
4198bbae6c | ||
|
|
ade54b38c1 | ||
|
|
0dd2c869a8 | ||
|
|
ed85ea69bd | ||
|
|
953298de74 | ||
|
|
628404e114 | ||
|
|
5638a40007 | ||
|
|
d6005dc0eb | ||
|
|
b3a7acbdad | ||
|
|
88ae550b93 | ||
|
|
2c3f5be093 | ||
|
|
95a4ca6f8e | ||
|
|
23432dd909 | ||
|
|
148f601bcb | ||
|
|
43d891b8d6 | ||
|
|
2eee079d3a | ||
|
|
30a555b108 | ||
|
|
8be970e688 | ||
|
|
12bf851c7d | ||
|
|
c837c54c39 | ||
|
|
5874529f43 | ||
|
|
e290710f67 | ||
|
|
438abd6003 | ||
|
|
442f6cd854 | ||
|
|
c2b154acad | ||
|
|
fbd61fcd17 | ||
|
|
b1529f19ad | ||
|
|
134566ed49 | ||
|
|
8da93fd762 | ||
|
|
63209ef71e | ||
|
|
f63ec38aae | ||
|
|
f858c8e750 | ||
|
|
26f80087dd | ||
|
|
0ac402792b | ||
|
|
974c6510b8 | ||
|
|
41df63cdc4 | ||
|
|
4080e9b501 | ||
|
|
53da858c06 | ||
|
|
50c9ae863a | ||
|
|
ce20d1b482 | ||
|
|
fcf916d138 | ||
|
|
f3c87bde88 | ||
|
|
3f7136fc7d | ||
|
|
59f5f5c1af | ||
|
|
1956301b1c | ||
|
|
1fd0f31682 | ||
|
|
e6a1bd6566 | ||
|
|
609f3f4bfa | ||
|
|
9b42cd2214 | ||
|
|
2d90e1e8ee | ||
|
|
ddf25e14af | ||
|
|
48f1adad49 | ||
|
|
379d37a255 | ||
|
|
a59ac064d2 | ||
|
|
433d54fcec | ||
|
|
146722beb8 | ||
|
|
eb5e54e9fd | ||
|
|
99707a527d | ||
|
|
9ee7793782 | ||
|
|
bc410d8e4a | ||
|
|
7561f5aa32 | ||
|
|
2855b5b4d5 | ||
|
|
419cb9feb8 | ||
|
|
dbf6bb5f27 | ||
|
|
f601108c5d | ||
|
|
b77abdc5e1 | ||
|
|
2fac2f9f1f | ||
|
|
e4beaf4de9 | ||
|
|
d4f134c6c7 | ||
|
|
7ebed76d16 | ||
|
|
2b812b01e9 | ||
|
|
2f5d5034db | ||
|
|
a32947e7a7 | ||
|
|
2fdadd383a | ||
|
|
9a2dc3fe15 | ||
|
|
f0c3d3fc4d | ||
|
|
2488e0044d | ||
|
|
9c866fd49c | ||
|
|
6c270b6e26 | ||
|
|
ae1c4536e6 | ||
|
|
f5b22d94d9 | ||
|
|
3c87ff4eff | ||
|
|
0f7b2c45d7 | ||
|
|
a12d18146c | ||
|
|
119d5be1a4 | ||
|
|
fcdc0174d9 | ||
|
|
4f4df8f9cc | ||
|
|
c730271e09 | ||
|
|
ac0eedda91 | ||
|
|
e87635295a | ||
|
|
62a662054b | ||
|
|
dc183c0d82 | ||
|
|
08e039bea9 | ||
|
|
88d329c52a | ||
|
|
fd8a455aff | ||
|
|
ed4574bda9 | ||
|
|
c9ae54a8c8 | ||
|
|
6fb83b740b | ||
|
|
7f89113245 | ||
|
|
0ea0c48631 | ||
|
|
cec4cb48cb | ||
|
|
b211a14a66 | ||
|
|
a3d1455c83 | ||
|
|
1716de3b59 | ||
|
|
44d8b3e8f3 | ||
|
|
4f4bb40ea6 | ||
|
|
db826b3c87 | ||
|
|
be658e7d64 | ||
|
|
53f06f6a4e | ||
|
|
c8add47fe7 | ||
|
|
28cd827cea | ||
|
|
ffda2839e0 | ||
|
|
28208e8364 | ||
|
|
9b7a6934b3 | ||
|
|
15229bbdab | ||
|
|
63e6eea9ec | ||
|
|
50d5b9e8e7 | ||
|
|
cc872b0444 | ||
|
|
17b84e09c0 | ||
|
|
43f8bae267 | ||
|
|
b0fe963f8a | ||
|
|
0822a9296c | ||
|
|
d9fa02c53b | ||
|
|
c44ee71ad4 | ||
|
|
826d1660c9 | ||
|
|
291a8e4de0 | ||
|
|
f02ccca0e0 | ||
|
|
1e12a60b34 | ||
|
|
8430b04492 | ||
|
|
35b72420ad | ||
|
|
28ba142fd6 | ||
|
|
b39bcd5c61 | ||
|
|
1fd35f3824 | ||
|
|
e73937c2bd | ||
|
|
b51ad4fcea | ||
|
|
d1a7c7283f | ||
|
|
b641ecdc74 | ||
|
|
13f567ff4c | ||
|
|
771d4b5811 | ||
|
|
3c944e0351 | ||
|
|
e26af258d6 | ||
|
|
76e5ec6d45 | ||
|
|
27cd12e2d9 | ||
|
|
bfaf1c4f70 | ||
|
|
2d18d089ce | ||
|
|
9c7e40906d | ||
|
|
401f291c3b | ||
|
|
bea2ae5ff5 | ||
|
|
f49e4946f2 | ||
|
|
8ff74072f8 | ||
|
|
fcd5aea04e | ||
|
|
1c0da2967c | ||
|
|
1b78a42b80 | ||
|
|
79e73d2eff | ||
|
|
23299f88e9 | ||
|
|
ef744e45c1 | ||
|
|
660cc2f3d1 | ||
|
|
469ac116ef | ||
|
|
a86103479b | ||
|
|
d49e75bd3e | ||
|
|
f4718a9047 | ||
|
|
7d5fe4b66c | ||
|
|
845c80721f | ||
|
|
0e65db10d8 | ||
|
|
a9cc321981 | ||
|
|
6349214f00 | ||
|
|
96f821b841 | ||
|
|
964e3872c1 | ||
|
|
5dfa26ea8b | ||
|
|
dbf042b8ad | ||
|
|
014e06eefd | ||
|
|
39a2122dc0 | ||
|
|
fe6d8d62c5 | ||
|
|
570d27ffaa | ||
|
|
7b69aa1fda | ||
|
|
21e478dd59 | ||
|
|
d14fb36cb9 | ||
|
|
19a808642f | ||
|
|
e921ba0910 | ||
|
|
0f5a073d57 | ||
|
|
cb0bdd89c0 | ||
|
|
e89bf5d06b | ||
|
|
e82d2f37a1 | ||
|
|
65e955c622 | ||
|
|
e73f4c6b7e | ||
|
|
cf5cefb2d6 | ||
|
|
36ac764133 | ||
|
|
003e45d2f5 | ||
|
|
04e93317b8 | ||
|
|
f8dedb710b | ||
|
|
1c259f69f6 | ||
|
|
913f17ee3e | ||
|
|
6291c53966 | ||
|
|
267730bc00 | ||
|
|
d5db02a899 | ||
|
|
7ed8ee160d | ||
|
|
3dd33b65a0 | ||
|
|
b85048f616 | ||
|
|
0852f53455 | ||
|
|
10fa119ab3 | ||
|
|
b5404c6159 | ||
|
|
42d21c4bb6 | ||
|
|
cc13ae252a | ||
|
|
b97f844a3e | ||
|
|
1d6eb015c1 | ||
|
|
07a8ae8c3e | ||
|
|
f05a5e531e | ||
|
|
68586ec49a | ||
|
|
6cf75af0af | ||
|
|
304607ae5d | ||
|
|
e9f28855a2 | ||
|
|
66d7d5f312 | ||
|
|
59734f1069 | ||
|
|
2974a57943 | ||
|
|
fcdcd1c335 | ||
|
|
4a35f9fcdb | ||
|
|
674b14802e | ||
|
|
3e36affa69 | ||
|
|
97d7a8ad0c | ||
|
|
b89ba365d0 | ||
|
|
47ff388549 | ||
|
|
647ab9bf0f | ||
|
|
76431b4673 | ||
|
|
be0dd29e3a | ||
|
|
40fbce91ce | ||
|
|
33d287d2f0 | ||
|
|
9eb1cbc514 | ||
|
|
40b173118a | ||
|
|
8822c409e2 | ||
|
|
aa750c0819 | ||
|
|
d90d9d7330 | ||
|
|
a8db672ffb | ||
|
|
76b66ae26f | ||
|
|
a2790cfe8e | ||
|
|
624ae45ebb | ||
|
|
2756b82f57 | ||
|
|
52f41ab0d5 | ||
|
|
fbb767893e | ||
|
|
229f5ee48c | ||
|
|
96c7741ba0 | ||
|
|
517b7d0283 | ||
|
|
0c0231c3e8 | ||
|
|
a9559a5c87 | ||
|
|
814ee24c8d | ||
|
|
7876cddf4a | ||
|
|
e9051355a1 | ||
|
|
29316a528a | ||
|
|
036b53acf8 | ||
|
|
919463ff02 | ||
|
|
3f7ec3f3b8 | ||
|
|
19604214d7 | ||
|
|
f7add8d788 | ||
|
|
d97c230747 | ||
|
|
906a49049e | ||
|
|
c1a4bd0482 | ||
|
|
d0336fe16f | ||
|
|
61b4bbf74e | ||
|
|
384c2e13d7 | ||
|
|
198d237679 | ||
|
|
39315ca1e2 | ||
|
|
efb51eee96 | ||
|
|
fbbd16bd82 | ||
|
|
bd2c1eef53 | ||
|
|
d1395b15bb | ||
|
|
2d8ed5e274 | ||
|
|
6a5d8ba859 | ||
|
|
320e2a6536 | ||
|
|
3858118340 | ||
|
|
6420068569 |
@@ -1,5 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [jesseduffield]
|
||||
ko_fi: jesseduffield
|
||||
custom: ['https://donorbox.org/lazygit']
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
name: automerge
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
- unlabeled
|
||||
- synchronize
|
||||
- opened
|
||||
- edited
|
||||
- ready_for_review
|
||||
- reopened
|
||||
- unlocked
|
||||
pull_request_review:
|
||||
types:
|
||||
- submitted
|
||||
check_suite:
|
||||
types:
|
||||
- completed
|
||||
status: {}
|
||||
jobs:
|
||||
automerge:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: automerge
|
||||
uses: "pascalgn/automerge-action@135f0bdb927d9807b5446f7ca9ecc2c51de03c4a"
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
MERGE_METHOD: rebase
|
||||
@@ -21,12 +21,8 @@ jobs:
|
||||
uses: goreleaser/goreleaser-action@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_API_TOKEN}}
|
||||
- name: Get tag
|
||||
id: tag
|
||||
uses: dawidd6/action-get-tag@v1
|
||||
- name: Bump Homebrew
|
||||
uses: dawidd6/action-homebrew-bump-formula@v1
|
||||
uses: dawidd6/action-homebrew-bump-formula@v3
|
||||
with:
|
||||
token: ${{secrets.GITHUB_API_TOKEN}}
|
||||
formula: lazygit
|
||||
url: "https://github.com/${{github.repository}}/archive/${{steps.tag.outputs.tag}}.tar.gz"
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
name: Continuous Integration
|
||||
|
||||
on: push
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
|
||||
+2
-4
@@ -40,8 +40,8 @@ changelog:
|
||||
- '^bump'
|
||||
brews:
|
||||
-
|
||||
# Reporitory to push the tap to.
|
||||
github:
|
||||
# Repository to push the tap to.
|
||||
tap:
|
||||
owner: jesseduffield
|
||||
name: homebrew-lazygit
|
||||
|
||||
@@ -61,5 +61,3 @@ brews:
|
||||
# conflicts:
|
||||
# - svn
|
||||
# - bash
|
||||
|
||||
# test comment to see if goreleaser only releases on new commits
|
||||
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
# run with:
|
||||
# docker build -t lazygit .
|
||||
# docker run -it lazygit:latest /bin/sh -l
|
||||
# docker run -it lazygit:latest /bin/sh
|
||||
|
||||
FROM golang:1.14-alpine3.11
|
||||
WORKDIR /go/src/github.com/jesseduffield/lazygit/
|
||||
@@ -13,3 +13,5 @@ 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
|
||||
|
||||
ENTRYPOINT [ "lazygit" ]
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
# lazygit
|
||||
<p align="center">
|
||||
<img src="https://i.imgur.com/oYB7Cj8.png">
|
||||
</p>
|
||||
|
||||
 [](https://goreportcard.com/report/github.com/jesseduffield/lazygit) [](https://golangci.com) [](http://godoc.org/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.
|
||||
|
||||
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 to step through each hunk and if a hunk can't be split down any further but contains code you don't want to stage, you have to edit an arcane patch file _by hand_? *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!*
|
||||
@@ -18,15 +23,17 @@ If you're a mere mortal like me and you're tired of hearing how powerful git is
|
||||
- [MacPorts](#macports)
|
||||
- [Ubuntu](#ubuntu)
|
||||
- [Void Linux](#void-linux)
|
||||
- [Scoop (Windows)](<#scoop-(windows)>)
|
||||
- [Scoop (Windows)](#scoop-windows)
|
||||
- [Arch Linux](#arch-linux)
|
||||
- [Fedora and CentOS 7](#fedora-and-centos-7)
|
||||
- [Solus Linux](#solus-linux)
|
||||
- [FreeBSD](#freebsd)
|
||||
- [Conda](#conda)
|
||||
- [Go](#go)
|
||||
- [Usage](#usage)
|
||||
- [Keybindings](#keybindings)
|
||||
- [Changing directory on exit](#changing-directory-on-exit)
|
||||
- [Undo/Redo](#undo/redo)
|
||||
- [Undo/Redo](#undoredo)
|
||||
- [Configuration](#configuration)
|
||||
- [Custom pagers](#configuration)
|
||||
- [Tutorials](#tutorials)
|
||||
@@ -43,7 +50,7 @@ Github Sponsors is matching all donations dollar-for-dollar for 12 months so if
|
||||
|
||||
### Binary Releases
|
||||
|
||||
For Windows, Mac OS or Linux, you can download a binary release [here](/releases).
|
||||
For Windows, Mac OS or Linux, you can download a binary release [here](../../releases).
|
||||
|
||||
### Homebrew
|
||||
|
||||
@@ -124,6 +131,19 @@ sudo dnf copr enable atim/lazygit -y
|
||||
sudo dnf install lazygit
|
||||
```
|
||||
|
||||
### Solus Linux
|
||||
|
||||
```sh
|
||||
sudo eopkg install lazygit
|
||||
```
|
||||
|
||||
### FreeBSD
|
||||
|
||||
```sh
|
||||
pkg install lazygit
|
||||
```
|
||||
|
||||
|
||||
### Conda
|
||||
|
||||
Released versions are available for different platforms, see <https://anaconda.org/conda-forge/lazygit>
|
||||
@@ -245,4 +265,5 @@ If you want to see what I (Jesse) am up to in terms of development, follow me on
|
||||
|
||||
If you find that lazygit doesn't quite satisfy your requirements, these may be a better fit:
|
||||
|
||||
- [GitUI](https://github.com/Extrawurst/gitui)
|
||||
- [tig](https://github.com/jonas/tig)
|
||||
|
||||
+61
-5
@@ -4,6 +4,7 @@ Default path for the config file:
|
||||
|
||||
* Linux: `~/.config/jesseduffield/lazygit/config.yml`
|
||||
* MacOS: `~/Library/Application Support/jesseduffield/lazygit/config.yml`
|
||||
* Windows: `%APPDATA%\jesseduffield\lazygit\config.yml`
|
||||
|
||||
## Default
|
||||
|
||||
@@ -13,6 +14,8 @@ Default path for the config file:
|
||||
scrollHeight: 2 # how many lines you scroll by
|
||||
scrollPastBottom: true # enable scrolling past the bottom
|
||||
sidePanelWidth: 0.3333 # number from 0 to 1
|
||||
expandFocusedSidePanel: false
|
||||
mainPanelSplitMode: 'flexible' # one of 'horizontal' | 'flexible' | 'vertical'
|
||||
theme:
|
||||
lightTheme: false # For terminals with a light background
|
||||
activeBorderColor:
|
||||
@@ -23,11 +26,14 @@ Default path for the config file:
|
||||
optionsTextColor:
|
||||
- blue
|
||||
selectedLineBgColor:
|
||||
- default
|
||||
selectedRangeBgColor:
|
||||
- blue
|
||||
commitLength:
|
||||
show: true
|
||||
mouseEvents: true
|
||||
skipUnstageLineWarning: false
|
||||
skipStashWarning: true
|
||||
git:
|
||||
paging:
|
||||
colorArg: always
|
||||
@@ -35,13 +41,21 @@ Default path for the config file:
|
||||
merging:
|
||||
# only applicable to unix users
|
||||
manualCommit: false
|
||||
# extra args passed to `git merge`, e.g. --no-ff
|
||||
args: ""
|
||||
pull:
|
||||
mode: 'merge' # one of 'merge' | 'rebase' | 'ff-only'
|
||||
skipHookPrefix: WIP
|
||||
autoFetch: true
|
||||
branchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --"
|
||||
overrideGpg: false # prevents lazygit from spawning a separate process when using GPG
|
||||
update:
|
||||
method: prompt # can be: prompt | background | never
|
||||
days: 14 # how often an update is checked for
|
||||
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
|
||||
confirmOnQuit: false
|
||||
# determines whether hitting 'esc' will quit the application when there is nothing to cancel/close
|
||||
quitOnTopLevelReturn: true
|
||||
keybinding:
|
||||
universal:
|
||||
quit: 'q'
|
||||
@@ -53,6 +67,10 @@ Default path for the config file:
|
||||
nextItem: '<down>' # go one line down
|
||||
prevItem-alt: 'k' # go one line up
|
||||
nextItem-alt: 'j' # go one line down
|
||||
prevPage: ',' # go to next page in list
|
||||
nextPage: '.' # go to previous page in list
|
||||
gotoTop: '<' # go to top of list
|
||||
gotoBottom: '>' # go to bottom of list
|
||||
prevBlock: '<left>' # goto the previous block / panel
|
||||
nextBlock: '<right>' # goto the next block / panel
|
||||
prevBlock-alt: 'h' # goto the previous block / panel
|
||||
@@ -63,6 +81,8 @@ Default path for the config file:
|
||||
optionMenu-alt1: '?' # show help menu
|
||||
select: '<space>'
|
||||
goInto: '<enter>'
|
||||
confirm: '<enter>'
|
||||
confirm-alt1: 'y'
|
||||
remove: 'd'
|
||||
new: 'n'
|
||||
edit: 'e'
|
||||
@@ -85,6 +105,10 @@ Default path for the config file:
|
||||
prevScreenMode: '_'
|
||||
undo: 'z'
|
||||
redo: '<c-z>'
|
||||
filteringMenu: '<c-s>'
|
||||
diffingMenu: 'W'
|
||||
diffingMenu-alt: '<c-e>' # deprecated
|
||||
copyToClipboard: '<c-o>'
|
||||
status:
|
||||
checkForUpdate: 'u'
|
||||
recentRepos: '<enter>'
|
||||
@@ -128,7 +152,6 @@ Default path for the config file:
|
||||
cherryPickCopyRange: 'C'
|
||||
pasteCommits: 'v'
|
||||
tagCommit: 'T'
|
||||
toggleDiffCommit: 'i'
|
||||
checkoutCommit: '<space>'
|
||||
resetCherryPick: '<c-R>'
|
||||
stash:
|
||||
@@ -171,7 +194,7 @@ for users of VSCode
|
||||
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'code -r {{filename}}'
|
||||
openCommand: 'code -rg {{filename}}'
|
||||
```
|
||||
|
||||
## Color Attributes
|
||||
@@ -206,7 +229,20 @@ If you have issues with a light terminal theme where you can't read / see the te
|
||||
inactiveBorderColor:
|
||||
- black
|
||||
selectedLineBgColor:
|
||||
- blue
|
||||
- default
|
||||
```
|
||||
|
||||
## Struggling to see selected line
|
||||
|
||||
If you struggle to see the selected line I recomment using the reverse attribute on selected lines like so:
|
||||
|
||||
```yaml
|
||||
gui:
|
||||
theme:
|
||||
selectedLineBgColor:
|
||||
- reverse
|
||||
selectedRangeBgColor:
|
||||
- reverse
|
||||
```
|
||||
|
||||
## Example Coloring
|
||||
@@ -215,7 +251,7 @@ If you have issues with a light terminal theme where you can't read / see the te
|
||||
|
||||
## Keybindings
|
||||
|
||||
For all possible keybinding options, check [Custom_Keybinding.md](https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybinding.md)
|
||||
For all possible keybinding options, check [Custom_Keybindings.md](https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md)
|
||||
|
||||
### Example Keybindings For Colemak Users
|
||||
|
||||
@@ -235,14 +271,18 @@ For all possible keybinding options, check [Custom_Keybinding.md](https://github
|
||||
scrollDownMain-alt1: 'E'
|
||||
scrollUpMain-alt2: '<c-u>'
|
||||
scrollDownMain-alt2: '<c-e>'
|
||||
undo: 'l'
|
||||
redo: '<c-r>'
|
||||
diffingMenu: 'M'
|
||||
filteringMenu: '<c-f>'
|
||||
files:
|
||||
ignoreFile: 'I'
|
||||
commits:
|
||||
moveDownCommit: '<c-e>'
|
||||
moveUpCommit: '<c-u>'
|
||||
toggleDiffCommit: 'l'
|
||||
branches:
|
||||
viewGitFlowOptions: 'I'
|
||||
setUpstream: 'U'
|
||||
```
|
||||
|
||||
## Custom pull request URLs
|
||||
@@ -261,3 +301,19 @@ 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`
|
||||
|
||||
## Predefined commit message prefix
|
||||
In situations where certain naming pattern is used for branches and commits, pattern can be used to populate
|
||||
commit message with prefix that is parsed from the branch name.
|
||||
|
||||
Example:
|
||||
* Branch name: feature/AB-123
|
||||
* Commit message: [AB-123] Adding feature
|
||||
|
||||
```yaml
|
||||
git:
|
||||
commitPrefixes:
|
||||
my_project: # This is repository folder name
|
||||
pattern: "^\\w+\\/(\\w+-\\w+)"
|
||||
replace: "[$1] "
|
||||
```
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# 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).
|
||||
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.
|
||||
Support does not extend to Windows users, because we're making use of a package which doesn't have Windows support.
|
||||
|
||||
## Default:
|
||||
|
||||
@@ -13,7 +13,7 @@ git:
|
||||
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'.
|
||||
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:
|
||||
|
||||
@@ -61,4 +61,4 @@ git:
|
||||
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).
|
||||
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).
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: prev screen mode
|
||||
<kbd>:</kbd>: execute custom command
|
||||
<kbd>|</kbd>: view scoping options
|
||||
<kbd>∂</kbd>: open diff menu
|
||||
</pre>
|
||||
|
||||
## Branches Panel
|
||||
@@ -40,20 +42,30 @@
|
||||
<kbd>f</kbd>: fast-forward this branch from its upstream
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>R</kbd>: rename branch
|
||||
<kbd>ctrl+o</kbd>: copy branch name to clipboard
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Branches Panel (Remote Branches (in Remotes tab))
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: return to remotes list
|
||||
<kbd>esc</kbd>: Return to remotes list
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>space</kbd>: checkout
|
||||
<kbd>n</kbd>: new branch
|
||||
<kbd>M</kbd>: merge into currently checked out branch
|
||||
<kbd>d</kbd>: delete branch
|
||||
<kbd>r</kbd>: rebase checked-out branch onto this branch
|
||||
<kbd>u</kbd>: set as upstream of checked-out branch
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Branches Panel (Remotes Tab)
|
||||
@@ -63,7 +75,11 @@
|
||||
<kbd>n</kbd>: add new remote
|
||||
<kbd>d</kbd>: remove remote
|
||||
<kbd>e</kbd>: edit remote
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Branches Panel (Tags Tab)
|
||||
@@ -74,7 +90,11 @@
|
||||
<kbd>P</kbd>: push tag
|
||||
<kbd>n</kbd>: create tag
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Commit Files Panel
|
||||
@@ -84,9 +104,14 @@
|
||||
<kbd>c</kbd>: checkout file
|
||||
<kbd>d</kbd>: discard this commit's changes to this file
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>e</kbd>: edit file
|
||||
<kbd>space</kbd>: toggle file included in patch
|
||||
<kbd>enter</kbd>: enter file to add selected lines to the patch
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Commits Panel
|
||||
@@ -94,7 +119,6 @@
|
||||
<pre>
|
||||
<kbd>]</kbd>: next tab
|
||||
<kbd>[</kbd>: previous tab
|
||||
<kbd>/</kbd>: start search
|
||||
</pre>
|
||||
|
||||
## Commits Panel (Commits Tab)
|
||||
@@ -115,13 +139,19 @@
|
||||
<kbd>p</kbd>: pick commit (when mid-rebase)
|
||||
<kbd>t</kbd>: revert commit
|
||||
<kbd>c</kbd>: copy commit (cherry-pick)
|
||||
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||
<kbd>C</kbd>: copy commit range (cherry-pick)
|
||||
<kbd>v</kbd>: paste commits (cherry-pick)
|
||||
<kbd>enter</kbd>: view commit's files
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>i</kbd>: select commit to diff with another commit
|
||||
<kbd>n</kbd>: create new branch off of commit
|
||||
<kbd>T</kbd>: tag commit
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Commits Panel (Reflog Tab)
|
||||
@@ -129,6 +159,11 @@
|
||||
<pre>
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Files Panel
|
||||
@@ -151,7 +186,11 @@
|
||||
<kbd>enter</kbd>: stage individual hunks/lines
|
||||
<kbd>f</kbd>: fetch
|
||||
<kbd>g</kbd>: view upstream reset options
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Main Panel (Merging)
|
||||
@@ -178,6 +217,7 @@
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: exit line-by-line mode
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>▲</kbd>: select previous line
|
||||
<kbd>▼</kbd>: select next line
|
||||
<kbd>◄</kbd>: select previous hunk
|
||||
@@ -195,6 +235,7 @@
|
||||
<kbd>space</kbd>: toggle line staged / unstaged
|
||||
<kbd>d</kbd>: delete change (git reset)
|
||||
<kbd>tab</kbd>: switch to other panel
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>▲</kbd>: select previous line
|
||||
<kbd>▼</kbd>: select next line
|
||||
<kbd>◄</kbd>: select previous hunk
|
||||
@@ -213,8 +254,11 @@
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: close menu
|
||||
<kbd>q</kbd>: close menu
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Stash Panel
|
||||
@@ -223,7 +267,11 @@
|
||||
<kbd>space</kbd>: apply
|
||||
<kbd>g</kbd>: pop
|
||||
<kbd>d</kbd>: drop
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Status Panel
|
||||
|
||||
@@ -1,31 +1,33 @@
|
||||
# Lazygit Keybindings
|
||||
# Lazygit Sneltoetsen
|
||||
|
||||
## Global
|
||||
## Globaale Sneltoetsen
|
||||
|
||||
<pre>
|
||||
<kbd>pgup</kbd>: scroll up main panel (fn+up)
|
||||
<kbd>pgdown</kbd>: scroll down main panel (fn+down)
|
||||
<kbd>pgup</kbd>: scroll naar beneden vanaf hooft paneel (fn+up)
|
||||
<kbd>pgdown</kbd>: scroll naar beneden vabaf hooft paneel (fn+down)
|
||||
<kbd>m</kbd>: bekijk merge/rebase opties
|
||||
<kbd>ctrl+p</kbd>: view custom patch options
|
||||
<kbd>ctrl+p</kbd>: bekijk aangepaste patch opties
|
||||
<kbd>P</kbd>: push
|
||||
<kbd>p</kbd>: pull
|
||||
<kbd>R</kbd>: verversen
|
||||
<kbd>x</kbd>: open menu
|
||||
<kbd>z</kbd>: undo (via reflog) (experimental)
|
||||
<kbd>ctrl+z</kbd>: redo (via reflog) (experimental)
|
||||
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: prev screen mode
|
||||
<kbd>z</kbd>: ongedaan maken (via reflog) (experimenteel)
|
||||
<kbd>ctrl+z</kbd>: redo (via reflog) (experimenteel)
|
||||
<kbd>+</kbd>: volgende schermmode (normaal/half/groot )
|
||||
<kbd>_</kbd>: vorige schermmode
|
||||
<kbd>:</kbd>: voor aangepast commando uit
|
||||
<kbd>|</kbd>: bekijk scoping opties
|
||||
<kbd>∂</kbd>: open diff menu
|
||||
</pre>
|
||||
|
||||
## Branches Panel
|
||||
## Branches Paneel
|
||||
|
||||
<pre>
|
||||
<kbd>]</kbd>: next tab
|
||||
<kbd>[</kbd>: previous tab
|
||||
<kbd>]</kbd>: volgende tab
|
||||
<kbd>[</kbd>: vorige tab
|
||||
</pre>
|
||||
|
||||
## Branches Panel (Branches Tab)
|
||||
## Branches Paneel (Branches Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: uitchecken
|
||||
@@ -36,102 +38,135 @@
|
||||
<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>i</kbd>: laat git-flow opties zien
|
||||
<kbd>f</kbd>: fast-forward deze branch vanaf zijn upstream
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>R</kbd>: rename branch
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>R</kbd>: hernoem branch
|
||||
<kbd>ctrl+o</kbd>: copieer branch name naar clipboard
|
||||
<kbd>,</kbd>: vorige pagina
|
||||
<kbd>.</kbd>: volgende pagina
|
||||
<kbd><</kbd>: scroll naar boven
|
||||
<kbd>/</kbd>: start met zoekken
|
||||
<kbd>></kbd>: scroll naar beneden
|
||||
</pre>
|
||||
|
||||
## Branches Panel (Remote Branches (in Remotes tab))
|
||||
## Branches Paneel (Remote Branches (in Remotes tab))
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: return to remotes list
|
||||
<kbd>esc</kbd>: Ga terug naar remotes lijst
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>space</kbd>: uitchecken
|
||||
<kbd>n</kbd>: nieuwe branch
|
||||
<kbd>M</kbd>: merge in met huidige checked out branch
|
||||
<kbd>d</kbd>: verwijder branch
|
||||
<kbd>r</kbd>: rebase branch
|
||||
<kbd>u</kbd>: set as upstream of checked-out branch
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>u</kbd>: stel in als upstream van uitgecheckte branch
|
||||
<kbd>,</kbd>: vorige pagina
|
||||
<kbd>.</kbd>: volgende pagina
|
||||
<kbd><</kbd>: scroll naar boven
|
||||
<kbd>/</kbd>: start met zoekken
|
||||
<kbd>></kbd>: scroll naar beneden
|
||||
</pre>
|
||||
|
||||
## Branches Panel (Remotes Tab)
|
||||
## Branches Paneel (Remotes Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>f</kbd>: fetch remote
|
||||
<kbd>n</kbd>: add new remote
|
||||
<kbd>d</kbd>: remove remote
|
||||
<kbd>e</kbd>: edit remote
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>n</kbd>: voeg een nieuwe remote toe
|
||||
<kbd>d</kbd>: verwijder remote
|
||||
<kbd>e</kbd>: wijzig remote
|
||||
<kbd>,</kbd>: vorige pagina
|
||||
<kbd>.</kbd>: volgende pagina
|
||||
<kbd><</kbd>: scroll naar boven
|
||||
<kbd>/</kbd>: start met zoekken
|
||||
<kbd>></kbd>: scroll naar beneden
|
||||
</pre>
|
||||
|
||||
## Branches Panel (Tags Tab)
|
||||
## Branches Paneel (Tags Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: uitchecken
|
||||
<kbd>d</kbd>: delete tag
|
||||
<kbd>d</kbd>: verwijder tag
|
||||
<kbd>P</kbd>: push tag
|
||||
<kbd>n</kbd>: create tag
|
||||
<kbd>n</kbd>: creëer tag
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>,</kbd>: vorige pagina
|
||||
<kbd>.</kbd>: volgende pagina
|
||||
<kbd><</kbd>: scroll naar boven
|
||||
<kbd>/</kbd>: start met zoekken
|
||||
<kbd>></kbd>: scroll naar beneden
|
||||
</pre>
|
||||
|
||||
## Commit bestanden Panel
|
||||
## Commit bestanden Paneel
|
||||
|
||||
<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
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>e</kbd>: verander bestand
|
||||
<kbd>space</kbd>: toggle bestand inbegrepen in patch
|
||||
<kbd>enter</kbd>: enter bestand to add selecteered lines to the patch
|
||||
<kbd>,</kbd>: vorige pagina
|
||||
<kbd>.</kbd>: volgende pagina
|
||||
<kbd><</kbd>: scroll naar boven
|
||||
<kbd>/</kbd>: start met zoekken
|
||||
<kbd>></kbd>: scroll naar beneden
|
||||
</pre>
|
||||
|
||||
## Commits Panel
|
||||
## Commits Paneel
|
||||
|
||||
<pre>
|
||||
<kbd>]</kbd>: next tab
|
||||
<kbd>[</kbd>: previous tab
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>]</kbd>: volgende tab
|
||||
<kbd>[</kbd>: vorige tab
|
||||
</pre>
|
||||
|
||||
## Commits Panel (Commits Tab)
|
||||
## Commits Paneel (Commits Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>s</kbd>: squash beneden
|
||||
<kbd>r</kbd>: hernoem commit
|
||||
<kbd>R</kbd>: rename commit with editor
|
||||
<kbd>R</kbd>: hernoem commit met 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>ctrl+j</kbd>: verplaats commit 1 naar beneden
|
||||
<kbd>ctrl+k</kbd>: verplaats commit 1 naar boven
|
||||
<kbd>e</kbd>: wijzig commit
|
||||
<kbd>A</kbd>: wijzig commit met staged veranderingen
|
||||
<kbd>p</kbd>: pick commit (when mid-rebase)
|
||||
<kbd>t</kbd>: commit omgedaan maken
|
||||
<kbd>p</kbd>: kies commit (wanneer midden in rebase)
|
||||
<kbd>t</kbd>: commit ongedaan maken
|
||||
<kbd>c</kbd>: kopiëer commit (cherry-pick)
|
||||
<kbd>ctrl+o</kbd>: copieer commit SHA naar clipboard
|
||||
<kbd>C</kbd>: kopiëer commit reeks (cherry-pick)
|
||||
<kbd>v</kbd>: plak commits (cherry-pick)
|
||||
<kbd>enter</kbd>: bekijk gecommite bestanden
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>i</kbd>: select commit to diff with another commit
|
||||
<kbd>n</kbd>: create new branch off of commit
|
||||
<kbd>T</kbd>: tag commit
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (gecopieerde) commits selectie
|
||||
<kbd>,</kbd>: vorige pagina
|
||||
<kbd>.</kbd>: volgende pagina
|
||||
<kbd><</kbd>: scroll naar boven
|
||||
<kbd>/</kbd>: start met zoekken
|
||||
<kbd>></kbd>: scroll naar beneden
|
||||
</pre>
|
||||
|
||||
## Commits Panel (Reflog Tab)
|
||||
## Commits Paneel (Reflog Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>,</kbd>: vorige pagina
|
||||
<kbd>.</kbd>: volgende pagina
|
||||
<kbd><</kbd>: scroll naar boven
|
||||
<kbd>/</kbd>: start met zoekken
|
||||
<kbd>></kbd>: scroll naar beneden
|
||||
</pre>
|
||||
|
||||
## Bestanden Panel
|
||||
## Bestanden Paneel
|
||||
|
||||
<pre>
|
||||
<kbd>c</kbd>: Commit veranderingen
|
||||
@@ -145,21 +180,25 @@
|
||||
<kbd>i</kbd>: voeg toe aan .gitignore
|
||||
<kbd>r</kbd>: refresh bestanden
|
||||
<kbd>s</kbd>: stash-bestanden
|
||||
<kbd>S</kbd>: view stash options
|
||||
<kbd>S</kbd>: bekijk stash opties
|
||||
<kbd>a</kbd>: toggle staged alle
|
||||
<kbd>D</kbd>: bekijk reset opties
|
||||
<kbd>enter</kbd>: stage individuele hunks/lijnen
|
||||
<kbd>f</kbd>: fetch
|
||||
<kbd>g</kbd>: view upstream reset options
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>g</kbd>: bekijk upstream reset opties
|
||||
<kbd>,</kbd>: vorige pagina
|
||||
<kbd>.</kbd>: volgende pagina
|
||||
<kbd><</kbd>: scroll naar boven
|
||||
<kbd>/</kbd>: start met zoekken
|
||||
<kbd>></kbd>: scroll naar beneden
|
||||
</pre>
|
||||
|
||||
## Hoofd Panel (Merging)
|
||||
## Hooft Paneel (Merggen)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: ga terug naar het bestanden paneel
|
||||
<kbd>space</kbd>: pick hunk
|
||||
<kbd>b</kbd>: pick beide hunks
|
||||
<kbd>space</kbd>: kies hunk
|
||||
<kbd>b</kbd>: kies bijde hunks
|
||||
<kbd>◄</kbd>: selecteer voorgaand conflict
|
||||
<kbd>►</kbd>: selecteer volgende conflict
|
||||
<kbd>▲</kbd>: selecteer bovenste hunk
|
||||
@@ -167,70 +206,79 @@
|
||||
<kbd>z</kbd>: ongedaan maken
|
||||
</pre>
|
||||
|
||||
## Hoofd Panel (Normaal)
|
||||
## Hooft Paneel (Normaal)
|
||||
|
||||
<pre>
|
||||
<kbd> ̄</kbd>: scroll omlaag (fn+up)
|
||||
<kbd>¦</kbd>: scroll omhoog (fn+down)
|
||||
</pre>
|
||||
|
||||
## Hoofd Panel (Patch Building)
|
||||
## Hooft Paneel (Patch Bouwen)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: exit line-by-line mode
|
||||
<kbd>esc</kbd>: sluit lijn-bij-lijn mode
|
||||
<kbd>o</kbd>: open bestand
|
||||
<kbd>▲</kbd>: selecteer de vorige lijn
|
||||
<kbd>▼</kbd>: selecteer de volgende lijn
|
||||
<kbd>◄</kbd>: selecteer de vorige hunk
|
||||
<kbd>►</kbd>: selecteer de volgende hunk
|
||||
<kbd>space</kbd>: add/remove line(s) to patch
|
||||
<kbd>v</kbd>: toggle drag select
|
||||
<kbd>V</kbd>: toggle drag select
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
<kbd>space</kbd>: voeg toe/verwijder lijn(en) in patch
|
||||
<kbd>v</kbd>: toggle drag selecteer
|
||||
<kbd>V</kbd>: toggle drag selecteer
|
||||
<kbd>a</kbd>: toggle selecteer hunk
|
||||
</pre>
|
||||
|
||||
## Hoofd Panel (Stage Lines/Hunks)
|
||||
## Hooft Paneel (Staging)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: ga terug naar het bestanden paneel
|
||||
<kbd>space</kbd>: toggle line staged / unstaged
|
||||
<kbd>d</kbd>: delete change (git reset)
|
||||
<kbd>tab</kbd>: switch to other panel
|
||||
<kbd>space</kbd>: toggle lijnen staged / unstaged
|
||||
<kbd>d</kbd>: verwijdert change (git reset)
|
||||
<kbd>tab</kbd>: ga naar een ander paneel
|
||||
<kbd>o</kbd>: open bestand
|
||||
<kbd>▲</kbd>: selecteer de vorige lijn
|
||||
<kbd>▼</kbd>: selecteer de volgende lijn
|
||||
<kbd>◄</kbd>: selecteer de vorige hunk
|
||||
<kbd>►</kbd>: selecteer de volgende hunk
|
||||
<kbd>e</kbd>: verander bestand
|
||||
<kbd>o</kbd>: open bestand
|
||||
<kbd>v</kbd>: toggle drag select
|
||||
<kbd>V</kbd>: toggle drag select
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
<kbd>v</kbd>: toggle drag selecteer
|
||||
<kbd>V</kbd>: toggle drag selecteer
|
||||
<kbd>a</kbd>: toggle selecteer hunk
|
||||
<kbd>c</kbd>: Commit veranderingen
|
||||
<kbd>w</kbd>: commit veranderingen zonder pre-commit hook
|
||||
<kbd>C</kbd>: commit veranderingen met de git editor
|
||||
</pre>
|
||||
|
||||
## Menu Panel
|
||||
## Menu Paneel
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: close menu
|
||||
<kbd>q</kbd>: close menu
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>esc</kbd>: sluit menu
|
||||
<kbd>,</kbd>: vorige pagina
|
||||
<kbd>.</kbd>: volgende pagina
|
||||
<kbd><</kbd>: scroll naar boven
|
||||
<kbd>/</kbd>: start met zoekken
|
||||
<kbd>></kbd>: scroll naar beneden
|
||||
</pre>
|
||||
|
||||
## Stash Panel
|
||||
## Stash Paneel
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: toepassen
|
||||
<kbd>g</kbd>: pop
|
||||
<kbd>d</kbd>: drop
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>d</kbd>: laten vallen
|
||||
<kbd>,</kbd>: vorige pagina
|
||||
<kbd>.</kbd>: volgende pagina
|
||||
<kbd><</kbd>: scroll naar boven
|
||||
<kbd>/</kbd>: start met zoekken
|
||||
<kbd>></kbd>: scroll naar beneden
|
||||
</pre>
|
||||
|
||||
## Status Panel
|
||||
## Status Paneel
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: verander config file
|
||||
<kbd>o</kbd>: open config file
|
||||
<kbd>e</kbd>: verander config bestand
|
||||
<kbd>o</kbd>: open config bestand
|
||||
<kbd>u</kbd>: check voor updates
|
||||
<kbd>enter</kbd>: wissel naar een recente repo
|
||||
</pre>
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: prev screen mode
|
||||
<kbd>:</kbd>: execute custom command
|
||||
<kbd>|</kbd>: view scoping options
|
||||
<kbd>∂</kbd>: open diff menu
|
||||
</pre>
|
||||
|
||||
## Gałęzie Panel
|
||||
@@ -40,7 +42,12 @@
|
||||
<kbd>f</kbd>: fast-forward this branch from its upstream
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>R</kbd>: rename branch
|
||||
<kbd>ctrl+o</kbd>: copy branch name to clipboard
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Gałęzie Panel (Remote Branches (in Remotes tab))
|
||||
@@ -49,11 +56,16 @@
|
||||
<kbd>esc</kbd>: return to remotes list
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>space</kbd>: przełącz
|
||||
<kbd>n</kbd>: nowa gałąź
|
||||
<kbd>M</kbd>: scal do obecnej gałęzi
|
||||
<kbd>d</kbd>: usuń gałąź
|
||||
<kbd>r</kbd>: rebase branch
|
||||
<kbd>u</kbd>: set as upstream of checked-out branch
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Gałęzie Panel (Remotes Tab)
|
||||
@@ -63,7 +75,11 @@
|
||||
<kbd>n</kbd>: add new remote
|
||||
<kbd>d</kbd>: remove remote
|
||||
<kbd>e</kbd>: edit remote
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Gałęzie Panel (Tags Tab)
|
||||
@@ -74,7 +90,11 @@
|
||||
<kbd>P</kbd>: push tag
|
||||
<kbd>n</kbd>: create tag
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Commit files Panel
|
||||
@@ -84,9 +104,14 @@
|
||||
<kbd>c</kbd>: checkout file
|
||||
<kbd>d</kbd>: discard this commit's changes to this file
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>e</kbd>: edytuj plik
|
||||
<kbd>space</kbd>: toggle file included in patch
|
||||
<kbd>enter</kbd>: enter file to add selected lines to the patch
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Commity Panel
|
||||
@@ -94,7 +119,6 @@
|
||||
<pre>
|
||||
<kbd>]</kbd>: next tab
|
||||
<kbd>[</kbd>: previous tab
|
||||
<kbd>/</kbd>: start search
|
||||
</pre>
|
||||
|
||||
## Commity Panel (Commits Tab)
|
||||
@@ -115,13 +139,19 @@
|
||||
<kbd>p</kbd>: pick commit (when mid-rebase)
|
||||
<kbd>t</kbd>: revert commit
|
||||
<kbd>c</kbd>: copy commit (cherry-pick)
|
||||
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||
<kbd>C</kbd>: copy commit range (cherry-pick)
|
||||
<kbd>v</kbd>: paste commits (cherry-pick)
|
||||
<kbd>enter</kbd>: view commit's files
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>i</kbd>: select commit to diff with another commit
|
||||
<kbd>n</kbd>: create new branch off of commit
|
||||
<kbd>T</kbd>: tag commit
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Commity Panel (Reflog Tab)
|
||||
@@ -129,6 +159,11 @@
|
||||
<pre>
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Pliki Panel
|
||||
@@ -151,7 +186,11 @@
|
||||
<kbd>enter</kbd>: zatwierdź pojedyncze linie
|
||||
<kbd>f</kbd>: fetch
|
||||
<kbd>g</kbd>: view upstream reset options
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Main Panel (Merging)
|
||||
@@ -178,6 +217,7 @@
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: exit line-by-line mode
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>▲</kbd>: select previous line
|
||||
<kbd>▼</kbd>: select next line
|
||||
<kbd>◄</kbd>: select previous hunk
|
||||
@@ -195,6 +235,7 @@
|
||||
<kbd>space</kbd>: toggle line staged / unstaged
|
||||
<kbd>d</kbd>: delete change (git reset)
|
||||
<kbd>tab</kbd>: switch to other panel
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>▲</kbd>: select previous line
|
||||
<kbd>▼</kbd>: select next line
|
||||
<kbd>◄</kbd>: select previous hunk
|
||||
@@ -213,8 +254,11 @@
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: close menu
|
||||
<kbd>q</kbd>: close menu
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Schowek Panel
|
||||
@@ -223,7 +267,11 @@
|
||||
<kbd>space</kbd>: zastosuj
|
||||
<kbd>g</kbd>: wyciągnij
|
||||
<kbd>d</kbd>: porzuć
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Status Panel
|
||||
|
||||
@@ -3,22 +3,24 @@ module github.com/jesseduffield/lazygit
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.2
|
||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
|
||||
github.com/creack/pty v1.1.10-0.20191209115840-8ab47f72e854
|
||||
github.com/creack/pty v1.1.11
|
||||
github.com/fatih/color v1.7.0
|
||||
github.com/fsnotify/fsnotify v1.4.7
|
||||
github.com/go-errors/errors v1.0.1
|
||||
github.com/go-errors/errors v1.1.1
|
||||
github.com/go-git/go-git/v5 v5.0.0
|
||||
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.20200309001002-7765949e1c8a
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20200130214842-1d31d1faa3c9 // indirect
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20200824100831-1b6ec5d7d449
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe // 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/mattn/go-runewidth v0.0.9
|
||||
github.com/mgutz/str v1.2.0
|
||||
github.com/nicksnyder/go-i18n/v2 v2.0.3
|
||||
github.com/onsi/ginkgo v1.10.3 // indirect
|
||||
@@ -33,11 +35,7 @@ require (
|
||||
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/sys v0.0.0-20200824131525-c12d262b63d8 // 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
|
||||
)
|
||||
|
||||
@@ -12,6 +12,8 @@ github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYU
|
||||
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/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY=
|
||||
github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
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=
|
||||
@@ -23,9 +25,9 @@ github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc
|
||||
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/creack/pty v1.1.10-0.20191209115840-8ab47f72e854 h1:NB4neYMzyBsw52kUdkTrQm4Q05ErObCdwLvJptpfJSc=
|
||||
github.com/creack/pty v1.1.10-0.20191209115840-8ab47f72e854/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
|
||||
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew 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=
|
||||
@@ -42,8 +44,16 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
|
||||
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-errors/errors v1.1.1 h1:ljK/pL5ltg3qoN+OtN6yCv9HWSfMwxSx90GJCZQxYNg=
|
||||
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
|
||||
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
|
||||
github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM=
|
||||
github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.0.1 h1:q+IFMfLx200Q3scvt2hN79JsEzy4AmBTp/pqnefH+Bc=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.0.1/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
|
||||
github.com/go-git/go-git/v5 v5.0.0 h1:k5RWPm4iJwYtfWoxIJy4wJX9ON7ihPeZZYC1fLYDnpg=
|
||||
github.com/go-git/go-git/v5 v5.0.0/go.mod h1:oYD8y9kWsGINPFJoLdaScGCN6dlKg23blmClfZwtUVA=
|
||||
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=
|
||||
@@ -78,10 +88,10 @@ 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.20200309001002-7765949e1c8a h1:JSORQue6V4bMppr22dtUuYX+w79cgupo66PcGZ9ijlU=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20200309001002-7765949e1c8a/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
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/jesseduffield/gocui v0.3.1-0.20200824100831-1b6ec5d7d449 h1:G5Cm2QuFil8fnrMqUHYFiUkVSS/SXnn3ATtU7MbMFI0=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20200824100831-1b6ec5d7d449/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe h1:qsVhCf2RFyyKIUe/+gJblbCpXMUki9rZrHuEctg6M/E=
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4=
|
||||
github.com/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=
|
||||
@@ -100,9 +110,10 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB
|
||||
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/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
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=
|
||||
@@ -110,8 +121,8 @@ github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc
|
||||
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/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/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=
|
||||
@@ -122,13 +133,14 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
|
||||
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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
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=
|
||||
@@ -147,8 +159,8 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R
|
||||
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/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/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=
|
||||
@@ -175,13 +187,9 @@ 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=
|
||||
@@ -202,9 +210,8 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf
|
||||
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/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/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=
|
||||
@@ -215,10 +222,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
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/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/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=
|
||||
@@ -235,10 +240,11 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-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/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200824131525-c12d262b63d8 h1:AvbQYmiaaaza3cW3QXRyPo5kYgpFIzOAfeAAN7m3qQ4=
|
||||
golang.org/x/sys v0.0.0-20200824131525-c12d262b63d8/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=
|
||||
@@ -249,7 +255,6 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm
|
||||
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=
|
||||
@@ -259,17 +264,13 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
|
||||
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/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/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=
|
||||
|
||||
@@ -25,6 +25,9 @@ func main() {
|
||||
repoPath := "."
|
||||
flaggy.String(&repoPath, "p", "path", "Path of git repo")
|
||||
|
||||
filterPath := ""
|
||||
flaggy.String(&filterPath, "f", "filter", "Path to filter on in `git log -- <path>`. When in filter mode, the commits, reflog, and stash are filtered based on the given path, and some operations are restricted")
|
||||
|
||||
dump := ""
|
||||
flaggy.AddPositionalValue(&dump, "gitargs", 1, false, "Todo file")
|
||||
flaggy.DefaultParser.PositionalFlags[0].Hidden = true
|
||||
@@ -61,7 +64,7 @@ func main() {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
app, err := app.NewApp(appConfig)
|
||||
app, err := app.NewApp(appConfig, filterPath)
|
||||
|
||||
if err == nil {
|
||||
err = app.Run()
|
||||
|
||||
+30
-9
@@ -91,7 +91,7 @@ func newLogger(config config.AppConfigurer) *logrus.Entry {
|
||||
}
|
||||
|
||||
// NewApp bootstrap a new application
|
||||
func NewApp(config config.AppConfigurer) (*App, error) {
|
||||
func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
|
||||
app := &App{
|
||||
closers: []io.Closer{},
|
||||
Config: config,
|
||||
@@ -113,7 +113,8 @@ func NewApp(config config.AppConfigurer) (*App, error) {
|
||||
return app, err
|
||||
}
|
||||
|
||||
if err := app.setupRepo(); err != nil {
|
||||
showRecentRepos, err := app.setupRepo()
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
|
||||
@@ -121,29 +122,49 @@ func NewApp(config config.AppConfigurer) (*App, error) {
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config, app.Updater)
|
||||
app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config, app.Updater, filterPath, showRecentRepos)
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
return app, nil
|
||||
}
|
||||
|
||||
func (app *App) setupRepo() error {
|
||||
func (app *App) setupRepo() (bool, 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
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
info, _ := os.Stat(filepath.Join(cwd, ".git"))
|
||||
if info != nil && info.IsDir() {
|
||||
return false, err // Current directory appears to be a git repository.
|
||||
}
|
||||
|
||||
// Offer to initialize a new repository in current directory.
|
||||
fmt.Print(app.Tr.SLocalize("CreateRepo"))
|
||||
response, _ := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||
if strings.Trim(response, " \n") != "y" {
|
||||
// check if we have a recent repo we can open
|
||||
recentRepos := app.Config.GetAppState().RecentRepos
|
||||
if len(recentRepos) > 0 {
|
||||
var err error
|
||||
// try opening each repo in turn, in case any have been deleted
|
||||
for _, repoDir := range recentRepos {
|
||||
if err = os.Chdir(repoDir); err == nil {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := app.OSCommand.RunCommand("git init"); err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (app *App) Run() error {
|
||||
@@ -197,7 +218,7 @@ func (app *App) KnownError(err error) (string, bool) {
|
||||
|
||||
mappings := []errorMapping{
|
||||
{
|
||||
originalError: "fatal: not a git repository (or any of the parent directories): .git",
|
||||
originalError: "fatal: not a git repository",
|
||||
newError: app.Tr.SLocalize("notARepository"),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -12,3 +12,15 @@ type Branch struct {
|
||||
UpstreamName string
|
||||
Head bool
|
||||
}
|
||||
|
||||
func (b *Branch) RefName() string {
|
||||
return b.Name
|
||||
}
|
||||
|
||||
func (b *Branch) ID() string {
|
||||
return b.RefName()
|
||||
}
|
||||
|
||||
func (b *Branch) Description() string {
|
||||
return b.RefName()
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@@ -22,15 +21,17 @@ import (
|
||||
|
||||
// BranchListBuilder returns a list of Branch objects for the current repo
|
||||
type BranchListBuilder struct {
|
||||
Log *logrus.Entry
|
||||
GitCommand *GitCommand
|
||||
Log *logrus.Entry
|
||||
GitCommand *GitCommand
|
||||
ReflogCommits []*Commit
|
||||
}
|
||||
|
||||
// NewBranchListBuilder builds a new branch list builder
|
||||
func NewBranchListBuilder(log *logrus.Entry, gitCommand *GitCommand) (*BranchListBuilder, error) {
|
||||
func NewBranchListBuilder(log *logrus.Entry, gitCommand *GitCommand, reflogCommits []*Commit) (*BranchListBuilder, error) {
|
||||
return &BranchListBuilder{
|
||||
Log: log,
|
||||
GitCommand: gitCommand,
|
||||
Log: log,
|
||||
GitCommand: gitCommand,
|
||||
ReflogCommits: reflogCommits,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -51,7 +52,7 @@ func (b *BranchListBuilder) obtainBranches() []*Branch {
|
||||
|
||||
split := strings.Split(line, SEPARATION_CHAR)
|
||||
|
||||
name := split[1]
|
||||
name := strings.TrimPrefix(split[1], "heads/")
|
||||
branch := &Branch{
|
||||
Name: name,
|
||||
Pullables: "?",
|
||||
@@ -132,63 +133,28 @@ outer:
|
||||
}
|
||||
branches = append([]*Branch{{Name: currentBranchName, DisplayName: currentBranchDisplayName, Head: true, Recency: " *"}}, branches...)
|
||||
}
|
||||
|
||||
return branches
|
||||
}
|
||||
|
||||
// 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]
|
||||
}
|
||||
|
||||
// TODO: only look at the new reflog commits, and otherwise store the recencies in
|
||||
// int form against the branch to recalculate the time ago
|
||||
func (b *BranchListBuilder) obtainReflogBranches() []*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
|
||||
}
|
||||
|
||||
branchNameMap := map[string]bool{}
|
||||
|
||||
branchLines := utils.SplitLines(rawString)
|
||||
for _, line := range branchLines {
|
||||
recency, branchName := branchInfoFromLine(line)
|
||||
if branchName == "" {
|
||||
continue
|
||||
foundBranchesMap := map[string]bool{}
|
||||
re := regexp.MustCompile(`checkout: moving from ([\S]+) to ([\S]+)`)
|
||||
reflogBranches := make([]*Branch, 0, len(b.ReflogCommits))
|
||||
for _, commit := range b.ReflogCommits {
|
||||
if match := re.FindStringSubmatch(commit.Name); len(match) == 3 {
|
||||
recency := utils.UnixToTimeAgo(commit.UnixTimestamp)
|
||||
for _, branchName := range match[1:] {
|
||||
if !foundBranchesMap[branchName] {
|
||||
foundBranchesMap[branchName] = true
|
||||
reflogBranches = append(reflogBranches, &Branch{
|
||||
Recency: recency,
|
||||
Name: branchName,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, ok := branchNameMap[branchName]; ok {
|
||||
continue
|
||||
}
|
||||
branchNameMap[branchName] = true
|
||||
branch := &Branch{Name: branchName, Recency: recency}
|
||||
branches = append(branches, branch)
|
||||
}
|
||||
return branches
|
||||
return reflogBranches
|
||||
}
|
||||
|
||||
+22
-8
@@ -1,15 +1,17 @@
|
||||
package commands
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Commit : A git commit
|
||||
type Commit struct {
|
||||
Sha string
|
||||
Name string
|
||||
Status string // one of "unpushed", "pushed", "merged", "rebasing" or "selected"
|
||||
Action string // one of "", "pick", "edit", "squash", "reword", "drop", "fixup"
|
||||
Tags []string
|
||||
ExtraInfo string // something like 'HEAD -> master, tag: v0.15.2'
|
||||
Author string
|
||||
Date string
|
||||
Sha string
|
||||
Name string
|
||||
Status string // one of "unpushed", "pushed", "merged", "rebasing" or "selected"
|
||||
Action string // one of "", "pick", "edit", "squash", "reword", "drop", "fixup"
|
||||
Tags []string
|
||||
ExtraInfo string // something like 'HEAD -> master, tag: v0.15.2'
|
||||
Author string
|
||||
UnixTimestamp int64
|
||||
}
|
||||
|
||||
func (c *Commit) ShortSha() string {
|
||||
@@ -18,3 +20,15 @@ func (c *Commit) ShortSha() string {
|
||||
}
|
||||
return c.Sha[:8]
|
||||
}
|
||||
|
||||
func (c *Commit) RefName() string {
|
||||
return c.Sha
|
||||
}
|
||||
|
||||
func (c *Commit) ID() string {
|
||||
return c.RefName()
|
||||
}
|
||||
|
||||
func (c *Commit) Description() string {
|
||||
return fmt.Sprintf("%s %s", c.Sha[:7], c.Name)
|
||||
}
|
||||
|
||||
+15
-13
@@ -2,18 +2,20 @@ package commands
|
||||
|
||||
// CommitFile : A git commit file
|
||||
type CommitFile struct {
|
||||
Sha string
|
||||
Name string
|
||||
DisplayString string
|
||||
Status int // one of 'WHOLE' 'PART' 'NONE'
|
||||
// Parent is the identifier of the parent object e.g. a commit SHA if this commit file is for a commit, or a stash entry ref like 'stash@{1}'
|
||||
Parent string
|
||||
Name string
|
||||
|
||||
// PatchStatus tells us whether the file has been wholly or partially added to a patch. We might want to pull this logic up into the gui package and make it a map like we do with cherry picked commits
|
||||
PatchStatus int // one of 'WHOLE' 'PART' 'NONE'
|
||||
|
||||
ChangeStatus string // e.g. 'A' for added or 'M' for modified. This is based on the result from git diff --name-status
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
func (f *CommitFile) ID() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
func (f *CommitFile) Description() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
@@ -33,19 +34,17 @@ type CommitListBuilder struct {
|
||||
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) {
|
||||
func NewCommitListBuilder(log *logrus.Entry, gitCommand *GitCommand, osCommand *OSCommand, tr *i18n.Localizer, cherryPickedCommits []*Commit) *CommitListBuilder {
|
||||
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
|
||||
@@ -56,7 +55,7 @@ func (c *CommitListBuilder) extractCommitFromLine(line string) *Commit {
|
||||
split := strings.Split(line, SEPARATION_CHAR)
|
||||
|
||||
sha := split[0]
|
||||
date := split[1]
|
||||
unixTimestamp := split[1]
|
||||
author := split[2]
|
||||
extraInfo := strings.TrimSpace(split[3])
|
||||
message := strings.Join(split[4:], SEPARATION_CHAR)
|
||||
@@ -70,44 +69,59 @@ func (c *CommitListBuilder) extractCommitFromLine(line string) *Commit {
|
||||
}
|
||||
}
|
||||
|
||||
unitTimestampInt, _ := strconv.Atoi(unixTimestamp)
|
||||
|
||||
return &Commit{
|
||||
Sha: sha,
|
||||
Name: message,
|
||||
Tags: tags,
|
||||
ExtraInfo: extraInfo,
|
||||
Date: date,
|
||||
Author: author,
|
||||
Sha: sha,
|
||||
Name: message,
|
||||
Tags: tags,
|
||||
ExtraInfo: extraInfo,
|
||||
UnixTimestamp: int64(unitTimestampInt),
|
||||
Author: author,
|
||||
}
|
||||
}
|
||||
|
||||
type GetCommitsOptions struct {
|
||||
Limit bool
|
||||
FilterPath string
|
||||
IncludeRebaseCommits bool
|
||||
RefName string // e.g. "HEAD" or "my_branch"
|
||||
}
|
||||
|
||||
// GetCommits obtains the commits of the current branch
|
||||
func (c *CommitListBuilder) GetCommits(limit bool) ([]*Commit, error) {
|
||||
func (c *CommitListBuilder) GetCommits(opts GetCommitsOptions) ([]*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)
|
||||
rebaseMode := ""
|
||||
|
||||
if opts.IncludeRebaseCommits {
|
||||
var err error
|
||||
rebaseMode, err = c.GitCommand.RebaseMode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rebasingCommits) > 0 {
|
||||
commits = append(commits, rebasingCommits...)
|
||||
if rebaseMode != "" && opts.FilterPath == "" {
|
||||
// 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()
|
||||
cmd := c.getLogCmd(limit)
|
||||
|
||||
err = RunLineOutputCmd(cmd, func(line string) (bool, error) {
|
||||
commit := c.extractCommitFromLine(line)
|
||||
_, unpushed := unpushedCommits[commit.ShortSha()]
|
||||
commit.Status = map[bool]string{true: "unpushed", false: "pushed"}[unpushed]
|
||||
commits = append(commits, commit)
|
||||
unpushedCommits := c.getUnpushedCommits(opts.RefName)
|
||||
cmd := c.getLogCmd(opts)
|
||||
|
||||
err := RunLineOutputCmd(cmd, func(line string) (bool, error) {
|
||||
if strings.Split(line, " ")[0] != "gpg:" {
|
||||
commit := c.extractCommitFromLine(line)
|
||||
_, unpushed := unpushedCommits[commit.ShortSha()]
|
||||
commit.Status = map[bool]string{true: "unpushed", false: "pushed"}[unpushed]
|
||||
commits = append(commits, commit)
|
||||
}
|
||||
return false, nil
|
||||
})
|
||||
if err != nil {
|
||||
@@ -126,14 +140,6 @@ func (c *CommitListBuilder) GetCommits(limit bool) ([]*Commit, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, commit := range commits {
|
||||
for _, entry := range c.DiffEntries {
|
||||
if entry.Sha == commit.Sha {
|
||||
commit.Status = "selected"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return commits, nil
|
||||
}
|
||||
|
||||
@@ -216,6 +222,9 @@ func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*Commit, error) {
|
||||
if line == "" || line == "noop" {
|
||||
return commits, nil
|
||||
}
|
||||
if strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
splitLine := strings.Split(line, " ")
|
||||
commits = append([]*Commit{{
|
||||
Sha: splitLine[1],
|
||||
@@ -225,7 +234,7 @@ func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*Commit, error) {
|
||||
}}, commits...)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
return commits, nil
|
||||
}
|
||||
|
||||
// assuming the file starts like this:
|
||||
@@ -285,9 +294,9 @@ func (c *CommitListBuilder) getMergeBase() (string, error) {
|
||||
|
||||
// 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 {
|
||||
func (c *CommitListBuilder) getUnpushedCommits(refName string) map[string]bool {
|
||||
pushables := map[string]bool{}
|
||||
o, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..HEAD --abbrev-commit --abbrev=8")
|
||||
o, err := c.OSCommand.RunCommandWithOutput("git rev-list %s@{u}..%s --abbrev-commit --abbrev=8", refName, refName)
|
||||
if err != nil {
|
||||
return pushables
|
||||
}
|
||||
@@ -299,11 +308,16 @@ func (c *CommitListBuilder) getUnpushedCommits() map[string]bool {
|
||||
}
|
||||
|
||||
// getLog gets the git log.
|
||||
func (c *CommitListBuilder) getLogCmd(limit bool) *exec.Cmd {
|
||||
func (c *CommitListBuilder) getLogCmd(opts GetCommitsOptions) *exec.Cmd {
|
||||
limitFlag := ""
|
||||
if limit {
|
||||
if opts.Limit {
|
||||
limitFlag = "-300"
|
||||
}
|
||||
|
||||
return c.OSCommand.ExecutableFromString(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))
|
||||
filterFlag := ""
|
||||
if opts.FilterPath != "" {
|
||||
filterFlag = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(opts.FilterPath))
|
||||
}
|
||||
|
||||
return c.OSCommand.ExecutableFromString(fmt.Sprintf("git log %s --oneline --pretty=format:\"%%H%s%%at%s%%aN%s%%d%s%%s\" %s --abbrev=%d --date=unix %s", opts.RefName, SEPARATION_CHAR, SEPARATION_CHAR, SEPARATION_CHAR, SEPARATION_CHAR, limitFlag, 20, filterFlag))
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ func TestCommitListBuilderGetUnpushedCommits(t *testing.T) {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
c := NewDummyCommitListBuilder()
|
||||
c.OSCommand.SetCommand(s.command)
|
||||
s.test(c.getUnpushedCommits())
|
||||
s.test(c.getUnpushedCommits("HEAD"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@ func NewDummyOSCommand() *OSCommand {
|
||||
|
||||
// NewDummyAppConfig creates a new dummy AppConfig for testing
|
||||
func NewDummyAppConfig() *config.AppConfig {
|
||||
userConfig := viper.New()
|
||||
userConfig.SetConfigType("yaml")
|
||||
if err := config.LoadDefaults(userConfig, config.GetDefaultConfig()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
appConfig := &config.AppConfig{
|
||||
Name: "lazygit",
|
||||
Version: "unversioned",
|
||||
@@ -26,7 +31,7 @@ func NewDummyAppConfig() *config.AppConfig {
|
||||
BuildDate: "",
|
||||
Debug: false,
|
||||
BuildSource: "",
|
||||
UserConfig: viper.New(),
|
||||
UserConfig: userConfig,
|
||||
}
|
||||
_ = yaml.Unmarshal([]byte{}, appConfig.AppState)
|
||||
return appConfig
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// File : A file from git status
|
||||
// duplicating this for now
|
||||
type File struct {
|
||||
@@ -14,3 +20,27 @@ type File struct {
|
||||
Type string // one of 'file', 'directory', and 'other'
|
||||
ShortStatus string // e.g. 'AD', ' A', 'M ', '??'
|
||||
}
|
||||
|
||||
const RENAME_SEPARATOR = " -> "
|
||||
|
||||
func (f *File) IsRename() bool {
|
||||
return strings.Contains(f.Name, RENAME_SEPARATOR)
|
||||
}
|
||||
|
||||
// Names returns an array containing just the filename, or in the case of a rename, the after filename and the before filename
|
||||
func (f *File) Names() []string {
|
||||
return strings.Split(f.Name, RENAME_SEPARATOR)
|
||||
}
|
||||
|
||||
// returns true if the file names are the same or if a a file rename includes the filename of the other
|
||||
func (f *File) Matches(f2 *File) bool {
|
||||
return utils.StringArraysOverlap(f.Names(), f2.Names())
|
||||
}
|
||||
|
||||
func (f *File) ID() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
func (f *File) Description() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
+305
-99
@@ -15,12 +15,13 @@ import (
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
gogit "github.com/go-git/go-git/v5"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
gitconfig "github.com/tcnksm/go-gitconfig"
|
||||
gogit "gopkg.in/src-d/go-git.v4"
|
||||
)
|
||||
|
||||
// this takes something like:
|
||||
@@ -84,7 +85,10 @@ type GitCommand struct {
|
||||
removeFile func(string) error
|
||||
DotGitDir string
|
||||
onSuccessfulContinue func() error
|
||||
PatchManager *PatchManager
|
||||
PatchManager *patch.PatchManager
|
||||
|
||||
// Push to current determines whether the user has configured to push to the remote branch of the same name as the current or not
|
||||
PushToCurrent bool
|
||||
}
|
||||
|
||||
// NewGitCommand it runs git commands
|
||||
@@ -92,6 +96,15 @@ func NewGitCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localizer,
|
||||
var worktree *gogit.Worktree
|
||||
var repo *gogit.Repository
|
||||
|
||||
// see what our default push behaviour is
|
||||
output, err := osCommand.RunCommandWithOutput("git config --get push.default")
|
||||
pushToCurrent := false
|
||||
if err != nil {
|
||||
log.Errorf("error reading git config: %v", err)
|
||||
} else {
|
||||
pushToCurrent = strings.TrimSpace(output) == "current"
|
||||
}
|
||||
|
||||
fs := []func() error{
|
||||
func() error {
|
||||
return verifyInGitRepo(osCommand.RunCommand)
|
||||
@@ -128,9 +141,10 @@ func NewGitCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localizer,
|
||||
getLocalGitConfig: gitconfig.Local,
|
||||
removeFile: os.RemoveAll,
|
||||
DotGitDir: dotGitDir,
|
||||
PushToCurrent: pushToCurrent,
|
||||
}
|
||||
|
||||
gitCommand.PatchManager = NewPatchManager(log, gitCommand.ApplyPatch)
|
||||
gitCommand.PatchManager = patch.NewPatchManager(log, gitCommand.ApplyPatch, gitCommand.ShowFileDiff)
|
||||
|
||||
return gitCommand, nil
|
||||
}
|
||||
@@ -156,9 +170,7 @@ func findDotGitDir(stat func(string) (os.FileInfo, error), readFile func(filenam
|
||||
return strings.TrimSpace(strings.TrimPrefix(fileContent, "gitdir: ")), nil
|
||||
}
|
||||
|
||||
// GetStashEntries stash entries
|
||||
func (c *GitCommand) GetStashEntries() []*StashEntry {
|
||||
// if we directly put this string in RunCommandWithOutput the compiler complains because it thinks it's a format string
|
||||
func (c *GitCommand) getUnfilteredStashEntries() []*StashEntry {
|
||||
unescaped := "git stash list --pretty='%gs'"
|
||||
rawString, _ := c.OSCommand.RunCommandWithOutput(unescaped)
|
||||
stashEntries := []*StashEntry{}
|
||||
@@ -168,26 +180,75 @@ func (c *GitCommand) GetStashEntries() []*StashEntry {
|
||||
return stashEntries
|
||||
}
|
||||
|
||||
// GetStashEntries stash entries
|
||||
func (c *GitCommand) GetStashEntries(filterPath string) []*StashEntry {
|
||||
if filterPath == "" {
|
||||
return c.getUnfilteredStashEntries()
|
||||
}
|
||||
|
||||
unescaped := fmt.Sprintf("git stash list --name-only")
|
||||
rawString, err := c.OSCommand.RunCommandWithOutput(unescaped)
|
||||
if err != nil {
|
||||
return c.getUnfilteredStashEntries()
|
||||
}
|
||||
stashEntries := []*StashEntry{}
|
||||
var currentStashEntry *StashEntry
|
||||
lines := utils.SplitLines(rawString)
|
||||
isAStash := func(line string) bool { return strings.HasPrefix(line, "stash@{") }
|
||||
re := regexp.MustCompile(`stash@\{(\d+)\}`)
|
||||
|
||||
outer:
|
||||
for i := 0; i < len(lines); i++ {
|
||||
if !isAStash(lines[i]) {
|
||||
continue
|
||||
}
|
||||
match := re.FindStringSubmatch(lines[i])
|
||||
idx, err := strconv.Atoi(match[1])
|
||||
if err != nil {
|
||||
return c.getUnfilteredStashEntries()
|
||||
}
|
||||
currentStashEntry = stashEntryFromLine(lines[i], idx)
|
||||
for i+1 < len(lines) && !isAStash(lines[i+1]) {
|
||||
i++
|
||||
if lines[i] == filterPath {
|
||||
stashEntries = append(stashEntries, currentStashEntry)
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
}
|
||||
return stashEntries
|
||||
}
|
||||
|
||||
func stashEntryFromLine(line string, index int) *StashEntry {
|
||||
return &StashEntry{
|
||||
Name: line,
|
||||
Index: index,
|
||||
DisplayString: line,
|
||||
Name: line,
|
||||
Index: index,
|
||||
}
|
||||
}
|
||||
|
||||
// GetStashEntryDiff stash diff
|
||||
func (c *GitCommand) ShowStashEntryCmdStr(index int) string {
|
||||
return fmt.Sprintf("git stash show -p --color=%s stash@{%d}", c.colorArg(), index)
|
||||
return fmt.Sprintf("git stash show -p --stat --color=%s stash@{%d}", c.colorArg(), index)
|
||||
}
|
||||
|
||||
// GetStatusFiles git status files
|
||||
func (c *GitCommand) GetStatusFiles() []*File {
|
||||
statusOutput, _ := c.GitStatus()
|
||||
type GetStatusFileOptions struct {
|
||||
NoRenames bool
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*File {
|
||||
statusOutput, err := c.GitStatus(GitStatusOptions{NoRenames: opts.NoRenames})
|
||||
if err != nil {
|
||||
c.Log.Error(err)
|
||||
}
|
||||
statusStrings := utils.SplitLines(statusOutput)
|
||||
files := []*File{}
|
||||
|
||||
for _, statusString := range statusStrings {
|
||||
if strings.HasPrefix(statusString, "warning") {
|
||||
c.Log.Warning(statusString)
|
||||
continue
|
||||
}
|
||||
change := statusString[0:2]
|
||||
stagedChange := change[0:1]
|
||||
unstagedChange := statusString[1:2]
|
||||
@@ -226,7 +287,7 @@ func (c *GitCommand) StashSave(message string) error {
|
||||
}
|
||||
|
||||
// MergeStatusFiles merge status files
|
||||
func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []*File) []*File {
|
||||
func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []*File, selectedFile *File) []*File {
|
||||
if len(oldFiles) == 0 {
|
||||
return newFiles
|
||||
}
|
||||
@@ -237,10 +298,15 @@ func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []*File) []*File {
|
||||
result := []*File{}
|
||||
for _, oldFile := range oldFiles {
|
||||
for newIndex, newFile := range newFiles {
|
||||
if oldFile.Name == newFile.Name {
|
||||
if includesInt(appendedIndexes, newIndex) {
|
||||
continue
|
||||
}
|
||||
// if we just staged B and in doing so created 'A -> B' and we are currently have oldFile: A and newFile: 'A -> B', we want to wait until we come across B so the our cursor isn't jumping anywhere
|
||||
waitForMatchingFile := selectedFile != nil && newFile.IsRename() && !selectedFile.IsRename() && newFile.Matches(selectedFile) && !oldFile.Matches(selectedFile)
|
||||
|
||||
if oldFile.Matches(newFile) && !waitForMatchingFile {
|
||||
result = append(result, newFile)
|
||||
appendedIndexes = append(appendedIndexes, newIndex)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -311,11 +377,26 @@ func (c *GitCommand) RebaseBranch(branchName string) error {
|
||||
return c.OSCommand.RunPreparedCommand(cmd)
|
||||
}
|
||||
|
||||
type FetchOptions struct {
|
||||
PromptUserForCredential func(string) string
|
||||
RemoteName string
|
||||
BranchName string
|
||||
}
|
||||
|
||||
// Fetch fetch git repo
|
||||
func (c *GitCommand) Fetch(unamePassQuestion func(string) string, canAskForCredentials bool) error {
|
||||
return c.OSCommand.DetectUnamePass("git fetch", func(question string) string {
|
||||
if canAskForCredentials {
|
||||
return unamePassQuestion(question)
|
||||
func (c *GitCommand) Fetch(opts FetchOptions) error {
|
||||
command := "git fetch"
|
||||
|
||||
if opts.RemoteName != "" {
|
||||
command = fmt.Sprintf("%s %s", command, opts.RemoteName)
|
||||
}
|
||||
if opts.BranchName != "" {
|
||||
command = fmt.Sprintf("%s %s", command, opts.BranchName)
|
||||
}
|
||||
|
||||
return c.OSCommand.DetectUnamePass(command, func(question string) string {
|
||||
if opts.PromptUserForCredential != nil {
|
||||
return opts.PromptUserForCredential(question)
|
||||
}
|
||||
return "\n"
|
||||
})
|
||||
@@ -327,8 +408,8 @@ func (c *GitCommand) ResetToCommit(sha string, strength string, options RunComma
|
||||
}
|
||||
|
||||
// NewBranch create new branch
|
||||
func (c *GitCommand) NewBranch(name string, baseBranch string) error {
|
||||
return c.OSCommand.RunCommand("git checkout -b %s %s", name, baseBranch)
|
||||
func (c *GitCommand) NewBranch(name string, base string) error {
|
||||
return c.OSCommand.RunCommand("git checkout -b %s %s", name, base)
|
||||
}
|
||||
|
||||
// CurrentBranchName get the current branch name and displayname.
|
||||
@@ -372,9 +453,20 @@ func (c *GitCommand) ListStash() (string, error) {
|
||||
return c.OSCommand.RunCommandWithOutput("git stash list")
|
||||
}
|
||||
|
||||
type MergeOpts struct {
|
||||
FastForwardOnly bool
|
||||
}
|
||||
|
||||
// Merge merge
|
||||
func (c *GitCommand) Merge(branchName string) error {
|
||||
return c.OSCommand.RunCommand("git merge --no-edit %s", branchName)
|
||||
func (c *GitCommand) Merge(branchName string, opts MergeOpts) error {
|
||||
mergeArgs := c.Config.GetUserConfig().GetString("git.merging.args")
|
||||
|
||||
command := fmt.Sprintf("git merge --no-edit %s %s", mergeArgs, branchName)
|
||||
if opts.FastForwardOnly {
|
||||
command = fmt.Sprintf("%s --ff-only", command)
|
||||
}
|
||||
|
||||
return c.OSCommand.RunCommand(command)
|
||||
}
|
||||
|
||||
// AbortMerge abort merge
|
||||
@@ -385,6 +477,11 @@ func (c *GitCommand) AbortMerge() error {
|
||||
// usingGpg tells us whether the user has gpg enabled so that we can know
|
||||
// whether we need to run a subprocess to allow them to enter their password
|
||||
func (c *GitCommand) usingGpg() bool {
|
||||
overrideGpg := c.Config.GetUserConfig().GetBool("git.overrideGpg")
|
||||
if overrideGpg {
|
||||
return false
|
||||
}
|
||||
|
||||
gpgsign, _ := c.getLocalGitConfig("commit.gpgsign")
|
||||
if gpgsign == "" {
|
||||
gpgsign, _ = c.getGlobalGitConfig("commit.gpgsign")
|
||||
@@ -396,36 +493,40 @@ func (c *GitCommand) usingGpg() bool {
|
||||
|
||||
// Commit commits to git
|
||||
func (c *GitCommand) Commit(message string, flags string) (*exec.Cmd, error) {
|
||||
command := fmt.Sprintf("git commit %s -m %s", flags, c.OSCommand.Quote(message))
|
||||
command := fmt.Sprintf("git commit %s -m %s", flags, strconv.Quote(message))
|
||||
if c.usingGpg() {
|
||||
return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command), nil
|
||||
return c.OSCommand.ShellCommandFromString(command), nil
|
||||
}
|
||||
|
||||
return nil, c.OSCommand.RunCommand(command)
|
||||
}
|
||||
|
||||
// Get the subject of the HEAD commit
|
||||
func (c *GitCommand) GetHeadCommitMessage() (string, error) {
|
||||
cmdStr := "git log -1 --pretty=%s"
|
||||
message, err := c.OSCommand.RunCommandWithOutput(cmdStr)
|
||||
return strings.TrimSpace(message), err
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetCommitMessage(commitSha string) (string, error) {
|
||||
cmdStr := "git rev-list --format=%B --max-count=1 " + commitSha
|
||||
messageWithHeader, err := c.OSCommand.RunCommandWithOutput(cmdStr)
|
||||
message := strings.Join(strings.SplitAfter(messageWithHeader, "\n")[1:], "\n")
|
||||
return strings.TrimSpace(message), err
|
||||
}
|
||||
|
||||
// AmendHead amends HEAD with whatever is staged in your working tree
|
||||
func (c *GitCommand) AmendHead() (*exec.Cmd, error) {
|
||||
command := "git commit --amend --no-edit --allow-empty"
|
||||
if c.usingGpg() {
|
||||
return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command), nil
|
||||
return c.OSCommand.ShellCommandFromString(command), nil
|
||||
}
|
||||
|
||||
return nil, c.OSCommand.RunCommand(command)
|
||||
}
|
||||
|
||||
// Pull pulls from repo
|
||||
func (c *GitCommand) Pull(args string, ask func(string) string) error {
|
||||
return c.OSCommand.DetectUnamePass("git pull --no-edit "+args, ask)
|
||||
}
|
||||
|
||||
// PullWithoutPasswordCheck assumes that the pull will not prompt the user for a password
|
||||
func (c *GitCommand) PullWithoutPasswordCheck(args string) error {
|
||||
return c.OSCommand.RunCommand("git pull --no-edit " + args)
|
||||
}
|
||||
|
||||
// Push pushes to a branch
|
||||
func (c *GitCommand) Push(branchName string, force bool, upstream string, args string, ask func(string) string) error {
|
||||
func (c *GitCommand) Push(branchName string, force bool, upstream string, args string, promptUserForCredential func(string) string) error {
|
||||
forceFlag := ""
|
||||
if force {
|
||||
forceFlag = "--force-with-lease"
|
||||
@@ -437,17 +538,19 @@ func (c *GitCommand) Push(branchName string, force bool, upstream string, args s
|
||||
}
|
||||
|
||||
cmd := fmt.Sprintf("git push --follow-tags %s %s %s", forceFlag, setUpstreamArg, args)
|
||||
return c.OSCommand.DetectUnamePass(cmd, ask)
|
||||
return c.OSCommand.DetectUnamePass(cmd, promptUserForCredential)
|
||||
}
|
||||
|
||||
// CatFile obtains the content of a file
|
||||
func (c *GitCommand) CatFile(fileName string) (string, error) {
|
||||
return c.OSCommand.RunCommandWithOutput("cat %s", c.OSCommand.Quote(fileName))
|
||||
return c.OSCommand.RunCommandWithOutput("%s %s", c.OSCommand.Platform.catCmd, c.OSCommand.Quote(fileName))
|
||||
}
|
||||
|
||||
// StageFile stages a file
|
||||
func (c *GitCommand) StageFile(fileName string) error {
|
||||
return c.OSCommand.RunCommand("git add %s", c.OSCommand.Quote(fileName))
|
||||
// renamed files look like "file1 -> file2"
|
||||
fileNames := strings.Split(fileName, " -> ")
|
||||
return c.OSCommand.RunCommand("git add %s", c.OSCommand.Quote(fileNames[len(fileNames)-1]))
|
||||
}
|
||||
|
||||
// StageAll stages all files
|
||||
@@ -478,17 +581,21 @@ func (c *GitCommand) UnStageFile(fileName string, tracked bool) error {
|
||||
}
|
||||
|
||||
// GitStatus returns the plaintext short status of the repo
|
||||
func (c *GitCommand) GitStatus() (string, error) {
|
||||
return c.OSCommand.RunCommandWithOutput("git status --untracked-files=all --porcelain")
|
||||
type GitStatusOptions struct {
|
||||
NoRenames bool
|
||||
}
|
||||
|
||||
func (c *GitCommand) GitStatus(opts GitStatusOptions) (string, error) {
|
||||
noRenamesFlag := ""
|
||||
if opts.NoRenames {
|
||||
noRenamesFlag = "--no-renames"
|
||||
}
|
||||
return c.OSCommand.RunCommandWithOutput("git status --untracked-files=all --porcelain %s", noRenamesFlag)
|
||||
}
|
||||
|
||||
// IsInMergeState states whether we are still mid-merge
|
||||
func (c *GitCommand) IsInMergeState() (bool, error) {
|
||||
output, err := c.OSCommand.RunCommandWithOutput("git status --untracked-files=all")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return strings.Contains(output, "conclude merge") || strings.Contains(output, "unmerged paths"), nil
|
||||
return c.OSCommand.FileExists(fmt.Sprintf("%s/MERGE_HEAD", c.DotGitDir))
|
||||
}
|
||||
|
||||
// RebaseMode returns "" for non-rebase mode, "normal" for normal rebase
|
||||
@@ -509,8 +616,61 @@ func (c *GitCommand) RebaseMode() (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GitCommand) BeforeAndAfterFileForRename(file *File) (*File, *File, error) {
|
||||
|
||||
if !file.IsRename() {
|
||||
return nil, nil, errors.New("Expected renamed file")
|
||||
}
|
||||
|
||||
// we've got a file that represents a rename from one file to another. Unfortunately
|
||||
// our File abstraction fails to consider this case, so here we will refetch
|
||||
// all files, passing the --no-renames flag and then recursively call the function
|
||||
// again for the before file and after file. At some point we should fix the abstraction itself
|
||||
|
||||
split := strings.Split(file.Name, " -> ")
|
||||
filesWithoutRenames := c.GetStatusFiles(GetStatusFileOptions{NoRenames: true})
|
||||
var beforeFile *File
|
||||
var afterFile *File
|
||||
for _, f := range filesWithoutRenames {
|
||||
if f.Name == split[0] {
|
||||
beforeFile = f
|
||||
}
|
||||
if f.Name == split[1] {
|
||||
afterFile = f
|
||||
}
|
||||
}
|
||||
|
||||
if beforeFile == nil || afterFile == nil {
|
||||
return nil, nil, errors.New("Could not find deleted file or new file for file rename")
|
||||
}
|
||||
|
||||
if beforeFile.IsRename() || afterFile.IsRename() {
|
||||
// probably won't happen but we want to ensure we don't get an infinite loop
|
||||
return nil, nil, errors.New("Nested rename found")
|
||||
}
|
||||
|
||||
return beforeFile, afterFile, nil
|
||||
}
|
||||
|
||||
// DiscardAllFileChanges directly
|
||||
func (c *GitCommand) DiscardAllFileChanges(file *File) error {
|
||||
if file.IsRename() {
|
||||
beforeFile, afterFile, err := c.BeforeAndAfterFileForRename(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.DiscardAllFileChanges(beforeFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.DiscardAllFileChanges(afterFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// if the file isn't tracked, we assume you want to delete it
|
||||
quotedFileName := c.OSCommand.Quote(file.Name)
|
||||
if file.HasStagedChanges || file.HasMergeConflicts {
|
||||
@@ -573,12 +733,20 @@ func (c *GitCommand) Ignore(filename string) error {
|
||||
return c.OSCommand.AppendLineToFile(".gitignore", filename)
|
||||
}
|
||||
|
||||
func (c *GitCommand) ShowCmdStr(sha string) string {
|
||||
return fmt.Sprintf("git show --color=%s --no-renames --stat -p %s", c.colorArg(), sha)
|
||||
func (c *GitCommand) ShowCmdStr(sha string, filterPath string) string {
|
||||
filterPathArg := ""
|
||||
if filterPath != "" {
|
||||
filterPathArg = fmt.Sprintf(" -- %s", c.OSCommand.Quote(filterPath))
|
||||
}
|
||||
return fmt.Sprintf("git show --color=%s --no-renames --stat -p %s %s", c.colorArg(), sha, filterPathArg)
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetBranchGraphCmdStr(branchName string) string {
|
||||
return fmt.Sprintf("git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium %s --", branchName)
|
||||
branchLogCmdTemplate := c.Config.GetUserConfig().GetString("git.branchLogCmd")
|
||||
templateValues := map[string]string{
|
||||
"branchName": branchName,
|
||||
}
|
||||
return utils.ResolvePlaceholderString(branchLogCmdTemplate, templateValues)
|
||||
}
|
||||
|
||||
// GetRemoteURL returns current repo remote url
|
||||
@@ -597,14 +765,14 @@ func (c *GitCommand) CheckRemoteBranchExists(branch *Branch) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Diff returns the diff of a file
|
||||
func (c *GitCommand) Diff(file *File, plain bool, cached bool) string {
|
||||
// WorktreeFileDiff returns the diff of a file
|
||||
func (c *GitCommand) WorktreeFileDiff(file *File, plain bool, cached bool) string {
|
||||
// for now we assume an error means the file was deleted
|
||||
s, _ := c.OSCommand.RunCommandWithOutput(c.DiffCmdStr(file, plain, cached))
|
||||
s, _ := c.OSCommand.RunCommandWithOutput(c.WorktreeFileDiffCmdStr(file, plain, cached))
|
||||
return s
|
||||
}
|
||||
|
||||
func (c *GitCommand) DiffCmdStr(file *File, plain bool, cached bool) string {
|
||||
func (c *GitCommand) WorktreeFileDiffCmdStr(file *File, plain bool, cached bool) string {
|
||||
cachedArg := ""
|
||||
trackedArg := "--"
|
||||
colorArg := c.colorArg()
|
||||
@@ -638,8 +806,9 @@ func (c *GitCommand) ApplyPatch(patch string, flags ...string) error {
|
||||
return c.OSCommand.RunCommand("git apply %s %s", flagStr, c.OSCommand.Quote(filepath))
|
||||
}
|
||||
|
||||
func (c *GitCommand) FastForward(branchName string, remoteName string, remoteBranchName string) error {
|
||||
return c.OSCommand.RunCommand("git fetch %s %s:%s", remoteName, remoteBranchName, branchName)
|
||||
func (c *GitCommand) FastForward(branchName string, remoteName string, remoteBranchName string, promptUserForCredential func(string) string) error {
|
||||
command := fmt.Sprintf("git fetch %s %s:%s", remoteName, remoteBranchName, branchName)
|
||||
return c.OSCommand.DetectUnamePass(command, promptUserForCredential)
|
||||
}
|
||||
|
||||
func (c *GitCommand) RunSkipEditorCommand(command string) error {
|
||||
@@ -648,6 +817,7 @@ func (c *GitCommand) RunSkipEditorCommand(command string) error {
|
||||
cmd.Env = append(
|
||||
cmd.Env,
|
||||
"LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY",
|
||||
"GIT_EDITOR="+lazyGitPath,
|
||||
"EDITOR="+lazyGitPath,
|
||||
"VISUAL="+lazyGitPath,
|
||||
)
|
||||
@@ -665,7 +835,10 @@ func (c *GitCommand) GenericMerge(commandType string, command string) error {
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
if !strings.Contains(err.Error(), "no rebase in progress") {
|
||||
return err
|
||||
}
|
||||
c.Log.Warn(err)
|
||||
}
|
||||
|
||||
// sometimes we need to do a sequence of things in a rebase but the user needs to
|
||||
@@ -758,7 +931,7 @@ func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string
|
||||
)
|
||||
|
||||
if overrideEditor {
|
||||
cmd.Env = append(cmd.Env, "EDITOR="+ex)
|
||||
cmd.Env = append(cmd.Env, "GIT_EDITOR="+ex)
|
||||
}
|
||||
|
||||
return cmd, nil
|
||||
@@ -879,45 +1052,67 @@ func (c *GitCommand) CherryPickCommits(commits []*Commit) error {
|
||||
return c.OSCommand.RunPreparedCommand(cmd)
|
||||
}
|
||||
|
||||
// GetCommitFiles get the specified commit files
|
||||
func (c *GitCommand) GetCommitFiles(commitSha string, patchManager *PatchManager) ([]*CommitFile, error) {
|
||||
files, err := c.OSCommand.RunCommandWithOutput("git diff-tree --no-commit-id --name-only -r --no-renames %s", commitSha)
|
||||
// GetFilesInDiff get the specified commit files
|
||||
func (c *GitCommand) GetFilesInDiff(from string, to string, reverse bool, patchManager *patch.PatchManager) ([]*CommitFile, error) {
|
||||
reverseFlag := ""
|
||||
if reverse {
|
||||
reverseFlag = " -R "
|
||||
}
|
||||
|
||||
filenames, err := c.OSCommand.RunCommandWithOutput("git diff --name-status %s %s %s", reverseFlag, from, to)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.GetCommitFilesFromFilenames(filenames, to, patchManager), nil
|
||||
}
|
||||
|
||||
// filenames string is something like "file1\nfile2\nfile3"
|
||||
func (c *GitCommand) GetCommitFilesFromFilenames(filenames string, parent string, patchManager *patch.PatchManager) []*CommitFile {
|
||||
commitFiles := make([]*CommitFile, 0)
|
||||
|
||||
for _, file := range strings.Split(strings.TrimRight(files, "\n"), "\n") {
|
||||
status := UNSELECTED
|
||||
if patchManager != nil && patchManager.CommitSha == commitSha {
|
||||
status = patchManager.GetFileStatus(file)
|
||||
for _, line := range strings.Split(strings.TrimRight(filenames, "\n"), "\n") {
|
||||
// typical result looks like 'A my_file' meaning my_file was added
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
changeStatus := line[0:1]
|
||||
name := line[2:]
|
||||
status := patch.UNSELECTED
|
||||
if patchManager != nil && patchManager.To == parent {
|
||||
status = patchManager.GetFileStatus(name)
|
||||
}
|
||||
|
||||
commitFiles = append(commitFiles, &CommitFile{
|
||||
Sha: commitSha,
|
||||
Name: file,
|
||||
DisplayString: file,
|
||||
Status: status,
|
||||
Parent: parent,
|
||||
Name: name,
|
||||
ChangeStatus: changeStatus,
|
||||
PatchStatus: status,
|
||||
})
|
||||
}
|
||||
|
||||
return commitFiles, nil
|
||||
return commitFiles
|
||||
}
|
||||
|
||||
// ShowCommitFile get the diff of specified commit file
|
||||
func (c *GitCommand) ShowCommitFile(commitSha, fileName string, plain bool) (string, error) {
|
||||
cmdStr := c.ShowCommitFileCmdStr(commitSha, fileName, plain)
|
||||
// ShowFileDiff get the diff of specified from and to. Typically this will be used for a single commit so it'll be 123abc^..123abc
|
||||
// but when we're in diff mode it could be any 'from' to any 'to'. The reverse flag is also here thanks to diff mode.
|
||||
func (c *GitCommand) ShowFileDiff(from string, to string, reverse bool, fileName string, plain bool) (string, error) {
|
||||
cmdStr := c.ShowFileDiffCmdStr(from, to, reverse, fileName, plain)
|
||||
return c.OSCommand.RunCommandWithOutput(cmdStr)
|
||||
}
|
||||
|
||||
func (c *GitCommand) ShowCommitFileCmdStr(commitSha, fileName string, plain bool) string {
|
||||
func (c *GitCommand) ShowFileDiffCmdStr(from string, to string, reverse bool, fileName string, plain bool) string {
|
||||
colorArg := c.colorArg()
|
||||
if plain {
|
||||
colorArg = "never"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("git show --no-renames --color=%s %s -- %s", colorArg, commitSha, fileName)
|
||||
reverseFlag := ""
|
||||
if reverse {
|
||||
reverseFlag = " -R "
|
||||
}
|
||||
|
||||
return fmt.Sprintf("git diff --no-renames --color=%s %s %s %s -- %s", colorArg, from, to, reverseFlag, fileName)
|
||||
}
|
||||
|
||||
// CheckoutFile checks out the file for the given commit
|
||||
@@ -981,11 +1176,6 @@ func (c *GitCommand) ResetSoft(ref string) error {
|
||||
return c.OSCommand.RunCommand("git reset --soft " + ref)
|
||||
}
|
||||
|
||||
// DiffCommits show diff between commits
|
||||
func (c *GitCommand) DiffCommits(sha1, sha2 string) (string, error) {
|
||||
return c.OSCommand.RunCommandWithOutput("git diff --color=%s --stat -p %s %s", c.colorArg(), sha1, sha2)
|
||||
}
|
||||
|
||||
// CreateFixupCommit creates a commit that fixes up a previous commit
|
||||
func (c *GitCommand) CreateFixupCommit(sha string) error {
|
||||
return c.OSCommand.RunCommand("git commit --fixup=%s", sha)
|
||||
@@ -1028,7 +1218,7 @@ func (c *GitCommand) StashSaveStagedChanges(message string) error {
|
||||
// if you had staged an untracked file, that will now appear as 'AD' in git status
|
||||
// meaning it's deleted in your working tree but added in your index. Given that it's
|
||||
// now safely stashed, we need to remove it.
|
||||
files := c.GetStatusFiles()
|
||||
files := c.GetStatusFiles(GetStatusFileOptions{})
|
||||
for _, file := range files {
|
||||
if file.ShortStatus == "AD" {
|
||||
if err := c.UnStageFile(file.Name, false); err != nil {
|
||||
@@ -1120,33 +1310,37 @@ func (c *GitCommand) FetchRemote(remoteName string) error {
|
||||
return c.OSCommand.RunCommand("git fetch %s", remoteName)
|
||||
}
|
||||
|
||||
// GetNewReflogCommits only returns the new reflog commits since the given lastReflogCommit
|
||||
// GetReflogCommits only returns the new reflog commits since the given lastReflogCommit
|
||||
// if none is passed (i.e. it's value is nil) then we get all the reflog commits
|
||||
func (c *GitCommand) GetNewReflogCommits(lastReflogCommit *Commit) ([]*Commit, error) {
|
||||
output, err := c.OSCommand.RunCommandWithOutput("git reflog --abbrev=20 --date=iso")
|
||||
if err != nil {
|
||||
// assume error means we have no reflog
|
||||
return []*Commit{}, nil
|
||||
|
||||
func (c *GitCommand) GetReflogCommits(lastReflogCommit *Commit, filterPath string) ([]*Commit, bool, error) {
|
||||
commits := make([]*Commit, 0)
|
||||
re := regexp.MustCompile(`(\w+).*HEAD@\{([^\}]+)\}: (.*)`)
|
||||
|
||||
filterPathArg := ""
|
||||
if filterPath != "" {
|
||||
filterPathArg = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(filterPath))
|
||||
}
|
||||
|
||||
lines := strings.Split(strings.TrimSpace(output), "\n")
|
||||
commits := make([]*Commit, 0, len(lines))
|
||||
re := regexp.MustCompile(`(\w+).*HEAD@\{([^\}]+)\}: (.*)`)
|
||||
cmd := c.OSCommand.ExecutableFromString("git reflog --abbrev=20 --date=iso")
|
||||
err = RunLineOutputCmd(cmd, func(line string) (bool, error) {
|
||||
cmd := c.OSCommand.ExecutableFromString(fmt.Sprintf("git reflog --abbrev=20 --date=unix %s", filterPathArg))
|
||||
onlyObtainedNewReflogCommits := false
|
||||
err := RunLineOutputCmd(cmd, func(line string) (bool, error) {
|
||||
match := re.FindStringSubmatch(line)
|
||||
if len(match) <= 1 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
unixTimestamp, _ := strconv.Atoi(match[2])
|
||||
|
||||
commit := &Commit{
|
||||
Sha: match[1],
|
||||
Name: match[3],
|
||||
Date: match[2],
|
||||
Status: "reflog",
|
||||
Sha: match[1],
|
||||
Name: match[3],
|
||||
UnixTimestamp: int64(unixTimestamp),
|
||||
Status: "reflog",
|
||||
}
|
||||
|
||||
if lastReflogCommit != nil && commit.Sha == lastReflogCommit.Sha && commit.Date == lastReflogCommit.Date {
|
||||
if lastReflogCommit != nil && commit.Sha == lastReflogCommit.Sha && commit.UnixTimestamp == lastReflogCommit.UnixTimestamp {
|
||||
onlyObtainedNewReflogCommits = true
|
||||
// after this point we already have these reflogs loaded so we'll simply return the new ones
|
||||
return true, nil
|
||||
}
|
||||
@@ -1155,10 +1349,10 @@ func (c *GitCommand) GetNewReflogCommits(lastReflogCommit *Commit) ([]*Commit, e
|
||||
return false, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
return commits, nil
|
||||
return commits, onlyObtainedNewReflogCommits, nil
|
||||
}
|
||||
|
||||
func (c *GitCommand) ConfiguredPager() string {
|
||||
@@ -1198,3 +1392,15 @@ func (c *GitCommand) colorArg() string {
|
||||
func (c *GitCommand) RenameBranch(oldName string, newName string) error {
|
||||
return c.OSCommand.RunCommand("git branch --move %s %s", oldName, newName)
|
||||
}
|
||||
|
||||
func (c *GitCommand) WorkingTreeState() string {
|
||||
rebaseMode, _ := c.RebaseMode()
|
||||
if rebaseMode != "" {
|
||||
return "rebasing"
|
||||
}
|
||||
merging, _ := c.IsInMergeState()
|
||||
if merging {
|
||||
return "merging"
|
||||
}
|
||||
return "normal"
|
||||
}
|
||||
|
||||
+27
-162
@@ -6,14 +6,15 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
gogit "github.com/go-git/go-git/v5"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
gogit "gopkg.in/src-d/go-git.v4"
|
||||
)
|
||||
|
||||
type fileInfoMock struct {
|
||||
@@ -294,12 +295,10 @@ func TestGitCommandGetStashEntries(t *testing.T) {
|
||||
{
|
||||
0,
|
||||
"WIP on add-pkg-commands-test: 55c6af2 increase parallel build",
|
||||
"WIP on add-pkg-commands-test: 55c6af2 increase parallel build",
|
||||
},
|
||||
{
|
||||
1,
|
||||
"WIP on master: bb86a3f update github template",
|
||||
"WIP on master: bb86a3f update github template",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -314,7 +313,7 @@ func TestGitCommandGetStashEntries(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.command = s.command
|
||||
|
||||
s.test(gitCmd.GetStashEntries())
|
||||
s.test(gitCmd.GetStashEntries(""))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -421,7 +420,7 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.command = s.command
|
||||
|
||||
s.test(gitCmd.GetStatusFiles())
|
||||
s.test(gitCmd.GetStatusFiles(GetStatusFileOptions{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -542,7 +541,7 @@ func TestGitCommandMergeStatusFiles(t *testing.T) {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
s.test(gitCmd.MergeStatusFiles(s.oldFiles, s.newFiles))
|
||||
s.test(gitCmd.MergeStatusFiles(s.oldFiles, s.newFiles, nil))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -704,7 +703,7 @@ func TestGitCommandMerge(t *testing.T) {
|
||||
return exec.Command("echo")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.Merge("test"))
|
||||
assert.NoError(t, gitCmd.Merge("test", MergeOpts{}))
|
||||
}
|
||||
|
||||
// TestGitCommandUsingGpg is a function.
|
||||
@@ -816,7 +815,7 @@ func TestGitCommandCommit(t *testing.T) {
|
||||
"Commit using gpg",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "bash", cmd)
|
||||
assert.EqualValues(t, []string{"-c", `git commit -m 'test'`}, args)
|
||||
assert.EqualValues(t, []string{"-c", "git commit -m \"test\""}, args)
|
||||
|
||||
return exec.Command("echo")
|
||||
},
|
||||
@@ -1024,11 +1023,18 @@ func TestGitCommandPush(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandCatFile is a function.
|
||||
// TestGitCommandCatFile tests emitting a file using commands, where commands vary by OS.
|
||||
func TestGitCommandCatFile(t *testing.T) {
|
||||
var osCmd string
|
||||
switch os := runtime.GOOS; os {
|
||||
case "windows":
|
||||
osCmd = "type"
|
||||
default:
|
||||
osCmd = "cat"
|
||||
}
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "cat", cmd)
|
||||
assert.EqualValues(t, osCmd, cmd)
|
||||
assert.EqualValues(t, []string{"test.txt"}, args)
|
||||
|
||||
return exec.Command("echo", "-n", "test")
|
||||
@@ -1099,75 +1105,6 @@ func TestGitCommandUnstageFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandIsInMergeState is a function.
|
||||
func TestGitCommandIsInMergeState(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(bool, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"An error occurred when running status command",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"status", "--untracked-files=all"}, args)
|
||||
|
||||
return exec.Command("test")
|
||||
},
|
||||
func(isInMergeState bool, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.False(t, isInMergeState)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Is not in merge state",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"status", "--untracked-files=all"}, args)
|
||||
return exec.Command("echo")
|
||||
},
|
||||
func(isInMergeState bool, err error) {
|
||||
assert.False(t, isInMergeState)
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Command output contains conclude merge",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"status", "--untracked-files=all"}, args)
|
||||
return exec.Command("echo", "'conclude merge'")
|
||||
},
|
||||
func(isInMergeState bool, err error) {
|
||||
assert.True(t, isInMergeState)
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Command output contains unmerged paths",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"status", "--untracked-files=all"}, args)
|
||||
return exec.Command("echo", "'unmerged paths'")
|
||||
},
|
||||
func(isInMergeState bool, err error) {
|
||||
assert.True(t, isInMergeState)
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.command = s.command
|
||||
s.test(gitCmd.IsInMergeState())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandDiscardAllFileChanges is a function.
|
||||
func TestGitCommandDiscardAllFileChanges(t *testing.T) {
|
||||
type scenario struct {
|
||||
@@ -1450,10 +1387,8 @@ func TestGitCommandGetBranchGraph(t *testing.T) {
|
||||
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"log", "--graph", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium", "test", "--"}, args)
|
||||
|
||||
return exec.Command("echo")
|
||||
}
|
||||
|
||||
_, err := gitCmd.GetBranchGraph("test")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@@ -1473,7 +1408,7 @@ func TestGitCommandDiff(t *testing.T) {
|
||||
"Default case",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"diff", "--color=", "--", "test.txt"}, args)
|
||||
assert.EqualValues(t, []string{"diff", "--color=always", "--", "test.txt"}, args)
|
||||
|
||||
return exec.Command("echo")
|
||||
},
|
||||
@@ -1489,7 +1424,7 @@ func TestGitCommandDiff(t *testing.T) {
|
||||
"cached",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"diff", "--color=", "--cached", "--", "test.txt"}, args)
|
||||
assert.EqualValues(t, []string{"diff", "--color=always", "--cached", "--", "test.txt"}, args)
|
||||
|
||||
return exec.Command("echo")
|
||||
},
|
||||
@@ -1521,7 +1456,7 @@ func TestGitCommandDiff(t *testing.T) {
|
||||
"File not tracked and file has no staged changes",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"diff", "--color=", "--no-index", "/dev/null", "test.txt"}, args)
|
||||
assert.EqualValues(t, []string{"diff", "--color=always", "--no-index", "/dev/null", "test.txt"}, args)
|
||||
|
||||
return exec.Command("echo")
|
||||
},
|
||||
@@ -1539,7 +1474,7 @@ func TestGitCommandDiff(t *testing.T) {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.command = s.command
|
||||
gitCmd.Diff(s.file, s.plain, s.cached)
|
||||
gitCmd.WorktreeFileDiff(s.file, s.plain, s.cached)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1879,83 +1814,6 @@ func TestGitCommandDiscardOldFileChanges(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandShowCommitFile is a function.
|
||||
func TestGitCommandShowCommitFile(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
commitSha string
|
||||
fileName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(string, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
"123456",
|
||||
"hello.txt",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git show --no-renames --color=never 123456 -- hello.txt",
|
||||
Replace: "echo -n hello",
|
||||
},
|
||||
}),
|
||||
func(str string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hello", str)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.command = s.command
|
||||
s.test(gitCmd.ShowCommitFile(s.commitSha, s.fileName, true))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandGetCommitFiles is a function.
|
||||
func TestGitCommandGetCommitFiles(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
commitSha string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func([]*CommitFile, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
"123456",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git diff-tree --no-commit-id --name-only -r --no-renames 123456",
|
||||
Replace: "echo 'hello\nworld'",
|
||||
},
|
||||
}),
|
||||
func(commitFiles []*CommitFile, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []*CommitFile{
|
||||
{Sha: "123456", Name: "hello", DisplayString: "hello"},
|
||||
{Sha: "123456", Name: "world", DisplayString: "world"},
|
||||
}, commitFiles)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.command = s.command
|
||||
s.test(gitCmd.GetCommitFiles(s.commitSha, nil))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandDiscardUnstagedFileChanges is a function.
|
||||
func TestGitCommandDiscardUnstagedFileChanges(t *testing.T) {
|
||||
type scenario struct {
|
||||
@@ -2147,6 +2005,13 @@ func TestGitCommandSkipEditorCommand(t *testing.T) {
|
||||
"expected EDITOR to be set for a non-interactive external command",
|
||||
)
|
||||
|
||||
test.AssertContainsMatch(
|
||||
t,
|
||||
cmd.Env,
|
||||
regexp.MustCompile("^GIT_EDITOR="),
|
||||
"expected GIT_EDITOR to be set for a non-interactive external command",
|
||||
)
|
||||
|
||||
test.AssertContainsMatch(
|
||||
t,
|
||||
cmd.Env,
|
||||
|
||||
+32
-8
@@ -8,11 +8,13 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/mgutz/str"
|
||||
@@ -23,6 +25,7 @@ import (
|
||||
// Platform stores the os state
|
||||
type Platform struct {
|
||||
os string
|
||||
catCmd string
|
||||
shell string
|
||||
shellArg string
|
||||
escapedQuote string
|
||||
@@ -117,28 +120,43 @@ func (c *OSCommand) ExecutableFromString(commandStr string) *exec.Cmd {
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ShellCommandFromString takes a string like `git commit` and returns an executable shell command for it
|
||||
func (c *OSCommand) ShellCommandFromString(commandStr string) *exec.Cmd {
|
||||
quotedCommand := ""
|
||||
// Windows does not seem to like quotes around the command
|
||||
if c.Platform.os == "windows" {
|
||||
quotedCommand = commandStr
|
||||
} else {
|
||||
quotedCommand = strconv.Quote(commandStr)
|
||||
}
|
||||
|
||||
shellCommand := fmt.Sprintf("%s %s %s", c.Platform.shell, c.Platform.shellArg, quotedCommand)
|
||||
return c.ExecutableFromString(shellCommand)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// promptUserForCredential is a function that gets executed when this function detect you need to fillin a password
|
||||
// The promptUserForCredential argument will be "username" or "password" and expects the user's password or username back
|
||||
func (c *OSCommand) DetectUnamePass(command string, promptUserForCredential 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*'.+':`,
|
||||
`.+'s password:`: "password",
|
||||
`Password\s*for\s*'.+':`: "password",
|
||||
`Username\s*for\s*'.+':`: "username",
|
||||
}
|
||||
|
||||
for askFor, pattern := range prompts {
|
||||
for pattern, askFor := range prompts {
|
||||
if match, _ := regexp.MatchString(pattern, ttyText); match {
|
||||
ttyText = ""
|
||||
return ask(askFor)
|
||||
return promptUserForCredential(askFor)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,7 +250,9 @@ func (c *OSCommand) EditFile(filename string) (*exec.Cmd, error) {
|
||||
return nil, errors.New("No editor defined in $VISUAL, $EDITOR, or git config")
|
||||
}
|
||||
|
||||
return c.PrepareSubProcess(editor, filename), nil
|
||||
splitCmd := str.ToArgv(fmt.Sprintf("%s %s", editor, filename))
|
||||
|
||||
return c.PrepareSubProcess(splitCmd[0], splitCmd[1:]...), nil
|
||||
}
|
||||
|
||||
// PrepareSubProcess iniPrepareSubProcessrocess then tells the Gui to switch to it
|
||||
@@ -454,3 +474,7 @@ func RunLineOutputCmd(cmd *exec.Cmd, onLine func(line string) (bool, error)) err
|
||||
cmd.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *OSCommand) CopyToClipboard(str string) error {
|
||||
return clipboard.WriteAll(str)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
func getPlatform() *Platform {
|
||||
return &Platform{
|
||||
os: runtime.GOOS,
|
||||
catCmd: "cat",
|
||||
shell: "bash",
|
||||
shellArg: "-c",
|
||||
escapedQuote: "'",
|
||||
|
||||
@@ -3,6 +3,7 @@ package commands
|
||||
func getPlatform() *Platform {
|
||||
return &Platform{
|
||||
os: "windows",
|
||||
catCmd: "type",
|
||||
shell: "cmd",
|
||||
shellArg: "/c",
|
||||
escapedQuote: `\"`,
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type PatchHunk struct {
|
||||
FirstLineIdx int
|
||||
oldStart int
|
||||
newStart int
|
||||
heading string
|
||||
bodyLines []string
|
||||
}
|
||||
|
||||
func (hunk *PatchHunk) LastLineIdx() int {
|
||||
return hunk.FirstLineIdx + len(hunk.bodyLines)
|
||||
}
|
||||
|
||||
func newHunk(lines []string, firstLineIdx int) *PatchHunk {
|
||||
header := lines[0]
|
||||
bodyLines := lines[1:]
|
||||
|
||||
oldStart, newStart, heading := headerInfo(header)
|
||||
|
||||
return &PatchHunk{
|
||||
oldStart: oldStart,
|
||||
newStart: newStart,
|
||||
heading: heading,
|
||||
FirstLineIdx: firstLineIdx,
|
||||
bodyLines: bodyLines,
|
||||
}
|
||||
}
|
||||
|
||||
func headerInfo(header string) (int, int, string) {
|
||||
match := hunkHeaderRegexp.FindStringSubmatch(header)
|
||||
|
||||
oldStart := utils.MustConvertToInt(match[1])
|
||||
newStart := utils.MustConvertToInt(match[2])
|
||||
heading := match[3]
|
||||
|
||||
return oldStart, newStart, heading
|
||||
}
|
||||
|
||||
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 := nLinesWithPrefix(newBodyLines, []string{"+", "-"})
|
||||
oldLength := nLinesWithPrefix(newBodyLines, []string{" ", "-"})
|
||||
newLength := nLinesWithPrefix(newBodyLines, []string{"+", " "})
|
||||
|
||||
if changeCount == 0 {
|
||||
// if nothing has changed we just return nothing
|
||||
return startOffset, "", false
|
||||
}
|
||||
|
||||
var oldStart int
|
||||
if reverse {
|
||||
oldStart = hunk.newStart
|
||||
} else {
|
||||
oldStart = hunk.oldStart
|
||||
}
|
||||
|
||||
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, hunk.heading)
|
||||
return newStartOffset, formattedHeader, true
|
||||
}
|
||||
@@ -1,12 +1,24 @@
|
||||
package commands
|
||||
package patch
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
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
|
||||
// PART is for when you're only talking about specific lines that have been modified
|
||||
PART
|
||||
)
|
||||
|
||||
type fileInfo struct {
|
||||
mode int // one of WHOLE/PART
|
||||
includedLineIndices []int
|
||||
@@ -14,55 +26,76 @@ type fileInfo struct {
|
||||
}
|
||||
|
||||
type applyPatchFunc func(patch string, flags ...string) error
|
||||
type loadFileDiffFunc func(from string, to string, reverse bool, filename string, plain bool) (string, error)
|
||||
|
||||
// PatchManager manages the building of a patch for a commit to be applied to another commit (or the working tree, or removed from the current commit)
|
||||
// PatchManager manages the building of a patch for a commit to be applied to another commit (or the working tree, or removed from the current commit). We also support building patches from things like stashes, for which there is less flexibility
|
||||
type PatchManager struct {
|
||||
CommitSha string
|
||||
// To is the commit sha if we're dealing with files of a commit, or a stash ref for a stash
|
||||
To string
|
||||
From string
|
||||
Reverse bool
|
||||
|
||||
// CanRebase tells us whether we're allowed to modify our commits. CanRebase should be true for commits of the currently checked out branch and false for everything else
|
||||
// TODO: move this out into a proper mode struct in the gui package: it doesn't really belong here
|
||||
CanRebase bool
|
||||
|
||||
// fileInfoMap starts empty but you add files to it as you go along
|
||||
fileInfoMap map[string]*fileInfo
|
||||
Log *logrus.Entry
|
||||
ApplyPatch applyPatchFunc
|
||||
|
||||
// LoadFileDiff loads the diff of a file, for a given to (typically a commit SHA)
|
||||
LoadFileDiff loadFileDiffFunc
|
||||
}
|
||||
|
||||
// NewPatchManager returns a new PatchModifier
|
||||
func NewPatchManager(log *logrus.Entry, applyPatch applyPatchFunc) *PatchManager {
|
||||
// NewPatchManager returns a new PatchManager
|
||||
func NewPatchManager(log *logrus.Entry, applyPatch applyPatchFunc, loadFileDiff loadFileDiffFunc) *PatchManager {
|
||||
return &PatchManager{
|
||||
Log: log,
|
||||
ApplyPatch: applyPatch,
|
||||
Log: log,
|
||||
ApplyPatch: applyPatch,
|
||||
LoadFileDiff: loadFileDiff,
|
||||
}
|
||||
}
|
||||
|
||||
// NewPatchManager returns a new PatchModifier
|
||||
func (p *PatchManager) Start(commitSha string, diffMap map[string]string) {
|
||||
p.CommitSha = commitSha
|
||||
// NewPatchManager returns a new PatchManager
|
||||
func (p *PatchManager) Start(from, to string, reverse bool, canRebase bool) {
|
||||
p.To = to
|
||||
p.From = from
|
||||
p.Reverse = reverse
|
||||
p.CanRebase = canRebase
|
||||
p.fileInfoMap = map[string]*fileInfo{}
|
||||
for filename, diff := range diffMap {
|
||||
p.fileInfoMap[filename] = &fileInfo{
|
||||
mode: UNSELECTED,
|
||||
diff: diff,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PatchManager) addFileWhole(info *fileInfo) {
|
||||
info.mode = WHOLE
|
||||
lineCount := len(strings.Split(info.diff, "\n"))
|
||||
info.includedLineIndices = make([]int, lineCount)
|
||||
// add every line index
|
||||
for i := 0; i < lineCount; i++ {
|
||||
info.includedLineIndices[i] = i
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PatchManager) AddFile(filename string) {
|
||||
p.fileInfoMap[filename].mode = WHOLE
|
||||
p.fileInfoMap[filename].includedLineIndices = nil
|
||||
func (p *PatchManager) removeFile(info *fileInfo) {
|
||||
info.mode = UNSELECTED
|
||||
info.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]
|
||||
func (p *PatchManager) ToggleFileWhole(filename string) error {
|
||||
info, err := p.getFileInfo(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch info.mode {
|
||||
case UNSELECTED:
|
||||
p.AddFile(filename)
|
||||
case UNSELECTED, PART:
|
||||
p.addFileWhole(info)
|
||||
case WHOLE:
|
||||
p.RemoveFile(filename)
|
||||
case PART:
|
||||
p.AddFile(filename)
|
||||
p.removeFile(info)
|
||||
default:
|
||||
return errors.New("unknown file mode")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getIndicesForRange(first, last int) []int {
|
||||
@@ -73,24 +106,55 @@ func getIndicesForRange(first, last int) []int {
|
||||
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) getFileInfo(filename string) (*fileInfo, error) {
|
||||
info, ok := p.fileInfoMap[filename]
|
||||
if ok {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
diff, err := p.LoadFileDiff(p.From, p.To, p.Reverse, filename, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info = &fileInfo{
|
||||
mode: UNSELECTED,
|
||||
diff: diff,
|
||||
}
|
||||
|
||||
p.fileInfoMap[filename] = info
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (p *PatchManager) RemoveFileLineRange(filename string, firstLineIdx, lastLineIdx int) {
|
||||
info := p.fileInfoMap[filename]
|
||||
func (p *PatchManager) AddFileLineRange(filename string, firstLineIdx, lastLineIdx int) error {
|
||||
info, err := p.getFileInfo(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info.mode = PART
|
||||
info.includedLineIndices = utils.UnionInt(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PatchManager) RemoveFileLineRange(filename string, firstLineIdx, lastLineIdx int) error {
|
||||
info, err := p.getFileInfo(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info.mode = PART
|
||||
info.includedLineIndices = utils.DifferenceInt(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx))
|
||||
if len(info.includedLineIndices) == 0 {
|
||||
p.RemoveFile(filename)
|
||||
p.removeFile(info)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PatchManager) RenderPlainPatchForFile(filename string, reverse bool, keepOriginalHeader bool) string {
|
||||
info := p.fileInfoMap[filename]
|
||||
if info == nil {
|
||||
func (p *PatchManager) renderPlainPatchForFile(filename string, reverse bool, keepOriginalHeader bool) string {
|
||||
info, err := p.getFileInfo(filename)
|
||||
if err != nil {
|
||||
p.Log.Error(err)
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -101,15 +165,14 @@ func (p *PatchManager) RenderPlainPatchForFile(filename string, reverse bool, ke
|
||||
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)
|
||||
return ModifiedPatchForLines(p.Log, filename, info.diff, 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)
|
||||
patch := p.renderPlainPatchForFile(filename, reverse, keepOriginalHeader)
|
||||
if plain {
|
||||
return patch
|
||||
}
|
||||
@@ -122,7 +185,7 @@ func (p *PatchManager) RenderPatchForFile(filename string, plain bool, reverse b
|
||||
return parser.Render(-1, -1, nil)
|
||||
}
|
||||
|
||||
func (p *PatchManager) RenderEachFilePatch(plain bool) []string {
|
||||
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
|
||||
@@ -145,7 +208,7 @@ func (p *PatchManager) RenderEachFilePatch(plain bool) []string {
|
||||
|
||||
func (p *PatchManager) RenderAggregatedPatchColored(plain bool) string {
|
||||
result := ""
|
||||
for _, patch := range p.RenderEachFilePatch(plain) {
|
||||
for _, patch := range p.renderEachFilePatch(plain) {
|
||||
if patch != "" {
|
||||
result += patch + "\n"
|
||||
}
|
||||
@@ -154,19 +217,20 @@ func (p *PatchManager) RenderAggregatedPatchColored(plain bool) string {
|
||||
}
|
||||
|
||||
func (p *PatchManager) GetFileStatus(filename string) int {
|
||||
info := p.fileInfoMap[filename]
|
||||
if info == nil {
|
||||
info, ok := p.fileInfoMap[filename]
|
||||
if !ok {
|
||||
return UNSELECTED
|
||||
}
|
||||
|
||||
return info.mode
|
||||
}
|
||||
|
||||
func (p *PatchManager) GetFileIncLineIndices(filename string) []int {
|
||||
info := p.fileInfoMap[filename]
|
||||
if info == nil {
|
||||
return []int{}
|
||||
func (p *PatchManager) GetFileIncLineIndices(filename string) ([]int, error) {
|
||||
info, err := p.getFileInfo(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return info.includedLineIndices
|
||||
return info.includedLineIndices, nil
|
||||
}
|
||||
|
||||
func (p *PatchManager) ApplyPatches(reverse bool) error {
|
||||
@@ -210,12 +274,12 @@ func (p *PatchManager) ApplyPatches(reverse bool) error {
|
||||
|
||||
// clears the patch
|
||||
func (p *PatchManager) Reset() {
|
||||
p.CommitSha = ""
|
||||
p.To = ""
|
||||
p.fileInfoMap = map[string]*fileInfo{}
|
||||
}
|
||||
|
||||
func (p *PatchManager) CommitSelected() bool {
|
||||
return p.CommitSha != ""
|
||||
func (p *PatchManager) Active() bool {
|
||||
return p.To != ""
|
||||
}
|
||||
|
||||
func (p *PatchManager) IsEmpty() bool {
|
||||
@@ -227,3 +291,8 @@ func (p *PatchManager) IsEmpty() bool {
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// if any of these things change we'll need to reset and start a new patch
|
||||
func (p *PatchManager) NewPatchRequired(from string, to string, reverse bool) bool {
|
||||
return from != p.From || to != p.To || reverse != p.Reverse
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var hunkHeaderRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)[^\+]+\+(\d+)[^@]+@@(.*)$`)
|
||||
var patchHeaderRegexp = regexp.MustCompile(`(?ms)(^diff.*?)^@@`)
|
||||
|
||||
func GetHeaderFromDiff(diff string) string {
|
||||
match := patchHeaderRegexp.FindStringSubmatch(diff)
|
||||
if len(match) <= 1 {
|
||||
return ""
|
||||
}
|
||||
return match[1]
|
||||
}
|
||||
|
||||
func GetHunksFromDiff(diff string) []*PatchHunk {
|
||||
hunks := []*PatchHunk{}
|
||||
firstLineIdx := -1
|
||||
var hunkLines []string
|
||||
pastDiffHeader := false
|
||||
|
||||
for lineIdx, line := range strings.SplitAfter(diff, "\n") {
|
||||
isHunkHeader := strings.HasPrefix(line, "@@ -")
|
||||
|
||||
if isHunkHeader {
|
||||
if pastDiffHeader { // we need to persist the current hunk
|
||||
hunks = append(hunks, newHunk(hunkLines, firstLineIdx))
|
||||
}
|
||||
pastDiffHeader = true
|
||||
firstLineIdx = lineIdx
|
||||
hunkLines = []string{line}
|
||||
continue
|
||||
}
|
||||
|
||||
if !pastDiffHeader { // skip through the stuff that precedes the first hunk
|
||||
continue
|
||||
}
|
||||
|
||||
hunkLines = append(hunkLines, line)
|
||||
}
|
||||
|
||||
if pastDiffHeader {
|
||||
hunks = append(hunks, newHunk(hunkLines, firstLineIdx))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func ModifiedPatchForLines(log *logrus.Entry, filename string, diffText string, includedLineIndices []int, reverse bool, keepOriginalHeader bool) string {
|
||||
p := NewPatchModifier(log, filename, diffText)
|
||||
return p.ModifiedPatchForLines(includedLineIndices, reverse, keepOriginalHeader)
|
||||
}
|
||||
|
||||
// I want to know, given a hunk, what line a given index is on
|
||||
func (hunk *PatchHunk) LineNumberOfLine(idx int) int {
|
||||
lines := hunk.bodyLines[0 : idx-hunk.FirstLineIdx-1]
|
||||
|
||||
offset := nLinesWithPrefix(lines, []string{"+", " "})
|
||||
|
||||
return hunk.newStart + offset
|
||||
}
|
||||
|
||||
func nLinesWithPrefix(lines []string, chars []string) int {
|
||||
result := 0
|
||||
for _, line := range lines {
|
||||
for _, char := range chars {
|
||||
if line[:1] == char {
|
||||
result++
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
package commands
|
||||
package patch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -88,6 +89,15 @@ index e69de29..c6568ea 100644
|
||||
\ No newline at end of file
|
||||
`
|
||||
|
||||
const exampleHunk = `@@ -1,5 +1,5 @@
|
||||
apple
|
||||
-grape
|
||||
+orange
|
||||
...
|
||||
...
|
||||
...
|
||||
`
|
||||
|
||||
// TestModifyPatchForRange is a function.
|
||||
func TestModifyPatchForRange(t *testing.T) {
|
||||
type scenario struct {
|
||||
@@ -509,3 +519,30 @@ func TestModifyPatchForRange(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLineNumberOfLine(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
hunk *PatchHunk
|
||||
idx int
|
||||
expected int
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "nothing selected",
|
||||
hunk: newHunk(strings.SplitAfter(exampleHunk, "\n"), 10),
|
||||
idx: 15,
|
||||
expected: 3,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
result := s.hunk.LineNumberOfLine(s.idx)
|
||||
if !assert.Equal(t, s.expected, result) {
|
||||
fmt.Println(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package commands
|
||||
package patch
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
@@ -63,7 +63,7 @@ func (p *PatchParser) GetHunkContainingLine(lineIndex int, offset int) *PatchHun
|
||||
}
|
||||
|
||||
for index, hunk := range p.PatchHunks {
|
||||
if lineIndex >= hunk.FirstLineIdx && lineIndex <= hunk.LastLineIdx {
|
||||
if lineIndex >= hunk.FirstLineIdx && lineIndex <= hunk.LastLineIdx() {
|
||||
resultIndex := index + offset
|
||||
if resultIndex < 0 {
|
||||
resultIndex = 0
|
||||
@@ -75,7 +75,7 @@ func (p *PatchParser) GetHunkContainingLine(lineIndex int, offset int) *PatchHun
|
||||
}
|
||||
|
||||
// if your cursor is past the last hunk, select the last hunk
|
||||
if lineIndex > p.PatchHunks[len(p.PatchHunks)-1].LastLineIdx {
|
||||
if lineIndex > p.PatchHunks[len(p.PatchHunks)-1].LastLineIdx() {
|
||||
return p.PatchHunks[len(p.PatchHunks)-1]
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ func coloredString(colorAttr color.Attribute, str string, selected bool, include
|
||||
var cl *color.Color
|
||||
attributes := []color.Attribute{colorAttr}
|
||||
if selected {
|
||||
attributes = append(attributes, theme.SelectedLineBgColor)
|
||||
attributes = append(attributes, theme.SelectedRangeBgColor)
|
||||
}
|
||||
cl = color.New(attributes...)
|
||||
var clIncluded *color.Color
|
||||
@@ -1,260 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
package commands
|
||||
|
||||
import "github.com/go-errors/errors"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
)
|
||||
|
||||
// DeletePatchesFromCommit applies a patch in reverse for a commit
|
||||
func (c *GitCommand) DeletePatchesFromCommit(commits []*Commit, commitIndex int, p *PatchManager) error {
|
||||
func (c *GitCommand) DeletePatchesFromCommit(commits []*Commit, commitIndex int, p *patch.PatchManager) error {
|
||||
if err := c.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -30,7 +35,7 @@ func (c *GitCommand) DeletePatchesFromCommit(commits []*Commit, commitIndex int,
|
||||
return c.GenericMerge("rebase", "continue")
|
||||
}
|
||||
|
||||
func (c *GitCommand) MovePatchToSelectedCommit(commits []*Commit, sourceCommitIdx int, destinationCommitIdx int, p *PatchManager) error {
|
||||
func (c *GitCommand) MovePatchToSelectedCommit(commits []*Commit, sourceCommitIdx int, destinationCommitIdx int, p *patch.PatchManager) error {
|
||||
if sourceCommitIdx < destinationCommitIdx {
|
||||
if err := c.BeginInteractiveRebaseForCommit(commits, destinationCommitIdx); err != nil {
|
||||
return err
|
||||
@@ -131,14 +136,22 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*Commit, sourceCommitId
|
||||
return c.GenericMerge("rebase", "continue")
|
||||
}
|
||||
|
||||
func (c *GitCommand) PullPatchIntoIndex(commits []*Commit, commitIdx int, p *PatchManager) error {
|
||||
func (c *GitCommand) PullPatchIntoIndex(commits []*Commit, commitIdx int, p *patch.PatchManager, stash bool) error {
|
||||
if stash {
|
||||
if err := c.StashSave(c.Tr.SLocalize("StashPrefix") + commits[commitIdx].Sha); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
if c.WorkingTreeState() == "rebasing" {
|
||||
if err := c.GenericMerge("rebase", "abort"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -155,15 +168,63 @@ func (c *GitCommand) PullPatchIntoIndex(commits []*Commit, commitIdx int, p *Pat
|
||||
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
|
||||
if c.WorkingTreeState() == "rebasing" {
|
||||
if err := c.GenericMerge("rebase", "abort"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if stash {
|
||||
if err := c.StashDo(0, "apply"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
c.PatchManager.Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.GenericMerge("rebase", "continue")
|
||||
}
|
||||
|
||||
func (c *GitCommand) PullPatchIntoNewCommit(commits []*Commit, commitIdx int, p *patch.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
|
||||
}
|
||||
|
||||
// add patches to index
|
||||
if err := p.ApplyPatches(false); err != nil {
|
||||
if err := c.GenericMerge("rebase", "abort"); err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
head_message, _ := c.GetHeadCommitMessage()
|
||||
new_message := fmt.Sprintf("Split from \"%s\"", head_message)
|
||||
_, err := c.Commit(new_message, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.onSuccessfulContinue != nil {
|
||||
return errors.New("You are midway through another rebase operation. Please abort to start again")
|
||||
}
|
||||
|
||||
c.PatchManager.Reset()
|
||||
return c.GenericMerge("rebase", "continue")
|
||||
}
|
||||
|
||||
+8
-12
@@ -1,24 +1,20 @@
|
||||
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 {
|
||||
func (r *Remote) RefName() string {
|
||||
return r.Name
|
||||
}
|
||||
|
||||
branchCount := len(r.Branches)
|
||||
func (r *Remote) ID() string {
|
||||
return r.RefName()
|
||||
}
|
||||
|
||||
return []string{r.Name, utils.ColoredString(fmt.Sprintf("%d branches", branchCount), color.FgBlue)}
|
||||
func (r *Remote) Description() string {
|
||||
return r.RefName()
|
||||
}
|
||||
|
||||
@@ -5,3 +5,19 @@ type RemoteBranch struct {
|
||||
Name string
|
||||
RemoteName string
|
||||
}
|
||||
|
||||
func (r *RemoteBranch) FullName() string {
|
||||
return r.RemoteName + "/" + r.Name
|
||||
}
|
||||
|
||||
func (r *RemoteBranch) RefName() string {
|
||||
return r.FullName()
|
||||
}
|
||||
|
||||
func (r *RemoteBranch) ID() string {
|
||||
return r.RefName()
|
||||
}
|
||||
|
||||
func (r *RemoteBranch) Description() string {
|
||||
return r.RefName()
|
||||
}
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
package commands
|
||||
|
||||
import "fmt"
|
||||
|
||||
// StashEntry : A git stash entry
|
||||
type StashEntry struct {
|
||||
Index int
|
||||
Name string
|
||||
DisplayString string
|
||||
Index int
|
||||
Name string
|
||||
}
|
||||
|
||||
// GetDisplayStrings returns the display string of branch
|
||||
func (s *StashEntry) GetDisplayStrings(isFocused bool) []string {
|
||||
return []string{s.DisplayString}
|
||||
func (s *StashEntry) RefName() string {
|
||||
return fmt.Sprintf("stash@{%d}", s.Index)
|
||||
}
|
||||
|
||||
func (s *StashEntry) ID() string {
|
||||
return s.RefName()
|
||||
}
|
||||
|
||||
func (s *StashEntry) Description() string {
|
||||
return s.RefName() + ": " + s.Name
|
||||
}
|
||||
|
||||
@@ -4,3 +4,15 @@ package commands
|
||||
type Tag struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (t *Tag) RefName() string {
|
||||
return t.Name
|
||||
}
|
||||
|
||||
func (t *Tag) ID() string {
|
||||
return t.RefName()
|
||||
}
|
||||
|
||||
func (t *Tag) Description() string {
|
||||
return "tag " + t.Name
|
||||
}
|
||||
|
||||
@@ -244,7 +244,10 @@ func GetDefaultConfig() []byte {
|
||||
scrollPastBottom: true
|
||||
mouseEvents: true
|
||||
skipUnstageLineWarning: false
|
||||
skipStashWarning: true
|
||||
sidePanelWidth: 0.3333
|
||||
expandFocusedSidePanel: false
|
||||
mainPanelSplitMode: 'flexible' # one of 'horizontal' | 'flexible' | 'vertical'
|
||||
theme:
|
||||
lightTheme: false
|
||||
activeBorderColor:
|
||||
@@ -255,6 +258,8 @@ func GetDefaultConfig() []byte {
|
||||
optionsTextColor:
|
||||
- blue
|
||||
selectedLineBgColor:
|
||||
- default
|
||||
selectedRangeBgColor:
|
||||
- blue
|
||||
commitLength:
|
||||
show: true
|
||||
@@ -264,14 +269,20 @@ git:
|
||||
useConfig: false
|
||||
merging:
|
||||
manualCommit: false
|
||||
args: ""
|
||||
pull:
|
||||
mode: 'merge' # one of 'merge' | 'rebase' | 'ff-only'
|
||||
skipHookPrefix: 'WIP'
|
||||
autoFetch: true
|
||||
branchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --"
|
||||
overrideGpg: false # prevents lazygit from spawning a separate process when using GPG
|
||||
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
|
||||
quitOnTopLevelReturn: true
|
||||
keybinding:
|
||||
universal:
|
||||
quit: 'q'
|
||||
@@ -283,6 +294,10 @@ keybinding:
|
||||
nextItem: '<down>'
|
||||
prevItem-alt: 'k'
|
||||
nextItem-alt: 'j'
|
||||
prevPage: ','
|
||||
nextPage: '.'
|
||||
gotoTop: '<'
|
||||
gotoBottom: '>'
|
||||
prevBlock: '<left>'
|
||||
nextBlock: '<right>'
|
||||
prevBlock-alt: 'h'
|
||||
@@ -294,6 +309,8 @@ keybinding:
|
||||
optionMenu-alt1: '?'
|
||||
select: '<space>'
|
||||
goInto: '<enter>'
|
||||
confirm: '<enter>'
|
||||
confirm-alt1: 'y'
|
||||
remove: 'd'
|
||||
new: 'n'
|
||||
edit: 'e'
|
||||
@@ -316,6 +333,10 @@ keybinding:
|
||||
prevScreenMode: '_'
|
||||
undo: 'z'
|
||||
redo: '<c-z>'
|
||||
filteringMenu: <c-s>
|
||||
diffingMenu: 'W'
|
||||
diffingMenu-alt: '<c-e>'
|
||||
copyToClipboard: '<c-o>'
|
||||
status:
|
||||
checkForUpdate: 'u'
|
||||
recentRepos: '<enter>'
|
||||
@@ -360,7 +381,6 @@ keybinding:
|
||||
cherryPickCopyRange: 'C'
|
||||
pasteCommits: 'v'
|
||||
tagCommit: 'T'
|
||||
toggleDiffCommit: 'i'
|
||||
checkoutCommit: '<space>'
|
||||
resetCherryPick: '<c-R>'
|
||||
stash:
|
||||
|
||||
@@ -66,13 +66,13 @@ func (gui *Gui) WithWaitingStatus(name string, f func() error) error {
|
||||
if appStatus == "" {
|
||||
return
|
||||
}
|
||||
gui.renderString(gui.g, "appStatus", appStatus)
|
||||
gui.renderString("appStatus", appStatus)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := f(); err != nil {
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/boxlayout"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func (gui *Gui) mainSectionChildren() []*boxlayout.Box {
|
||||
currentWindow := gui.currentWindow()
|
||||
|
||||
// if we're not in split mode we can just show the one main panel. Likewise if
|
||||
// the main panel is focused and we're in full-screen mode
|
||||
if !gui.isMainPanelSplit() || (gui.State.ScreenMode == SCREEN_FULL && currentWindow == "main") {
|
||||
return []*boxlayout.Box{
|
||||
{
|
||||
Window: "main",
|
||||
Weight: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
main := "main"
|
||||
secondary := "secondary"
|
||||
if gui.secondaryViewFocused() {
|
||||
// when you think you've focused the secondary view, we've actually just swapped them around in the layout
|
||||
main, secondary = secondary, main
|
||||
}
|
||||
|
||||
return []*boxlayout.Box{
|
||||
{
|
||||
Window: main,
|
||||
Weight: 1,
|
||||
},
|
||||
{
|
||||
Window: secondary,
|
||||
Weight: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) getMidSectionWeights() (int, int) {
|
||||
currentWindow := gui.currentWindow()
|
||||
|
||||
// we originally specified this as a ratio i.e. .20 would correspond to a weight of 1 against 4
|
||||
sidePanelWidthRatio := gui.Config.GetUserConfig().GetFloat64("gui.sidePanelWidth")
|
||||
// we could make this better by creating ratios like 2:3 rather than always 1:something
|
||||
mainSectionWeight := int(1/sidePanelWidthRatio) - 1
|
||||
sideSectionWeight := 1
|
||||
|
||||
if gui.isMainPanelSplit() {
|
||||
mainSectionWeight = 5 // need to shrink side panel to make way for main panels if side-by-side
|
||||
}
|
||||
|
||||
if currentWindow == "main" {
|
||||
if gui.State.ScreenMode == SCREEN_HALF || gui.State.ScreenMode == SCREEN_FULL {
|
||||
sideSectionWeight = 0
|
||||
}
|
||||
} else {
|
||||
if gui.State.ScreenMode == SCREEN_HALF {
|
||||
mainSectionWeight = 1
|
||||
} else if gui.State.ScreenMode == SCREEN_FULL {
|
||||
mainSectionWeight = 0
|
||||
}
|
||||
}
|
||||
|
||||
return sideSectionWeight, mainSectionWeight
|
||||
}
|
||||
|
||||
func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*boxlayout.Box {
|
||||
if gui.State.Searching.isSearching {
|
||||
return []*boxlayout.Box{
|
||||
{
|
||||
Window: "searchPrefix",
|
||||
Size: len(SEARCH_PREFIX),
|
||||
},
|
||||
{
|
||||
Window: "search",
|
||||
Weight: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
result := []*boxlayout.Box{}
|
||||
|
||||
if len(appStatus) > 0 {
|
||||
result = append(result,
|
||||
&boxlayout.Box{
|
||||
Window: "appStatus",
|
||||
Size: len(appStatus) + len(INFO_SECTION_PADDING),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
result = append(result,
|
||||
[]*boxlayout.Box{
|
||||
{
|
||||
Window: "options",
|
||||
Weight: 1,
|
||||
},
|
||||
{
|
||||
Window: "information",
|
||||
// unlike appStatus, informationStr has various colors so we need to decolorise before taking the length
|
||||
Size: len(INFO_SECTION_PADDING) + len(utils.Decolorise(informationStr)),
|
||||
},
|
||||
}...,
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions {
|
||||
width, height := gui.g.Size()
|
||||
|
||||
sideSectionWeight, mainSectionWeight := gui.getMidSectionWeights()
|
||||
|
||||
sidePanelsDirection := boxlayout.COLUMN
|
||||
portraitMode := width <= 84 && height > 45
|
||||
if portraitMode {
|
||||
sidePanelsDirection = boxlayout.ROW
|
||||
}
|
||||
|
||||
root := &boxlayout.Box{
|
||||
Direction: boxlayout.ROW,
|
||||
Children: []*boxlayout.Box{
|
||||
{
|
||||
Direction: sidePanelsDirection,
|
||||
Weight: 1,
|
||||
Children: []*boxlayout.Box{
|
||||
{
|
||||
Direction: boxlayout.ROW,
|
||||
Weight: sideSectionWeight,
|
||||
ConditionalChildren: gui.sidePanelChildren,
|
||||
},
|
||||
{
|
||||
ConditionalDirection: func(width int, height int) int {
|
||||
mainPanelSplitMode := gui.Config.GetUserConfig().GetString("gui.mainPanelSplitMode")
|
||||
|
||||
switch mainPanelSplitMode {
|
||||
case "vertical":
|
||||
return boxlayout.ROW
|
||||
case "horizontal":
|
||||
return boxlayout.COLUMN
|
||||
default:
|
||||
if width < 160 && height > 30 { // 2 80 character width panels
|
||||
return boxlayout.ROW
|
||||
} else {
|
||||
return boxlayout.COLUMN
|
||||
}
|
||||
}
|
||||
},
|
||||
Direction: boxlayout.COLUMN,
|
||||
Weight: mainSectionWeight,
|
||||
Children: gui.mainSectionChildren(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Direction: boxlayout.COLUMN,
|
||||
Size: 1,
|
||||
Children: gui.infoSectionChildren(informationStr, appStatus),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return boxlayout.ArrangeWindows(root, 0, 0, width, height)
|
||||
}
|
||||
|
||||
// The stash window by default only contains one line so that it's not hogging
|
||||
// too much space, but if you access it it should take up some space. This is
|
||||
// the default behaviour when accordian mode is NOT in effect. If it is in effect
|
||||
// then when it's accessed it will have weight 2, not 1.
|
||||
func (gui *Gui) getDefaultStashWindowBox() *boxlayout.Box {
|
||||
box := &boxlayout.Box{Window: "stash"}
|
||||
stashWindowAccessed := false
|
||||
for _, context := range gui.State.ContextStack {
|
||||
if context.GetWindowName() == "stash" {
|
||||
stashWindowAccessed = true
|
||||
}
|
||||
}
|
||||
// if the stash window is anywhere in our stack we should enlargen it
|
||||
if stashWindowAccessed {
|
||||
box.Weight = 1
|
||||
} else {
|
||||
box.Size = 3
|
||||
}
|
||||
|
||||
return box
|
||||
}
|
||||
|
||||
func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
|
||||
currentWindow := gui.currentSideWindowName()
|
||||
|
||||
if gui.State.ScreenMode == SCREEN_FULL || gui.State.ScreenMode == SCREEN_HALF {
|
||||
fullHeightBox := func(window string) *boxlayout.Box {
|
||||
if window == currentWindow {
|
||||
return &boxlayout.Box{
|
||||
Window: window,
|
||||
Weight: 1,
|
||||
}
|
||||
} else {
|
||||
return &boxlayout.Box{
|
||||
Window: window,
|
||||
Size: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return []*boxlayout.Box{
|
||||
fullHeightBox("status"),
|
||||
fullHeightBox("files"),
|
||||
fullHeightBox("branches"),
|
||||
fullHeightBox("commits"),
|
||||
fullHeightBox("stash"),
|
||||
}
|
||||
} else if height >= 28 {
|
||||
accordianMode := gui.Config.GetUserConfig().GetBool("gui.expandFocusedSidePanel")
|
||||
accordianBox := func(defaultBox *boxlayout.Box) *boxlayout.Box {
|
||||
if accordianMode && defaultBox.Window == currentWindow {
|
||||
return &boxlayout.Box{
|
||||
Window: defaultBox.Window,
|
||||
Weight: 2,
|
||||
}
|
||||
}
|
||||
|
||||
return defaultBox
|
||||
}
|
||||
|
||||
return []*boxlayout.Box{
|
||||
{
|
||||
Window: "status",
|
||||
Size: 3,
|
||||
},
|
||||
accordianBox(&boxlayout.Box{Window: "files", Weight: 1}),
|
||||
accordianBox(&boxlayout.Box{Window: "branches", Weight: 1}),
|
||||
accordianBox(&boxlayout.Box{Window: "commits", Weight: 1}),
|
||||
accordianBox(gui.getDefaultStashWindowBox()),
|
||||
}
|
||||
} else {
|
||||
squashedHeight := 1
|
||||
if height >= 21 {
|
||||
squashedHeight = 3
|
||||
}
|
||||
|
||||
squashedSidePanelBox := func(window string) *boxlayout.Box {
|
||||
if window == currentWindow {
|
||||
return &boxlayout.Box{
|
||||
Window: window,
|
||||
Weight: 1,
|
||||
}
|
||||
} else {
|
||||
return &boxlayout.Box{
|
||||
Window: window,
|
||||
Size: squashedHeight,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return []*boxlayout.Box{
|
||||
squashedSidePanelBox("status"),
|
||||
squashedSidePanelBox("files"),
|
||||
squashedSidePanelBox("branches"),
|
||||
squashedSidePanelBox("commits"),
|
||||
squashedSidePanelBox("stash"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) currentSideWindowName() string {
|
||||
// there is always one and only one cyclable context in the context stack. We'll look from top to bottom
|
||||
for idx := range gui.State.ContextStack {
|
||||
reversedIdx := len(gui.State.ContextStack) - 1 - idx
|
||||
context := gui.State.ContextStack[reversedIdx]
|
||||
|
||||
if context.GetKind() == SIDE_CONTEXT {
|
||||
return context.GetWindowName()
|
||||
}
|
||||
}
|
||||
|
||||
return "files" // default
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package boxlayout
|
||||
|
||||
import "math"
|
||||
|
||||
type Dimensions struct {
|
||||
X0 int
|
||||
X1 int
|
||||
Y0 int
|
||||
Y1 int
|
||||
}
|
||||
|
||||
const (
|
||||
ROW = iota
|
||||
COLUMN
|
||||
)
|
||||
|
||||
// to give a high-level explanation of what's going on here. We layout our windows by arranging a bunch of boxes in the available space.
|
||||
// If a box has children, it needs to specify how it wants to arrange those children: ROW or COLUMN.
|
||||
// If a box represents a window, you can put the window name in the Window field.
|
||||
// When determining how to divvy-up the available height (for row children) or width (for column children), we first
|
||||
// give the boxes with a static `size` the space that they want. Then we apportion
|
||||
// the remaining space based on the weights of the dynamic boxes (you can't define
|
||||
// both size and weight at the same time: you gotta pick one). If there are two
|
||||
// boxes, one with weight 1 and the other with weight 2, the first one gets 33%
|
||||
// of the available space and the second one gets the remaining 66%
|
||||
|
||||
type Box struct {
|
||||
// Direction decides how the children boxes are laid out. ROW means the children will each form a row i.e. that they will be stacked on top of eachother.
|
||||
Direction int // ROW or COLUMN
|
||||
|
||||
// function which takes the width and height assigned to the box and decides which orientation it will have
|
||||
ConditionalDirection func(width int, height int) int
|
||||
|
||||
Children []*Box
|
||||
|
||||
// function which takes the width and height assigned to the box and decides the layout of the children.
|
||||
ConditionalChildren func(width int, height int) []*Box
|
||||
|
||||
// Window refers to the name of the window this box represents, if there is one
|
||||
Window string
|
||||
|
||||
// static Size. If parent box's direction is ROW this refers to height, otherwise width
|
||||
Size int
|
||||
|
||||
// dynamic size. Once all statically sized children have been considered, Weight decides how much of the remaining space will be taken up by the box
|
||||
// TODO: consider making there be one int and a type enum so we can't have size and Weight simultaneously defined
|
||||
Weight int
|
||||
}
|
||||
|
||||
func ArrangeWindows(root *Box, x0, y0, width, height int) map[string]Dimensions {
|
||||
children := root.getChildren(width, height)
|
||||
if len(children) == 0 {
|
||||
// leaf node
|
||||
if root.Window != "" {
|
||||
dimensionsForWindow := Dimensions{X0: x0, Y0: y0, X1: x0 + width - 1, Y1: y0 + height - 1}
|
||||
return map[string]Dimensions{root.Window: dimensionsForWindow}
|
||||
}
|
||||
return map[string]Dimensions{}
|
||||
}
|
||||
|
||||
direction := root.getDirection(width, height)
|
||||
|
||||
var availableSize int
|
||||
if direction == COLUMN {
|
||||
availableSize = width
|
||||
} else {
|
||||
availableSize = height
|
||||
}
|
||||
|
||||
// work out size taken up by children
|
||||
reservedSize := 0
|
||||
totalWeight := 0
|
||||
for _, child := range children {
|
||||
// assuming either size or weight are non-zero
|
||||
reservedSize += child.Size
|
||||
totalWeight += child.Weight
|
||||
}
|
||||
|
||||
remainingSize := availableSize - reservedSize
|
||||
if remainingSize < 0 {
|
||||
remainingSize = 0
|
||||
}
|
||||
|
||||
unitSize := 0
|
||||
extraSize := 0
|
||||
if totalWeight > 0 {
|
||||
unitSize = remainingSize / totalWeight
|
||||
extraSize = remainingSize % totalWeight
|
||||
}
|
||||
|
||||
result := map[string]Dimensions{}
|
||||
offset := 0
|
||||
for _, child := range children {
|
||||
var boxSize int
|
||||
if child.isStatic() {
|
||||
boxSize = child.Size
|
||||
} else {
|
||||
// TODO: consider more evenly distributing the remainder
|
||||
boxSize = unitSize * child.Weight
|
||||
boxExtraSize := int(math.Min(float64(extraSize), float64(child.Weight)))
|
||||
boxSize += boxExtraSize
|
||||
extraSize -= boxExtraSize
|
||||
}
|
||||
|
||||
var resultForChild map[string]Dimensions
|
||||
if direction == COLUMN {
|
||||
resultForChild = ArrangeWindows(child, x0+offset, y0, boxSize, height)
|
||||
} else {
|
||||
resultForChild = ArrangeWindows(child, x0, y0+offset, width, boxSize)
|
||||
}
|
||||
|
||||
result = mergeDimensionMaps(result, resultForChild)
|
||||
offset += boxSize
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (b *Box) isStatic() bool {
|
||||
return b.Size > 0
|
||||
}
|
||||
|
||||
func (b *Box) getDirection(width int, height int) int {
|
||||
if b.ConditionalDirection != nil {
|
||||
return b.ConditionalDirection(width, height)
|
||||
}
|
||||
return b.Direction
|
||||
}
|
||||
|
||||
func (b *Box) getChildren(width int, height int) []*Box {
|
||||
if b.ConditionalChildren != nil {
|
||||
return b.ConditionalChildren(width, height)
|
||||
}
|
||||
return b.Children
|
||||
}
|
||||
|
||||
func mergeDimensionMaps(a map[string]Dimensions, b map[string]Dimensions) map[string]Dimensions {
|
||||
result := map[string]Dimensions{}
|
||||
for _, dimensionMap := range []map[string]Dimensions{a, b} {
|
||||
for k, v := range dimensionMap {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package boxlayout
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestArrangeWindows(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
root *Box
|
||||
x0 int
|
||||
y0 int
|
||||
width int
|
||||
height int
|
||||
test func(result map[string]Dimensions)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Empty box",
|
||||
&Box{},
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
10,
|
||||
func(result map[string]Dimensions) {
|
||||
assert.EqualValues(t, result, map[string]Dimensions{})
|
||||
},
|
||||
},
|
||||
{
|
||||
"Box with static and dynamic panel",
|
||||
&Box{Children: []*Box{{Size: 1, Window: "static"}, {Weight: 1, Window: "dynamic"}}},
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
10,
|
||||
func(result map[string]Dimensions) {
|
||||
assert.EqualValues(
|
||||
t,
|
||||
result,
|
||||
map[string]Dimensions{
|
||||
"dynamic": {X0: 0, X1: 9, Y0: 1, Y1: 9},
|
||||
"static": {X0: 0, X1: 9, Y0: 0, Y1: 0},
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Box with static and two dynamic panels",
|
||||
&Box{Children: []*Box{{Size: 1, Window: "static"}, {Weight: 1, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}}},
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
10,
|
||||
func(result map[string]Dimensions) {
|
||||
assert.EqualValues(
|
||||
t,
|
||||
result,
|
||||
map[string]Dimensions{
|
||||
"static": {X0: 0, X1: 9, Y0: 0, Y1: 0},
|
||||
"dynamic1": {X0: 0, X1: 9, Y0: 1, Y1: 3},
|
||||
"dynamic2": {X0: 0, X1: 9, Y0: 4, Y1: 9},
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Box with COLUMN direction",
|
||||
&Box{Direction: COLUMN, Children: []*Box{{Size: 1, Window: "static"}, {Weight: 1, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}}},
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
10,
|
||||
func(result map[string]Dimensions) {
|
||||
assert.EqualValues(
|
||||
t,
|
||||
result,
|
||||
map[string]Dimensions{
|
||||
"static": {X0: 0, X1: 0, Y0: 0, Y1: 9},
|
||||
"dynamic1": {X0: 1, X1: 3, Y0: 0, Y1: 9},
|
||||
"dynamic2": {X0: 4, X1: 9, Y0: 0, Y1: 9},
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Box with COLUMN direction only on wide boxes with narrow box",
|
||||
&Box{ConditionalDirection: func(width int, height int) int {
|
||||
if width > 4 {
|
||||
return COLUMN
|
||||
} else {
|
||||
return ROW
|
||||
}
|
||||
}, Children: []*Box{{Weight: 1, Window: "dynamic1"}, {Weight: 1, Window: "dynamic2"}}},
|
||||
0,
|
||||
0,
|
||||
4,
|
||||
4,
|
||||
func(result map[string]Dimensions) {
|
||||
assert.EqualValues(
|
||||
t,
|
||||
result,
|
||||
map[string]Dimensions{
|
||||
"dynamic1": {X0: 0, X1: 3, Y0: 0, Y1: 1},
|
||||
"dynamic2": {X0: 0, X1: 3, Y0: 2, Y1: 3},
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Box with COLUMN direction only on wide boxes with wide box",
|
||||
&Box{ConditionalDirection: func(width int, height int) int {
|
||||
if width > 4 {
|
||||
return COLUMN
|
||||
} else {
|
||||
return ROW
|
||||
}
|
||||
}, Children: []*Box{{Weight: 1, Window: "dynamic1"}, {Weight: 1, Window: "dynamic2"}}},
|
||||
0,
|
||||
0,
|
||||
5,
|
||||
5,
|
||||
func(result map[string]Dimensions) {
|
||||
assert.EqualValues(
|
||||
t,
|
||||
result,
|
||||
map[string]Dimensions{
|
||||
"dynamic1": {X0: 0, X1: 2, Y0: 0, Y1: 4},
|
||||
"dynamic2": {X0: 3, X1: 4, Y0: 0, Y1: 4},
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Box with conditional children where box is wide",
|
||||
&Box{ConditionalChildren: func(width int, height int) []*Box {
|
||||
if width > 4 {
|
||||
return []*Box{{Window: "wide", Weight: 1}}
|
||||
} else {
|
||||
return []*Box{{Window: "narrow", Weight: 1}}
|
||||
}
|
||||
}},
|
||||
0,
|
||||
0,
|
||||
5,
|
||||
5,
|
||||
func(result map[string]Dimensions) {
|
||||
assert.EqualValues(
|
||||
t,
|
||||
result,
|
||||
map[string]Dimensions{
|
||||
"wide": {X0: 0, X1: 4, Y0: 0, Y1: 4},
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Box with conditional children where box is narrow",
|
||||
&Box{ConditionalChildren: func(width int, height int) []*Box {
|
||||
if width > 4 {
|
||||
return []*Box{{Window: "wide", Weight: 1}}
|
||||
} else {
|
||||
return []*Box{{Window: "narrow", Weight: 1}}
|
||||
}
|
||||
}},
|
||||
0,
|
||||
0,
|
||||
4,
|
||||
4,
|
||||
func(result map[string]Dimensions) {
|
||||
assert.EqualValues(
|
||||
t,
|
||||
result,
|
||||
map[string]Dimensions{
|
||||
"narrow": {X0: 0, X1: 3, Y0: 0, Y1: 3},
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
s.test(ArrangeWindows(s.root, s.x0, s.y0, s.width, s.height))
|
||||
})
|
||||
}
|
||||
}
|
||||
+235
-243
@@ -6,14 +6,16 @@ import (
|
||||
|
||||
"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 len(gui.State.Branches) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
selectedLine := gui.State.Panels.Branches.SelectedLineIdx
|
||||
if selectedLine == -1 {
|
||||
return nil
|
||||
}
|
||||
@@ -21,89 +23,64 @@ func (gui *Gui) getSelectedBranch() *commands.Branch {
|
||||
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"))
|
||||
}
|
||||
func (gui *Gui) handleBranchSelect() error {
|
||||
var task updateTask
|
||||
branch := gui.getSelectedBranch()
|
||||
v.FocusPoint(0, gui.State.Panels.Branches.SelectedLine)
|
||||
if branch == nil {
|
||||
task = gui.createRenderStringTask(gui.Tr.SLocalize("NoBranchesThisRepo"))
|
||||
} else {
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.GetBranchGraphCmdStr(branch.Name),
|
||||
)
|
||||
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.GetBranchGraphCmdStr(branch.Name),
|
||||
)
|
||||
if err := gui.newCmdTask("main", cmd); err != nil {
|
||||
gui.Log.Error(err)
|
||||
task = gui.createRunPtyTask(cmd)
|
||||
}
|
||||
return nil
|
||||
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
main: &viewUpdateOpts{
|
||||
title: "Log",
|
||||
task: task,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// gui.refreshStatus is called at the end of this because that's when we can
|
||||
// be sure there is a state.Branches array to pick the current branch from
|
||||
func (gui *Gui) refreshBranches(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)
|
||||
func (gui *Gui) refreshBranches() {
|
||||
reflogCommits := gui.State.FilteredReflogCommits
|
||||
if gui.State.Modes.Filtering.Active() {
|
||||
// in filter mode we filter our reflog commits to just those containing the path
|
||||
// however we need all the reflog entries to populate the recencies of our branches
|
||||
// which allows us to order them correctly. So if we're filtering we'll just
|
||||
// manually load all the reflog commits here
|
||||
var err error
|
||||
reflogCommits, _, err = gui.GitCommand.GetReflogCommits(nil, "")
|
||||
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))
|
||||
displayStrings := presentation.GetBranchListDisplayStrings(gui.State.Branches, gui.State.ScreenMode != SCREEN_NORMAL)
|
||||
gui.renderDisplayStrings(branchesView, displayStrings)
|
||||
if gui.g.CurrentView() == branchesView {
|
||||
if err := gui.handleBranchSelect(gui.g, branchesView); err != nil {
|
||||
return err
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
builder, err := commands.NewBranchListBuilder(gui.Log, gui.GitCommand, reflogCommits)
|
||||
if err != nil {
|
||||
_ = gui.surfaceError(err)
|
||||
}
|
||||
gui.State.Branches = builder.Build()
|
||||
|
||||
if err := gui.postRefreshUpdate(gui.Contexts.Branches.Context); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
|
||||
gui.refreshStatus()
|
||||
}
|
||||
|
||||
// specific functions
|
||||
|
||||
func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.State.Panels.Branches.SelectedLine == -1 {
|
||||
if gui.State.Panels.Branches.SelectedLineIdx == -1 {
|
||||
return nil
|
||||
}
|
||||
if gui.State.Panels.Branches.SelectedLine == 0 {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("AlreadyCheckedOutBranch"))
|
||||
if gui.State.Panels.Branches.SelectedLineIdx == 0 {
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("AlreadyCheckedOutBranch"))
|
||||
}
|
||||
branch := gui.getSelectedBranch()
|
||||
return gui.handleCheckoutRef(branch.Name, handleCheckoutRefOptions{})
|
||||
@@ -114,19 +91,20 @@ func (gui *Gui) handleCreatePullRequestPress(g *gocui.Gui, v *gocui.View) error
|
||||
|
||||
branch := gui.getSelectedBranch()
|
||||
if err := pullRequest.Create(branch); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
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 {
|
||||
if err := gui.createLoaderPanel(v, gui.Tr.SLocalize("FetchWait")); err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
unamePassOpend, err := gui.fetch(g, v, true)
|
||||
gui.HandleCredentialsPopup(g, unamePassOpend, err)
|
||||
err := gui.fetch(true)
|
||||
gui.handleCredentialsPopup(err)
|
||||
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
@@ -135,17 +113,23 @@ 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, commands.CheckoutOptions{Force: true}); err != nil {
|
||||
_ = gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
return gui.refreshSidePanels(g)
|
||||
}, nil)
|
||||
|
||||
return gui.ask(askOpts{
|
||||
title: title,
|
||||
prompt: message,
|
||||
handleConfirm: func() error {
|
||||
if err := gui.GitCommand.Checkout(branch.Name, commands.CheckoutOptions{Force: true}); err != nil {
|
||||
_ = gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
type handleCheckoutRefOptions struct {
|
||||
WaitingStatus string
|
||||
EnvVars []string
|
||||
onRefNotFound func(ref string) error
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions) error {
|
||||
@@ -157,8 +141,8 @@ func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions)
|
||||
cmdOptions := commands.CheckoutOptions{Force: false, EnvVars: options.EnvVars}
|
||||
|
||||
onSuccess := func() {
|
||||
gui.State.Panels.Branches.SelectedLine = 0
|
||||
gui.State.Panels.Commits.SelectedLine = 0
|
||||
gui.State.Panels.Branches.SelectedLineIdx = 0
|
||||
gui.State.Panels.Commits.SelectedLineIdx = 0
|
||||
// loading a heap of commits is slow so we limit them whenever doing a reset
|
||||
gui.State.Panels.Commits.LimitCommits = true
|
||||
}
|
||||
@@ -167,43 +151,61 @@ func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions)
|
||||
if err := gui.GitCommand.Checkout(ref, cmdOptions); err != nil {
|
||||
// note, this will only work for english-language git commands. If we force git to use english, and the error isn't this one, then the user will receive an english command they may not understand. I'm not sure what the best solution to this is. Running the command once in english and a second time in the native language is one option
|
||||
|
||||
if 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, cmdOptions); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
|
||||
onSuccess()
|
||||
|
||||
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 options.onRefNotFound != nil && strings.Contains(err.Error(), "did not match any file(s) known to git") {
|
||||
return options.onRefNotFound(ref)
|
||||
}
|
||||
|
||||
if err := gui.createErrorPanel(gui.g, err.Error()); err != nil {
|
||||
if strings.Contains(err.Error(), "Please commit your changes or stash them before you switch branch") {
|
||||
// offer to autostash changes
|
||||
return gui.ask(askOpts{
|
||||
|
||||
title: gui.Tr.SLocalize("AutoStashTitle"),
|
||||
prompt: gui.Tr.SLocalize("AutoStashPrompt"),
|
||||
handleConfirm: func() error {
|
||||
if err := gui.GitCommand.StashSave(gui.Tr.SLocalize("StashPrefix") + ref); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
if err := gui.GitCommand.Checkout(ref, cmdOptions); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
onSuccess()
|
||||
if err := gui.GitCommand.StashDo(0, "pop"); err != nil {
|
||||
if err := gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI}); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if err := gui.surfaceError(err); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
onSuccess()
|
||||
|
||||
return gui.refreshSidePanels(gui.g)
|
||||
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI})
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCheckoutByName(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("BranchName")+":", "", func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.handleCheckoutRef(gui.trimmedContent(v), handleCheckoutRefOptions{})
|
||||
return gui.prompt(gui.Tr.SLocalize("BranchName")+":", "", func(response string) error {
|
||||
return gui.handleCheckoutRef(response, handleCheckoutRefOptions{
|
||||
onRefNotFound: func(ref string) error {
|
||||
|
||||
return gui.ask(askOpts{
|
||||
|
||||
title: gui.Tr.SLocalize("BranchNotFoundTitle"),
|
||||
prompt: fmt.Sprintf("%s %s%s", gui.Tr.SLocalize("BranchNotFoundPrompt"), ref, "?"),
|
||||
handleConfirm: func() error {
|
||||
return gui.createNewBranchWithName(ref)
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -215,45 +217,37 @@ func (gui *Gui) getCheckedOutBranch() *commands.Branch {
|
||||
return gui.State.Branches[0]
|
||||
}
|
||||
|
||||
func (gui *Gui) handleNewBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) createNewBranchWithName(newBranchName string) error {
|
||||
branch := gui.getSelectedBranch()
|
||||
if branch == nil {
|
||||
return nil
|
||||
}
|
||||
message := gui.Tr.TemplateLocalize(
|
||||
"NewBranchNameBranchOff",
|
||||
Teml{
|
||||
"branchName": branch.Name,
|
||||
},
|
||||
)
|
||||
return gui.createPromptPanel(g, v, message, "", func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.NewBranch(gui.trimmedContent(v), branch.Name); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
if err := gui.refreshSidePanels(g); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
return gui.handleBranchSelect(g, v)
|
||||
})
|
||||
|
||||
if err := gui.GitCommand.NewBranch(newBranchName, branch.Name); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
gui.State.Panels.Branches.SelectedLineIdx = 0
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleDeleteBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.deleteBranch(g, v, false)
|
||||
return gui.deleteBranch(false)
|
||||
}
|
||||
|
||||
func (gui *Gui) deleteBranch(g *gocui.Gui, v *gocui.View, force bool) error {
|
||||
func (gui *Gui) deleteBranch(force bool) error {
|
||||
selectedBranch := gui.getSelectedBranch()
|
||||
if selectedBranch == nil {
|
||||
return nil
|
||||
}
|
||||
checkedOutBranch := gui.getCheckedOutBranch()
|
||||
if checkedOutBranch.Name == selectedBranch.Name {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantDeleteCheckOutBranch"))
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("CantDeleteCheckOutBranch"))
|
||||
}
|
||||
return gui.deleteNamedBranch(g, v, selectedBranch, force)
|
||||
return gui.deleteNamedBranch(selectedBranch, force)
|
||||
}
|
||||
|
||||
func (gui *Gui) deleteNamedBranch(g *gocui.Gui, v *gocui.View, selectedBranch *commands.Branch, force bool) error {
|
||||
func (gui *Gui) deleteNamedBranch(selectedBranch *commands.Branch, force bool) error {
|
||||
title := gui.Tr.SLocalize("DeleteBranch")
|
||||
var messageID string
|
||||
if force {
|
||||
@@ -267,25 +261,35 @@ func (gui *Gui) deleteNamedBranch(g *gocui.Gui, v *gocui.View, selectedBranch *c
|
||||
"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.ask(askOpts{
|
||||
|
||||
title: title,
|
||||
prompt: message,
|
||||
handleConfirm: func() 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(selectedBranch, true)
|
||||
}
|
||||
return gui.createErrorPanel(errMessage)
|
||||
}
|
||||
return gui.createErrorPanel(g, errMessage)
|
||||
}
|
||||
return gui.refreshSidePanels(g)
|
||||
}, nil)
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{BRANCHES}})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
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")
|
||||
return gui.createErrorPanel("Cannot merge branch in detached head state. You might have checked out a commit directly or a remote branch, in which case you should checkout the local branch you want to be on")
|
||||
}
|
||||
checkedOutBranchName := gui.getCheckedOutBranch().Name
|
||||
if checkedOutBranchName == branchName {
|
||||
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("CantMergeBranchIntoItself"))
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("CantMergeBranchIntoItself"))
|
||||
}
|
||||
prompt := gui.Tr.TemplateLocalize(
|
||||
"ConfirmMerge",
|
||||
@@ -294,15 +298,23 @@ func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error {
|
||||
"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.ask(askOpts{
|
||||
|
||||
title: gui.Tr.SLocalize("MergingTitle"),
|
||||
prompt: prompt,
|
||||
handleConfirm: func() error {
|
||||
err := gui.GitCommand.Merge(branchName, commands.MergeOpts{})
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
}, nil)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
selectedBranchName := gui.getSelectedBranch().Name
|
||||
return gui.mergeBranchIntoCheckedOutBranch(selectedBranchName)
|
||||
}
|
||||
@@ -313,9 +325,13 @@ func (gui *Gui) handleRebaseOntoLocalBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRebaseOntoBranch(selectedBranchName string) error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
checkedOutBranch := gui.getCheckedOutBranch().Name
|
||||
if selectedBranchName == checkedOutBranch {
|
||||
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("CantRebaseOntoSelf"))
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("CantRebaseOntoSelf"))
|
||||
}
|
||||
prompt := gui.Tr.TemplateLocalize(
|
||||
"ConfirmRebase",
|
||||
@@ -324,11 +340,16 @@ func (gui *Gui) handleRebaseOntoBranch(selectedBranchName string) error {
|
||||
"selectedBranch": selectedBranchName,
|
||||
},
|
||||
)
|
||||
return gui.createConfirmationPanel(gui.g, gui.getBranchesView(), true, gui.Tr.SLocalize("RebasingTitle"), prompt,
|
||||
func(g *gocui.Gui, v *gocui.View) error {
|
||||
|
||||
return gui.ask(askOpts{
|
||||
|
||||
title: gui.Tr.SLocalize("RebasingTitle"),
|
||||
prompt: prompt,
|
||||
handleConfirm: func() error {
|
||||
err := gui.GitCommand.RebaseBranch(selectedBranchName)
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
}, nil)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
|
||||
@@ -340,15 +361,15 @@ func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
|
||||
return nil
|
||||
}
|
||||
if branch.Pushables == "?" {
|
||||
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("FwdNoUpstream"))
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("FwdNoUpstream"))
|
||||
}
|
||||
if branch.Pushables != "0" {
|
||||
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("FwdCommitsToPush"))
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("FwdCommitsToPush"))
|
||||
}
|
||||
|
||||
upstream, err := gui.GitCommand.GetUpstreamForBranch(branch.Name)
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
split := strings.Split(upstream, "/")
|
||||
@@ -363,81 +384,19 @@ func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
|
||||
},
|
||||
)
|
||||
go func() {
|
||||
_ = gui.createLoaderPanel(gui.g, v, message)
|
||||
_ = gui.createLoaderPanel(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)
|
||||
if gui.State.Panels.Branches.SelectedLineIdx == 0 {
|
||||
_ = gui.pullWithMode("ff-only", PullFilesOptions{})
|
||||
} else {
|
||||
if err := gui.GitCommand.FastForward(branch.Name, remoteName, remoteBranchName); err != nil {
|
||||
_ = gui.createErrorPanel(gui.g, err.Error())
|
||||
}
|
||||
_ = gui.refreshBranches(gui.g)
|
||||
err := gui.GitCommand.FastForward(branch.Name, remoteName, remoteBranchName, gui.promptUserForCredential)
|
||||
gui.handleCredentialsPopup(err)
|
||||
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{BRANCHES}})
|
||||
}
|
||||
|
||||
_ = 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
|
||||
if err := gui.onSearchEscape(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
contextTabIndexMap := map[string]int{
|
||||
"local-branches": 0,
|
||||
"remotes": 1,
|
||||
"remote-branches": 1,
|
||||
"tags": 2,
|
||||
}
|
||||
|
||||
branchesView.TabIndex = contextTabIndexMap[context]
|
||||
|
||||
return gui.refreshBranchesViewWithSelection()
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshBranchesViewWithSelection() error {
|
||||
branchesView := gui.getBranchesView()
|
||||
|
||||
switch branchesView.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 {
|
||||
@@ -447,41 +406,27 @@ func (gui *Gui) handleCreateResetToBranchMenu(g *gocui.Gui, v *gocui.View) error
|
||||
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
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRenameBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
branch := gui.getSelectedBranch()
|
||||
if branch == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: find a way to not checkout the branch here if it's not the current branch (i.e. find some
|
||||
// way to get it to show up in the reflog)
|
||||
|
||||
promptForNewName := func() error {
|
||||
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("NewBranchNamePrompt")+" "+branch.Name+":", "", func(g *gocui.Gui, v *gocui.View) error {
|
||||
newName := gui.trimmedContent(v)
|
||||
if err := gui.GitCommand.RenameBranch(branch.Name, newName); err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.prompt(gui.Tr.SLocalize("NewBranchNamePrompt")+" "+branch.Name+":", "", func(newBranchName string) error {
|
||||
if err := gui.GitCommand.RenameBranch(branch.Name, newBranchName); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
// need to checkout so that the branch shows up in our reflog and therefore
|
||||
// doesn't get lost among all the other branches when we switch to something else
|
||||
if err := gui.GitCommand.Checkout(newName, commands.CheckoutOptions{Force: false}); err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
if err := gui.GitCommand.Checkout(newBranchName, commands.CheckoutOptions{Force: false}); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.refreshBranches(gui.g)
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -492,9 +437,13 @@ func (gui *Gui) handleRenameBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
if notTrackingRemote {
|
||||
return promptForNewName()
|
||||
}
|
||||
return gui.createConfirmationPanel(gui.g, v, true, gui.Tr.SLocalize("renameBranch"), gui.Tr.SLocalize("RenameBranchWarning"), func(_g *gocui.Gui, _v *gocui.View) error {
|
||||
return promptForNewName()
|
||||
}, nil)
|
||||
|
||||
return gui.ask(askOpts{
|
||||
|
||||
title: gui.Tr.SLocalize("renameBranch"),
|
||||
prompt: gui.Tr.SLocalize("RenameBranchWarning"),
|
||||
handleConfirm: promptForNewName,
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) currentBranch() *commands.Branch {
|
||||
@@ -503,3 +452,46 @@ func (gui *Gui) currentBranch() *commands.Branch {
|
||||
}
|
||||
return gui.State.Branches[0]
|
||||
}
|
||||
|
||||
func (gui *Gui) handleNewBranchOffCurrentItem() error {
|
||||
context := gui.currentSideContext()
|
||||
|
||||
item, ok := context.GetSelectedItem()
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
message := gui.Tr.TemplateLocalize(
|
||||
"NewBranchNameBranchOff",
|
||||
Teml{
|
||||
"branchName": item.Description(),
|
||||
},
|
||||
)
|
||||
|
||||
prefilledName := ""
|
||||
if context.GetKey() == REMOTE_BRANCHES_CONTEXT_KEY {
|
||||
// will set to the remote's existing name
|
||||
prefilledName = item.ID()
|
||||
}
|
||||
return gui.prompt(message, prefilledName, func(response string) error {
|
||||
if err := gui.GitCommand.NewBranch(response, item.ID()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if we're currently in the branch commits context then the selected commit
|
||||
// is about to go to the top of the list
|
||||
if context.GetKey() == BRANCH_COMMITS_CONTEXT_KEY {
|
||||
context.GetPanelState().SetSelectedLineIdx(0)
|
||||
}
|
||||
|
||||
if context.GetKey() != gui.Contexts.Branches.Context.GetKey() {
|
||||
if err := gui.switchContext(gui.Contexts.Branches.Context); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
gui.State.Panels.Branches.SelectedLineIdx = 0
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
)
|
||||
|
||||
// you can only copy from one context at a time, because the order and position of commits matter
|
||||
|
||||
func (gui *Gui) resetCherryPickingIfNecessary(context Context) error {
|
||||
oldContextKey := gui.State.Modes.CherryPicking.ContextKey
|
||||
|
||||
if oldContextKey != context.GetKey() {
|
||||
// need to reset the cherry picking mode
|
||||
gui.State.Modes.CherryPicking.ContextKey = context.GetKey()
|
||||
gui.State.Modes.CherryPicking.CherryPickedCommits = make([]*commands.Commit, 0)
|
||||
|
||||
return gui.rerenderContextViewIfPresent(oldContextKey)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCopyCommit() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
// get currently selected commit, add the sha to state.
|
||||
context := gui.currentSideContext()
|
||||
if context == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := gui.resetCherryPickingIfNecessary(context); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
item, ok := context.SelectedItem()
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
commit, ok := item.(*commands.Commit)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// we will un-copy it if it's already copied
|
||||
for index, cherryPickedCommit := range gui.State.Modes.CherryPicking.CherryPickedCommits {
|
||||
if commit.Sha == cherryPickedCommit.Sha {
|
||||
gui.State.Modes.CherryPicking.CherryPickedCommits = append(gui.State.Modes.CherryPicking.CherryPickedCommits[0:index], gui.State.Modes.CherryPicking.CherryPickedCommits[index+1:]...)
|
||||
return context.HandleRender()
|
||||
}
|
||||
}
|
||||
|
||||
gui.addCommitToCherryPickedCommits(context.GetPanelState().GetSelectedLineIdx())
|
||||
return context.HandleRender()
|
||||
}
|
||||
|
||||
func (gui *Gui) cherryPickedCommitShaMap() map[string]bool {
|
||||
commitShaMap := map[string]bool{}
|
||||
for _, commit := range gui.State.Modes.CherryPicking.CherryPickedCommits {
|
||||
commitShaMap[commit.Sha] = true
|
||||
}
|
||||
return commitShaMap
|
||||
}
|
||||
|
||||
func (gui *Gui) commitsListForContext() []*commands.Commit {
|
||||
context := gui.currentSideContext()
|
||||
if context == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// using a switch statement, but we should use polymorphism
|
||||
switch context.GetKey() {
|
||||
case BRANCH_COMMITS_CONTEXT_KEY:
|
||||
return gui.State.Commits
|
||||
case REFLOG_COMMITS_CONTEXT_KEY:
|
||||
return gui.State.FilteredReflogCommits
|
||||
case SUB_COMMITS_CONTEXT_KEY:
|
||||
return gui.State.SubCommits
|
||||
default:
|
||||
gui.Log.Errorf("no commit list for context %s", context.GetKey())
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) addCommitToCherryPickedCommits(index int) {
|
||||
commitShaMap := gui.cherryPickedCommitShaMap()
|
||||
commitsList := gui.commitsListForContext()
|
||||
commitShaMap[commitsList[index].Sha] = true
|
||||
|
||||
newCommits := []*commands.Commit{}
|
||||
for _, commit := range commitsList {
|
||||
if commitShaMap[commit.Sha] {
|
||||
// duplicating just the things we need to put in the rebase TODO list
|
||||
newCommits = append(newCommits, &commands.Commit{Name: commit.Name, Sha: commit.Sha})
|
||||
}
|
||||
}
|
||||
|
||||
gui.State.Modes.CherryPicking.CherryPickedCommits = newCommits
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCopyCommitRange() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
// get currently selected commit, add the sha to state.
|
||||
context := gui.currentSideContext()
|
||||
if context == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := gui.resetCherryPickingIfNecessary(context); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
commitShaMap := gui.cherryPickedCommitShaMap()
|
||||
commitsList := gui.commitsListForContext()
|
||||
selectedLineIdx := context.GetPanelState().GetSelectedLineIdx()
|
||||
|
||||
if selectedLineIdx > len(commitsList)-1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// find the last commit that is copied that's above our position
|
||||
// if there are none, startIndex = 0
|
||||
startIndex := 0
|
||||
for index, commit := range commitsList[0:selectedLineIdx] {
|
||||
if commitShaMap[commit.Sha] {
|
||||
startIndex = index
|
||||
}
|
||||
}
|
||||
|
||||
for index := startIndex; index <= selectedLineIdx; index++ {
|
||||
gui.addCommitToCherryPickedCommits(index)
|
||||
}
|
||||
|
||||
return context.HandleRender()
|
||||
}
|
||||
|
||||
// HandlePasteCommits begins a cherry-pick rebase with the commits the user has copied
|
||||
func (gui *Gui) HandlePasteCommits() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.SLocalize("CherryPick"),
|
||||
prompt: gui.Tr.SLocalize("SureCherryPick"),
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.SLocalize("CherryPickingStatus"), func() error {
|
||||
err := gui.GitCommand.CherryPickCommits(gui.State.Modes.CherryPicking.CherryPickedCommits)
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) exitCherryPickingMode() error {
|
||||
contextKey := gui.State.Modes.CherryPicking.ContextKey
|
||||
|
||||
gui.State.Modes.CherryPicking.ContextKey = ""
|
||||
gui.State.Modes.CherryPicking.CherryPickedCommits = nil
|
||||
|
||||
if contextKey == "" {
|
||||
gui.Log.Warn("context key blank when trying to exit cherry picking mode")
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.rerenderContextViewIfPresent(contextKey)
|
||||
}
|
||||
|
||||
func (gui *Gui) rerenderContextViewIfPresent(contextKey string) error {
|
||||
if contextKey == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
context := gui.contextForContextKey(contextKey)
|
||||
|
||||
viewName := context.GetViewName()
|
||||
|
||||
view, err := gui.g.View(viewName)
|
||||
if err != nil {
|
||||
gui.Log.Warn(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if view.Context == contextKey {
|
||||
if err := context.HandleRender(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+111
-113
@@ -1,73 +1,55 @@
|
||||
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 {
|
||||
func (gui *Gui) getSelectedCommitFile() *commands.CommitFile {
|
||||
selectedLine := gui.State.Panels.CommitFiles.SelectedLineIdx
|
||||
if selectedLine == -1 || selectedLine > len(gui.State.CommitFiles)-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
|
||||
func (gui *Gui) handleCommitFileSelect() error {
|
||||
gui.handleEscapeLineByLinePanel()
|
||||
|
||||
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)
|
||||
commitFile := gui.getSelectedCommitFile()
|
||||
if commitFile == nil {
|
||||
gui.renderString(g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := gui.refreshSecondaryPatchPanel(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v.FocusPoint(0, gui.State.Panels.CommitFiles.SelectedLine)
|
||||
to := commitFile.Parent
|
||||
from, reverse := gui.getFromAndReverseArgsForDiff(to)
|
||||
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.ShowCommitFileCmdStr(commitFile.Sha, commitFile.Name, false),
|
||||
gui.GitCommand.ShowFileDiffCmdStr(from, to, reverse, commitFile.Name, false),
|
||||
)
|
||||
if err := gui.newPtyTask("main", cmd); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
task := gui.createRunPtyTask(cmd)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSwitchToCommitsPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.switchFocus(g, v, gui.getCommitsView())
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
main: &viewUpdateOpts{
|
||||
title: "Patch",
|
||||
task: task,
|
||||
},
|
||||
secondary: gui.secondaryPatchPanelUpdateOpts(),
|
||||
})
|
||||
}
|
||||
|
||||
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())
|
||||
file := gui.getSelectedCommitFile()
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.refreshFiles()
|
||||
if err := gui.GitCommand.CheckoutFile(file.Parent, file.Name); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleDiscardOldFileChange(g *gocui.Gui, v *gocui.View) error {
|
||||
@@ -75,104 +57,105 @@ func (gui *Gui) handleDiscardOldFileChange(g *gocui.Gui, v *gocui.View) error {
|
||||
return err
|
||||
}
|
||||
|
||||
fileName := gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLine].Name
|
||||
fileName := gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLineIdx].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.ask(askOpts{
|
||||
title: gui.Tr.SLocalize("DiscardFileChangesTitle"),
|
||||
prompt: gui.Tr.SLocalize("DiscardFileChangesPrompt"),
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
|
||||
if err := gui.GitCommand.DiscardOldFileChanges(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, fileName); err != nil {
|
||||
if err := gui.handleGenericMergeCommandResult(err); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(gui.g)
|
||||
})
|
||||
}, nil)
|
||||
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI})
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
to := gui.State.Panels.CommitFiles.refName
|
||||
from, reverse := gui.getFromAndReverseArgsForDiff(to)
|
||||
|
||||
files, err := gui.GitCommand.GetCommitFiles(commit.Sha, gui.GitCommand.PatchManager)
|
||||
files, err := gui.GitCommand.GetFilesInDiff(from, to, reverse, gui.GitCommand.PatchManager)
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
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)
|
||||
return gui.postRefreshUpdate(gui.Contexts.CommitFiles.Context)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleOpenOldCommitFile(g *gocui.Gui, v *gocui.View) error {
|
||||
file := gui.getSelectedCommitFile(g)
|
||||
file := gui.getSelectedCommitFile()
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.openFile(file.Name)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleToggleFileForPatch(g *gocui.Gui, v *gocui.View) error {
|
||||
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
|
||||
return err
|
||||
func (gui *Gui) handleEditCommitFile(g *gocui.Gui, v *gocui.View) error {
|
||||
file := gui.getSelectedCommitFile()
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
commitFile := gui.getSelectedCommitFile(g)
|
||||
return gui.editFile(file.Name)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleToggleFileForPatch(g *gocui.Gui, v *gocui.View) error {
|
||||
commitFile := gui.getSelectedCommitFile()
|
||||
if commitFile == nil {
|
||||
gui.renderString(g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
|
||||
return nil
|
||||
}
|
||||
|
||||
toggleTheFile := func() error {
|
||||
if !gui.GitCommand.PatchManager.CommitSelected() {
|
||||
if !gui.GitCommand.PatchManager.Active() {
|
||||
if err := gui.startPatchManager(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
gui.GitCommand.PatchManager.ToggleFileWhole(commitFile.Name)
|
||||
if err := gui.GitCommand.PatchManager.ToggleFileWhole(commitFile.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if gui.GitCommand.PatchManager.IsEmpty() {
|
||||
gui.GitCommand.PatchManager.Reset()
|
||||
}
|
||||
|
||||
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)
|
||||
if gui.GitCommand.PatchManager.Active() && gui.GitCommand.PatchManager.To != commitFile.Parent {
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.SLocalize("DiscardPatch"),
|
||||
prompt: gui.Tr.SLocalize("DiscardPatchConfirm"),
|
||||
handleConfirm: func() error {
|
||||
gui.GitCommand.PatchManager.Reset()
|
||||
return toggleTheFile()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
canRebase := gui.State.Panels.CommitFiles.canRebase
|
||||
|
||||
commit := gui.getSelectedCommit(gui.g)
|
||||
if commit == nil {
|
||||
return errors.New("No commit selected")
|
||||
}
|
||||
to := gui.State.Panels.CommitFiles.refName
|
||||
from, reverse := gui.getFromAndReverseArgsForDiff(to)
|
||||
|
||||
gui.GitCommand.PatchManager.Start(commit.Sha, diffMap)
|
||||
gui.GitCommand.PatchManager.Start(from, to, reverse, canRebase)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -181,41 +164,56 @@ func (gui *Gui) handleEnterCommitFile(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
|
||||
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
commitFile := gui.getSelectedCommitFile(gui.g)
|
||||
commitFile := gui.getSelectedCommitFile()
|
||||
if commitFile == nil {
|
||||
gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
|
||||
return nil
|
||||
}
|
||||
|
||||
enterTheFile := func(selectedLineIdx int) error {
|
||||
if !gui.GitCommand.PatchManager.CommitSelected() {
|
||||
if !gui.GitCommand.PatchManager.Active() {
|
||||
if err := gui.startPatchManager(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
gui.changeMainViewsContext("patch-building")
|
||||
if err := gui.switchFocus(gui.g, gui.getCommitFilesView(), gui.getMainView()); err != nil {
|
||||
if err := gui.switchContext(gui.Contexts.PatchBuilding.Context); 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)
|
||||
if gui.GitCommand.PatchManager.Active() && gui.GitCommand.PatchManager.To != commitFile.Parent {
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.SLocalize("DiscardPatch"),
|
||||
prompt: gui.Tr.SLocalize("DiscardPatchConfirm"),
|
||||
handlersManageFocus: true,
|
||||
handleConfirm: func() error {
|
||||
gui.GitCommand.PatchManager.Reset()
|
||||
return enterTheFile(selectedLineIdx)
|
||||
},
|
||||
handleClose: func() error {
|
||||
return gui.switchContext(gui.Contexts.CommitFiles.Context)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return enterTheFile(selectedLineIdx)
|
||||
}
|
||||
|
||||
func (gui *Gui) onCommitFilesPanelSearchSelect(selectedLine int) error {
|
||||
gui.State.Panels.CommitFiles.SelectedLine = selectedLine
|
||||
return gui.handleCommitFileSelect(gui.g, gui.getCommitFilesView())
|
||||
func (gui *Gui) switchToCommitFilesContext(refName string, canRebase bool, context Context, windowName string) error {
|
||||
// sometimes the commitFiles view is already shown in another window, so we need to ensure that window
|
||||
// no longer considers the commitFiles view as its main view.
|
||||
gui.resetWindowForView("commitFiles")
|
||||
|
||||
gui.State.Panels.CommitFiles.SelectedLineIdx = 0
|
||||
gui.State.Panels.CommitFiles.refName = refName
|
||||
gui.State.Panels.CommitFiles.canRebase = canRebase
|
||||
gui.Contexts.CommitFiles.Context.SetParentContext(context)
|
||||
gui.Contexts.CommitFiles.Context.SetWindowName(windowName)
|
||||
|
||||
if err := gui.refreshCommitFilesView(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.switchContext(gui.Contexts.CommitFiles.Context)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
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())
|
||||
return false, gui.surfaceError(err)
|
||||
}
|
||||
}
|
||||
if sub != nil {
|
||||
@@ -28,7 +28,7 @@ func (gui *Gui) runSyncOrAsyncCommand(sub *exec.Cmd, err error) (bool, error) {
|
||||
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"))
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("CommitWithoutMessageErr"))
|
||||
}
|
||||
flags := ""
|
||||
skipHookPrefix := gui.Config.GetUserConfig().GetString("git.skipHookPrefix")
|
||||
@@ -43,32 +43,25 @@ func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
|
||||
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)
|
||||
gui.clearEditorView(v)
|
||||
_ = gui.returnFromContext()
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitClose(g *gocui.Gui, v *gocui.View) error {
|
||||
_, _ = g.SetViewOnBottom("commitMessage")
|
||||
return gui.switchFocus(g, v, gui.getFilesView())
|
||||
return gui.returnFromContext()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitFocused(g *gocui.Gui, v *gocui.View) error {
|
||||
if _, err := g.SetViewOnTop("commitMessage"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitMessageFocused() error {
|
||||
message := gui.Tr.TemplateLocalize(
|
||||
"CloseConfirm",
|
||||
"CommitMessageConfirm",
|
||||
Teml{
|
||||
"keyBindClose": "esc",
|
||||
"keyBindConfirm": "enter",
|
||||
"keyBindNewLine": "tab",
|
||||
},
|
||||
)
|
||||
gui.renderString(g, "options", message)
|
||||
gui.renderString("options", message)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+270
-367
@@ -1,18 +1,16 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"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
|
||||
func (gui *Gui) getSelectedLocalCommit() *commands.Commit {
|
||||
selectedLine := gui.State.Panels.Commits.SelectedLineIdx
|
||||
if selectedLine == -1 {
|
||||
return nil
|
||||
}
|
||||
@@ -20,106 +18,112 @@ func (gui *Gui) getSelectedCommit(g *gocui.Gui) *commands.Commit {
|
||||
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
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitSelect() error {
|
||||
state := gui.State.Panels.Commits
|
||||
if state.SelectedLine > 290 && state.LimitCommits {
|
||||
if state.SelectedLineIdx > 290 && state.LimitCommits {
|
||||
state.LimitCommits = false
|
||||
go func() {
|
||||
if err := gui.refreshCommitsWithLimit(); err != nil {
|
||||
_ = gui.createErrorPanel(gui.g, err.Error())
|
||||
_ = gui.surfaceError(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
gui.getMainView().Title = "Patch"
|
||||
gui.getSecondaryView().Title = "Custom Patch"
|
||||
gui.handleEscapeLineByLinePanel()
|
||||
|
||||
commit := gui.getSelectedCommit(g)
|
||||
var task updateTask
|
||||
commit := gui.getSelectedLocalCommit()
|
||||
if commit == nil {
|
||||
return gui.newStringTask("main", gui.Tr.SLocalize("NoCommitsThisBranch"))
|
||||
task = gui.createRenderStringTask(gui.Tr.SLocalize("NoCommitsThisBranch"))
|
||||
} else {
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.Modes.Filtering.Path),
|
||||
)
|
||||
task = gui.createRunPtyTask(cmd)
|
||||
}
|
||||
|
||||
v.FocusPoint(0, gui.State.Panels.Commits.SelectedLine)
|
||||
|
||||
// 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
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
main: &viewUpdateOpts{
|
||||
title: "Patch",
|
||||
task: task,
|
||||
},
|
||||
secondary: gui.secondaryPatchPanelUpdateOpts(),
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
// during startup, the bottleneck is fetching the reflog entries. We need these
|
||||
// on startup to sort the branches by recency. So we have two phases: INITIAL, and COMPLETE.
|
||||
// In the initial phase we don't get any reflog commits, but we asynchronously get them
|
||||
// and refresh the branches after that
|
||||
func (gui *Gui) refreshReflogCommitsConsideringStartup() {
|
||||
switch gui.State.StartupStage {
|
||||
case INITIAL:
|
||||
go func() {
|
||||
if err := gui.refreshReflogCommits(); err != nil {
|
||||
_ = gui.createErrorPanel(gui.g, err.Error())
|
||||
}
|
||||
_ = gui.refreshReflogCommits()
|
||||
gui.refreshBranches()
|
||||
gui.State.StartupStage = COMPLETE
|
||||
}()
|
||||
|
||||
if g.CurrentView() == gui.getCommitFilesView() || (g.CurrentView() == gui.getMainView() || gui.State.MainContext == "patch-building") {
|
||||
return gui.refreshCommitFilesView()
|
||||
case COMPLETE:
|
||||
_ = gui.refreshReflogCommits()
|
||||
}
|
||||
}
|
||||
|
||||
// whenever we change commits, we should update branches because the upstream/downstream
|
||||
// counts can change. Whenever we change branches we should probably also change commits
|
||||
// e.g. in the case of switching branches.
|
||||
func (gui *Gui) refreshCommits() error {
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
|
||||
go func() {
|
||||
gui.refreshReflogCommitsConsideringStartup()
|
||||
|
||||
gui.refreshBranches()
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
_ = gui.refreshCommitsWithLimit()
|
||||
if gui.g.CurrentView() == gui.getCommitFilesView() || (gui.currentContext().GetKey() == gui.Contexts.PatchBuilding.Context.GetKey()) {
|
||||
_ = gui.refreshCommitFilesView()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
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
|
||||
}
|
||||
builder := commands.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr, gui.State.Modes.CherryPicking.CherryPickedCommits)
|
||||
|
||||
commits, err := builder.GetCommits(gui.State.Panels.Commits.LimitCommits)
|
||||
commits, err := builder.GetCommits(
|
||||
commands.GetCommitsOptions{
|
||||
Limit: gui.State.Panels.Commits.LimitCommits,
|
||||
FilterPath: gui.State.Modes.Filtering.Path,
|
||||
IncludeRebaseCommits: true,
|
||||
RefName: "HEAD",
|
||||
},
|
||||
)
|
||||
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
|
||||
return gui.postRefreshUpdate(gui.Contexts.BranchCommits.Context)
|
||||
}
|
||||
|
||||
// specific functions
|
||||
|
||||
func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(gui.State.Commits) <= 1 {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash"))
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("YouNoCommitsToSquash"))
|
||||
}
|
||||
|
||||
applied, err := gui.handleMidRebaseCommand("squash")
|
||||
@@ -130,17 +134,25 @@ func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return 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 gui.ask(askOpts{
|
||||
title: gui.Tr.SLocalize("Squash"),
|
||||
prompt: gui.Tr.SLocalize("SureSquashThisCommit"),
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.SLocalize("SquashingStatus"), func() error {
|
||||
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "squash")
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(gui.State.Commits) <= 1 {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash"))
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("YouNoCommitsToSquash"))
|
||||
}
|
||||
|
||||
applied, err := gui.handleMidRebaseCommand("fixup")
|
||||
@@ -151,15 +163,23 @@ func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return 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 gui.ask(askOpts{
|
||||
title: gui.Tr.SLocalize("Fixup"),
|
||||
prompt: gui.Tr.SLocalize("SureFixupThisCommit"),
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.SLocalize("FixingStatus"), func() error {
|
||||
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "fixup")
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
applied, err := gui.handleMidRebaseCommand("reword")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -168,21 +188,34 @@ func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if gui.State.Panels.Commits.SelectedLine != 0 {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlyRenameTopCommit"))
|
||||
if gui.State.Panels.Commits.SelectedLineIdx != 0 {
|
||||
return gui.createErrorPanel(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())
|
||||
|
||||
commit := gui.getSelectedLocalCommit()
|
||||
if commit == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
message, err := gui.GitCommand.GetCommitMessage(commit.Sha)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.prompt(gui.Tr.SLocalize("renameCommit"), message, func(response string) error {
|
||||
if err := gui.GitCommand.RenameCommit(response); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
if err := gui.refreshCommits(g); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return gui.handleCommitSelect(g, v)
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
applied, err := gui.handleMidRebaseCommand("reword")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -191,9 +224,9 @@ func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
subProcess, err := gui.GitCommand.RewordCommit(gui.State.Commits, gui.State.Panels.Commits.SelectedLine)
|
||||
subProcess, err := gui.GitCommand.RewordCommit(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx)
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
if subProcess != nil {
|
||||
gui.SubProcess = subProcess
|
||||
@@ -207,7 +240,7 @@ func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
|
||||
// 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]
|
||||
selectedCommit := gui.State.Commits[gui.State.Panels.Commits.SelectedLineIdx]
|
||||
if selectedCommit.Status != "rebasing" {
|
||||
return false, nil
|
||||
}
|
||||
@@ -217,16 +250,22 @@ func (gui *Gui) handleMidRebaseCommand(action string) (bool, error) {
|
||||
// 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"))
|
||||
return true, gui.createErrorPanel(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())
|
||||
if err := gui.GitCommand.EditRebaseTodo(gui.State.Panels.Commits.SelectedLineIdx, action); err != nil {
|
||||
return false, gui.surfaceError(err)
|
||||
}
|
||||
return true, gui.refreshCommits(gui.g)
|
||||
// TODO: consider doing this in a way that is less expensive. We don't actually
|
||||
// need to reload all the commits, just the TODO commits.
|
||||
return true, gui.refreshSidePanels(refreshOptions{scope: []int{COMMITS}})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitDelete(g *gocui.Gui, v *gocui.View) error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
applied, err := gui.handleMidRebaseCommand("drop")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -235,61 +274,77 @@ func (gui *Gui) handleCommitDelete(g *gocui.Gui, v *gocui.View) error {
|
||||
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)
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.SLocalize("DeleteCommitTitle"),
|
||||
prompt: gui.Tr.SLocalize("DeleteCommitPrompt"),
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.SLocalize("DeletingStatus"), func() error {
|
||||
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "drop")
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitMoveDown(g *gocui.Gui, v *gocui.View) error {
|
||||
index := gui.State.Panels.Commits.SelectedLine
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
index := gui.State.Panels.Commits.SelectedLineIdx
|
||||
selectedCommit := gui.State.Commits[index]
|
||||
if selectedCommit.Status == "rebasing" {
|
||||
if gui.State.Commits[index+1].Status != "rebasing" {
|
||||
return nil
|
||||
}
|
||||
if err := gui.GitCommand.MoveTodoDown(index); err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
gui.State.Panels.Commits.SelectedLine++
|
||||
return gui.refreshCommits(gui.g)
|
||||
gui.State.Panels.Commits.SelectedLineIdx++
|
||||
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI, scope: []int{COMMITS, BRANCHES}})
|
||||
}
|
||||
|
||||
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++
|
||||
gui.State.Panels.Commits.SelectedLineIdx++
|
||||
}
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitMoveUp(g *gocui.Gui, v *gocui.View) error {
|
||||
index := gui.State.Panels.Commits.SelectedLine
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
index := gui.State.Panels.Commits.SelectedLineIdx
|
||||
if index == 0 {
|
||||
return nil
|
||||
}
|
||||
selectedCommit := gui.State.Commits[index]
|
||||
if selectedCommit.Status == "rebasing" {
|
||||
if err := gui.GitCommand.MoveTodoDown(index - 1); err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
gui.State.Panels.Commits.SelectedLine--
|
||||
return gui.refreshCommits(gui.g)
|
||||
gui.State.Panels.Commits.SelectedLineIdx--
|
||||
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI, scope: []int{COMMITS, BRANCHES}})
|
||||
}
|
||||
|
||||
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--
|
||||
gui.State.Panels.Commits.SelectedLineIdx--
|
||||
}
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitEdit(g *gocui.Gui, v *gocui.View) error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
applied, err := gui.handleMidRebaseCommand("edit")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -299,21 +354,33 @@ func (gui *Gui) handleCommitEdit(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
|
||||
err = gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "edit")
|
||||
err = gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "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)
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.SLocalize("AmendCommitTitle"),
|
||||
prompt: gui.Tr.SLocalize("AmendCommitPrompt"),
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.SLocalize("AmendingStatus"), func() error {
|
||||
err := gui.GitCommand.AmendTo(gui.State.Commits[gui.State.Panels.Commits.SelectedLineIdx].Sha)
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitPick(g *gocui.Gui, v *gocui.View) error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
applied, err := gui.handleMidRebaseCommand("pick")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -328,137 +395,24 @@ func (gui *Gui) handleCommitPick(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
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) cherryPickedCommitShaMap() map[string]bool {
|
||||
commitShaMap := map[string]bool{}
|
||||
for _, commit := range gui.State.CherryPickedCommits {
|
||||
commitShaMap[commit.Sha] = true
|
||||
}
|
||||
return commitShaMap
|
||||
}
|
||||
|
||||
func (gui *Gui) addCommitToCherryPickedCommits(index int) {
|
||||
commitShaMap := gui.cherryPickedCommitShaMap()
|
||||
commitShaMap[gui.State.Commits[index].Sha] = true
|
||||
|
||||
newCommits := []*commands.Commit{}
|
||||
for _, commit := range gui.State.Commits {
|
||||
if commitShaMap[commit.Sha] {
|
||||
// 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 {
|
||||
commitShaMap := gui.cherryPickedCommitShaMap()
|
||||
|
||||
// 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 commitShaMap[commit.Sha] {
|
||||
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.renderBranchCommitsWithSelection()
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.switchFocus(g, gui.getCommitsView(), gui.getCommitFilesView())
|
||||
if err := gui.GitCommand.Revert(gui.State.Commits[gui.State.Panels.Commits.SelectedLineIdx].Sha); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
gui.State.Panels.Commits.SelectedLineIdx++
|
||||
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI, scope: []int{COMMITS, BRANCHES}})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleToggleDiffCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
selectLimit := 2
|
||||
|
||||
// get selected commit
|
||||
commit := gui.getSelectedCommit(g)
|
||||
func (gui *Gui) handleViewCommitFiles() error {
|
||||
commit := gui.getSelectedLocalCommit()
|
||||
if commit == nil {
|
||||
return gui.newStringTask("main", gui.Tr.SLocalize("NoCommitsThisBranch"))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
return gui.switchToCommitFilesContext(commit.Sha, true, gui.Contexts.BranchCommits.Context, "commits")
|
||||
}
|
||||
|
||||
func (gui *Gui) hasCommit(commits []*commands.Commit, target string) (int, bool) {
|
||||
@@ -475,49 +429,65 @@ func (gui *Gui) unchooseCommit(commits []*commands.Commit, i int) []*commands.Co
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateFixupCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
commit := gui.getSelectedCommit(g)
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
commit := gui.getSelectedLocalCommit()
|
||||
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.ask(askOpts{
|
||||
title: gui.Tr.SLocalize("CreateFixupCommit"),
|
||||
prompt: gui.Tr.TemplateLocalize(
|
||||
"SureCreateFixupCommit",
|
||||
Teml{
|
||||
"commit": commit.Sha,
|
||||
},
|
||||
),
|
||||
handleConfirm: func() error {
|
||||
if err := gui.GitCommand.CreateFixupCommit(commit.Sha); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(gui.g)
|
||||
}, nil)
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSquashAllAboveFixupCommits(g *gocui.Gui, v *gocui.View) error {
|
||||
commit := gui.getSelectedCommit(g)
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
commit := gui.getSelectedLocalCommit()
|
||||
if commit == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("SquashAboveCommits"), gui.Tr.TemplateLocalize(
|
||||
"SureSquashAboveCommits",
|
||||
Teml{
|
||||
"commit": commit.Sha,
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.SLocalize("SquashAboveCommits"),
|
||||
prompt: gui.Tr.TemplateLocalize(
|
||||
"SureSquashAboveCommits",
|
||||
Teml{
|
||||
"commit": commit.Sha,
|
||||
},
|
||||
),
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.SLocalize("SquashingStatus"), func() error {
|
||||
err := gui.GitCommand.SquashAllAboveFixupCommits(commit.Sha)
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
},
|
||||
), 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)
|
||||
commit := gui.getSelectedLocalCommit()
|
||||
if commit == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -526,123 +496,43 @@ func (gui *Gui) handleTagCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
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())
|
||||
return gui.prompt(gui.Tr.SLocalize("TagNameTitle"), "", func(response string) error {
|
||||
if err := gui.GitCommand.CreateLightweightTag(response, commitSha); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
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)
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{COMMITS, TAGS}})
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCheckoutCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
commit := gui.getSelectedCommit(g)
|
||||
commit := gui.getSelectedLocalCommit()
|
||||
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, handleCheckoutRefOptions{})
|
||||
}, 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.cherryPickedCommitShaMap())
|
||||
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
|
||||
if err := gui.onSearchEscape(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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)),
|
||||
)
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.SLocalize("checkoutCommit"),
|
||||
prompt: gui.Tr.SLocalize("SureCheckoutThisCommit"),
|
||||
handleConfirm: func() error {
|
||||
return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateCommitResetMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
commit := gui.getSelectedCommit(g)
|
||||
commit := gui.getSelectedLocalCommit()
|
||||
if commit == nil {
|
||||
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NoCommitsThisBranch"))
|
||||
return gui.createErrorPanel(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 {
|
||||
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{COMMITS}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -650,7 +540,20 @@ func (gui *Gui) handleOpenSearchForCommitsPanel(g *gocui.Gui, v *gocui.View) err
|
||||
return gui.handleOpenSearch(gui.g, v)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleResetCherryPick(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.State.CherryPickedCommits = []*commands.Commit{}
|
||||
return gui.refreshCommits(gui.g)
|
||||
func (gui *Gui) handleGotoBottomForCommitsPanel(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.refreshSidePanels(refreshOptions{mode: SYNC, scope: []int{COMMITS}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, context := range gui.getListContexts() {
|
||||
if context.ViewName == "commits" {
|
||||
return context.handleGotoBottom(g, v)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+144
-77
@@ -15,33 +15,107 @@ import (
|
||||
"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 {
|
||||
type createPopupPanelOpts struct {
|
||||
hasLoader bool
|
||||
editable bool
|
||||
title string
|
||||
prompt string
|
||||
handleConfirm func() error
|
||||
handleConfirmPrompt func(string) error
|
||||
handleClose func() error
|
||||
|
||||
// when handlersManageFocus is true, do not return from the confirmation context automatically. It's expected that the handlers will manage focus, whether that means switching to another context, or manually returning the context.
|
||||
handlersManageFocus bool
|
||||
}
|
||||
|
||||
type askOpts struct {
|
||||
title string
|
||||
prompt string
|
||||
handleConfirm func() error
|
||||
handleClose func() error
|
||||
handlersManageFocus bool
|
||||
}
|
||||
|
||||
func (gui *Gui) createLoaderPanel(currentView *gocui.View, prompt string) error {
|
||||
return gui.createPopupPanel(createPopupPanelOpts{
|
||||
prompt: prompt,
|
||||
hasLoader: true,
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) ask(opts askOpts) error {
|
||||
return gui.createPopupPanel(createPopupPanelOpts{
|
||||
title: opts.title,
|
||||
prompt: opts.prompt,
|
||||
handleConfirm: opts.handleConfirm,
|
||||
handleClose: opts.handleClose,
|
||||
handlersManageFocus: opts.handlersManageFocus,
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) prompt(title string, initialContent string, handleConfirm func(string) error) error {
|
||||
return gui.createPopupPanel(createPopupPanelOpts{
|
||||
title: title,
|
||||
prompt: initialContent,
|
||||
editable: true,
|
||||
handleConfirmPrompt: handleConfirm,
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) wrappedConfirmationFunction(handlersManageFocus bool, function func() 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 {
|
||||
if err := function(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return gui.closeConfirmationPrompt(g, returnFocusOnClose)
|
||||
if err := gui.closeConfirmationPrompt(handlersManageFocus); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) closeConfirmationPrompt(g *gocui.Gui, returnFocusOnClose bool) error {
|
||||
view, err := g.View("confirmation")
|
||||
if err != nil {
|
||||
func (gui *Gui) wrappedPromptConfirmationFunction(handlersManageFocus bool, function func(string) error) func(*gocui.Gui, *gocui.View) error {
|
||||
return func(g *gocui.Gui, v *gocui.View) error {
|
||||
if function != nil {
|
||||
if err := function(v.Buffer()); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := gui.closeConfirmationPrompt(handlersManageFocus); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) deleteConfirmationView() {
|
||||
gui.g.DeleteKeybinding("confirmation", gui.getKey("universal.confirm"), gocui.ModNone)
|
||||
gui.g.DeleteKeybinding("confirmation", gui.getKey("universal.confirm-alt1"), gocui.ModNone)
|
||||
gui.g.DeleteKeybinding("confirmation", gui.getKey("universal.return"), gocui.ModNone)
|
||||
|
||||
_ = gui.g.DeleteView("confirmation")
|
||||
}
|
||||
|
||||
func (gui *Gui) closeConfirmationPrompt(handlersManageFocus bool) error {
|
||||
view := gui.getConfirmationView()
|
||||
if view == 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)
|
||||
|
||||
if !handlersManageFocus {
|
||||
if err := gui.returnFromContext(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
g.DeleteKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone)
|
||||
g.DeleteKeybinding("confirmation", gocui.KeyEsc, gocui.ModNone)
|
||||
return g.DeleteView("confirmation")
|
||||
|
||||
gui.deleteConfirmationView()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) getMessageHeight(wrap bool, message string, width int) int {
|
||||
@@ -58,9 +132,18 @@ func (gui *Gui) getMessageHeight(wrap bool, message string, width int) int {
|
||||
return lineCount
|
||||
}
|
||||
|
||||
func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, wrap bool, prompt string) (int, int, int, int) {
|
||||
width, height := g.Size()
|
||||
func (gui *Gui) getConfirmationPanelDimensions(wrap bool, prompt string) (int, int, int, int) {
|
||||
width, height := gui.g.Size()
|
||||
// we want a minimum width up to a point, then we do it based on ratio.
|
||||
panelWidth := 4 * width / 7
|
||||
minWidth := 80
|
||||
if panelWidth < minWidth {
|
||||
if width-2 < minWidth {
|
||||
panelWidth = width - 2
|
||||
} else {
|
||||
panelWidth = minWidth
|
||||
}
|
||||
}
|
||||
panelHeight := gui.getMessageHeight(wrap, prompt, panelWidth)
|
||||
if panelHeight > height*3/4 {
|
||||
panelHeight = height * 3 / 4
|
||||
@@ -71,8 +154,8 @@ func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, wrap bool, prompt s
|
||||
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)
|
||||
func (gui *Gui) prepareConfirmationPanel(title, prompt string, hasLoader bool) (*gocui.View, error) {
|
||||
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(true, prompt)
|
||||
confirmationView, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0)
|
||||
if err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
@@ -87,35 +170,23 @@ func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt
|
||||
confirmationView.FgColor = theme.GocuiDefaultTextColor
|
||||
}
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
return gui.switchFocus(gui.g, currentView, confirmationView)
|
||||
return gui.switchContext(gui.Contexts.Confirmation.Context)
|
||||
})
|
||||
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 {
|
||||
func (gui *Gui) createPopupPanel(opts createPopupPanelOpts) error {
|
||||
gui.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)
|
||||
}
|
||||
gui.deleteConfirmationView()
|
||||
}
|
||||
confirmationView, err := gui.prepareConfirmationPanel(currentView, title, prompt, hasLoader)
|
||||
confirmationView, err := gui.prepareConfirmationPanel(opts.title, opts.prompt, opts.hasLoader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
confirmationView.Editable = editable
|
||||
if editable {
|
||||
confirmationView.Editable = opts.editable
|
||||
if opts.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)
|
||||
@@ -126,26 +197,13 @@ func (gui *Gui) createPopupPanel(g *gocui.Gui, currentView *gocui.View, title, p
|
||||
}()
|
||||
}
|
||||
|
||||
gui.renderString(g, "confirmation", prompt)
|
||||
return gui.setKeyBindings(g, handleConfirm, handleClose, returnFocusOnClose)
|
||||
gui.renderString("confirmation", opts.prompt)
|
||||
return gui.setKeyBindings(opts)
|
||||
})
|
||||
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 {
|
||||
func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
|
||||
actions := gui.Tr.TemplateLocalize(
|
||||
"CloseConfirm",
|
||||
Teml{
|
||||
@@ -154,34 +212,43 @@ func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*go
|
||||
},
|
||||
)
|
||||
|
||||
gui.renderString(g, "options", actions)
|
||||
if err := g.SetKeybinding("confirmation", nil, gocui.KeyEnter, gocui.ModNone, gui.wrappedConfirmationFunction(handleConfirm, returnFocusOnClose)); err != nil {
|
||||
gui.renderString("options", actions)
|
||||
var onConfirm func(*gocui.Gui, *gocui.View) error
|
||||
if opts.handleConfirmPrompt != nil {
|
||||
onConfirm = gui.wrappedPromptConfirmationFunction(opts.handlersManageFocus, opts.handleConfirmPrompt)
|
||||
} else {
|
||||
onConfirm = gui.wrappedConfirmationFunction(opts.handlersManageFocus, opts.handleConfirm)
|
||||
}
|
||||
|
||||
if err := gui.g.SetKeybinding("confirmation", nil, gui.getKey("universal.confirm"), gocui.ModNone, onConfirm); err != nil {
|
||||
return err
|
||||
}
|
||||
return g.SetKeybinding("confirmation", nil, gocui.KeyEsc, gocui.ModNone, gui.wrappedConfirmationFunction(handleClose, returnFocusOnClose))
|
||||
}
|
||||
|
||||
// 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)
|
||||
}()
|
||||
if err := gui.g.SetKeybinding("confirmation", nil, gui.getKey("universal.confirm-alt1"), gocui.ModNone, onConfirm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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)
|
||||
return gui.g.SetKeybinding("confirmation", nil, gui.getKey("universal.return"), gocui.ModNone, gui.wrappedConfirmationFunction(opts.handlersManageFocus, opts.handleClose))
|
||||
}
|
||||
|
||||
func (gui *Gui) createErrorPanel(g *gocui.Gui, message string) error {
|
||||
return gui.createSpecificErrorPanel(message, g.CurrentView(), true)
|
||||
func (gui *Gui) createErrorPanel(message string) error {
|
||||
colorFunction := color.New(color.FgRed).SprintFunc()
|
||||
coloredMessage := colorFunction(strings.TrimSpace(message))
|
||||
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.SLocalize("Error"),
|
||||
prompt: coloredMessage,
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) surfaceError(err error) error {
|
||||
for _, sentinelError := range gui.sentinelErrorsArr() {
|
||||
if err == sentinelError {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
|
||||
+710
-7
@@ -1,19 +1,722 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
const (
|
||||
SIDE_CONTEXT int = iota
|
||||
MAIN_CONTEXT
|
||||
TEMPORARY_POPUP
|
||||
PERSISTENT_POPUP
|
||||
)
|
||||
|
||||
const (
|
||||
STATUS_CONTEXT_KEY = "status"
|
||||
FILES_CONTEXT_KEY = "files"
|
||||
LOCAL_BRANCHES_CONTEXT_KEY = "localBranches"
|
||||
REMOTES_CONTEXT_KEY = "remotes"
|
||||
REMOTE_BRANCHES_CONTEXT_KEY = "remoteBranches"
|
||||
TAGS_CONTEXT_KEY = "tags"
|
||||
BRANCH_COMMITS_CONTEXT_KEY = "commits"
|
||||
REFLOG_COMMITS_CONTEXT_KEY = "reflogCommits"
|
||||
SUB_COMMITS_CONTEXT_KEY = "subCommits"
|
||||
COMMIT_FILES_CONTEXT_KEY = "commitFiles"
|
||||
STASH_CONTEXT_KEY = "stash"
|
||||
MAIN_NORMAL_CONTEXT_KEY = "normal"
|
||||
MAIN_MERGING_CONTEXT_KEY = "merging"
|
||||
MAIN_PATCH_BUILDING_CONTEXT_KEY = "patchBuilding"
|
||||
MAIN_STAGING_CONTEXT_KEY = "staging"
|
||||
MENU_CONTEXT_KEY = "menu"
|
||||
CREDENTIALS_CONTEXT_KEY = "credentials"
|
||||
CONFIRMATION_CONTEXT_KEY = "confirmation"
|
||||
SEARCH_CONTEXT_KEY = "confirmation"
|
||||
COMMIT_MESSAGE_CONTEXT_KEY = "commitMessage"
|
||||
)
|
||||
|
||||
type Context interface {
|
||||
HandleFocus() error
|
||||
HandleFocusLost() error
|
||||
HandleRender() error
|
||||
GetKind() int
|
||||
GetViewName() string
|
||||
GetWindowName() string
|
||||
SetWindowName(string)
|
||||
GetKey() string
|
||||
SetParentContext(Context)
|
||||
|
||||
// we return a bool here to tell us whether or not the returned value just wraps a nil
|
||||
GetParentContext() (Context, bool)
|
||||
GetOptionsMap() map[string]string
|
||||
}
|
||||
|
||||
type BasicContext struct {
|
||||
OnFocus func() error
|
||||
OnFocusLost func() error
|
||||
OnRender func() error
|
||||
OnGetOptionsMap func() map[string]string
|
||||
Kind int
|
||||
Key string
|
||||
ViewName string
|
||||
}
|
||||
|
||||
func (c BasicContext) GetOptionsMap() map[string]string {
|
||||
if c.OnGetOptionsMap != nil {
|
||||
return c.OnGetOptionsMap()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c BasicContext) SetWindowName(windowName string) {
|
||||
panic("can't set window name on basic context")
|
||||
}
|
||||
|
||||
func (c BasicContext) GetWindowName() string {
|
||||
// TODO: fix this up
|
||||
return c.GetViewName()
|
||||
}
|
||||
|
||||
func (c BasicContext) SetParentContext(Context) {
|
||||
panic("can't set parent context on basic context")
|
||||
}
|
||||
|
||||
func (c BasicContext) GetParentContext() (Context, bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (c BasicContext) HandleRender() error {
|
||||
if c.OnRender != nil {
|
||||
return c.OnRender()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c BasicContext) GetViewName() string {
|
||||
return c.ViewName
|
||||
}
|
||||
|
||||
func (c BasicContext) HandleFocus() error {
|
||||
return c.OnFocus()
|
||||
}
|
||||
|
||||
func (c BasicContext) HandleFocusLost() error {
|
||||
if c.OnFocusLost != nil {
|
||||
return c.OnFocusLost()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c BasicContext) GetKind() int {
|
||||
return c.Kind
|
||||
}
|
||||
|
||||
func (c BasicContext) GetKey() string {
|
||||
return c.Key
|
||||
}
|
||||
|
||||
type SimpleContextNode struct {
|
||||
Context Context
|
||||
}
|
||||
|
||||
type RemotesContextNode struct {
|
||||
Context Context
|
||||
Branches SimpleContextNode
|
||||
}
|
||||
|
||||
type ContextTree struct {
|
||||
Status SimpleContextNode
|
||||
Files SimpleContextNode
|
||||
Menu SimpleContextNode
|
||||
Branches SimpleContextNode
|
||||
Remotes RemotesContextNode
|
||||
Tags SimpleContextNode
|
||||
BranchCommits SimpleContextNode
|
||||
CommitFiles SimpleContextNode
|
||||
ReflogCommits SimpleContextNode
|
||||
SubCommits SimpleContextNode
|
||||
Stash SimpleContextNode
|
||||
Normal SimpleContextNode
|
||||
Staging SimpleContextNode
|
||||
PatchBuilding SimpleContextNode
|
||||
Merging SimpleContextNode
|
||||
Credentials SimpleContextNode
|
||||
Confirmation SimpleContextNode
|
||||
CommitMessage SimpleContextNode
|
||||
Search SimpleContextNode
|
||||
}
|
||||
|
||||
func (gui *Gui) allContexts() []Context {
|
||||
return []Context{
|
||||
gui.Contexts.Status.Context,
|
||||
gui.Contexts.Files.Context,
|
||||
gui.Contexts.Branches.Context,
|
||||
gui.Contexts.Remotes.Context,
|
||||
gui.Contexts.Remotes.Branches.Context,
|
||||
gui.Contexts.Tags.Context,
|
||||
gui.Contexts.BranchCommits.Context,
|
||||
gui.Contexts.CommitFiles.Context,
|
||||
gui.Contexts.ReflogCommits.Context,
|
||||
gui.Contexts.Stash.Context,
|
||||
gui.Contexts.Menu.Context,
|
||||
gui.Contexts.Confirmation.Context,
|
||||
gui.Contexts.Credentials.Context,
|
||||
gui.Contexts.CommitMessage.Context,
|
||||
gui.Contexts.Normal.Context,
|
||||
gui.Contexts.Staging.Context,
|
||||
gui.Contexts.Merging.Context,
|
||||
gui.Contexts.PatchBuilding.Context,
|
||||
gui.Contexts.SubCommits.Context,
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) contextTree() ContextTree {
|
||||
return ContextTree{
|
||||
Status: SimpleContextNode{
|
||||
Context: BasicContext{
|
||||
OnFocus: gui.handleStatusSelect,
|
||||
Kind: SIDE_CONTEXT,
|
||||
ViewName: "status",
|
||||
Key: STATUS_CONTEXT_KEY,
|
||||
},
|
||||
},
|
||||
Files: SimpleContextNode{
|
||||
Context: gui.filesListContext(),
|
||||
},
|
||||
Menu: SimpleContextNode{
|
||||
Context: gui.menuListContext(),
|
||||
},
|
||||
Remotes: RemotesContextNode{
|
||||
Context: gui.remotesListContext(),
|
||||
Branches: SimpleContextNode{
|
||||
Context: gui.remoteBranchesListContext(),
|
||||
},
|
||||
},
|
||||
BranchCommits: SimpleContextNode{
|
||||
Context: gui.branchCommitsListContext(),
|
||||
},
|
||||
CommitFiles: SimpleContextNode{
|
||||
Context: gui.commitFilesListContext(),
|
||||
},
|
||||
ReflogCommits: SimpleContextNode{
|
||||
Context: gui.reflogCommitsListContext(),
|
||||
},
|
||||
SubCommits: SimpleContextNode{
|
||||
Context: gui.subCommitsListContext(),
|
||||
},
|
||||
Branches: SimpleContextNode{
|
||||
Context: gui.branchesListContext(),
|
||||
},
|
||||
Tags: SimpleContextNode{
|
||||
Context: gui.tagsListContext(),
|
||||
},
|
||||
Stash: SimpleContextNode{
|
||||
Context: gui.stashListContext(),
|
||||
},
|
||||
Normal: SimpleContextNode{
|
||||
Context: BasicContext{
|
||||
OnFocus: func() error {
|
||||
return nil // TODO: should we do something here? We should allow for scrolling the panel
|
||||
},
|
||||
Kind: MAIN_CONTEXT,
|
||||
ViewName: "main",
|
||||
Key: MAIN_NORMAL_CONTEXT_KEY,
|
||||
},
|
||||
},
|
||||
Staging: SimpleContextNode{
|
||||
Context: BasicContext{
|
||||
OnFocus: func() error {
|
||||
return nil
|
||||
// TODO: centralise the code here
|
||||
// return gui.refreshStagingPanel(false, -1)
|
||||
},
|
||||
Kind: MAIN_CONTEXT,
|
||||
ViewName: "main",
|
||||
Key: MAIN_STAGING_CONTEXT_KEY,
|
||||
},
|
||||
},
|
||||
PatchBuilding: SimpleContextNode{
|
||||
Context: BasicContext{
|
||||
OnFocus: func() error {
|
||||
return nil
|
||||
// TODO: centralise the code here
|
||||
// return gui.refreshPatchBuildingPanel(-1)
|
||||
},
|
||||
Kind: MAIN_CONTEXT,
|
||||
ViewName: "main",
|
||||
Key: MAIN_PATCH_BUILDING_CONTEXT_KEY,
|
||||
},
|
||||
},
|
||||
Merging: SimpleContextNode{
|
||||
Context: BasicContext{
|
||||
OnFocus: func() error {
|
||||
return gui.refreshMergePanel()
|
||||
},
|
||||
Kind: MAIN_CONTEXT,
|
||||
ViewName: "main",
|
||||
Key: MAIN_MERGING_CONTEXT_KEY,
|
||||
OnGetOptionsMap: gui.getMergingOptions,
|
||||
},
|
||||
},
|
||||
Credentials: SimpleContextNode{
|
||||
Context: BasicContext{
|
||||
OnFocus: func() error { return gui.handleCredentialsViewFocused() },
|
||||
Kind: PERSISTENT_POPUP,
|
||||
ViewName: "credentials",
|
||||
Key: CREDENTIALS_CONTEXT_KEY,
|
||||
},
|
||||
},
|
||||
Confirmation: SimpleContextNode{
|
||||
Context: BasicContext{
|
||||
OnFocus: func() error { return nil },
|
||||
Kind: TEMPORARY_POPUP,
|
||||
ViewName: "confirmation",
|
||||
Key: CONFIRMATION_CONTEXT_KEY,
|
||||
},
|
||||
},
|
||||
CommitMessage: SimpleContextNode{
|
||||
Context: BasicContext{
|
||||
OnFocus: func() error { return gui.handleCommitMessageFocused() },
|
||||
Kind: PERSISTENT_POPUP,
|
||||
ViewName: "commitMessage",
|
||||
Key: COMMIT_MESSAGE_CONTEXT_KEY,
|
||||
},
|
||||
},
|
||||
Search: SimpleContextNode{
|
||||
Context: BasicContext{
|
||||
OnFocus: func() error { return nil },
|
||||
Kind: PERSISTENT_POPUP,
|
||||
ViewName: "search",
|
||||
Key: SEARCH_CONTEXT_KEY,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) initialViewContextMap() map[string]Context {
|
||||
return map[string]Context{
|
||||
"status": gui.Contexts.Status.Context,
|
||||
"files": gui.Contexts.Files.Context,
|
||||
"branches": gui.Contexts.Branches.Context,
|
||||
"commits": gui.Contexts.BranchCommits.Context,
|
||||
"commitFiles": gui.Contexts.CommitFiles.Context,
|
||||
"stash": gui.Contexts.Stash.Context,
|
||||
"menu": gui.Contexts.Menu.Context,
|
||||
"confirmation": gui.Contexts.Confirmation.Context,
|
||||
"credentials": gui.Contexts.Credentials.Context,
|
||||
"commitMessage": gui.Contexts.CommitMessage.Context,
|
||||
"main": gui.Contexts.Normal.Context,
|
||||
"secondary": gui.Contexts.Normal.Context,
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) viewTabContextMap() map[string][]tabContext {
|
||||
return map[string][]tabContext{
|
||||
"branches": {
|
||||
{
|
||||
tab: "Local Branches",
|
||||
contexts: []Context{gui.Contexts.Branches.Context},
|
||||
},
|
||||
{
|
||||
tab: "Remotes",
|
||||
contexts: []Context{
|
||||
gui.Contexts.Remotes.Context,
|
||||
gui.Contexts.Remotes.Branches.Context,
|
||||
},
|
||||
},
|
||||
{
|
||||
tab: "Tags",
|
||||
contexts: []Context{gui.Contexts.Tags.Context},
|
||||
},
|
||||
},
|
||||
"commits": {
|
||||
{
|
||||
tab: "Commits",
|
||||
contexts: []Context{gui.Contexts.BranchCommits.Context},
|
||||
},
|
||||
{
|
||||
tab: "Reflog",
|
||||
contexts: []Context{
|
||||
gui.Contexts.ReflogCommits.Context,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) currentContextKeyIgnoringPopups() string {
|
||||
stack := gui.State.ContextStack
|
||||
|
||||
for i := range stack {
|
||||
reversedIndex := len(stack) - 1 - i
|
||||
context := stack[reversedIndex]
|
||||
kind := stack[reversedIndex].GetKind()
|
||||
if kind != TEMPORARY_POPUP && kind != PERSISTENT_POPUP {
|
||||
return context.GetKey()
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (gui *Gui) switchContext(c Context) error {
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
// push onto stack
|
||||
// if we are switching to a side context, remove all other contexts in the stack
|
||||
if c.GetKind() == SIDE_CONTEXT {
|
||||
for _, stackContext := range gui.State.ContextStack {
|
||||
if stackContext.GetKey() != c.GetKey() {
|
||||
if err := gui.deactivateContext(stackContext); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
gui.State.ContextStack = []Context{c}
|
||||
} else {
|
||||
// TODO: think about other exceptional cases
|
||||
gui.State.ContextStack = append(gui.State.ContextStack, c)
|
||||
}
|
||||
|
||||
return gui.activateContext(c)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// switchContextToView is to be used when you don't know which context you
|
||||
// want to switch to: you only know the view that you want to switch to. It will
|
||||
// look up the context currently active for that view and switch to that context
|
||||
func (gui *Gui) switchContextToView(viewName string) error {
|
||||
return gui.switchContext(gui.State.ViewContextMap[viewName])
|
||||
}
|
||||
|
||||
func (gui *Gui) returnFromContext() error {
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
// TODO: add mutexes
|
||||
|
||||
if len(gui.State.ContextStack) == 1 {
|
||||
// cannot escape from bottommost context
|
||||
return nil
|
||||
}
|
||||
|
||||
n := len(gui.State.ContextStack) - 1
|
||||
|
||||
currentContext := gui.State.ContextStack[n]
|
||||
newContext := gui.State.ContextStack[n-1]
|
||||
|
||||
gui.State.ContextStack = gui.State.ContextStack[:n]
|
||||
|
||||
if err := gui.deactivateContext(currentContext); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.activateContext(newContext)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) deactivateContext(c Context) error {
|
||||
// if we are the kind of context that is sent to back upon deactivation, we should do that
|
||||
if c.GetKind() == TEMPORARY_POPUP || c.GetKind() == PERSISTENT_POPUP {
|
||||
_, _ = gui.g.SetViewOnBottom(c.GetViewName())
|
||||
}
|
||||
|
||||
if err := c.HandleFocusLost(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// postRefreshUpdate is to be called on a context after the state that it depends on has been refreshed
|
||||
// if the context's view is set to another context we do nothing.
|
||||
// if the context's view is the current view we trigger a focus; re-selecting the current item.
|
||||
func (gui *Gui) postRefreshUpdate(c Context) error {
|
||||
v, err := gui.g.View(c.GetViewName())
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if v.Context != c.GetKey() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := c.HandleRender(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if gui.currentViewName() == c.GetViewName() {
|
||||
if err := c.HandleFocus(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) activateContext(c Context) error {
|
||||
viewName := c.GetViewName()
|
||||
v, err := gui.g.View(viewName)
|
||||
// if view no longer exists, pop again
|
||||
if err != nil {
|
||||
return gui.returnFromContext()
|
||||
}
|
||||
originalViewContextKey := v.Context
|
||||
|
||||
// ensure that any other window for which this view was active is now set to the default for that window.
|
||||
gui.setViewAsActiveForWindow(viewName)
|
||||
|
||||
if viewName == "main" {
|
||||
gui.changeMainViewsContext(c.GetKey())
|
||||
} else {
|
||||
gui.changeMainViewsContext("normal")
|
||||
}
|
||||
|
||||
gui.setViewTabForContext(c)
|
||||
|
||||
if _, err := gui.g.SetCurrentView(viewName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := gui.g.SetViewOnTop(viewName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if the new context's view was previously displaying another context, render the new context
|
||||
if originalViewContextKey != c.GetKey() {
|
||||
if err := c.HandleRender(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
v.Context = c.GetKey()
|
||||
|
||||
gui.g.Cursor = v.Editable
|
||||
|
||||
// render the options available for the current context at the bottom of the screen
|
||||
optionsMap := c.GetOptionsMap()
|
||||
if optionsMap == nil {
|
||||
optionsMap = gui.globalOptionsMap()
|
||||
}
|
||||
gui.renderOptionsMap(optionsMap)
|
||||
|
||||
if err := c.HandleFocus(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: consider removing this and instead depending on the .Context field of views
|
||||
gui.State.ViewContextMap[c.GetViewName()] = c
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) renderContextStack() string {
|
||||
result := ""
|
||||
for _, context := range gui.State.ContextStack {
|
||||
result += context.GetKey() + "\n"
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (gui *Gui) currentContext() Context {
|
||||
if len(gui.State.ContextStack) == 0 {
|
||||
return gui.defaultSideContext()
|
||||
}
|
||||
|
||||
return gui.State.ContextStack[len(gui.State.ContextStack)-1]
|
||||
}
|
||||
|
||||
func (gui *Gui) currentSideContext() *ListContext {
|
||||
stack := gui.State.ContextStack
|
||||
|
||||
// on startup the stack can be empty so we'll return an empty string in that case
|
||||
if len(stack) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// find the first context in the stack with the type of SIDE_CONTEXT
|
||||
for i := range stack {
|
||||
context := stack[len(stack)-1-i]
|
||||
|
||||
if context.GetKind() == SIDE_CONTEXT {
|
||||
// not all side contexts are list contexts (e.g. the status panel)
|
||||
listContext, ok := context.(*ListContext)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return listContext
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) defaultSideContext() Context {
|
||||
return gui.Contexts.Files.Context
|
||||
}
|
||||
|
||||
func (gui *Gui) setInitialViewContexts() {
|
||||
// arguably we should only have our ViewContextMap and we should do away with
|
||||
// contexts on views, or vice versa
|
||||
for viewName, context := range gui.State.ViewContextMap {
|
||||
// see if the view exists. If it does, set the context on it
|
||||
view, err := gui.g.View(viewName)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
view.Context = context.GetKey()
|
||||
}
|
||||
}
|
||||
|
||||
// getFocusLayout returns a manager function for when view gain and lose focus
|
||||
func (gui *Gui) getFocusLayout() func(g *gocui.Gui) error {
|
||||
var previousView *gocui.View
|
||||
return func(g *gocui.Gui) error {
|
||||
newView := gui.g.CurrentView()
|
||||
if err := gui.onViewFocusChange(); err != nil {
|
||||
return err
|
||||
}
|
||||
// for now we don't consider losing focus to a popup panel as actually losing focus
|
||||
if newView != previousView && !gui.isPopupPanel(newView.Name()) {
|
||||
if err := gui.onViewFocusLost(previousView, newView); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
previousView = newView
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) onViewFocusChange() error {
|
||||
currentView := gui.g.CurrentView()
|
||||
for _, view := range gui.g.Views() {
|
||||
view.Highlight = view.Name() != "main" && view == currentView
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) onViewFocusLost(v *gocui.View, newView *gocui.View) error {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if v.IsSearching() && newView.Name() != "search" {
|
||||
if err := gui.onSearchEscape(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
gui.Log.Info(v.Name() + " focus lost")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
func (gui *Gui) changeMainViewsContext(contextKey string) {
|
||||
if gui.State.MainContext == contextKey {
|
||||
return
|
||||
}
|
||||
|
||||
switch context {
|
||||
case "normal", "patch-building", "staging", "merging":
|
||||
gui.getMainView().Context = context
|
||||
gui.getSecondaryView().Context = context
|
||||
switch contextKey {
|
||||
case MAIN_NORMAL_CONTEXT_KEY, MAIN_PATCH_BUILDING_CONTEXT_KEY, MAIN_STAGING_CONTEXT_KEY, MAIN_MERGING_CONTEXT_KEY:
|
||||
gui.getMainView().Context = contextKey
|
||||
gui.getSecondaryView().Context = contextKey
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown context for main: %s", contextKey))
|
||||
}
|
||||
|
||||
gui.State.MainContext = context
|
||||
gui.State.MainContext = contextKey
|
||||
}
|
||||
|
||||
func (gui *Gui) viewTabNames(viewName string) []string {
|
||||
tabContexts := gui.ViewTabContextMap[viewName]
|
||||
|
||||
result := make([]string, len(tabContexts))
|
||||
for i, tabContext := range tabContexts {
|
||||
result[i] = tabContext.tab
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (gui *Gui) setViewTabForContext(c Context) {
|
||||
// search for the context in our map and if we find it, set the tab for the corresponding view
|
||||
tabContexts, ok := gui.ViewTabContextMap[c.GetViewName()]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
for tabIndex, tabContext := range tabContexts {
|
||||
for _, context := range tabContext.contexts {
|
||||
if context.GetKey() == c.GetKey() {
|
||||
// get the view, set the tab
|
||||
v, err := gui.g.View(c.GetViewName())
|
||||
if err != nil {
|
||||
gui.Log.Error(err)
|
||||
return
|
||||
}
|
||||
v.TabIndex = tabIndex
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type tabContext struct {
|
||||
tab string
|
||||
contexts []Context
|
||||
}
|
||||
|
||||
func (gui *Gui) contextForContextKey(contextKey string) Context {
|
||||
for _, context := range gui.allContexts() {
|
||||
if context.GetKey() == contextKey {
|
||||
return context
|
||||
}
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("context now found for key %s", contextKey))
|
||||
}
|
||||
|
||||
func (gui *Gui) rerenderView(viewName string) error {
|
||||
v, err := gui.g.View(viewName)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
contextKey := v.Context
|
||||
context := gui.contextForContextKey(contextKey)
|
||||
|
||||
return context.HandleRender()
|
||||
}
|
||||
|
||||
func (gui *Gui) getCurrentSideView() *gocui.View {
|
||||
currentSideContext := gui.currentSideContext()
|
||||
if currentSideContext == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
view, _ := gui.g.View(currentSideContext.GetViewName())
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
func (gui *Gui) getSideContextSelectedItemId() string {
|
||||
currentSideContext := gui.currentSideContext()
|
||||
if currentSideContext == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
item, ok := currentSideContext.GetSelectedItem()
|
||||
|
||||
if ok {
|
||||
return item.ID()
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
|
||||
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 {
|
||||
// promptUserForCredential wait for a username or password input from the credentials popup
|
||||
func (gui *Gui) promptUserForCredential(passOrUname string) string {
|
||||
gui.credentials = make(chan string)
|
||||
g.Update(func(g *gocui.Gui) error {
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
credentialsView, _ := g.View("credentials")
|
||||
if passOrUname == "username" {
|
||||
credentialsView.Title = gui.Tr.SLocalize("CredentialsUsername")
|
||||
@@ -20,10 +20,11 @@ func (gui *Gui) waitForPassUname(g *gocui.Gui, currentView *gocui.View, passOrUn
|
||||
credentialsView.Title = gui.Tr.SLocalize("CredentialsPassword")
|
||||
credentialsView.Mask = '*'
|
||||
}
|
||||
err := gui.switchFocus(g, currentView, credentialsView)
|
||||
if err != nil {
|
||||
|
||||
if err := gui.switchContext(gui.Contexts.Credentials.Context); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.RenderCommitLength()
|
||||
return nil
|
||||
})
|
||||
@@ -36,70 +37,41 @@ func (gui *Gui) waitForPassUname(g *gocui.Gui, currentView *gocui.View, passOrUn
|
||||
func (gui *Gui) handleSubmitCredential(g *gocui.Gui, v *gocui.View) error {
|
||||
message := gui.trimmedContent(v)
|
||||
gui.credentials <- message
|
||||
err := gui.refreshFiles()
|
||||
if err != nil {
|
||||
gui.clearEditorView(v)
|
||||
if err := gui.returnFromContext(); 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)
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{})
|
||||
}
|
||||
|
||||
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())
|
||||
return gui.returnFromContext()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCredentialsViewFocused(g *gocui.Gui, v *gocui.View) error {
|
||||
if _, err := g.SetViewOnTop("credentials"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCredentialsViewFocused() error {
|
||||
message := gui.Tr.TemplateLocalize(
|
||||
"CloseConfirm",
|
||||
Teml{
|
||||
"keyBindClose": "esc",
|
||||
"keyBindConfirm": "enter",
|
||||
"keyBindClose": gui.getKeyDisplay("universal.return"),
|
||||
"keyBindConfirm": gui.getKeyDisplay("universal.confirm"),
|
||||
},
|
||||
)
|
||||
gui.renderString(g, "options", message)
|
||||
gui.renderString("options", message)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
// handleCredentialsPopup handles the views after executing a command that might ask for credentials
|
||||
func (gui *Gui) handleCredentialsPopup(cmdErr error) {
|
||||
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)
|
||||
gui.createErrorPanel(errMessage)
|
||||
} else {
|
||||
_ = gui.closeConfirmationPrompt(g, true)
|
||||
_ = gui.refreshSidePanels(g)
|
||||
_ = gui.closeConfirmationPrompt(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
func (gui *Gui) exitDiffMode() error {
|
||||
gui.State.Modes.Diffing = Diffing{}
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}
|
||||
|
||||
func (gui *Gui) renderDiff() error {
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
fmt.Sprintf("git diff --color %s", gui.diffStr()),
|
||||
)
|
||||
task := gui.createRunPtyTask(cmd)
|
||||
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
main: &viewUpdateOpts{
|
||||
title: "Diff",
|
||||
task: task,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// currentDiffTerminals returns the current diff terminals of the currently selected item.
|
||||
// in the case of a branch it returns both the branch and it's upstream name,
|
||||
// which becomes an option when you bring up the diff menu, but when you're just
|
||||
// flicking through branches it will be using the local branch name.
|
||||
func (gui *Gui) currentDiffTerminals() []string {
|
||||
switch gui.currentContext().GetKey() {
|
||||
case "":
|
||||
return nil
|
||||
case FILES_CONTEXT_KEY:
|
||||
return []string{""}
|
||||
case COMMIT_FILES_CONTEXT_KEY:
|
||||
return []string{gui.State.Panels.CommitFiles.refName}
|
||||
case LOCAL_BRANCHES_CONTEXT_KEY:
|
||||
// for our local branches we want to include both the branch and its upstream
|
||||
branch := gui.getSelectedBranch()
|
||||
if branch != nil {
|
||||
names := []string{branch.ID()}
|
||||
if branch.UpstreamName != "" {
|
||||
names = append(names, branch.UpstreamName)
|
||||
}
|
||||
return names
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
context := gui.currentSideContext()
|
||||
if context == nil {
|
||||
return nil
|
||||
}
|
||||
item, ok := context.GetSelectedItem()
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return []string{item.ID()}
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) currentDiffTerminal() string {
|
||||
names := gui.currentDiffTerminals()
|
||||
if len(names) == 0 {
|
||||
return ""
|
||||
}
|
||||
return names[0]
|
||||
}
|
||||
|
||||
func (gui *Gui) currentlySelectedFilename() string {
|
||||
switch gui.currentContext().GetKey() {
|
||||
case FILES_CONTEXT_KEY, COMMIT_FILES_CONTEXT_KEY:
|
||||
return gui.getSideContextSelectedItemId()
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) diffStr() string {
|
||||
output := gui.State.Modes.Diffing.Ref
|
||||
|
||||
right := gui.currentDiffTerminal()
|
||||
if right != "" {
|
||||
output += " " + right
|
||||
}
|
||||
|
||||
if gui.State.Modes.Diffing.Reverse {
|
||||
output += " -R"
|
||||
}
|
||||
|
||||
file := gui.currentlySelectedFilename()
|
||||
if file != "" {
|
||||
output += " -- " + file
|
||||
} else if gui.State.Modes.Filtering.Active() {
|
||||
output += " -- " + gui.State.Modes.Filtering.Path
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateDiffingMenuPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
names := gui.currentDiffTerminals()
|
||||
|
||||
menuItems := []*menuItem{}
|
||||
for _, name := range names {
|
||||
name := name
|
||||
menuItems = append(menuItems, []*menuItem{
|
||||
{
|
||||
displayString: fmt.Sprintf("%s %s", gui.Tr.SLocalize("diff"), name),
|
||||
onPress: func() error {
|
||||
gui.State.Modes.Diffing.Ref = name
|
||||
// can scope this down based on current view but too lazy right now
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
},
|
||||
},
|
||||
}...)
|
||||
}
|
||||
|
||||
menuItems = append(menuItems, []*menuItem{
|
||||
{
|
||||
displayString: gui.Tr.SLocalize("enterRefToDiff"),
|
||||
onPress: func() error {
|
||||
return gui.prompt(gui.Tr.SLocalize("enteRefName"), "", func(response string) error {
|
||||
gui.State.Modes.Diffing.Ref = strings.TrimSpace(response)
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
})
|
||||
},
|
||||
},
|
||||
}...)
|
||||
|
||||
if gui.State.Modes.Diffing.Active() {
|
||||
menuItems = append(menuItems, []*menuItem{
|
||||
{
|
||||
displayString: gui.Tr.SLocalize("swapDiff"),
|
||||
onPress: func() error {
|
||||
gui.State.Modes.Diffing.Reverse = !gui.State.Modes.Diffing.Reverse
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: gui.Tr.SLocalize("exitDiffMode"),
|
||||
onPress: func() error {
|
||||
gui.State.Modes.Diffing = Diffing{}
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
},
|
||||
},
|
||||
}...)
|
||||
}
|
||||
|
||||
return gui.createMenu(gui.Tr.SLocalize("DiffingMenuTitle"), menuItems, createMenuOptions{showCancel: true})
|
||||
}
|
||||
@@ -5,11 +5,8 @@ import (
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -18,9 +15,9 @@ func (gui *Gui) handleCreateDiscardMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
displayString: gui.Tr.SLocalize("discardAllChanges"),
|
||||
onPress: func() error {
|
||||
if err := gui.GitCommand.DiscardAllFileChanges(file); err != nil {
|
||||
return err
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshFiles()
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -30,10 +27,10 @@ func (gui *Gui) handleCreateDiscardMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
displayString: gui.Tr.SLocalize("discardUnstagedChanges"),
|
||||
onPress: func() error {
|
||||
if err := gui.GitCommand.DiscardUnstagedFileChanges(file); err != nil {
|
||||
return err
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.refreshFiles()
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -132,12 +132,7 @@ func (gui *Gui) watchFilesForChanges() {
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
|
||||
}
|
||||
|
||||
// watch for errors
|
||||
|
||||
+282
-208
@@ -8,44 +8,40 @@ import (
|
||||
// "strings"
|
||||
|
||||
"fmt"
|
||||
"regexp"
|
||||
"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) getSelectedFile(g *gocui.Gui) (*commands.File, error) {
|
||||
selectedLine := gui.State.Panels.Files.SelectedLine
|
||||
func (gui *Gui) getSelectedFile() *commands.File {
|
||||
selectedLine := gui.State.Panels.Files.SelectedLineIdx
|
||||
if selectedLine == -1 {
|
||||
return &commands.File{}, gui.Errors.ErrNoFiles
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.State.Files[selectedLine], nil
|
||||
return gui.State.Files[selectedLine]
|
||||
}
|
||||
|
||||
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"))
|
||||
}
|
||||
gui.getFilesView().FocusPoint(0, gui.State.Panels.Files.SelectedLineIdx)
|
||||
|
||||
gui.getFilesView().FocusPoint(0, gui.State.Panels.Files.SelectedLine)
|
||||
|
||||
if file.HasInlineMergeConflicts {
|
||||
gui.getMainView().Title = gui.Tr.SLocalize("MergeConflictsTitle")
|
||||
gui.State.SplitMainPanel = false
|
||||
return gui.refreshMergePanel()
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
main: &viewUpdateOpts{
|
||||
title: "",
|
||||
task: gui.createRenderStringTask(gui.Tr.SLocalize("NoChangedFiles")),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if !alreadySelected {
|
||||
// TODO: pull into update task interface
|
||||
if err := gui.resetOrigin(gui.getMainView()); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -54,31 +50,31 @@ func (gui *Gui) selectFile(alreadySelected bool) error {
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
if file.HasInlineMergeConflicts {
|
||||
return gui.refreshMergePanel()
|
||||
}
|
||||
|
||||
cmdStr := gui.GitCommand.DiffCmdStr(file, false, !file.HasUnstagedChanges && file.HasStagedChanges)
|
||||
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(file, false, !file.HasUnstagedChanges && file.HasStagedChanges)
|
||||
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
|
||||
if err := gui.newPtyTask("main", cmd); err != nil {
|
||||
return err
|
||||
|
||||
refreshOpts := refreshMainOpts{main: &viewUpdateOpts{
|
||||
title: gui.Tr.SLocalize("UnstagedChanges"),
|
||||
task: gui.createRunPtyTask(cmd),
|
||||
}}
|
||||
|
||||
if file.HasStagedChanges && file.HasUnstagedChanges {
|
||||
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(file, false, true)
|
||||
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
|
||||
|
||||
refreshOpts.secondary = &viewUpdateOpts{
|
||||
title: gui.Tr.SLocalize("StagedChanges"),
|
||||
task: gui.createRunPtyTask(cmd),
|
||||
}
|
||||
} else if !file.HasUnstagedChanges {
|
||||
refreshOpts.main.title = gui.Tr.SLocalize("StagedChanges")
|
||||
}
|
||||
|
||||
return nil
|
||||
return gui.refreshMainViews(refreshOpts)
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshFiles() error {
|
||||
@@ -89,7 +85,7 @@ func (gui *Gui) refreshFiles() error {
|
||||
gui.State.RefreshingFilesMutex.Unlock()
|
||||
}()
|
||||
|
||||
selectedFile, _ := gui.getSelectedFile(gui.g)
|
||||
selectedFile := gui.getSelectedFile()
|
||||
|
||||
filesView := gui.getFilesView()
|
||||
if filesView == nil {
|
||||
@@ -101,12 +97,13 @@ func (gui *Gui) refreshFiles() error {
|
||||
}
|
||||
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
displayStrings := presentation.GetFileListDisplayStrings(gui.State.Files)
|
||||
gui.renderDisplayStrings(filesView, displayStrings)
|
||||
if err := gui.Contexts.Files.Context.HandleRender(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if g.CurrentView() == filesView || (g.CurrentView() == gui.getMainView() && g.CurrentView().Context == "merging") {
|
||||
newSelectedFile, _ := gui.getSelectedFile(gui.g)
|
||||
alreadySelected := newSelectedFile.Name == selectedFile.Name
|
||||
if g.CurrentView() == filesView || (g.CurrentView() == gui.getMainView() && g.CurrentView().Context == MAIN_MERGING_CONTEXT_KEY) {
|
||||
newSelectedFile := gui.getSelectedFile()
|
||||
alreadySelected := selectedFile != nil && newSelectedFile != nil && newSelectedFile.Name == selectedFile.Name
|
||||
return gui.selectFile(alreadySelected)
|
||||
}
|
||||
return nil
|
||||
@@ -130,7 +127,7 @@ func (gui *Gui) stagedFiles() []*commands.File {
|
||||
|
||||
func (gui *Gui) trackedFiles() []*commands.File {
|
||||
files := gui.State.Files
|
||||
result := make([]*commands.File, 0)
|
||||
result := make([]*commands.File, 0, len(files))
|
||||
for _, file := range files {
|
||||
if file.Tracked {
|
||||
result = append(result, file)
|
||||
@@ -140,10 +137,11 @@ func (gui *Gui) trackedFiles() []*commands.File {
|
||||
}
|
||||
|
||||
func (gui *Gui) stageSelectedFile(g *gocui.Gui) error {
|
||||
file, err := gui.getSelectedFile(g)
|
||||
if err != nil {
|
||||
return err
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.GitCommand.StageFile(file.Name)
|
||||
}
|
||||
|
||||
@@ -152,49 +150,43 @@ func (gui *Gui) handleEnterFile(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if file.HasInlineMergeConflicts {
|
||||
return gui.handleSwitchToMerge(gui.g, gui.getFilesView())
|
||||
return gui.handleSwitchToMerge()
|
||||
}
|
||||
if file.HasMergeConflicts {
|
||||
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("FileStagingRequirements"))
|
||||
return gui.createErrorPanel(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)
|
||||
gui.switchContext(gui.Contexts.Staging.Context)
|
||||
|
||||
return gui.refreshStagingPanel(forceSecondaryFocused, selectedLineIdx) // TODO: check if this is broken, try moving into context code
|
||||
}
|
||||
|
||||
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
|
||||
func (gui *Gui) handleFilePress() error {
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if file.HasInlineMergeConflicts {
|
||||
return gui.handleSwitchToMerge(g, v)
|
||||
return gui.handleSwitchToMerge()
|
||||
}
|
||||
|
||||
if file.HasUnstagedChanges {
|
||||
err = gui.GitCommand.StageFile(file.Name)
|
||||
if err := gui.GitCommand.StageFile(file.Name); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
} else {
|
||||
err = gui.GitCommand.UnStageFile(file.Name, file.Tracked)
|
||||
}
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
if err := gui.GitCommand.UnStageFile(file.Name, file.Tracked); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := gui.refreshFiles(); err != nil {
|
||||
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -210,11 +202,7 @@ func (gui *Gui) allFilesStaged() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (gui *Gui) focusAndSelectFile(g *gocui.Gui, v *gocui.View) error {
|
||||
if _, err := gui.g.SetCurrentView("files"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
func (gui *Gui) focusAndSelectFile() error {
|
||||
return gui.selectFile(false)
|
||||
}
|
||||
|
||||
@@ -226,10 +214,10 @@ func (gui *Gui) handleStageAll(g *gocui.Gui, v *gocui.View) error {
|
||||
err = gui.GitCommand.StageAll()
|
||||
}
|
||||
if err != nil {
|
||||
_ = gui.createErrorPanel(g, err.Error())
|
||||
_ = gui.surfaceError(err)
|
||||
}
|
||||
|
||||
if err := gui.refreshFiles(); err != nil {
|
||||
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -237,57 +225,72 @@ func (gui *Gui) handleStageAll(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
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())
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
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 {
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.SLocalize("IgnoreTracked"),
|
||||
prompt: gui.Tr.SLocalize("IgnoreTrackedPrompt"),
|
||||
handleConfirm: func() 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)
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{FILES}})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if err := gui.GitCommand.Ignore(file.Name); err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.refreshFiles()
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{FILES}})
|
||||
}
|
||||
|
||||
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"))
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("SkipHookPrefixNotConfigured"))
|
||||
}
|
||||
|
||||
gui.renderString(g, "commitMessage", skipHookPreifx)
|
||||
gui.renderStringSync("commitMessage", skipHookPreifx)
|
||||
if err := gui.getCommitMessageView().SetCursor(len(skipHookPreifx), 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.handleCommitPress(g, filesView)
|
||||
return gui.handleCommitPress()
|
||||
}
|
||||
|
||||
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"))
|
||||
func (gui *Gui) handleCommitPress() error {
|
||||
if len(gui.stagedFiles()) == 0 {
|
||||
return gui.promptToStageAllAndRetry(func() error {
|
||||
return gui.handleCommitPress()
|
||||
})
|
||||
}
|
||||
|
||||
commitMessageView := gui.getCommitMessageView()
|
||||
g.Update(func(g *gocui.Gui) error {
|
||||
if _, err := g.SetViewOnTop("commitMessage"); err != nil {
|
||||
prefixPattern := gui.Config.GetUserConfig().GetString("git.commitPrefixes." + utils.GetCurrentRepoName() + ".pattern")
|
||||
prefixReplace := gui.Config.GetUserConfig().GetString("git.commitPrefixes." + utils.GetCurrentRepoName() + ".replace")
|
||||
if len(prefixPattern) > 0 && len(prefixReplace) > 0 {
|
||||
rgx, err := regexp.Compile(prefixPattern)
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(fmt.Sprintf("%s: %s", gui.Tr.SLocalize("commitPrefixPatternError"), err.Error()))
|
||||
}
|
||||
prefix := rgx.ReplaceAllString(gui.getCheckedOutBranch().Name, prefixReplace)
|
||||
gui.renderString("commitMessage", prefix)
|
||||
if err := commitMessageView.SetCursor(len(prefix), 0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := gui.switchFocus(g, filesView, commitMessageView); err != nil {
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
if err := gui.switchContext(gui.Contexts.CommitMessage.Context); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -297,44 +300,68 @@ func (gui *Gui) handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
|
||||
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"))
|
||||
func (gui *Gui) promptToStageAllAndRetry(retry func() error) error {
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.SLocalize("NoFilesStagedTitle"),
|
||||
prompt: gui.Tr.SLocalize("NoFilesStagedPrompt"),
|
||||
handleConfirm: func() error {
|
||||
if err := gui.GitCommand.StageAll(); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
if err := gui.refreshFiles(); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return retry()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleAmendCommitPress() error {
|
||||
if len(gui.stagedFiles()) == 0 {
|
||||
return gui.promptToStageAllAndRetry(func() error {
|
||||
return gui.handleAmendCommitPress()
|
||||
})
|
||||
}
|
||||
|
||||
if len(gui.State.Commits) == 0 {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoCommitToAmend"))
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("NoCommitToAmend"))
|
||||
}
|
||||
|
||||
title := strings.Title(gui.Tr.SLocalize("AmendLastCommit"))
|
||||
question := gui.Tr.SLocalize("SureToAmend")
|
||||
return gui.ask(askOpts{
|
||||
title: strings.Title(gui.Tr.SLocalize("AmendLastCommit")),
|
||||
prompt: gui.Tr.SLocalize("SureToAmend"),
|
||||
handleConfirm: func() error {
|
||||
ok, err := gui.runSyncOrAsyncCommand(gui.GitCommand.AmendHead())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
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)
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 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"))
|
||||
func (gui *Gui) handleCommitEditorPress() error {
|
||||
if len(gui.stagedFiles()) == 0 {
|
||||
return gui.promptToStageAllAndRetry(func() error {
|
||||
return gui.handleCommitEditorPress()
|
||||
})
|
||||
}
|
||||
gui.PrepareSubProcess(g, "git", "commit")
|
||||
|
||||
gui.PrepareSubProcess("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) {
|
||||
func (gui *Gui) PrepareSubProcess(commands ...string) {
|
||||
gui.SubProcess = gui.GitCommand.PrepareCommitSubProcess()
|
||||
g.Update(func(g *gocui.Gui) error {
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
return gui.Errors.ErrSubProcess
|
||||
})
|
||||
}
|
||||
@@ -345,123 +372,172 @@ func (gui *Gui) editFile(filename string) error {
|
||||
}
|
||||
|
||||
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())
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
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())
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
return gui.openFile(file.Name)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRefreshFiles(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.refreshFiles()
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{FILES}})
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshStateFiles() error {
|
||||
// keep track of where the cursor is currently and the current file names
|
||||
// when we refresh, go looking for a matching name
|
||||
// move the cursor to there.
|
||||
selectedFile := gui.getSelectedFile()
|
||||
prevSelectedLineIdx := gui.State.Panels.Files.SelectedLineIdx
|
||||
|
||||
// get files to stage
|
||||
files := gui.GitCommand.GetStatusFiles()
|
||||
gui.State.Files = gui.GitCommand.MergeStatusFiles(gui.State.Files, files)
|
||||
files := gui.GitCommand.GetStatusFiles(commands.GetStatusFileOptions{})
|
||||
gui.State.Files = gui.GitCommand.MergeStatusFiles(gui.State.Files, files, selectedFile)
|
||||
|
||||
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
|
||||
// let's try to find our file again and move the cursor to that
|
||||
if selectedFile != nil {
|
||||
for idx, f := range gui.State.Files {
|
||||
selectedFileHasMoved := f.Matches(selectedFile) && idx != prevSelectedLineIdx
|
||||
if selectedFileHasMoved {
|
||||
gui.State.Panels.Files.SelectedLineIdx = idx
|
||||
break
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
gui.refreshSelectedLine(gui.State.Panels.Files, len(gui.State.Files))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handlePullFiles(g *gocui.Gui, v *gocui.View) error {
|
||||
// if we have no upstream branch we need to set that first
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
currentBranch := gui.currentBranch()
|
||||
if currentBranch == nil {
|
||||
// need to wait for branches to refresh
|
||||
return nil
|
||||
}
|
||||
|
||||
// if we have no upstream branch we need to set that first
|
||||
if currentBranch.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())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
for branchName, branch := range conf.Branches {
|
||||
if branchName == currentBranch.Name {
|
||||
return gui.pullFiles(v, fmt.Sprintf("%s %s", branch.Remote, branchName))
|
||||
return gui.pullFiles(PullFilesOptions{RemoteName: branch.Remote, BranchName: branch.Name})
|
||||
}
|
||||
}
|
||||
|
||||
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("EnterUpstream"), "origin/"+currentBranch.Name, func(g *gocui.Gui, v *gocui.View) error {
|
||||
upstream := gui.trimmedContent(v)
|
||||
return gui.prompt(gui.Tr.SLocalize("EnterUpstream"), "origin/"+currentBranch.Name, func(upstream string) error {
|
||||
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.createErrorPanel(errorMessage)
|
||||
}
|
||||
return gui.pullFiles(v, "")
|
||||
return gui.pullFiles(PullFilesOptions{})
|
||||
})
|
||||
}
|
||||
|
||||
return gui.pullFiles(v, "")
|
||||
return gui.pullFiles(PullFilesOptions{})
|
||||
}
|
||||
|
||||
func (gui *Gui) pullFiles(v *gocui.View, args string) error {
|
||||
if err := gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("PullWait")); err != nil {
|
||||
type PullFilesOptions struct {
|
||||
RemoteName string
|
||||
BranchName string
|
||||
}
|
||||
|
||||
func (gui *Gui) pullFiles(opts PullFilesOptions) error {
|
||||
if err := gui.createLoaderPanel(gui.g.CurrentView(), 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)
|
||||
}()
|
||||
mode := gui.Config.GetUserConfig().GetString("git.pull.mode")
|
||||
|
||||
go gui.pullWithMode(mode, opts)
|
||||
|
||||
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 {
|
||||
func (gui *Gui) pullWithMode(mode string, opts PullFilesOptions) error {
|
||||
gui.State.FetchMutex.Lock()
|
||||
defer gui.State.FetchMutex.Unlock()
|
||||
|
||||
err := gui.GitCommand.Fetch(
|
||||
commands.FetchOptions{
|
||||
PromptUserForCredential: gui.promptUserForCredential,
|
||||
RemoteName: opts.RemoteName,
|
||||
BranchName: opts.BranchName,
|
||||
},
|
||||
)
|
||||
gui.handleCredentialsPopup(err)
|
||||
if err != nil {
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case "rebase":
|
||||
err := gui.GitCommand.RebaseBranch("FETCH_HEAD")
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
case "merge":
|
||||
err := gui.GitCommand.Merge("FETCH_HEAD", commands.MergeOpts{})
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
case "ff-only":
|
||||
err := gui.GitCommand.Merge("FETCH_HEAD", commands.MergeOpts{FastForwardOnly: true})
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
default:
|
||||
return gui.createErrorPanel(fmt.Sprintf("git pull mode '%s' unrecognised", mode))
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) pushWithForceFlag(v *gocui.View, force bool, upstream string, args string) error {
|
||||
if err := gui.createLoaderPanel(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)
|
||||
err := gui.GitCommand.Push(branchName, force, upstream, args, gui.promptUserForCredential)
|
||||
if err != nil && !force && strings.Contains(err.Error(), "Updates were rejected") {
|
||||
gui.ask(askOpts{
|
||||
title: gui.Tr.SLocalize("ForcePush"),
|
||||
prompt: gui.Tr.SLocalize("ForcePushPrompt"),
|
||||
handleConfirm: func() error {
|
||||
return gui.pushWithForceFlag(v, true, upstream, args)
|
||||
},
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
gui.handleCredentialsPopup(err)
|
||||
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// if we have pullables we'll ask if the user wants to force push
|
||||
currentBranch := gui.currentBranch()
|
||||
|
||||
@@ -469,46 +545,50 @@ func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error {
|
||||
// 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())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
for branchName, branch := range conf.Branches {
|
||||
if branchName == currentBranch.Name {
|
||||
return gui.pushWithForceFlag(g, v, false, "", fmt.Sprintf("%s %s", branch.Remote, branchName))
|
||||
return gui.pushWithForceFlag(v, false, "", fmt.Sprintf("%s %s", branch.Remote, branchName))
|
||||
}
|
||||
}
|
||||
|
||||
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("EnterUpstream"), "origin "+currentBranch.Name, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.pushWithForceFlag(g, v, false, gui.trimmedContent(v), "")
|
||||
})
|
||||
if gui.GitCommand.PushToCurrent {
|
||||
return gui.pushWithForceFlag(v, false, "", "--set-upstream")
|
||||
} else {
|
||||
return gui.prompt(gui.Tr.SLocalize("EnterUpstream"), "origin "+currentBranch.Name, func(response string) error {
|
||||
return gui.pushWithForceFlag(v, false, response, "")
|
||||
})
|
||||
}
|
||||
} else if currentBranch.Pullables == "0" {
|
||||
return gui.pushWithForceFlag(g, v, false, "", "")
|
||||
return gui.pushWithForceFlag(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)
|
||||
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.SLocalize("ForcePush"),
|
||||
prompt: gui.Tr.SLocalize("ForcePushPrompt"),
|
||||
handleConfirm: func() error {
|
||||
return gui.pushWithForceFlag(v, true, "", "")
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
func (gui *Gui) handleSwitchToMerge() error {
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !file.HasInlineMergeConflicts {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("FileNoMergeCons"))
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("FileNoMergeCons"))
|
||||
}
|
||||
gui.changeMainViewsContext("merging")
|
||||
if err := gui.switchFocus(g, v, gui.getMainView()); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.refreshMergePanel()
|
||||
|
||||
return gui.switchContext(gui.Contexts.Merging.Context)
|
||||
}
|
||||
|
||||
func (gui *Gui) openFile(filename string) error {
|
||||
if err := gui.OSCommand.OpenFile(filename); err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -523,8 +603,7 @@ func (gui *Gui) anyFilesWithMergeConflicts() bool {
|
||||
}
|
||||
|
||||
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)
|
||||
return gui.prompt(gui.Tr.SLocalize("CustomCommand"), "", func(command string) error {
|
||||
gui.SubProcess = gui.OSCommand.RunCustomCommand(command)
|
||||
return gui.Errors.ErrSubProcess
|
||||
})
|
||||
@@ -556,8 +635,3 @@ func (gui *Gui) handleStashChanges(g *gocui.Gui, v *gocui.View) error {
|
||||
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())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package gui
|
||||
|
||||
func (gui *Gui) validateNotInFilterMode() (bool, error) {
|
||||
if gui.State.Modes.Filtering.Active() {
|
||||
err := gui.ask(askOpts{
|
||||
title: gui.Tr.SLocalize("MustExitFilterModeTitle"),
|
||||
prompt: gui.Tr.SLocalize("MustExitFilterModePrompt"),
|
||||
handleConfirm: func() error {
|
||||
return gui.exitFilterMode()
|
||||
},
|
||||
})
|
||||
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (gui *Gui) exitFilterMode() error {
|
||||
gui.State.Modes.Filtering.Path = ""
|
||||
return gui.Errors.ErrRestart
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
func (gui *Gui) handleCreateFilteringMenuPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
fileName := ""
|
||||
switch v.Name() {
|
||||
case "files":
|
||||
file := gui.getSelectedFile()
|
||||
if file != nil {
|
||||
fileName = file.Name
|
||||
}
|
||||
case "commitFiles":
|
||||
file := gui.getSelectedCommitFile()
|
||||
if file != nil {
|
||||
fileName = file.Name
|
||||
}
|
||||
}
|
||||
|
||||
menuItems := []*menuItem{}
|
||||
|
||||
if fileName != "" {
|
||||
menuItems = append(menuItems, &menuItem{
|
||||
displayString: fmt.Sprintf("%s '%s'", gui.Tr.SLocalize("filterBy"), fileName),
|
||||
onPress: func() error {
|
||||
gui.State.Modes.Filtering.Path = fileName
|
||||
return gui.Errors.ErrRestart
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
menuItems = append(menuItems, &menuItem{
|
||||
displayString: gui.Tr.SLocalize("filterPathOption"),
|
||||
onPress: func() error {
|
||||
return gui.prompt(gui.Tr.SLocalize("enterFileName"), "", func(response string) error {
|
||||
gui.State.Modes.Filtering.Path = strings.TrimSpace(response)
|
||||
return gui.Errors.ErrRestart
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
if gui.State.Modes.Filtering.Active() {
|
||||
menuItems = append(menuItems, &menuItem{
|
||||
displayString: gui.Tr.SLocalize("exitFilterMode"),
|
||||
onPress: func() error {
|
||||
gui.State.Modes.Filtering.Path = ""
|
||||
return gui.Errors.ErrRestart
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return gui.createMenu(gui.Tr.SLocalize("FilteringMenuTitle"), menuItems, createMenuOptions{showCancel: true})
|
||||
}
|
||||
+7
-4
@@ -28,7 +28,7 @@ func (gui *Gui) gitFlowFinishBranch(gitFlowConfig string, branchName string) err
|
||||
}
|
||||
|
||||
if branchType == "" {
|
||||
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NotAGitFlowBranch"))
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("NotAGitFlowBranch"))
|
||||
}
|
||||
|
||||
subProcess := gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "finish", suffix)
|
||||
@@ -45,14 +45,13 @@ func (gui *Gui) handleCreateGitFlowMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
// 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")
|
||||
return gui.createErrorPanel("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)
|
||||
return gui.prompt(title, "", func(name string) error {
|
||||
subProcess := gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "start", name)
|
||||
gui.SubProcess = subProcess
|
||||
return gui.Errors.ErrSubProcess
|
||||
@@ -76,6 +75,10 @@ func (gui *Gui) handleCreateGitFlowMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
displayString: "start hotfix",
|
||||
onPress: startHandler("hotfix"),
|
||||
},
|
||||
{
|
||||
displayString: "start bugfix",
|
||||
onPress: startHandler("bugfix"),
|
||||
},
|
||||
{
|
||||
displayString: "start release",
|
||||
onPress: startHandler("release"),
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// these views need to be re-rendered when the screen mode changes. The commits view,
|
||||
// for example, will show authorship information in half and full screen mode.
|
||||
func (gui *Gui) rerenderViewsWithScreenModeDependentContent() error {
|
||||
for _, viewName := range []string{"branches", "commits"} {
|
||||
if err := gui.rerenderView(viewName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) nextScreenMode(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.State.ScreenMode = utils.NextIntInCycle([]int{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode)
|
||||
|
||||
return gui.rerenderViewsWithScreenModeDependentContent()
|
||||
}
|
||||
|
||||
func (gui *Gui) prevScreenMode(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.State.ScreenMode = utils.PrevIntInCycle([]int{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode)
|
||||
|
||||
return gui.rerenderViewsWithScreenModeDependentContent()
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollUpView(viewName string) error {
|
||||
mainView, err := gui.g.View(viewName)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
ox, oy := mainView.Origin()
|
||||
newOy := int(math.Max(0, float64(oy-gui.Config.GetUserConfig().GetInt("gui.scrollHeight"))))
|
||||
return mainView.SetOrigin(ox, newOy)
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollDownView(viewName string) error {
|
||||
mainView, err := gui.g.View(viewName)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
ox, oy := mainView.Origin()
|
||||
y := oy
|
||||
if !gui.Config.GetUserConfig().GetBool("gui.scrollPastBottom") {
|
||||
_, sy := mainView.Size()
|
||||
y += sy
|
||||
}
|
||||
scrollHeight := gui.Config.GetUserConfig().GetInt("gui.scrollHeight")
|
||||
if y < mainView.LinesHeight() {
|
||||
if err := mainView.SetOrigin(ox, oy+scrollHeight); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if manager, ok := gui.viewBufferManagerMap[viewName]; ok {
|
||||
manager.ReadLines(scrollHeight)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollUpMain(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.canScrollMergePanel() {
|
||||
gui.State.Panels.Merging.UserScrolling = true
|
||||
}
|
||||
|
||||
return gui.scrollUpView("main")
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollDownMain(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.canScrollMergePanel() {
|
||||
gui.State.Panels.Merging.UserScrolling = true
|
||||
}
|
||||
|
||||
return gui.scrollDownView("main")
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollUpSecondary(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.scrollUpView("secondary")
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollDownSecondary(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.scrollDownView("secondary")
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollUpConfirmationPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
if v.Editable {
|
||||
return nil
|
||||
}
|
||||
return gui.scrollUpView("confirmation")
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollDownConfirmationPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
if v.Editable {
|
||||
return nil
|
||||
}
|
||||
return gui.scrollDownView("confirmation")
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRefresh(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMouseDownMain(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch g.CurrentView().Name() {
|
||||
case "files":
|
||||
// set filename, set primary/secondary selected, set line number, then switch context
|
||||
// I'll need to know it was changed though.
|
||||
// Could I pass something along to the context change?
|
||||
return gui.enterFile(false, v.SelectedLineIdx())
|
||||
case "commitFiles":
|
||||
return gui.enterCommitFile(v.SelectedLineIdx())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMouseDownSecondary(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch g.CurrentView().Name() {
|
||||
case "files":
|
||||
return gui.enterFile(true, v.SelectedLineIdx())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleInfoClick(g *gocui.Gui, v *gocui.View) error {
|
||||
if !gui.g.Mouse {
|
||||
return nil
|
||||
}
|
||||
|
||||
cx, _ := v.Cursor()
|
||||
width, _ := v.Size()
|
||||
|
||||
if width-cx > len(gui.Tr.SLocalize("(reset)")) {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, mode := range gui.modeStatuses() {
|
||||
if mode.isActive() {
|
||||
return mode.reset()
|
||||
}
|
||||
}
|
||||
|
||||
// if we're not in an active mode we show the donate button
|
||||
if cx <= len(gui.Tr.SLocalize("Donate"))+len(INFO_SECTION_PADDING) {
|
||||
return gui.OSCommand.OpenLink("https://github.com/sponsors/jesseduffield")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) fetch(canPromptForCredentials bool) (err error) {
|
||||
gui.State.FetchMutex.Lock()
|
||||
defer gui.State.FetchMutex.Unlock()
|
||||
|
||||
fetchOpts := commands.FetchOptions{}
|
||||
if canPromptForCredentials {
|
||||
fetchOpts.PromptUserForCredential = gui.promptUserForCredential
|
||||
}
|
||||
|
||||
err = gui.GitCommand.Fetch(fetchOpts)
|
||||
|
||||
if canPromptForCredentials && err != nil && strings.Contains(err.Error(), "exit status 128") {
|
||||
gui.createErrorPanel(gui.Tr.SLocalize("PassUnameWrong"))
|
||||
}
|
||||
|
||||
gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, COMMITS, REMOTES, TAGS}, mode: ASYNC})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCopySelectedSideContextItemToClipboard() error {
|
||||
// important to note that this assumes we've selected an item in a side context
|
||||
itemId := gui.getSideContextSelectedItemId()
|
||||
|
||||
if itemId == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.OSCommand.CopyToClipboard(itemId)
|
||||
}
|
||||
+295
-831
File diff suppressed because it is too large
Load Diff
+349
-333
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,362 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
)
|
||||
|
||||
const SEARCH_PREFIX = "search: "
|
||||
const INFO_SECTION_PADDING = " "
|
||||
|
||||
func (gui *Gui) informationStr() string {
|
||||
for _, mode := range gui.modeStatuses() {
|
||||
if mode.isActive() {
|
||||
return mode.description()
|
||||
}
|
||||
}
|
||||
|
||||
if gui.g.Mouse {
|
||||
donate := color.New(color.FgMagenta, color.Underline).Sprint(gui.Tr.SLocalize("Donate"))
|
||||
return donate + " " + gui.Config.GetVersion()
|
||||
} else {
|
||||
return gui.Config.GetVersion()
|
||||
}
|
||||
}
|
||||
|
||||
// layout is called for every screen re-render e.g. when the screen is resized
|
||||
func (gui *Gui) layout(g *gocui.Gui) error {
|
||||
g.Highlight = true
|
||||
width, height := g.Size()
|
||||
|
||||
minimumHeight := 9
|
||||
minimumWidth := 10
|
||||
if height < minimumHeight || width < minimumWidth {
|
||||
v, err := g.SetView("limit", 0, 0, width-1, height-1, 0)
|
||||
if err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
v.Title = gui.Tr.SLocalize("NotEnoughSpace")
|
||||
v.Wrap = true
|
||||
_, _ = g.SetViewOnTop("limit")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
informationStr := gui.informationStr()
|
||||
appStatus := gui.statusManager.getStatusString()
|
||||
|
||||
viewDimensions := gui.getWindowDimensions(informationStr, appStatus)
|
||||
|
||||
_, _ = g.SetViewOnBottom("limit")
|
||||
_ = g.DeleteView("limit")
|
||||
|
||||
textColor := theme.GocuiDefaultTextColor
|
||||
|
||||
// reading more lines into main view buffers upon resize
|
||||
prevMainView, err := gui.g.View("main")
|
||||
if err == nil {
|
||||
_, prevMainHeight := prevMainView.Size()
|
||||
newMainHeight := viewDimensions["main"].Y1 - viewDimensions["main"].Y0 - 1
|
||||
heightDiff := newMainHeight - prevMainHeight
|
||||
if heightDiff > 0 {
|
||||
if manager, ok := gui.viewBufferManagerMap["main"]; ok {
|
||||
manager.ReadLines(heightDiff)
|
||||
}
|
||||
if manager, ok := gui.viewBufferManagerMap["secondary"]; ok {
|
||||
manager.ReadLines(heightDiff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setViewFromDimensions := func(viewName string, windowName string, frame bool) (*gocui.View, error) {
|
||||
dimensionsObj, ok := viewDimensions[windowName]
|
||||
|
||||
if !ok {
|
||||
// view not specified in dimensions object: so create the view and hide it
|
||||
// making the view take up the whole space in the background in case it needs
|
||||
// to render content as soon as it appears, because lazyloaded content (via a pty task)
|
||||
// cares about the size of the view.
|
||||
view, err := g.SetView(viewName, 0, 0, width, height, 0)
|
||||
if err != nil {
|
||||
return view, err
|
||||
}
|
||||
return g.SetViewOnBottom(viewName)
|
||||
}
|
||||
|
||||
frameOffset := 1
|
||||
if frame {
|
||||
frameOffset = 0
|
||||
}
|
||||
return g.SetView(
|
||||
viewName,
|
||||
dimensionsObj.X0-frameOffset,
|
||||
dimensionsObj.Y0-frameOffset,
|
||||
dimensionsObj.X1+frameOffset,
|
||||
dimensionsObj.Y1+frameOffset,
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
v, err := setViewFromDimensions("main", "main", true)
|
||||
if err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
v.Title = gui.Tr.SLocalize("DiffTitle")
|
||||
v.Wrap = true
|
||||
v.FgColor = textColor
|
||||
v.IgnoreCarriageReturns = true
|
||||
}
|
||||
|
||||
secondaryView, err := setViewFromDimensions("secondary", "secondary", true)
|
||||
if err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
secondaryView.Title = gui.Tr.SLocalize("DiffTitle")
|
||||
secondaryView.Wrap = true
|
||||
secondaryView.FgColor = textColor
|
||||
secondaryView.IgnoreCarriageReturns = true
|
||||
}
|
||||
|
||||
hiddenViewOffset := 9999
|
||||
|
||||
if v, err := setViewFromDimensions("status", "status", true); err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
v.Title = gui.Tr.SLocalize("StatusTitle")
|
||||
v.FgColor = textColor
|
||||
}
|
||||
|
||||
filesView, err := setViewFromDimensions("files", "files", true)
|
||||
if err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
filesView.Highlight = true
|
||||
filesView.Title = gui.Tr.SLocalize("FilesTitle")
|
||||
filesView.ContainsList = true
|
||||
}
|
||||
|
||||
branchesView, err := setViewFromDimensions("branches", "branches", true)
|
||||
if err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
branchesView.Title = gui.Tr.SLocalize("BranchesTitle")
|
||||
branchesView.Tabs = gui.viewTabNames("branches")
|
||||
branchesView.FgColor = textColor
|
||||
branchesView.ContainsList = true
|
||||
}
|
||||
|
||||
commitFilesView, err := setViewFromDimensions("commitFiles", gui.Contexts.CommitFiles.Context.GetWindowName(), true)
|
||||
if err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
commitFilesView.Title = gui.Tr.SLocalize("CommitFiles")
|
||||
commitFilesView.FgColor = textColor
|
||||
commitFilesView.ContainsList = true
|
||||
}
|
||||
|
||||
commitsView, err := setViewFromDimensions("commits", "commits", true)
|
||||
if err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
commitsView.Title = gui.Tr.SLocalize("CommitsTitle")
|
||||
commitsView.Tabs = gui.viewTabNames("commits")
|
||||
commitsView.FgColor = textColor
|
||||
commitsView.ContainsList = true
|
||||
}
|
||||
|
||||
stashView, err := setViewFromDimensions("stash", "stash", true)
|
||||
if err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
stashView.Title = gui.Tr.SLocalize("StashTitle")
|
||||
stashView.FgColor = textColor
|
||||
stashView.ContainsList = true
|
||||
}
|
||||
|
||||
if gui.getCommitMessageView() == nil {
|
||||
// doesn't matter where this view starts because it will be hidden
|
||||
if commitMessageView, err := g.SetView("commitMessage", hiddenViewOffset, hiddenViewOffset, hiddenViewOffset+10, hiddenViewOffset+10, 0); err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
_, _ = g.SetViewOnBottom("commitMessage")
|
||||
commitMessageView.Title = gui.Tr.SLocalize("CommitMessage")
|
||||
commitMessageView.FgColor = textColor
|
||||
commitMessageView.Editable = true
|
||||
commitMessageView.Editor = gocui.EditorFunc(gui.commitMessageEditor)
|
||||
}
|
||||
}
|
||||
|
||||
if check, _ := g.View("credentials"); check == nil {
|
||||
// doesn't matter where this view starts because it will be hidden
|
||||
if credentialsView, err := g.SetView("credentials", hiddenViewOffset, hiddenViewOffset, hiddenViewOffset+10, hiddenViewOffset+10, 0); err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
_, _ = g.SetViewOnBottom("credentials")
|
||||
credentialsView.Title = gui.Tr.SLocalize("CredentialsUsername")
|
||||
credentialsView.FgColor = textColor
|
||||
credentialsView.Editable = true
|
||||
}
|
||||
}
|
||||
|
||||
if v, err := setViewFromDimensions("options", "options", false); err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
v.Frame = false
|
||||
v.FgColor = theme.OptionsColor
|
||||
|
||||
// doing this here because it'll only happen once
|
||||
if err := gui.onInitialViewsCreation(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// this view takes up one character. Its only purpose is to show the slash when searching
|
||||
if searchPrefixView, err := setViewFromDimensions("searchPrefix", "searchPrefix", false); err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
|
||||
searchPrefixView.BgColor = gocui.ColorDefault
|
||||
searchPrefixView.FgColor = gocui.ColorGreen
|
||||
searchPrefixView.Frame = false
|
||||
gui.setViewContent(searchPrefixView, SEARCH_PREFIX)
|
||||
}
|
||||
|
||||
if searchView, err := setViewFromDimensions("search", "search", false); err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
|
||||
searchView.BgColor = gocui.ColorDefault
|
||||
searchView.FgColor = gocui.ColorGreen
|
||||
searchView.Frame = false
|
||||
searchView.Editable = true
|
||||
}
|
||||
|
||||
if appStatusView, err := setViewFromDimensions("appStatus", "appStatus", false); err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
appStatusView.BgColor = gocui.ColorDefault
|
||||
appStatusView.FgColor = gocui.ColorCyan
|
||||
appStatusView.Frame = false
|
||||
_, _ = g.SetViewOnBottom("appStatus")
|
||||
}
|
||||
|
||||
informationView, err := setViewFromDimensions("information", "information", false)
|
||||
if err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
informationView.BgColor = gocui.ColorDefault
|
||||
informationView.FgColor = gocui.ColorGreen
|
||||
informationView.Frame = false
|
||||
gui.renderString("information", INFO_SECTION_PADDING+informationStr)
|
||||
}
|
||||
if gui.State.OldInformation != informationStr {
|
||||
gui.setViewContent(informationView, informationStr)
|
||||
gui.State.OldInformation = informationStr
|
||||
}
|
||||
|
||||
if gui.g.CurrentView() == nil {
|
||||
initialContext := gui.Contexts.Files.Context
|
||||
if gui.State.Modes.Filtering.Active() {
|
||||
initialContext = gui.Contexts.BranchCommits.Context
|
||||
}
|
||||
|
||||
if err := gui.switchContext(initialContext); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
type listContextState struct {
|
||||
view *gocui.View
|
||||
listContext *ListContext
|
||||
}
|
||||
|
||||
listContextStates := []listContextState{
|
||||
{view: filesView, listContext: gui.filesListContext()},
|
||||
{view: branchesView, listContext: gui.branchesListContext()},
|
||||
{view: branchesView, listContext: gui.remotesListContext()},
|
||||
{view: branchesView, listContext: gui.remoteBranchesListContext()},
|
||||
{view: branchesView, listContext: gui.tagsListContext()},
|
||||
{view: commitsView, listContext: gui.branchCommitsListContext()},
|
||||
{view: commitsView, listContext: gui.reflogCommitsListContext()},
|
||||
{view: stashView, listContext: gui.stashListContext()},
|
||||
{view: commitFilesView, listContext: gui.commitFilesListContext()},
|
||||
}
|
||||
|
||||
// menu view might not exist so we check to be safe
|
||||
if menuView, err := gui.g.View("menu"); err == nil {
|
||||
listContextStates = append(listContextStates, listContextState{view: menuView, listContext: gui.menuListContext()})
|
||||
}
|
||||
for _, listContextState := range listContextStates {
|
||||
// ignore contexts whose view is owned by another context right now
|
||||
if listContextState.view.Context != listContextState.listContext.GetKey() {
|
||||
continue
|
||||
}
|
||||
// check if the selected line is now out of view and if so refocus it
|
||||
listContextState.view.FocusPoint(0, listContextState.listContext.GetPanelState().GetSelectedLineIdx())
|
||||
|
||||
listContextState.view.SelBgColor = theme.GocuiSelectedLineBgColor
|
||||
|
||||
// I doubt this is expensive though it's admittedly redundant after the first render
|
||||
listContextState.view.SetOnSelectItem(gui.onSelectItemWrapper(listContextState.listContext.onSearchSelect))
|
||||
}
|
||||
|
||||
mainViewWidth, mainViewHeight := gui.getMainView().Size()
|
||||
if mainViewWidth != gui.State.PrevMainWidth || mainViewHeight != gui.State.PrevMainHeight {
|
||||
gui.State.PrevMainWidth = mainViewWidth
|
||||
gui.State.PrevMainHeight = mainViewHeight
|
||||
if err := gui.onResize(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// here is a good place log some stuff
|
||||
// if you download humanlog and do tail -f development.log | humanlog
|
||||
// this will let you see these branches as prettified json
|
||||
// gui.Log.Info(utils.AsJson(gui.State.Branches[0:4]))
|
||||
return gui.resizeCurrentPopupPanel()
|
||||
}
|
||||
|
||||
func (gui *Gui) onInitialViewsCreation() error {
|
||||
gui.setInitialViewContexts()
|
||||
|
||||
if err := gui.switchContext(gui.defaultSideContext()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.keybindings(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if gui.showRecentRepos {
|
||||
if err := gui.handleCreateRecentReposMenu(); err != nil {
|
||||
return err
|
||||
}
|
||||
gui.showRecentRepos = false
|
||||
}
|
||||
|
||||
return gui.loadNewRepo()
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
)
|
||||
|
||||
// Currently there are two 'pseudo-panels' that make use of this 'pseudo-panel'.
|
||||
@@ -24,7 +27,7 @@ const (
|
||||
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)
|
||||
patchParser, err := patch.NewPatchParser(gui.Log, diff)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
@@ -50,7 +53,7 @@ func (gui *Gui) refreshLineByLinePanel(diff string, secondaryDiff string, second
|
||||
prevNewHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0)
|
||||
selectedLineIdx = patchParser.GetNextStageableLineIndex(prevNewHunk.FirstLineIdx)
|
||||
newHunk := patchParser.GetHunkContainingLine(selectedLineIdx, 0)
|
||||
firstLineIdx, lastLineIdx = newHunk.FirstLineIdx, newHunk.LastLineIdx
|
||||
firstLineIdx, lastLineIdx = newHunk.FirstLineIdx, newHunk.LastLineIdx()
|
||||
} else {
|
||||
selectedLineIdx = patchParser.GetNextStageableLineIndex(state.SelectedLineIdx)
|
||||
firstLineIdx, lastLineIdx = selectedLineIdx, selectedLineIdx
|
||||
@@ -70,7 +73,7 @@ func (gui *Gui) refreshLineByLinePanel(diff string, secondaryDiff string, second
|
||||
SecondaryFocused: secondaryFocused,
|
||||
}
|
||||
|
||||
if err := gui.refreshMainView(); err != nil {
|
||||
if err := gui.refreshMainViewForLineByLine(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -82,13 +85,13 @@ func (gui *Gui) refreshLineByLinePanel(diff string, secondaryDiff string, second
|
||||
secondaryView.Highlight = true
|
||||
secondaryView.Wrap = false
|
||||
|
||||
secondaryPatchParser, err := commands.NewPatchParser(gui.Log, secondaryDiff)
|
||||
secondaryPatchParser, err := patch.NewPatchParser(gui.Log, secondaryDiff)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
gui.setViewContent(gui.g, gui.getSecondaryView(), secondaryPatchParser.Render(-1, -1, nil))
|
||||
gui.setViewContent(gui.getSecondaryView(), secondaryPatchParser.Render(-1, -1, nil))
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -117,16 +120,16 @@ func (gui *Gui) handleSelectNextHunk(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.selectNewHunk(newHunk)
|
||||
}
|
||||
|
||||
func (gui *Gui) selectNewHunk(newHunk *commands.PatchHunk) error {
|
||||
func (gui *Gui) selectNewHunk(newHunk *patch.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
|
||||
state.FirstLineIdx, state.LastLineIdx = newHunk.FirstLineIdx, newHunk.LastLineIdx()
|
||||
} else {
|
||||
state.FirstLineIdx, state.LastLineIdx = state.SelectedLineIdx, state.SelectedLineIdx
|
||||
}
|
||||
|
||||
if err := gui.refreshMainView(); err != nil {
|
||||
if err := gui.refreshMainViewForLineByLine(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -166,7 +169,7 @@ func (gui *Gui) handleSelectNewLine(newSelectedLineIdx int) error {
|
||||
state.FirstLineIdx = state.SelectedLineIdx
|
||||
}
|
||||
|
||||
if err := gui.refreshMainView(); err != nil {
|
||||
if err := gui.refreshMainViewForLineByLine(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -222,18 +225,22 @@ func (gui *Gui) handleMouseScrollDown(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) getSelectedCommitFileName() string {
|
||||
return gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLine].Name
|
||||
return gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLineIdx].Name
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshMainView() error {
|
||||
func (gui *Gui) refreshMainViewForLineByLine() 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" {
|
||||
if gui.currentContext().GetKey() == gui.Contexts.PatchBuilding.Context.GetKey() {
|
||||
filename := gui.getSelectedCommitFileName()
|
||||
includedLineIndices = gui.GitCommand.PatchManager.GetFileIncLineIndices(filename)
|
||||
var err error
|
||||
includedLineIndices, err = gui.GitCommand.PatchManager.GetFileIncLineIndices(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
colorDiff := state.PatchParser.Render(state.FirstLineIdx, state.LastLineIdx, includedLineIndices)
|
||||
|
||||
@@ -242,7 +249,7 @@ func (gui *Gui) refreshMainView() error {
|
||||
mainView.Wrap = false
|
||||
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
gui.setViewContent(gui.g, gui.getMainView(), colorDiff)
|
||||
gui.setViewContent(gui.getMainView(), colorDiff)
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -265,7 +272,7 @@ func (gui *Gui) focusSelection(includeCurrentHunk bool) error {
|
||||
if includeCurrentHunk {
|
||||
hunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0)
|
||||
firstLineIdx = hunk.FirstLineIdx
|
||||
lastLineIdx = hunk.LastLineIdx
|
||||
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
|
||||
@@ -299,7 +306,7 @@ func (gui *Gui) handleToggleSelectRange(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
state.FirstLineIdx, state.LastLineIdx = state.SelectedLineIdx, state.SelectedLineIdx
|
||||
|
||||
return gui.refreshMainView()
|
||||
return gui.refreshMainViewForLineByLine()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleToggleSelectHunk(g *gocui.Gui, v *gocui.View) error {
|
||||
@@ -311,10 +318,10 @@ func (gui *Gui) handleToggleSelectHunk(g *gocui.Gui, v *gocui.View) error {
|
||||
} else {
|
||||
state.SelectMode = HUNK
|
||||
selectedHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0)
|
||||
state.FirstLineIdx, state.LastLineIdx = selectedHunk.FirstLineIdx, selectedHunk.LastLineIdx
|
||||
state.FirstLineIdx, state.LastLineIdx = selectedHunk.FirstLineIdx, selectedHunk.LastLineIdx()
|
||||
}
|
||||
|
||||
if err := gui.refreshMainView(); err != nil {
|
||||
if err := gui.refreshMainViewForLineByLine(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -322,6 +329,33 @@ func (gui *Gui) handleToggleSelectHunk(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleEscapeLineByLinePanel() {
|
||||
gui.changeMainViewsContext("normal")
|
||||
gui.State.Panels.LineByLine = nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleOpenFileAtLine() error {
|
||||
// again, would be good to use inheritance here (or maybe even composition)
|
||||
var filename string
|
||||
switch gui.State.MainContext {
|
||||
case gui.Contexts.PatchBuilding.Context.GetKey():
|
||||
filename = gui.getSelectedCommitFileName()
|
||||
case gui.Contexts.Staging.Context.GetKey():
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
filename = file.Name
|
||||
default:
|
||||
return errors.Errorf("unknown main context: %s", gui.State.MainContext)
|
||||
}
|
||||
|
||||
state := gui.State.Panels.LineByLine
|
||||
// need to look at current index, then work out what my hunk's header information is, and see how far my line is away from the hunk header
|
||||
selectedHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0)
|
||||
lineNumber := selectedHunk.LineNumberOfLine(state.SelectedLineIdx)
|
||||
filenameWithLineNum := fmt.Sprintf("%s:%d", filename, lineNumber)
|
||||
if err := gui.OSCommand.OpenFile(filenameWithLineNum); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,526 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
)
|
||||
|
||||
type ListContext struct {
|
||||
ViewName string
|
||||
ContextKey string
|
||||
GetItemsLength func() int
|
||||
GetDisplayStrings func() [][]string
|
||||
OnFocus func() error
|
||||
OnFocusLost func() error
|
||||
OnClickSelectedItem func() error
|
||||
OnGetOptionsMap func() map[string]string
|
||||
|
||||
// the boolean here tells us whether the item is nil. This is needed because you can't work it out on the calling end once the pointer is wrapped in an interface (unless you want to use reflection)
|
||||
SelectedItem func() (ListItem, bool)
|
||||
GetPanelState func() IListPanelState
|
||||
|
||||
Gui *Gui
|
||||
ResetMainViewOriginOnFocus bool
|
||||
Kind int
|
||||
ParentContext Context
|
||||
// we can't know on the calling end whether a Context is actually a nil value without reflection, so we're storing this flag here to tell us. There has got to be a better way around this.
|
||||
hasParent bool
|
||||
WindowName string
|
||||
}
|
||||
|
||||
type ListItem interface {
|
||||
// ID is a SHA when the item is a commit, a filename when the item is a file, 'stash@{4}' when it's a stash entry, 'my_branch' when it's a branch
|
||||
ID() string
|
||||
|
||||
// Description is something we would show in a message e.g. '123as14: push blah' for a commit
|
||||
Description() string
|
||||
}
|
||||
|
||||
func (lc *ListContext) GetSelectedItem() (ListItem, bool) {
|
||||
return lc.SelectedItem()
|
||||
}
|
||||
|
||||
func (lc *ListContext) SetWindowName(windowName string) {
|
||||
lc.WindowName = windowName
|
||||
}
|
||||
|
||||
func (lc *ListContext) GetWindowName() string {
|
||||
windowName := lc.WindowName
|
||||
|
||||
if windowName != "" {
|
||||
return windowName
|
||||
}
|
||||
|
||||
// TODO: actually set this for everything so we don't default to the view name
|
||||
return lc.ViewName
|
||||
}
|
||||
|
||||
func (lc *ListContext) SetParentContext(c Context) {
|
||||
lc.ParentContext = c
|
||||
lc.hasParent = true
|
||||
}
|
||||
|
||||
func (lc *ListContext) GetParentContext() (Context, bool) {
|
||||
return lc.ParentContext, lc.hasParent
|
||||
}
|
||||
|
||||
func (lc *ListContext) GetSelectedItemId() string {
|
||||
item, ok := lc.SelectedItem()
|
||||
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
return item.ID()
|
||||
}
|
||||
|
||||
func (lc *ListContext) GetOptionsMap() map[string]string {
|
||||
if lc.OnGetOptionsMap != nil {
|
||||
return lc.OnGetOptionsMap()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnFocus assumes that the content of the context has already been rendered to the view. OnRender is the function which actually renders the content to the view
|
||||
func (lc *ListContext) OnRender() error {
|
||||
view, err := lc.Gui.g.View(lc.ViewName)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if lc.GetDisplayStrings != nil {
|
||||
lc.Gui.refreshSelectedLine(lc.GetPanelState(), lc.GetItemsLength())
|
||||
lc.Gui.renderDisplayStrings(view, lc.GetDisplayStrings())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lc *ListContext) GetKey() string {
|
||||
return lc.ContextKey
|
||||
}
|
||||
|
||||
func (lc *ListContext) GetKind() int {
|
||||
return lc.Kind
|
||||
}
|
||||
|
||||
func (lc *ListContext) GetViewName() string {
|
||||
return lc.ViewName
|
||||
}
|
||||
|
||||
func (lc *ListContext) HandleFocusLost() error {
|
||||
if lc.OnFocusLost != nil {
|
||||
return lc.OnFocusLost()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lc *ListContext) HandleFocus() error {
|
||||
if lc.Gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
view, err := lc.Gui.g.View(lc.ViewName)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
view.FocusPoint(0, lc.GetPanelState().GetSelectedLineIdx())
|
||||
|
||||
if lc.Gui.State.Modes.Diffing.Active() {
|
||||
return lc.Gui.renderDiff()
|
||||
}
|
||||
|
||||
if lc.OnFocus != nil {
|
||||
return lc.OnFocus()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lc *ListContext) HandleRender() error {
|
||||
return lc.OnRender()
|
||||
}
|
||||
|
||||
func (lc *ListContext) handlePrevLine(g *gocui.Gui, v *gocui.View) error {
|
||||
return lc.handleLineChange(-1)
|
||||
}
|
||||
|
||||
func (lc *ListContext) handleNextLine(g *gocui.Gui, v *gocui.View) error {
|
||||
return lc.handleLineChange(1)
|
||||
}
|
||||
|
||||
func (lc *ListContext) handleLineChange(change int) error {
|
||||
if !lc.Gui.isPopupPanel(lc.ViewName) && lc.Gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
view, err := lc.Gui.g.View(lc.ViewName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lc.Gui.changeSelectedLine(lc.GetPanelState(), lc.GetItemsLength(), change)
|
||||
view.FocusPoint(0, lc.GetPanelState().GetSelectedLineIdx())
|
||||
|
||||
if lc.ResetMainViewOriginOnFocus {
|
||||
if err := lc.Gui.resetOrigin(lc.Gui.getMainView()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := lc.Gui.resetOrigin(lc.Gui.getSecondaryView()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return lc.HandleFocus()
|
||||
}
|
||||
|
||||
func (lc *ListContext) handleNextPage(g *gocui.Gui, v *gocui.View) error {
|
||||
view, err := lc.Gui.g.View(lc.ViewName)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
_, height := view.Size()
|
||||
delta := height - 1
|
||||
if delta == 0 {
|
||||
delta = 1
|
||||
}
|
||||
return lc.handleLineChange(delta)
|
||||
}
|
||||
|
||||
func (lc *ListContext) handleGotoTop(g *gocui.Gui, v *gocui.View) error {
|
||||
return lc.handleLineChange(-lc.GetItemsLength())
|
||||
}
|
||||
|
||||
func (lc *ListContext) handleGotoBottom(g *gocui.Gui, v *gocui.View) error {
|
||||
return lc.handleLineChange(lc.GetItemsLength())
|
||||
}
|
||||
|
||||
func (lc *ListContext) handlePrevPage(g *gocui.Gui, v *gocui.View) error {
|
||||
view, err := lc.Gui.g.View(lc.ViewName)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
_, height := view.Size()
|
||||
delta := height - 1
|
||||
if delta == 0 {
|
||||
delta = 1
|
||||
}
|
||||
return lc.handleLineChange(-delta)
|
||||
}
|
||||
|
||||
func (lc *ListContext) handleClick(g *gocui.Gui, v *gocui.View) error {
|
||||
if !lc.Gui.isPopupPanel(lc.ViewName) && lc.Gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
prevSelectedLineIdx := lc.GetPanelState().GetSelectedLineIdx()
|
||||
newSelectedLineIdx := v.SelectedLineIdx()
|
||||
|
||||
// we need to focus the view
|
||||
if err := lc.Gui.switchContext(lc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if newSelectedLineIdx > lc.GetItemsLength()-1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
lc.GetPanelState().SetSelectedLineIdx(newSelectedLineIdx)
|
||||
|
||||
prevViewName := lc.Gui.currentViewName()
|
||||
if prevSelectedLineIdx == newSelectedLineIdx && prevViewName == lc.ViewName && lc.OnClickSelectedItem != nil {
|
||||
return lc.OnClickSelectedItem()
|
||||
}
|
||||
return lc.HandleFocus()
|
||||
}
|
||||
|
||||
func (lc *ListContext) onSearchSelect(selectedLineIdx int) error {
|
||||
lc.GetPanelState().SetSelectedLineIdx(selectedLineIdx)
|
||||
return lc.HandleFocus()
|
||||
}
|
||||
|
||||
func (gui *Gui) menuListContext() *ListContext {
|
||||
return &ListContext{
|
||||
ViewName: "menu",
|
||||
ContextKey: "menu",
|
||||
GetItemsLength: func() int { return gui.getMenuView().LinesHeight() },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Menu },
|
||||
OnFocus: gui.handleMenuSelect,
|
||||
OnClickSelectedItem: func() error { return gui.onMenuPress() },
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: false,
|
||||
Kind: PERSISTENT_POPUP,
|
||||
OnGetOptionsMap: gui.getMenuOptions,
|
||||
|
||||
// no GetDisplayStrings field because we do a custom render on menu creation
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) filesListContext() *ListContext {
|
||||
return &ListContext{
|
||||
ViewName: "files",
|
||||
ContextKey: FILES_CONTEXT_KEY,
|
||||
GetItemsLength: func() int { return len(gui.State.Files) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Files },
|
||||
OnFocus: gui.focusAndSelectFile,
|
||||
OnClickSelectedItem: gui.handleFilePress,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: false,
|
||||
Kind: SIDE_CONTEXT,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetFileListDisplayStrings(gui.State.Files, gui.State.Modes.Diffing.Ref)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedFile()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) branchesListContext() *ListContext {
|
||||
return &ListContext{
|
||||
ViewName: "branches",
|
||||
ContextKey: LOCAL_BRANCHES_CONTEXT_KEY,
|
||||
GetItemsLength: func() int { return len(gui.State.Branches) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Branches },
|
||||
OnFocus: gui.handleBranchSelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
Kind: SIDE_CONTEXT,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetBranchListDisplayStrings(gui.State.Branches, gui.State.ScreenMode != SCREEN_NORMAL, gui.State.Modes.Diffing.Ref)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedBranch()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) remotesListContext() *ListContext {
|
||||
return &ListContext{
|
||||
ViewName: "branches",
|
||||
ContextKey: REMOTES_CONTEXT_KEY,
|
||||
GetItemsLength: func() int { return len(gui.State.Remotes) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Remotes },
|
||||
OnFocus: gui.handleRemoteSelect,
|
||||
OnClickSelectedItem: gui.handleRemoteEnter,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
Kind: SIDE_CONTEXT,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetRemoteListDisplayStrings(gui.State.Remotes, gui.State.Modes.Diffing.Ref)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedRemote()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) remoteBranchesListContext() *ListContext {
|
||||
return &ListContext{
|
||||
ViewName: "branches",
|
||||
ContextKey: REMOTE_BRANCHES_CONTEXT_KEY,
|
||||
GetItemsLength: func() int { return len(gui.State.RemoteBranches) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.RemoteBranches },
|
||||
OnFocus: gui.handleRemoteBranchSelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
Kind: SIDE_CONTEXT,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetRemoteBranchListDisplayStrings(gui.State.RemoteBranches, gui.State.Modes.Diffing.Ref)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedRemoteBranch()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) tagsListContext() *ListContext {
|
||||
return &ListContext{
|
||||
ViewName: "branches",
|
||||
ContextKey: TAGS_CONTEXT_KEY,
|
||||
GetItemsLength: func() int { return len(gui.State.Tags) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Tags },
|
||||
OnFocus: gui.handleTagSelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
Kind: SIDE_CONTEXT,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetTagListDisplayStrings(gui.State.Tags, gui.State.Modes.Diffing.Ref)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedTag()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) branchCommitsListContext() *ListContext {
|
||||
return &ListContext{
|
||||
ViewName: "commits",
|
||||
ContextKey: BRANCH_COMMITS_CONTEXT_KEY,
|
||||
GetItemsLength: func() int { return len(gui.State.Commits) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Commits },
|
||||
OnFocus: gui.handleCommitSelect,
|
||||
OnClickSelectedItem: gui.handleViewCommitFiles,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
Kind: SIDE_CONTEXT,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetCommitListDisplayStrings(gui.State.Commits, gui.State.ScreenMode != SCREEN_NORMAL, gui.cherryPickedCommitShaMap(), gui.State.Modes.Diffing.Ref)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedLocalCommit()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) reflogCommitsListContext() *ListContext {
|
||||
return &ListContext{
|
||||
ViewName: "commits",
|
||||
ContextKey: REFLOG_COMMITS_CONTEXT_KEY,
|
||||
GetItemsLength: func() int { return len(gui.State.FilteredReflogCommits) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.ReflogCommits },
|
||||
OnFocus: gui.handleReflogCommitSelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
Kind: SIDE_CONTEXT,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetReflogCommitListDisplayStrings(gui.State.FilteredReflogCommits, gui.State.ScreenMode != SCREEN_NORMAL, gui.cherryPickedCommitShaMap(), gui.State.Modes.Diffing.Ref)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedReflogCommit()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) subCommitsListContext() *ListContext {
|
||||
return &ListContext{
|
||||
ViewName: "branches",
|
||||
ContextKey: SUB_COMMITS_CONTEXT_KEY,
|
||||
GetItemsLength: func() int { return len(gui.State.SubCommits) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.SubCommits },
|
||||
OnFocus: gui.handleSubCommitSelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
Kind: SIDE_CONTEXT,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
gui.Log.Warn("getting display strings for sub commits")
|
||||
return presentation.GetCommitListDisplayStrings(gui.State.SubCommits, gui.State.ScreenMode != SCREEN_NORMAL, gui.cherryPickedCommitShaMap(), gui.State.Modes.Diffing.Ref)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedSubCommit()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) stashListContext() *ListContext {
|
||||
return &ListContext{
|
||||
ViewName: "stash",
|
||||
ContextKey: STASH_CONTEXT_KEY,
|
||||
GetItemsLength: func() int { return len(gui.State.StashEntries) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Stash },
|
||||
OnFocus: gui.handleStashEntrySelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
Kind: SIDE_CONTEXT,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetStashEntryListDisplayStrings(gui.State.StashEntries, gui.State.Modes.Diffing.Ref)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedStashEntry()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) commitFilesListContext() *ListContext {
|
||||
return &ListContext{
|
||||
ViewName: "commitFiles",
|
||||
WindowName: "commits",
|
||||
ContextKey: COMMIT_FILES_CONTEXT_KEY,
|
||||
GetItemsLength: func() int { return len(gui.State.CommitFiles) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.CommitFiles },
|
||||
OnFocus: gui.handleCommitFileSelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
Kind: SIDE_CONTEXT,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetCommitFileListDisplayStrings(gui.State.CommitFiles, gui.State.Modes.Diffing.Ref)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedCommitFile()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) getListContexts() []*ListContext {
|
||||
return []*ListContext{
|
||||
gui.menuListContext(),
|
||||
gui.filesListContext(),
|
||||
gui.branchesListContext(),
|
||||
gui.remotesListContext(),
|
||||
gui.remoteBranchesListContext(),
|
||||
gui.tagsListContext(),
|
||||
gui.branchCommitsListContext(),
|
||||
gui.reflogCommitsListContext(),
|
||||
gui.subCommitsListContext(),
|
||||
gui.stashListContext(),
|
||||
gui.commitFilesListContext(),
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) getListContextKeyBindings() []*Binding {
|
||||
bindings := make([]*Binding, 0)
|
||||
|
||||
for _, listContext := range gui.getListContexts() {
|
||||
bindings = append(bindings, []*Binding{
|
||||
{ViewName: listContext.ViewName, Contexts: []string{listContext.ContextKey}, Key: gui.getKey("universal.prevItem-alt"), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
|
||||
{ViewName: listContext.ViewName, Contexts: []string{listContext.ContextKey}, Key: gui.getKey("universal.prevItem"), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
|
||||
{ViewName: listContext.ViewName, Contexts: []string{listContext.ContextKey}, Key: gocui.MouseWheelUp, Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
|
||||
{ViewName: listContext.ViewName, Contexts: []string{listContext.ContextKey}, Key: gui.getKey("universal.nextItem-alt"), Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
|
||||
{ViewName: listContext.ViewName, Contexts: []string{listContext.ContextKey}, Key: gui.getKey("universal.nextItem"), Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
|
||||
{ViewName: listContext.ViewName, Contexts: []string{listContext.ContextKey}, Key: gui.getKey("universal.prevPage"), Modifier: gocui.ModNone, Handler: listContext.handlePrevPage, Description: gui.Tr.SLocalize("prevPage")},
|
||||
{ViewName: listContext.ViewName, Contexts: []string{listContext.ContextKey}, Key: gui.getKey("universal.nextPage"), Modifier: gocui.ModNone, Handler: listContext.handleNextPage, Description: gui.Tr.SLocalize("nextPage")},
|
||||
{ViewName: listContext.ViewName, Contexts: []string{listContext.ContextKey}, Key: gui.getKey("universal.gotoTop"), Modifier: gocui.ModNone, Handler: listContext.handleGotoTop, Description: gui.Tr.SLocalize("gotoTop")},
|
||||
{ViewName: listContext.ViewName, Contexts: []string{listContext.ContextKey}, Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
|
||||
{ViewName: listContext.ViewName, Contexts: []string{listContext.ContextKey}, Key: gocui.MouseLeft, Modifier: gocui.ModNone, Handler: listContext.handleClick},
|
||||
}...)
|
||||
|
||||
// the commits panel needs to lazyload things so it has a couple of its own handlers
|
||||
openSearchHandler := gui.handleOpenSearch
|
||||
gotoBottomHandler := listContext.handleGotoBottom
|
||||
if listContext.ViewName == "commits" {
|
||||
openSearchHandler = gui.handleOpenSearchForCommitsPanel
|
||||
gotoBottomHandler = gui.handleGotoBottomForCommitsPanel
|
||||
}
|
||||
|
||||
bindings = append(bindings, []*Binding{
|
||||
{
|
||||
ViewName: listContext.ViewName,
|
||||
Contexts: []string{listContext.ContextKey},
|
||||
Key: gui.getKey("universal.startSearch"),
|
||||
Handler: openSearchHandler,
|
||||
Description: gui.Tr.SLocalize("startSearch"),
|
||||
},
|
||||
{
|
||||
ViewName: listContext.ViewName,
|
||||
Contexts: []string{listContext.ContextKey},
|
||||
Key: gui.getKey("universal.gotoBottom"),
|
||||
Handler: gotoBottomHandler,
|
||||
Description: gui.Tr.SLocalize("gotoBottom"),
|
||||
},
|
||||
}...)
|
||||
}
|
||||
|
||||
return bindings
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package gui
|
||||
|
||||
import "os/exec"
|
||||
|
||||
type viewUpdateOpts struct {
|
||||
title string
|
||||
|
||||
// awkwardly calling this noWrap because of how hard Go makes it to have
|
||||
// a boolean option that defaults to true
|
||||
noWrap bool
|
||||
|
||||
highlight bool
|
||||
|
||||
task updateTask
|
||||
}
|
||||
|
||||
type coordinates struct {
|
||||
x int
|
||||
y int
|
||||
}
|
||||
|
||||
type refreshMainOpts struct {
|
||||
main *viewUpdateOpts
|
||||
secondary *viewUpdateOpts
|
||||
}
|
||||
|
||||
// constants for updateTask's kind field
|
||||
const (
|
||||
RENDER_STRING = iota
|
||||
RENDER_STRING_WITHOUT_SCROLL
|
||||
RUN_FUNCTION
|
||||
RUN_COMMAND
|
||||
RUN_PTY
|
||||
)
|
||||
|
||||
type updateTask interface {
|
||||
GetKind() int
|
||||
}
|
||||
|
||||
type renderStringTask struct {
|
||||
str string
|
||||
}
|
||||
|
||||
func (t *renderStringTask) GetKind() int {
|
||||
return RENDER_STRING
|
||||
}
|
||||
|
||||
func (gui *Gui) createRenderStringTask(str string) *renderStringTask {
|
||||
return &renderStringTask{str: str}
|
||||
}
|
||||
|
||||
type renderStringWithoutScrollTask struct {
|
||||
str string
|
||||
}
|
||||
|
||||
func (t *renderStringWithoutScrollTask) GetKind() int {
|
||||
return RENDER_STRING_WITHOUT_SCROLL
|
||||
}
|
||||
|
||||
func (gui *Gui) createRenderStringWithoutScrollTask(str string) *renderStringWithoutScrollTask {
|
||||
return &renderStringWithoutScrollTask{str: str}
|
||||
}
|
||||
|
||||
type runCommandTask struct {
|
||||
cmd *exec.Cmd
|
||||
}
|
||||
|
||||
func (t *runCommandTask) GetKind() int {
|
||||
return RUN_COMMAND
|
||||
}
|
||||
|
||||
func (gui *Gui) createRunCommandTask(cmd *exec.Cmd) *runCommandTask {
|
||||
return &runCommandTask{cmd: cmd}
|
||||
}
|
||||
|
||||
type runPtyTask struct {
|
||||
cmd *exec.Cmd
|
||||
}
|
||||
|
||||
func (t *runPtyTask) GetKind() int {
|
||||
return RUN_PTY
|
||||
}
|
||||
|
||||
func (gui *Gui) createRunPtyTask(cmd *exec.Cmd) *runPtyTask {
|
||||
return &runPtyTask{cmd: cmd}
|
||||
}
|
||||
|
||||
type runFunctionTask struct {
|
||||
f func(chan struct{}) error
|
||||
}
|
||||
|
||||
func (t *runFunctionTask) GetKind() int {
|
||||
return RUN_FUNCTION
|
||||
}
|
||||
|
||||
func (gui *Gui) createRunFunctionTask(f func(chan struct{}) error) *runFunctionTask {
|
||||
return &runFunctionTask{f: f}
|
||||
}
|
||||
|
||||
func (gui *Gui) runTaskForView(viewName string, task updateTask) error {
|
||||
switch task.GetKind() {
|
||||
case RENDER_STRING:
|
||||
specificTask := task.(*renderStringTask)
|
||||
return gui.newStringTask(viewName, specificTask.str)
|
||||
|
||||
case RENDER_STRING_WITHOUT_SCROLL:
|
||||
specificTask := task.(*renderStringWithoutScrollTask)
|
||||
return gui.newStringTaskWithoutScroll(viewName, specificTask.str)
|
||||
|
||||
case RUN_FUNCTION:
|
||||
specificTask := task.(*runFunctionTask)
|
||||
return gui.newTask(viewName, specificTask.f)
|
||||
|
||||
case RUN_COMMAND:
|
||||
specificTask := task.(*runCommandTask)
|
||||
return gui.newCmdTask(viewName, specificTask.cmd)
|
||||
|
||||
case RUN_PTY:
|
||||
specificTask := task.(*runPtyTask)
|
||||
return gui.newPtyTask(viewName, specificTask.cmd)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshMainView(opts *viewUpdateOpts, viewName string) error {
|
||||
view, err := gui.g.View(viewName)
|
||||
if err != nil {
|
||||
gui.Log.Error(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
view.Title = opts.title
|
||||
view.Wrap = !opts.noWrap
|
||||
view.Highlight = opts.highlight
|
||||
|
||||
if err := gui.runTaskForView(viewName, opts.task); err != nil {
|
||||
gui.Log.Error(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshMainViews(opts refreshMainOpts) error {
|
||||
if opts.main != nil {
|
||||
if err := gui.refreshMainView(opts.main, "main"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
gui.splitMainPanel(opts.secondary != nil)
|
||||
|
||||
if opts.secondary != nil {
|
||||
if err := gui.refreshMainView(opts.secondary, "secondary"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) splitMainPanel(splitMainPanel bool) {
|
||||
gui.State.SplitMainPanel = splitMainPanel
|
||||
|
||||
// no need to set view on bottom when splitMainPanel is false: it will have zero size anyway thanks to our view arrangement code.
|
||||
if splitMainPanel {
|
||||
_, _ = gui.g.SetViewOnTop("secondary")
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) isMainPanelSplit() bool {
|
||||
return gui.State.SplitMainPanel
|
||||
}
|
||||
+31
-47
@@ -2,6 +2,7 @@ package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
@@ -14,35 +15,37 @@ type menuItem struct {
|
||||
onPress func() error
|
||||
}
|
||||
|
||||
// every item in a list context needs an ID
|
||||
func (i *menuItem) ID() string {
|
||||
if i.displayString != "" {
|
||||
return i.displayString
|
||||
}
|
||||
|
||||
return strings.Join(i.displayStrings, "-")
|
||||
}
|
||||
|
||||
// list panel functions
|
||||
|
||||
func (gui *Gui) handleMenuSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
v.FocusPoint(0, gui.State.Panels.Menu.SelectedLine)
|
||||
func (gui *Gui) handleMenuSelect() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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"),
|
||||
func (gui *Gui) getMenuOptions() map[string]string {
|
||||
return map[string]string{
|
||||
gui.getKeyDisplay("universal.return"): 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)
|
||||
return gui.returnFromContext()
|
||||
}
|
||||
|
||||
type createMenuOptions struct {
|
||||
@@ -60,7 +63,7 @@ func (gui *Gui) createMenu(title string, items []*menuItem, createMenuOptions cr
|
||||
})
|
||||
}
|
||||
|
||||
gui.State.MenuItemCount = len(items)
|
||||
gui.State.MenuItems = items
|
||||
|
||||
stringArrays := make([][]string, len(items))
|
||||
for i, item := range items {
|
||||
@@ -73,48 +76,29 @@ func (gui *Gui) createMenu(title string, items []*menuItem, createMenuOptions cr
|
||||
|
||||
list := utils.RenderDisplayStrings(stringArrays)
|
||||
|
||||
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, false, list)
|
||||
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(false, list)
|
||||
menuView, _ := gui.g.SetView("menu", x0, y0, x1, y1, 0)
|
||||
menuView.Title = title
|
||||
menuView.FgColor = theme.GocuiDefaultTextColor
|
||||
menuView.ContainsList = true
|
||||
menuView.Clear()
|
||||
menuView.SetOnSelectItem(gui.onSelectItemWrapper(func(selectedLine int) error {
|
||||
return nil
|
||||
}))
|
||||
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.State.Panels.Menu.SelectedLineIdx = 0
|
||||
|
||||
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 gui.switchContext(gui.Contexts.Menu.Context)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) onMenuPress() error {
|
||||
selectedLine := gui.State.Panels.Menu.SelectedLineIdx
|
||||
if err := gui.State.MenuItems[selectedLine].onPress(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.returnFromContext()
|
||||
}
|
||||
|
||||
+113
-40
@@ -12,6 +12,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/golang-collections/collections/stack"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
@@ -19,8 +20,13 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func (gui *Gui) findConflicts(content string) ([]commands.Conflict, error) {
|
||||
func (gui *Gui) findConflicts(content string) []commands.Conflict {
|
||||
conflicts := make([]commands.Conflict, 0)
|
||||
|
||||
if content == "" {
|
||||
return conflicts
|
||||
}
|
||||
|
||||
var newConflict commands.Conflict
|
||||
for i, line := range utils.SplitLines(content) {
|
||||
trimmedLine := strings.TrimPrefix(line, "++")
|
||||
@@ -34,7 +40,7 @@ func (gui *Gui) findConflicts(content string) ([]commands.Conflict, error) {
|
||||
conflicts = append(conflicts, newConflict)
|
||||
}
|
||||
}
|
||||
return conflicts, nil
|
||||
return conflicts
|
||||
}
|
||||
|
||||
func (gui *Gui) shiftConflict(conflicts []commands.Conflict) (commands.Conflict, []commands.Conflict) {
|
||||
@@ -59,6 +65,7 @@ func (gui *Gui) coloredConflictFile(content string, conflicts []commands.Conflic
|
||||
colour := color.New(colourAttr)
|
||||
if hasFocus && conflictIndex < len(conflicts) && conflicts[conflictIndex] == conflict && gui.shouldHighlightLine(i, conflict, conflictTop) {
|
||||
colour.Add(color.Bold)
|
||||
colour.Add(theme.SelectedRangeBgColor)
|
||||
}
|
||||
if i == conflict.End && len(remainingConflicts) > 0 {
|
||||
conflict, remainingConflicts = gui.shiftConflict(remainingConflicts)
|
||||
@@ -68,17 +75,24 @@ func (gui *Gui) coloredConflictFile(content string, conflicts []commands.Conflic
|
||||
return outputBuffer.String(), nil
|
||||
}
|
||||
|
||||
func (gui *Gui) takeOverScrolling() {
|
||||
gui.State.Panels.Merging.UserScrolling = false
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSelectTop(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.takeOverScrolling()
|
||||
gui.State.Panels.Merging.ConflictTop = true
|
||||
return gui.refreshMergePanel()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSelectBottom(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.takeOverScrolling()
|
||||
gui.State.Panels.Merging.ConflictTop = false
|
||||
return gui.refreshMergePanel()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSelectNextConflict(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.takeOverScrolling()
|
||||
if gui.State.Panels.Merging.ConflictIndex >= len(gui.State.Panels.Merging.Conflicts)-1 {
|
||||
return nil
|
||||
}
|
||||
@@ -87,6 +101,7 @@ func (gui *Gui) handleSelectNextConflict(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSelectPrevConflict(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.takeOverScrolling()
|
||||
if gui.State.Panels.Merging.ConflictIndex <= 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -103,10 +118,10 @@ func (gui *Gui) isIndexToDelete(i int, conflict commands.Conflict, pick string)
|
||||
(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
|
||||
func (gui *Gui) resolveConflict(conflict commands.Conflict, pick string) error {
|
||||
gitFile := gui.getSelectedFile()
|
||||
if gitFile == nil {
|
||||
return nil
|
||||
}
|
||||
file, err := os.Open(gitFile.Name)
|
||||
if err != nil {
|
||||
@@ -130,9 +145,9 @@ func (gui *Gui) resolveConflict(g *gocui.Gui, conflict commands.Conflict, pick s
|
||||
}
|
||||
|
||||
func (gui *Gui) pushFileSnapshot(g *gocui.Gui) error {
|
||||
gitFile, err := gui.getSelectedFile(g)
|
||||
if err != nil {
|
||||
return err
|
||||
gitFile := gui.getSelectedFile()
|
||||
if gitFile == nil {
|
||||
return nil
|
||||
}
|
||||
content, err := gui.GitCommand.CatFile(gitFile.Name)
|
||||
if err != nil {
|
||||
@@ -147,9 +162,9 @@ func (gui *Gui) handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error {
|
||||
return nil
|
||||
}
|
||||
prevContent := gui.State.Panels.Merging.EditHistory.Pop().(string)
|
||||
gitFile, err := gui.getSelectedFile(g)
|
||||
if err != nil {
|
||||
return err
|
||||
gitFile := gui.getSelectedFile()
|
||||
if gitFile == nil {
|
||||
return nil
|
||||
}
|
||||
if err := ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644); err != nil {
|
||||
return err
|
||||
@@ -159,6 +174,8 @@ func (gui *Gui) handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handlePickHunk(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.takeOverScrolling()
|
||||
|
||||
conflict := gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex]
|
||||
if err := gui.pushFileSnapshot(g); err != nil {
|
||||
return err
|
||||
@@ -168,7 +185,7 @@ func (gui *Gui) handlePickHunk(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.State.Panels.Merging.ConflictTop {
|
||||
pick = "top"
|
||||
}
|
||||
err := gui.resolveConflict(g, conflict, pick)
|
||||
err := gui.resolveConflict(conflict, pick)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -183,11 +200,13 @@ func (gui *Gui) handlePickHunk(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handlePickBothHunks(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.takeOverScrolling()
|
||||
|
||||
conflict := gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex]
|
||||
if err := gui.pushFileSnapshot(g); err != nil {
|
||||
return err
|
||||
}
|
||||
err := gui.resolveConflict(g, conflict, "both")
|
||||
err := gui.resolveConflict(conflict, "both")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -198,16 +217,16 @@ 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
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
main: &viewUpdateOpts{
|
||||
title: "",
|
||||
task: gui.createRenderStringTask(err.Error()),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
panelState.Conflicts = gui.findConflicts(cat)
|
||||
|
||||
// handle potential fixes that the user made in their editor since we last refreshed
|
||||
if len(panelState.Conflicts) == 0 {
|
||||
return gui.handleCompleteMerge()
|
||||
@@ -221,18 +240,42 @@ func (gui *Gui) refreshMergePanel() error {
|
||||
return err
|
||||
}
|
||||
|
||||
mainView := gui.getMainView()
|
||||
mainView.Wrap = false
|
||||
gui.setViewContent(gui.g, mainView, content)
|
||||
gui.Log.Warn("scrolling to conflict")
|
||||
if err := gui.scrollToConflict(gui.g); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
main: &viewUpdateOpts{
|
||||
title: gui.Tr.SLocalize("MergeConflictsTitle"),
|
||||
task: gui.createRenderStringWithoutScrollTask(content),
|
||||
noWrap: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) {
|
||||
item := gui.getSelectedFile()
|
||||
if item == nil {
|
||||
return "", errors.New(gui.Tr.SLocalize("NoFilesDisplay"))
|
||||
}
|
||||
|
||||
if item.Type != "file" {
|
||||
return "", errors.New(gui.Tr.SLocalize("NotAFile"))
|
||||
}
|
||||
|
||||
cat, err := gui.GitCommand.CatFile(item.Name)
|
||||
if err != nil {
|
||||
gui.Log.Error(err)
|
||||
return "", err
|
||||
}
|
||||
return cat, nil
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollToConflict(g *gocui.Gui) error {
|
||||
if gui.State.Panels.Merging.UserScrolling {
|
||||
return nil
|
||||
}
|
||||
|
||||
panelState := gui.State.Panels.Merging
|
||||
if len(panelState.Conflicts) == 0 {
|
||||
return nil
|
||||
@@ -249,25 +292,27 @@ func (gui *Gui) scrollToConflict(g *gocui.Gui) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) renderMergeOptions() error {
|
||||
return gui.renderOptionsMap(map[string]string{
|
||||
func (gui *Gui) getMergingOptions() map[string]string {
|
||||
return 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("universal.undo"): gui.Tr.SLocalize("undo"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) handleEscapeMerge(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleEscapeMerge() error {
|
||||
gui.takeOverScrolling()
|
||||
|
||||
gui.State.Panels.Merging.EditHistory = stack.New()
|
||||
if err := gui.refreshFiles(); err != nil {
|
||||
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); 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 gui.switchContext(gui.Contexts.Files.Context)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -276,24 +321,52 @@ func (gui *Gui) handleCompleteMerge() error {
|
||||
if err := gui.stageSelectedFile(gui.g); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.refreshFiles(); err != nil {
|
||||
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); 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 gui.GitCommand.WorkingTreeState() == "normal" {
|
||||
return gui.handleEscapeMerge()
|
||||
}
|
||||
// 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())
|
||||
return gui.handleEscapeMerge()
|
||||
}
|
||||
|
||||
// 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)
|
||||
gui.takeOverScrolling()
|
||||
|
||||
return gui.ask(askOpts{
|
||||
title: "continue",
|
||||
prompt: gui.Tr.SLocalize("ConflictsResolved"),
|
||||
handlersManageFocus: true,
|
||||
handleConfirm: func() error {
|
||||
if err := gui.switchContext(gui.Contexts.Files.Context); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.genericMergeCommand("continue")
|
||||
},
|
||||
handleClose: func() error {
|
||||
return gui.switchContext(gui.Contexts.Files.Context)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) canScrollMergePanel() bool {
|
||||
currentViewName := gui.currentViewName()
|
||||
if currentViewName != "main" {
|
||||
return false
|
||||
}
|
||||
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return file.HasInlineMergeConflicts
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type modeStatus struct {
|
||||
isActive func() bool
|
||||
description func() string
|
||||
reset func() error
|
||||
}
|
||||
|
||||
func (gui *Gui) modeStatuses() []modeStatus {
|
||||
return []modeStatus{
|
||||
{
|
||||
isActive: gui.State.Modes.Diffing.Active,
|
||||
description: func() string {
|
||||
return utils.ColoredString(
|
||||
fmt.Sprintf("%s %s %s", gui.Tr.SLocalize("showingGitDiff"), "git diff "+gui.diffStr(), utils.ColoredString(gui.Tr.SLocalize("(reset)"), color.Underline)),
|
||||
color.FgMagenta,
|
||||
)
|
||||
},
|
||||
reset: gui.exitDiffMode,
|
||||
},
|
||||
{
|
||||
isActive: gui.State.Modes.Filtering.Active,
|
||||
description: func() string {
|
||||
return utils.ColoredString(
|
||||
fmt.Sprintf("%s '%s' %s", gui.Tr.SLocalize("filteringBy"), gui.State.Modes.Filtering.Path, utils.ColoredString(gui.Tr.SLocalize("(reset)"), color.Underline)),
|
||||
color.FgRed,
|
||||
color.Bold,
|
||||
)
|
||||
},
|
||||
reset: gui.exitFilterMode,
|
||||
},
|
||||
{
|
||||
isActive: gui.GitCommand.PatchManager.Active,
|
||||
description: func() string {
|
||||
return utils.ColoredString(
|
||||
fmt.Sprintf("%s %s", gui.Tr.SLocalize("buildingPatch"), utils.ColoredString(gui.Tr.SLocalize("(reset)"), color.Underline)),
|
||||
color.FgYellow,
|
||||
color.Bold,
|
||||
)
|
||||
},
|
||||
reset: gui.handleResetPatch,
|
||||
},
|
||||
{
|
||||
isActive: gui.State.Modes.CherryPicking.Active,
|
||||
description: func() string {
|
||||
return utils.ColoredString(
|
||||
fmt.Sprintf("%d commits copied %s", len(gui.State.Modes.CherryPicking.CherryPickedCommits), utils.ColoredString(gui.Tr.SLocalize("(reset)"), color.Underline)),
|
||||
color.FgCyan,
|
||||
)
|
||||
},
|
||||
reset: gui.exitCherryPickingMode,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -5,24 +5,38 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int) error {
|
||||
if !gui.GitCommand.PatchManager.CommitSelected() {
|
||||
return gui.handleEscapePatchBuildingPanel(gui.g, nil)
|
||||
// getFromAndReverseArgsForDiff tells us the from and reverse args to be used in a diff command. If we're not in diff mode we'll end up with the equivalent of a `git show` i.e `git diff blah^..blah`.
|
||||
func (gui *Gui) getFromAndReverseArgsForDiff(to string) (string, bool) {
|
||||
from := to + "^"
|
||||
reverse := false
|
||||
|
||||
if gui.State.Modes.Diffing.Active() {
|
||||
reverse = gui.State.Modes.Diffing.Reverse
|
||||
from = gui.State.Modes.Diffing.Ref
|
||||
}
|
||||
|
||||
gui.State.SplitMainPanel = true
|
||||
return from, reverse
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int) error {
|
||||
if !gui.GitCommand.PatchManager.Active() {
|
||||
return gui.handleEscapePatchBuildingPanel()
|
||||
}
|
||||
|
||||
gui.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)
|
||||
commitFile := gui.getSelectedCommitFile()
|
||||
if commitFile == nil {
|
||||
gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
|
||||
return nil
|
||||
}
|
||||
|
||||
diff, err := gui.GitCommand.ShowCommitFile(commitFile.Sha, commitFile.Name, true)
|
||||
to := commitFile.Parent
|
||||
from, reverse := gui.getFromAndReverseArgsForDiff(to)
|
||||
diff, err := gui.GitCommand.ShowFileDiff(from, to, reverse, commitFile.Name, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -38,7 +52,7 @@ func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int) error {
|
||||
}
|
||||
|
||||
if empty {
|
||||
return gui.handleEscapePatchBuildingPanel(gui.g, nil)
|
||||
return gui.handleEscapePatchBuildingPanel()
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -49,16 +63,18 @@ func (gui *Gui) handleToggleSelectionForPatch(g *gocui.Gui, v *gocui.View) error
|
||||
|
||||
toggleFunc := gui.GitCommand.PatchManager.AddFileLineRange
|
||||
filename := gui.getSelectedCommitFileName()
|
||||
includedLineIndices := gui.GitCommand.PatchManager.GetFileIncLineIndices(filename)
|
||||
includedLineIndices, err := gui.GitCommand.PatchManager.GetFileIncLineIndices(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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)
|
||||
commitFile := gui.getSelectedCommitFile()
|
||||
if commitFile == nil {
|
||||
gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -75,30 +91,31 @@ func (gui *Gui) handleToggleSelectionForPatch(g *gocui.Gui, v *gocui.View) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleEscapePatchBuildingPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleEscapePatchBuildingPanel() error {
|
||||
gui.handleEscapeLineByLinePanel()
|
||||
|
||||
if gui.GitCommand.PatchManager.IsEmpty() {
|
||||
gui.GitCommand.PatchManager.Reset()
|
||||
gui.State.SplitMainPanel = false
|
||||
}
|
||||
|
||||
return gui.switchFocus(gui.g, nil, gui.getCommitFilesView())
|
||||
if gui.currentContext().GetKey() == gui.Contexts.PatchBuilding.Context.GetKey() {
|
||||
return gui.switchContext(gui.Contexts.CommitFiles.Context)
|
||||
} else {
|
||||
// need to re-focus in case the secondary view should now be hidden
|
||||
return gui.currentContext().HandleFocus()
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshSecondaryPatchPanel() error {
|
||||
if gui.GitCommand.PatchManager.CommitSelected() {
|
||||
gui.State.SplitMainPanel = true
|
||||
secondaryView := gui.getSecondaryView()
|
||||
secondaryView.Highlight = true
|
||||
secondaryView.Wrap = false
|
||||
func (gui *Gui) secondaryPatchPanelUpdateOpts() *viewUpdateOpts {
|
||||
if gui.GitCommand.PatchManager.Active() {
|
||||
patch := gui.GitCommand.PatchManager.RenderAggregatedPatchColored(false)
|
||||
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
gui.setViewContent(gui.g, gui.getSecondaryView(), gui.GitCommand.PatchManager.RenderAggregatedPatchColored(false))
|
||||
return nil
|
||||
})
|
||||
} else {
|
||||
gui.State.SplitMainPanel = false
|
||||
return &viewUpdateOpts{
|
||||
title: "Custom Patch",
|
||||
noWrap: true,
|
||||
highlight: true,
|
||||
task: gui.createRenderStringWithoutScrollTask(patch),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -7,43 +7,58 @@ import (
|
||||
)
|
||||
|
||||
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"))
|
||||
if !gui.GitCommand.PatchManager.Active() {
|
||||
return gui.createErrorPanel(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: "apply patch",
|
||||
onPress: gui.handleApplyPatch,
|
||||
},
|
||||
{
|
||||
displayString: "reset patch",
|
||||
onPress: gui.handleResetPatch,
|
||||
},
|
||||
{
|
||||
displayString: "apply patch",
|
||||
onPress: func() error { return gui.handleApplyPatch(false) },
|
||||
},
|
||||
{
|
||||
displayString: "apply patch in reverse",
|
||||
onPress: func() error { return gui.handleApplyPatch(true) },
|
||||
},
|
||||
}
|
||||
|
||||
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:]...,
|
||||
)...,
|
||||
)
|
||||
if gui.GitCommand.PatchManager.CanRebase && gui.workingTreeState() == "normal" {
|
||||
menuItems = append(menuItems, []*menuItem{
|
||||
{
|
||||
displayString: fmt.Sprintf("remove patch from original commit (%s)", gui.GitCommand.PatchManager.To),
|
||||
onPress: gui.handleDeletePatchFromCommit,
|
||||
},
|
||||
{
|
||||
displayString: "pull patch out into index",
|
||||
onPress: gui.handlePullPatchIntoWorkingTree,
|
||||
},
|
||||
{
|
||||
displayString: "pull patch into new commit",
|
||||
onPress: gui.handlePullPatchIntoNewCommit,
|
||||
},
|
||||
}...)
|
||||
|
||||
if gui.currentContext().GetKey() == gui.Contexts.BranchCommits.Context.GetKey() {
|
||||
selectedCommit := gui.getSelectedLocalCommit()
|
||||
if selectedCommit != nil && gui.GitCommand.PatchManager.To != 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})
|
||||
@@ -51,7 +66,7 @@ func (gui *Gui) handleCreatePatchOptionsMenu(g *gocui.Gui, v *gocui.View) error
|
||||
|
||||
func (gui *Gui) getPatchCommitIndex() int {
|
||||
for index, commit := range gui.State.Commits {
|
||||
if commit.Sha == gui.GitCommand.PatchManager.CommitSha {
|
||||
if commit.Sha == gui.GitCommand.PatchManager.To {
|
||||
return index
|
||||
}
|
||||
}
|
||||
@@ -59,15 +74,15 @@ func (gui *Gui) getPatchCommitIndex() int {
|
||||
}
|
||||
|
||||
func (gui *Gui) validateNormalWorkingTreeState() (bool, error) {
|
||||
if gui.State.WorkingTreeState != "normal" {
|
||||
return false, gui.createErrorPanel(gui.g, gui.Tr.SLocalize("CantPatchWhileRebasingError"))
|
||||
if gui.GitCommand.WorkingTreeState() != "normal" {
|
||||
return false, gui.createErrorPanel(gui.Tr.SLocalize("CantPatchWhileRebasingError"))
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (gui *Gui) returnFocusFromLineByLinePanelIfNecessary() error {
|
||||
if gui.State.MainContext == "patch-building" {
|
||||
return gui.handleEscapePatchBuildingPanel(gui.g, nil)
|
||||
if gui.State.MainContext == MAIN_PATCH_BUILDING_CONTEXT_KEY {
|
||||
return gui.handleEscapePatchBuildingPanel()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -99,7 +114,7 @@ func (gui *Gui) handleMovePatchToSelectedCommit() error {
|
||||
|
||||
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)
|
||||
err := gui.GitCommand.MovePatchToSelectedCommit(gui.State.Commits, commitIndex, gui.State.Panels.Commits.SelectedLineIdx, gui.GitCommand.PatchManager)
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
}
|
||||
@@ -113,25 +128,60 @@ func (gui *Gui) handlePullPatchIntoWorkingTree() error {
|
||||
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)
|
||||
})
|
||||
pull := func(stash bool) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
|
||||
commitIndex := gui.getPatchCommitIndex()
|
||||
err := gui.GitCommand.PullPatchIntoIndex(gui.State.Commits, commitIndex, gui.GitCommand.PatchManager, stash)
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
}
|
||||
|
||||
if len(gui.trackedFiles()) > 0 {
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.SLocalize("MustStashTitle"),
|
||||
prompt: gui.Tr.SLocalize("MustStashWarning"),
|
||||
handleConfirm: func() error {
|
||||
return pull(true)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
return pull(false)
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) handleApplyPatch() error {
|
||||
func (gui *Gui) handlePullPatchIntoNewCommit() error {
|
||||
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.returnFocusFromLineByLinePanelIfNecessary(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.GitCommand.PatchManager.ApplyPatches(false); err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
|
||||
commitIndex := gui.getPatchCommitIndex()
|
||||
err := gui.GitCommand.PullPatchIntoNewCommit(gui.State.Commits, commitIndex, gui.GitCommand.PatchManager)
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleApplyPatch(reverse bool) error {
|
||||
if err := gui.returnFocusFromLineByLinePanelIfNecessary(); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.refreshSidePanels(gui.g)
|
||||
|
||||
if err := gui.GitCommand.PatchManager.ApplyPatches(reverse); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleResetPatch() error {
|
||||
gui.GitCommand.PatchManager.Reset()
|
||||
if gui.currentContextKeyIgnoringPopups() == MAIN_PATCH_BUILDING_CONTEXT_KEY {
|
||||
if err := gui.switchContext(gui.Contexts.CommitFiles.Context); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return gui.refreshCommitFilesView()
|
||||
}
|
||||
|
||||
@@ -10,23 +10,29 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func GetBranchListDisplayStrings(branches []*commands.Branch, fullDescription bool) [][]string {
|
||||
func GetBranchListDisplayStrings(branches []*commands.Branch, fullDescription bool, diffName string) [][]string {
|
||||
lines := make([][]string, len(branches))
|
||||
|
||||
for i := range branches {
|
||||
lines[i] = getBranchDisplayStrings(branches[i], fullDescription)
|
||||
diffed := branches[i].Name == diffName
|
||||
lines[i] = getBranchDisplayStrings(branches[i], fullDescription, diffed)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
// getBranchDisplayStrings returns the display string of branch
|
||||
func getBranchDisplayStrings(b *commands.Branch, fullDescription bool) []string {
|
||||
func getBranchDisplayStrings(b *commands.Branch, fullDescription bool, diffed bool) []string {
|
||||
displayName := b.Name
|
||||
if b.DisplayName != "" {
|
||||
displayName = b.DisplayName
|
||||
}
|
||||
coloredName := utils.ColoredString(displayName, GetBranchColor(b.Name))
|
||||
|
||||
nameColorAttr := GetBranchColor(b.Name)
|
||||
if diffed {
|
||||
nameColorAttr = theme.DiffTerminalColor
|
||||
}
|
||||
coloredName := utils.ColoredString(displayName, nameColorAttr)
|
||||
if b.Pushables != "" && b.Pullables != "" && b.Pushables != "?" && b.Pullables != "?" {
|
||||
trackColor := color.FgYellow
|
||||
if b.Pushables == "0" && b.Pullables == "0" {
|
||||
|
||||
@@ -3,33 +3,61 @@ package presentation
|
||||
import (
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func GetCommitFileListDisplayStrings(branches []*commands.CommitFile) [][]string {
|
||||
lines := make([][]string, len(branches))
|
||||
func GetCommitFileListDisplayStrings(commitFiles []*commands.CommitFile, diffName string) [][]string {
|
||||
if len(commitFiles) == 0 {
|
||||
return [][]string{{utils.ColoredString("(none)", color.FgRed)}}
|
||||
}
|
||||
|
||||
for i := range branches {
|
||||
lines[i] = getCommitFileDisplayStrings(branches[i])
|
||||
lines := make([][]string, len(commitFiles))
|
||||
|
||||
for i := range commitFiles {
|
||||
diffed := commitFiles[i].Name == diffName
|
||||
lines[i] = getCommitFileDisplayStrings(commitFiles[i], diffed)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
// getCommitFileDisplayStrings returns the display string of branch
|
||||
func getCommitFileDisplayStrings(f *commands.CommitFile) []string {
|
||||
func getCommitFileDisplayStrings(f *commands.CommitFile, diffed bool) []string {
|
||||
yellow := color.New(color.FgYellow)
|
||||
green := color.New(color.FgGreen)
|
||||
defaultColor := color.New(theme.DefaultTextColor)
|
||||
diffTerminalColor := color.New(theme.DiffTerminalColor)
|
||||
|
||||
var colour *color.Color
|
||||
switch f.Status {
|
||||
case commands.UNSELECTED:
|
||||
switch f.PatchStatus {
|
||||
case patch.UNSELECTED:
|
||||
colour = defaultColor
|
||||
case commands.WHOLE:
|
||||
case patch.WHOLE:
|
||||
colour = green
|
||||
case commands.PART:
|
||||
case patch.PART:
|
||||
colour = yellow
|
||||
}
|
||||
return []string{colour.Sprint(f.DisplayString)}
|
||||
if diffed {
|
||||
colour = diffTerminalColor
|
||||
}
|
||||
return []string{utils.ColoredString(f.ChangeStatus, getColorForChangeStatus(f.ChangeStatus)), colour.Sprint(f.Name)}
|
||||
}
|
||||
|
||||
func getColorForChangeStatus(changeStatus string) color.Attribute {
|
||||
switch changeStatus {
|
||||
case "A":
|
||||
return color.FgGreen
|
||||
case "M", "R":
|
||||
return color.FgYellow
|
||||
case "D":
|
||||
return color.FgRed
|
||||
case "C":
|
||||
return color.FgCyan
|
||||
case "T":
|
||||
return color.FgMagenta
|
||||
default:
|
||||
return theme.DefaultTextColor
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func GetCommitListDisplayStrings(commits []*commands.Commit, fullDescription bool, cherryPickedCommitShaMap map[string]bool) [][]string {
|
||||
func GetCommitListDisplayStrings(commits []*commands.Commit, fullDescription bool, cherryPickedCommitShaMap map[string]bool, diffName string) [][]string {
|
||||
lines := make([][]string, len(commits))
|
||||
|
||||
var displayFunc func(*commands.Commit, map[string]bool) []string
|
||||
var displayFunc func(*commands.Commit, map[string]bool, bool) []string
|
||||
if fullDescription {
|
||||
displayFunc = getFullDescriptionDisplayStringsForCommit
|
||||
} else {
|
||||
@@ -20,20 +20,21 @@ func GetCommitListDisplayStrings(commits []*commands.Commit, fullDescription boo
|
||||
}
|
||||
|
||||
for i := range commits {
|
||||
lines[i] = displayFunc(commits[i], cherryPickedCommitShaMap)
|
||||
diffed := commits[i].Sha == diffName
|
||||
lines[i] = displayFunc(commits[i], cherryPickedCommitShaMap, diffed)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
func getFullDescriptionDisplayStringsForCommit(c *commands.Commit, cherryPickedCommitShaMap map[string]bool) []string {
|
||||
func getFullDescriptionDisplayStringsForCommit(c *commands.Commit, cherryPickedCommitShaMap map[string]bool, diffed bool) []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)
|
||||
diffedColor := color.New(theme.DiffTerminalColor)
|
||||
|
||||
// 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,
|
||||
@@ -52,19 +53,18 @@ func getFullDescriptionDisplayStringsForCommit(c *commands.Commit, cherryPickedC
|
||||
shaColor = blue
|
||||
case "reflog":
|
||||
shaColor = blue
|
||||
case "selected":
|
||||
shaColor = magenta
|
||||
default:
|
||||
shaColor = defaultColor
|
||||
}
|
||||
|
||||
if cherryPickedCommitShaMap[c.Sha] {
|
||||
if diffed {
|
||||
shaColor = diffedColor
|
||||
} else if cherryPickedCommitShaMap[c.Sha] {
|
||||
shaColor = copied
|
||||
}
|
||||
|
||||
tagString := ""
|
||||
truncatedDate := utils.TruncateWithEllipsis(c.Date, 15)
|
||||
secondColumnString := blue.Sprint(truncatedDate)
|
||||
secondColumnString := blue.Sprint(utils.UnixToDate(c.UnixTimestamp))
|
||||
if c.Action != "" {
|
||||
secondColumnString = cyan.Sprint(c.Action)
|
||||
} else if c.ExtraInfo != "" {
|
||||
@@ -77,14 +77,14 @@ func getFullDescriptionDisplayStringsForCommit(c *commands.Commit, cherryPickedC
|
||||
return []string{shaColor.Sprint(c.ShortSha()), secondColumnString, yellow.Sprint(truncatedAuthor), tagString + defaultColor.Sprint(c.Name)}
|
||||
}
|
||||
|
||||
func getDisplayStringsForCommit(c *commands.Commit, cherryPickedCommitShaMap map[string]bool) []string {
|
||||
func getDisplayStringsForCommit(c *commands.Commit, cherryPickedCommitShaMap map[string]bool, diffed bool) []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)
|
||||
diffedColor := color.New(theme.DiffTerminalColor)
|
||||
|
||||
// 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,
|
||||
@@ -103,13 +103,13 @@ func getDisplayStringsForCommit(c *commands.Commit, cherryPickedCommitShaMap map
|
||||
shaColor = blue
|
||||
case "reflog":
|
||||
shaColor = blue
|
||||
case "selected":
|
||||
shaColor = magenta
|
||||
default:
|
||||
shaColor = defaultColor
|
||||
}
|
||||
|
||||
if cherryPickedCommitShaMap[c.Sha] {
|
||||
if diffed {
|
||||
shaColor = diffedColor
|
||||
} else if cherryPickedCommitShaMap[c.Sha] {
|
||||
shaColor = copied
|
||||
}
|
||||
|
||||
|
||||
@@ -3,34 +3,55 @@ package presentation
|
||||
import (
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
)
|
||||
|
||||
func GetFileListDisplayStrings(files []*commands.File) [][]string {
|
||||
func GetFileListDisplayStrings(files []*commands.File, diffName string) [][]string {
|
||||
lines := make([][]string, len(files))
|
||||
|
||||
for i := range files {
|
||||
lines[i] = getFileDisplayStrings(files[i])
|
||||
diffed := files[i].Name == diffName
|
||||
lines[i] = getFileDisplayStrings(files[i], diffed)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
// getFileDisplayStrings returns the display string of branch
|
||||
func getFileDisplayStrings(f *commands.File) []string {
|
||||
func getFileDisplayStrings(f *commands.File, diffed bool) []string {
|
||||
// potentially inefficient to be instantiating these color
|
||||
// objects with each render
|
||||
red := color.New(color.FgRed)
|
||||
green := color.New(color.FgGreen)
|
||||
diffColor := color.New(theme.DiffTerminalColor)
|
||||
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)
|
||||
var restColor *color.Color
|
||||
if diffed {
|
||||
restColor = diffColor
|
||||
} else if f.HasUnstagedChanges {
|
||||
restColor = red
|
||||
} else {
|
||||
output += green.Sprint(f.Name)
|
||||
restColor = green
|
||||
}
|
||||
|
||||
// this is just making things look nice when the background attribute is 'reverse'
|
||||
firstChar := f.DisplayString[0:1]
|
||||
firstCharCl := green
|
||||
if firstChar == " " {
|
||||
firstCharCl = restColor
|
||||
}
|
||||
|
||||
secondChar := f.DisplayString[1:2]
|
||||
secondCharCl := red
|
||||
if secondChar == " " {
|
||||
secondCharCl = restColor
|
||||
}
|
||||
|
||||
output := firstCharCl.Sprint(firstChar)
|
||||
output += secondCharCl.Sprint(secondChar)
|
||||
output += restColor.Sprintf(" %s", f.Name)
|
||||
return []string{output}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func GetReflogCommitListDisplayStrings(commits []*commands.Commit, fullDescription bool) [][]string {
|
||||
func GetReflogCommitListDisplayStrings(commits []*commands.Commit, fullDescription bool, cherryPickedCommitShaMap map[string]bool, diffName string) [][]string {
|
||||
lines := make([][]string, len(commits))
|
||||
|
||||
var displayFunc func(*commands.Commit) []string
|
||||
var displayFunc func(*commands.Commit, map[string]bool, bool) []string
|
||||
if fullDescription {
|
||||
displayFunc = getFullDescriptionDisplayStringsForReflogCommit
|
||||
} else {
|
||||
@@ -18,20 +18,42 @@ func GetReflogCommitListDisplayStrings(commits []*commands.Commit, fullDescripti
|
||||
}
|
||||
|
||||
for i := range commits {
|
||||
lines[i] = displayFunc(commits[i])
|
||||
diffed := commits[i].Sha == diffName
|
||||
lines[i] = displayFunc(commits[i], cherryPickedCommitShaMap, diffed)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
func getFullDescriptionDisplayStringsForReflogCommit(c *commands.Commit) []string {
|
||||
defaultColor := color.New(theme.DefaultTextColor)
|
||||
func coloredReflogSha(c *commands.Commit, cherryPickedCommitShaMap map[string]bool) string {
|
||||
var shaColor *color.Color
|
||||
if cherryPickedCommitShaMap[c.Sha] {
|
||||
shaColor = color.New(color.FgCyan, color.BgBlue)
|
||||
} else {
|
||||
shaColor = color.New(color.FgBlue)
|
||||
}
|
||||
|
||||
return []string{utils.ColoredString(c.ShortSha(), color.FgBlue), utils.ColoredString(c.Date, color.FgMagenta), defaultColor.Sprint(c.Name)}
|
||||
return shaColor.Sprint(c.ShortSha())
|
||||
}
|
||||
|
||||
func getDisplayStringsForReflogCommit(c *commands.Commit) []string {
|
||||
func getFullDescriptionDisplayStringsForReflogCommit(c *commands.Commit, cherryPickedCommitShaMap map[string]bool, diffed bool) []string {
|
||||
colorAttr := theme.DefaultTextColor
|
||||
if diffed {
|
||||
colorAttr = theme.DiffTerminalColor
|
||||
}
|
||||
|
||||
return []string{
|
||||
coloredReflogSha(c, cherryPickedCommitShaMap),
|
||||
utils.ColoredString(utils.UnixToDate(c.UnixTimestamp), color.FgMagenta),
|
||||
utils.ColoredString(c.Name, colorAttr),
|
||||
}
|
||||
}
|
||||
|
||||
func getDisplayStringsForReflogCommit(c *commands.Commit, cherryPickedCommitShaMap map[string]bool, diffed bool) []string {
|
||||
defaultColor := color.New(theme.DefaultTextColor)
|
||||
|
||||
return []string{utils.ColoredString(c.ShortSha(), color.FgBlue), defaultColor.Sprint(c.Name)}
|
||||
return []string{
|
||||
coloredReflogSha(c, cherryPickedCommitShaMap),
|
||||
defaultColor.Sprint(c.Name),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,22 +2,29 @@ package presentation
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func GetRemoteBranchListDisplayStrings(branches []*commands.RemoteBranch) [][]string {
|
||||
func GetRemoteBranchListDisplayStrings(branches []*commands.RemoteBranch, diffName string) [][]string {
|
||||
lines := make([][]string, len(branches))
|
||||
|
||||
for i := range branches {
|
||||
lines[i] = getRemoteBranchDisplayStrings(branches[i])
|
||||
diffed := branches[i].FullName() == diffName
|
||||
lines[i] = getRemoteBranchDisplayStrings(branches[i], diffed)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
// getRemoteBranchDisplayStrings returns the display string of branch
|
||||
func getRemoteBranchDisplayStrings(b *commands.RemoteBranch) []string {
|
||||
displayName := utils.ColoredString(b.Name, GetBranchColor(b.Name))
|
||||
func getRemoteBranchDisplayStrings(b *commands.RemoteBranch, diffed bool) []string {
|
||||
nameColorAttr := GetBranchColor(b.Name)
|
||||
if diffed {
|
||||
nameColorAttr = theme.DiffTerminalColor
|
||||
}
|
||||
|
||||
displayName := utils.ColoredString(b.Name, nameColorAttr)
|
||||
|
||||
return []string{displayName}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,33 @@
|
||||
package presentation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func GetRemoteListDisplayStrings(remotes []*commands.Remote) [][]string {
|
||||
func GetRemoteListDisplayStrings(remotes []*commands.Remote, diffName string) [][]string {
|
||||
lines := make([][]string, len(remotes))
|
||||
|
||||
for i := range remotes {
|
||||
lines[i] = getRemoteDisplayStrings(remotes[i])
|
||||
diffed := remotes[i].Name == diffName
|
||||
lines[i] = getRemoteDisplayStrings(remotes[i], diffed)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
// getRemoteDisplayStrings returns the display string of branch
|
||||
func getRemoteDisplayStrings(r *commands.Remote) []string {
|
||||
return []string{r.Name}
|
||||
func getRemoteDisplayStrings(r *commands.Remote, diffed bool) []string {
|
||||
branchCount := len(r.Branches)
|
||||
|
||||
nameColorAttr := theme.DefaultTextColor
|
||||
if diffed {
|
||||
nameColorAttr = theme.DiffTerminalColor
|
||||
}
|
||||
|
||||
return []string{utils.ColoredString(r.Name, nameColorAttr), utils.ColoredString(fmt.Sprintf("%d branches", branchCount), color.FgBlue)}
|
||||
}
|
||||
|
||||
@@ -2,19 +2,26 @@ package presentation
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func GetStashEntryListDisplayStrings(stashEntries []*commands.StashEntry) [][]string {
|
||||
func GetStashEntryListDisplayStrings(stashEntries []*commands.StashEntry, diffName string) [][]string {
|
||||
lines := make([][]string, len(stashEntries))
|
||||
|
||||
for i := range stashEntries {
|
||||
lines[i] = getStashEntryDisplayStrings(stashEntries[i])
|
||||
diffed := stashEntries[i].RefName() == diffName
|
||||
lines[i] = getStashEntryDisplayStrings(stashEntries[i], diffed)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
// getStashEntryDisplayStrings returns the display string of branch
|
||||
func getStashEntryDisplayStrings(s *commands.StashEntry) []string {
|
||||
return []string{s.DisplayString}
|
||||
func getStashEntryDisplayStrings(s *commands.StashEntry, diffed bool) []string {
|
||||
attr := theme.DefaultTextColor
|
||||
if diffed {
|
||||
attr = theme.DiffTerminalColor
|
||||
}
|
||||
return []string{utils.ColoredString(s.Name, attr)}
|
||||
}
|
||||
|
||||
@@ -2,19 +2,26 @@ package presentation
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func GetTagListDisplayStrings(tags []*commands.Tag) [][]string {
|
||||
func GetTagListDisplayStrings(tags []*commands.Tag, diffName string) [][]string {
|
||||
lines := make([][]string, len(tags))
|
||||
|
||||
for i := range tags {
|
||||
lines[i] = getTagDisplayStrings(tags[i])
|
||||
diffed := tags[i].Name == diffName
|
||||
lines[i] = getTagDisplayStrings(tags[i], diffed)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
// getTagDisplayStrings returns the display string of branch
|
||||
func getTagDisplayStrings(t *commands.Tag) []string {
|
||||
return []string{t.Name}
|
||||
func getTagDisplayStrings(t *commands.Tag, diffed bool) []string {
|
||||
attr := theme.DefaultTextColor
|
||||
if diffed {
|
||||
attr = theme.DiffTerminalColor
|
||||
}
|
||||
return []string{utils.ColoredString(t.Name, attr)}
|
||||
}
|
||||
|
||||
+36
-9
@@ -26,22 +26,49 @@ func (gui *Gui) recordCurrentDirectory() error {
|
||||
|
||||
func (gui *Gui) handleQuitWithoutChangingDirectory(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.State.RetainOriginalDir = true
|
||||
return gui.quit(v)
|
||||
return gui.quit()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleQuit(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleQuit() error {
|
||||
gui.State.RetainOriginalDir = false
|
||||
return gui.quit(v)
|
||||
return gui.quit()
|
||||
}
|
||||
|
||||
func (gui *Gui) quit(v *gocui.View) error {
|
||||
if gui.State.Updating {
|
||||
return gui.createUpdateQuitConfirmation(gui.g, v)
|
||||
func (gui *Gui) handleTopLevelReturn(g *gocui.Gui, v *gocui.View) error {
|
||||
currentContext := gui.currentContext()
|
||||
|
||||
parentContext, hasParent := currentContext.GetParentContext()
|
||||
if hasParent && currentContext != nil && parentContext != nil {
|
||||
// TODO: think about whether this should be marked as a return rather than adding to the stack
|
||||
return gui.switchContext(parentContext)
|
||||
}
|
||||
|
||||
for _, mode := range gui.modeStatuses() {
|
||||
if mode.isActive() {
|
||||
return mode.reset()
|
||||
}
|
||||
}
|
||||
|
||||
if gui.Config.GetUserConfig().GetBool("quitOnTopLevelReturn") {
|
||||
return gui.handleQuit()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) quit() error {
|
||||
if gui.State.Updating {
|
||||
return gui.createUpdateQuitConfirmation()
|
||||
}
|
||||
|
||||
if gui.Config.GetUserConfig().GetBool("confirmOnQuit") {
|
||||
return gui.createConfirmationPanel(gui.g, v, true, "", gui.Tr.SLocalize("ConfirmQuit"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gocui.ErrQuit
|
||||
}, nil)
|
||||
return gui.ask(askOpts{
|
||||
title: "",
|
||||
prompt: gui.Tr.SLocalize("ConfirmQuit"),
|
||||
handleConfirm: func() error {
|
||||
return gocui.ErrQuit
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return gocui.ErrQuit
|
||||
|
||||
@@ -3,14 +3,12 @@ package gui
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
func (gui *Gui) handleCreateRebaseOptionsMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCreateRebaseOptionsMenu() error {
|
||||
options := []string{"continue", "abort"}
|
||||
|
||||
if gui.State.WorkingTreeState == "rebasing" {
|
||||
if gui.GitCommand.WorkingTreeState() == "rebasing" {
|
||||
options = append(options, "skip")
|
||||
}
|
||||
|
||||
@@ -27,7 +25,7 @@ func (gui *Gui) handleCreateRebaseOptionsMenu(g *gocui.Gui, v *gocui.View) error
|
||||
}
|
||||
|
||||
var title string
|
||||
if gui.State.WorkingTreeState == "merging" {
|
||||
if gui.GitCommand.WorkingTreeState() == "merging" {
|
||||
title = gui.Tr.SLocalize("MergeOptionsTitle")
|
||||
} else {
|
||||
title = gui.Tr.SLocalize("RebaseOptionsTitle")
|
||||
@@ -37,10 +35,10 @@ func (gui *Gui) handleCreateRebaseOptionsMenu(g *gocui.Gui, v *gocui.View) error
|
||||
}
|
||||
|
||||
func (gui *Gui) genericMergeCommand(command string) error {
|
||||
status := gui.State.WorkingTreeState
|
||||
status := gui.GitCommand.WorkingTreeState()
|
||||
|
||||
if status != "merging" && status != "rebasing" {
|
||||
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NotMergingOrRebasing"))
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("NotMergingOrRebasing"))
|
||||
}
|
||||
|
||||
commandType := strings.Replace(status, "ing", "e", 1)
|
||||
@@ -63,7 +61,7 @@ func (gui *Gui) genericMergeCommand(command string) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleGenericMergeCommandResult(result error) error {
|
||||
if err := gui.refreshSidePanels(gui.g); err != nil {
|
||||
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil {
|
||||
return err
|
||||
}
|
||||
if result == nil {
|
||||
@@ -74,15 +72,26 @@ func (gui *Gui) handleGenericMergeCommandResult(result error) error {
|
||||
return gui.genericMergeCommand("skip")
|
||||
} else if strings.Contains(result.Error(), "The previous cherry-pick is now empty") {
|
||||
return gui.genericMergeCommand("continue")
|
||||
} else if strings.Contains(result.Error(), "No rebase in progress?") {
|
||||
// assume in this case that we're already done
|
||||
return nil
|
||||
} else if strings.Contains(result.Error(), "When you have resolved this problem") || strings.Contains(result.Error(), "fix conflicts") || strings.Contains(result.Error(), "Resolve all conflicts manually") {
|
||||
return gui.createConfirmationPanel(gui.g, gui.getFilesView(), true, gui.Tr.SLocalize("FoundConflictsTitle"), gui.Tr.SLocalize("FoundConflicts"),
|
||||
func(g *gocui.Gui, v *gocui.View) error {
|
||||
return nil
|
||||
}, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.SLocalize("FoundConflictsTitle"),
|
||||
prompt: gui.Tr.SLocalize("FoundConflicts"),
|
||||
handlersManageFocus: true,
|
||||
handleConfirm: func() error {
|
||||
return gui.switchContext(gui.Contexts.Files.Context)
|
||||
},
|
||||
handleClose: func() error {
|
||||
if err := gui.returnFromContext(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.genericMergeCommand("abort")
|
||||
},
|
||||
)
|
||||
})
|
||||
} else {
|
||||
return gui.createErrorPanel(gui.g, result.Error())
|
||||
return gui.createErrorPanel(result.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,11 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func (gui *Gui) handleCreateRecentReposMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCreateRecentReposMenu() error {
|
||||
recentRepoPaths := gui.Config.GetAppState().RecentRepos
|
||||
reposCount := utils.Min(len(recentRepoPaths), 20)
|
||||
yellow := color.New(color.FgMagenta)
|
||||
@@ -32,6 +31,7 @@ func (gui *Gui) handleCreateRecentReposMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
return err
|
||||
}
|
||||
gui.GitCommand = newGitCommand
|
||||
gui.State.Modes.Filtering.Path = ""
|
||||
return gui.Errors.ErrSwitchRepo
|
||||
},
|
||||
}
|
||||
|
||||
+75
-60
@@ -3,82 +3,84 @@ package gui
|
||||
import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
)
|
||||
|
||||
// list panel functions
|
||||
|
||||
func (gui *Gui) getSelectedReflogCommit() *commands.Commit {
|
||||
selectedLine := gui.State.Panels.ReflogCommits.SelectedLine
|
||||
if selectedLine == -1 || len(gui.State.ReflogCommits) == 0 {
|
||||
selectedLine := gui.State.Panels.ReflogCommits.SelectedLineIdx
|
||||
reflogComits := gui.State.FilteredReflogCommits
|
||||
if selectedLine == -1 || len(reflogComits) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.State.ReflogCommits[selectedLine]
|
||||
return reflogComits[selectedLine]
|
||||
}
|
||||
|
||||
func (gui *Gui) handleReflogCommitSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.popupPanelFocused() {
|
||||
func (gui *Gui) handleReflogCommitSelect() error {
|
||||
commit := gui.getSelectedReflogCommit()
|
||||
var task updateTask
|
||||
if commit == nil {
|
||||
task = gui.createRenderStringTask("No reflog history")
|
||||
} else {
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.Modes.Filtering.Path),
|
||||
)
|
||||
|
||||
task = gui.createRunPtyTask(cmd)
|
||||
}
|
||||
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
main: &viewUpdateOpts{
|
||||
title: "Reflog Entry",
|
||||
task: task,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// the reflogs panel is the only panel where we cache data, in that we only
|
||||
// load entries that have been created since we last ran the call. This means
|
||||
// we need to be more careful with how we use this, and to ensure we're emptying
|
||||
// the reflogs array when changing contexts.
|
||||
// This method also manages two things: ReflogCommits and FilteredReflogCommits.
|
||||
// FilteredReflogCommits are rendered in the reflogs panel, and ReflogCommits
|
||||
// are used by the branches panel to obtain recency values for sorting.
|
||||
func (gui *Gui) refreshReflogCommits() error {
|
||||
// pulling state into its own variable incase it gets swapped out for another state
|
||||
// and we get an out of bounds exception
|
||||
state := gui.State
|
||||
var lastReflogCommit *commands.Commit
|
||||
if len(state.ReflogCommits) > 0 {
|
||||
lastReflogCommit = state.ReflogCommits[0]
|
||||
}
|
||||
|
||||
refresh := func(stateCommits *[]*commands.Commit, filterPath string) error {
|
||||
commits, onlyObtainedNewReflogCommits, err := gui.GitCommand.GetReflogCommits(lastReflogCommit, filterPath)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
if onlyObtainedNewReflogCommits {
|
||||
*stateCommits = append(commits, *stateCommits...)
|
||||
} else {
|
||||
*stateCommits = commits
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.State.SplitMainPanel = false
|
||||
|
||||
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
|
||||
if err := refresh(&state.ReflogCommits, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.getMainView().Title = "Reflog Entry"
|
||||
|
||||
commit := gui.getSelectedReflogCommit()
|
||||
if commit == nil {
|
||||
return gui.newStringTask("main", "No reflog history")
|
||||
}
|
||||
v.FocusPoint(0, gui.State.Panels.ReflogCommits.SelectedLine)
|
||||
|
||||
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) refreshReflogCommits() error {
|
||||
var lastReflogCommit *commands.Commit
|
||||
if len(gui.State.ReflogCommits) > 0 {
|
||||
lastReflogCommit = gui.State.ReflogCommits[0]
|
||||
}
|
||||
|
||||
commits, err := gui.GitCommand.GetNewReflogCommits(lastReflogCommit)
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
}
|
||||
|
||||
gui.State.ReflogCommits = append(commits, gui.State.ReflogCommits...)
|
||||
|
||||
if gui.getCommitsView().Context == "reflog-commits" {
|
||||
return gui.renderReflogCommitsWithSelection()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) renderReflogCommitsWithSelection() error {
|
||||
commitsView := gui.getCommitsView()
|
||||
|
||||
gui.refreshSelectedLine(&gui.State.Panels.ReflogCommits.SelectedLine, len(gui.State.ReflogCommits))
|
||||
displayStrings := presentation.GetReflogCommitListDisplayStrings(gui.State.ReflogCommits, gui.State.ScreenMode != SCREEN_NORMAL)
|
||||
gui.renderDisplayStrings(commitsView, displayStrings)
|
||||
if gui.g.CurrentView() == commitsView && commitsView.Context == "reflog-commits" {
|
||||
if err := gui.handleReflogCommitSelect(gui.g, commitsView); err != nil {
|
||||
if gui.State.Modes.Filtering.Active() {
|
||||
if err := refresh(&state.FilteredReflogCommits, state.Modes.Filtering.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
state.FilteredReflogCommits = state.ReflogCommits
|
||||
}
|
||||
|
||||
return nil
|
||||
return gui.postRefreshUpdate(gui.Contexts.ReflogCommits.Context)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCheckoutReflogCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
@@ -87,14 +89,18 @@ func (gui *Gui) handleCheckoutReflogCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := 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, handleCheckoutRefOptions{})
|
||||
}, nil)
|
||||
err := gui.ask(askOpts{
|
||||
title: gui.Tr.SLocalize("checkoutCommit"),
|
||||
prompt: gui.Tr.SLocalize("SureCheckoutThisCommit"),
|
||||
handleConfirm: func() error {
|
||||
return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{})
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.State.Panels.ReflogCommits.SelectedLine = 0
|
||||
gui.State.Panels.ReflogCommits.SelectedLineIdx = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -104,3 +110,12 @@ func (gui *Gui) handleCreateReflogResetMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
|
||||
return gui.createResetMenu(commit.Sha)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleViewReflogCommitFiles() error {
|
||||
commit := gui.getSelectedReflogCommit()
|
||||
if commit == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.switchToCommitFilesContext(commit.Sha, false, gui.Contexts.ReflogCommits.Context, "commits")
|
||||
}
|
||||
|
||||
@@ -5,13 +5,12 @@ import (
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
)
|
||||
|
||||
// list panel functions
|
||||
|
||||
func (gui *Gui) getSelectedRemoteBranch() *commands.RemoteBranch {
|
||||
selectedLine := gui.State.Panels.RemoteBranches.SelectedLine
|
||||
selectedLine := gui.State.Panels.RemoteBranches.SelectedLineIdx
|
||||
if selectedLine == -1 || len(gui.State.RemoteBranches) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -19,71 +18,32 @@ func (gui *Gui) getSelectedRemoteBranch() *commands.RemoteBranch {
|
||||
return gui.State.RemoteBranches[selectedLine]
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRemoteBranchSelect(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 = "Remote Branch"
|
||||
|
||||
remote := gui.getSelectedRemote()
|
||||
func (gui *Gui) handleRemoteBranchSelect() error {
|
||||
var task updateTask
|
||||
remoteBranch := gui.getSelectedRemoteBranch()
|
||||
if remoteBranch == nil {
|
||||
return gui.newStringTask("main", "No branches for this remote")
|
||||
task = gui.createRenderStringTask("No branches for this remote")
|
||||
} else {
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.GetBranchGraphCmdStr(remoteBranch.FullName()),
|
||||
)
|
||||
task = gui.createRunCommandTask(cmd)
|
||||
}
|
||||
|
||||
v.FocusPoint(0, gui.State.Panels.RemoteBranches.SelectedLine)
|
||||
|
||||
branchName := fmt.Sprintf("%s/%s", remote.Name, remoteBranch.Name)
|
||||
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.GetBranchGraphCmdStr(branchName),
|
||||
)
|
||||
if err := gui.newCmdTask("main", cmd); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
main: &viewUpdateOpts{
|
||||
title: "Remote Branch",
|
||||
task: task,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRemoteBranchesEscape(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.switchBranchesPanelContext("remotes")
|
||||
}
|
||||
|
||||
func (gui *Gui) renderRemoteBranchesWithSelection() error {
|
||||
branchesView := gui.getBranchesView()
|
||||
|
||||
gui.refreshSelectedLine(&gui.State.Panels.RemoteBranches.SelectedLine, len(gui.State.RemoteBranches))
|
||||
displayStrings := presentation.GetRemoteBranchListDisplayStrings(gui.State.RemoteBranches)
|
||||
gui.renderDisplayStrings(branchesView, displayStrings)
|
||||
if gui.g.CurrentView() == branchesView && branchesView.Context == "remote-branches" {
|
||||
if err := gui.handleRemoteBranchSelect(gui.g, branchesView); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCheckoutRemoteBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
remoteBranch := gui.getSelectedRemoteBranch()
|
||||
if remoteBranch == nil {
|
||||
return nil
|
||||
}
|
||||
if err := gui.handleCheckoutRef(remoteBranch.RemoteName+"/"+remoteBranch.Name, handleCheckoutRefOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.switchBranchesPanelContext("local-branches")
|
||||
return gui.switchContext(gui.Contexts.Remotes.Context)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMergeRemoteBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
selectedBranchName := gui.getSelectedRemoteBranch().Name
|
||||
selectedBranchName := gui.getSelectedRemoteBranch().FullName()
|
||||
return gui.mergeBranchIntoCheckedOutBranch(selectedBranchName)
|
||||
}
|
||||
|
||||
@@ -92,20 +52,25 @@ func (gui *Gui) handleDeleteRemoteBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
if remoteBranch == nil {
|
||||
return nil
|
||||
}
|
||||
message := fmt.Sprintf("%s '%s/%s'?", gui.Tr.SLocalize("DeleteRemoteBranchMessage"), remoteBranch.RemoteName, remoteBranch.Name)
|
||||
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("DeleteRemoteBranch"), message, func(*gocui.Gui, *gocui.View) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.SLocalize("DeletingStatus"), func() error {
|
||||
if err := gui.GitCommand.DeleteRemoteBranch(remoteBranch.RemoteName, remoteBranch.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
message := fmt.Sprintf("%s '%s'?", gui.Tr.SLocalize("DeleteRemoteBranchMessage"), remoteBranch.FullName())
|
||||
|
||||
return gui.refreshRemotes()
|
||||
})
|
||||
}, nil)
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.SLocalize("DeleteRemoteBranch"),
|
||||
prompt: message,
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.SLocalize("DeletingStatus"), func() error {
|
||||
if err := gui.GitCommand.DeleteRemoteBranch(remoteBranch.RemoteName, remoteBranch.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRebaseOntoRemoteBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
selectedBranchName := gui.getSelectedRemoteBranch().Name
|
||||
selectedBranchName := gui.getSelectedRemoteBranch().FullName()
|
||||
return gui.handleRebaseOntoBranch(selectedBranchName)
|
||||
}
|
||||
|
||||
@@ -117,17 +82,21 @@ func (gui *Gui) handleSetBranchUpstream(g *gocui.Gui, v *gocui.View) error {
|
||||
"SetUpstreamMessage",
|
||||
Teml{
|
||||
"checkedOut": checkedOutBranch.Name,
|
||||
"selected": selectedBranch.RemoteName + "/" + selectedBranch.Name,
|
||||
"selected": selectedBranch.FullName(),
|
||||
},
|
||||
)
|
||||
|
||||
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("SetUpstreamTitle"), message, func(*gocui.Gui, *gocui.View) error {
|
||||
if err := gui.GitCommand.SetBranchUpstream(selectedBranch.RemoteName, selectedBranch.Name, checkedOutBranch.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.SLocalize("SetUpstreamTitle"),
|
||||
prompt: message,
|
||||
handleConfirm: func() error {
|
||||
if err := gui.GitCommand.SetBranchUpstream(selectedBranch.RemoteName, selectedBranch.Name, checkedOutBranch.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(gui.g)
|
||||
}, nil)
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateResetToRemoteBranchMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
@@ -136,5 +105,5 @@ func (gui *Gui) handleCreateResetToRemoteBranchMenu(g *gocui.Gui, v *gocui.View)
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.createResetMenu(fmt.Sprintf("%s/%s", selectedBranch.RemoteName, selectedBranch.Name))
|
||||
return gui.createResetMenu(selectedBranch.FullName())
|
||||
}
|
||||
|
||||
+45
-70
@@ -7,14 +7,13 @@ import (
|
||||
"github.com/fatih/color"
|
||||
"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) getSelectedRemote() *commands.Remote {
|
||||
selectedLine := gui.State.Panels.Remotes.SelectedLine
|
||||
selectedLine := gui.State.Panels.Remotes.SelectedLineIdx
|
||||
if selectedLine == -1 || len(gui.State.Remotes) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -22,26 +21,21 @@ func (gui *Gui) getSelectedRemote() *commands.Remote {
|
||||
return gui.State.Remotes[selectedLine]
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRemoteSelect(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 = "Remote"
|
||||
|
||||
func (gui *Gui) handleRemoteSelect() error {
|
||||
var task updateTask
|
||||
remote := gui.getSelectedRemote()
|
||||
if remote == nil {
|
||||
return gui.newStringTask("main", "No remotes")
|
||||
task = gui.createRenderStringTask("No remotes")
|
||||
} else {
|
||||
task = gui.createRenderStringTask(fmt.Sprintf("%s\nUrls:\n%s", utils.ColoredString(remote.Name, color.FgGreen), strings.Join(remote.Urls, "\n")))
|
||||
}
|
||||
v.FocusPoint(0, gui.State.Panels.Remotes.SelectedLine)
|
||||
|
||||
return gui.newStringTask("main", fmt.Sprintf("%s\nUrls:\n%s", utils.ColoredString(remote.Name, color.FgGreen), strings.Join(remote.Urls, "\n")))
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
main: &viewUpdateOpts{
|
||||
title: "Remote",
|
||||
task: task,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshRemotes() error {
|
||||
@@ -49,7 +43,7 @@ func (gui *Gui) refreshRemotes() error {
|
||||
|
||||
remotes, err := gui.GitCommand.GetRemotes()
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
gui.State.Remotes = remotes
|
||||
@@ -64,35 +58,10 @@ func (gui *Gui) refreshRemotes() error {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: see if this works for deleting remote branches
|
||||
switch gui.getBranchesView().Context {
|
||||
case "remotes":
|
||||
return gui.renderRemotesWithSelection()
|
||||
case "remote-branches":
|
||||
return gui.renderRemoteBranchesWithSelection()
|
||||
}
|
||||
|
||||
return nil
|
||||
return gui.postRefreshUpdate(gui.contextForContextKey(gui.getBranchesView().Context))
|
||||
}
|
||||
|
||||
func (gui *Gui) renderRemotesWithSelection() error {
|
||||
branchesView := gui.getBranchesView()
|
||||
|
||||
gui.refreshSelectedLine(&gui.State.Panels.Remotes.SelectedLine, len(gui.State.Remotes))
|
||||
|
||||
displayStrings := presentation.GetRemoteListDisplayStrings(gui.State.Remotes)
|
||||
gui.renderDisplayStrings(branchesView, displayStrings)
|
||||
|
||||
if gui.g.CurrentView() == branchesView && branchesView.Context == "remotes" {
|
||||
if err := gui.handleRemoteSelect(gui.g, branchesView); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRemoteEnter(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleRemoteEnter() error {
|
||||
// naive implementation: get the branches and render them to the list, change the context
|
||||
remote := gui.getSelectedRemote()
|
||||
if remote == nil {
|
||||
@@ -105,21 +74,18 @@ func (gui *Gui) handleRemoteEnter(g *gocui.Gui, v *gocui.View) error {
|
||||
if len(remote.Branches) == 0 {
|
||||
newSelectedLine = -1
|
||||
}
|
||||
gui.State.Panels.RemoteBranches.SelectedLine = newSelectedLine
|
||||
gui.State.Panels.RemoteBranches.SelectedLineIdx = newSelectedLine
|
||||
|
||||
return gui.switchBranchesPanelContext("remote-branches")
|
||||
return gui.switchContext(gui.Contexts.Remotes.Branches.Context)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleAddRemote(g *gocui.Gui, v *gocui.View) error {
|
||||
branchesView := gui.getBranchesView()
|
||||
return gui.createPromptPanel(g, branchesView, gui.Tr.SLocalize("newRemoteName"), "", func(g *gocui.Gui, v *gocui.View) error {
|
||||
remoteName := gui.trimmedContent(v)
|
||||
return gui.createPromptPanel(g, branchesView, gui.Tr.SLocalize("newRemoteUrl"), "", func(g *gocui.Gui, v *gocui.View) error {
|
||||
remoteUrl := gui.trimmedContent(v)
|
||||
return gui.prompt(gui.Tr.SLocalize("newRemoteName"), "", func(remoteName string) error {
|
||||
return gui.prompt(gui.Tr.SLocalize("newRemoteUrl"), "", func(remoteUrl string) error {
|
||||
if err := gui.GitCommand.AddRemote(remoteName, remoteUrl); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.refreshRemotes()
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{REMOTES}})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -129,18 +95,21 @@ func (gui *Gui) handleRemoveRemote(g *gocui.Gui, v *gocui.View) error {
|
||||
if remote == nil {
|
||||
return nil
|
||||
}
|
||||
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("removeRemote"), gui.Tr.SLocalize("removeRemotePrompt")+" '"+remote.Name+"'?", func(*gocui.Gui, *gocui.View) error {
|
||||
if err := gui.GitCommand.RemoveRemote(remote.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.refreshRemotes()
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.SLocalize("removeRemote"),
|
||||
prompt: gui.Tr.SLocalize("removeRemotePrompt") + " '" + remote.Name + "'?",
|
||||
handleConfirm: func() error {
|
||||
if err := gui.GitCommand.RemoveRemote(remote.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}, nil)
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleEditRemote(g *gocui.Gui, v *gocui.View) error {
|
||||
branchesView := gui.getBranchesView()
|
||||
remote := gui.getSelectedRemote()
|
||||
if remote == nil {
|
||||
return nil
|
||||
@@ -153,12 +122,10 @@ func (gui *Gui) handleEditRemote(g *gocui.Gui, v *gocui.View) error {
|
||||
},
|
||||
)
|
||||
|
||||
return gui.createPromptPanel(g, branchesView, editNameMessage, "", func(g *gocui.Gui, v *gocui.View) error {
|
||||
updatedRemoteName := gui.trimmedContent(v)
|
||||
|
||||
return gui.prompt(editNameMessage, remote.Name, func(updatedRemoteName string) error {
|
||||
if updatedRemoteName != remote.Name {
|
||||
if err := gui.GitCommand.RenameRemote(remote.Name, updatedRemoteName); err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,12 +136,17 @@ func (gui *Gui) handleEditRemote(g *gocui.Gui, v *gocui.View) error {
|
||||
},
|
||||
)
|
||||
|
||||
return gui.createPromptPanel(g, branchesView, editUrlMessage, "", func(g *gocui.Gui, v *gocui.View) error {
|
||||
updatedRemoteUrl := gui.trimmedContent(v)
|
||||
urls := remote.Urls
|
||||
url := ""
|
||||
if len(urls) > 0 {
|
||||
url = urls[0]
|
||||
}
|
||||
|
||||
return gui.prompt(editUrlMessage, url, func(updatedRemoteUrl string) error {
|
||||
if err := gui.GitCommand.UpdateRemoteUrl(updatedRemoteName, updatedRemoteUrl); err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshRemotes()
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -186,10 +158,13 @@ func (gui *Gui) handleFetchRemote(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.SLocalize("FetchingRemoteStatus"), func() error {
|
||||
gui.State.FetchMutex.Lock()
|
||||
defer gui.State.FetchMutex.Unlock()
|
||||
|
||||
if err := gui.GitCommand.FetchRemote(remote.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.refreshRemotes()
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,32 +9,23 @@ import (
|
||||
|
||||
func (gui *Gui) resetToRef(ref string, strength string, options commands.RunCommandOptions) error {
|
||||
if err := gui.GitCommand.ResetToCommit(ref, strength, options); err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
if err := gui.switchCommitsPanelContext("branch-commits"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.State.Panels.Commits.SelectedLine = 0
|
||||
gui.State.Panels.ReflogCommits.SelectedLine = 0
|
||||
gui.State.Panels.Commits.SelectedLineIdx = 0
|
||||
gui.State.Panels.ReflogCommits.SelectedLineIdx = 0
|
||||
// loading a heap of commits is slow so we limit them whenever doing a reset
|
||||
gui.State.Panels.Commits.LimitCommits = true
|
||||
|
||||
if err := gui.refreshCommits(gui.g); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.refreshFiles(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.refreshBranches(gui.g); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.resetOrigin(gui.getCommitsView()); err != nil {
|
||||
if err := gui.switchContext(gui.Contexts.BranchCommits.Context); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.handleCommitSelect(gui.g, gui.getCommitsView())
|
||||
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES, BRANCHES, REFLOG, COMMITS}}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) createResetMenu(ref string) error {
|
||||
|
||||
+17
-8
@@ -11,8 +11,10 @@ import (
|
||||
func (gui *Gui) handleOpenSearch(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.State.Searching.isSearching = true
|
||||
gui.State.Searching.view = v
|
||||
gui.renderString(gui.g, "search", "")
|
||||
if err := gui.switchFocus(gui.g, v, gui.getSearchView()); err != nil {
|
||||
|
||||
gui.renderString("search", "")
|
||||
|
||||
if err := gui.switchContext(gui.Contexts.Search.Context); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -21,11 +23,16 @@ func (gui *Gui) handleOpenSearch(g *gocui.Gui, v *gocui.View) error {
|
||||
|
||||
func (gui *Gui) handleSearch(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.State.Searching.searchString = gui.getSearchView().Buffer()
|
||||
if err := gui.switchFocus(gui.g, nil, gui.State.Searching.view); err != nil {
|
||||
if err := gui.returnFromContext(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.State.Searching.view.Search(gui.State.Searching.searchString); err != nil {
|
||||
view := gui.State.Searching.view
|
||||
if view == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := view.Search(gui.State.Searching.searchString); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -36,7 +43,6 @@ func (gui *Gui) onSelectItemWrapper(innerFunc func(int) error) func(int, int, in
|
||||
return func(y int, index int, total int) error {
|
||||
if total == 0 {
|
||||
gui.renderString(
|
||||
gui.g,
|
||||
"search",
|
||||
fmt.Sprintf(
|
||||
"no matches for '%s' %s",
|
||||
@@ -50,7 +56,6 @@ func (gui *Gui) onSelectItemWrapper(innerFunc func(int) error) func(int, int, in
|
||||
return nil
|
||||
}
|
||||
gui.renderString(
|
||||
gui.g,
|
||||
"search",
|
||||
fmt.Sprintf(
|
||||
"matches for '%s' (%d of %d) %s",
|
||||
@@ -86,9 +91,13 @@ func (gui *Gui) onSearchEscape() error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSearchEscape(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.switchFocus(gui.g, nil, gui.State.Searching.view); err != nil {
|
||||
if err := gui.onSearchEscape(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.onSearchEscape()
|
||||
if err := gui.returnFromContext(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package gui
|
||||
|
||||
import "github.com/jesseduffield/gocui"
|
||||
|
||||
func (gui *Gui) nextSideWindow() error {
|
||||
windows := gui.getCyclableWindows()
|
||||
currentWindow := gui.currentWindow()
|
||||
var newWindow string
|
||||
if currentWindow == "" || currentWindow == windows[len(windows)-1] {
|
||||
newWindow = windows[0]
|
||||
} else {
|
||||
for i := range windows {
|
||||
if currentWindow == windows[i] {
|
||||
newWindow = windows[i+1]
|
||||
break
|
||||
}
|
||||
if i == len(windows)-1 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := gui.resetOrigin(gui.getMainView()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
viewName := gui.getViewNameForWindow(newWindow)
|
||||
|
||||
return gui.switchContextToView(viewName)
|
||||
}
|
||||
|
||||
func (gui *Gui) previousSideWindow() error {
|
||||
windows := gui.getCyclableWindows()
|
||||
currentWindow := gui.currentWindow()
|
||||
var newWindow string
|
||||
if currentWindow == "" || currentWindow == windows[0] {
|
||||
newWindow = windows[len(windows)-1]
|
||||
} else {
|
||||
for i := range windows {
|
||||
if currentWindow == windows[i] {
|
||||
newWindow = windows[i-1]
|
||||
break
|
||||
}
|
||||
if i == len(windows)-1 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := gui.resetOrigin(gui.getMainView()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
viewName := gui.getViewNameForWindow(newWindow)
|
||||
|
||||
return gui.switchContextToView(viewName)
|
||||
}
|
||||
|
||||
func (gui *Gui) goToSideWindow(sideViewName string) func(g *gocui.Gui, v *gocui.View) error {
|
||||
return func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.switchContextToView(sideViewName)
|
||||
}
|
||||
}
|
||||
+35
-33
@@ -4,31 +4,21 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
)
|
||||
|
||||
func (gui *Gui) refreshStagingPanel(forceSecondaryFocused bool, selectedLineIdx int) error {
|
||||
gui.State.SplitMainPanel = true
|
||||
gui.splitMainPanel(true)
|
||||
|
||||
state := gui.State.Panels.LineByLine
|
||||
|
||||
// We need to force focus here because the confirmation panel for safely staging lines does not return focus automatically.
|
||||
// This is because if we tell it to return focus it will unconditionally return it to the main panel which may not be what we want
|
||||
// e.g. in the event that there's nothing left to stage.
|
||||
if err := gui.switchFocus(gui.g, nil, gui.getMainView()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := gui.getSelectedFile(gui.g)
|
||||
if err != nil {
|
||||
if err != gui.Errors.ErrNoFiles {
|
||||
return err
|
||||
}
|
||||
return gui.handleStagingEscape(gui.g, nil)
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
return gui.handleStagingEscape()
|
||||
}
|
||||
|
||||
if !file.HasUnstagedChanges && !file.HasStagedChanges {
|
||||
return gui.handleStagingEscape(gui.g, nil)
|
||||
return gui.handleStagingEscape()
|
||||
}
|
||||
|
||||
secondaryFocused := false
|
||||
@@ -53,14 +43,14 @@ func (gui *Gui) refreshStagingPanel(forceSecondaryFocused bool, selectedLineIdx
|
||||
}
|
||||
|
||||
// note for custom diffs, we'll need to send a flag here saying not to use the custom diff
|
||||
diff := gui.GitCommand.Diff(file, true, secondaryFocused)
|
||||
secondaryDiff := gui.GitCommand.Diff(file, true, !secondaryFocused)
|
||||
diff := gui.GitCommand.WorktreeFileDiff(file, true, secondaryFocused)
|
||||
secondaryDiff := gui.GitCommand.WorktreeFileDiff(file, true, !secondaryFocused)
|
||||
|
||||
// if we have e.g. a deleted file with nothing else to the diff will have only
|
||||
// 4-5 lines in which case we'll swap panels
|
||||
if len(strings.Split(diff, "\n")) < 5 {
|
||||
if len(strings.Split(secondaryDiff, "\n")) < 5 {
|
||||
return gui.handleStagingEscape(gui.g, nil)
|
||||
return gui.handleStagingEscape()
|
||||
}
|
||||
secondaryFocused = !secondaryFocused
|
||||
diff, secondaryDiff = secondaryDiff, diff
|
||||
@@ -72,7 +62,7 @@ func (gui *Gui) refreshStagingPanel(forceSecondaryFocused bool, selectedLineIdx
|
||||
}
|
||||
|
||||
if empty {
|
||||
return gui.handleStagingEscape(gui.g, nil)
|
||||
return gui.handleStagingEscape()
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -93,10 +83,10 @@ func (gui *Gui) handleTogglePanel(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.refreshStagingPanel(false, -1)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStagingEscape(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleStagingEscape() error {
|
||||
gui.handleEscapeLineByLinePanel()
|
||||
|
||||
return gui.switchFocus(gui.g, nil, gui.getFilesView())
|
||||
return gui.switchContext(gui.Contexts.Files.Context)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleToggleStagedSelection(g *gocui.Gui, v *gocui.View) error {
|
||||
@@ -113,10 +103,22 @@ func (gui *Gui) handleResetSelection(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.applySelection(true)
|
||||
}
|
||||
|
||||
if gui.Config.GetUserConfig().GetBool("gui.skipUnstageLineWarning") {
|
||||
return gui.createConfirmationPanel(gui.g, gui.getMainView(), false, "unstage lines", "Are you sure you want to delete the selected lines (git reset)? It is irreversible.\nTo disable this dialogue set the config key of 'gui.skipUnstageLineWarning' to true", func(*gocui.Gui, *gocui.View) error {
|
||||
return gui.applySelection(true)
|
||||
}, nil)
|
||||
if !gui.Config.GetUserConfig().GetBool("gui.skipUnstageLineWarning") {
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.SLocalize("UnstageLinesTitle"),
|
||||
prompt: gui.Tr.SLocalize("UnstageLinesPrompt"),
|
||||
handlersManageFocus: true,
|
||||
handleConfirm: func() error {
|
||||
if err := gui.switchContext(gui.Contexts.Staging.Context); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.applySelection(true)
|
||||
},
|
||||
handleClose: func() error {
|
||||
return gui.switchContext(gui.Contexts.Staging.Context)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
return gui.applySelection(true)
|
||||
}
|
||||
@@ -125,12 +127,12 @@ func (gui *Gui) handleResetSelection(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) applySelection(reverse bool) error {
|
||||
state := gui.State.Panels.LineByLine
|
||||
|
||||
file, err := gui.getSelectedFile(gui.g)
|
||||
if err != nil {
|
||||
return err
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
patch := commands.ModifiedPatchForRange(gui.Log, file.Name, state.Diff, state.FirstLineIdx, state.LastLineIdx, reverse, false)
|
||||
patch := patch.ModifiedPatchForRange(gui.Log, file.Name, state.Diff, state.FirstLineIdx, state.LastLineIdx, reverse, false)
|
||||
|
||||
if patch == "" {
|
||||
return nil
|
||||
@@ -142,16 +144,16 @@ func (gui *Gui) applySelection(reverse bool) error {
|
||||
if !reverse || state.SecondaryFocused {
|
||||
applyFlags = append(applyFlags, "cached")
|
||||
}
|
||||
err = gui.GitCommand.ApplyPatch(patch, applyFlags...)
|
||||
err := gui.GitCommand.ApplyPatch(patch, applyFlags...)
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
if state.SelectMode == RANGE {
|
||||
state.SelectMode = LINE
|
||||
}
|
||||
|
||||
if err := gui.refreshFiles(); err != nil {
|
||||
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.refreshStagingPanel(false, -1); err != nil {
|
||||
|
||||
+78
-69
@@ -3,13 +3,12 @@ package gui
|
||||
import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
)
|
||||
|
||||
// list panel functions
|
||||
|
||||
func (gui *Gui) getSelectedStashEntry(v *gocui.View) *commands.StashEntry {
|
||||
selectedLine := gui.State.Panels.Stash.SelectedLine
|
||||
func (gui *Gui) getSelectedStashEntry() *commands.StashEntry {
|
||||
selectedLine := gui.State.Panels.Stash.SelectedLineIdx
|
||||
if selectedLine == -1 {
|
||||
return nil
|
||||
}
|
||||
@@ -17,74 +16,86 @@ func (gui *Gui) getSelectedStashEntry(v *gocui.View) *commands.StashEntry {
|
||||
return gui.State.StashEntries[selectedLine]
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStashEntrySelect(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 = "Stash"
|
||||
|
||||
stashEntry := gui.getSelectedStashEntry(v)
|
||||
func (gui *Gui) handleStashEntrySelect() error {
|
||||
var task updateTask
|
||||
stashEntry := gui.getSelectedStashEntry()
|
||||
if stashEntry == nil {
|
||||
return gui.newStringTask("main", gui.Tr.SLocalize("NoStashEntries"))
|
||||
}
|
||||
v.FocusPoint(0, gui.State.Panels.Stash.SelectedLine)
|
||||
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.ShowStashEntryCmdStr(stashEntry.Index),
|
||||
)
|
||||
if err := gui.newPtyTask("main", cmd); err != nil {
|
||||
gui.Log.Error(err)
|
||||
task = gui.createRenderStringTask(gui.Tr.SLocalize("NoStashEntries"))
|
||||
} else {
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.ShowStashEntryCmdStr(stashEntry.Index),
|
||||
)
|
||||
task = gui.createRunPtyTask(cmd)
|
||||
}
|
||||
|
||||
return nil
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
main: &viewUpdateOpts{
|
||||
title: "Stash",
|
||||
task: task,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshStashEntries(g *gocui.Gui) error {
|
||||
g.Update(func(g *gocui.Gui) error {
|
||||
gui.State.StashEntries = gui.GitCommand.GetStashEntries()
|
||||
func (gui *Gui) refreshStashEntries() error {
|
||||
gui.State.StashEntries = gui.GitCommand.GetStashEntries(gui.State.Modes.Filtering.Path)
|
||||
|
||||
gui.refreshSelectedLine(&gui.State.Panels.Stash.SelectedLine, len(gui.State.StashEntries))
|
||||
|
||||
stashView := gui.getStashView()
|
||||
|
||||
displayStrings := presentation.GetStashEntryListDisplayStrings(gui.State.StashEntries)
|
||||
gui.renderDisplayStrings(stashView, displayStrings)
|
||||
|
||||
if err := gui.resetOrigin(stashView); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
return gui.Contexts.Stash.Context.HandleRender()
|
||||
}
|
||||
|
||||
// specific functions
|
||||
|
||||
func (gui *Gui) handleStashApply(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.stashDo(g, v, "apply")
|
||||
skipStashWarning := gui.Config.GetUserConfig().GetBool("gui.skipStashWarning")
|
||||
|
||||
apply := func() error {
|
||||
return gui.stashDo("apply")
|
||||
}
|
||||
|
||||
if skipStashWarning {
|
||||
return apply()
|
||||
}
|
||||
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.SLocalize("StashApply"),
|
||||
prompt: gui.Tr.SLocalize("SureApplyStashEntry"),
|
||||
handleConfirm: func() error {
|
||||
return apply()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStashPop(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.stashDo(g, v, "pop")
|
||||
skipStashWarning := gui.Config.GetUserConfig().GetBool("gui.skipStashWarning")
|
||||
|
||||
pop := func() error {
|
||||
return gui.stashDo("pop")
|
||||
}
|
||||
|
||||
if skipStashWarning {
|
||||
return pop()
|
||||
}
|
||||
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.SLocalize("StashPop"),
|
||||
prompt: gui.Tr.SLocalize("SurePopStashEntry"),
|
||||
handleConfirm: func() error {
|
||||
return pop()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStashDrop(g *gocui.Gui, v *gocui.View) error {
|
||||
title := gui.Tr.SLocalize("StashDrop")
|
||||
message := gui.Tr.SLocalize("SureDropStashEntry")
|
||||
return gui.createConfirmationPanel(g, v, true, title, message, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.stashDo(g, v, "drop")
|
||||
}, nil)
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.SLocalize("StashDrop"),
|
||||
prompt: gui.Tr.SLocalize("SureDropStashEntry"),
|
||||
handleConfirm: func() error {
|
||||
return gui.stashDo("drop")
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) stashDo(g *gocui.Gui, v *gocui.View, method string) error {
|
||||
stashEntry := gui.getSelectedStashEntry(v)
|
||||
func (gui *Gui) stashDo(method string) error {
|
||||
stashEntry := gui.getSelectedStashEntry()
|
||||
if stashEntry == nil {
|
||||
errorMessage := gui.Tr.TemplateLocalize(
|
||||
"NoStashTo",
|
||||
@@ -92,33 +103,31 @@ func (gui *Gui) stashDo(g *gocui.Gui, v *gocui.View, method string) error {
|
||||
"method": method,
|
||||
},
|
||||
)
|
||||
return gui.createErrorPanel(g, errorMessage)
|
||||
return gui.createErrorPanel(errorMessage)
|
||||
}
|
||||
if err := gui.GitCommand.StashDo(stashEntry.Index, method); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
if err := gui.refreshStashEntries(g); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
return gui.refreshFiles()
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{STASH, FILES}})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStashSave(stashFunc func(message string) error) error {
|
||||
if len(gui.trackedFiles()) == 0 && len(gui.stagedFiles()) == 0 {
|
||||
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NoTrackedStagedFilesStash"))
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("NoTrackedStagedFilesStash"))
|
||||
}
|
||||
return gui.createPromptPanel(gui.g, gui.getFilesView(), gui.Tr.SLocalize("StashChanges"), "", func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := stashFunc(gui.trimmedContent(v)); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
return gui.prompt(gui.Tr.SLocalize("StashChanges"), "", func(stashComment string) error {
|
||||
if err := stashFunc(stashComment); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
if err := gui.refreshStashEntries(g); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
return gui.refreshFiles()
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{STASH, FILES}})
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) onStashPanelSearchSelect(selectedLine int) error {
|
||||
gui.State.Panels.Stash.SelectedLine = selectedLine
|
||||
return gui.handleStashEntrySelect(gui.g, gui.getStashView())
|
||||
func (gui *Gui) handleViewStashFiles() error {
|
||||
stashEntry := gui.getSelectedStashEntry()
|
||||
if stashEntry == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.switchToCommitFilesContext(stashEntry.RefName(), false, gui.Contexts.Stash.Context, "stash")
|
||||
}
|
||||
|
||||
+68
-76
@@ -10,50 +10,41 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func (gui *Gui) refreshStatus(g *gocui.Gui) error {
|
||||
state := gui.State.Panels.Status
|
||||
// never call this on its own, it should only be called from within refreshCommits()
|
||||
func (gui *Gui) refreshStatus() {
|
||||
gui.State.RefreshingStatusMutex.Lock()
|
||||
defer gui.State.RefreshingStatusMutex.Unlock()
|
||||
|
||||
v, err := g.View("status")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
currentBranch := gui.currentBranch()
|
||||
if currentBranch == nil {
|
||||
// need to wait for branches to refresh
|
||||
return
|
||||
}
|
||||
// for some reason if this isn't wrapped in an update the clear seems to
|
||||
// be applied after the other things or something like that; the panel's
|
||||
// contents end up cleared
|
||||
g.Update(func(*gocui.Gui) error {
|
||||
v.Clear()
|
||||
// TODO: base this off of the current branch
|
||||
state.pushables, state.pullables = gui.GitCommand.GetCurrentBranchUpstreamDifferenceCount()
|
||||
if err := gui.updateWorkTreeState(); err != nil {
|
||||
return err
|
||||
}
|
||||
status := ""
|
||||
|
||||
if currentBranch.Pushables != "" && currentBranch.Pullables != "" {
|
||||
trackColor := color.FgYellow
|
||||
if state.pushables == "0" && state.pullables == "0" {
|
||||
if currentBranch.Pushables == "0" && currentBranch.Pullables == "0" {
|
||||
trackColor = color.FgGreen
|
||||
} else if state.pushables == "?" && state.pullables == "?" {
|
||||
} else if currentBranch.Pushables == "?" && currentBranch.Pullables == "?" {
|
||||
trackColor = color.FgRed
|
||||
}
|
||||
|
||||
status := utils.ColoredString(fmt.Sprintf("↑%s↓%s", state.pushables, state.pullables), trackColor)
|
||||
branches := gui.State.Branches
|
||||
status = utils.ColoredString(fmt.Sprintf("↑%s↓%s ", currentBranch.Pushables, currentBranch.Pullables), trackColor)
|
||||
}
|
||||
|
||||
if gui.State.WorkingTreeState != "normal" {
|
||||
status += utils.ColoredString(fmt.Sprintf(" (%s)", gui.State.WorkingTreeState), color.FgYellow)
|
||||
}
|
||||
if gui.GitCommand.WorkingTreeState() != "normal" {
|
||||
status += utils.ColoredString(fmt.Sprintf("(%s) ", gui.GitCommand.WorkingTreeState()), color.FgYellow)
|
||||
}
|
||||
|
||||
if len(branches) > 0 {
|
||||
branch := branches[0]
|
||||
name := utils.ColoredString(branch.Name, presentation.GetBranchColor(branch.Name))
|
||||
repoName := utils.GetCurrentRepoName()
|
||||
status += fmt.Sprintf(" %s → %s", repoName, name)
|
||||
}
|
||||
name := utils.ColoredString(currentBranch.Name, presentation.GetBranchColor(currentBranch.Name))
|
||||
repoName := utils.GetCurrentRepoName()
|
||||
status += fmt.Sprintf("%s → %s ", repoName, name)
|
||||
|
||||
fmt.Fprint(v, status)
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
gui.setViewContent(gui.getStatusView(), status)
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runeCount(str string) int {
|
||||
@@ -66,46 +57,51 @@ func cursorInSubstring(cx int, prefix string, substring string) bool {
|
||||
|
||||
func (gui *Gui) handleCheckForUpdate(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.Updater.CheckForNewUpdate(gui.onUserUpdateCheckFinish, true)
|
||||
return gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("CheckingForUpdates"))
|
||||
return gui.createLoaderPanel(v, gui.Tr.SLocalize("CheckingForUpdates"))
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStatusClick(g *gocui.Gui, v *gocui.View) error {
|
||||
state := gui.State.Panels.Status
|
||||
|
||||
cx, _ := v.Cursor()
|
||||
upstreamStatus := fmt.Sprintf("↑%s↓%s", state.pushables, state.pullables)
|
||||
repoName := utils.GetCurrentRepoName()
|
||||
gui.Log.Warn(gui.State.WorkingTreeState)
|
||||
switch gui.State.WorkingTreeState {
|
||||
case "rebasing", "merging":
|
||||
workingTreeStatus := fmt.Sprintf("(%s)", gui.State.WorkingTreeState)
|
||||
if cursorInSubstring(cx, upstreamStatus+" ", workingTreeStatus) {
|
||||
return gui.handleCreateRebaseOptionsMenu(gui.g, v)
|
||||
}
|
||||
if cursorInSubstring(cx, upstreamStatus+" "+workingTreeStatus+" ", repoName) {
|
||||
return gui.handleCreateRecentReposMenu(gui.g, v)
|
||||
}
|
||||
default:
|
||||
if cursorInSubstring(cx, upstreamStatus+" ", repoName) {
|
||||
return gui.handleCreateRecentReposMenu(gui.g, v)
|
||||
}
|
||||
}
|
||||
|
||||
return gui.handleStatusSelect(gui.g, v)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStatusSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
// TODO: move into some abstraction (status is currently not a listViewContext where a lot of this code lives)
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.State.SplitMainPanel = false
|
||||
currentBranch := gui.currentBranch()
|
||||
if currentBranch == nil {
|
||||
// need to wait for branches to refresh
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
|
||||
if err := gui.switchContext(gui.Contexts.Status.Context); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.getMainView().Title = ""
|
||||
cx, _ := v.Cursor()
|
||||
upstreamStatus := fmt.Sprintf("↑%s↓%s", currentBranch.Pushables, currentBranch.Pullables)
|
||||
repoName := utils.GetCurrentRepoName()
|
||||
switch gui.GitCommand.WorkingTreeState() {
|
||||
case "rebasing", "merging":
|
||||
workingTreeStatus := fmt.Sprintf("(%s)", gui.GitCommand.WorkingTreeState())
|
||||
if cursorInSubstring(cx, upstreamStatus+" ", workingTreeStatus) {
|
||||
return gui.handleCreateRebaseOptionsMenu()
|
||||
}
|
||||
if cursorInSubstring(cx, upstreamStatus+" "+workingTreeStatus+" ", repoName) {
|
||||
return gui.handleCreateRecentReposMenu()
|
||||
}
|
||||
default:
|
||||
if cursorInSubstring(cx, upstreamStatus+" ", repoName) {
|
||||
return gui.handleCreateRecentReposMenu()
|
||||
}
|
||||
}
|
||||
|
||||
return gui.handleStatusSelect()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStatusSelect() error {
|
||||
// TODO: move into some abstraction (status is currently not a listViewContext where a lot of this code lives)
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
magenta := color.New(color.FgMagenta)
|
||||
|
||||
@@ -120,7 +116,12 @@ func (gui *Gui) handleStatusSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
magenta.Sprint("Become a sponsor (github is matching all donations for 12 months): https://github.com/sponsors/jesseduffield"), // caffeine ain't free
|
||||
}, "\n\n")
|
||||
|
||||
return gui.newStringTask("main", dashboardString)
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
main: &viewUpdateOpts{
|
||||
title: "",
|
||||
task: gui.createRenderStringTask(dashboardString),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleOpenConfig(g *gocui.Gui, v *gocui.View) error {
|
||||
@@ -144,23 +145,14 @@ func lazygitTitle() string {
|
||||
|___/ |___/ `
|
||||
}
|
||||
|
||||
func (gui *Gui) updateWorkTreeState() error {
|
||||
rebaseMode, err := gui.GitCommand.RebaseMode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
func (gui *Gui) workingTreeState() string {
|
||||
rebaseMode, _ := gui.GitCommand.RebaseMode()
|
||||
if rebaseMode != "" {
|
||||
gui.State.WorkingTreeState = "rebasing"
|
||||
return nil
|
||||
}
|
||||
merging, err := gui.GitCommand.IsInMergeState()
|
||||
if err != nil {
|
||||
return err
|
||||
return "rebasing"
|
||||
}
|
||||
merging, _ := gui.GitCommand.IsInMergeState()
|
||||
if merging {
|
||||
gui.State.WorkingTreeState = "merging"
|
||||
return nil
|
||||
return "merging"
|
||||
}
|
||||
gui.State.WorkingTreeState = "normal"
|
||||
return nil
|
||||
return "normal"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
)
|
||||
|
||||
// list panel functions
|
||||
|
||||
func (gui *Gui) getSelectedSubCommit() *commands.Commit {
|
||||
selectedLine := gui.State.Panels.SubCommits.SelectedLineIdx
|
||||
commits := gui.State.SubCommits
|
||||
if selectedLine == -1 || len(commits) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return commits[selectedLine]
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSubCommitSelect() error {
|
||||
commit := gui.getSelectedSubCommit()
|
||||
var task updateTask
|
||||
if commit == nil {
|
||||
task = gui.createRenderStringTask("No commits")
|
||||
} else {
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.Modes.Filtering.Path),
|
||||
)
|
||||
|
||||
task = gui.createRunPtyTask(cmd)
|
||||
}
|
||||
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
main: &viewUpdateOpts{
|
||||
title: "Commit",
|
||||
task: task,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCheckoutSubCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
commit := gui.getSelectedSubCommit()
|
||||
if commit == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := gui.ask(askOpts{
|
||||
title: gui.Tr.SLocalize("checkoutCommit"),
|
||||
prompt: gui.Tr.SLocalize("SureCheckoutThisCommit"),
|
||||
handleConfirm: func() error {
|
||||
return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{})
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.State.Panels.SubCommits.SelectedLineIdx = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateSubCommitResetMenu() error {
|
||||
commit := gui.getSelectedSubCommit()
|
||||
|
||||
return gui.createResetMenu(commit.Sha)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleViewSubCommitFiles() error {
|
||||
commit := gui.getSelectedSubCommit()
|
||||
if commit == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.switchToCommitFilesContext(commit.Sha, false, gui.Contexts.SubCommits.Context, "branches")
|
||||
}
|
||||
|
||||
func (gui *Gui) switchToSubCommitsContext(refName string) error {
|
||||
// need to populate my sub commits
|
||||
builder := commands.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr, gui.State.Modes.CherryPicking.CherryPickedCommits)
|
||||
|
||||
commits, err := builder.GetCommits(
|
||||
commands.GetCommitsOptions{
|
||||
Limit: gui.State.Panels.Commits.LimitCommits,
|
||||
FilterPath: gui.State.Modes.Filtering.Path,
|
||||
IncludeRebaseCommits: false,
|
||||
RefName: refName,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.State.SubCommits = commits
|
||||
gui.State.Panels.SubCommits.refName = refName
|
||||
gui.State.Panels.SubCommits.SelectedLineIdx = 0
|
||||
gui.Contexts.SubCommits.Context.SetParentContext(gui.currentSideContext())
|
||||
|
||||
return gui.switchContext(gui.Contexts.SubCommits.Context)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSwitchToSubCommits() error {
|
||||
currentContext := gui.currentSideContext()
|
||||
if currentContext == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.switchToSubCommitsContext(currentContext.GetSelectedItemId())
|
||||
}
|
||||
+49
-73
@@ -3,13 +3,12 @@ package gui
|
||||
import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
)
|
||||
|
||||
// list panel functions
|
||||
|
||||
func (gui *Gui) getSelectedTag() *commands.Tag {
|
||||
selectedLine := gui.State.Panels.Tags.SelectedLine
|
||||
selectedLine := gui.State.Panels.Tags.SelectedLineIdx
|
||||
if selectedLine == -1 || len(gui.State.Tags) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -17,63 +16,35 @@ func (gui *Gui) getSelectedTag() *commands.Tag {
|
||||
return gui.State.Tags[selectedLine]
|
||||
}
|
||||
|
||||
func (gui *Gui) handleTagSelect(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 = "Tag"
|
||||
|
||||
func (gui *Gui) handleTagSelect() error {
|
||||
var task updateTask
|
||||
tag := gui.getSelectedTag()
|
||||
if tag == nil {
|
||||
return gui.newStringTask("main", "No tags")
|
||||
}
|
||||
v.FocusPoint(0, gui.State.Panels.Tags.SelectedLine)
|
||||
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.GetBranchGraphCmdStr(tag.Name),
|
||||
)
|
||||
if err := gui.newCmdTask("main", cmd); err != nil {
|
||||
gui.Log.Error(err)
|
||||
task = gui.createRenderStringTask("No tags")
|
||||
} else {
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.GetBranchGraphCmdStr(tag.Name),
|
||||
)
|
||||
task = gui.createRunCommandTask(cmd)
|
||||
}
|
||||
|
||||
return nil
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
main: &viewUpdateOpts{
|
||||
title: "Tag",
|
||||
task: task,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshTags() error {
|
||||
tags, err := gui.GitCommand.GetTags()
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
gui.State.Tags = tags
|
||||
|
||||
if gui.getBranchesView().Context == "tags" {
|
||||
return gui.renderTagsWithSelection()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) renderTagsWithSelection() error {
|
||||
branchesView := gui.getBranchesView()
|
||||
|
||||
gui.refreshSelectedLine(&gui.State.Panels.Tags.SelectedLine, len(gui.State.Tags))
|
||||
displayStrings := presentation.GetTagListDisplayStrings(gui.State.Tags)
|
||||
gui.renderDisplayStrings(branchesView, displayStrings)
|
||||
if gui.g.CurrentView() == branchesView && branchesView.Context == "tags" {
|
||||
if err := gui.handleTagSelect(gui.g, branchesView); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return gui.postRefreshUpdate(gui.Contexts.Tags.Context)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCheckoutTag(g *gocui.Gui, v *gocui.View) error {
|
||||
@@ -84,7 +55,7 @@ func (gui *Gui) handleCheckoutTag(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.handleCheckoutRef(tag.Name, handleCheckoutRefOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.switchBranchesPanelContext("local-branches")
|
||||
return gui.switchContext(gui.Contexts.Branches.Context)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleDeleteTag(g *gocui.Gui, v *gocui.View) error {
|
||||
@@ -100,18 +71,16 @@ func (gui *Gui) handleDeleteTag(g *gocui.Gui, v *gocui.View) error {
|
||||
},
|
||||
)
|
||||
|
||||
return gui.createConfirmationPanel(gui.g, v, true, gui.Tr.SLocalize("DeleteTagTitle"), prompt, func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.DeleteTag(tag.Name); err != nil {
|
||||
return gui.createErrorPanel(gui.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 nil
|
||||
}, nil)
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.SLocalize("DeleteTagTitle"),
|
||||
prompt: prompt,
|
||||
handleConfirm: func() error {
|
||||
if err := gui.GitCommand.DeleteTag(tag.Name); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{COMMITS, TAGS}})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handlePushTag(g *gocui.Gui, v *gocui.View) error {
|
||||
@@ -127,27 +96,34 @@ func (gui *Gui) handlePushTag(g *gocui.Gui, v *gocui.View) error {
|
||||
},
|
||||
)
|
||||
|
||||
return gui.createPromptPanel(gui.g, v, title, "origin", func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.PushTag(v.Buffer(), tag.Name); err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.prompt(title, "origin", func(response string) error {
|
||||
if err := gui.GitCommand.PushTag(response, tag.Name); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshTags()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateTag(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.createPromptPanel(gui.g, v, gui.Tr.SLocalize("CreateTagTitle"), "", func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.prompt(gui.Tr.SLocalize("CreateTagTitle"), "", func(tagName string) error {
|
||||
// leaving commit SHA blank so that we're just creating the tag for the current commit
|
||||
if err := gui.GitCommand.CreateLightweightTag(v.Buffer(), ""); err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
if err := gui.GitCommand.CreateLightweightTag(tagName, ""); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
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 nil
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{COMMITS, TAGS}, then: func() {
|
||||
// find the index of the tag and set that as the currently selected line
|
||||
for i, tag := range gui.State.Tags {
|
||||
if tag.Name == tagName {
|
||||
gui.State.Panels.Tags.SelectedLineIdx = i
|
||||
if err := gui.Contexts.Tags.Context.HandleRender(); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,27 @@ func (gui *Gui) newStringTask(viewName string, str string) error {
|
||||
manager := gui.getManager(view)
|
||||
|
||||
f := func(stop chan struct{}) error {
|
||||
gui.renderString(gui.g, viewName, str)
|
||||
gui.renderString(viewName, str)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := manager.NewTask(f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) newStringTaskWithoutScroll(viewName string, str string) error {
|
||||
view, err := gui.g.View(viewName)
|
||||
if err != nil {
|
||||
return nil // swallowing for now
|
||||
}
|
||||
|
||||
manager := gui.getManager(view)
|
||||
|
||||
f := func(stop chan struct{}) error {
|
||||
gui.setViewContent(view, str)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+29
-35
@@ -37,7 +37,7 @@ type reflogAction struct {
|
||||
// Though we might support this later, hence the use of the CURRENT_REBASE action kind.
|
||||
func (gui *Gui) parseReflogForActions(onUserAction func(counter int, action reflogAction) (bool, error)) error {
|
||||
counter := 0
|
||||
reflogCommits := gui.State.ReflogCommits
|
||||
reflogCommits := gui.State.FilteredReflogCommits
|
||||
rebaseFinishCommitSha := ""
|
||||
var action *reflogAction
|
||||
for reflogCommitIdx, reflogCommit := range reflogCommits {
|
||||
@@ -87,8 +87,8 @@ func (gui *Gui) reflogUndo(g *gocui.Gui, v *gocui.View) error {
|
||||
undoEnvVars := []string{"GIT_REFLOG_ACTION=[lazygit undo]"}
|
||||
undoingStatus := gui.Tr.SLocalize("UndoingStatus")
|
||||
|
||||
if gui.State.WorkingTreeState == "rebasing" {
|
||||
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("cantUndoWhileRebasing"))
|
||||
if gui.GitCommand.WorkingTreeState() == "rebasing" {
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("cantUndoWhileRebasing"))
|
||||
}
|
||||
|
||||
return gui.parseReflogForActions(func(counter int, action reflogAction) (bool, error) {
|
||||
@@ -118,8 +118,8 @@ func (gui *Gui) reflogRedo(g *gocui.Gui, v *gocui.View) error {
|
||||
redoEnvVars := []string{"GIT_REFLOG_ACTION=[lazygit redo]"}
|
||||
redoingStatus := gui.Tr.SLocalize("RedoingStatus")
|
||||
|
||||
if gui.State.WorkingTreeState == "rebasing" {
|
||||
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("cantRedoWhileRebasing"))
|
||||
if gui.GitCommand.WorkingTreeState() == "rebasing" {
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("cantRedoWhileRebasing"))
|
||||
}
|
||||
|
||||
return gui.parseReflogForActions(func(counter int, action reflogAction) (bool, error) {
|
||||
@@ -155,48 +155,42 @@ type handleHardResetWithAutoStashOptions struct {
|
||||
|
||||
// only to be used in the undo flow for now
|
||||
func (gui *Gui) handleHardResetWithAutoStash(commitSha string, options handleHardResetWithAutoStashOptions) error {
|
||||
// if we have any modified tracked files we need to ask the user if they want us to stash for them
|
||||
dirtyWorkingTree := false
|
||||
for _, file := range gui.State.Files {
|
||||
if file.Tracked {
|
||||
dirtyWorkingTree = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
reset := func() error {
|
||||
if err := gui.resetToRef(commitSha, "hard", commands.RunCommandOptions{EnvVars: options.EnvVars}); err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// if we have any modified tracked files we need to ask the user if they want us to stash for them
|
||||
dirtyWorkingTree := len(gui.trackedFiles()) > 0
|
||||
if dirtyWorkingTree {
|
||||
// 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 {
|
||||
return gui.WithWaitingStatus(options.WaitingStatus, func() error {
|
||||
if err := gui.GitCommand.StashSave(gui.Tr.SLocalize("StashPrefix") + commitSha); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
if err := reset(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.GitCommand.StashDo(0, "pop"); err != nil {
|
||||
if err := gui.refreshSidePanels(g); err != nil {
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.SLocalize("AutoStashTitle"),
|
||||
prompt: gui.Tr.SLocalize("AutoStashPrompt"),
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(options.WaitingStatus, func() error {
|
||||
if err := gui.GitCommand.StashSave(gui.Tr.SLocalize("StashPrefix") + commitSha); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
if err := reset(); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
return gui.refreshSidePanels(g)
|
||||
})
|
||||
}, nil)
|
||||
|
||||
if err := gui.GitCommand.StashDo(0, "pop"); err != nil {
|
||||
if err := gui.refreshSidePanels(refreshOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return gui.WithWaitingStatus(options.WaitingStatus, func() error {
|
||||
if err := reset(); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.refreshSidePanels(gui.g)
|
||||
return reset()
|
||||
})
|
||||
}
|
||||
|
||||
+20
-17
@@ -3,21 +3,22 @@ package gui
|
||||
import "github.com/jesseduffield/gocui"
|
||||
|
||||
func (gui *Gui) showUpdatePrompt(newVersion string) error {
|
||||
title := "New version available!"
|
||||
message := "Download latest version? (enter/esc)"
|
||||
currentView := gui.g.CurrentView()
|
||||
return gui.createConfirmationPanel(gui.g, currentView, true, title, message, func(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.startUpdating(newVersion)
|
||||
return nil
|
||||
}, nil)
|
||||
return gui.ask(askOpts{
|
||||
title: "New version available!",
|
||||
prompt: "Download latest version? (enter/esc)",
|
||||
handleConfirm: func() error {
|
||||
gui.startUpdating(newVersion)
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) onUserUpdateCheckFinish(newVersion string, err error) error {
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
if newVersion == "" {
|
||||
return gui.createErrorPanel(gui.g, "New version not found")
|
||||
return gui.createErrorPanel("New version not found")
|
||||
}
|
||||
return gui.showUpdatePrompt(newVersion)
|
||||
}
|
||||
@@ -47,17 +48,19 @@ func (gui *Gui) startUpdating(newVersion string) {
|
||||
func (gui *Gui) onUpdateFinish(err error) error {
|
||||
gui.State.Updating = false
|
||||
gui.statusManager.removeStatus("updating")
|
||||
gui.renderString(gui.g, "appStatus", "")
|
||||
gui.renderString("appStatus", "")
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(gui.g, "Update failed: "+err.Error())
|
||||
return gui.createErrorPanel("Update failed: " + err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) createUpdateQuitConfirmation(g *gocui.Gui, v *gocui.View) error {
|
||||
title := "Currently Updating"
|
||||
message := "An update is in progress. Are you sure you want to quit?"
|
||||
return gui.createConfirmationPanel(gui.g, v, true, title, message, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gocui.ErrQuit
|
||||
}, nil)
|
||||
func (gui *Gui) createUpdateQuitConfirmation() error {
|
||||
return gui.ask(askOpts{
|
||||
title: "Currently Updating",
|
||||
prompt: "An update is in progress. Are you sure you want to quit?",
|
||||
handleConfirm: func() error {
|
||||
return gocui.ErrQuit
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
+213
-256
@@ -4,215 +4,141 @@ import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/spkg/bom"
|
||||
)
|
||||
|
||||
var cyclableViews = []string{"status", "files", "branches", "commits", "stash"}
|
||||
|
||||
func (gui *Gui) refreshSidePanels(g *gocui.Gui) error {
|
||||
if err := gui.refreshBranches(g); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.refreshFiles(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.refreshCommits(g); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.refreshStashEntries(g)
|
||||
func (gui *Gui) getCyclableWindows() []string {
|
||||
return []string{"status", "files", "branches", "commits", "stash"}
|
||||
}
|
||||
|
||||
func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error {
|
||||
var focusedViewName string
|
||||
if v == nil || v.Name() == cyclableViews[len(cyclableViews)-1] {
|
||||
focusedViewName = cyclableViews[0]
|
||||
} else {
|
||||
// if we're in the commitFiles view we'll act like we're in the commits view
|
||||
viewName := v.Name()
|
||||
if viewName == "commitFiles" {
|
||||
viewName = "commits"
|
||||
}
|
||||
for i := range cyclableViews {
|
||||
if viewName == cyclableViews[i] {
|
||||
focusedViewName = cyclableViews[i+1]
|
||||
break
|
||||
}
|
||||
if i == len(cyclableViews)-1 {
|
||||
message := gui.Tr.TemplateLocalize(
|
||||
"IssntListOfViews",
|
||||
Teml{
|
||||
"name": viewName,
|
||||
},
|
||||
)
|
||||
gui.Log.Info(message)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
focusedView, err := g.View(focusedViewName)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return gui.switchFocus(g, v, focusedView)
|
||||
// models/views that we can refresh
|
||||
const (
|
||||
COMMITS = iota
|
||||
BRANCHES
|
||||
FILES
|
||||
STASH
|
||||
REFLOG
|
||||
TAGS
|
||||
REMOTES
|
||||
STATUS
|
||||
)
|
||||
|
||||
const (
|
||||
SYNC = iota // wait until everything is done before returning
|
||||
ASYNC // return immediately, allowing each independent thing to update itself
|
||||
BLOCK_UI // wrap code in an update call to ensure UI updates all at once and keybindings aren't executed till complete
|
||||
)
|
||||
|
||||
type refreshOptions struct {
|
||||
then func()
|
||||
scope []int // e.g. []int{COMMITS, BRANCHES}. Leave empty to refresh everything
|
||||
mode int // one of SYNC (default), ASYNC, and BLOCK_UI
|
||||
}
|
||||
|
||||
func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error {
|
||||
var focusedViewName string
|
||||
if v == nil || v.Name() == cyclableViews[0] {
|
||||
focusedViewName = cyclableViews[len(cyclableViews)-1]
|
||||
} else {
|
||||
// if we're in the commitFiles view we'll act like we're in the commits view
|
||||
viewName := v.Name()
|
||||
if viewName == "commitFiles" {
|
||||
viewName = "commits"
|
||||
}
|
||||
for i := range cyclableViews {
|
||||
if viewName == cyclableViews[i] {
|
||||
focusedViewName = cyclableViews[i-1] // TODO: make this work properly
|
||||
break
|
||||
}
|
||||
if i == len(cyclableViews)-1 {
|
||||
message := gui.Tr.TemplateLocalize(
|
||||
"IssntListOfViews",
|
||||
Teml{
|
||||
"name": viewName,
|
||||
},
|
||||
)
|
||||
gui.Log.Info(message)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
func intArrToMap(arr []int) map[int]bool {
|
||||
output := map[int]bool{}
|
||||
for _, el := range arr {
|
||||
output[el] = true
|
||||
}
|
||||
focusedView, err := g.View(focusedViewName)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return gui.switchFocus(g, v, focusedView)
|
||||
return output
|
||||
}
|
||||
|
||||
func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error {
|
||||
switch v.Name() {
|
||||
case "menu":
|
||||
return gui.handleMenuSelect(g, v)
|
||||
case "status":
|
||||
return gui.handleStatusSelect(g, v)
|
||||
case "files":
|
||||
return gui.focusAndSelectFile(g, v)
|
||||
case "branches":
|
||||
branchesView := gui.getBranchesView()
|
||||
switch branchesView.Context {
|
||||
case "local-branches":
|
||||
return gui.handleBranchSelect(g, v)
|
||||
case "remotes":
|
||||
return gui.handleRemoteSelect(g, v)
|
||||
case "remote-branches":
|
||||
return gui.handleRemoteBranchSelect(g, v)
|
||||
case "tags":
|
||||
return gui.handleTagSelect(g, v)
|
||||
default:
|
||||
return errors.New("unknown branches panel context: " + branchesView.Context)
|
||||
}
|
||||
case "commits":
|
||||
return gui.handleCommitSelect(g, v)
|
||||
case "commitFiles":
|
||||
return gui.handleCommitFileSelect(g, v)
|
||||
case "stash":
|
||||
return gui.handleStashEntrySelect(g, v)
|
||||
case "confirmation":
|
||||
return nil
|
||||
case "commitMessage":
|
||||
return gui.handleCommitFocused(g, v)
|
||||
case "credentials":
|
||||
return gui.handleCredentialsViewFocused(g, v)
|
||||
case "main":
|
||||
if gui.State.MainContext == "merging" {
|
||||
return gui.refreshMergePanel()
|
||||
}
|
||||
v.Highlight = false
|
||||
return nil
|
||||
case "search":
|
||||
return nil
|
||||
default:
|
||||
panic(gui.Tr.SLocalize("NoViewMachingNewLineFocusedSwitchStatement"))
|
||||
}
|
||||
}
|
||||
func (gui *Gui) refreshSidePanels(options refreshOptions) error {
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
func (gui *Gui) returnFocus(g *gocui.Gui, v *gocui.View) error {
|
||||
previousView, err := g.View(gui.State.PreviousView)
|
||||
if err != nil {
|
||||
// always fall back to files view if there's no 'previous' view stored
|
||||
previousView, err = g.View("files")
|
||||
if err != nil {
|
||||
gui.Log.Error(err)
|
||||
f := func() {
|
||||
var scopeMap map[int]bool
|
||||
if len(options.scope) == 0 {
|
||||
scopeMap = intArrToMap([]int{COMMITS, BRANCHES, FILES, STASH, REFLOG, TAGS, REMOTES, STATUS})
|
||||
} else {
|
||||
scopeMap = intArrToMap(options.scope)
|
||||
}
|
||||
|
||||
if scopeMap[COMMITS] || scopeMap[BRANCHES] || scopeMap[REFLOG] {
|
||||
wg.Add(1)
|
||||
func() {
|
||||
if options.mode == ASYNC {
|
||||
go gui.refreshCommits()
|
||||
} else {
|
||||
gui.refreshCommits()
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
if scopeMap[FILES] {
|
||||
wg.Add(1)
|
||||
func() {
|
||||
if options.mode == ASYNC {
|
||||
go gui.refreshFiles()
|
||||
} else {
|
||||
gui.refreshFiles()
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
if scopeMap[STASH] {
|
||||
wg.Add(1)
|
||||
func() {
|
||||
if options.mode == ASYNC {
|
||||
go gui.refreshStashEntries()
|
||||
} else {
|
||||
gui.refreshStashEntries()
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
if scopeMap[TAGS] {
|
||||
wg.Add(1)
|
||||
func() {
|
||||
if options.mode == ASYNC {
|
||||
go gui.refreshTags()
|
||||
} else {
|
||||
gui.refreshTags()
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
if scopeMap[REMOTES] {
|
||||
wg.Add(1)
|
||||
func() {
|
||||
if options.mode == ASYNC {
|
||||
go gui.refreshRemotes()
|
||||
} else {
|
||||
gui.refreshRemotes()
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
gui.refreshStatus()
|
||||
|
||||
if options.then != nil {
|
||||
options.then()
|
||||
}
|
||||
}
|
||||
return gui.switchFocus(g, v, previousView)
|
||||
}
|
||||
|
||||
func (gui *Gui) goToSideView(sideViewName string) func(g *gocui.Gui, v *gocui.View) error {
|
||||
return func(g *gocui.Gui, v *gocui.View) error {
|
||||
view, err := g.View(sideViewName)
|
||||
if err != nil {
|
||||
gui.Log.Error(err)
|
||||
if options.mode == BLOCK_UI {
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
f()
|
||||
return nil
|
||||
}
|
||||
err = gui.closePopupPanels()
|
||||
if err != nil {
|
||||
gui.Log.Error(err)
|
||||
return nil
|
||||
}
|
||||
return gui.switchFocus(g, nil, view)
|
||||
})
|
||||
} else {
|
||||
f()
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) closePopupPanels() error {
|
||||
gui.onNewPopupPanel()
|
||||
err := gui.closeConfirmationPrompt(gui.g, true)
|
||||
if err != nil {
|
||||
gui.Log.Error(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// pass in oldView = nil if you don't want to be able to return to your old view
|
||||
// TODO: move some of this logic into our onFocusLost and onFocus hooks
|
||||
func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error {
|
||||
// we assume we'll never want to return focus to a popup panel i.e.
|
||||
// we should never stack popup panels
|
||||
if oldView != nil && !gui.isPopupPanel(oldView.Name()) {
|
||||
gui.State.PreviousView = oldView.Name()
|
||||
}
|
||||
|
||||
gui.Log.Info("setting highlight to true for view" + newView.Name())
|
||||
message := gui.Tr.TemplateLocalize(
|
||||
"newFocusedViewIs",
|
||||
Teml{
|
||||
"newFocusedView": newView.Name(),
|
||||
},
|
||||
)
|
||||
gui.Log.Info(message)
|
||||
if _, err := g.SetCurrentView(newView.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := g.SetViewOnTop(newView.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
g.Cursor = newView.Editable
|
||||
|
||||
if err := gui.renderPanelOptions(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.newLineFocused(g, newView)
|
||||
}
|
||||
|
||||
func (gui *Gui) resetOrigin(v *gocui.View) error {
|
||||
_ = v.SetCursor(0, 0)
|
||||
return v.SetOrigin(0, 0)
|
||||
@@ -223,29 +149,33 @@ func (gui *Gui) cleanString(s string) string {
|
||||
return utils.NormalizeLinefeeds(output)
|
||||
}
|
||||
|
||||
func (gui *Gui) setViewContent(g *gocui.Gui, v *gocui.View, s string) {
|
||||
func (gui *Gui) setViewContent(v *gocui.View, s string) {
|
||||
v.Clear()
|
||||
fmt.Fprint(v, gui.cleanString(s))
|
||||
}
|
||||
|
||||
// renderString resets the origin of a view and sets its content
|
||||
func (gui *Gui) renderString(g *gocui.Gui, viewName, s string) {
|
||||
g.Update(func(*gocui.Gui) error {
|
||||
v, err := g.View(viewName)
|
||||
if err != nil {
|
||||
return nil // return gracefully if view has been deleted
|
||||
}
|
||||
if err := v.SetOrigin(0, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := v.SetCursor(0, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
gui.setViewContent(gui.g, v, s)
|
||||
return nil
|
||||
func (gui *Gui) renderString(viewName, s string) {
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
return gui.renderStringSync(viewName, s)
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) renderStringSync(viewName, s string) error {
|
||||
v, err := gui.g.View(viewName)
|
||||
if err != nil {
|
||||
return nil // return gracefully if view has been deleted
|
||||
}
|
||||
if err := v.SetOrigin(0, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := v.SetCursor(0, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
gui.setViewContent(v, s)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) optionsMapToString(optionsMap map[string]string) string {
|
||||
optionsArray := make([]string, 0)
|
||||
for key, description := range optionsMap {
|
||||
@@ -255,9 +185,8 @@ func (gui *Gui) optionsMapToString(optionsMap map[string]string) string {
|
||||
return strings.Join(optionsArray, ", ")
|
||||
}
|
||||
|
||||
func (gui *Gui) renderOptionsMap(optionsMap map[string]string) error {
|
||||
gui.renderString(gui.g, "options", gui.optionsMapToString(optionsMap))
|
||||
return nil
|
||||
func (gui *Gui) renderOptionsMap(optionsMap map[string]string) {
|
||||
gui.renderString("options", gui.optionsMapToString(optionsMap))
|
||||
}
|
||||
|
||||
// TODO: refactor properly
|
||||
@@ -312,56 +241,79 @@ func (gui *Gui) getSearchView() *gocui.View {
|
||||
return v
|
||||
}
|
||||
|
||||
func (gui *Gui) getStatusView() *gocui.View {
|
||||
v, _ := gui.g.View("status")
|
||||
return v
|
||||
}
|
||||
|
||||
func (gui *Gui) getConfirmationView() *gocui.View {
|
||||
v, _ := gui.g.View("confirmation")
|
||||
return v
|
||||
}
|
||||
|
||||
func (gui *Gui) trimmedContent(v *gocui.View) string {
|
||||
return strings.TrimSpace(v.Buffer())
|
||||
}
|
||||
|
||||
func (gui *Gui) currentViewName() string {
|
||||
currentView := gui.g.CurrentView()
|
||||
if currentView == nil {
|
||||
return ""
|
||||
}
|
||||
return currentView.Name()
|
||||
}
|
||||
|
||||
func (gui *Gui) resizeCurrentPopupPanel(g *gocui.Gui) error {
|
||||
v := g.CurrentView()
|
||||
func (gui *Gui) resizeCurrentPopupPanel() error {
|
||||
v := gui.g.CurrentView()
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
if gui.isPopupPanel(v.Name()) {
|
||||
return gui.resizePopupPanel(g, v)
|
||||
return gui.resizePopupPanel(v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) resizePopupPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) resizePopupPanel(v *gocui.View) error {
|
||||
// If the confirmation panel is already displayed, just resize the width,
|
||||
// otherwise continue
|
||||
content := v.Buffer()
|
||||
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, v.Wrap, content)
|
||||
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(v.Wrap, content)
|
||||
vx0, vy0, vx1, vy1 := v.Dimensions()
|
||||
if vx0 == x0 && vy0 == y0 && vx1 == x1 && vy1 == y1 {
|
||||
return nil
|
||||
}
|
||||
gui.Log.Info(gui.Tr.SLocalize("resizingPopupPanel"))
|
||||
_, err := g.SetView(v.Name(), x0, y0, x1, y1, 0)
|
||||
_, err := gui.g.SetView(v.Name(), x0, y0, x1, y1, 0)
|
||||
return err
|
||||
}
|
||||
|
||||
func (gui *Gui) changeSelectedLine(line *int, total int, change int) {
|
||||
func (gui *Gui) changeSelectedLine(panelState IListPanelState, total int, change int) {
|
||||
// TODO: find out why we're doing this
|
||||
if *line == -1 {
|
||||
line := panelState.GetSelectedLineIdx()
|
||||
|
||||
if line == -1 {
|
||||
return
|
||||
}
|
||||
if *line+change < 0 {
|
||||
*line = 0
|
||||
} else if *line+change >= total {
|
||||
*line = total - 1
|
||||
var newLine int
|
||||
if line+change < 0 {
|
||||
newLine = 0
|
||||
} else if line+change >= total {
|
||||
newLine = total - 1
|
||||
} else {
|
||||
*line += change
|
||||
newLine = line + change
|
||||
}
|
||||
|
||||
panelState.SetSelectedLineIdx(newLine)
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshSelectedLine(line *int, total int) {
|
||||
if *line == -1 && total > 0 {
|
||||
*line = 0
|
||||
} else if total-1 < *line {
|
||||
*line = total - 1
|
||||
func (gui *Gui) refreshSelectedLine(panelState IListPanelState, total int) {
|
||||
line := panelState.GetSelectedLineIdx()
|
||||
|
||||
if line == -1 && total > 0 {
|
||||
panelState.SetSelectedLineIdx(0)
|
||||
} else if total-1 < line {
|
||||
panelState.SetSelectedLineIdx(total - 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,17 +326,15 @@ func (gui *Gui) renderDisplayStrings(v *gocui.View, displayStrings [][]string) {
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) renderPanelOptions() error {
|
||||
currentView := gui.g.CurrentView()
|
||||
switch currentView.Name() {
|
||||
case "menu":
|
||||
return gui.renderMenuOptions()
|
||||
case "main":
|
||||
if gui.State.MainContext == "merging" {
|
||||
return gui.renderMergeOptions()
|
||||
}
|
||||
func (gui *Gui) globalOptionsMap() map[string]string {
|
||||
return map[string]string{
|
||||
fmt.Sprintf("%s/%s", gui.getKeyDisplay("universal.scrollUpMain"), gui.getKeyDisplay("universal.scrollDownMain")): gui.Tr.SLocalize("scroll"),
|
||||
fmt.Sprintf("%s %s %s %s", gui.getKeyDisplay("universal.prevBlock"), gui.getKeyDisplay("universal.nextBlock"), gui.getKeyDisplay("universal.prevItem"), gui.getKeyDisplay("universal.nextItem")): gui.Tr.SLocalize("navigate"),
|
||||
gui.getKeyDisplay("universal.return"): gui.Tr.SLocalize("cancel"),
|
||||
gui.getKeyDisplay("universal.quit"): gui.Tr.SLocalize("quit"),
|
||||
gui.getKeyDisplay("universal.optionMenu"): gui.Tr.SLocalize("menu"),
|
||||
"1-5": gui.Tr.SLocalize("jump"),
|
||||
}
|
||||
return gui.renderGlobalOptions()
|
||||
}
|
||||
|
||||
func (gui *Gui) isPopupPanel(viewName string) bool {
|
||||
@@ -395,30 +345,6 @@ func (gui *Gui) popupPanelFocused() bool {
|
||||
return gui.isPopupPanel(gui.currentViewName())
|
||||
}
|
||||
|
||||
func (gui *Gui) handleClick(v *gocui.View, itemCount int, selectedLine *int, handleSelect func(*gocui.Gui, *gocui.View) error) error {
|
||||
if gui.popupPanelFocused() && v != nil && !gui.isPopupPanel(v.Name()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newSelectedLine := v.SelectedLineIdx()
|
||||
|
||||
if newSelectedLine < 0 {
|
||||
newSelectedLine = 0
|
||||
}
|
||||
|
||||
if newSelectedLine > itemCount-1 {
|
||||
newSelectedLine = itemCount - 1
|
||||
}
|
||||
|
||||
*selectedLine = newSelectedLine
|
||||
|
||||
return handleSelect(gui.g, v)
|
||||
}
|
||||
|
||||
// often gocui wants functions in the form `func(g *gocui.Gui, v *gocui.View) error`
|
||||
// but sometimes we just have a function that returns an error, so this is a
|
||||
// convenience wrapper to give gocui what it wants.
|
||||
@@ -427,3 +353,34 @@ func (gui *Gui) wrappedHandler(f func() error) func(g *gocui.Gui, v *gocui.View)
|
||||
return f()
|
||||
}
|
||||
}
|
||||
|
||||
// secondaryViewFocused tells us whether it appears that the secondary view is focused. The view is actually never focused for real: we just swap the main and secondary views and then you're still focused on the main view so that we can give you access to all its keybindings for free. I will probably regret this design decision soon enough.
|
||||
func (gui *Gui) secondaryViewFocused() bool {
|
||||
return gui.State.Panels.LineByLine != nil && gui.State.Panels.LineByLine.SecondaryFocused
|
||||
}
|
||||
|
||||
func (gui *Gui) clearEditorView(v *gocui.View) {
|
||||
v.Clear()
|
||||
_ = v.SetCursor(0, 0)
|
||||
_ = v.SetOrigin(0, 0)
|
||||
}
|
||||
|
||||
func (gui *Gui) onViewTabClick(viewName string, tabIndex int) error {
|
||||
context := gui.ViewTabContextMap[viewName][tabIndex].contexts[0]
|
||||
|
||||
return gui.switchContext(context)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleNextTab(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.onViewTabClick(
|
||||
v.Name(),
|
||||
utils.ModuloWithWrap(v.TabIndex+1, len(v.Tabs)),
|
||||
)
|
||||
}
|
||||
|
||||
func (gui *Gui) handlePrevTab(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.onViewTabClick(
|
||||
v.Name(),
|
||||
utils.ModuloWithWrap(v.TabIndex-1, len(v.Tabs)),
|
||||
)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user